<?xml version="1.0" encoding="UTF-8"?>    <rss version="2.0"
        xmlns:content="http://purl.org/rss/1.0/modules/content/"
        xmlns:wfw="http://wellformedweb.org/CommentAPI/"
        xmlns:dc="http://purl.org/dc/elements/1.1/"
        xmlns:atom="http://www.w3.org/2005/Atom"
        xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
        xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
        >
    
    <channel>
        <title>System Design Roadmap - Hands-On System Design Lessons</title>
        <atom:link href="https://systemdrd.com/feed/lessons" rel="self" type="application/rss+xml" />
        <link></link>
        <description>Hands-On System Design lessons, AI Agents tutorials, and practical programming tutorials. Learn by doing with real-world projects and examples.</description>
        <lastBuildDate>Thu, 01 Oct 2026 10:15:33 +0000</lastBuildDate>
        <language>en-US</language>
        <sy:updatePeriod>hourly</sy:updatePeriod>
        <sy:updateFrequency>1</sy:updateFrequency>
        <generator>https://wordpress.org/?v=7.1.2</generator>

<image>
	<url>https://systemdrd.com/wp-content/uploads/2026/07/cropped-cropped-systemdr-inc-32x32.jpeg</url>
	<title>System Design Roadmap</title>
	<link>https://systemdrd.com</link>
	<width>32</width>
	<height>32</height>
