<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Arun Lakshman's Blog]]></title><description><![CDATA[Arun Lakshman's Blog]]></description><link>https://www.arunlakshman.info</link><image><url>https://substackcdn.com/image/fetch/$s_!xHWo!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e07aebe-1d86-4841-a6f5-b8edb883bf2f_608x608.png</url><title>Arun Lakshman&apos;s Blog</title><link>https://www.arunlakshman.info</link></image><generator>Substack</generator><lastBuildDate>Sat, 13 Jun 2026 07:22:15 GMT</lastBuildDate><atom:link href="https://www.arunlakshman.info/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Arun Lakshman R]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[arunlakshman@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[arunlakshman@substack.com]]></itunes:email><itunes:name><![CDATA[Arun Lakshman R]]></itunes:name></itunes:owner><itunes:author><![CDATA[Arun Lakshman R]]></itunes:author><googleplay:owner><![CDATA[arunlakshman@substack.com]]></googleplay:owner><googleplay:email><![CDATA[arunlakshman@substack.com]]></googleplay:email><googleplay:author><![CDATA[Arun Lakshman R]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[AWS EC2 : What's Running Underneath]]></title><description><![CDATA[Every developer who&#8217;s worked with AWS has launched an EC2 instance.]]></description><link>https://www.arunlakshman.info/p/aws-ec2-whats-running-underneath</link><guid isPermaLink="false">https://www.arunlakshman.info/p/aws-ec2-whats-running-underneath</guid><dc:creator><![CDATA[Arun Lakshman R]]></dc:creator><pubDate>Fri, 12 Jun 2026 10:01:23 GMT</pubDate><enclosure url="https://substackcdn.com/image/youtube/w_728,c_limit/LabltEXk0VQ" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Every developer who&#8217;s worked with AWS has launched an EC2 instance. You pick an instance type, choose an AMI, SSH in, and deploy your app. Somewhere in the back of your mind, you know there&#8217;s virtualization happening. But that&#8217;s where most people stop thinking about it.</p><p>Here&#8217;s what might surprise you: when AWS launched EC2 in August 2006, every instance ran on <a href="https://en.wikipedia.org/wiki/Xen">Xen</a> - an open-source Type 1 bare-metal hypervisor originally created by <a href="https://en.wikipedia.org/wiki/Ian_Pratt_(computer_scientist)">Ian Pratt</a> and Keir Fraser at the <a href="https://en.wikipedia.org/wiki/University_of_Cambridge">University of Cambridge</a> in 2003. Then, starting around 2017 with the C5 instance family, AWS began migrating to <a href="https://en.wikipedia.org/wiki/Nitro_(microprocessor)">Nitro</a>: a custom platform built on <a href="https://en.wikipedia.org/wiki/Kernel-based_Virtual_Machine">KVM</a>, which is a Type 2 hosted hypervisor. In the textbook hierarchy, Type 1 sits closer to hardware and is considered superior. So why would AWS move down a tier?</p><p>The answer is that the Type 1 vs Type 2 distinction is misleading. What actually matters is where I/O is handled. And Nitro solved that problem in dedicated hardware, making the hypervisor classification almost irrelevant.</p><h2><strong>Glossary</strong></h2><p>Before we dive in, here are the key terms you&#8217;ll encounter throughout this post</p><p><strong>Hypervisor </strong>Software (or firmware) that creates and manages virtual machines. It sits between the physical hardware and the guest operating systems, dividing resources among them.</p><p><strong>Type 1 (bare-metal) hypervisor </strong>A hypervisor that runs directly on physical hardware with no host operating system underneath. Examples: Xen, VMware ESXi.</p><p><strong>Type 2 (hosted) hypervisor </strong>A hypervisor that runs as software inside a conventional operating system. Examples: KVM (inside Linux), VirtualBox (inside Windows/macOS).<strong>Virtual </strong></p><p><strong>Machine (VM) </strong>A software emulation of a complete computer. It has its own CPU, memory, disk, and network - all virtual - and runs its own operating system.<strong>Guest </strong></p><p><strong>OS </strong>The operating system running inside a virtual machine. It believes it&#8217;s running on real hardware (unless paravirtualized).</p><p><strong>Host OS </strong>The operating system running on the physical machine that hosts the hypervisor and virtual machines.</p><p><strong>Paravirtualization </strong>A technique where the guest OS knows it&#8217;s virtualized and uses special lightweight drivers to communicate with the hypervisor, instead of the hypervisor emulating real hardware. Faster, but requires guest modification.</p><p><strong>Full virtualization </strong>The guest OS runs unmodified, believing it&#8217;s on real hardware. The hypervisor intercepts and translates privileged instructions. Slower than paravirtualization, but compatible with any OS.</p><p><strong>KVM (Kernel-based Virtual Machine) </strong>A Linux kernel module that turns Linux into a hypervisor. Handles CPU and memory virtualization using hardware extensions.</p><p><strong>QEMU (Quick Emulator) </strong>A userspace program that emulates I/O devices (disks, NICs, USB, etc.) for virtual machines. Often paired with KVM - KVM handles CPU/memory, QEMU handles everything else.</p><p><strong>Dom0 (Domain 0) </strong>In Xen, the first and most privileged virtual machine that boots. It runs Linux, has direct hardware access, and manages all other VMs.</p><p><strong>DomU (Domain U) </strong>In Xen, an unprivileged guest virtual machine. The &#8220;U&#8221; stands for unprivileged. It has no direct hardware access and relies on Dom0 for I/O.</p><p><strong>VMkernel </strong>ESXi&#8217;s custom operating system kernel. Not Linux - VMware built it from scratch to handle scheduling, networking, storage, and device drivers within the hypervisor itself.</p><p><strong>Hypercall </strong>A system call from a guest OS to the hypervisor, analogous to a syscall from a userspace program to the kernel. Used in paravirtualization.</p><p><strong>VT-x / AMD-V </strong>Hardware virtualization extensions built into Intel and AMD processors. They allow the CPU to natively run guest code without software emulation of privileged instructions.</p><p><strong>EPT / NPT </strong>Extended Page Tables (Intel) / Nested Page Tables (AMD). Hardware extensions for memory virtualization that let the CPU translate guest virtual addresses to physical addresses without hypervisor intervention on every memory access.</p><p><strong>NVMe </strong>Non-Volatile Memory Express. A protocol for accessing storage devices over PCIe. In the Nitro context, EBS volumes appear as NVMe devices to the guest.</p><p><strong>ENA </strong>Elastic Network Adapter. AWS&#8217;s custom network driver for Nitro instances, replacing Xen&#8217;s paravirtual network frontend.</p><p><strong>PCIe </strong>Peripheral Component Interconnect Express. The standard high-speed bus for connecting hardware devices (GPUs, NICs, storage controllers) to a CPU.</p><p><strong>SR-IOV </strong>Single Root I/O Virtualization. A hardware standard that allows a single physical PCIe device to present itself as multiple virtual devices, each assignable directly to a VM - bypassing the hypervisor for I/O.</p><div><hr></div><h2><strong>The Setup</strong></h2><p>Xen was the natural choice for early EC2: it was open source (<a href="https://en.wikipedia.org/wiki/GNU_General_Public_License#Version_2">GPLv2</a>), battle-tested, and designed from the ground up to run multiple operating systems on a single physical machine. Amazon could take it, modify it, and build a cloud on top without licensing fees or vendor lock-in.</p><p>KVM itself was created by <a href="https://en.wikipedia.org/wiki/Avi_Kivity">Avi Kivity</a> at <a href="https://en.wikipedia.org/wiki/Qumranet">Qumranet</a> in 2006 and merged into the <a href="https://en.wikipedia.org/wiki/Linux_kernel">Linux kernel</a> (version 2.6.20) in February 2007. AWS acquired <a href="https://en.wikipedia.org/wiki/Annapurna_Labs">Annapurna Labs</a>, an Israeli chip design company, in January 2015 for approximately $350 million - and Annapurna became the team that built the Nitro hardware cards.</p><div><hr></div><h2><strong>Pillar 1: What Each Virtualization Layer Actually Does</strong></h2><p>If you want a visual primer on how virtualization works before we go deeper, this is a solid overview:</p><div id="youtube2-LabltEXk0VQ" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;LabltEXk0VQ&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/LabltEXk0VQ?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><p>To understand why the hypervisor type matters less than you think, you first need to understand what each piece of the virtualization stack is responsible for.</p><p>KVM virtualizes CPU execution and memory management. It&#8217;s a Linux kernel module that leverages hardware extensions - <a href="https://en.wikipedia.org/wiki/X86_virtualization#Intel_virtualization_(VT-x)">Intel VT-x</a> (introduced in November 2005 with the <a href="https://en.wikipedia.org/wiki/Pentium_4">Pentium 4</a> 662/672) and <a href="https://en.wikipedia.org/wiki/X86_virtualization#AMD_virtualization_(AMD-V)">AMD-V</a> (introduced in May 2006 with the <a href="https://en.wikipedia.org/wiki/Athlon_64">Athlon 64</a>) - to run guest code directly on the physical CPU. Before these extensions, hypervisors had to use complex <a href="https://en.wikipedia.org/wiki/Binary_translation">binary translation</a> techniques to trap and emulate privileged instructions. With VT-x/AMD-V, the CPU itself understands the concept of a guest and a host, switching between them in hardware. For compute-bound work, the overhead is near zero.</p><p>Memory virtualization followed a similar path. Early hypervisors maintained &#8220;<a href="https://en.wikipedia.org/wiki/Shadow_page_table">shadow page tables</a>&#8220; - a software layer that translated guest virtual addresses to host physical addresses, intercepting every page table update. This was expensive. <a href="https://en.wikipedia.org/wiki/Second_Level_Address_Translation#EPT">Intel EPT</a> (introduced in 2008 with the <a href="https://en.wikipedia.org/wiki/Nehalem_(microarchitecture)">Nehalem architecture</a>) and AMD NPT (introduced in 2007 with <a href="https://en.wikipedia.org/wiki/AMD_10h">Barcelona</a>) moved this translation into hardware, letting the CPU walk <a href="https://en.wikipedia.org/wiki/Second_Level_Address_Translation">nested page tables</a> without hypervisor intervention.</p><p><a href="https://en.wikipedia.org/wiki/QEMU">QEMU</a> (Quick Emulator), originally written by <a href="https://en.wikipedia.org/wiki/Fabrice_Bellard">Fabrice Bellard</a> in 2003, emulates I/O devices - virtual disk controllers, network cards, USB devices, graphics adapters, and so on. It presents what looks like real hardware to the guest. Each VM is a QEMU process running in userspace on the host. Before KVM existed, QEMU could do full system emulation entirely in software - including CPU emulation - but it was slow. The KVM+QEMU pairing splits the work: KVM handles the fast path (CPU and memory in kernel space), QEMU handles the complex path (device emulation in userspace).</p><p>But here&#8217;s the part people miss: you still need a host OS. KVM is a kernel module - it&#8217;s not a standalone program. It depends on Linux&#8217;s <a href="https://en.wikipedia.org/wiki/Completely_Fair_Scheduler">CFS (Completely Fair Scheduler)</a> to schedule VCPUs (each VCPU is just a Linux thread). It depends on Linux&#8217;s memory manager for page tables, <a href="https://en.wikipedia.org/wiki/Non-uniform_memory_access">NUMA</a> awareness, <a href="https://en.wikipedia.org/wiki/Huge_page">hugepages</a>, and <a href="https://en.wikipedia.org/wiki/Kernel_same-page_merging">KSM (Kernel Same-page Merging)</a> (which deduplicates identical memory pages across VMs). QEMU is a regular process that makes syscalls for file I/O, networking, and signal handling. Without Linux underneath, neither can function. If you run <code>ps aux</code> on a KVM host, you&#8217;ll see one QEMU process per VM, just like any other program.</p><p>And you still need a guest OS. KVM and QEMU together build you a virtual computer - CPU, memory, disk, NIC, all virtualized. But a computer with no operating system is just hardware sitting idle. Something still has to:</p><ul><li><p>Boot up and initialize the virtual hardware</p></li><li><p>Load drivers for the virtual devices QEMU presents</p></li><li><p>Implement a filesystem (<a href="https://en.wikipedia.org/wiki/Ext4">ext4</a>, <a href="https://en.wikipedia.org/wiki/XFS">XFS</a>, <a href="https://en.wikipedia.org/wiki/NTFS">NTFS</a>) on the virtual disk</p></li><li><p>Provide a <a href="https://en.wikipedia.org/wiki/Internet_protocol_suite">TCP/IP</a> networking stack</p></li><li><p>Offer a kernel that applications can make syscalls against</p></li><li><p>Manage processes, users, permissions, and libraries</p></li></ul><p>Virtual hardware still needs software to run on it. (This is also why containers became popular - for many workloads, you can skip the guest OS entirely by sharing the host kernel. <a href="https://en.wikipedia.org/wiki/Docker_(software)">Docker</a>, released in March 2013, and <a href="https://en.wikipedia.org/wiki/Firecracker_(software)">Firecracker</a>, open-sourced in November 2018, both exploit this insight.)</p><div><hr></div><h2><strong>Pillar 2: Type 1 vs Type 2 - And Why the Line Is Blurry</strong></h2><p>The textbook distinction, formalized by <a href="https://en.wikipedia.org/wiki/Gerald_J._Popek">Gerald J. Popek</a> and <a href="https://en.wikipedia.org/wiki/Robert_P._Goldberg">Robert P. Goldberg</a> in their 1974 paper &#8220;<a href="https://en.wikipedia.org/wiki/Popek_and_Goldberg_virtualization_requirements">Formal Requirements for Virtualizable Third Generation Architectures</a>,&#8221; is clean:</p><ul><li><p><strong>Type 1 (bare-metal):</strong> The hypervisor runs directly on hardware. No host OS. It manages hardware resources and guest VMs itself.</p></li><li><p><strong>Type 2 (hosted):</strong> The hypervisor runs as software inside a conventional operating system. It depends on the host OS for hardware access.</p></li></ul><p>Type 1 hypervisors introduced an important concept: <a href="https://en.wikipedia.org/wiki/Paravirtualization">paravirtualization</a>. The term was coined by the Xen team in their 2003 <a href="https://en.wikipedia.org/wiki/Symposium_on_Operating_Systems_Principles">SOSP</a> paper &#8220;Xen and the Art of Virtualization.&#8221; Instead of tricking the guest into thinking it&#8217;s on real hardware (full virtualization), the guest knows it&#8217;s virtualized and cooperates with the hypervisor. Xen&#8217;s guests used lightweight &#8220;frontend&#8221; drivers - blkfront for block devices, netfront for networking - that communicated with &#8220;backend&#8221; drivers in Dom0 through shared memory ring buffers and event channels. No hardware emulation, no trap-and-emulate overhead. This was critical in 2003 because hardware virtualization extensions (VT-x/AMD-V) didn&#8217;t exist yet - paravirtualization was the only way to get acceptable performance.</p><p>Type 2 guests, by contrast, are typically unaware they&#8217;re virtualized. QEMU emulates a complete hardware environment - an Intel <a href="https://en.wikipedia.org/wiki/Intel_PRO/1000">e1000</a> NIC, an <a href="https://en.wikipedia.org/wiki/Parallel_ATA">IDE</a> or <a href="https://en.wikipedia.org/wiki/SCSI">SCSI</a> disk controller - and the guest runs its standard drivers against what it believes is real hardware.</p><p>But here&#8217;s where it gets interesting. The Linux kernel ships with both Xen guest drivers and KVM host code in the same binary. The <code>drivers/xen/</code> directory contains the paravirtual frontend drivers for running as a guest on Xen - these were merged upstream between 2007 and 2009 through a sustained effort by the Xen community, particularly Jeremy Fitzhardinge and others at <a href="https://en.wikipedia.org/wiki/XenSource">XenSource</a> (later acquired by <a href="https://en.wikipedia.org/wiki/Citrix_Systems">Citrix</a> in 2007 for $500 million). The <code>virt/kvm/</code> directory contains the code that makes Linux a hypervisor, merged in February 2007. They coexist peacefully.</p><p>At boot, the kernel detects what&#8217;s underneath:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;a96958ef-2973-48b2-96e8-28ec22e0d43e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext"># On a Xen instance:
dmesg | grep -i hypervisor
&gt; Hypervisor detected: Xen

