<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="/feeds/atom-style.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://eryi.dev/en/</id>
    <title>ERYI</title>
    <updated>2026-04-08T09:53:25.422Z</updated>
    <generator>Astro-Theme-Retypeset with Feed for Node.js</generator>
    <author>
        <name>eryi</name>
        <uri>https://eryi.dev/</uri>
    </author>
    <link rel="alternate" href="https://eryi.dev/en/"/>
    <link rel="self" href="https://eryi.dev/en/atom.xml"/>
    <subtitle>Spiraling Upward</subtitle>
    <rights>Copyright © 2026 eryi</rights>
    <entry>
        <title type="html"><![CDATA[Service Rate Limiting in Practice]]></title>
        <id>https://eryi.dev/en/posts/service-rate-limiting-practice/</id>
        <link href="https://eryi.dev/en/posts/service-rate-limiting-practice/"/>
        <updated>2025-11-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[A practical design note on user-facing rate limiting, downstream circuit breaking, and asynchronous write-back for an LLM-powered comment bot.]]></summary>
        <content type="html"><![CDATA[<p><em>This post was translated by LLM.</em></p>
<p>Imagine a scenario like this: I integrated a comment bot into my UGC platform, and users can trigger replies in other users' comment sections through <code>@robot</code>. Because the bot relies on third-party APIs such as OpenRouter to generate personalized LLM replies, rate limiting becomes a core concern.</p>
<h2>Bottlenecks</h2>
<p><img src="https://img.eryi.me/astro-blog/2026/04/aba31bfac98ad57239deef625b80e7f7.png" alt="image.png" /></p>
<p><strong>Rate limiting for the AI service</strong></p>
<ul>
<li>The first bottleneck is at the user-request layer. We cannot let users call the comment bot service without limits.
<ul>
<li>
<ol>
<li>At the product level, we can cap how many times a user may call the service within a unit of time, for example by using Redis to record that calls per minute must stay below 5.</li>
</ol>
</li>
<li>
<ol>
<li>At the technical level, even if the product layer already enforces limits, the user base may still be so large that the service faces heavy concurrent demand. In that case we can rely on MQ or a token bucket to control throughput.</li>
</ol>
</li>
</ul>
</li>
<li>The second bottleneck is the traffic limit imposed by the LLM provider.
<ul>
<li>Providers such as OpenRouter enforce RPM and TPM limits per user, which is an external constraint.</li>
<li>We can use MQ plus a token bucket for traffic shaping, along with a circuit breaker strategy.</li>
</ul>
</li>
<li>The third bottleneck appears after the LLM returns a response to us. If write traffic back into the comment service is too high, a single thread may not keep up. We can therefore use a thread pool for parallel writes, and also use MQ to smooth burst traffic.</li>
</ul>
<h2>Rate Limiting and Circuit Breaking</h2>
<p>First, it helps to separate these two concepts clearly.</p>
<p><strong>Rate limiting</strong> means restricting request frequency or the execution frequency of certain internal functions so that sudden traffic spikes do not make the whole system unavailable. Common rate limiting algorithms include the sliding window, leaky bucket, and token bucket.</p>
<p><strong>Circuit breaking</strong> means that when calling an external service, database, or microservice, if failures, timeouts, or very slow responses happen continuously, the breaker opens the circuit and temporarily stops calling that dependency. While the breaker is open, the system immediately returns an error or a degraded result instead of sending more requests. After a cooling-off period, it lets a small number of requests through as probes. If the dependency has recovered, the breaker closes automatically.</p>
<h3>Circuit Breaker Properties</h3>
<ul>
<li>Slow-call circuit breaking</li>
<li>Exception-ratio circuit breaking</li>
<li>Half-open probing</li>
</ul>
<p>For the three bottlenecks mentioned above, we mainly use rate limiting for the user-request layer, circuit breaking for calls to downstream LLM services, and asynchronous MQ-based buffering for writing back to the database because that part is internal I/O.</p>
<h2>In Practice</h2>
<h3>Plan</h3>
<p>The first part is split into business-level and technical rate limiting.</p>
<ul>
<li>Business rate limiting:
<ul>
<li>Limit each user to no more than 5 requests per minute</li>
<li>Limit each user to no more than 20 requests per day
<ul>
<li>key design</li>
</ul>
</li>
<li>Each request consumes one coin, and requests are rejected if the user does not have enough balance</li>
</ul>
</li>
<li>Technical rate limiting:
<ul>
<li>Use a token bucket to cap overall QPS at 100</li>
</ul>
</li>
</ul>
<p>The second part is calling the external LLM service, where the focus is on circuit breaker design.</p>
<ul>
<li>Requirements
<ul>
<li>Trip the breaker if latency exceeds 60 seconds</li>
<li>Trip the breaker if the exception ratio exceeds 50%</li>
<li>After the breaker timeout passes, allow a real user request through as a probe, similar to granting a token, before sending traffic downstream again</li>
</ul>
</li>
</ul>
<p>The third part is writing back to the DB.</p>
<ul>
<li>Introducing Kafka is enough</li>
</ul>
<h3>Middleware Choice</h3>
<ul>
<li>We could implement the rate limiting and circuit breaking logic ourselves, but mature middleware already exists for these needs.</li>
<li>Both Resilience4j and <a href="https://github.com/alibaba/sentinel">alibaba/Sentinel</a> provide the required capabilities, and in the end I chose Sentinel.</li>
<li>Reasons
<ul>
<li>Sentinel comes with a native console and supports visual tuning.</li>
<li>For traffic shaping in LLM-like scenarios, Sentinel supports Pacing, which can send sudden bursts downstream at a steady rate.</li>
<li>Sentinel also supports a token-bucket mode, which matches our traffic-control requirements well.</li>
</ul>
</li>
</ul>
]]></content>
        <author>
            <name>eryi</name>
            <uri>https://eryi.dev/</uri>
        </author>
        <published>2025-11-08T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[BILIBILI Like System Architecture]]></title>
        <id>https://eryi.dev/en/posts/bilibililike/</id>
        <link href="https://eryi.dev/en/posts/bilibililike/"/>
        <updated>2025-10-01T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[A quick note on BILIBILI's like system under write-heavy traffic, hot-key pressure, and disaster recovery constraints.]]></summary>
        <content type="html"><![CDATA[<p><em>This post was translated by LLM.</em></p>
<h2>System Pressure</h2>
<h3>Traffic Pressure</h3>
<h5>Global Traffic Pressure</h5>
<ul>
<li>Write traffic
<ul>
<li>Aggregate writes. For example, batch like counts within a 10-second window and flush them in one shot to reduce I/O.</li>
<li>Make DB writes asynchronous through MQ or similar infrastructure.</li>
</ul>
</li>
<li>Before every like-state update, check the previous like state first, because unliking requires an existing like, and liking requires the opposite.</li>
</ul>
<h5>Per-Item Traffic Pressure</h5>
<ul>
<li>Viral content events
<ul>
<li>Hotspots can appear in both the DB and cache. The system needs a hot-key detection mechanism so hot data can be cached locally with a reasonable TTL.</li>
</ul>
</li>
</ul>
<h3>Storage Pressure</h3>
<ul>
<li>Store data in a KV-oriented form.</li>
</ul>
<h3>Disaster Recovery Pressure</h3>
<ul>
<li>DB outage</li>
<li>Redis cluster instability</li>
<li>Datacenter outage</li>
<li>Network failure</li>
</ul>
<h2>System Architecture</h2>
<p><img src="https://i0.hdslb.com/bfs/article/758b2b4bef2f3dd719ef82ccf3bf077f9331d7e4.png" alt="img" /></p>
<h3>Three-Tier Storage Layer</h3>
<h4>DB - TiDB</h4>
<ul>
<li>TiDB is distributed, so there is no need for manual sharding.</li>
<li>Like record table</li>
<li>Like count table</li>
</ul>
<h4>Cache</h4>
<ul>
<li>Use the cache-aside pattern.</li>
<li>
<pre><code>key-value = user:likes:patten:{mid}:{business_id} - member(messageID)-score(likeTimestamp)
</code></pre>
</li>
<li>Use a <code>zset</code> directly, maintain a maximum length, and evict the earliest liked messages.</li>
</ul>
<h4>Local Cache</h4>
<ul>
<li>Handle cache hotspots.</li>
<li>Use a min-heap algorithm to count the most frequently accessed cache keys within a configurable time window, then keep hot keys and values in local memory with a business-acceptable TTL.</li>
</ul>
<h4>Data Migration and Archiving</h4>
<ul>
<li>Migrate data from TiDB to a KV database (Taishan) to reduce cost.</li>
</ul>
<h3>Like Service Layer</h3>
<h4>Storage Disaster Recovery (DB, Redis)</h4>
<ul>
<li>Two datacenters serve as disaster-recovery peers.
<ul>
<li>Datacenter A handles all writes plus part of the reads.</li>
<li>Datacenter B handles part of the reads.</li>
</ul>
</li>
<li>If the DB fails, switch read/write traffic to the standby datacenter through a <code>db-proxy</code> sidecar.</li>
<li>Cross-datacenter cache consistency is maintained by asynchronously consuming TiDB binlogs. When needed, traffic can be switched between datacenters without causing a large amount of cold data to fall back to the database. Cold data here means data missing from Redis and therefore requiring a DB lookup.</li>
</ul>
<h4>Service Disaster Recovery</h4>
<ul>
<li>Multiple storage layers back each other up.</li>
<li><code>redis -&gt; kv -&gt; DB</code></li>
<li>Retry like operations indefinitely at every layer.</li>
</ul>
<h4>Async Task Layer</h4>
<ul>
<li>Write like data, refresh cache, and send like and like-count messages to downstream services such as the recommendation system.</li>
<li>Disaster recovery for binlog interruption
<ul>
<li>Monitor the binlog stream first.</li>
<li>Let the business service emit standby messages ahead of time. When the job detects a binlog anomaly, it automatically switches to the fallback consumer stream.</li>
</ul>
</li>
</ul>
<p>TODO:</p>
<ul>
<li>Cache strategy, plus updates for my own like and favorite operations.</li>
</ul>
<h2>Reference</h2>
<ul>
<li><a href="https://www.bilibili.com/read/cv21576373/?opus_fallback=1">"Click the Like Button": Bilibili's Architecture Design for a Hundred-Billion-Scale Like System</a></li>
</ul>
]]></content>
        <author>
            <name>eryi</name>
            <uri>https://eryi.dev/</uri>
        </author>
        <published>2025-04-22T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Spring Servlet Recap]]></title>
        <id>https://eryi.dev/en/posts/spring-servlet-recap/</id>
        <link href="https://eryi.dev/en/posts/spring-servlet-recap/"/>
        <updated>2024-12-14T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Java Servlets evolved as an efficient, standardized way to handle web requests in Java, replacing earlier CGI solutions and serving as the foundation for Spring MVC's architecture through the DispatcherServlet.]]></summary>
        <content type="html"><![CDATA[<p><em>This post was translated by LLM.</em></p>
<h2>Servlet</h2>
<p>A servlet is a Java class that handles HTTP requests and generate responses.
<img src="https://img.eryi.me/image-20241213223146486.png" alt="" /></p>
<ul>
<li>The servlet container converts HTTP requests into a <code>HttpServletRequest</code> object and prepare <code>HttpServletResponse</code> object.</li>
<li>And these objects are delivered to a web component. Which can interact with beans or database to generate dynamic content.</li>
<li>The web component can fill the <code>HttpServletResponse</code> object or pass it to another component to fill it.</li>
<li>The servlet container ultimately converts the <code>HttpServletResponse</code> object into HTTP response and the web server returns it to the client.</li>
</ul>
<h3>code</h3>
<pre><code>package jakarta.servlet;

import java.io.IOException;

public interface Servlet {
    void init(ServletConfig var1) throws ServletException;

    ServletConfig getServletConfig();

    void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;

    String getServletInfo();

    void destroy();
}

// service in Httpservlet
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        HttpServletRequest request;
        HttpServletResponse response;
        try {
            request = (HttpServletRequest)req;
            response = (HttpServletResponse)res;
        } catch (ClassCastException var6) {
            throw new ServletException(lStrings.getString("http.non_http"));
        }

        this.service(request, response);
    }
</code></pre>
<h3>lifecycle</h3>
<ul>
<li>an instance of servlet does not exist, the servlet container:
<ul>
<li>load the servlet class</li>
<li>create an instance of servlet class</li>
<li><code>init()</code>: Called only once at start</li>
<li><code>service()</code>: Called for every request</li>
<li><code>doGet()/doPost()</code>: Called by service() based on HTTP method</li>
<li><code>destroy()</code>: Called once at end</li>
</ul>
</li>
</ul>
<h3>Why we need servlet</h3>
<p>At first, the HTTP server can only provide static content, like plain HTML files. Server-side extensions were developed to enable dynamic content generation. This means pages could be translated based on user input, database, etc.</p>
<p>Early server-side extensions came in different forms:</p>
<ul>
<li>CGI: An open standard that any server could implement</li>
<li>Proprietary APIs like NSAPI (Netscape) and ISAPI (Microsoft), which were specific to their web servers</li>
</ul>
<h4>CGI(Common Gateway Interface)</h4>
<p>CGI (Common Gateway Interface) is a standardized protocol that defines how web servers communicate with external applications or scripts. These CGI programs can be written in any programming language (like C, C++, Perl, Python, etc.) and are responsible for processing requests and generating dynamic content.</p>
<p><img src="https://img.eryi.me/image-20241214143933871.png" alt="image-20241214143933871" /></p>
<p>We can see that the web server need to pass every request to the CGI applications to get the response from it. And the server has to create and destroy the process for every request.</p>
<blockquote>
<p>FastCGI: Instead of creating a new process for each request, FastCGI uses persistent processes to handle a series of requests. These processes are owned by the FastCGI server, not the web server.</p>
</blockquote>
<p>Later, Java Servlet was developed as part of Jakarta EE to provide a standardized, vendor-neutral API for creating dynamic web applications in Java.</p>
<h2>DispatcherServlet</h2>
<p>DispatherServlet is a specialized Sevlet that extends HttpServlet and acts as a Front Controller in Spring MVC applications. It intercepts incoming HTTP requests and dispatches them to appropriate controller methods for processing. <em>It inherits from FrameworkServlet which inherits from HttpServletBean which inherits from HttpServlet.</em></p>
<h3>process of spring MVC</h3>
<h4>request processing flow</h4>
<pre><code>HTTP request
	-&gt; Filter Chain
	-&gt; DispatcherServlet
			- DispatcherServlet consults HandlerMapping to find the right controller
	-&gt; HandlerMapping
	    - HandlerMapping based on url to find specific handler(controller)
	-&gt; HandlerExecutionChain
	-&gt; HandlerAdapter
		  - DispatcherServlet calls HandlerAdapter to execute controller method and returns ModelAndView
	-&gt; Controller
			- return ModelAndView
	-&gt; ViewResolver
	    - ViewResolver translates view name to actual View
	-&gt; View
	    - View renders the response
	-&gt; Response
</code></pre>
<h2>Spring filter</h2>
<p>Custom filter implementing the Filter interface</p>
<pre><code>public interface Filter {
    default void init(FilterConfig filterConfig) throws ServletException {
    }

    void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;

    default void destroy() {
    }
}

</code></pre>
<p>A HTTP request first arrives at filters and then the DispatcherServlet. Multiple filters can be chained together (FilterChain), and we could use @Order to regulate the filter order.</p>
<h2>Q&amp;A</h2>
<ol>
<li>Why servlet is not thread-safe?
<ul>
<li>The servlet container creates only one instance of each Servlet.</li>
<li>But multiple client requests creates multiple threads that access this single instance simultaneously</li>
<li>All threads share the Servlet's instance variables and class variables</li>
</ul>
</li>
</ol>
<h2>Reference</h2>
<p><a href="https://www.geeksforgeeks.org/introduction-java-servlets/">Introduction to Java Servlets - GeeksforGeeks</a></p>
]]></content>
        <author>
            <name>eryi</name>
            <uri>https://eryi.dev/</uri>
        </author>
        <published>2024-12-14T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Explaining cookies, sessions and tokens in authentication]]></title>
        <id>https://eryi.dev/en/posts/cookies-session-token/</id>
        <link href="https://eryi.dev/en/posts/cookies-session-token/"/>
        <updated>2024-10-30T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[A quick refresher on how cookies keep state, why servers maintain sessions, and where stateless tokens such as JWT fit in.]]></summary>
        <content type="html"><![CDATA[<p><em>This post was translated by LLM.</em>
In web authentication we frequently encounter the three concepts <em>cookie</em>, <em>session</em>, and <em>token</em>. They form the basis of an application’s identity system, so it is important to understand how they work together.</p>
<h2>Cookie</h2>
<p><a href="https://datatracker.ietf.org/doc/html/rfc6265">RFC 6265</a> explains HTTP state management clearly:</p>
<blockquote>
<p>This document defines the HTTP Cookie and Set-Cookie header fields. These header fields can be used by HTTP servers to store state (called cookies) at HTTP user agents, letting the servers maintain a stateful session over the mostly stateless HTTP protocol.
To store state, the origin server includes a Set-Cookie header in an HTTP response. In subsequent requests, the user agent returns a Cookie request header to the origin server. The Cookie header contains cookies the user agent received in previous Set-Cookie headers. The origin server is free to ignore the Cookie header or use its contents for an application-defined purpose.</p>
</blockquote>
<p>In other words, to perform session management on top of what used to be a stateless protocol (pre–HTTP/1.0), the server stores <strong>state information</strong> on the client in the form of cookies. The flow looks like this:</p>
<p><img src="https://img.eryi.me/Pasted%20image%2020241030143916.png" alt="cs-cookie" /></p>
<p>Cookies are just HTTP headers. They do not have to contain only server-issued data; you could also keep preferences such as language. Because every HTTP request automatically carries cookies, they are perfect for storing a session ID (state) used in session management.</p>
<h2>Why does this enable session management?</h2>
<p>HTTP itself is stateless, meaning each request is independent. By storing something like a session ID on the client, the browser automatically sends it on each request, allowing the server to distinguish between users. The server keeps a session store keyed by the session ID and records details such as login status or cart items. When the same user comes back, the browser includes the session ID, the server looks it up, and it can respond with the appropriate state.</p>
<h2>Session</h2>
<p>We already touched on sessions when discussing cookies. OWASP defines a session as follows in its <a href="https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#introduction">Session Management Cheat Sheet</a>:</p>
<blockquote>
<p>A web session is a sequence of network HTTP request and response transactions associated with the same user.</p>
</blockquote>
<p>So a session consists of all requests/responses tied to a user. On the server side we must manage the entire lifecycle (creation, storage, validation, destruction). Using cookies to store a session ID for user identification is known as <strong>cookie-based authentication</strong>—the session ID acts as the temporary identifier.</p>
<h3>Drawbacks</h3>
<p>Because cookies carry the session ID, there is always a risk of leakage even if we set <code>HttpOnly</code>, <code>Secure</code>, and <code>SameSite</code>. In distributed systems we also have to synchronize session data, e.g., by storing it in Redis.</p>
<h2>JSON Web Token (JWT)</h2>
<p><a href="https://datatracker.ietf.org/doc/html/rfc7519#section-3">RFC 7519</a> defines JWT as:</p>
<blockquote>
<p>JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted.</p>
</blockquote>
<p>This tells us two things: 1) the content is JSON when created; 2) JWT supports two protection modes, JWS and JWE. A JWT on the wire looks like <code>Header.Payload.Signature</code>, i.e., three parts separated by dots. Here is how a JWT is translated.</p>
<h2>Creating a JWT</h2>
<ul>
<li><strong>Create</strong> the JOSE Header (JSON Object Signing and Encryption Header) to describe metadata, and base64-encode it:</li>
</ul>
<pre><code>{
  "alg": "HS256",
  "typ": "JWT"
}
</code></pre>
<ul>
<li><strong>Create</strong> the payload that contains the claims, then base64-encode it:</li>
</ul>
<pre><code>{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}
</code></pre>
<ul>
<li><strong>Generate</strong> the signature
<ul>
<li>Concatenate the encoded header and payload: <code>base64UrlEncode(header) + "." + base64UrlEncode(payload)</code></li>
<li>Sign with the chosen algorithm: <code>HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)</code>
<ul>
<li>For example, NextAuth asks you to set <code>NEXTAUTH_SECRET</code> in the environment. This is a symmetric secret: the same key signs and verifies the token. Other algorithms such as RSA use an asymmetric key pair (private key for signing, public key for verification).</li>
</ul>
</li>
<li>Base64-encode the result to obtain the signature part.</li>
</ul>
</li>
<li><strong>Assemble</strong> the JWT as <code>Header.Payload.Signature</code>.</li>
</ul>
<p>You might have noticed that the steps already include signing—this is the JWS variant of JWT. In practice almost every library (Node’s <code>jsonwebtoken</code>, Java’s <code>jjwt</code>, etc.) produces JWS tokens because JWS already meets common security requirements. Most applications only need to verify authenticity and integrity, not confidentiality, so JWE is rarely used.</p>
<p>At first glance JWT and sessions both seem to rely on encoded strings. What is the difference?</p>
<h2>Comparing JWT and Session</h2>
<h2>Storage location</h2>
<p>Sessions are created on the server and stored in memory or a database. Each request triggers a lookup, and any change on the server is immediately reflected in the stored session.</p>
<p>A JWT carries its own data. Each request simply sends the token, and the server verifies the signature without a database lookup.</p>
<h2>Lifecycle</h2>
<p>Session lifecycles are managed on the server. When a session expires, the server deletes the corresponding data.</p>
<p>For JWTs, the lifetime is defined when the token is created, typically via the <code>exp</code> field in the payload. Once issued, the expiration cannot be changed; you must issue a new token to extend it.</p>
<h2>Security</h2>
<p>Sessions themselves do not have signatures and the session ID might leak.</p>
<p>JWTs include a signature so the server can verify integrity before trusting the contents.</p>
<h2>Summary</h2>
<p>Cookies, sessions, and JWTs are all fundamental to web authentication. In real projects we choose the appropriate mechanism based on the scenario. Modern frameworks provide mature libraries, but understanding the concepts behind them is still immensely valuable.</p>
]]></content>
        <author>
            <name>eryi</name>
            <uri>https://eryi.dev/</uri>
        </author>
        <published>2024-10-30T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Troubleshooting SSH prompts triggered by the Obsidian Git plugin]]></title>
        <id>https://eryi.dev/en/posts/obsidian-ssh-issues/</id>
        <link href="https://eryi.dev/en/posts/obsidian-ssh-issues/"/>
        <updated>2024-08-12T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Document the root cause of repeated passphrase prompts when Obsidian's Git plugin pushes via SSH, recap how the SSH handshake works, and configure ssh-agent so keys are unlocked automatically.]]></summary>
        <content type="html"><![CDATA[<p><em>This post was translated by LLM.</em></p>
<h2>Where the problem came from</h2>
<ul>
<li>After configuring the Obsidian Git plugin, every upload reported <code>permission denied</code>. I remembered that the CLI always asked me to “enter passphrase for key id_rsa” whenever I ran <code>git push</code>, so it was clear that the plugin failed because it could not input the passphrase while executing the script.</li>
</ul>
<h2>Digging deeper</h2>
<ul>
<li>What exactly is SSH? What are the public key and private key we translated?</li>
<li>Why does <code>git push</code> trigger the passphrase prompt, and what is the passphrase protecting?</li>
</ul>
<h2>What is SSH</h2>
<blockquote>
<p>Secure Shell (SSH) is a protocol for sending commands securely over an untrusted network. It uses cryptography both to authenticate the devices involved and to encrypt the data that flows between them. SSH also supports <a href="https://www.cloudflare.com/learning/network-layer/what-is-tunneling/">tunneling</a> or port forwarding so that <a href="https://www.cloudflare.com/learning/network-layer/what-is-a-packet/">packets</a> can traverse networks that would otherwise block them. It is commonly used to control servers remotely, manage infrastructure, and transfer files. — <a href="https://www.cloudflare.com/zh-cn/learning/access-management/what-is-ssh/">Cloudflare</a></p>
</blockquote>
<h3>How SSH works</h3>
<h4>Characteristics</h4>
<ul>
<li>Runs on top of the TCP/IP suite.</li>
<li>Uses <a href="https://www.cloudflare.com/learning/ssl/how-does-public-key-encryption-work/">public-key cryptography</a>.</li>
</ul>
<h4>Workflow</h4>
<ol>
<li><strong>Version negotiation</strong>
<ol>
<li>The server listens on port 22 and waits for clients.</li>
<li>The client initiates a TCP connection.</li>
<li>Both sides exchange protocol versions.</li>
</ol>
</li>
<li><strong>Key and algorithm negotiation</strong>
<ol>
<li>Client and server send algorithm negotiation packets to each other to agree on the algorithms that will be used.</li>
<li>The server sends its host public key to the client, creates a session ID (call it <code>id</code>), and sends it over.</li>
<li>The client generates a session key <code>key</code>, computes <code>res = id XOR key</code>, and encrypts <code>res</code> with the server's public key before sending it back.</li>
<li>The server decrypts the packet with its private key to obtain <code>res</code>.</li>
<li>The server computes <code>res XOR id</code> to recover the session key. Now both parties share the same session key and ID, and all subsequent data is encrypted with that session key.</li>
</ol>
</li>
<li><strong>Authentication</strong>
<ol>
<li>The SSH client attempts authentication methods in the order <code>publickey, gssapi-keyex, gssapi-with-mic, password</code>. <code>publickey</code> uses key pairs; <code>password</code> is the traditional credential-based method.</li>
<li><strong>publickey</strong>
a. The client runs <code>ssh-keygen</code> to create <code>id_rsa.pub</code> (public key) and <code>id_rsa</code> (private key), then sends the public key to the server and stores it under the server's <code>.ssh</code> directory.<br />
b. The client encrypts the username, authentication method, and public key with the session key and sends the result to the server.<br />
c. The server decrypts the packet using the session key, checks whether the <code>.ssh</code> directory contains the corresponding public key, and, if it finds a match, encrypts a random challenge with that public key and then again with the session key.<br />
d. The client decrypts twice (session key + private key), re-encrypts the challenge with the session key, and sends it back.<br />
e. The server decrypts with the session key, compares the challenge with the one it translated, and accepts or rejects accordingly.</li>
<li><strong>password</strong>
a. The client encrypts the username, authentication method, and password with the session key and sends the packet.<br />
b. The server decrypts it, validates the credentials, and returns success or failure.</li>
</ol>
</li>
<li><strong>Session request</strong>
The client specifies the session type (start a shell, run a command, forward a port, etc.). This happens implicitly when we type the <code>ssh</code> command.</li>
<li><strong>Session interaction</strong>
Once the channel is set up, data starts flowing in both directions.</li>
</ol>
<h3>What is a passphrase? What does ssh-agent do?</h3>
<ul>
<li>
<p>The passphrase is the password that protects an SSH private key. You set it when running <code>ssh-keygen</code>.</p>
</li>
<li>
<p><code>ssh-agent</code> is a helper program that keeps decrypted private keys in memory so that you do not have to type the passphrase repeatedly within the same login session.</p>
<ul>
<li>Start the agent: <code>ssh-agent -s</code></li>
<li>Add the private key to the agent: <code>ssh-add --apple-use-keychain ~/.ssh/id_rsa</code>
<ul>
<li><code>--apple-use-keychain</code> stores the passphrase in the macOS keychain.</li>
</ul>
</li>
<li>Auto-start the agent and load the key by putting the commands into your shell profile (e.g., <code>.zshrc</code>):</li>
</ul>
<pre><code>ssh-agent -s
ssh-add --apple-use-keychain ~/.ssh/id_rsa
</code></pre>
<ul>
<li>Configure the SSH client (<code>~/.ssh/config</code>):</li>
</ul>
</li>
</ul>
<pre><code>Host *
    AddKeysToAgent yes
    UseKeychain yes
    IdentityFile ~/.ssh/id_rsa
</code></pre>
<h2>Solution</h2>
<ul>
<li>With all of the above in mind, the questions from the beginning are now easy to answer.</li>
<li>As long as we auto-start <code>ssh-agent</code> and preload the key via environment configuration, we no longer need to enter the passphrase manually each time. Alternatively, you could generate a key pair without a passphrase, but that is not recommended for security reasons.</li>
</ul>
]]></content>
        <author>
            <name>eryi</name>
            <uri>https://eryi.dev/</uri>
        </author>
        <published>2024-08-12T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[synchronized & volatile: reasoning from the singleton pattern]]></title>
        <id>https://eryi.dev/en/posts/singleton-synchronized-volatile/</id>
        <link href="https://eryi.dev/en/posts/singleton-synchronized-volatile/"/>
        <updated>2023-12-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Walk through several singleton implementations to explain why synchronized, double-checked locking and volatile matter, and how eager, static-inner-class and enum singletons compare.]]></summary>
        <content type="html"><![CDATA[<p><em>This post was translated by LLM.</em></p>
<h2>Implementing the Singleton Pattern</h2>
<p>The definition of a singleton says that a class must guarantee there is only <strong>one</strong> instance and must offer a global access point. In practice we usually distinguish between the <strong>lazy</strong> (create on first use) and the <strong>eager</strong> (create when the class is loaded) strategies.</p>
<p>Lazy vs. eager:</p>
<ol>
<li>Lazy: the global instance is created the first time it is needed.</li>
<li>Eager: the global instance is created while the class is being loaded.</li>
</ol>
<h3>Lazy singleton</h3>
<h4>Implementation 1</h4>
<p>Let's start with the most naïve version:</p>
<pre><code>// version1
class Singleton {
    private static Singleton instance;
    // Private constructor prevents external code from calling new
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }


}
public class Main{
    public static void main(String[] args) {
        // Singleton singleton = new Singleton();
        // Because the constructor is private, call the static method instead
        Singleton obj = Singleton.getInstance();
        System.out.println(obj.getInstance().toString());
    }
}
Output:
Singleton@30f39991
</code></pre>
<p><em>Logic</em><br />
Before every access we check whether <code>instance</code> is <code>null</code>; if so we create it, otherwise we just return the existing object.</p>
<p><em>Problem</em><br />
When multiple threads run to <code>if (instance == null)</code> at the same time, they all see <code>null</code> and each creates its own object, breaking the singleton guarantee.</p>
<p>To reproduce this we can use <code>CountDownLatch</code> to make two threads call <code>getInstance()</code> simultaneously:</p>
<pre><code>public class Main{
    public static void main(String[] args) throws InterruptedException {
        int numberOfThreads = 2;
        CountDownLatch latch = new CountDownLatch(numberOfThreads);
        Runnable runnable = () -&gt; {
            try {
                latch.countDown();
                latch.await(); // wait for the other thread
                Singleton instance = Singleton.getInstance();
                // print current thread and instance info
                System.out.println("Thread: " + Thread.currentThread().getName()
                    + " - instance: " + instance);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        Thread[] threads = new Thread[numberOfThreads];
        for (int i = 0; i &lt; numberOfThreads; i++) {
            threads[i] = new Thread(runnable);
            threads[i].start();
        }

        for (Thread thread : threads) {
            thread.join();
        }
    }

}
output:
case1
Thread: Thread-1 - instance: Singleton@25e4c956
Thread: Thread-0 - instance: Singleton@3e483bf7
case2
Thread: Thread-0 - instance: Singleton@726166f6
Thread: Thread-1 - instance: Singleton@726166f6
</code></pre>
<p><code>CountDownLatch</code> lets one or more threads wait until other threads have finished a certain operation. The major methods in Java 21 (implementation omitted) are:</p>
<pre><code>public class CountDownLatch {
    private final Sync sync;
    // Constructor takes the count and creates a Sync
    public CountDownLatch(int count) {}
    // Wait until the counter reaches zero
    public void await() throws InterruptedException {}
    // Wait with timeout
    public boolean await(long timeout, TimeUnit unit) throws InterruptedException {}
    // Decrease the counter by one
    public void countDown() {}
    public long getCount() {}
    public String toString() {}
    // Inner Sync class extends AbstractQueuedSynchronizer
    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;
        Sync(int count) {}
        int getCount() {}
        protected int tryAcquireShared(int acquires) {}
        protected boolean tryReleaseShared(int releases) {}
    }
}
</code></pre>
<p>Internally it uses a synchronizer that extends <code>AbstractQueuedSynchronizer</code> (AQS). We'll leave AQS itself for another article.</p>
<p>In the earlier sample we used:</p>
<pre><code>latch.countDown(); // decrease the counter to signal readiness
latch.await();     // block until the counter hits zero
</code></pre>
<p>to make sure both threads start fetching the singleton at the same time.</p>
<p><em>PS:</em> operating-system scheduling, hardware timing, and other factors can still cause tiny differences. Threads do not start at the exact same nanosecond, so the output may vary between runs.</p>
<p><em>Fix</em><br />
The obvious idea is to add <code>synchronized</code> to <code>getInstance()</code>, which leads us to the second version.</p>
<h4>Implementation 2 (synchronized method)</h4>
<pre><code>// version2
class Singleton {
    private static Singleton instance;
    private Singleton() {}
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
Output:
Thread: Thread-1 - instance: Singleton@46bd3fc9
Thread: Thread-0 - instance: Singleton@46bd3fc9
</code></pre>
<ul>
<li>We now obtain the same object every time.</li>
</ul>
<p><em>Problem</em><br />
Every call to <code>getInstance()</code> must acquire the lock even after the object has been created, which means blocked threads and wasted time.</p>
<p><em>Fix</em><br />
Instead of locking the whole method we can narrow the critical section to reduce contention. This produces the third version, also known as double-checked locking (DCL).</p>
<h4>Implementation 3 (Double-Checked Locking)</h4>
<pre><code>// version3
class Singleton {
    private static Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
</code></pre>
<p><em>Logic</em></p>
<ul>
<li>The first <code>if (instance == null)</code> avoids locking when the object already exists.</li>
<li>The second <code>if (instance == null)</code> ensures that, even if several threads enter the method at the same time, only one of them creates the instance (same role as version2).</li>
</ul>
<p><em>Problem</em><br />
Compiler and CPU optimizations may reorder instructions, which breaks thread safety.</p>
<p>What does <code>singleton = new Singleton()</code> actually do?</p>
<ol>
<li>Allocate memory on the heap.</li>
<li>Call the constructor to initialize the fields.</li>
<li>Assign the object reference to <code>singleton</code> (which now becomes non-null).</li>
</ol>
<p>If steps 2 and 3 are reordered (e.g. 1-3-2), thread A might finish step 3 before step 2, then get preempted. Thread B reads <code>instance != null</code> and returns the partially constructed object. In other words, <strong>thread A has not finished writing when thread B begins reading</strong>.</p>
<p>Hence version 4 introduces <code>volatile</code>.</p>
<h4>Implementation 4 (volatile)</h4>
<pre><code>class Singleton {
    private static volatile Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
</code></pre>
<p>This is identical to version 3 except that the field is declared <code>volatile</code>.</p>
<p>Let's revisit what <code>volatile</code> means.</p>
<h5>volatile</h5>
<p><code>volatile</code> has two effects:</p>
<ol>
<li><strong>Visibility</strong> – when one thread writes to a <code>volatile</code> variable, the new value is flushed to main memory so every other thread sees it immediately. The write invalidates cached copies in CPU cores, forcing readers to fetch the latest value.</li>
<li><strong>Prohibiting instruction reordering</strong> – neither the compiler nor the CPU may reorder reads/writes around a <code>volatile</code> access, which preserves the required happens-before relationship.</li>
</ol>
<p>Note that <code>volatile</code> does <strong>not</strong> provide full atomicity; it only guarantees that each individual read or write is atomic.</p>
<p><strong>How it works</strong></p>
<ol>
<li>Memory visibility is implemented via memory barriers. They tell the compiler and CPU that nothing may be reordered across the barrier.</li>
<li>The Java Memory Model expresses this using the happens-before rule: a write to a <code>volatile</code> field happens-before every subsequent read of that same field.</li>
</ol>
<p>In our singleton, volatile does not stop the internal steps (1-2-3) from being reordered, but it guarantees that the write cannot be observed by another thread before the constructor has finished, because the assignment (step 3) cannot move ahead of the constructor (step 2).</p>
<p>While reading about DCL you may also encounter an idea that uses ThreadLocal to “fix” it:</p>
<h4>Implementation 5: ThreadLocal</h4>
<p>// TODO</p>
<p>So far we have covered the lazy family of implementations. Next up is the eager approach.</p>
<h3>Eager singleton</h3>
<p>An eager singleton creates the instance when the class is loaded.</p>
<pre><code>public class Singleton {
    // Create and initialize the singleton during class loading
    private static final Singleton instance = new Singleton();

    // Private constructor prevents external instantiation
    private Singleton() {}

    // Public accessor
    public static Singleton getInstance() {
        return instance;
    }
}
</code></pre>
<p><em>Logic</em></p>
<p>Why does <code>static final</code> suffice for a singleton?</p>
<p>Recall the lifecycle of a Java class:</p>
<ol>
<li><strong>Loading</strong>
<ol>
<li>Locate the class bytecode via the fully qualified name.</li>
<li>Convert the static storage structures in the bytecode into the runtime data structures in the method area.</li>
<li>Create a <code>java.lang.Class</code> object on the heap to serve as the access entry for the method-area data.</li>
</ol>
</li>
<li><strong>Linking</strong>
<ol>
<li>
<p><em>Verification</em></p>
</li>
<li>
<p><em>Preparation</em> – allocate memory for static variables and set them to their default values.</p>
<p><strong>Notes:</strong></p>
<ul>
<li>Only static variables are involved; instance fields are allocated when objects are instantiated.</li>
<li>The default value is usually the zero value of the type unless an explicit initializer is provided.</li>
</ul>
</li>
<li>
<p><em>Resolution</em> – convert symbolic references in the constant pool into direct references (addresses).</p>
</li>
</ol>
</li>
<li><strong>Initialization</strong> – run the class initializer to assign the actual values to static variables.</li>
</ol>
<p>The <code>static final</code> field is assigned during the preparation phase, and initialization happens during class initialization. Class loading itself is thread-safe, so writing to that static constant is also <strong>thread-safe</strong>. Because the field is a <strong>constant</strong>, it cannot change afterwards, which preserves <strong>uniqueness</strong>.</p>
<p><em>Problem</em></p>
<p>The code above is the canonical eager singleton. Its drawbacks are inherent to the eager approach: the instance is created even if it is never used. When the object is heavy or expensive to initialize, this wastes memory and slows down startup.</p>
<p>We still have two additional patterns: static inner classes and enums.</p>
<h3>Static inner class</h3>
<pre><code>public class Singleton {
    private Singleton() {
        // Private constructor prevents direct instantiation
    }

    // Static inner class
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    // Public accessor
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
</code></pre>
<p><em>Logic</em></p>
<p><code>SingletonHolder</code> is a static inner class. It is initialized when <code>Singleton</code> is loaded, and it creates the <code>INSTANCE</code> field. Because <code>Singleton</code>'s constructor is private, the only place that can instantiate it is the inner class.</p>
<p>This means <code>INSTANCE</code> is already initialized before <code>getInstance()</code> is ever called, so there is no race. More specifically:</p>
<ol>
<li>The first thread that calls <code>getInstance()</code> triggers the initialization block of <code>SingletonHolder</code>, which creates the <code>Singleton</code> instance and assigns it to <code>INSTANCE</code>.</li>
<li>The other threads find that <code>INSTANCE</code> already exists and simply return it without re-entering the initialization block.</li>
</ol>
<p>Therefore only one thread runs the initialization code, which gives us thread safety while keeping lazy semantics (since the holder class is loaded only when needed).</p>
<h3>Enum-based singleton</h3>
<pre><code>public enum Singleton {
    INSTANCE; // Enum value

    // Additional methods or fields can be declared here
}
</code></pre>
<p><em>Logic</em></p>
<p>When the compiler translates an enum, it generates a class and turns each enum constant into a static final field. Those constants are instantiated during class loading, and the process is thread-safe. Whenever you reference <code>Singleton.INSTANCE</code> you get the same object.</p>
<p>Conceptually it becomes something like:</p>
<pre><code>public final class Singleton extends Enum&lt;Singleton&gt; {
    public static final Singleton INSTANCE = new Singleton();
    // other enum-related code
}
</code></pre>
<p>Because the instance is created during class loading, the enum approach is also an eager singleton.</p>
<hr />
<p>To wrap up: the singleton pattern guarantees a single instance with a global access point. In Java we can implement it via lazy loading (with synchronization, DCL + volatile, etc.), eager loading, static inner classes, or enums. Each approach has trade-offs. Lazy implementations need extra synchronization to avoid races, eager ones may waste resources, the static-inner-class trick gives us lazy loading with thread safety courtesy of the class loader, and enums provide a concise, reflection-safe eager singleton. Choose the implementation that best balances thread safety, resource usage, and complexity for your scenario.</p>
]]></content>
        <author>
            <name>eryi</name>
            <uri>https://eryi.dev/</uri>
        </author>
        <published>2023-12-10T00:00:00.000Z</published>
    </entry>
</feed>