</image> 
        
                <item>
            <title> - Hands-On Tutorial</title>
            <link></link>
            <comments>#respond</comments>
            <pubDate></pubDate>
            <dc:creator><![CDATA[admin]]></dc:creator>
                        <guid isPermaLink="false"></guid>
            <description><![CDATA[# Day 27: Profile JIT Compilation and Inline Decisions Using PrintCompilation Flags In Day 26, we implemented a Token-Bucket backpressure mechanism to prevent our AstraKV socket buffers from overflowing under... Hands-On System Design tutorial with practical examples and real-world applications.]]></description>
            <content:encoded><![CDATA[<div class="lesson-rss-content"><h3>Hands-On System Design Tutorial</h3><p data-ai-summary="true"># Day 27: Profile JIT Compilation and Inline Decisions Using PrintCompilation Flags</p>
<p data-ai-summary="true">In Day 26, we implemented a Token-Bucket backpressure mechanism to prevent our AstraKV socket buffers from overflowing under load. By throttling incoming requests, we stopped the server from crashing due to `OutOfMemoryError` or kernel-level packet drops. However, even with backpressure, your server’s throughput might still hit a &#8220;<span data-ai-definition="performance">performance</span> ceiling&#8221; where CPU usage is high, but request latency spikes unpredictably. </p>
<p data-ai-summary="true">Today, we look inside the JVM&#8217;s engine room: the Just-In-Time (JIT) compiler. We will learn how to observe the JVM&#8217;s decision-making process as it transforms your bytecode into machine code, specifically focusing on **inlining**—the most critical optimization for high-<span data-ai-definition="performance">performance</span> storage engines.</p>
<p data-ai-summary="true">## The Problem: The &#8220;Black Box&#8221; <span data-ai-definition="performance">performance</span> Plateau</p>
<p data-ai-summary="true">In a system like AstraKV, your `StorageEngine` and `NetworkHandler` methods are called millions of times per second. If the JIT compiler fails to inline these hot methods, the overhead of repeated method calls (stack frame allocation, register saving) becomes a significant tax on your throughput. </p>
<p data-ai-summary="true">Consider the &#8220;LMAX Disruptor&#8221; architecture. At the scale of hundreds of millions of requests, the difference between an inlined method call and a virtual method call is the difference between meeting your latency SLA or timing out. When the JIT compiler makes a &#8220;bad&#8221; decision—or when it is constantly re-compiling due to class loading—your system experiences &#8220;jitter,&#8221; or intermittent latency spikes that are nearly impossible to debug without visibility into the compilation log.</p>
<p data-ai-summary="true">## The Mechanism: JIT Compilation and Inlining</p>
<p data-ai-summary="true">The HotSpot JVM uses two compilers: C1 (Client, fast, basic optimization) and C2 (Server, slow, aggressive optimization). When a method is &#8220;hot,&#8221; C2 takes over. The most powerful optimization C2 performs is **inlining**: replacing a method call with the actual body of the method.</p>
<p>&#8220;`java<br />
// Conceptual: The JIT compiler transforms this&#8230;<br />
public void handle(Request req) {<br />
    if (this.validate(req)) {<br />
        this.process(req);<br />
    }<br />
}</p>
<p>// &#8230;into this (machine code level):<br />
public void handle(Request req) {<br />
    // Inlined body of validate()<br />
    if (req.isValid()) {<br />
        // Inlined body of process()<br />
        this.storage.put(req.key, req.val);<br />
    }<br />
}<br />
&#8220;`</p>
<p data-ai-summary="true">By inlining, we eliminate the jump instruction and allow the CPU to perform better branch prediction and instruction scheduling. If your critical path methods are too large or contain too many polymorphic call sites, the JIT compiler will refuse to inline them, leaving your system running at &#8220;interpreted&#8221; or &#8220;partially optimized&#8221; speed.</p>
<p data-ai-summary="true">## The Production Stakes: The &#8220;Warm-up&#8221; Outage</p>
<p data-ai-summary="true">In 2013, engineers at a major financial firm observed that their trading engines would periodically stall for 500ms every time they deployed a new configuration. It turned out that the configuration change triggered the loading of a new class, which forced the JIT compiler to de-optimize existing code and re-compile the hot path. This is the &#8220;warm-up&#8221; problem: systems are not production-ready the moment they start; they are only ready once the JIT has finished its work. Understanding `PrintCompilation` allows you to see exactly when your system is &#8220;warmed up.&#8221;</p>
<p data-ai-summary="true">## Observing the Compiler</p>
<p data-ai-summary="true">We will use the `-XX:+PrintCompilation` and `-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining` flags. These flags turn the JVM&#8217;s internal decision-making process into a readable log.</p>
<p>&#8220;`java<br />
// Example of what we are looking for in the logs:<br />
// 123 45   n  com.astrakv.StorageEngine::put (15 bytes)<br />
// 124 46 %   com.astrakv.NetworkHandler::loop @ 10 (50 bytes)<br />
&#8220;`</p>
<p>The output tells you:<br />
1.**Timestamp/ID:** When the compilation happened.<br />
2.**Compiler:** `n` (native), `s` (synchronized), or `%` (OSR &#8211; On-Stack Replacement).<br />
3.**Method:** The signature being compiled.</p>
<p data-ai-summary="true">## Failure Demo: The &#8220;De-optimization&#8221; Trap</p>
<p data-ai-summary="true">Today, we will deliberately break the JIT&#8217;s ability to optimize by introducing a &#8220;polymorphic&#8221; call site that changes its implementation type frequently. You will see the JIT compiler work hard to inline a method, only to be forced to &#8220;de-optimize&#8221; it when it realizes the implementation is no longer stable. This causes a massive, observable drop in throughput.</p>
<p data-ai-summary="true">## Production Reality vs. Laptop Constraints</p>
<p data-ai-summary="true">In production, you would never keep `PrintCompilation` enabled, as it creates significant I/O overhead. You use it during load testing to verify that your &#8220;hot&#8221; methods (like `put` and `get` in AstraKV) are being inlined by C2. On your laptop, we will use it to learn the signal. We ignore the complexity of tiered compilation levels (C1 vs C2) for now, focusing only on the *fact* of compilation.</p>
<p data-ai-summary="true">### Assignment</p>
<p>Your task is to identify the &#8220;hot&#8221; method in your `StorageEngine` implementation.<br />
1.Run the server with `-XX:+PrintCompilation`.<br />
2.Use a simple loop to call `put()` 1,000,000 times.<br />
3.Capture the output to a file and grep for your class name.<br />
4.**Success Criteria:** You must find the line where your `put` method is compiled by the C2 compiler (indicated by the `4` in the compilation ID column).</p>
<p>### Solution Hints<br />
*The JVM needs a &#8220;warm-up&#8221; period. A single call won&#8217;t trigger C2. You need a loop of at least 10,000–50,000 iterations for the JIT to consider a method &#8220;hot.&#8221;<br />
*If you don&#8217;t see your method, it might be too small (the JVM inlines it automatically without a separate compilation log entry) or too large (exceeding the default inline budget).<br />
*Check the `PrintInlining` output to see if the JVM rejected your method for being &#8220;too big.&#8221;</p>
</div>]]></content:encoded>
                                </item>
                <item>
            <title> - Hands-On Tutorial</title>
            <link></link>
            <comments>#respond</comments>
            <pubDate></pubDate>
            <dc:creator><![CDATA[admin]]></dc:creator>
                        <guid isPermaLink="false"></guid>
            <description><![CDATA[# Day 26: Implement Token-Bucket Backpressure In Day 25, we successfully exposed our AstraKV storage engine over a TCP socket using Java NIO. You built a server that can accept... Hands-On System Design tutorial with practical examples and real-world applications.]]></description>
            <content:encoded><![CDATA[<div class="lesson-rss-content"><h3>Hands-On System Design Tutorial</h3><p data-ai-summary="true"># Day 26: Implement Token-Bucket Backpressure</p>
<p data-ai-summary="true">In Day 25, we successfully exposed our AstraKV storage engine over a TCP socket using Java NIO. You built a server that can accept client connections and process `GET` and `PUT` commands. However, that server has a fatal flaw: it is &#8220;too eager.&#8221; It will accept every byte a client sends as fast as the kernel buffer allows, potentially exhausting memory or CPU cycles before the storage engine can even acknowledge the request. Today, we fix this by implementing a Token-Bucket algorithm to enforce backpressure.</p>
<p>### The Problem: The &#8220;Firehose&#8221; Effect<br />
When a client sends data faster than your server can process it, the OS kernel queues those bytes in the TCP receive buffer. If you don&#8217;t read them, the buffer fills up, and the TCP window size drops to zero, forcing the client to pause. This is a form of passive backpressure. But what happens if you *are* reading, but your internal logic (disk I/O or index lookups) is slower than the network? You end up with an unbounded queue of pending requests in your application heap.</p>
<p data-ai-summary="true">This is exactly what led to the famous &#8220;Cascading Failure&#8221; scenarios in systems like those documented by AWS during their early DynamoDB outages: when one node slows down, the upstream clients retry more aggressively, flooding the struggling node until it crashes, causing its neighbors to take the load and crash in turn. </p>
<p>### The Mechanism: Token Bucket<br />
To prevent this, we introduce a **Token Bucket**. Imagine a bucket that holds a fixed number of &#8220;tokens.&#8221; Each request consumes one token. Tokens are added to the bucket at a constant rate. If the bucket is empty, the server refuses the request (or forces the client to wait).</p>
<p>***Bucket Capacity ($B$):** Allows for short bursts of traffic.<br />
***Refill Rate ($R$):** The long-term sustainable throughput of your system.</p>
<p data-ai-summary="true">If your storage engine can handle 1,000 requests per second, you set $R=1000$. If you get a sudden spike, the $B$ tokens allow you to absorb it, provided the average stays within $R$.</p>
<p>### Architectural Integration<br />
We place the Token Bucket directly in the `RequestProcessor` pipeline, just after the `SocketChannel` reads the bytes but before the `StorageEngine` executes the command.</p>
<p>[DIAGRAM: Client -> Socket Buffer -> [Token Bucket] -> Storage Engine]<br />
*Annotation: If Bucket == 0, return 429 Too Many Requests or close the connection to signal backpressure.*</p>
<p>### Conceptual Implementation<br />
You don&#8217;t need a complex library for this. A simple `AtomicLong` and a timestamp check suffice for a single-threaded reactor:</p>
<p>&#8220;`java<br />
public class TokenBucket {<br />
    private final long capacity;<br />
    private final long refillRatePerNanos;<br />
    private long tokens;<br />
    private long lastRefillTimestamp;</p>
<p>    public synchronized boolean tryConsume() {<br />
        refill();<br />
        if (tokens > 0) {<br />
            tokens&#8211;;<br />
            return true;<br />
        }<br />
        return false;<br />
    }</p>
<p>    private void refill() {<br />
        long now = System.nanoTime();<br />
        long delta = now &#8211; lastRefillTimestamp;<br />
        tokens = Math.min(capacity, tokens + (delta / refillRatePerNanos));<br />
        lastRefillTimestamp = now;<br />
    }<br />
}<br />
&#8220;`</p>
<p>### Trade-offs: Why not a Semaphore?<br />
You might be tempted to use a `java.util.concurrent.Semaphore`. A semaphore is great for limiting *concurrency* (how many requests are running *right now*). A Token Bucket limits *rate* (how many requests per second). In storage engines, we care about rate because disk I/O latency is throughput-dependent. If you use a semaphore, you might allow a burst of 100 requests that all hit the disk simultaneously, causing IOPS saturation and latency spikes.</p>
<p>### The Failure Demo<br />
Today, you will use a client script to blast 5,000 requests per second at a server configured to handle only 500. Without the bucket, your JVM heap will grow until an `OutOfMemoryError` or the latency will climb into the seconds as the GC struggles to clean up the queue of pending objects. With the bucket, you will observe the server rejecting requests, keeping the system stable at the defined 500 RPS.</p>
<p>### Looking Ahead<br />
Tomorrow, in Day 27, we will look at how the JVM JIT compiler decides to inline our `tryConsume()` method. Because we are now doing math in the hot path of every request, we need to ensure our Token Bucket doesn&#8217;t become a <span data-ai-definition="performance">performance</span> bottleneck itself.</p>
<p data-ai-summary="true">&#8212;</p>
<p>### Assignment<br />
1.**Integrate:** Add the `TokenBucket` class to your `astra.net` package.<br />
2.**Instrument:** Modify your `NioServer` to instantiate a bucket with a capacity of 100 and a rate of 500 RPS.<br />
3.**Reject:** If `tryConsume()` returns false, send a custom &#8220;429 Too Many Requests&#8221; response back to the client.<br />
4.**Verify:** Run the load test provided in the repo. Ensure that while the client reports failures, the server process remains responsive and memory usage stays flat.</p>
<p>### Solution Hints<br />
*Don&#8217;t use `Thread.sleep` to refill; use the delta-time approach shown in the snippet.<br />
*Your `NioServer` loop should check the bucket before calling `storageEngine.execute()`.<br />
*If you see &#8220;Connection Reset&#8221; errors, your client might be closing too fast; ensure you handle the server&#8217;s response even when it&#8217;s a 429.</p>
</div>]]></content:encoded>
                                </item>
                <item>
            <title> - Hands-On Tutorial</title>
            <link></link>
            <comments>#respond</comments>
            <pubDate></pubDate>
            <dc:creator><![CDATA[admin]]></dc:creator>
                        <guid isPermaLink="false"></guid>
            <description><![CDATA[# Day 25: Expose the Storage Engine Over a TCP Socket Server Using Java NIO Yesterday, we built a memory-bounded storage engine that handles data safely by pinning buffers and... Hands-On System Design tutorial with practical examples and real-world applications.]]></description>
            <content:encoded><![CDATA[<div class="lesson-rss-content"><h3>Hands-On System Design Tutorial</h3><p data-ai-summary="true"># Day 25: Expose the Storage Engine Over a TCP Socket Server Using Java NIO</p>
<p data-ai-summary="true">Yesterday, we built a memory-bounded storage engine that handles data safely by pinning buffers and managing its own memory outside the unpredictable influence of the JVM Garbage Collector. Today, we bridge the gap between that engine and the outside world by building a TCP server using Java NIO (New I/O).</p>
<p>### The Concept: Non-Blocking Event Loops vs. Thread-per-Connection<br />
In traditional Java networking (the old `java.io` package), every incoming TCP connection requires a dedicated thread. If you have 10,000 clients, you have 10,000 threads. This is a disaster: the kernel spends more time context-switching between threads than executing your code, and the memory overhead of 10,000 thread stacks—even at 1MB each—will crash your JVM long before you hit your <span data-ai-definition="performance">performance</span> goals.</p>
<p data-ai-summary="true">We use **Java NIO** to solve this. Instead of one thread per client, we use a single &#8220;Selector&#8221; thread that monitors thousands of sockets for events (like &#8220;data ready to read&#8221;). This is the same architectural pattern used by high-<span data-ai-definition="performance">performance</span> systems like Netty, Kafka, and the internal RPC layers of distributed databases like Apache Cassandra.</p>
<p>### The Production Stakes: The &#8220;Thundering Herd&#8221; and Socket Saturation<br />
When you expose a storage engine directly to a network, you lose control over the arrival rate of requests. If your engine takes 5ms to process a request and 1,000 clients connect simultaneously, your queue will grow unbounded, leading to **latency inflation**. </p>
<p data-ai-summary="true">A famous example of this is the &#8220;TCP Backlog&#8221; failure documented in various Linux kernel tuning guides: when the server’s listen queue fills up, the kernel begins dropping new connections (SYN drops). If your application logic doesn&#8217;t handle the transition between the network buffer and the storage engine correctly, you can end up with partial writes or, worse, memory exhaustion as incoming requests buffer indefinitely in the JVM heap.</p>
<p>### Architecture: The Reactor Pattern<br />
We implement a simplified **Reactor Pattern**. </p>
<p>1.**The Acceptor:** Monitors the `ServerSocketChannel` for new connection attempts.<br />
2.**The Reactor:** A single thread running a `Selector` loop. It polls the OS for channels that have data ready to be read.<br />
3.**The Processor:** Once data is present, we read it into the buffer pool we built in Day 24, route the command to our storage engine, and write the response back.</p>
<p>### The Failure Demo: The &#8220;Slow Consumer&#8221;<br />
In the Implementation Guide, you will trigger a failure by intentionally pausing the processing logic while flooding the server with requests. You will observe the `Selector` continue to accept connections, but the &#8220;Read&#8221; operations will start piling up in the kernel&#8217;s receive buffer. Eventually, the client-side TCP window will close, and the client will hang—this is the network performing **natural backpressure**. Tomorrow, we will learn how to manage this explicitly before the kernel has to force it on us.</p>
<p>### Trade-off: Why not use Netty?<br />
You might ask: &#8220;Why build this raw NIO instead of using Netty?&#8221; Netty is an incredible library, but it hides the `Selector` and `ByteBuffer` management. By building this today, you learn how to debug `Selector` starvation and buffer leaks. In production, you would use Netty to save development time, but you would use the knowledge from today to tune its `EventLoopGroup` threads and `ByteBufAllocator`.</p>
<p>### Assignment: The Throughput Test<br />
Your task is to modify the `AstraKVServer` to log the number of bytes processed per second.<br />
1.Use the provided `load-tester` script to send 10,000 small GET requests.<br />
2.Observe the latency.<br />
3.**The Challenge:** Introduce a `Thread.sleep(10)` inside the `processCommand` method. Run the test again. Observe how the server stops responding to new requests once the internal queue depth is exceeded.</p>
<p>### Solution Hints<br />
-To log throughput, use a `LongAdder` to count bytes read in the `handleRead` method.<br />
-To see the &#8220;hang,&#8221; use `netstat -an | grep 8080` to see the `Recv-Q` (Receive Queue) filling up. If `Recv-Q` stays at a high number, your server is not reading fast enough.</p>
</div>]]></content:encoded>
                                </item>
                <item>
            <title> - Hands-On Tutorial</title>
            <link></link>
            <comments>#respond</comments>
            <pubDate></pubDate>
            <dc:creator><![CDATA[admin]]></dc:creator>
                        <guid isPermaLink="false"></guid>
            <description><![CDATA[# Day 24: Building a Memory-Bounded Storage Engine with Automatic GC-Friendly Buffer Pools In our last session, we triggered an `OutOfMemoryError` by naively buffering incoming write requests into the JVM... Hands-On System Design tutorial with practical examples and real-world applications.]]></description>
            <content:encoded><![CDATA[<div class="lesson-rss-content"><h3>Hands-On System Design Tutorial</h3><p data-ai-summary="true"># Day 24: Building a Memory-Bounded Storage Engine with Automatic GC-Friendly Buffer Pools</p>
<p data-ai-summary="true">In our last session, we triggered an `OutOfMemoryError` by naively buffering incoming write requests into the JVM heap. We saw how quickly the Garbage Collector (GC) becomes the bottleneck when it has to manage millions of short-lived objects. Today, we move beyond the heap. We are going to build a **Buffer Pool**—a fixed-size, off-heap memory region—to act as the gatekeeper for AstraKV’s storage engine.</p>
<p>### The Problem: When GC Becomes Your Worst Enemy<br />
In high-throughput systems like Apache Kafka or LinkedIn’s Espresso, if you allocate every incoming request as a new Java object on the heap, you aren&#8217;t just storing data; you are creating work for the GC. At 100,000 requests per second, the GC will spend more time marking and sweeping your objects than your application will spend writing them to disk. This is the &#8220;Stop-the-World&#8221; tax.</p>
<p data-ai-summary="true">By moving our buffers off-heap using `java.nio.ByteBuffer.allocateDirect()`, we move the memory out of the reach of the GC’s reachability analysis. The tradeoff? We lose automatic memory management. If we don&#8217;t recycle these buffers, we leak memory at the OS level, eventually crashing the process with a native `OutOfMemoryError` that no heap dump can explain.</p>
<p>### The Architecture: The Buffer Pool<br />
A Buffer Pool is a pre-allocated slab of memory divided into fixed-size &#8220;slots.&#8221; Instead of `new byte[]`, our engine checks out a slice of this slab. When the storage operation completes, the slice is returned (checked in) to the pool.</p>
<p>#### Component Interaction<br />
&#8211; **The Pool:** A `ConcurrentLinkedQueue` or `ArrayBlockingQueue` holding available `DirectByteBuffer` instances.<br />
&#8211; **The Engine:** Requests a buffer from the pool before parsing the network packet.<br />
&#8211; **The Lifecycle:** Once the data is flushed to the WAL (Write Ahead Log), the engine returns the buffer to the pool.</p>
<p>&#8220;`java<br />
// A simple pool entry retrieval<br />
public ByteBuffer acquire() {<br />
    ByteBuffer buffer = pool.poll();<br />
    if (buffer == null) {<br />
        // We are at capacity, apply backpressure or block<br />
        throw new BufferPoolExhaustedException();<br />
    }<br />
    buffer.clear();<br />
    return buffer;<br />
}<br />
&#8220;`</p>
<p>### The Trade-off: Fixed Capacity vs. Dynamic <span data-ai-definition="scalability">scalability</span><br />
We are choosing a **Fixed-Size Buffer Pool**.<br />
***Alternative:** Dynamic allocation. It’s easier to implement but leads to fragmentation and non-deterministic latency.<br />
***Why we choose Fixed-Size:** It provides an inherent **backpressure mechanism**. If the pool is empty, the system cannot accept more work. This prevents the &#8220;death spiral&#8221; where an overloaded system crashes itself by trying to buffer everything at once.</p>
<p>### Failure Demo: The &#8220;Memory Leak&#8221; Simulation<br />
In the implementation guide, you will run a load test that deliberately leaks buffers by failing to &#8220;check them in.&#8221; You will observe the system reach its hard limit and reject new requests. This is not a failure; this is the system protecting itself from total collapse.</p>
<p>### Scaling to Production<br />
At scale, you don&#8217;t just have one pool. You might have pools for different sizes (e.g., 4KB, 16KB, 64KB) to prevent internal fragmentation. Remember: off-heap memory is invisible to standard JVM monitoring tools like `jstat`. You must monitor `process_resident_memory_bytes` at the OS level to ensure your pool isn&#8217;t leaking.</p>
<p data-ai-summary="true">As we move toward Day 19, we will use this pool to measure how much &#8220;garbage&#8221; we have removed from the heap, allowing us to tune the JVM’s GC parameters for a much smaller, more stable heap footprint.</p>
<p data-ai-summary="true">&#8212;</p>
<p>### Assignment<br />
1.**Instrument the Pool:** Add a counter to the `acquire` and `release` methods to track the current &#8220;in-use&#8221; count.<br />
2.**Break the Pool:** Create a test case where you acquire 100 buffers but only release 90. Observe the log output as the system hits the limit.<br />
3.**Verify:** Use the provided `verify_pool.sh` to confirm that under a sustained load of 5,000 requests, the heap usage remains flat while the native memory usage stays within your configured bounds.</p>
<p>### Solution Hints<br />
*Use a `Semaphore` initialized to the pool size to implement the blocking logic when the pool is empty.<br />
*Ensure that every `ByteBuffer` is `clear()`ed before reuse to avoid cross-pollination of data from previous requests.<br />
*Check the repository&#8217;s `BufferPool.java` for the base implementation of the `acquire`/`release` cycle.</p>
</div>]]></content:encoded>
                                </item>
                <item>
            <title> - Hands-On Tutorial</title>
            <link></link>
            <comments>#respond</comments>
            <pubDate></pubDate>
            <dc:creator><![CDATA[admin]]></dc:creator>
                        <guid isPermaLink="false"></guid>
            <description><![CDATA[# Day 23: Trigger an OutOfMemoryError by Saturating the Heap and Analyze the Heap Dump In our previous session, we used GC logs to compare G1GC and ZGC, observing how... Hands-On System Design tutorial with practical examples and real-world applications.]]></description>
            <content:encoded><![CDATA[<div class="lesson-rss-content"><h3>Hands-On System Design Tutorial</h3><p data-ai-summary="true"># Day 23: Trigger an OutOfMemoryError by Saturating the Heap and Analyze the Heap Dump</p>
<p data-ai-summary="true">In our previous session, we used GC logs to compare G1GC and ZGC, observing how different collectors handle high-allocation pressure. Today, we confront the &#8220;end-state&#8221; of heap management: the `OutOfMemoryError` (OOME). </p>
<p data-ai-summary="true">When a Java application hits its heap limit, it doesn&#8217;t just crash gracefully. It often enters a &#8220;death spiral&#8221; where the GC works at 100% CPU capacity trying to reclaim a few bytes, causing latency to spike to seconds before the JVM finally throws an OOME. Understanding this transition is critical for building robust storage engines.</p>
<p>### The Stakes: The &#8220;Death Spiral&#8221;<br />
In production, an OOME is rarely a clean exit. Consider the 2012 GitHub outage, where a memory leak in a Ruby-based component caused the system to thrash in GC before failing. In Java, this often manifests as &#8220;Stop-the-World&#8221; (STW) pauses that grow exponentially as the heap fills. If your storage engine doesn&#8217;t have an explicit memory-bounding strategy, it will eventually consume the entire container limit, triggering the OOM Killer at the OS level, which leaves behind no heap dump and zero diagnostic context. </p>
<p>### The Concept: Heap Saturation and Forensic Capture<br />
Today, we will intentionally force a heap exhaustion event. The goal is to observe the transition from &#8220;healthy allocation&#8221; to &#8220;GC thrashing&#8221; to &#8220;OOME,&#8221; and then extract a heap dump to diagnose the culprit.</p>
<p data-ai-summary="true">We rely on the JVM’s ability to generate a snapshot of the heap at the moment of death. This is the difference between guessing why a system failed and knowing exactly which object type consumed your memory.</p>
<p>### Anatomy of a Memory Leak<br />
In AstraKV, we represent data as `byte[]` arrays. If we store these in a `HashMap` without a bound or an eviction policy, we create a classic memory leak. </p>
<p>&#8220;`java<br />
// A simple, dangerous cache that never evicts<br />
public class UnboundedCache {<br />
    private final Map<String, byte[]> storage = new HashMap<>();</p>
<p>    public void put(String key, byte[] value) {<br />
        // No capacity check: memory will grow until OOME<br />
        storage.put(key, value);<br />
    }<br />
}<br />
&#8220;`</p>
<p data-ai-summary="true">The logic above is simple, but it ignores the fundamental law of distributed systems: **Finite resources require explicit bounds.** In production, an unbounded cache is a time bomb.</p>
<p>### The Failure Demo: The &#8220;OOM Trap&#8221;<br />
We will run a workload that inserts data into an unbounded map while monitoring the process. You will observe the JVM attempting to reclaim space, the GC frequency increasing, and finally, the process terminating with a heap dump file.</p>
<p>### Production Realities<br />
At hyperscale, we rarely rely on default JVM behavior for memory management. We use:<br />
1.**Off-heap memory:** Using `java.nio.ByteBuffer.allocateDirect()` to move data outside the GC-managed heap, preventing GC pauses for large caches.<br />
2.**Backpressure:** Preventing the system from accepting new requests when memory utilization exceeds a threshold (e.g., 80%).<br />
3.**Soft/Weak References:** Allowing the GC to reclaim cache entries under extreme pressure, though this is often too unpredictable for high-<span data-ai-definition="performance">performance</span> storage engines.</p>
<p data-ai-summary="true">We choose to learn about heap saturation today because before we can build a memory-bounded engine tomorrow, we must be able to recognize the exact moment a system becomes &#8220;unbounded.&#8221;</p>
<p data-ai-summary="true">&#8212;</p>
<p>### Assignment<br />
1.**Trigger:** Run the `OOMGenerator` utility in the provided repository. This will fill an `ArrayList` with large objects until the heap is exhausted.<br />
2.**Observe:** Watch the console output to see the GC activity log messages transition from short pauses to multi-second &#8220;Full GC&#8221; events.<br />
3.**Analyze:** Use `jhat` or VisualVM to open the resulting `.hprof` file. Identify the class consuming the most memory.<br />
4.**Compare:** Run the same test with `-Xmx128m` versus `-Xmx512m` and observe how the time-to-failure correlates with heap size.</p>
<p>### Solution Hints<br />
&#8211; The heap dump will be generated in the project root if you set the JVM flag `-XX:+HeapDumpOnOutOfMemoryError`.<br />
&#8211; If the JVM doesn&#8217;t crash fast enough, increase the `byte[]` size in the `OOMGenerator` to consume memory faster.<br />
&#8211; Use `jvisualvm` (or `jhat`) to inspect the &#8220;Heap Histogram&#8221; to see which object type (likely `byte[]`) dominates the heap.</p>
</div>]]></content:encoded>
                                </item>
                <item>
            <title> - Hands-On Tutorial</title>
            <link></link>
            <comments>#respond</comments>
            <pubDate></pubDate>
            <dc:creator><![CDATA[admin]]></dc:creator>
                        <guid isPermaLink="false"></guid>
            <description><![CDATA[# Day 22: Profile Garbage Collection Pauses Using GC Logs and Compare G1GC with ZGC In our previous session, we used JMH to quantify the performance gap between off-heap memory... Hands-On System Design tutorial with practical examples and real-world applications.]]></description>
            <content:encoded><![CDATA[<div class="lesson-rss-content"><h3>Hands-On System Design Tutorial</h3><p data-ai-summary="true"># Day 22: Profile Garbage Collection Pauses Using GC Logs and Compare G1GC with ZGC</p>
<p data-ai-summary="true">In our previous session, we used JMH to quantify the <span data-ai-definition="performance">performance</span> gap between off-heap memory and the standard JVM heap. We saw that while heap storage is easier to manage, it introduces a hidden tax: the Garbage Collector (GC). Today, we move from micro-benchmarking to macro-observability. We will instrument AstraKV to visualize exactly when and why the GC stops our world, and compare the &#8220;stop-the-world&#8221; behavior of the traditional G1GC against the low-latency ZGC.</p>
<p>### The Problem: The &#8220;Death Spiral&#8221; of Latency<br />
In distributed storage systems, tail latency is often more important than average throughput. If 99.9% of your requests take 1ms, but 0.1% take 500ms due to a GC pause, your downstream clients will eventually time out, retry, and trigger a cascading failure. </p>
<p data-ai-summary="true">A famous example of this occurred in the early days of LinkedIn’s transition to large heap JVMs. They observed &#8220;GC pauses&#8221; that grew linearly with heap size, eventually causing nodes to drop out of the cluster because the heartbeat thread couldn&#8217;t get CPU time. They had to abandon large heaps in favor of many smaller JVM instances—a complexity tax paid solely because the GC couldn&#8217;t keep up with the object churn.</p>
<p>### The Mechanism: GC Logs as a System Heartbeat<br />
The JVM provides internal logging to track every reclamation event. By enabling Unified Logging (`-Xlog:gc*`), we move from guessing why a system stalled to having a timestamped audit trail of heap occupancy, promotion rates, and pause durations.</p>
<p data-ai-summary="true">The core concept here is **Pause Time vs. Throughput**. G1GC (Garbage First) is a regionalized collector that prioritizes throughput, often resulting in pauses that correlate with the number of live objects. ZGC (Z Garbage Collector), by contrast, performs almost all its work concurrently with the application threads, keeping pause times sub-millisecond regardless of heap size.</p>
<p>### Architecture: Observing the Lifecycle<br />
In AstraKV, we will configure the JVM to emit structured logs that track the &#8220;Young Gen&#8221; and &#8220;Old Gen&#8221; transitions. </p>
<p>&#8220;`java<br />
// Conceptual snippet: How AstraKV interacts with the GC<br />
// We don&#8217;t trigger GC manually; we monitor it via JMX<br />
public void reportGCMetrics() {<br />
    List<GarbageCollectorMXBean> beans = ManagementFactory.getGarbageCollectorMXBeans();<br />
    for (GarbageCollectorMXBean bean : beans) {<br />
        // This captures the &#8220;Collection Time&#8221; metric we&#8217;ll use in our dashboard<br />
        long totalTime = bean.getCollectionTime();<br />
        long count = bean.getCollectionCount();<br />
        System.out.printf(&#8220;GC: %s, Count: %d, Total Time: %dms%n&#8221;, bean.getName(), count, totalTime);<br />
    }<br />
}<br />
&#8220;`</p>
<p data-ai-summary="true">When you look at these logs, you are looking at the &#8220;breath&#8221; of your system. A healthy system has consistent, short pauses. A system in a &#8220;GC Death Spiral&#8221; shows increasing pause frequency and duration as the heap becomes fragmented or exhausted.</p>
<p>### The Failure Demo: The &#8220;Allocation Wall&#8221;<br />
We will deliberately saturate AstraKV with high-frequency, short-lived object allocations to force the GC into a frenzy. You will observe that G1GC pauses grow noticeably as the heap fills, whereas ZGC maintains a flat latency profile, even as the system struggles to keep up with the allocation rate.</p>
<p>### Trade-offs: Why not use ZGC everywhere?<br />
If ZGC is so fast, why does G1GC still exist? ZGC requires more CPU overhead to maintain its &#8220;colored pointers&#8221; and concurrent marking. In a throughput-heavy batch processing job, G1GC might finish 10% faster overall, even if it has a few 50ms pauses. In a real-time storage engine like AstraKV, we prioritize the sub-millisecond pause.</p>
<p>### Looking Ahead<br />
Today we observe the pauses. Tomorrow, in Day 23, we will intentionally push the heap beyond its limit to trigger an `OutOfMemoryError`, capture a heap dump, and use tools like Eclipse MAT to identify which specific AstraKV objects are pinning our memory and preventing reclamation.</p>
<p data-ai-summary="true">&#8212;</p>
<p>### Assignment<br />
1.**Instrument:** Modify your run-script to include `-Xlog:gc:file=gc.log`.<br />
2.**Observe:** Run the `AstraKVLoadGenerator` (provided in the repo) under both G1GC and ZGC.<br />
3.**Compare:** Use `grep` to find the &#8220;Pause&#8221; duration lines in your `gc.log` files.<br />
4.**Analyze:** Identify the maximum pause time for each collector.</p>
<p>### Solution Hints<br />
*To switch collectors, use `-XX:+UseG1GC` or `-XX:+UseZGC`.<br />
*Look for lines in the GC log containing `Pause Young` or `Pause Remark`.<br />
*The `AstraKVLoadGenerator` uses a memory-heavy workload designed to fill the Eden space rapidly. If you don&#8217;t see pauses, increase the `-Xmx` (heap size) to be smaller to force more frequent collections.</p>
</div>]]></content:encoded>
                                </item>
                <item>
            <title> - Hands-On Tutorial</title>
            <link></link>
            <comments>#respond</comments>
            <pubDate></pubDate>
            <dc:creator><![CDATA[admin]]></dc:creator>
                        <guid isPermaLink="false"></guid>
            <description><![CDATA[# Day 21: Benchmarking the Boundary between Heap and Off-Heap In Day 20, you refactored AstraKV to store its payload segments in `DirectByteBuffer` instances, effectively moving your primary data storage... Hands-On System Design tutorial with practical examples and real-world applications.]]></description>
            <content:encoded><![CDATA[<div class="lesson-rss-content"><h3>Hands-On System Design Tutorial</h3><p data-ai-summary="true"># Day 21: Benchmarking the Boundary between Heap and Off-Heap</p>
<p data-ai-summary="true">In Day 20, you refactored AstraKV to store its payload segments in `DirectByteBuffer` instances, effectively moving your primary data storage out of the JVM&#8217;s garbage-collected heap. By doing this, you successfully reduced the &#8220;stop-the-world&#8221; pause times caused by the GC scanning millions of objects. Today, we confront the reality of that decision: moving data off-heap is not a free lunch. We are trading GC pressure for explicit memory management and potential cache-miss overhead.</p>
<p data-ai-summary="true">To make informed engineering decisions, you cannot rely on intuition or simple `System.currentTimeMillis()` measurements. You need the Java Microbenchmark Harness (JMH).</p>
<p>### The Production Stakes: The &#8220;Hidden&#8221; Latency Tax<br />
In high-throughput systems like Apache Kafka or Cassandra, developers often move data off-heap to manage massive caches (dozens of gigabytes) without triggering long GC cycles. However, there is a catch: if you mismanage your memory lifecycle, you can cause native memory fragmentation or introduce significant latency spikes during allocation. In 2014, the LinkedIn engineering team documented how improper memory management in their storage layers led to &#8220;silent&#8221; latency degradation—where the system didn&#8217;t crash, but P99 latencies climbed because the CPU spent more time managing memory pointers than serving requests. Today, you will build the tool to detect if your off-heap implementation is actually faster, or if you&#8217;ve just traded one <span data-ai-definition="performance">performance</span> tax for another.</p>
<p>### The Concept: Measuring &#8220;Hot&#8221; Code Paths<br />
JMH is designed to prevent the two most common sins of microbenchmarking:<br />
1.**Dead Code Elimination:** If the JVM detects that your benchmark&#8217;s result isn&#8217;t used, it will optimize the entire method away.<br />
2.**Constant Folding:** If the JVM detects your input is constant, it will pre-calculate the result at compile time.</p>
<p data-ai-summary="true">JMH forces the JVM to treat your code as a real, live workload by using `Blackhole` objects to consume results and `@State` objects to simulate concurrent access.</p>
<p>### Architecture: The Benchmark Harness<br />
Our benchmark compares three storage strategies for AstraKV:<br />
1.**Heap-based:** A standard `byte[]` array.<br />
2.**Off-Heap (Direct):** A `DirectByteBuffer`.<br />
3.**Off-Heap (Unsafe):** Raw pointer access via `sun.misc.Unsafe`.</p>
<p>### Implementation Snippet: The Benchmark State<br />
The following snippet shows how we define the state for our benchmark. Note the use of `@Setup` to initialize the memory before the measurement begins, ensuring we aren&#8217;t benchmarking the cost of memory allocation itself.</p>
<p>&#8220;`java<br />
@State(Scope.Thread)<br />
public class StorageBenchmark {<br />
    private ByteBuffer directBuffer;<br />
    private byte[] heapArray;</p>
<p>    @Setup<br />
    public void setup() {<br />
        directBuffer = ByteBuffer.allocateDirect(1024);<br />
        heapArray = new byte[1024];<br />
    }</p>
<p>    @Benchmark<br />
    public byte benchmarkHeap() {<br />
        return heapArray[512];<br />
    }</p>
<p>    @Benchmark<br />
    public byte benchmarkDirect(Blackhole bh) {<br />
        bh.consume(directBuffer.get(512));<br />
        return 0;<br />
    }<br />
}<br />
&#8220;`</p>
<p>### Failure Demo: The &#8220;Memory Leak&#8221; Simulation<br />
In the Implementation Guide, you will trigger a scenario where you allocate native memory within the benchmark loop without calling `cleaner.clean()`. You will observe the resident set size (RSS) of your process climbing until the OS invokes the OOM killer. This demonstrates why off-heap memory is a &#8220;manual&#8221; responsibility: the JVM doesn&#8217;t know it exists, so it cannot reclaim it.</p>
<p>### Forward Link<br />
After today, you will have a clear <span data-ai-definition="performance">performance</span> profile of your storage engine. In **Day 22: Profile Garbage Collection Pauses Using GC Logs and Compare G1GC with ZGC**, we will use this knowledge to tune the JVM&#8217;s GC collector settings to handle the remaining heap objects while your off-heap data sits safely outside the collector&#8217;s reach.</p>
</div>]]></content:encoded>
                                </item>
                <item>
            <title> - Hands-On Tutorial</title>
            <link></link>
            <comments>#respond</comments>
            <pubDate></pubDate>
            <dc:creator><![CDATA[admin]]></dc:creator>
                        <guid isPermaLink="false"></guid>
            <description><![CDATA[# Day 20: Refactor Storage Payloads to Off-Heap ByteBuffers In Day 19, we used `jmap` and `jhat` to visualize how your AstraKV write-load was bloating the JVM heap. You saw... Hands-On System Design tutorial with practical examples and real-world applications.]]></description>
            <content:encoded><![CDATA[<div class="lesson-rss-content"><h3>Hands-On System Design Tutorial</h3><p data-ai-summary="true"># Day 20: Refactor Storage Payloads to Off-Heap ByteBuffers</p>
<p data-ai-summary="true">In Day 19, we used `jmap` and `jhat` to visualize how your AstraKV write-load was bloating the JVM heap. You saw firsthand how millions of small `byte[]` objects created a &#8220;GC tax&#8221;—the garbage collector spent more time tracing your objects than your application spent processing requests. Today, we break that cycle by moving our storage payloads out of the managed heap entirely.</p>
<p>### The Problem: The GC Tax<br />
When you store data on the Java heap, the Garbage Collector (GC) must track every single object. If you have 10 million keys in AstraKV, that’s 10 million objects the GC must scan during every &#8220;mark&#8221; phase. Even if the data is static, the GC overhead scales linearly with the number of objects, not just the volume of data. </p>
<p data-ai-summary="true">In production systems like **Apache Cassandra** or **Kafka**, storing massive volumes of data on the heap leads to &#8220;Stop-the-World&#8221; pauses that can last seconds. When the GC pauses, your network buffers fill up, TCP connections time out, and your p99 latency spikes.</p>
<p>### The Mechanism: Off-Heap Memory<br />
We will use `java.nio.ByteBuffer.allocateDirect()`. This allocates memory in the process&#8217;s native memory space, outside the JVM&#8217;s managed heap. </p>
<p data-ai-summary="true">The JVM GC does not scan this memory. It treats it as an opaque blob. This is a double-edged sword: you get consistent latency regardless of data volume, but you lose automatic memory management. If you allocate off-heap memory and lose the reference, you have a native memory leak that `jmap` won&#8217;t show you. You must manually manage the lifecycle.</p>
<p>### The Architecture: Off-Heap Slab<br />
Instead of creating a new object per write, we will implement a `SlabAllocator`. Think of this like a physical warehouse: you have a large pre-allocated floor space (the `DirectByteBuffer`), and you manage the &#8220;slots&#8221; yourself.</p>
<p>&#8220;`java<br />
// Conceptual snippet: Allocating a slab<br />
public class OffHeapStorage {<br />
    private final ByteBuffer buffer;</p>
<p>    public OffHeapStorage(int capacityBytes) {<br />
        // This memory lives in the OS process space, not the JVM Heap<br />
        this.buffer = ByteBuffer.allocateDirect(capacityBytes);<br />
    }</p>
<p>    public void write(int offset, byte[] data) {<br />
        buffer.put(offset, data);<br />
    }<br />
}<br />
&#8220;`</p>
<p>### Why This Matters<br />
By moving payloads off-heap, your GC frequency should drop significantly. You are trading complexity (manual index management) for predictability. </p>
<p data-ai-summary="true">**Trade-off:** If you require ultra-low latency but have highly variable data sizes, off-heap management becomes complex due to fragmentation. In those cases, a &#8220;pooled&#8221; approach (reusing fixed-size buffers) is often preferred over a contiguous slab.</p>
<p>### The Failure Demo: The Silent Leak<br />
In the Implementation Guide, you will run a load test that fills memory. We will then &#8220;break&#8221; the system by intentionally failing to recycle a buffer. You will observe the process&#8217;s Resident Set Size (RSS) grow in `top` while the JVM heap usage remains flat. This is the hallmark of a native memory leak—the JVM thinks it&#8217;s healthy, but the OS is about to trigger the OOM Killer.</p>
<p>### Production Stakes<br />
This approach mirrors how **Netty** handles network buffers. In high-throughput systems, copying data from the kernel to the JVM heap and back is a <span data-ai-definition="performance">performance</span> killer. By using direct buffers, you enable &#8220;Zero-Copy&#8221; patterns, where the OS can move data directly from the network card to your application memory.</p>
<p data-ai-summary="true">&#8212;</p>
<p>### Assignment<br />
1.Modify your existing `StorageEngine` to use a `DirectByteBuffer` instead of a `HashMap<String, byte[]>`.<br />
2.Implement a simple &#8220;offset-based&#8221; index that maps your keys to a `long` position in the `ByteBuffer`.<br />
3.Verify the reduction in GC activity using `jstat -gc <pid> 1000`.</p>
</div>]]></content:encoded>
                                </item>
                <item>
            <title> - Hands-On Tutorial</title>
            <link></link>
            <comments>#respond</comments>
            <pubDate></pubDate>
            <dc:creator><![CDATA[admin]]></dc:creator>
                        <guid isPermaLink="false"></guid>
            <description><![CDATA[# Day 19: Measuring Heap vs. Stack Allocations Under High Write Load In our previous lesson, we successfully integrated a Concurrent Write-Ahead Log (WAL) with an in-memory index for AstraKV.... Hands-On System Design tutorial with practical examples and real-world applications.]]></description>
            <content:encoded><![CDATA[<div class="lesson-rss-content"><h3>Hands-On System Design Tutorial</h3><p data-ai-summary="true"># Day 19: Measuring Heap vs. Stack Allocations Under High Write Load</p>
<p data-ai-summary="true">In our previous lesson, we successfully integrated a Concurrent Write-Ahead Log (WAL) with an in-memory index for AstraKV. We now have a system that promises durability by flushing writes to disk before acknowledging them. However, as we scale, you will notice that the JVM’s garbage collector (GC) begins to fight your application for CPU cycles. Today, we focus on the &#8220;why&#8221; behind these pauses.</p>
<p>### The Problem: The Hidden Cost of Object Allocation<br />
In Java, we are taught to treat objects as cheap abstractions. In systems engineering, we treat them as memory-pressure events. Every time you create an `Entry` object or a `LogRecord` for your WAL, you are allocating memory on the heap. When that memory is no longer reachable, the GC must traverse the object graph to reclaim it.</p>
<p data-ai-summary="true">At high request rates, this creates &#8220;allocation churn.&#8221; If your allocation rate exceeds the GC’s ability to reclaim, you hit a &#8220;Stop-the-World&#8221; (STW) event. This is the primary reason for tail-latency spikes in systems like Apache Cassandra or Kafka. When the GC pauses, your network buffers fill up, your WAL threads block, and your P99 latency goes off a cliff.</p>
<p>### The Concept: Escape Analysis and Allocation Profiling<br />
The JVM tries to be smart. Through **Escape Analysis**, the Just-In-Time (JIT) compiler determines if an object is only used within a single method. If it is, the JVM can perform &#8220;Scalar Replacement,&#8221; allocating the object&#8217;s fields directly on the stack (or even in registers), bypassing the heap entirely. </p>
<p data-ai-summary="true">If an object &#8220;escapes&#8221; (is returned, stored in a static field, or passed to a long-lived concurrent thread), it must go to the heap. Your goal as a systems engineer is to maximize stack allocation and minimize heap transit.</p>
<p>### The Anchor: The &#8220;Stop-the-World&#8221; Reality<br />
Consider the famous 2012 LinkedIn post-mortem on their transition from Java to C++ for certain components. They hit GC walls where 30-second pauses were common. They weren&#8217;t just &#8220;bad coders&#8221;; they were victims of high-volume object churn that the JVM at the time couldn&#8217;t optimize away. While modern GCs (ZGC, Shenandoah) are better, they are not magic. If you allocate 1GB/sec, you will eventually pay the tax.</p>
<p>### Architecture: Monitoring the Churn<br />
We will use `jstat` and `async-profiler` to visualize this. We are not just looking at &#8220;how much memory is used,&#8221; but &#8220;how much memory is allocated per second.&#8221;</p>
<p>&#8220;`java<br />
// Conceptual snippet: Tracking allocation in the Write Path<br />
public void write(byte[] key, byte[] value) {<br />
    // Each Entry instantiation here is a potential heap allocation<br />
    Entry entry = new Entry(key, value);<br />
    wal.append(entry);<br />
    index.put(key, entry.offset());<br />
}<br />
&#8220;`</p>
<p data-ai-summary="true">If `Entry` escapes the `write` method (which it does, as it’s referenced by the index), the JVM *must* put it on the heap. Understanding this boundary is the difference between a system that handles 10k ops/sec and one that handles 100k.</p>
<p>### The Failure Demo: The GC Death Spiral<br />
In the Implementation Guide, you will run a load test that saturates the system. We will then trigger a configuration change that forces the JVM to perform frequent young-generation GCs. You will observe the &#8220;sawtooth&#8221; pattern of the heap usage and, eventually, a sustained GC pause that causes your client-side latency to jump from 1ms to 500ms.</p>
<p>### Trade-offs: Heap vs. Off-Heap<br />
We choose to use the Heap for now because it is safe and idiomatic. The alternative is manual memory management using `DirectByteBuffers` (Off-Heap). Off-heap is faster and invisible to the GC, but it introduces the risk of memory leaks and segmentation faults—the very things Java was designed to prevent. We will move to Off-Heap in Day 20, but only after we have mastered the art of measuring exactly what the heap is doing.</p>
<p>### Assignment: The Allocation Audit<br />
1.**Run the Load Generator:** Use the provided script to push 50,000 writes/sec into AstraKV.<br />
2.**Observe:** Use `jstat -gc` to watch the `YGC` (Young Generation GC) count increment.<br />
3.**Analyze:** Identify the point where the `Eden` space fills up and triggers a collection.<br />
4.**Break:** Increase the `Entry` size by 10x and observe how the `YGC` frequency increases, directly correlating to the throughput drop.</p>
<p>### Solution Hint<br />
Look at the `Eden` space utilization in `jstat`. If your `Eden` space is 256MB and you are allocating 1GB/sec, you should expect a `YGC` every 250ms. If you see them happening faster, your system is &#8220;allocating&#8221; more than just your data—it&#8217;s allocating temporary objects you didn&#8217;t account for.</p>
</div>]]></content:encoded>
                                </item>
                <item>
            <title> - Hands-On Tutorial</title>
            <link></link>
            <comments>#respond</comments>
            <pubDate></pubDate>
            <dc:creator><![CDATA[admin]]></dc:creator>
                        <guid isPermaLink="false"></guid>
            <description><![CDATA[# Day 18: Integrate the Concurrent Write-Ahead Log with the In-Memory Index In Day 17, we diagnosed thread allocation deadlocks and extracted thread dumps to see where threads got stuck... Hands-On System Design tutorial with practical examples and real-world applications.]]></description>
            <content:encoded><![CDATA[<div class="lesson-rss-content"><h3>Hands-On System Design Tutorial</h3><p data-ai-summary="true"># Day 18: Integrate the Concurrent Write-Ahead Log with the In-Memory Index</p>
<p data-ai-summary="true">In Day 17, we diagnosed thread allocation deadlocks and extracted thread dumps to see where threads got stuck under heavy, uncoordinated resource contention. Today, we transition from diagnosing thread failures to building a highly concurrent, crash-consistent storage engine. </p>
<p data-ai-summary="true">We will integrate two core components of AstraKV: the disk-backed **Write-Ahead Log (WAL)** and the volatile **In-Memory Index (MemTable)**. This integration is the foundation of any crash-resilient <span data-ai-definition="database">database</span>. In Day 13, we will spin up multiple platform threads to hammer this integrated store concurrently, exposing subtle race conditions if our index-WAL boundary is not properly synchronized. Today, we focus on establishing the atomic write path and crash-recovery mechanics that guarantee durability without killing concurrent write throughput.</p>
<p data-ai-summary="true">## The Production Stakes: The Cost of Out-of-Order Writes</p>
<p data-ai-summary="true">When a <span data-ai-definition="database">database</span> client executes a write, the engine must update both the persistent log (WAL) on disk and the fast, sorted index (MemTable) in memory. </p>
<p data-ai-summary="true">In 2015, early deployments of distributed databases frequently suffered from &#8220;phantom updates&#8221; or silent data corruption during sudden power losses. If a system updates the in-memory MemTable *before* the write is fully flushed and fsynced to the WAL, concurrent readers can immediately see the new value. However, if the server crashes a millisecond later, that un-flushed write vanishes from disk. Upon reboot, the <span data-ai-definition="database">database</span> recovers to an older state. The client, which received a success acknowledgment, discovers its data has vanished—a direct violation of ACID durability.</p>
<p data-ai-summary="true">## Deep Dive: The Coordination Engine</p>
<p data-ai-summary="true">Let us examine the core coordinate write path. The class `AstraKVStore` manages this orchestration. We must write to the `FileChannel` sequentially to prevent log corruption, but we want to allow concurrent threads to update the index as soon as their respective disk writes complete.</p>
<p>&#8220;`java<br />
public void put(String key, String value) throws IOException {<br />
    byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);<br />
    byte[] valBytes = value.getBytes(StandardCharsets.UTF_8);</p>
<p>    // Step 1: Append to the physical log under a fine-grained write lock<br />
    long lsn;<br />
    writeLock.lock();<br />
    try {<br />
        lsn = wal.append(keyBytes, valBytes);<br />
    } finally {<br />
        writeLock.unlock();<br />
    }</p>
<p>    // Step 2: Update the in-memory index concurrently without holding the write lock<br />
    memTable.put(key, value);<br />
}<br />
&#8220;`</p>
<p data-ai-summary="true">This design ensures that disk serialization is the only bottleneck. Multiple threads can concurrently traverse and update the lock-free skip-list of the MemTable while another thread is waiting for the OS to complete its disk write.</p>
<p data-ai-summary="true">### Recovery Mechanics: Scanning the Log</p>
<p data-ai-summary="true">Upon startup, the store must transition from an empty state to a fully restored state by parsing the binary WAL. We must handle corrupt or partially written entries at the end of the log (which occur when a crash happens mid-write).</p>
<p>&#8220;`java<br />
public void recover() throws IOException {<br />
    wal.seekToStart();<br />
    while (wal.hasNext()) {<br />
        try {<br />
            LogEntry entry = wal.readNext();<br />
            if (entry.isValid()) {<br />
                memTable.put(entry.key(), entry.value());<br />
            } else {<br />
                // Stop recovery at the first corrupted record (typically a partial write at crash time)<br />
                break;<br />
            }<br />
        } catch (EOFException e) {<br />
            break;<br />
        }<br />
    }<br />
}<br />
&#8220;`</p>
<p data-ai-summary="true">## Assignment: Implement Corrupt Record Detection during Recovery</p>
<p data-ai-summary="true">Currently, if the <span data-ai-definition="database">database</span> crashes mid-write, the WAL might contain trailing garbage bytes. Your task is to implement a strict CRC32 checksum validation inside the recovery path.</p>
<p>### Steps to Complete<br />
1.Open `WriteAheadLog.java` and locate the `readNext()` method.<br />
2.Read the recorded CRC32 checksum from the log entry header.<br />
3.Compute the checksum of the read key and value bytes using `java.util.zip.CRC32`.<br />
4.If the computed checksum does not match the recorded checksum, throw a `CorruptedLogException` rather than returning a corrupt entry.<br />
5.In `AstraKVStore.java`, catch the `CorruptedLogException` during recovery, truncate the log file to the last known valid position to discard the partial write, and complete startup.</p>
<p>### Success Criteria<br />
*Running the recovery test on a truncated, corrupted WAL successfully discards the corrupted trailing bytes.<br />
*The store starts up cleanly with only the valid, pre-crash records present in the index.</p>
<p data-ai-summary="true">&#8212;</p>
<p data-ai-summary="true">## Solution Hints</p>
<p>To calculate the checksum of the key and value:<br />
&#8220;`java<br />
CRC32 crc = new CRC32();<br />
crc.update(keyBytes);<br />
crc.update(valBytes);<br />
long expectedChecksum = crc.getValue();<br />
&#8220;`</p>
<p>When writing to the log, ensure you write the checksum as a 64-bit `long` (8 bytes) into the entry header:<br />
&#8220;`<br />
[ Magic Byte (1B) ] [ Key Length (4B) ] [ Value Length (4B) ] [ Checksum (8B) ] [ Key Bytes ] [ Value Bytes ]<br />
&#8220;`</p>
<p>During recovery, if the checksum fails, truncate the file channel to the position of the start of the corrupt record:<br />
&#8220;`java<br />
fileChannel.truncate(lastValidPosition);<br />
&#8220;`</p>
</div>]]></content:encoded>
                                </item>
                
    </channel>
    </rss>
    