# On a KVM/Nitro instance:
dmesg | grep -i hypervisor
&gt; Hypervisor detected: KVM
</code></pre></div><p>The same kernel image works on bare metal, on Xen, or on KVM without modification. It simply activates the right code path based on what it finds.</p><p>And KVM itself blurs the Type 1/Type 2 line. Yes, it runs inside Linux. But it operates in kernel space (<a href="https://en.wikipedia.org/wiki/Protection_ring">ring 0</a>) with direct access to hardware virtualization extensions. It doesn&#8217;t emulate a CPU - it runs guest code natively using VT-x/AMD-V. The guest enters a special CPU mode (VMX non-root on Intel), executes at near-native speed, and only exits back to the hypervisor (&#8221;VM exit&#8221;) when it does something that requires intervention. Performance benchmarks consistently put KVM alongside Type 1 hypervisors. Some people call it &#8220;Type 1.5,&#8221; which tells you the classification system from 1974 doesn&#8217;t map cleanly onto modern architectures.</p><div><hr></div><h2><strong>Pillar 3: I/O Is the Real Differentiator</strong></h2><p>If CPU and memory virtualization are essentially solved by hardware extensions, then the real question becomes: who handles I/O, and how?</p><p>Each major hypervisor answered this differently, and the differences reveal where performance is actually won or lost.</p><p><strong>Xen</strong> used Dom0 as an I/O proxy. When Xen boots on a physical machine, the first thing it launches is Dom0 - a privileged Linux VM with direct access to all physical hardware. Dom0 runs real Linux device drivers: the actual Intel NIC driver, the actual SATA controller driver, everything. Every unprivileged guest (DomU) that wants to read a disk block or send a network packet goes through Dom0:</p><ol><li><p>The guest&#8217;s frontend driver (blkfront) places a request into a shared memory ring buffer</p></li><li><p>An event channel notifies Dom0</p></li><li><p>Dom0&#8217;s backend driver (blkback) picks up the request</p></li><li><p>Dom0 talks to the real hardware using standard Linux drivers</p></li><li><p>The result travels back through the shared memory ring</p></li></ol><p>This was elegant - Xen itself stayed tiny (around 150,000 lines of code in early versions), and it reused Linux&#8217;s entire driver ecosystem through Dom0. But Dom0 was a bottleneck. It consumed CPU and memory on every physical host just to proxy I/O. Under heavy I/O load, Dom0 could become saturated. And it was a single point of failure - if Dom0 crashed, every VM on that host lost I/O.</p><p><strong><a href="https://en.wikipedia.org/wiki/VMware_ESXi">ESXi</a></strong> took the monolithic approach. <a href="https://en.wikipedia.org/wiki/VMware">VMware</a>, founded in 1998 by <a href="https://en.wikipedia.org/wiki/Diane_Greene">Diane Greene</a>, <a href="https://en.wikipedia.org/wiki/Mendel_Rosenblum">Mendel Rosenblum</a>, and others at Stanford, released ESX Server in 2001 and the thin ESXi variant in 2007. VMware built their own mini operating system from scratch - the VMkernel - with its own scheduler, its own TCP/IP stack, its own filesystem (<a href="https://en.wikipedia.org/wiki/VMware_VMFS">VMFS</a>, a clustered filesystem designed for VM disk images), and its own device drivers. No Dom0, no Linux, no middleman. The hypervisor is the I/O layer. ESXi installs from a ~150MB ISO.</p><p>The upside: fewer layers, lower latency, no I/O proxy bottleneck. The downside: VMware has to write and maintain drivers for every piece of hardware they support, which is why they publish a strict Hardware Compatibility List (HCL). You can&#8217;t just plug in any NIC and expect it to work - it needs a VMware driver.</p><p><strong>KVM/QEMU</strong> delegates I/O to userspace. Each VM&#8217;s QEMU process emulates virtual devices and translates I/O operations into host Linux syscalls. Guest writes to virtual disk &#8594; QEMU catches it &#8594; QEMU calls <code>pwrite()</code> on the host &#8594; Linux kernel handles the actual disk I/O. It&#8217;s flexible and benefits from Linux&#8217;s entire driver ecosystem, but there&#8217;s overhead in the userspace-to-kernel context switches. Technologies like <a href="https://en.wikipedia.org/wiki/Virtio">virtio</a> (a standardized paravirtual I/O framework, proposed by <a href="https://en.wikipedia.org/wiki/Rusty_Russell">Rusty Russell</a> in 2007 and merged into Linux 2.6.25) reduced this overhead significantly by giving guests lightweight drivers that cooperate with QEMU, similar in spirit to Xen&#8217;s frontend/backend model.</p><p>Notice the pattern: in every case, the performance bottleneck isn&#8217;t CPU virtualization - hardware extensions made that nearly free. It&#8217;s the I/O path. Dom0 proxying, VMkernel processing, QEMU translating - that&#8217;s where the latency lives.</p><div><hr></div><h2><strong>Pillar 4: How Nitro Made the Hypervisor Type Irrelevant</strong></h2><p>AWS saw this clearly. The problem was never &#8220;Type 1 vs Type 2.&#8221; The problem was that I/O was handled in software, and software I/O has overhead no matter how you architect it.</p><p>The Nitro journey happened in stages:</p><ul><li><p><strong>2013:</strong> AWS introduced enhanced networking using <a href="https://en.wikipedia.org/wiki/Single-root_input/output_virtualization">SR-IOV</a> (Single Root I/O Virtualization) on C3 instances. SR-IOV is a <a href="https://en.wikipedia.org/wiki/PCI_Express">PCIe</a> hardware standard (ratified in 2007 by the <a href="https://en.wikipedia.org/wiki/PCI-SIG">PCI-SIG</a>) that allows a single physical NIC to present multiple virtual functions, each assignable directly to a VM. This bypassed Dom0 for networking - the guest talked directly to a virtual function on the physical NIC. It was the first crack in Dom0&#8217;s monopoly on I/O.</p></li><li><p><strong>January 2015:</strong> AWS acquired Annapurna Labs for ~$350 million. Annapurna, founded in 2011 in Yokneam, Israel, by <a href="https://en.wikipedia.org/wiki/Avigdor_Willenz">Avigdor Willenz</a> (who had previously founded <a href="https://en.wikipedia.org/wiki/Galileo_Technology">Galileo Technology</a> and <a href="https://en.wikipedia.org/wiki/Marvell_Technology">Marvell</a>), specialized in custom <a href="https://en.wikipedia.org/wiki/ARM_architecture_family">ARM-based</a> <a href="https://en.wikipedia.org/wiki/System_on_a_chip">SoCs</a>. This acquisition gave AWS the silicon design capability to build custom I/O hardware.</p></li><li><p><strong>2016:</strong> The Nitro card for EBS appeared, offloading storage I/O from the host CPU to a dedicated hardware card. No more Dom0 or QEMU in the storage path.</p></li><li><p><strong>2017:</strong> AWS launched the C5 instance family - the first instance type running on the full Nitro platform. The hypervisor was KVM-based. Networking was handled by the Nitro card for VPC (with <a href="https://en.wikipedia.org/wiki/Elastic_Network_Adapter">ENA</a> drivers). Storage was handled by the Nitro card for EBS (with <a href="https://en.wikipedia.org/wiki/NVM_Express">NVMe</a> drivers). Security and management ran on the Nitro security chip. The host CPU ran a minimal KVM hypervisor that handled only CPU and memory isolation.</p></li><li><p><strong>2018:</strong> AWS open-sourced <a href="https://en.wikipedia.org/wiki/Firecracker_(software)">Firecracker</a>, the <a href="https://en.wikipedia.org/wiki/Microvm">microVM</a> monitor built on KVM that powers Lambda and Fargate. Firecracker boots a VM in ~125 milliseconds with ~5MB of memory overhead - demonstrating just how thin the virtualization layer can be when I/O is handled elsewhere.</p></li><li><p><strong>2023:</strong> AWS announced Nitro v5 with further performance improvements and the Nitro <a href="https://en.wikipedia.org/wiki/Trusted_Platform_Module">Trusted Platform Module (TPM)</a> for enhanced security.</p></li></ul><p>The architecture shift looks like this</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!-MX4!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4c552aa-552d-4c9b-a0a3-34cb84e8a914_1836x504.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!-MX4!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4c552aa-552d-4c9b-a0a3-34cb84e8a914_1836x504.png 424w, https://substackcdn.com/image/fetch/$s_!-MX4!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4c552aa-552d-4c9b-a0a3-34cb84e8a914_1836x504.png 848w, https://substackcdn.com/image/fetch/$s_!-MX4!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4c552aa-552d-4c9b-a0a3-34cb84e8a914_1836x504.png 1272w, https://substackcdn.com/image/fetch/$s_!-MX4!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4c552aa-552d-4c9b-a0a3-34cb84e8a914_1836x504.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!-MX4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4c552aa-552d-4c9b-a0a3-34cb84e8a914_1836x504.png" width="1456" height="400" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b4c552aa-552d-4c9b-a0a3-34cb84e8a914_1836x504.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:400,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:288430,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.arunlakshman.info/i/200825070?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4c552aa-552d-4c9b-a0a3-34cb84e8a914_1836x504.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!-MX4!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4c552aa-552d-4c9b-a0a3-34cb84e8a914_1836x504.png 424w, https://substackcdn.com/image/fetch/$s_!-MX4!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4c552aa-552d-4c9b-a0a3-34cb84e8a914_1836x504.png 848w, https://substackcdn.com/image/fetch/$s_!-MX4!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4c552aa-552d-4c9b-a0a3-34cb84e8a914_1836x504.png 1272w, https://substackcdn.com/image/fetch/$s_!-MX4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4c552aa-552d-4c9b-a0a3-34cb84e8a914_1836x504.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Dom0 is gone. QEMU is not in the I/O path. The hypervisor is so thin it barely exists. And the migration from Xen to KVM was transparent to customers because - as we covered in Pillar 2 - the Linux kernel already carried both Xen guest drivers and KVM support. Existing AMIs worked without modification. The kernel detected KVM instead of Xen at boot and activated the right code path. Customers on newer instance types saw NVMe and ENA devices instead of Xen paravirtual devices, but those drivers were already in the kernel too.</p><p>Nobody had to rebuild their AMI. Nobody had to change their deployment scripts. The entire hypervisor substrate changed underneath millions of running workloads, and the abstraction held.</p><div><hr></div><h2><strong>The Takeaway</strong></h2><p>The Type 1 vs Type 2 classification made sense in 1974 when Popek and Goldberg formalized it, and it still made sense in 2003 when the choice between Xen and VMware Workstation was a meaningful architectural decision. But hardware virtualization extensions leveled the playing field for CPU and memory. What remained was the I/O problem - and that turned out to be a hardware design problem, not a software classification problem.</p><p>AWS didn&#8217;t move &#8220;down&#8221; from Type 1 to Type 2. They moved the thing that actually mattered - I/O - into dedicated silicon, and made the hypervisor layer so thin that its classification became academic. The question isn&#8217;t &#8220;Type 1 or Type 2?&#8221; The question is &#8220;where does I/O happen?&#8221; And if the answer is &#8220;in purpose-built hardware,&#8221; the hypervisor type barely matters.</p><p>The next time you launch an EC2 instance, you&#8217;re not just running a VM. You&#8217;re running on a decade of architectural decisions - from a Cambridge research project in 2003, through an Israeli chip startup acquisition in 2015, to custom silicon that made the oldest debate in virtualization irrelevant.</p>]]></content:encoded></item><item><title><![CDATA[Inside Flink’s Control Plane: How Apache Pekko Powers the RPC Layer]]></title><description><![CDATA[Flink&#8217;s distributed components must communicate constantly.]]></description><link>https://www.arunlakshman.info/p/inside-flinks-control-plane-how-apache</link><guid isPermaLink="false">https://www.arunlakshman.info/p/inside-flinks-control-plane-how-apache</guid><dc:creator><![CDATA[Arun Lakshman R]]></dc:creator><pubDate>Fri, 05 Jun 2026 19:00:47 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!4zgA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff4ef5a9b-c390-4a1d-98ff-8efd29f894a6_2086x1834.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p></p><p>Flink&#8217;s distributed components must communicate constantly. TaskManagers report task state changes to JobMaster. JobMaster requests slots from ResourceManager. Dispatchers serve REST API queries about job status. All these components access shared state, particularly the ExecutionGraph. Traditional multi-threading with locks would create race conditions, deadlocks, and unmaintainable code. Flink solves this by adopting the Actor Model through the Akka/Pekko framework. Each component processes all requests on a single thread through a FIFO mailbox. This design eliminates concurrency bugs by architecture, not by locks.</p><h2><strong>The Problem: Distributed Components and Shared State</strong></h2><h3><strong>Why Components Must Communicate</strong></h3><p>Flink&#8217;s runtime consists of distributed components that exchange messages continuously. The table below shows the primary RPC interactions in a running Flink cluster.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ObSD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e69c652-d631-447b-8d85-d74b67b85902_740x384.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ObSD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e69c652-d631-447b-8d85-d74b67b85902_740x384.png 424w, https://substackcdn.com/image/fetch/$s_!ObSD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e69c652-d631-447b-8d85-d74b67b85902_740x384.png 848w, https://substackcdn.com/image/fetch/$s_!ObSD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e69c652-d631-447b-8d85-d74b67b85902_740x384.png 1272w, https://substackcdn.com/image/fetch/$s_!ObSD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e69c652-d631-447b-8d85-d74b67b85902_740x384.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ObSD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e69c652-d631-447b-8d85-d74b67b85902_740x384.png" width="740" height="384" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8e69c652-d631-447b-8d85-d74b67b85902_740x384.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:384,&quot;width&quot;:740,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:231960,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.arunlakshman.info/i/200802412?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e69c652-d631-447b-8d85-d74b67b85902_740x384.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ObSD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e69c652-d631-447b-8d85-d74b67b85902_740x384.png 424w, https://substackcdn.com/image/fetch/$s_!ObSD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e69c652-d631-447b-8d85-d74b67b85902_740x384.png 848w, https://substackcdn.com/image/fetch/$s_!ObSD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e69c652-d631-447b-8d85-d74b67b85902_740x384.png 1272w, https://substackcdn.com/image/fetch/$s_!ObSD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e69c652-d631-447b-8d85-d74b67b85902_740x384.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>These interactions happen thousands of times per second in a production cluster. A single JobMaster coordinates with hundreds of TaskManagers. Each TaskManager runs dozens of tasks. Every task state change, checkpoint acknowledgment, and heartbeat flows through this RPC layer.</p><h3><strong>The Shared State Challenge</strong></h3><p>The <a href="https://github.com/apache/flink/blob/master/flink-runtime/src/main/java/org/apache/flink/runtime/executiongraph/ExecutionGraph.java">ExecutionGraph</a> sits at the center of JobMaster. It tracks the complete state of job execution: which tasks are running, which have finished, which checkpoints are in progress, and which resources are allocated. Multiple components access ExecutionGraph for different purposes.</p><p>TaskManagers update ExecutionGraph when they report state changes. A task transitions from DEPLOYING to RUNNING. Another task finishes and transitions to FINISHED. Each update modifies the graph&#8217;s internal state.</p><p>The <a href="https://github.com/apache/flink/blob/master/flink-runtime/src/main/java/org/apache/flink/runtime/checkpoint/CheckpointCoordinator.java">CheckpointCoordinator</a> reads ExecutionGraph to trigger checkpoints. It iterates through all execution vertices. It sends checkpoint barriers to each task. It tracks acknowledgments as they arrive.</p><p>The Dispatcher serves REST API queries. A user requests job status. The Dispatcher reads ExecutionGraph to return current state. Another user requests checkpoint details. The Dispatcher reads checkpoint metrics from the same graph.</p><h3><strong>What Breaks Without Protection</strong></h3><p>Consider what happens if these operations execute concurrently without protection. Thread 1 iterates through ExecutionGraph vertices to trigger a checkpoint. Thread 2 updates a task&#8217;s state, modifying the vertex collection. Thread 1&#8217;s iterator becomes invalid. The JVM throws <code>ConcurrentModificationException</code>. The checkpoint fails.</p><p>The alternative is worse. Without an exception, Thread 1 reads partially updated state. It triggers checkpoints on some tasks but misses others. It sees a task as RUNNING when it has already FINISHED. The checkpoint completes with inconsistent state. Data corruption follows.</p><p>Traditional solutions require locks. Every method that reads ExecutionGraph acquires a read lock. Every method that writes acquires a write lock. The code becomes littered with <code>lock.readLock().lock()</code> and <code>lock.writeLock().lock()</code> calls. Developers must remember to release locks in finally blocks. They must avoid nested lock acquisitions that cause deadlocks. They must reason about every possible thread interleaving across hundreds of methods.</p><p>This approach does not scale. Lock contention becomes a performance bottleneck. Debugging deadlocks in production takes days. New engineers introduce subtle race conditions because they forgot to acquire a lock in one code path.</p><div><hr></div><h2><strong>The Solution: Actor Model via Akka/Pekko</strong></h2><p>Flink adopts the Actor Model to eliminate these concurrency challenges. The Actor Model, popularized by Erlang and implemented in Java by Akka (now Apache Pekko), provides a simple guarantee: each actor processes one message at a time on a single thread. This guarantee makes shared state access inherently thread-safe without locks.</p><h3><strong>Core Mechanism: Single Thread Execution</strong></h3><p>The fundamental insight is simple. Instead of allowing multiple threads to access shared state concurrently, route all access through a single thread. Messages from different callers queue up in a mailbox. A single worker thread processes them one at a time in FIFO order. No two messages execute concurrently. No race conditions are possible.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4zgA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff4ef5a9b-c390-4a1d-98ff-8efd29f894a6_2086x1834.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4zgA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff4ef5a9b-c390-4a1d-98ff-8efd29f894a6_2086x1834.png 424w, https://substackcdn.com/image/fetch/$s_!4zgA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff4ef5a9b-c390-4a1d-98ff-8efd29f894a6_2086x1834.png 848w, https://substackcdn.com/image/fetch/$s_!4zgA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff4ef5a9b-c390-4a1d-98ff-8efd29f894a6_2086x1834.png 1272w, https://substackcdn.com/image/fetch/$s_!4zgA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff4ef5a9b-c390-4a1d-98ff-8efd29f894a6_2086x1834.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4zgA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff4ef5a9b-c390-4a1d-98ff-8efd29f894a6_2086x1834.png" width="1456" height="1280" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f4ef5a9b-c390-4a1d-98ff-8efd29f894a6_2086x1834.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1280,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:544529,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.arunlakshman.info/i/200802412?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff4ef5a9b-c390-4a1d-98ff-8efd29f894a6_2086x1834.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!4zgA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff4ef5a9b-c390-4a1d-98ff-8efd29f894a6_2086x1834.png 424w, https://substackcdn.com/image/fetch/$s_!4zgA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff4ef5a9b-c390-4a1d-98ff-8efd29f894a6_2086x1834.png 848w, https://substackcdn.com/image/fetch/$s_!4zgA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff4ef5a9b-c390-4a1d-98ff-8efd29f894a6_2086x1834.png 1272w, https://substackcdn.com/image/fetch/$s_!4zgA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff4ef5a9b-c390-4a1d-98ff-8efd29f894a6_2086x1834.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p><strong>Multiple Threads &#8594; Single Actor.</strong> When TaskManager reports a state change, it does not call JobMaster directly. It sends a message to JobMaster&#8217;s actor. When CheckpointCoordinator triggers a checkpoint, it sends another message. When REST API queries job status, it sends yet another message. Three different callers. Three different threads. All messages arrive at the same actor.</p><p><strong>Actor Mailbox = FIFO Queue.</strong> The actor maintains an internal mailbox. Messages arrive and queue up in order. The first message to arrive is the first message processed. The second message waits until the first completes. The third waits for the second. This ordering provides deterministic execution. Given the same message sequence, the actor produces the same results.</p><p><strong>MainThreadExecutor = Single Thread.</strong> The <a href="https://github.com/apache/flink/blob/master/flink-rpc/flink-rpc-core/src/main/java/org/apache/flink/runtime/rpc/RpcEndpoint.java">RpcEndpoint</a> base class provides a <code>MainThreadExecutor</code>. This executor runs on a single thread dedicated to the endpoint. Every RPC method executes on this thread. Every internal callback executes on this thread. Every scheduled task executes on this thread. The endpoint owns this thread exclusively.</p><p><strong>No Synchronization Needed.</strong> Because all code runs on a single thread, no synchronization is necessary. The ExecutionGraph has no locks. Methods read and write state directly. Iterators remain valid because no concurrent modification is possible. The code reads like a simple single-threaded program. Developers reason about sequential execution, not thread interleavings.</p><h3><strong>How Message Processing Works</strong></h3><p>Consider a concrete example. JobMaster receives three messages in quick succession.</p><p>Message 1 arrives from TaskManager: <code>updateTaskExecutionState(task=A, state=FINISHED)</code>. The mailbox queues this message. The main thread picks it up. JobMaster accesses ExecutionGraph, finds the execution for task A, and updates its state to FINISHED. The main thread completes processing.</p><p>Message 2 arrives from CheckpointCoordinator: <code>triggerCheckpoint(checkpointId=42)</code>. The mailbox already has this message queued. The main thread picks it up after completing Message 1. JobMaster accesses ExecutionGraph, iterates through all vertices, and triggers checkpoint 42 on each. The iteration is safe because Message 1 already completed. ExecutionGraph is in a consistent state.</p><p>Message 3 arrives from REST API: <code>requestJobDetails()</code>. The mailbox queues it behind Message 2. The main thread picks it up after completing Message 2. JobMaster reads ExecutionGraph and returns job details. The read sees all updates from Messages 1 and 2.</p><p>This sequential processing eliminates every concurrency concern. Message 2 never sees ExecutionGraph mid-update from Message 1. Message 3 always sees a consistent view. No locks required. No race conditions possible.</p><div><hr></div><h2><strong>Architecture: The RPC Abstraction Layers</strong></h2><p>Flink builds its RPC system in layers. Each layer has a specific responsibility. The layers compose to provide type-safe, single-threaded, distributed method invocation.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9_4Z!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68b4eb94-3063-4f7a-8ff9-efa32b0758df_2352x1834.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9_4Z!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68b4eb94-3063-4f7a-8ff9-efa32b0758df_2352x1834.png 424w, https://substackcdn.com/image/fetch/$s_!9_4Z!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68b4eb94-3063-4f7a-8ff9-efa32b0758df_2352x1834.png 848w, https://substackcdn.com/image/fetch/$s_!9_4Z!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68b4eb94-3063-4f7a-8ff9-efa32b0758df_2352x1834.png 1272w, https://substackcdn.com/image/fetch/$s_!9_4Z!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68b4eb94-3063-4f7a-8ff9-efa32b0758df_2352x1834.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9_4Z!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68b4eb94-3063-4f7a-8ff9-efa32b0758df_2352x1834.png" width="1456" height="1135" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/68b4eb94-3063-4f7a-8ff9-efa32b0758df_2352x1834.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1135,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:470999,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.arunlakshman.info/i/200802412?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68b4eb94-3063-4f7a-8ff9-efa32b0758df_2352x1834.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!9_4Z!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68b4eb94-3063-4f7a-8ff9-efa32b0758df_2352x1834.png 424w, https://substackcdn.com/image/fetch/$s_!9_4Z!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68b4eb94-3063-4f7a-8ff9-efa32b0758df_2352x1834.png 848w, https://substackcdn.com/image/fetch/$s_!9_4Z!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68b4eb94-3063-4f7a-8ff9-efa32b0758df_2352x1834.png 1272w, https://substackcdn.com/image/fetch/$s_!9_4Z!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68b4eb94-3063-4f7a-8ff9-efa32b0758df_2352x1834.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>To understand Flink&#8217;s RPC architecture, it helps to draw parallels with familiar Java patterns. If you&#8217;ve used the AWS SDK, Apache Tomcat, or Java Servlets, you already understand the core concepts - just with different names.</p><h3><strong>Mapping Flink RPC to Familiar Java Patterns</strong></h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!D9X5!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44477abb-8622-4722-9f17-42dc41b570a7_782x352.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!D9X5!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44477abb-8622-4722-9f17-42dc41b570a7_782x352.png 424w, https://substackcdn.com/image/fetch/$s_!D9X5!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44477abb-8622-4722-9f17-42dc41b570a7_782x352.png 848w, https://substackcdn.com/image/fetch/$s_!D9X5!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44477abb-8622-4722-9f17-42dc41b570a7_782x352.png 1272w, https://substackcdn.com/image/fetch/$s_!D9X5!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44477abb-8622-4722-9f17-42dc41b570a7_782x352.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!D9X5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44477abb-8622-4722-9f17-42dc41b570a7_782x352.png" width="782" height="352" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/44477abb-8622-4722-9f17-42dc41b570a7_782x352.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:352,&quot;width&quot;:782,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:227150,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.arunlakshman.info/i/200802412?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44477abb-8622-4722-9f17-42dc41b570a7_782x352.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!D9X5!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44477abb-8622-4722-9f17-42dc41b570a7_782x352.png 424w, https://substackcdn.com/image/fetch/$s_!D9X5!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44477abb-8622-4722-9f17-42dc41b570a7_782x352.png 848w, https://substackcdn.com/image/fetch/$s_!D9X5!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44477abb-8622-4722-9f17-42dc41b570a7_782x352.png 1272w, https://substackcdn.com/image/fetch/$s_!D9X5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44477abb-8622-4722-9f17-42dc41b570a7_782x352.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><h3><strong>RpcGateway: The Interface Contract (Like AWS SDK Service Clients)</strong></h3><p><a href="https://github.com/apache/flink/blob/master/flink-rpc/flink-rpc-core/src/main/java/org/apache/flink/runtime/rpc/RpcGateway.java">RpcGateway</a> defines the contract for remote calls. It serves the same purpose as an AWS SDK service client interface.</p><p><strong>AWS SDK Analogy:</strong> When you use <code>S3Client</code> from the AWS SDK, you call methods like <code>putObject()</code> or <code>getObject()</code>. You don&#8217;t think about HTTP, serialization, or retries. The interface abstracts the network layer completely. <code>RpcGateway</code> does the same for Flink&#8217;s internal communication.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;619ec7dc-4541-4418-a95f-5267881b70a1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">// AWS SDK pattern - you're familiar with this
public interface S3Client {
    PutObjectResponse putObject(PutObjectRequest request);
    GetObjectResponse getObject(GetObjectRequest request);
}

// Flink RPC pattern - same concept, different domain
public interface JobMasterGateway extends RpcGateway {
    CompletableFuture&lt;Acknowledge&gt; updateTaskExecutionState(TaskExecutionState state);
    CompletableFuture&lt;Acknowledge&gt; cancel(Duration timeout);
    CompletableFuture&lt;String&gt; triggerSavepoint(String targetDirectory, boolean cancelJob);
}
</code></pre></div><p><strong>Key Differences from AWS SDK:</strong></p><ol><li><p><strong>Async by Default:</strong> Every <code>RpcGateway</code> method returns <code>CompletableFuture</code>. AWS SDK v2 offers both sync (<code>S3Client</code>) and async (<code>S3AsyncClient</code>) variants. Flink chose async-only to make the non-blocking nature explicit. Callers never block waiting for results - they attach callbacks or chain operations.</p></li><li><p><strong>Bidirectional:</strong> AWS SDK clients only make outbound calls. Flink gateways are bidirectional. <code>TaskExecutorGateway</code> lets JobMaster call into TaskManager. <code>JobMasterGateway</code> lets TaskManager call into JobMaster. Both sides expose gateways.</p></li><li><p><strong>Internal Network:</strong> AWS SDK calls traverse the public internet to AWS services. Flink RPC calls stay within the cluster&#8217;s internal network, typically using direct TCP connections.</p></li></ol><p><a href="https://github.com/apache/flink/blob/master/flink-runtime/src/main/java/org/apache/flink/runtime/jobmaster/JobMasterGateway.java">JobMasterGateway</a> declares methods that callers can invoke on JobMaster. The interface serves as documentation - new engineers read it to understand what operations JobMaster supports. Method signatures specify exact parameter types and return types. Javadoc explains semantics. The interface is the source of truth for the RPC contract.</p><h3><strong>RpcEndpoint: The Base Class (Like a Servlet or Spring Controller)</strong></h3><p><a href="https://github.com/apache/flink/blob/master/flink-rpc/flink-rpc-core/src/main/java/org/apache/flink/runtime/rpc/RpcEndpoint.java">RpcEndpoint</a> is the server-side handler. Every distributed component extends this class. Think of it as a Servlet that handles incoming requests, but with a critical difference: all requests execute on a single thread.</p><p><strong>Servlet Analogy:</strong> In a traditional Java web application, you write a Servlet to handle HTTP requests:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;4bd9e172-6610-48c6-8249-b998e2165da2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">// Traditional Servlet - Tomcat spawns a thread per request
public class OrderServlet extends HttpServlet {
    private OrderRepository repository;  // Shared state - needs synchronization!
    
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
        // WARNING: Multiple threads execute this concurrently
        // Must synchronize access to repository
        synchronized(repository) {
            repository.createOrder(parseOrder(req));
        }
    }
}
</code></pre></div><p><strong>Flink RpcEndpoint - Same concept, but single-threaded:</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;951a58ec-5265-42b3-af04-b253b4dc0733&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">// Flink RpcEndpoint - only ONE thread ever executes methods
public class JobMaster extends FencedRpcEndpoint&lt;JobMasterId&gt; 
        implements JobMasterGateway {
    
    private SchedulerNG schedulerNG;  // Shared state - NO synchronization needed!
    
    @Override
    public CompletableFuture&lt;Acknowledge&gt; updateTaskExecutionState(
            TaskExecutionState state) {
        // SAFE: Only main thread executes this
        // No locks, no synchronization, no race conditions
        schedulerNG.updateTaskExecutionState(state);
        return CompletableFuture.completedFuture(Acknowledge.get());
    }
}
</code></pre></div><p><strong>Why Single-Threaded Beats Multi-Threaded Here:</strong></p><p>Tomcat&#8217;s thread-per-request model works well for stateless web applications. Each request is independent. But Flink&#8217;s components maintain complex shared state (ExecutionGraph with thousands of vertices, checkpoint state, slot allocations). The single-threaded model eliminates an entire class of bugs.</p><p><strong>Key RpcEndpoint Features:</strong></p><ol><li><p><strong>MainThreadExecutor:</strong> The constructor creates a dedicated executor bound to the endpoint. All RPC calls execute through this executor. The class provides methods to schedule work on the main thread:</p><ul><li><p><code>runAsync(Runnable)</code> - queues a task for later execution</p></li><li><p><code>callAsync(Callable&lt;V&gt;)</code> - queues a task and returns <code>CompletableFuture&lt;V&gt;</code></p></li><li><p><code>scheduleRunAsync(Runnable, Duration)</code> - queues work with a delay</p></li></ul></li><li><p><strong>Lifecycle Hooks:</strong> Like Servlet&#8217;s <code>init()</code> and <code>destroy()</code>:</p><ul><li><p><code>onStart()</code> - runs when the endpoint begins accepting messages</p></li><li><p><code>onStop()</code> - runs during shutdown Both execute on the main thread, making initialization and cleanup thread-safe.</p></li></ul></li><li><p><strong>Thread Safety Check:</strong> The <code>validateRunsInMainThread()</code> method catches programming errors early:</p></li></ol><pre><code><code>protected void validateRunsInMainThread() {
    if (!rpcServer.isCurrentThreadMainThread()) {
        throw new IllegalStateException(
            "This method must be called from within the main thread.");
    }
}
</code></code></pre><p><strong>Component Hierarchy:</strong></p><ul><li><p><a href="https://github.com/apache/flink/blob/master/flink-runtime/src/main/java/org/apache/flink/runtime/jobmaster/JobMaster.java">JobMaster</a> extends <code>FencedRpcEndpoint</code> - coordinates job execution</p></li><li><p><a href="https://github.com/apache/flink/blob/master/flink-runtime/src/main/java/org/apache/flink/runtime/taskexecutor/TaskExecutor.java">TaskExecutor</a> extends <code>RpcEndpoint</code> - runs tasks on worker nodes</p></li><li><p><a href="https://github.com/apache/flink/blob/master/flink-runtime/src/main/java/org/apache/flink/runtime/resourcemanager/ResourceManager.java">ResourceManager</a> extends <code>FencedRpcEndpoint</code> - manages cluster resources</p></li><li><p><a href="https://github.com/apache/flink/blob/master/flink-runtime/src/main/java/org/apache/flink/runtime/dispatcher/Dispatcher.java">Dispatcher</a> extends <code>FencedRpcEndpoint</code> - handles job submission</p></li></ul><h3><strong>RpcService: The Factory and Connection Manager (Like Tomcat&#8217;s Connector)</strong></h3><p><a href="https://github.com/apache/flink/blob/master/flink-rpc/flink-rpc-core/src/main/java/org/apache/flink/runtime/rpc/RpcService.java">RpcService</a> is an <strong>abstraction</strong> that manages endpoint lifecycles and gateway connections. It defines the contract for how endpoints are created and how connections are established - but not how messages travel over the wire.</p><p>Currently, the only production implementation is <a href="https://github.com/apache/flink/blob/master/flink-rpc/flink-rpc-akka/src/main/java/org/apache/flink/runtime/rpc/pekko/PekkoRpcService.java">PekkoRpcService</a>, which uses Pekko&#8217;s actor remoting over TCP. However, the abstraction exists precisely so the transport can be swapped without changing Flink&#8217;s core components. Future implementations could use:</p><ul><li><p><strong>gRPC</strong> - Industry-standard RPC with HTTP/2, protobuf serialization, and mature tooling</p></li><li><p><strong>HTTP/REST</strong> - Simpler debugging, standard load balancers, firewall-friendly</p></li><li><p><strong>Custom TCP</strong> - Optimized binary protocol without Pekko&#8217;s overhead</p></li></ul><p>The key insight: <code>JobMaster</code>, <code>TaskExecutor</code>, and <code>ResourceManager</code> don&#8217;t know or care whether messages travel via Pekko actors, gRPC streams, or HTTP requests. They only interact with the <code>RpcService</code> abstraction.</p><p><strong>Tomcat Analogy:</strong> Tomcat&#8217;s <code>Connector</code> accepts incoming connections, manages the thread pool, and routes requests to Servlets. <code>RpcService</code> does the same for Flink. Just as Tomcat can swap between NIO, NIO2, or APR connectors without changing your Servlets, Flink could swap RpcService implementations without changing endpoints:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!EtTr!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4290029f-44c5-43d4-83a7-d204a8e02ec4_793x208.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!EtTr!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4290029f-44c5-43d4-83a7-d204a8e02ec4_793x208.png 424w, https://substackcdn.com/image/fetch/$s_!EtTr!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4290029f-44c5-43d4-83a7-d204a8e02ec4_793x208.png 848w, https://substackcdn.com/image/fetch/$s_!EtTr!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4290029f-44c5-43d4-83a7-d204a8e02ec4_793x208.png 1272w, https://substackcdn.com/image/fetch/$s_!EtTr!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4290029f-44c5-43d4-83a7-d204a8e02ec4_793x208.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!EtTr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4290029f-44c5-43d4-83a7-d204a8e02ec4_793x208.png" width="793" height="208" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4290029f-44c5-43d4-83a7-d204a8e02ec4_793x208.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:208,&quot;width&quot;:793,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:131295,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.arunlakshman.info/i/200802412?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4290029f-44c5-43d4-83a7-d204a8e02ec4_793x208.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!EtTr!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4290029f-44c5-43d4-83a7-d204a8e02ec4_793x208.png 424w, https://substackcdn.com/image/fetch/$s_!EtTr!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4290029f-44c5-43d4-83a7-d204a8e02ec4_793x208.png 848w, https://substackcdn.com/image/fetch/$s_!EtTr!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4290029f-44c5-43d4-83a7-d204a8e02ec4_793x208.png 1272w, https://substackcdn.com/image/fetch/$s_!EtTr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4290029f-44c5-43d4-83a7-d204a8e02ec4_793x208.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p><strong>AWS SDK Analogy:</strong> <code>RpcService</code> also resembles <code>SdkClientBuilder</code> combined with connection pooling. The SDK abstracts whether it uses Apache HttpClient, Netty, or URL Connection under the hood:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;1e5a4951-c17d-4a2e-bc24-2fccbc59b46b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">// AWS SDK - builder creates configured client (transport abstracted)
S3Client s3 = S3Client.builder()
    .region(Region.US_EAST_1)
    .httpClient(NettyNioAsyncHttpClient.create())  // Could swap to ApacheHttpClient
    .build();

// Flink - RpcService abstraction (transport abstracted)
// Today: PekkoRpcService (actor-based TCP)
// Future: Could be GrpcRpcService, HttpRpcService, etc.
RpcService rpcService = new PekkoRpcService(config, actorSystem);

// These calls work identically regardless of RpcService implementation:
// Start a server (like deploying a Servlet)
rpcService.startServer(jobMaster);

// Connect to remote server (like creating SDK client)
JobMasterGateway gateway = rpcService.connect(address, JobMasterGateway.class).get();
</code></pre></div><p><strong>Key RpcService Responsibilities (Interface Contract):</strong></p><p>These responsibilities are defined by the <code>RpcService</code> interface. Any implementation - Pekko, gRPC, or HTTP - must fulfill them:</p><ol><li><p><strong>Server Creation:</strong> When JobMaster instantiates, it calls <code>rpcService.startServer(this)</code>. The implementation creates whatever underlying machinery is needed (actors for Pekko, gRPC stubs for gRPC, servlet registration for HTTP) and starts the main thread executor. The endpoint is now ready to receive messages.</p></li><li><p><strong>Client Connection:</strong> A TaskManager needs to communicate with JobMaster on another machine. It calls <code>rpcService.connect(address, JobMasterGateway.class)</code>. The implementation returns a proxy object implementing <code>JobMasterGateway</code>. Whether that proxy sends Pekko messages, gRPC calls, or HTTP requests is an implementation detail hidden from the caller.</p></li><li><p><strong>Transport Management:</strong> The implementation manages its transport layer - ActorSystem for Pekko, ManagedChannel for gRPC, HttpClient for HTTP. It handles configuration, connection pooling, and graceful shutdown.</p></li></ol><p><strong>Why This Abstraction Matters:</strong></p><p>The Pekko (formerly Akka) license change in 2022 forced Flink to migrate from Akka to Pekko. This abstraction means a future migration to gRPC or HTTP would only require implementing a new <code>RpcService</code> - no changes to <code>JobMaster</code>, <code>TaskExecutor</code>, or <code>ResourceManager</code>.</p><h3><strong>RpcServer: The Message Dispatcher (Like DispatcherServlet)</strong></h3><p><a href="https://github.com/apache/flink/blob/master/flink-rpc/flink-rpc-core/src/main/java/org/apache/flink/runtime/rpc/RpcServer.java">RpcServer</a> is the internal component that dispatches messages to the endpoint.</p><p><strong>Spring MVC Analogy:</strong> Spring&#8217;s <code>DispatcherServlet</code> receives all HTTP requests, determines which controller method to invoke, and dispatches the call. <code>RpcServer</code> does the same for RPC messages:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_dST!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96c81f43-d6c7-40d5-bfc4-29fef590660f_477x200.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_dST!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96c81f43-d6c7-40d5-bfc4-29fef590660f_477x200.png 424w, https://substackcdn.com/image/fetch/$s_!_dST!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96c81f43-d6c7-40d5-bfc4-29fef590660f_477x200.png 848w, https://substackcdn.com/image/fetch/$s_!_dST!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96c81f43-d6c7-40d5-bfc4-29fef590660f_477x200.png 1272w, https://substackcdn.com/image/fetch/$s_!_dST!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96c81f43-d6c7-40d5-bfc4-29fef590660f_477x200.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_dST!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96c81f43-d6c7-40d5-bfc4-29fef590660f_477x200.png" width="477" height="200" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/96c81f43-d6c7-40d5-bfc4-29fef590660f_477x200.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:200,&quot;width&quot;:477,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:95595,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.arunlakshman.info/i/200802412?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96c81f43-d6c7-40d5-bfc4-29fef590660f_477x200.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!_dST!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96c81f43-d6c7-40d5-bfc4-29fef590660f_477x200.png 424w, https://substackcdn.com/image/fetch/$s_!_dST!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96c81f43-d6c7-40d5-bfc4-29fef590660f_477x200.png 848w, https://substackcdn.com/image/fetch/$s_!_dST!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96c81f43-d6c7-40d5-bfc4-29fef590660f_477x200.png 1272w, https://substackcdn.com/image/fetch/$s_!_dST!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96c81f43-d6c7-40d5-bfc4-29fef590660f_477x200.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p><strong>Key RpcServer Responsibilities:</strong></p><ol><li><p><strong>Thread Tracking:</strong> Knows which thread is the endpoint&#8217;s main thread. Provides <code>isCurrentThreadMainThread()</code> for safety checks.</p></li><li><p><strong>Method Invocation:</strong> When a message arrives requesting <code>updateTaskExecutionState()</code>:</p><ul><li><p>Locates the method on the endpoint class</p></li><li><p>Deserializes the arguments</p></li><li><p>Invokes the method reflectively</p></li><li><p>Captures the return value</p></li><li><p>Serializes the result and sends it back</p></li></ul></li></ol><h3><strong>PekkoInvocationHandler: The Client-Side Proxy (Like AWS SDK&#8217;s HTTP Layer)</strong></h3><p><a href="https://github.com/apache/flink/blob/master/flink-rpc/flink-rpc-akka/src/main/java/org/apache/flink/runtime/rpc/pekko/PekkoInvocationHandler.java">PekkoInvocationHandler</a> implements <code>InvocationHandler</code> for the dynamic proxy. It converts method calls into network messages.</p><p><strong>AWS SDK Analogy:</strong> When you call <code>s3Client.putObject(request)</code>, the SDK internally:</p><ol><li><p>Serializes the request to HTTP</p></li><li><p>Signs the request</p></li><li><p>Sends over HTTPS</p></li><li><p>Deserializes the response</p></li></ol><p><code>PekkoInvocationHandler</code> does the same, but with Pekko&#8217;s actor messaging instead of HTTP:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;585a9af4-2c2c-4173-ba33-bd2814b08845&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">// What you write
gateway.updateTaskExecutionState(state);

// What PekkoInvocationHandler does internally (simplified)
public Object invoke(Object proxy, Method method, Object[] args) {
    // 1. Create invocation object (like HTTP request)
    RpcInvocation invocation = new RpcInvocation(
        method.getName(),           // "updateTaskExecutionState"
        method.getParameterTypes(), // [TaskExecutionState.class]
        args                        // [state]
    );
    
    // 2. Send via actor (like HTTP send)
    CompletableFuture&lt;Object&gt; result = actorRef.ask(invocation, timeout);
    
    // 3. Return future (response will arrive asynchronously)
    return result;
}
</code></pre></div><p><strong>HttpClient Comparison:</strong></p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9vu8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99c30071-1860-49c4-9761-b092453b89a9_560x214.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9vu8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99c30071-1860-49c4-9761-b092453b89a9_560x214.png 424w, https://substackcdn.com/image/fetch/$s_!9vu8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99c30071-1860-49c4-9761-b092453b89a9_560x214.png 848w, https://substackcdn.com/image/fetch/$s_!9vu8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99c30071-1860-49c4-9761-b092453b89a9_560x214.png 1272w, https://substackcdn.com/image/fetch/$s_!9vu8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99c30071-1860-49c4-9761-b092453b89a9_560x214.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9vu8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99c30071-1860-49c4-9761-b092453b89a9_560x214.png" width="560" height="214" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/99c30071-1860-49c4-9761-b092453b89a9_560x214.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:214,&quot;width&quot;:560,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:104233,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.arunlakshman.info/i/200802412?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99c30071-1860-49c4-9761-b092453b89a9_560x214.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!9vu8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99c30071-1860-49c4-9761-b092453b89a9_560x214.png 424w, https://substackcdn.com/image/fetch/$s_!9vu8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99c30071-1860-49c4-9761-b092453b89a9_560x214.png 848w, https://substackcdn.com/image/fetch/$s_!9vu8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99c30071-1860-49c4-9761-b092453b89a9_560x214.png 1272w, https://substackcdn.com/image/fetch/$s_!9vu8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99c30071-1860-49c4-9761-b092453b89a9_560x214.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><div><hr></div><h2><strong>Network Path: Client to Server Flow</strong></h2><p>When an RPC call crosses machine boundaries, a complex flow executes. Understanding this flow helps debug network-related failures.</p><h3><strong>Client Side: Gateway to Network</strong></h3><p>The flow mirrors what happens in an AWS SDK call, but with actors instead of HTTP.</p><p><strong>Step 1: Obtain Gateway (Like Creating SDK Client)</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;6b4a64d8-3fdb-4836-9e4e-71ab8bb7c30e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">// AWS SDK
S3Client s3 = S3Client.builder().region(Region.US_EAST_1).build();

// Flink RPC
JobMasterGateway gateway = rpcService.connect(jobMasterAddress, JobMasterGateway.class).get();
</code></pre></div><p>The <code>connect()</code> call doesn&#8217;t return a real <code>JobMasterGateway</code> implementation. It returns a dynamic proxy created by <code>Proxy.newProxyInstance()</code>. The proxy implements the interface but delegates all calls to <code>PekkoInvocationHandler</code>.</p><p><strong>Step 2: Method Invocation (Like SDK Method Call)</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;320d391a-a2a3-4b7a-8d36-fdcb73861e26&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">// AWS SDK
PutObjectResponse response = s3.putObject(request);  // Looks local, actually remote

// Flink RPC  
CompletableFuture&lt;Acknowledge&gt; future = gateway.updateTaskExecutionState(state);  // Same pattern
</code></pre></div><p>The proxy intercepts the call. No business logic executes locally.</p><p><strong>Step 3: Create Invocation Object (Like HTTP Request Building)</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;433f114b-1ef3-4e52-b01c-67fbceba4bc9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">// Conceptually similar to:
// HttpRequest.newBuilder()
//     .uri(URI.create("https://s3.amazonaws.com/bucket/key"))
//     .POST(BodyPublishers.ofByteArray(serialize(request)))
//     .build();

RpcInvocation invocation = new RpcInvocation(
    "updateTaskExecutionState",      // Method name (like URL path)
    new Class[]{TaskExecutionState.class},  // Parameter types
    new Object[]{state}              // Arguments (like request body)
);
</code></pre></div><p><strong>Step 4: Serialize and Send (Like HTTP Transport)</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;7f656310-142e-491e-9693-5d05c292a9af&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">// AWS SDK uses HTTP client internally
// httpClient.sendAsync(httpRequest, responseHandler);

// Flink uses Pekko actor messaging
actorRef.ask(invocation, timeout);  // Pekko serializes with Kryo, sends over TCP
</code></pre></div><h3><strong>Server Side: Network to Execution</strong></h3><p><strong>Step 1: TCP Receive (Like Tomcat Accepting Connection)</strong></p><p>The remote machine receives TCP bytes. Pekko&#8217;s network layer reads the frame and routes to the target actor based on the actor path.</p><p><strong>Step 2: Actor Receives Message (Like Servlet.service())</strong></p><p><a href="https://github.com/apache/flink/blob/master/flink-rpc/flink-rpc-akka/src/main/java/org/apache/flink/runtime/rpc/pekko/PekkoRpcActor.java">PekkoRpcActor</a> receives the message in its <code>onReceive()</code> method - the entry point for all incoming messages.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;2ef80dae-c533-491f-addd-f009447aa57e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">// Conceptually similar to:
// public void service(HttpServletRequest req, HttpServletResponse resp) {
//     String method = req.getMethod();
//     String path = req.getPathInfo();
//     // Route to appropriate handler
// }

public void onReceive(Object message) {
    if (message instanceof RpcInvocation) {
        handleRpcInvocation((RpcInvocation) message);
    }
}
</code></pre></div><p><strong>Step 3: Mailbox Queuing (Unlike Tomcat - This is the Key Difference)</strong></p><p>Here&#8217;s where Flink diverges from traditional web servers. Tomcat would spawn a thread and execute immediately. Flink enqueues in the mailbox:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;c58f789c-846a-453e-9eb9-3d9dee02ef28&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Tomcat: Request arrives &#8594; New thread &#8594; Execute handler &#8594; Return response
Flink:  Message arrives &#8594; Enqueue in mailbox &#8594; Wait turn &#8594; Main thread executes &#8594; Return response
</code></pre></div><p>The message joins the queue behind any previously arrived messages. FIFO ordering guarantees deterministic execution.</p><p><strong>Step 4: Main Thread Execution (Single-Threaded Handler)</strong></p><p>The main thread dequeues the invocation when it reaches the front. It uses reflection to call <code>updateTaskExecutionState(state)</code> on the <code>JobMaster</code> instance. The method executes with full access to internal state - no locks needed.</p><p><strong>Step 5: Response (Like HTTP Response)</strong></p><p>The method returns <code>CompletableFuture&lt;Acknowledge&gt;</code>. The actor captures the result, serializes it, and sends bytes back over TCP. The caller&#8217;s <code>CompletableFuture</code> completes with the result.</p><h3><strong>Complete Flow Comparison</strong></h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!J5D7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ad10243-bc9e-42ce-9254-de6210c7570e_685x356.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!J5D7!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ad10243-bc9e-42ce-9254-de6210c7570e_685x356.png 424w, https://substackcdn.com/image/fetch/$s_!J5D7!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ad10243-bc9e-42ce-9254-de6210c7570e_685x356.png 848w, https://substackcdn.com/image/fetch/$s_!J5D7!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ad10243-bc9e-42ce-9254-de6210c7570e_685x356.png 1272w, https://substackcdn.com/image/fetch/$s_!J5D7!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ad10243-bc9e-42ce-9254-de6210c7570e_685x356.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!J5D7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ad10243-bc9e-42ce-9254-de6210c7570e_685x356.png" width="685" height="356" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7ad10243-bc9e-42ce-9254-de6210c7570e_685x356.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:356,&quot;width&quot;:685,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:200530,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.arunlakshman.info/i/200802412?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ad10243-bc9e-42ce-9254-de6210c7570e_685x356.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!J5D7!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ad10243-bc9e-42ce-9254-de6210c7570e_685x356.png 424w, https://substackcdn.com/image/fetch/$s_!J5D7!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ad10243-bc9e-42ce-9254-de6210c7570e_685x356.png 848w, https://substackcdn.com/image/fetch/$s_!J5D7!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ad10243-bc9e-42ce-9254-de6210c7570e_685x356.png 1272w, https://substackcdn.com/image/fetch/$s_!J5D7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ad10243-bc9e-42ce-9254-de6210c7570e_685x356.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div><hr></div><h2><strong>Practical Implications</strong></h2><h3><strong>Code Simplicity</strong></h3><p>The RpcEndpoint pattern transforms how developers write distributed coordination code. Compare two approaches to updating ExecutionGraph.</p><p><strong>Without RpcEndpoint (Hypothetical - Like Traditional Servlet):</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;283816a2-6f54-4c7a-b0e8-4d4d0c61501f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">// Similar to a Servlet with shared state
class JobMaster {
    private ExecutionGraph executionGraph;
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    
    void updateTaskState(TaskExecutionState state) {
        lock.writeLock().lock();
        try {
            Execution exec = executionGraph.getExecution(state.getID());
            exec.updateState(state.getExecutionState());
        } finally {
            lock.writeLock().unlock();
        }
    }
    
    JobDetails getJobDetails() {
        lock.readLock().lock();
        try {
            return JobDetails.createFrom(executionGraph);
        } finally {
            lock.readLock().unlock();
        }
    }
}
</code></pre></div><p><strong>With RpcEndpoint (Actual Flink):</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;906b69fb-2d6b-4d0a-a563-7962c7728047&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">public class JobMaster extends FencedRpcEndpoint&lt;JobMasterId&gt; 
        implements JobMasterGateway {
    
    private SchedulerNG schedulerNG;  // Contains ExecutionGraph
    
    @Override
    public CompletableFuture&lt;Acknowledge&gt; updateTaskExecutionState(
            TaskExecutionState state) {
        // No lock needed - runs on main thread
        Execution exec = schedulerNG.getExecutionGraph()
                                    .getExecution(state.getID());
        exec.updateState(state.getExecutionState());
        return CompletableFuture.completedFuture(Acknowledge.get());
    }
    
    @Override
    public CompletableFuture&lt;JobDetails&gt; requestJobDetails(Duration timeout) {
        // No lock needed - runs on main thread
        return CompletableFuture.completedFuture(
            JobDetails.createFrom(schedulerNG.getExecutionGraph()));
    }
}
</code></pre></div><p>The actual Flink code has no locks. Methods read and write state directly. The single-threaded guarantee is architectural, not annotational. Developers cannot forget to acquire a lock because no lock exists.</p><h3><strong>Debugging Benefits</strong></h3><p>When investigating issues, the single-threaded model simplifies analysis. All state changes happen in sequence. Given a log of messages, you can reconstruct exact system state at any point. No thread interleavings to consider. No happens-before relationships to reason about.</p><p>Flink provides <code>validateRunsInMainThread()</code> for defensive programming. Critical methods call this check at entry. If a developer accidentally calls a state-modifying method from a wrong thread, the check throws immediately. The stack trace points to the violation. The bug is caught in development, not production.</p><h3><strong>Performance Considerations</strong></h3><p>The single-threaded model has a trade-off. All operations serialize through one thread. High message volume can create backlog in the mailbox. The main thread becomes a bottleneck.</p><p>Flink mitigates this in practice. RPC methods are designed to be fast. They update in-memory state and return quickly. Heavy computation offloads to separate thread pools via <code>callAsync()</code>. Blocking I/O never runs on the main thread.</p><p>For most workloads, the main thread handles thousands of messages per second without issue. The simplicity and correctness benefits outweigh the throughput limitation. Debugging a race condition costs more engineering time than optimizing a hot path.</p><div><hr></div><h2><strong>Historical Context: Akka to Pekko</strong></h2><p>Flink used Akka from its early versions. Akka provided a mature, battle-tested actor implementation. Flink&#8217;s usage was focused: message passing between components, single-threaded execution guarantees, and failure detection via DeathWatch.</p><p>In September 2022, Lightbend changed Akka&#8217;s license from Apache 2.0 to Business Source License (BSL). This license is incompatible with Apache Software Foundation projects. Flink could not continue using new Akka versions.</p><p>The Apache Software Foundation responded by forking Akka 2.6.x as Apache Pekko. Pekko maintains Apache 2.0 licensing. It provides API compatibility with Akka 2.6.x. Migration requires updating imports from <code>akka.*</code> to <code>org.apache.pekko.*</code> and configuration keys from <code>akka.*</code> to <code>pekko.*</code>.</p><p>Flink 1.18 completed the migration to Pekko. The architecture remains identical. The single-threaded execution guarantee is unchanged. Existing Flink applications require no code changes. Only operators running custom Akka code directly (rare) need updates.</p><div><hr></div><h2><strong>Summary</strong></h2><p>Flink&#8217;s RPC architecture solves a fundamental distributed systems problem. Multiple components must access shared state. Traditional locking creates complexity, deadlocks, and race conditions. The Actor Model provides an elegant alternative.</p><p>Each component extends RpcEndpoint. Each RpcEndpoint processes messages on a single thread. The mailbox queues messages in FIFO order. No concurrent access is possible. No locks are needed.</p><p>The RPC layer provides type-safe communication. RpcGateway interfaces define contracts (like AWS SDK client interfaces). Dynamic proxies implement these interfaces (like SDK internal handlers). RpcService abstracts the transport layer - currently Pekko, but designed to be swappable with gRPC or HTTP implementations. RpcEndpoint handles requests (like Servlets). The result is distributed method invocation that feels like local calls.</p><p>This architecture has served Flink well for years. It enables correct coordination across hundreds of distributed components. It simplifies debugging and testing. It allows developers to write straightforward sequential code for inherently concurrent problems.</p>]]></content:encoded></item><item><title><![CDATA[Compare And Swap is all you need]]></title><description><![CDATA[How CAS Became the Foundation of Concurrent Programming]]></description><link>https://www.arunlakshman.info/p/compare-and-swap-is-all-you-need</link><guid isPermaLink="false">https://www.arunlakshman.info/p/compare-and-swap-is-all-you-need</guid><pubDate>Thu, 21 May 2026 00:48:15 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!xHWo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e07aebe-1d86-4841-a6f5-b8edb883bf2f_608x608.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p></p><blockquote><p>This blog post is inspired by the first 6 chapters of <em>The Art of Multiprocessor Programming</em> by Maurice Herlihy and Nir Shavit.</p></blockquote><p>Imagine building a distributed counter that must handle millions of updates per second across dozens of threads. Traditional locks serialize access, creating bottlenecks. You need something better: a way for threads to coordinate without blocking, without deadlocks, without the performance collapse that comes with contention. This isn&#8217;t just a performance optimization problem; it&#8217;s a fundamental question about what synchronization primitives are actually necessary. Can we build wait-free concurrent data structures? Which hardware instructions must processors provide? The answer, discovered through decades of theoretical work, reveals that one primitive, Compare-And-Swap (CAS), is universal.</p><p><strong>Modern processors have dozens of cores, and our applications must leverage them all.</strong> From distributed databases processing millions of transactions per second to real-time analytics engines crunching streaming data, concurrent programming has moved from a specialized skill to a fundamental requirement. Yet building correct concurrent systems remains notoriously difficult: race conditions lurk in seemingly innocent code, deadlocks emerge from complex lock hierarchies, and performance bottlenecks appear where we least expect them.</p><p><strong>The real challenge isn&#8217;t just making threads cooperate; it&#8217;s understanding which synchronization primitives actually give us the power we need.</strong> Over decades, computer scientists developed countless approaches: Peterson&#8217;s algorithm for mutual exclusion, Lamport&#8217;s bakery algorithm for fairness, sophisticated lock implementations with elaborate protocols. But a deeper question remained unanswered: Are these atomic building blocks fundamentally different in power? Can some primitives solve problems that others simply cannot? More critically for systems engineers: If we&#8217;re designing hardware or choosing synchronization mechanisms for a new platform, which primitives must we provide?</p><p><strong>The answer fundamentally changed how we think about concurrent programming: Compare-And-Swap (CAS) is universal.</strong> This isn&#8217;t marketing hyperbole: it&#8217;s a mathematically proven property. Any concurrent object that can be specified sequentially can be implemented in a wait-free manner using CAS. From simple locks to complex data structures, from blocking algorithms to non-blocking ones, CAS provides sufficient power to construct them all. This universality explains why every modern processor, from ARM to x86, from mobile chips to data center CPUs, provides CAS or its equivalent as a fundamental instruction.</p><p>But what exactly is CAS? At its core, Compare-And-Swap is an atomic operation that reads a memory location, compares it to an expected value, and only updates it to a new value if the comparison succeeds. In Java, this is exposed through classes like <code>AtomicInteger</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;d100c055-30b3-4944-8c58-1e0fd9a3ec46&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">import java.util.concurrent.atomic.AtomicInteger;

// CAS operation: atomically compare and update
AtomicInteger counter = new AtomicInteger(0);  // Initialize to 0

// Thread 1: Try to increment from 0 to 1
int expected = 0;        // What we expect the current value to be
int newValue = 1;         // What we want to set it to
boolean success = counter.compareAndSet(expected, newValue);
// If counter was 0, it's now 1 and success = true
// If counter was already changed by another thread, success = false

// Thread 2 (concurrent): Also tries to increment
int myExpected = 0;
int myNewValue = 1;
boolean mySuccess = counter.compareAndSet(myExpected, myNewValue);
// Only one thread will succeed - CAS guarantees atomicity
</code></pre></div><p>The key insight is that <code>compareAndSet</code> executes atomically: it reads the current value, compares it to <code>expected</code>, and only updates to <code>newValue</code> if they match. If another thread modified the value between the read and write, the operation fails and returns <code>false</code>, allowing the thread to retry. This atomicity, the guarantee that the comparison and update happen as a single, indivisible operation, is what makes CAS powerful enough to build universal constructions.</p><p>Understanding this journey from basic mutual exclusion to universal constructions isn&#8217;t just academic: it&#8217;s the foundation for reasoning about concurrent systems at scale.</p><p>In this post, we&#8217;ll explore:</p><ol><li><p><strong>Early mutual exclusion algorithms</strong> (Peterson&#8217;s, Bakery) that revealed the limitations of read/write operations</p></li><li><p><strong>Formal definitions</strong> of correctness (linearizability) and progress conditions that enable rigorous reasoning</p></li><li><p><strong>The consensus hierarchy</strong> that measures primitive power and reveals fundamental limitations</p></li><li><p><strong>Universal constructions</strong> that prove CAS can implement any concurrent object wait-free</p></li></ol><h2>The Synchronization Problem</h2><p><strong>Mutual exclusion is the foundational problem in concurrent programming, and early solutions revealed both the possibility and the inherent limitations of using only read/write operations.</strong> When multiple threads access shared resources, we need mechanisms to ensure critical sections execute atomically: one thread at a time. The pioneering algorithms from the 1960s through 1980s demonstrated that mutual exclusion could be achieved using only memory reads and writes, but at a cost that foreshadowed deeper theoretical constraints.</p><p>Peterson&#8217;s algorithm elegantly solved mutual exclusion for two threads using just two flags and a turn variable. Each thread signals its intent to enter the critical section, then yields priority to the other thread. The beauty lies in its simplicity: no special hardware instructions required, just careful ordering of reads and writes. Yet this simplicity masks a critical limitation: it only works for two threads. Extending Peterson&#8217;s approach to n threads requires exponentially complex tournament trees, and even then, threads must actively spin while waiting, burning CPU cycles.</p><p>Here&#8217;s Peterson&#8217;s algorithm implemented in Java, showing how mutual exclusion is achieved using only read/write operations:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;1f040ff5-de78-45ec-aa11-aa8c8acf1092&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">class PetersonsLock {
    // Flags indicating each thread's desire to enter critical section
    private volatile boolean[] flag = new boolean[2];
    // Turn variable: which thread should yield priority
    private volatile int turn;
    
    // Thread 0 calls this to acquire the lock
    public void lock0() {
        flag[0] = true;           // Signal intent to enter
        turn = 1;                 // Give priority to thread 1
        // Wait while thread 1 wants to enter AND it's thread 1's turn
        while (flag[1] &amp;&amp; turn == 1) {
            // Busy-wait: spin until condition is false
            Thread.yield();       // Hint to scheduler (optional)
        }
        // Now in critical section
    }
    
    // Thread 1 calls this to acquire the lock
    public void lock1() {
        flag[1] = true;           // Signal intent to enter
        turn = 0;                 // Give priority to thread 0
        // Wait while thread 0 wants to enter AND it's thread 0's turn
        while (flag[0] &amp;&amp; turn == 0) {
            Thread.yield();
        }
        // Now in critical section
    }
    
    // Thread 0 releases the lock
    public void unlock0() {
        flag[0] = false;          // Signal we're done
    }
    
    // Thread 1 releases the lock
    public void unlock1() {
        flag[1] = false;          // Signal we're done
    }
}
</code></pre></div><p>The algorithm works through careful coordination: each thread sets its flag to <code>true</code> (indicating desire to enter), then sets <code>turn</code> to favor the other thread. The thread waits (spins) only if both threads want to enter AND it&#8217;s the other thread&#8217;s turn. This ensures mutual exclusion: at most one thread can be in the critical section. However, notice the <code>while</code> loop: threads must continuously check the condition, consuming CPU cycles. This busy-waiting is the blocking behavior that weaker primitives force upon us.</p><p><strong>Why this matters in practice:</strong> In real systems, busy-waiting wastes CPU cycles that could be used for productive work. If a thread holding a lock is preempted or runs slowly, all waiting threads spin uselessly, consuming power and reducing overall throughput. This is why blocking algorithms can perform poorly under contention: they&#8217;re vulnerable to priority inversion, convoying (where slow threads delay fast ones), and wasted CPU cycles. Modern systems need synchronization mechanisms that provide progress guarantees even when some threads are slow or crash.</p><p>Lamport&#8217;s bakery algorithm generalized the solution to n threads by drawing inspiration from a bakery&#8217;s ticket system. Each thread takes a number, and threads enter in numerical order. This achieved both safety (mutual exclusion) and fairness (first-come, first-served), making it a significant theoretical advance. The algorithm&#8217;s elegance comes at the price of complexity: comparing ticket numbers requires careful handling of ties, and threads must scan all other threads&#8217; tickets before entering. More critically, like Peterson&#8217;s algorithm, bakery forces threads into busy-waiting: they&#8217;re blocked not by sleeping, but by continuously checking conditions.</p><p><strong>What these algorithms collectively demonstrate is profound: mutual exclusion is achievable with read/write operations alone, but the resulting solutions are inherently blocking.</strong> Whether through busy-waiting spins in Peterson&#8217;s protocol or ticket-checking loops in bakery, threads cannot make progress independently. They must wait, they must check, they must coordinate through shared memory locations that require constant polling. This blocking nature isn&#8217;t a flaw in the algorithms: it&#8217;s a fundamental consequence of the weakness of read/write operations themselves, a limitation that would prove mathematically inevitable.</p><p>But to prove that limitation mathematically, and to understand which primitives are truly necessary, we need formal definitions. What does it mean for a concurrent algorithm to be &#8220;correct&#8221;? How do we characterize different levels of blocking? These questions require precise answers before we can establish the hierarchy of primitive power.</p><h2>Defining Correctness</h2><p><strong>Before we can evaluate synchronization mechanisms or prove algorithms correct, we must first define what &#8220;correct&#8221; actually means in concurrent systems.</strong> In sequential programming, correctness is straightforward: given the same inputs, your function produces the expected output. But in concurrent programming, operations overlap in time, multiple threads interleave their actions unpredictably, and the same sequence of function calls can produce different results depending on timing. Without a formal definition of correctness, we&#8217;re left arguing subjectively about whether an implementation &#8220;works&#8221; or debating whether a test failure represents a real bug or just unfortunate timing.</p><p><strong>Linearizability provides the gold standard: a concurrent execution is correct if it appears equivalent to some sequential execution, where each operation takes effect instantaneously at some point between its invocation and response.</strong> This &#8220;linearization point&#8221; gives us a powerful mental model: despite the chaos of concurrent operations overlapping in time, we can reason about them as if they happened one at a time in some valid order. A concurrent queue is correct if it behaves like a sequential FIFO queue, just with operations atomically &#8220;snapping&#8221; into place at their linearization points. Critically, linearizability is compositional: if each individual object in your system is linearizable, the entire system is linearizable. This compositionality is what makes large-scale concurrent systems tractable: you can reason about components independently without worrying about how their combination might violate correctness.</p><p>A quick example makes this concrete. Suppose three threads hit a queue concurrently:</p><p>Plaintext</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;02e29041-bc9d-46f2-b51c-52fec49aae99&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Time --------------------------------------------&gt;

T1:  |--- enqueue(5) ---|
T2:       |--- enqueue(7) ---|
T3:            |-- dequeue() --|
</code></pre></div><p>These operations overlap, but linearizability says we can pick one instant within each operation&#8217;s interval where it &#8220;takes effect.&#8221; One valid linearization order: <code>enqueue(5)</code>, then <code>enqueue(7)</code>, then <code>dequeue() &#8594; 5</code>. The queue behaves as if those three calls happened sequentially in that order. Verifying correctness reduces to: does some valid sequential ordering exist that respects the real-time constraints?</p><p><strong>Beyond correctness, we need to characterize how much blocking we&#8217;re willing to tolerate, which progress conditions formalize into a precise hierarchy.</strong> Wait-freedom is the strongest guarantee: every thread completes its operation in a bounded number of steps, regardless of what other threads do, even if they crash or run arbitrarily slowly. Lock-freedom weakens this slightly: at least one thread always makes progress, though individual threads might starve. Obstruction-freedom weakens further: a thread makes progress if it eventually runs without interference. At the bottom sits traditional blocking synchronization using locks, where threads can wait indefinitely.</p><p>This hierarchy isn&#8217;t just theoretical taxonomy: it has direct performance implications. Wait-free algorithms never stall on slow threads, making them ideal for real-time systems. Lock-free algorithms avoid deadlock and convoying but may starve individual threads. Blocking algorithms are simpler to write but vulnerable to priority inversion, deadlock, and performance collapse under contention.</p><p>The progress conditions form a clear hierarchy from strongest to weakest guarantees:</p><p><strong>Progress ConditionGuaranteeNotesWait-Freedom</strong>Every thread completes in bounded stepsEven if others crash or are slow<strong>Lock-Freedom</strong>At least one thread always makes progressIndividual threads may starve<strong>Obstruction-Freedom</strong>Thread makes progress if it runs aloneMay block under contention<strong>Blocking (Locks)</strong>Threads can wait indefinitelyDeadlock, convoying possible</p><p>Each level weakens the guarantee: wait-freedom promises per-thread progress, lock-freedom promises system-wide progress, obstruction-freedom promises progress only when uncontended, and blocking makes no progress guarantees. This hierarchy helps us choose the right progress condition for our use case: real-time systems need wait-freedom, while many high-performance systems can tolerate lock-freedom&#8217;s potential starvation.</p><p><strong>These definitions, linearizability for correctness and progress conditions for liveness, form the vocabulary that makes rigorous reasoning about concurrent systems possible.</strong> Without linearizability, we couldn&#8217;t formally state what it means for a concurrent hash table or queue to be &#8220;correct.&#8221; Without progress conditions, we couldn&#8217;t distinguish between a lock-free algorithm that guarantees system-wide progress and a wait-free algorithm that guarantees per-thread progress. More importantly, these definitions set up the critical questions that follow: Can we achieve wait-freedom with just read/write operations? Do different atomic primitives offer different guarantees? The precision of these definitions enables the mathematical proofs and impossibility results that come next.</p><p>Armed with these definitions, we can now ask the fundamental question: Are all synchronization primitives equally powerful, or do some offer capabilities that others simply cannot provide? The answer, discovered through the consensus problem, reveals a strict hierarchy that explains why modern processors provide CAS.</p><h2>Primitive Power Hierarchy</h2><p><strong>Not all atomic operations are created equal: some primitives are fundamentally more powerful than others, capable of solving problems that weaker primitives cannot.</strong> We&#8217;ve seen that read/write operations suffice for mutual exclusion through algorithms like Peterson&#8217;s and Bakery. We&#8217;ve defined correctness through linearizability and characterized blocking through progress conditions. But a critical question remains: Are the primitives we choose merely a matter of convenience and performance, or do they fundamentally determine what&#8217;s algorithmically possible? Can we achieve wait-free synchronization with read/write registers alone, or do we need stronger hardware support?</p><p><strong>Atomic registers establish the baseline by exploring what they can and cannot achieve.</strong> Atomic registers, memory locations supporting atomic read and write operations, form the weakest primitive in our hierarchy. They demonstrate their power through atomic snapshots, a technique that allows multiple registers to be read &#8220;simultaneously&#8221; in a consistent state despite concurrent updates. An atomic snapshot reads all registers atomically, giving a consistent view even if other threads are modifying them. Multi-reader, multi-writer registers can be constructed from single-writer registers using techniques like atomic snapshots, proving that certain concurrent abstractions are achievable with patient engineering.</p><p>Yet throughout these constructions, a pattern emerges: algorithms using only registers require threads to help each other, retry operations, and fundamentally cannot guarantee that every thread completes in bounded steps. The constructions work, but they&#8217;re complex, and they hint at fundamental limitations lurking beneath the surface.</p><p><strong>The consensus problem and its associated hierarchy make these limitations precise.</strong> Consensus is deceptively simple: n threads each propose a value, and they must all agree on one of the proposed values. It&#8217;s the atomic commitment problem at the heart of distributed systems, the &#8220;all or nothing&#8221; decision that underlies everything from database transactions to leader election.</p><p><strong>Critically, consensus is different from mutual exclusion.</strong> While Lamport&#8217;s bakery algorithm demonstrates that mutual exclusion can be solved for n threads using only read/write operations (albeit with blocking), consensus is a fundamentally harder problem. Mutual exclusion ensures only one thread accesses a resource at a time: it&#8217;s about exclusion. Consensus requires all threads to agree on a single value: it&#8217;s about agreement. More importantly, the consensus number measures a primitive&#8217;s ability to solve consensus <strong>wait-free</strong>, not just solve it with blocking. While read/write operations can achieve mutual exclusion for many threads through blocking algorithms, they cannot achieve wait-free consensus for even two threads.</p><p>Here&#8217;s a concrete example of the consensus problem:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;f16d86fe-ff3f-43fa-afdb-e7ecc7e35998&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">// Consensus problem: n threads propose values, all must agree on one
// Example scenario:
//   Thread 1 proposes: "Alice"
//   Thread 2 proposes: "Bob"  
//   Thread 3 proposes: "Alice"
// All threads must agree on either "Alice" or "Bob" (one of the proposed values)
// This is harder than mutual exclusion because it requires agreement, not just exclusion
</code></pre></div><p>Herlihy&#8217;s breakthrough insight was that consensus serves as a measuring stick for primitive power. Every synchronization primitive has a &#8220;consensus number&#8221;: the maximum number of threads for which it can solve consensus <strong>wait-free</strong>. Read/write registers have consensus number 1 (they can&#8217;t even solve two-thread consensus wait-free). Test-and-set and swap have consensus number 2. Compare-and-swap, along with Load-Linked/Store-Conditional, have consensus number infinity: they can solve consensus for any number of threads.</p><p>Here&#8217;s how CAS solves 2-thread consensus, demonstrating its power:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;2e7d189e-baa2-4a56-a50a-9635f9014aa1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">import java.util.concurrent.atomic.AtomicReference;

class Consensus {
    private AtomicReference&lt;Object&gt; decision = new AtomicReference&lt;Object&gt;(null);
    
    /**
     * Solve consensus for 2 threads using CAS.
     * Each thread proposes a value, all agree on the first one to succeed.
     */
    public Object decide(Object proposed) {
        // Try to set decision to our proposed value
        // Only the first thread succeeds; others see non-null and return that value
        if (decision.compareAndSet(null, proposed)) {
            // We won! Our value is the decision
            return proposed;
        } else {
            // Another thread already decided; we agree with their choice
            return decision.get();
        }
    }
}

// Usage:
// Thread 1: result = consensus.decide("Alice")  // Might return "Alice" or "Bob"
// Thread 2: result = consensus.decide("Bob")     // Returns same value as Thread 1
// Both threads now have the same result - consensus achieved!
</code></pre></div><p>This simple implementation shows why CAS has consensus number infinity: it can solve consensus for any number of threads by ensuring only one thread&#8217;s proposal wins, and all others agree with that winner.</p><p>The consensus number tells us the maximum number of threads for which a primitive can solve the consensus problem wait-free. Primitives with higher consensus numbers are strictly more powerful: they can solve problems that weaker primitives cannot. This isn&#8217;t just a performance difference; it&#8217;s a fundamental computational limitation.</p><p>Here&#8217;s a comparison of how different primitives stack up:</p><p><strong>PrimitiveConsensus NumberCan Solve Mutual Exclusion?Wait-Free Consensus?Notes</strong>Read/Write1Yes (blocks)NoLamport&#8217;s algorithm works but requires blocking/busy-waitingTest-and-Set2YesYes (2 threads max)Limited to 2 threads for wait-free consensusCAS&#8734;YesYesUniversal - can solve wait-free consensus for any number of threads</p><p><strong>The impossibility result is what makes this hierarchy mathematically rigorous rather than empirical observation: you cannot solve wait-free consensus for two or more threads using only read/write registers.</strong> This isn&#8217;t a statement about clever algorithms we haven&#8217;t discovered yet: it&#8217;s a fundamental impossibility proven through valency arguments and careful reasoning about execution schedules. No matter how ingenious your algorithm, no matter how many registers you use or how cleverly you structure them, you cannot build a wait-free consensus protocol for two threads with read/write operations alone. This explains why Peterson&#8217;s and Bakery algorithms must block: the blocking isn&#8217;t a design choice, it&#8217;s a mathematical necessity given their primitive operations. If you want wait-free synchronization for multiple threads, you need primitives with higher consensus numbers. The hierarchy isn&#8217;t about performance optimization: it&#8217;s about what&#8217;s computationally possible.</p><p><strong>Why hardware designers care:</strong> When designing a processor, you face a fundamental question: which atomic instructions should you provide? The consensus hierarchy provides a clear answer: if you want software to be able to build wait-free concurrent algorithms, you must provide primitives with consensus number infinity (like CAS). Without CAS, certain classes of problems are literally impossible to solve wait-free. This isn&#8217;t a matter of performance: it&#8217;s a matter of computational capability. This is why every modern processor architecture converged on providing CAS or its equivalent: they recognized that weak primitives fundamentally limit what software can achieve.</p><p><strong>These insights fundamentally reframe how we think about hardware synchronization support.</strong> Processors don&#8217;t provide compare-and-swap just because it&#8217;s faster than building complex protocols with reads and writes: they provide it because certain problems are literally impossible to solve wait-free without it. The consensus hierarchy explains why modern architectures converged on CAS-like instructions: they recognized that weak primitives fundamentally limit what software can achieve. This sets up the final revelation: primitives with infinite consensus numbers aren&#8217;t just powerful: they&#8217;re universal.</p><p>But universality is a bold claim. Does having consensus number infinity mean CAS can solve consensus for many threads, or does it mean something more profound? The universal construction theorem provides the answer: CAS doesn&#8217;t just solve consensus: it can implement any concurrent object whatsoever.</p><h2>Universal Solution</h2><p><strong>The consensus hierarchy revealed a gap between primitives, but primitives with infinite consensus numbers bridge that gap completely.</strong> We know read/write registers cannot solve consensus for multiple threads. We know test-and-set gets us to two threads but no further. We know compare-and-swap has consensus number infinity. But infinity is a strange claim: does it simply mean &#8220;works for arbitrarily many threads,&#8221; or does it mean something more profound? The answer comes with mathematical precision: objects that solve consensus for n threads are universal for n threads. They can implement any concurrent object whatsoever.</p><p><strong>The universal construction provides the explicit algorithm: given any sequential specification of an object and a consensus primitive, you can build a wait-free concurrent implementation.</strong> The construction is elegant in its directness. Maintain a log of operations applied to the object. When a thread wants to perform an operation, it proposes that operation as the &#8220;next&#8221; one to apply. Threads use consensus to agree on which operation wins. The winner&#8217;s operation gets appended to the log and applied to the object state. All threads can then compute the result by replaying the log. Repeat for the next operation. This isn&#8217;t an optimization or a special case: it&#8217;s a fully general construction that works for any object you can specify sequentially: queues, stacks, hash tables, counters, priority queues, or objects not yet invented.</p><p>The universal construction algorithm proceeds as follows:</p><ol><li><p><strong>Operation proposal</strong>: When a thread invokes an operation, it creates an operation descriptor containing the operation type and arguments, then proposes this descriptor as the next entry in the shared operation log.</p></li><li><p><strong>Consensus decision</strong>: All threads concurrently proposing operations participate in a consensus protocol. The consensus primitive guarantees that exactly one proposal wins: this is the operation that will be applied next.</p></li><li><p><strong>Log append</strong>: The winning operation descriptor is atomically appended to the shared log. This log serves as the linearization order: operations appear in the order they were decided by consensus.</p></li><li><p><strong>State reconstruction</strong>: Each thread independently replays the log from the beginning, applying each operation sequentially to reconstruct the current object state. Since all threads see the same log, they compute identical states.</p></li><li><p><strong>Result computation</strong>: Threads compute the operation&#8217;s return value by examining the reconstructed state. For read operations, this is straightforward. For write operations, the result may depend on the state after applying the operation.</p></li><li><p><strong>Completion</strong>: The thread returns the computed result. Since consensus is wait-free (each thread completes in bounded steps), and log replay is deterministic, the entire operation completes wait-free.</p></li></ol><p>The key insight is that consensus serializes operations (establishing a total order), while log replay ensures all threads compute consistent results without requiring explicit coordination beyond the consensus protocol itself.</p><p>Here&#8217;s a simplified example of how the universal construction builds a concurrent queue. The sequential specification is straightforward: a queue supports <code>enqueue(item)</code> and <code>dequeue()</code> operations that follow FIFO order.</p><p></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;45dcd65b-f361-4a6a-8279-2cbb7711ed0b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">// Simplified universal construction for a queue
class UniversalQueue&lt;T&gt; {
    private List&lt;Operation&gt; log = new ArrayList&lt;&gt;();  // Operation log
    private Queue&lt;T&gt; state = new LinkedList&lt;&gt;();      // Sequential state
    
    // Consensus object to decide next operation
    private Consensus&lt;Operation&gt; consensus = new Consensus&lt;&gt;();
    
    public void enqueue(T item) {
        // Propose enqueue operation
        Operation op = new EnqueueOp(item);
        
        // Use consensus to decide if this operation wins
        Operation winner = consensus.decide(op);
        
        // Append winner to log
        synchronized(log) {
            log.add(winner);
        }
        
        // All threads replay log to compute current state
        replayLog();
    }
    
    public T dequeue() {
        Operation op = new DequeueOp();
        Operation winner = consensus.decide(op);
        
        synchronized(log) {
            log.add(winner);
        }
        
        replayLog();
        
        // Return result based on final state
        return state.poll();  // Simplified - actual implementation tracks results
    }
    
    private void replayLog() {
        // Replay all operations to compute current state
        state.clear();
        for (Operation op : log) {
            op.apply(state);
        }
    }
}
</code></pre></div><p>This demonstrates the universal construction pattern: operations are proposed, consensus decides the winner, the log grows, and all threads independently compute results. While this simplified version has performance limitations (everyone replays the entire log), optimized versions use techniques like helping and early termination. The key insight is that CAS-based consensus makes this construction wait-free: every thread completes in bounded steps regardless of others&#8217; behavior.</p><p><strong>Why software engineers benefit:</strong> The universal construction provides a systematic recipe for building concurrent objects. Instead of inventing clever tricks for each data structure, you can apply the universal construction to any sequential specification. While optimized implementations often outperform the universal construction, it serves as a correctness proof: if CAS can build it wait-free using the universal construction, then optimized wait-free implementations are possible. This gives you confidence when designing concurrent systems: you know that CAS provides sufficient power to build whatever you need. Real-world systems like Java&#8217;s <code>ConcurrentHashMap</code> use sophisticated CAS-based algorithms that outperform the universal construction, but the universality theorem guarantees that such implementations exist.</p><p><strong>What makes this truly universal is that it guarantees wait-freedom: every thread completes its operation in a bounded number of steps.</strong> No thread waits for locks. No thread spins checking conditions. No thread can be blocked by slower threads or crashed threads. Each thread proposes, participates in consensus, computes the result, and completes, all in predictable, bounded time. This is the theoretical ideal of concurrent programming: the responsiveness of sequential code combined with the scalability of parallel execution. The construction proves that wait-freedom isn&#8217;t some unattainable dream requiring clever tricks for each data structure: it&#8217;s a systematic consequence of having consensus objects.</p><p><strong>This universality extends beyond wait-free algorithms to encompass the entire space of concurrent programming.</strong> The construction can implement locks themselves: mutual exclusion becomes just another concurrent object built atop consensus. It can implement semaphores, barriers, read-write locks, any synchronization primitive we&#8217;ve discussed or will invent. More subtly, while the universal construction produces wait-free implementations, consensus objects can also be used to build lock-free or even blocking implementations with different performance tradeoffs. The point isn&#8217;t that CAS forces you into wait-free algorithms; it&#8217;s that CAS gives you the power to choose. With weaker primitives like read/write registers, certain algorithmic approaches are simply impossible. With CAS, every approach becomes possible.</p><h3>Performance Implications</h3><p>Understanding when CAS helps versus when it might hurt is crucial for practical system design. CAS excels in low-contention scenarios where threads rarely conflict: operations typically succeed on the first attempt, providing excellent performance without the overhead of lock acquisition. CAS also shines when you need progress guarantees: wait-free and lock-free algorithms built with CAS never deadlock and provide stronger liveness guarantees than traditional locks.</p><p>However, CAS has trade-offs. Under high contention, CAS can suffer from cache line bouncing: multiple threads repeatedly modifying the same memory location cause expensive cache coherence traffic. In extreme cases, a simple lock might perform better because it serializes access and reduces cache misses. The retry loops in CAS-based algorithms can also waste CPU cycles when many threads compete, though at least one thread always makes progress (lock-freedom).</p><p>The choice between wait-free, lock-free, and blocking approaches depends on your requirements:</p><ul><li><p><strong>Wait-free</strong>: Best for real-time systems where every thread must complete in bounded time, even if others crash. Higher overhead but strongest guarantees.</p></li><li><p><strong>Lock-free</strong>: Good for high-performance systems where deadlock is unacceptable but some starvation is tolerable. Better scalability than locks under contention.</p></li><li><p><strong>Blocking (locks)</strong>: Simplest to reason about and often fastest under high contention due to reduced cache traffic. Vulnerable to deadlock and priority inversion.</p></li></ul><p>Modern systems often use hybrid approaches: CAS for hot paths with low contention, locks for high-contention scenarios, and sophisticated lock-free data structures (like Java&#8217;s <code>ConcurrentHashMap</code>) that combine multiple techniques.</p><p>Here&#8217;s a practical example: a lock-free counter implemented using CAS. This demonstrates CAS&#8217;s power in a simple, concrete form:</p><p></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;1ec927f6-3062-4376-af68-42ba2ff30ae5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">import java.util.concurrent.atomic.AtomicInteger;

class LockFreeCounter {
    // CAS-based counter: no locks, lock-free increment
    private AtomicInteger value = new AtomicInteger(0);
    
    /**
     * Increment the counter atomically using CAS.
     * This is lock-free: at least one thread makes progress, but retries may be unbounded.
     */
    public void increment() {
        int current;
        do {
            // Read current value
            current = value.get();
            // Try to update: CAS(current, current+1)
            // If another thread changed value, this fails and we retry
        } while (!value.compareAndSet(current, current + 1));
        // Loop exits when CAS succeeds (we won the race)
    }
    
    /**
     * Get the current counter value.
     * This is a simple read, always wait-free.
     */
    public int get() {
        return value.get();
    }
    
    /**
     * Decrement the counter atomically using CAS.
     * Same pattern as increment: retry until CAS succeeds.
     */
    public void decrement() {
        int current;
        do {
            current = value.get();
        } while (!value.compareAndSet(current, current - 1));
    }
}
</code></pre></div><p>The key pattern is the CAS loop: read the current value, attempt to update it, and retry if another thread modified it in between. This is lock-free (at least one thread makes progress) but not wait-free, as retries are unbounded: a thread could theoretically retry indefinitely if other threads keep modifying the value. Contrast this with Peterson&#8217;s algorithm, which requires busy-waiting and blocking. CAS gives us the power to build non-blocking algorithms that scale under contention.</p><p><strong>The practical impact explains the hardware landscape we inhabit today.</strong> Every modern processor architecture, from x86&#8217;s CMPXCHG to ARM&#8217;s LDREX/STREX to RISC-V&#8217;s LR/SC to SPARC&#8217;s CAS, provides compare-and-swap or its equivalent precisely because universality isn&#8217;t just theoretical elegance: it&#8217;s engineering necessity. When designing a processor, you could provide dozens of specialized atomic instructions for different data structures. Or you could provide one universal primitive and let software build everything else. The consensus hierarchy proved that some primitives are fundamentally insufficient. The universality theorem proved that CAS is fundamentally sufficient. This is why CAS became the assembly language of concurrency: not through committee decision or vendor preference, but through mathematical inevitability. If your hardware provides consensus objects, your software can build anything. And that &#8220;anything&#8221; includes both the sophisticated lock-free algorithms powering high-performance systems and the simple, correct locks that make everyday programming tractable.</p><h2>Key Takeaways</h2><p>Before we conclude, let&#8217;s summarize the essential insights:</p><ul><li><p><strong>CAS is universal</strong>: Any concurrent object that can be specified sequentially can be implemented wait-free using CAS. This isn&#8217;t just convenient: it&#8217;s mathematically proven.</p></li><li><p><strong>Consensus number measures primitive power</strong>: Every synchronization primitive has a consensus number: the maximum number of threads for which it can solve consensus wait-free. Higher consensus numbers mean strictly more powerful primitives.</p></li><li><p><strong>Wait-free consensus for 2+ threads is impossible with read/write alone</strong>: This impossibility result explains why Peterson&#8217;s and Bakery algorithms must block: it&#8217;s not a design choice, it&#8217;s a mathematical necessity.</p></li><li><p><strong>Modern processors provide CAS because it&#8217;s necessary, not just convenient</strong>: Hardware designers recognized that certain problems are literally impossible to solve wait-free without CAS-like primitives. This is why every modern architecture converged on CAS.</p></li><li><p><strong>The universal construction provides a systematic approach</strong>: Rather than inventing clever tricks for each data structure, the universal construction gives us a general recipe for building wait-free concurrent objects from consensus primitives.</p></li><li><p><strong>Performance trade-offs matter</strong>: CAS excels under low contention but can suffer from cache line bouncing under high contention. The choice between wait-free, lock-free, and blocking approaches depends on your specific requirements.</p></li></ul><h2>Conclusion</h2><p>The journey from Peterson&#8217;s algorithm to universal constructions isn&#8217;t just a historical progression: it&#8217;s a logical proof that unfolded over decades. Each step builds on the previous, moving from concrete examples to abstract principles, from intuitive algorithms to mathematical impossibility results, and finally to the profound realization that one primitive can serve as the foundation for all concurrent programming.</p><p>This theoretical foundation has profound practical implications. When you reach for a concurrent data structure library like Java&#8217;s <code>java.util.concurrent</code>, you&#8217;re benefiting from algorithms built on CAS. When you debate lock-free versus locked implementations, you&#8217;re weighing trade-offs that the consensus hierarchy makes precise. When you evaluate whether your architecture provides adequate synchronization support, you&#8217;re applying insights that explain why every modern processor provides CAS.</p><p>For practicing engineers, understanding the consensus hierarchy provides a framework for making informed decisions:</p><ul><li><p><strong>Choose CAS-based algorithms</strong> when you need progress guarantees and can tolerate some retry overhead</p></li><li><p><strong>Understand the limitations</strong> of read/write operations: they can solve mutual exclusion but require blocking</p></li><li><p><strong>Recognize that CAS universality</strong> means you can build any concurrent object, but optimized implementations often outperform the universal construction</p></li><li><p><strong>Appreciate why hardware matters</strong>: processors provide CAS not as a convenience, but as a necessity for certain classes of problems</p></li></ul><p>As concurrent systems continue to scale, from multi-core processors to distributed systems spanning continents, the principles established by the consensus hierarchy remain foundational. CAS isn&#8217;t just another instruction in the processor&#8217;s repertoire. It&#8217;s the universal building block that makes modern concurrent systems possible, and understanding why it&#8217;s universal helps us build better systems for the future.</p><h2>Bonus: Implementing a Lock with CAS</h2><p>To make the universality of CAS concrete, let&#8217;s implement a simple spin lock using only CAS operations. This demonstrates how CAS can build the fundamental synchronization primitive, mutual exclusion, that we started with.</p><p>A lock needs to track whether it&#8217;s currently held. We&#8217;ll use an <code>AtomicInteger</code> where <code>0</code> means unlocked and <code>1</code> means locked. The <code>lock()</code> method must atomically check if the lock is <code>0</code> and set it to <code>1</code> if so. The <code>unlock()</code> method simply sets it back to <code>0</code>.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;dc81cb25-0b96-4dfc-b482-01dac8d2c9f8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">import java.util.concurrent.atomic.AtomicInteger;

public class CASLock {
    private final AtomicInteger state = new AtomicInteger(0); // 0 = unlocked, 1 = locked
    
    /**
     * Acquire the lock by atomically transitioning from unlocked (0) to locked (1).
     * Spins until successful - this is lock-free (at least one thread makes progress)
     * but not wait-free (a thread may spin indefinitely).
     */
    public void lock() {
        // Keep trying until we successfully change state from 0 to 1
        while (!state.compareAndSet(0, 1)) {
            // Lock is held by another thread - spin (busy-wait)
            // In production, you might add Thread.yield() or exponential backoff
        }
        // We successfully acquired the lock
    }
    
    /**
     * Release the lock by setting state back to unlocked (0).
     * This is wait-free - always completes in one step.
     */
    public void unlock() {
        // Simply set state back to 0
        // No CAS needed - only the lock holder calls unlock()
        state.set(0);
    }
    
    /**
     * Try to acquire the lock without blocking.
     * Returns true if lock was acquired, false otherwise.
     */
    public boolean tryLock() {
        return state.compareAndSet(0, 1);
    }
}
</code></pre></div><p><strong>How it works:</strong></p><p>The <code>lock()</code> method uses CAS in a retry loop: it attempts to atomically change the state from <code>0</code> (unlocked) to <code>1</code> (locked). If another thread already holds the lock, the CAS fails (because state is already <code>1</code>), and the thread retries. Only one thread can successfully transition from <code>0</code> to <code>1</code>, ensuring mutual exclusion.</p><p><strong>Why this matters:</strong></p><p>This implementation demonstrates CAS&#8217;s power in a concrete way. We&#8217;ve built mutual exclusion, the problem Peterson&#8217;s algorithm solved with read/write operations, using CAS. Unlike Peterson&#8217;s algorithm, this lock:</p><ul><li><p>Works for any number of threads (not just two)</p></li><li><p>Uses a single memory location (not multiple flags and turn variables)</p></li><li><p>Is simpler to understand and reason about</p></li></ul><p>However, this is a <strong>spin lock</strong>: threads busy-wait when the lock is held. In practice, production locks combine CAS with OS-level blocking primitives (like <code>futex</code> on Linux) to avoid wasting CPU cycles. But the core mechanism, using CAS to atomically transition between states, remains the same.</p><p><strong>The deeper insight:</strong></p><p>This lock implementation is lock-free (at least one thread always makes progress) but not wait-free (individual threads may spin indefinitely). To build a wait-free lock, you&#8217;d need more sophisticated techniques, but the universality theorem guarantees such implementations exist: CAS provides sufficient power to build them.</p><p>This simple example illustrates why CAS is universal: if you can build locks with CAS, and locks can build any synchronization primitive, then CAS can build anything. The universal construction provides the general recipe; this lock is a concrete, practical example of CAS&#8217;s power.</p><h2>References</h2><ul><li><p>Herlihy, M., &amp; Shavit, N. (2012). <em>The Art of Multiprocessor Programming</em> (Revised First Edition). Morgan Kaufmann.</p></li><li><p>Herlihy, M. (1991). Wait-free synchronization. <em>ACM Transactions on Programming Languages and Systems (TOPLAS)</em>, 13(1)</p></li><li><p>Herlihy, M. (1991). Impossibility and universality results for wait-free synchronization. <em>Proceedings of the seventh annual ACM symposium on Principles of distributed computing</em></p></li><li><p>Java <code>java.util.concurrent</code> package: Real-world implementations of CAS-based concurrent data structures.</p></li><li><p>Peterson, G. L. (1981). Myths about the mutual exclusion problem. <em>Information Processing Letters</em>, 12(3)</p></li><li><p>Lamport, L. (1974). A new solution of Dijkstra&#8217;s concurrent programming problem. <em>Communications of the ACM</em>, 17(8)</p></li></ul>]]></content:encoded></item></channel></rss>