<?xml version="1.0" encoding="utf-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>&quot;Anirudh Rowjee&quot;</title><link>https://rowjee.com/feed.xml</link><description>&quot;Learning, Building, and Breaking | SWE Storage @ Couchbase&quot;</description><item><title>On Student Technical Communities</title><link>https://rowjee.com/blog/student_tech_communities.html</link><description><![CDATA[Here's what I've learnt]]></description><author>null</author><pubDate>Fri, 18 Apr 2025 09:00:00 +0000</pubDate><content:encoded><![CDATA[<p>I've been a part of and led a few tech-based student communities throughout my college life. Here are some things I've learnt:</p>
<ol>
<li>Every community needs a long-term vision if you want to survive. All decisions need to be made in alignment with that vision, and it can change, but it needs to exist. Without a long-term vision, there is no coherent direction on what actions to take, and more importantly, there is no north star according to which the community will align itself.</li>
<li>People who truly believe in the vision are the ones who will take the community forward. The community will see many types of people who interact with it, but what differentiates the long-term folks from the passerby is that the long-term folks are believers at heart, and to whatever feasible extent, put the aims of the community before that of their own. The ideal situation is where the aims of the person and the community intersect in harmonious ways.</li>
<li>People will remember the fun times much more than they will the work. That's where lasting memories are made. Being serious all the time with no room for goofing off is a recipe for much more of a corporate environment than is suitable for a student community.</li>
<li>Build stuff together. It's the single highest ROI activity you can do as a tech community; it's a space for everyone to learn and grow together, and the pride of seeing your own code being used to make things happen is a fantastic thing to experience as a student.</li>
<li>Conflict is human and cannot be avoided. It's important to remember that knowing how to manage conflict and work with other people makes you a much better engineer, because at the end of the day, it's other humans that you're working with, no matter how they write their code. Conflict can also teach us a lot about how to build things together, and how to bring together conflicting viewpoints to work towards the same goal.</li>
<li>Proof of Work is critical. There is no better testament to how good a community is than the visible work they have, be it blog posts, projects (another reason to build stuff together), or even a well-authored reel.</li>
<li>Mutual respect is key. Not having this makes it a toxic environment for everyone.</li>
</ol>
]]></content:encoded></item><item><title>Exploring Clickhouse and the MergeTree Engine</title><link>https://rowjee.com/blog/clickhouse_mergetree.html</link><description><![CDATA[This is a small transcript of a talk about the MergeTree Engine]]></description><author>null</author><pubDate>Thu, 10 Aug 2023 09:00:00 +0000</pubDate><content:encoded><![CDATA[<p>Today we're learning about Clickhouse, which is a column-oriented DBMS that's
optimized for speed and performance.</p>
<h2><code>primary.idx</code></h2>
<ol>
<li>What is an Engine, and DBMS 101</li>
<li>Why Clickhouse?
<ol>
<li>It's just fast</li>
<li>It's fast because -&gt; &quot;to go faster, do less stuff&quot;</li>
<li>Columnarity as an advantage</li>
</ol>
</li>
<li>Clickhouse 101
<ol>
<li>Table Creation and Reads</li>
<li>Parts, Partitions, and Sort Order</li>
<li>No Primary Keys?</li>
<li>Indexing?
<ol>
<li>Data Skipping Indexes (Bloom Filters)</li>
</ol>
</li>
<li>But What about Concurrency?
<ol>
<li>Clickhouse uses MVCC -&gt; No Locks, non-blocking inserts</li>
</ol>
</li>
</ol>
</li>
<li>Introducing the MergeTree Engine
<ol>
<li>Why is it called MergeTree?</li>
<li>What's the file structure like?
<ol>
<li>Sparse Indices</li>
<li>Parts</li>
<li>Marks</li>
</ol>
</li>
<li>The Basic Principle: merging parts in the background
<ol>
<li>Variants -&gt; CollapsingMergeTree, SummingMergeTree</li>
</ol>
</li>
</ol>
</li>
<li>Why are Writes Fast?
<ol>
<li>Writing as parts</li>
<li>Compression</li>
<li>Wide and Narrow Layouts</li>
<li>Mark Files and Sparse Indexing</li>
<li>Auto-shifting data between HDD and SSD (Multiple Block Devices feature)</li>
</ol>
</li>
<li>Why are Reads Fast?
<ol>
<li>The Primary Key is a superpower
<ol>
<li>How the Sparse Index helps eliminate granules (to go faster, do less
stuff)</li>
</ol>
</li>
</ol>
</li>
</ol>
<p>Approach - Start off with how data is stored, and how that makes things faster -
focus more on organization; data structures exist to make lives easier, choosing
the right data structure is essential</p>
<h2>What is an Engine?</h2>
<p>To understand what an Engine is, we must first understand the components of a
database.</p>
<p><img src="https://www.oreilly.com/api/v2/epubs/9781492040330/files/assets/dbin_0101.png" alt="" /></p>
<p><img src="https://cdn.mindmajix.com/blog/images/sql-server-architecture-060223.png" alt="" /></p>
<p>Here, we see that a DBMS is nothing but a lot of layers of abstraction that
terminate at the storage level. These abstractions inlcude a query parser, a
query processor, a compiler, a planner, and a transaction manager. All of these
ultimately work towards ensuring that data that has been stored can be queried
and manipulated in the most optimal manner possible.</p>
<blockquote>
<p>&quot;The storage engine is the part of the DBMS that's responsible for storing,
retrieving and managing data in-memory and on-disk&quot; ~ Alex Petrov, Database
Internals</p>
</blockquote>
<p>So, given the ultimate dependence of the rest of the database on storage and
retrieval, the storage engine sometimes supports or implements other features
like transactions to give the DBMS developers fine-grained control of what's
going on.</p>
<p>The TL;DR is that the storage engine is what supports storing and retrieving
data as efficiently as possible, and the rest of the DBMS is built around it,
and using it. A DBMS is useless without a good storage engine.</p>
<h2>What is Clickhouse?</h2>
<blockquote>
<p>ClickHouse® is a high-performance, column-oriented SQL database management
system (DBMS) for online analytical processing (OLAP).</p>
</blockquote>
<p>Clickhouse is what we use here in prod at Bytebeam.</p>
<p>It's a column-oriented DBMS, which means that data is grouped by column, not by
row. Through sheer locality and cache-friendliness, this decision to store data
by column and not by row makes a massive difference in performance, especially
for analytical usecases where column-wise aggregate statistics (mean, median,
mode, min, max) and groupings (group by) are extremely common.</p>
<h3>OLAP vs OLTP</h3>
<p>When we think of a database, we think of postgres or mysql. Clickhouse is
similar, but it's an OLAP database, not an OLTP Database. The scope of problems
is different here -</p>
<ol>
<li>Dealing with <em>massive</em> datasets -&gt; Billions or Trillions of rows</li>
<li>The tables have many, many columns</li>
<li>Of these columns, any query needs only a few columns</li>
<li>results <em>must</em> be returned in milliseconds or seconds</li>
</ol>
<p>So, given these constraints, Clickhouse has managed to find a space in which it
can make signinficant optimizations with the entire DBMS.</p>
<h2>Why is Clickhouse Fast?</h2>
<p>To go fast, do less stuff.</p>
<p>Clickhouse makes some pretty impressive claims - &quot;Query a billion rows in
milliseconds&quot; is no small matter. There are multiple choices that they took at a
design and architecture level to make sure their performance was top-notch, so
in reality it is <em>possible</em> for them to query a billion rows in milliseconds.</p>
<p>Let's explore a few of these choices
<a href="https://clickhouse.com/docs/en/intro">listed here</a>.</p>
<h2>What difference does the engine make? Isn't it just a part of the architecture?</h2>
<p>So as we've been able to see, the reason clickhouse is able to be so performant
is that it has optimizations that reach all the way down to the storage layer
and the format. The engine can be considered to be the bottleneck of the entire
operation, as it issues the syscalls necessary to persist, fetch, or re-organize
data.</p>
<p>You can force Direct I/O (Kernel Page/Buffer Cache Bypass) in Clickhouse with
the following</p>
<pre><code>SET min_bytes_to_use_direct_io=1
</code></pre>
<ul>
<li>clickhouse parallelizes I/O really well. (how?)</li>
<li>If clickhouse was a car, it would be a drag racer. It doesn't have an
optimizer (but somehow has transactional catalogs! Non-ACID Transactions!</li>
</ul>
<p>Materialized Views can be thought of as synchronous post-insert triggers!!
last-point queries in time-series data -&gt; the latest sample of data double-delta
encoding for time-series data ~ 99.9% compression ratio</p>
<p>Distributed Joins are <em>not</em> optimized in Clickhouse.</p>
<blockquote>
<p>Sharding and replication -&gt; Multi-master, eventually consistent</p>
</blockquote>
<h2>The MergeTree Engine</h2>
<p><a href="https://ibb.co/6PTQBmh"><img src="https://i.ibb.co/BZQkNtD/2023-08-10-17-45.png" alt="2023-08-10-17-45" border="0"></a></p>
<ul>
<li>MergeTree Layout consists of indexed chunks of data (every ~8000 or so rows,
sparse index) -&gt; Sorted and Compressed. Sprase index (<code>primary.idx</code>) -&gt;
multiple granules (distance between rows) <code>.mrk</code> file -&gt; maps granules to a
compressed segment inside the <code>.bin</code> file</li>
</ul>
<p>Insert data into table, atomic merge in the background - you can somehow
instantly query them? &quot;Instantly query after insert, but optimize over time&quot;</p>
<p>deletes are expensive! you need to rewrite the entire part.</p>
<h3>Why should you know?</h3>
<p>The more you understand how the engine works, the faster you can drive it.</p>
<h3>The execution Model</h3>
<p>Key Components:</p>
<ul>
<li>Thread</li>
<li>Hash Table</li>
</ul>
<h4>Table Creation</h4>
<pre><code class="language-sql">CREATE TABLE IF NOT EXISTS sdata (
	DevId Int32,
	Type String,
	MDate Date,
	MDatetime Datetime,
	Value Float64
	-- This is the table engine - there are many variants but can only be one per table
) ENGINE = MergeTree()

-- This is how we'll partition the data (break to pieces in a reasonable way)
PARTITION BY toYYYYMM(MDate)
-- This is how we'll index and sort the data (A Clustered Index)
ORDER BY (DevId, MDatetime)
</code></pre>
<h4>Insert Processing</h4>
<pre><code class="language-sql">INSERT INTO sdata VALUES
(15, 'TEMP', '2018-01-01', '2018-01-01 23:29:55', 18.0),
(15, 'TEMP', '2018-01-01', '2018-01-01 23:30:56', 18.7),
</code></pre>
<p>Once this is done, data is assembled in memory (post parsing and planning)</p>
<ul>
<li>rows pulled in mem</li>
<li>&quot;part&quot; of the table created, sorted, and index created, too</li>
<li>once this is done, we store in the file</li>
</ul>
<p>Basic parallelization to make inserts run faster. Each thread works on one part.</p>
<pre><code>set max_insert_threads = 4
</code></pre>
<blockquote>
<p><em>Q: What is a Part?</em></p>
</blockquote>
<h4>Storage Structure</h4>
<p><img src="" alt="" /></p>
<p><a href="https://altinity.com/wp-content/uploads/2022/05/So-Thats-Why-Its-So-Fast-An-Introduction-to-ClickHouse-Internals-2022-05-16.pdf">https://altinity.com/wp-content/uploads/2022/05/So-Thats-Why-Its-So-Fast-An-Introduction-to-ClickHouse-Internals-2022-05-16.pdf</a></p>
<p>A Table consists of multiple <em>Parts</em> A Part consists of a <em>Sparse Index</em> and a
set of <em>Columns</em> - Clickhouse uses the primary key as the sort order, same as
the clustered index</p>
<p>Section -&gt; 8000~ or so rows in one Granule (Clickhouse is optimized for
aggregates, not point lookups)</p>
<p><code>.mrk</code> -&gt; Mark file, index from primary key to point in compressed <code>.bin</code> file
that may or may not contain multiple granules</p>
<p>MergeTree: Because it merges parts in the background!</p>
<ul>
<li>Also known as compaction (see tsdbs)</li>
<li>Also see: Log-Structured Storage (super, super interesting) and LSM Trees</li>
</ul>
<p><strong>Bigger parts are more efficient!</strong></p>
<ul>
<li><code>PARTITION BY</code> should give you large partitions</li>
<li>Insert in BULK (~10s of Millions of Rows), keeps merge count low -&gt; Batching
is GOOD!</li>
</ul>
<blockquote>
<p>ClickHouse cannot use an index if the values of the primary key in the query
parameter range do not represent a monotonic sequence. In this case,
ClickHouse uses the full scan method.</p>
</blockquote>
<p>For example, the days of the month are partially monotonic sequences.</p>
<h4>Sources</h4>
<ul>
<li><a href="https://www.youtube.com/watch?v=fGG9dApIhDU">https://www.youtube.com/watch?v=fGG9dApIhDU</a></li>
<li><a href="https://www.youtube.com/watch?v=ZOZQCQEtrz8">https://www.youtube.com/watch?v=ZOZQCQEtrz8</a></li>
<li><a href="https://clickhouse.com/docs/en/intro">https://clickhouse.com/docs/en/intro</a></li>
<li><a href="https://clickhouse.com/docs/en/about-us/distinctive-features">https://clickhouse.com/docs/en/about-us/distinctive-features</a></li>
<li><a href="https://clickhouse.com/docs/en/engines/table-engines">https://clickhouse.com/docs/en/engines/table-engines</a></li>
<li><a href="https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/mergetree#mergetree">https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/mergetree#mergetree</a></li>
<li><a href="https://www.youtube.com/watch?v=XpkFEj1rVXg">https://www.youtube.com/watch?v=XpkFEj1rVXg</a></li>
<li><a href="https://posthog.com/handbook/engineering/clickhouse/data-storage">https://posthog.com/handbook/engineering/clickhouse/data-storage</a></li>
</ul>
]]></content:encoded></item><item><title>Linux IO Subsystem Walkthrough</title><link>https://rowjee.com/blog/linux_io_subsystem_walkthrough.html</link><description><![CDATA[High-level overview of the Linux Kernel IO Subsystem]]></description><author>null</author><pubDate>Thu, 9 Apr 2026 09:00:00 +0000</pubDate><content:encoded><![CDATA[<p><img src="https://www.thomas-krenn.com/en/wikiEN/images/d/d1/Linux-storage-stack-diagram_v6.18.png" alt="" /></p>
<p>This post hopes to walk through some basics of how IO works in Linux. For now, we'll only be considering the interaction of applications with block-based Filesystems, and how they interact with the rest of the subsystems beneath them, more specifically the Block Layer, the Block IO Scheduler, and the request-based device drivers.</p>
<p>The goal here is for both me, and you, dear reader, to understand how the IO subsystem works. To write this post, I will be using two primary sources of information on how this works:</p>
<ol>
<li>Other blog posts and articles about how the IO subsystem works, including, but not limited to, kernel documentation</li>
<li>A debugging session that'll trace through what actually goes on when we <code>open</code>, <code>read</code>, and <code>write</code> a file.</li>
</ol>
<p>Roughly, this is what we're looking at for this blog post.</p>
<pre class="mermaid">
flowchart TD
	A[Application] -- read(2), write(2), open(2), chmod(2)--> B[VFS Layer]
	B --> C[Block-based FS ext4]
	C --> D[Block Layer with IO Scheduler]
	D --> E[Request-Based Disk Driver]
</pre>
<h2>Design constraints faced by the kernel</h2>
<p>Let's consider for a moment that the kernel's job is to make a finite amount of resources (files, memory, CPU) appear as though it's infinite. To this end, the kernel will need to wrap many of the underlying subsystems in abstractions to maintain the &quot;illusion of infinity&quot;. For example, the application doesn't know that the memory layout exists a certain way in the actual hardware; it simply assumes that the memory it asks for is given to it, with a far more limited error handling surface than the kernel actually has to deal with.</p>
<p>The point I'm trying to make is that underneath every seemingly simple operation there's a lot of complexity.  The application doesn't (and need not) know how all the files on disk are organized relative to each other; it should simply be able to ask to read a file and get it.</p>
<h2>System Calls</h2>
<p>How does a program make something happen on the host computer? For example, while it is reasonable for us to expect a program to be able to work on its unique functionality, some actions (such as allocating memory, reading/writing from/to files on disk, displaying an image on the monitor) are left in the hands of the Kernel, whose sole function is to perform these duties. This is accomplished by exposing various kernel APIs to the programs, so that when the program wishes to, say, allocate some memory, it can &quot;ask&quot; or &quot;call into&quot; the kernel to do so.</p>
<p>This is known as a system call, or a syscall. These are specialized types of functions that don't exist in the application source code, but are instead run by the kernel. If you've programmed in C before, there's a good chance you've directly used some of these syscalls before - remember <code>malloc</code>?</p>
<p>(more about how system calls work here, if patience permits)</p>
<p>Now, why are system calls important? Doing IO is something we must ask the kernel to do for us, which is why every IO operation happens via a syscall. <code>open</code>, <code>read</code>, <code>write</code> are all syscalls.</p>
<h2>Some code</h2>
<p>Even in C, we'll usually access these syscalls via a standard library of some sort. Here, it's <code>stdio.h</code>.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;

int main() {

  FILE *myfile = NULL;
  myfile = fopen(&quot;hello.txt&quot;, &quot;w+&quot;); // &lt;- HERE
  // elided error handling

  char *text = &quot;hello, world!\0&quot;;
  size_t stat = 0;

  stat = fwrite(text, sizeof(char), 14, myfile);
  // elided error handling

  // read the same data
  char *newdata = malloc(sizeof(char) * 14);
  // elided error handling

  fseek(myfile, 0, SEEK_SET);

  stat = fread(newdata, sizeof(char), 14, myfile);
  // elided error handling

  size_t cmpres = strncmp(newdata, text, 14);
  // elided error handling

  free(newdata);
  return 0;
}
</code></pre>
<p>In the first part that we're concerned about, we'll look at the <code>FILE</code> object, which points to a file descriptor under the hood.</p>
<pre><code class="language-c">FILE *myfile = NULL;
myfile = fopen(&quot;hello.txt&quot;, &quot;w+&quot;); // &lt;- HERE
</code></pre>
<p>When we call <code>fopen</code>, the IO library <code>stdio</code> does the following:</p>
<p>~~I'm currently on MacOS, so here's the source to apple libc's implementation of <code>fopen</code>: <a href="https://github.com/apple-open-source-mirror/Libc/blob/5e566be7a7047360adfb35ffc44c6a019a854bea/stdio/FreeBSD/fopen.c#L60">https://github.com/apple-open-source-mirror/Libc/blob/5e566be7a7047360adfb35ffc44c6a019a854bea/stdio/FreeBSD/fopen.c#L60</a> ~~</p>
<p>I feel it'll be better to trace the code via <code>glibc</code> so we can see what's going on in Linux, not FreeBSD.</p>
<p><a href="https://github.com/torvalds/linux/blob/32a92f8c89326985e05dce8b22d3f0aa07a3e1bd/fs/open.c#L1076">https://github.com/torvalds/linux/blob/32a92f8c89326985e05dce8b22d3f0aa07a3e1bd/fs/open.c#L1076</a>
<code>vfs_open</code></p>
<p>here we first see that we set the path of the new file struct to the path passed in, and then we call into <code>do_dentry_open</code>.</p>
<p>do_dentry_open - this seems like where most of the work is happening: <a href="https://github.com/torvalds/linux/blob/32a92f8c89326985e05dce8b22d3f0aa07a3e1bd/fs/open.c#L887">https://github.com/torvalds/linux/blob/32a92f8c89326985e05dce8b22d3f0aa07a3e1bd/fs/open.c#L887</a></p>
<h2>The VFS Layer</h2>
<blockquote>
<p>A file system is an organization of data and metadata on a storage device. With a vague definition like that, you know that the code required to support this will be interesting.</p>
<p><em>&quot;<a href="https://developer.ibm.com/tutorials/l-linux-filesystem/">Anatomy of the Linux File System</a>&quot; by M. Tim Jones</em></p>
</blockquote>
<p>The bit of software that manages how the data is physically laid out on disk - i.e. which bits in a file map to which physical locations on disk - is called the <strong>Filesystem</strong>. You may have heard of multiple filesystems like <code>xfs</code>, <code>ext4</code>, <code>btrfs</code>, and so on. All of these filesystems have their own way of representing your files physically. The Linux Kernel provides an abstraction called the VFS to ensure that applications need only one codepath for the Virtual File System (VFS) layer, while the VFS calls into the underlying filesystems. the VFS layer implements multiple syscalls such as <code>open</code>, <code>stat</code>, <code>read</code>, <code>write</code>, and <code>chmod</code> <sup class="footnote-ref"><a href="#fn1" id="fnref1">1</a></sup>.</p>
<p>The VFS layer also contains two caches, one for <code>dentry</code>s and one for <code>inode</code>s. What are <code>dentry</code> and <code>inode</code>?  Both are filesystem objects used to represent items present in the filesystem.</p>
<h3>inode</h3>
<p>An Inode represents a file on the disk (We need to keep in mind that in linux, directories are files too, just special kinds of files). They store metadata about a file, as well as the locations of the disk blocks that make up the file. The inodes are stored on the disk. They notably do not contain the filename. Each inode has an inode number, which is unique within a filesystem.</p>
<p>The inode contains (by a number of means) a list of disk block locations that comprise the file. An important point to consider is that the same way the inode for a file points to other (data) blocks on disk, the inode structure can be reused to be a directory that points to multiple other inode blocks on disk.</p>
<p><a href="https://www.linfo.org/inode.html">https://www.linfo.org/inode.html</a></p>
<h3>dentry</h3>
<p>so, given that we don't have filenames defined yet, and directories are just files, how do we figure out what our filesystem looks like? The answer to this is the <code>dentry</code>. These are in-memory objects that are created on boot by the kernel walking the filesystem, which map directories to paths, such that if you would like to look at all the folders on your disk, you simply need to look at all the dentries in memory.</p>
<p>So, the VFS layer caches both inodes and dentries. This allows us to speed up a number of operations on disk.</p>
<p>is there a buffer cache as well?</p>
<h2>The Filesystem</h2>
<p>The filesystem determines how data is ultimately organized on disk and how it's updated. It follows that the type of filesystem you use (and its many features) will end up determining application performance and other run-time characteristics. Some common filesystems that are used today are <code>ext3</code>, <code>ext4</code>, <code>xfs</code> and <code>btrfs</code>.</p>
<p>Each filesystem has a <strong>superblock</strong> which has a list of all the inodes present on the filesystem. This superblock is usually located at exactly the same location in every partition/disk.</p>
<p><a href="https://blogs.oracle.com/linux/understanding-ext4-disk-layout-part-1">https://blogs.oracle.com/linux/understanding-ext4-disk-layout-part-1</a>
GDT? BDT?</p>
<h2>The Block Layer</h2>
<p>The linux kernel must manage many block devices. It is possible for a physical disk to have multiple partitions (each can be considered a disk in its own right), each with its own partition on it - this means that though there's a very real hardware bottleneck that exists in the bandwidth of the disk, the kernel must schedule block IO in such a way that the bandwidth of the disk is distributed &quot;Fairly&quot;. To that end, there are also multiple IO Schedulers that exist to manage all the inflow of IO onto the disk. Some schedulers are the <code>kyber</code> scheduler, <code>mq</code> scheduler, and <code>deadline</code> scheduler.
<a href="https://www.cs.cornell.edu/courses/cs4410/2021fa/assets/material/lecture24_blk_layer.pdf">https://www.cs.cornell.edu/courses/cs4410/2021fa/assets/material/lecture24_blk_layer.pdf</a></p>
<p>BIOs -&gt; Block IOs</p>
<p>what is the device mapper?</p>
<p>[superblock ][inode section][data section]</p>
<section class="footnotes">
<ol>
<li id="fn1">
<p><a href="https://www.kernel.org/doc/html/latest/filesystems/vfs.html#introduction">https://www.kernel.org/doc/html/latest/filesystems/vfs.html#introduction</a> <a href="#fnref1" class="footnote-backref">↩</a></p>
</li>
</ol>
</section>
]]></content:encoded></item><item><title>Fixing a Bug in Meilisearch</title><link>https://rowjee.com/blog/experiences/meilisearch_2021.html</link><description><![CDATA[How I made my first Hacktoberfest 2021 Contribution in Rustlang]]></description><author>null</author><pubDate>Mon, 10 Jan 2022 09:00:00 +0000</pubDate><content:encoded><![CDATA[<p>In October 2021, I got my first pull request in a major open-source project merged. Here's what happened, and how I did it -</p>
<h2>What's Meilisearch?</h2>
<p><a href="https://github.com/meilisearch/MeiliSearch">Meilisearch</a> is a &quot;powerful, fast, open-source, easy to use and deploy search engine. Both searching and indexing are highly customizable. Features such as typo-tolerance, filters, and synonyms are provided out-of-the-box&quot;.</p>
<p>To put it simply, Meilisearch allows you to implement fast search at scale, and does it in <a href="https://www.rust-lang.org/">Rust</a>. Take a look at <a href="https://github.com/meilisearch/MeiliSearch/blob/main/assets/trumen-fast.gif">this GIF</a> and see it in action. It's safe to say it blew my mind the first time around.</p>
<h2>What was the Issue?</h2>
<ul>
<li>Find the issue itself <a href="https://github.com/meilisearch/MeiliSearch/issues/1750">here</a>.</li>
<li>Find the Pull Request to close this issue <a href="https://github.com/meilisearch/MeiliSearch/pull/1755">here</a>.</li>
</ul>
<p>Meilisearch has a browser-accessible dashboard that helps you test the implementation as and when you install it. This dashboard is usually how most people first interact with Meilisearch, and it's where the demo GIF comes from. Given the above, it's easy to understand why it's a critical user-experience component of the application.</p>
<p>I, on the other hand, was (and still am) a curious rustacean looking to better my knowledge, so I was naturally interested when I saw this issue open with the <code>good first issue</code> label. I jumped at the chance to figure this out, and then I cloned the repository. It's the largest codebase I've interacted with, and I was naturally somewhat intimidated by what was going on.</p>
<p>This repository didn't deal with the core implementation of search - that's taken care of by <a href="https://github.com/meilisearch/milli/">milli</a> - to quote, &quot;The code in this repository is only concerned with exposing the HTTP API, managing multiple indexes, and handling the update store&quot;.</p>
<p>After I cloned it, I got started with trying to fix this error. <strong>There were no error messages while this project ran</strong>, which meant that this bug would be harder to sniff out.</p>
<h2>The Debugging Process</h2>
<p>All I had to start with were a bunch of errors in the browser console.
<img src="https://user-images.githubusercontent.com/7032172/135531091-5ce938ef-5ebf-485e-8c18-2beec258e1b6.png" alt="browser console describing the error" /></p>
<p>From my experience with frameworks such as Django and Flask, This usually meant some file wasn't being served properly. Here's what I did to dig a little deeper -</p>
<ol>
<li>
<p>I Ran the test suite, and All the tests passed</p>
<p>This was simple enough - all I had to do was run</p>
<pre><code class="language-shell">$ cargo test
</code></pre>
<p>Rust has some pretty neat features baked directly into the toolchain, which helps you write tests pretty easily. As you see above, it's also super easy to run a test suite.</p>
</li>
<li>
<p>Investigated the built output</p>
<p>I then proceeded to build the project, for which all I had to do was run</p>
<pre><code class="language-shell">$ cargo build
</code></pre>
<p>I found the build output at <code>/meilisearch-http/target/rls/debug/build/meilisearch-http-a6e2793cb../out/mini-dashboard/</code>. This consisted of a bunch of static files pulled from an external source, which were then served using the <a href="https://actix.rs/">Actix</a> Web Framework.</p>
<p>Since there were a bunch of static files wired up, I used the npm package <code>serve</code> to run a temporary web server here, which served all files correctly.</p>
<pre><code class="language-shell"># if you haven't got it installed already
$ npm install -g serve # installs this globally
$ serve
</code></pre>
<p>This confirmed my initial suspicion that it <strong>wasn't a problem with the dashboard itself, but rather with how it was being served.</strong></p>
</li>
<li>
<p>I then Looked into how the server was setup.</p>
<p>At this point I used simple print debugging to ensure that the paths were generated correctly, along with their MIME types. This, too, is relatively simple - you can use the <code>println!()</code> macro to easily print the resource in question.</p>
<p>All seemed to be in order. I also tried using the <code>format!</code> macro to prepend a slash onto the file path for each file, but this didn't make a difference at all.</p>
</li>
<li>
<p>At this point I noticed that the <code>index.html</code> file was the only file being served correctly.</p>
<p>Taking a look into the route configuration, I noticed that the route for the index file was configured like this -</p>
<pre><code class="language-rust">config.service(web::resource(&quot;/&quot;).route(
    web::get().to(move || HttpResponse::Ok().content_type(mime_type).body(data)),
));
</code></pre>
<p>as opposed to</p>
<pre><code class="language-rust">scope = scope.service(web::resource(path).route(
    web::get().to(move || HttpResponse::Ok().content_type(mime_type).body(data)),
));
</code></pre>
<p>Given I'm not too familiar with Actix, I took the approach of registering the routes for the static files the same way as the <code>index.html</code> file, replacing the above block with the following to serve static files -</p>
<pre><code class="language-rust">config.service(web::resource(path).route(
    web::get().to(move || HttpResponse::Ok().content_type(mime_type).body(data)),
));
</code></pre>
</li>
</ol>
<p>This worked! <code>localhost:7700</code> now produced the dashboard properly.</p>
<p><img src="https://user-images.githubusercontent.com/42117791/135704477-c39912c1-78f6-4b7b-aa0b-aecfb06f86fd.png" alt="An Image of the Meilisearch dashboard working" /></p>
<h2>Why did this change work?</h2>
<p>Given I haven't explored Actix in depth, I'm probably not the best person to answer this question, but if I had to take a guess, I'd say it has to do with how Actix internals decided route registration for static assets should take place. It also probably calls for special handling of the index page at <code>/</code>, and this caused the issue.</p>
<p>I'll be back with more information on this when I've learnt more!</p>
<h2>Wrapping Up</h2>
<p>This was my first contribution of any sort to a major open-source project, and I'm glad it was a Rust project! It's a really cool language, and I'm glad I got to learn more about it. The maintainers were super nice, and this made me feel welcome! I was happy when this PR got merged, and have, since then, felt a little more confident about my ability to navigate large codebases which I have no familiarity with. This was fun!</p>
<p>I look forward to contributing to more, larger open-source projects!</p>
]]></content:encoded></item><item><title>The Replicated Log 0001</title><link>https://rowjee.com/the_replicated_log/0001.html</link><description><![CDATA[this is the replicated log]]></description><author>null</author><pubDate>Thu, 17 Jul 2025 09:00:00 +0000</pubDate><content:encoded><![CDATA[<p>Hello, and welcome to <em>The Replicated Log</em>, my newsletter centered roughly around the distributed systems and database space. I'm <a href="https://rowjee.com">Anirudh</a>, and I work on the Magma storage engine at Couchbase. In my free time I read, run, watch TV and occasionally write poetry. Welcome!</p>
<p>I'm very passionate and curious about the data systems space as a whole, with specific interest in distributed databases, streaming computation, dataflow systems, and storage engines, with the occasional detour into things like formal verification, and then some. I'm also interested in keeping up with the research happening in said spaces - that should give you a good enough idea of what's to come as a part of this newsletter.</p>
<p>I'm writing this newsletter to share the things I know (admittedly not much) and the things I come across (much more than the latter), along with possibly a deep dive or some small explanation of a new thing I've learnt, every two weeks or so. I'm doing this in the hope that there's at least one person who learns something new every two weeks; I find that to be a non-negotiable given that my curiosity has largely been fuelled by the work of folks who chose to share what they knew and learnt, and this is my way of giving back.</p>
<p>That being said, I'm still largely figuring out what this newsletter is going to look like, the cadence, the content, so much of it is undetermined - you're seeing the transaction before it commits here xD I'm sure I'll end up changing a bunch of stuff, but what isn't going to change is that I'm going to be writing about the things I've been interested in.</p>
<p>I have no deep dive for you folks this month, as I keep getting nerd-sniped by nice questions that I come across whilst writing it; for example, why do LSM Trees have levels? More on that in the next edition.</p>
<h2>A Tribute to some of my inspirations</h2>
<p>It seems fair that I start this off by talking about some of the talks and lectures that inspired me the most.</p>
<h3>CockroachDB Internals</h3>
<p>This was the first distributed database I learnt about. I was so fascinated by everything in this video - distributed transactions, consensus, a distributed KV layer, consistency semantics, consensus groups, sharding... this melted 19-year-old me's brain, and gave me a burning curiosity that brought me to where I am today. No further comments from me, this is a fantastic watch!</p>
<p><a href="https://www.youtube.com/watch?v=tV-WXM2IJ3U">Video Link</a></p>
<h3>Deterministic Simulation and Testing in FoundationDB</h3>
<p>I came across this talk before Deterministic Testing/Simulation was made a popular idea by Antithesis (founded by the speaker!), Turso, and friends; I find FoundationDB to be one of the most interesting databases out there for a number of reasons (it has a mini transaction engine in every client, among other things), and this talk dived really deep into how the team worked really hard to find and eliminate what would have otherwise been nightmarishly hard bugs to catch. I found it really cool that they had an array of nodes that they just left running to catch some of those particularly crazy bugs.</p>
<p><a href="https://www.youtube.com/watch?v=4fFDFbi3toc">Video Link</a></p>
<h3>RocksDB Internals, explained by Dhruba Borthakur</h3>
<p>Having worked on LSM Trees at work, I have a fraction of an insight into how hard it is to build performant and correct storage systems. This talk by Dhruba Borthakur, one of the earliest folks at RocksDB, now relatively ubiquitous as an embeddable and performant Key-Value store, talks about embedded KV Databases are necessary, explains the LSM Tree architecture, talks about some of the problems with LevelDB and then introduces RocksDB. Truly a fantastic tour of the space not just from the perspective of a systems engineer but also from that of someone who would consume the store in their product.</p>
<p><a href="https://www.youtube.com/watch?v=V_C-T5S-w8g">Video Link</a></p>
<h3>SkyPlane (NSDI 2023)</h3>
<p>This project aims to solve the problem of slow and expensive data egress (for many reasons) from object storage offerings that major cloud providers offer by rather craftily building an ad-hoc overlay routing network on top of compute nodes on said providers, exploiting parallel VMs and parallel connections to make the data transfer much faster. Admittedly, I don't understand this very well, but it's very cool.</p>
<p><a href="https://www.youtube.com/watch?v=zMrvHdeQXao">Video Link</a></p>
<h3>Time, Clocks, and the Ordering of Events in a Distributed System - Leslie Lamport</h3>
<p>Paper: <a href="https://lamport.azurewebsites.net/pubs/time-clocks.pdf">https://lamport.azurewebsites.net/pubs/time-clocks.pdf</a></p>
<p>This is one of my favourite research papers of all time, right alongside the Mapreduce paper.</p>
<p>Lamport starts by considering the problem of ordering events in a distributed system, defined more succinctly as a collection of processes that are spatially separated, that communicate with each other by sending messages. Ordering is important, and is often the source of correctness in many systems. This paper walks through the notion of &quot;Logical Time&quot; - time that doesn't rely on (at the time) inaccurate physical clocks - and also provides an algorithm to convert a partial ordering of events to a total ordering, deriving bounds on clock drift.</p>
<p>It's a dense paper, with a lot of invariant-based reasoning, but it's so worth it - especially the part where they explain the design of a distributed, leaderless mutex lock.</p>
<h2>And that's a wrap</h2>
<p>Thank you for reading! I hope at least one of these videos has brought you some joy, and most importantly, curiousity!</p>
<p>As always, I'm open to a conversation or feedback about any of this at <code>ani dot rowjee at gmail dot com</code>. Stay tuned for the next edition, where I'll be talking about LSM Trees in more detail.</p>
<p>Signing off,<br />
Anirudh</p>
]]></content:encoded></item><item><title>reclaim</title><link>https://rowjee.com/blog/reclaim.html</link><description><![CDATA[Some of my thoughts on where this blog is going.]]></description><author>null</author><pubDate>Sat, 28 May 2022 09:00:00 +0000</pubDate><content:encoded><![CDATA[<p><img src="https://imgs.xkcd.com/comics/blogging.png" alt="relevant XKCD" /><br />
<em><a href="https://xkcd.com/741">Relevant XKCD</a> - find out why in part 2</em></p>
<p>I was reading my friend, Anant Thazhemadam's <a href="https://thazhemadam.github.io/blog/">blog</a>, and happened to come across this line.</p>
<blockquote>
<p><em>&quot;This blog is for me. By that, I mean it is for me, and me only.&quot;</em></p>
</blockquote>
<p>I realized that i've spent most of my time with this blog as a platform to showcase who I am. While that's definitely the aim I've got in mind when I work on it, I'm realising it quickly became a place for me to put content out for someone else to see. To that end, I started considering the blog to be a place to show what I <em>wanted to be</em>, not <em>what I am</em>.</p>
<p>Till date, every single thing I wrote on this blog was written keeping in mind the perspective of someone who's looking to hire me, or work with me. While this is important in terms of building a brand, it's absolutely terrible for keeping this home on the internet in sync with who I am and what I do.</p>
<p>No more!</p>
<p>This blog is going to</p>
<ul>
<li>
<p>firstly, not be called a blog anymore, because that isn't what I want to limit it to - It's going to be my home on the internet. I'm going to post a lot more about my life - pictures I like, for instance, or poems that I wrote and like.</p>
</li>
<li>
<p>secondly, not limit itself to only CS Stuff. I'm passionate about Computer Science, Software Development and Technology, but that isn't <em>all</em> I'm passionate about. I've got other interests, and write about things that aren't necessarily this. It follows that my space on the internet reflects this, and puts these other things out there for the rest of the internet to see, too.</p>
</li>
</ul>
<p>So here's what's changing, apart from the interface of some components of this blog -</p>
<blockquote>
<p><em>I'm going to stop writing posts specifically for the blog.</em></p>
</blockquote>
<p>Anything that goes up on the blog will be a side effect of what i'm currently doing. Sure, this may make the space a little less technical at times, but that's the entire point - I'm not (and should not be) writing it to meet some criteria of what a &quot;good&quot; blog looks like. It might also change the frequency with which I'm trying to post.</p>
<p>This blog is my home on the internet. It should reflect what I'm up to, and what I'm working on, not necessarily only what other people are looking to understand out of the blog. It's going to be a lead indicator of what I'm working on, and a lag indicator of who I am.</p>
<p>In the old days, back when every single bit of information on the internet wasn't commoditized, people's homepages were a true window into who they were. It represented their thoughts, their ideas and their opinions (yes, opinions!).</p>
<p>This post is called reclaim, because I'm reclaiming this space to be my own.</p>
]]></content:encoded></item><item><title>Understanding and Implementing Skiplists</title><link>https://rowjee.com/blog/skiplists.html</link><description><![CDATA[This is my account of my attempt to understand and build a skiplist.]]></description><author>null</author><pubDate>Tue, 1 Oct 2024 09:00:00 +0000</pubDate><content:encoded><![CDATA[<p>I've been in a terrible rut recently, and I figured the only way to get myself out of it is to publicly announce that I'll do something.</p>
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Skiplists it is. public declaration: I&#39;m going to write a blog post titled &quot;understanding and implementing skiplists&quot; and publish it on the 1st of October, and It&#39;s going to be good! <a href="https://t.co/LQ5VhKqsx8">https://t.co/LQ5VhKqsx8</a></p>&mdash; Anirudh Rowjee @ override.bsky.social (@AnirudhRowjee) <a href="https://twitter.com/AnirudhRowjee/status/1832509379905778104?ref_src=twsrc%5Etfw">September 7, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 
<p>I gave myself 22 days to build a skiplist and write about it, so here we are. This is in no ways a perfect or even a good implementation; I've tried to be as hacky as possible, getting the bare minimum core functionality working while ignorning just about everything else.</p>
<p>You also have access to my C++ code<sup class="footnote-ref"><a href="#fn1" id="fnref1">1</a></sup> and the original paper<sup class="footnote-ref"><a href="#fn2" id="fnref2">2</a></sup>, which contains most of the diagrams you'll see on this post. I've tried my level best to not look at other implementations before I implemented this, so the spirit of &quot;implemented straight from the paper&quot; is retained to a fair extent.</p>
<h2>What are Skiplists?</h2>
<p>A skiplist is a probabilistic, associative data structure based on linked lists. From a functionality point of view, it resembles a <strong>hashmap, or a dictionary</strong>. The best way to think about it is that it's an in-memory key-value store.</p>
<p>Formally, It allows the following operations (and you can swap out the strings for bytes, etc, any things that you can compare)</p>
<ol>
<li><code>upsert(string key, string value)</code></li>
<li><code>search(string key) -&gt; optional&lt;string value&gt;</code></li>
<li><code>scan(string start, string end) -&gt; Vector&lt;pair&lt;string, string&gt;&gt;</code></li>
<li><code>delete(string key)</code></li>
</ol>
<h3>How did we get here?</h3>
<p>I have a bad problem with explaining everything - and though it makes for a good read, I'm trying to be more succinct so we can cover more stuff!</p>
<p>Consider the problem of searching over a set of keys - a collection, if you will. Attaching a value to the key is trivial.</p>
<p>The primitive linear search algorithm for almost any data structure (linked list, array, etc) is almost always <code>O(n)</code>, which means it grows asymptotically at the same rate as the input. If we know all data beforehand, we can sort it, and then we can leverage algorithms such as <strong>binary search</strong> to give us <code>O(log_2(n))</code> complexity, which work by successively reducing the search space by a factor of two (often at an additional <code>O(n * log_2(n))</code> cost to sort the input).</p>
<p><img src="https://upload.wikimedia.org/wikipedia/commons/c/c1/Binary-search-work.gif" alt="" />
<em>Binary Search, Wikimedia Commons</em></p>
<p>However, we rarely know all inputs beforehand - we can't just dump the inputs in the order they came in, because that would break the sortedness invariant we have in order to achieve <code>O(log_2(n))</code> complexity on search.</p>
<p>The ideal data structure then is something that supports maintaining the &quot;sorted-ness&quot; invariant with little interference from the user, and that also makes it easy to handle the following operations - insert, update, delete, scan - with relatively low asymptotic complexity.</p>
<p>The most ideal data structure for this task, and indeed, the choice of implementation for most languages and their standard implementations, is the Red-Black Tree, or some other form of self-balancing binary search trees. These data structures usually use a heuristic to approximate the &quot;balanced-ness&quot; of the tree - usually called balance factor - and often rely on complex rebalancing algorithms that must shuffle multiple pointers in place to accommodate for this.</p>
<p><img src="https://upload.wikimedia.org/wikipedia/commons/4/41/Red-black_tree_example_with_NIL.svg" alt="" />
<em>Red-Black Tree, Wikimedia Commons</em></p>
<p><img src="https://upload.wikimedia.org/wikipedia/commons/f/f2/Binary_Tree_Rotation_%28animated%29.gif" alt="" />
<em>Balanced Binary Search Tree Rotation, Wikimedia Commons</em></p>
<p>What if we could trade complexity for correctness? What if we could settle for an <strong>average case <code>O(log(n))</code> with a worst case <code>O(n)</code></strong>, as opposed to the guaranteed <strong>average and worst case <code>O(log(n))</code></strong> with Binary Search? This is exactly the guarantee that skiplists make.</p>
<h2>What does a skiplist look like?</h2>
<p>Think of a singly linked list where each node has <strong>more than one pointers to other nodes</strong> organized in <strong>levels</strong>, where the highest level (say, 5) represents the largest number of nodes skipped, and the lowest level represents the lowest number (0) of nodes skipped (i.e. points to its immediate neighbour).</p>
<p>In this case, the 4th level connects every 8th node, the 3rd level connects every 4 nodes, the 2nd level connects every 2 nodes, and the 1st level connects every successive node.</p>
<p><img src="/static/images/opus/001-skiplist/skiplist-level-growth.png" alt="" />
<em>Primitive example of a singly linked list with multiple forward pointers</em></p>
<p>A Skiplist can be thought of as a modification of a singly linked list that is maintained in sorted order, with the change that each node holds a fixed, <em>randomly determined</em> number of pointers to successive nodes, organized in the same paradigm of levels.</p>
<p><img src="/static/images/opus/001-skiplist/skiplist-example.png" alt="" />
<em>A Skiplist, from the paper</em></p>
<p>When we combine the sorted nature of the nodes in the a singly linked list (sorted by key, for example), and the idea of levels that allow you to <strong>skip some series of nodes</strong>, you start to see how it all falls into place; You can start at the highest level and make the largest jumps, successively reducing the search space as we move down the levels, and finally, arriving at the target node. This is a skiplist.</p>
<pre><code>search(key k):
	Start at the highest level
	repeat until we are at the lowest level:
		repeat until the next element key is greater than the search key
			go to the next element
		move down one level
</code></pre>
<h3>An Intuition for Search</h3>
<p>The best intuition I've heard for this is from Prof Srini Devas from MIT 6.046J<sup class="footnote-ref"><a href="#fn3" id="fnref3">3</a></sup>. In this case, we consider each node to be a subway/train station, and we consider pointers to be the train routes between them.</p>
<p><img src="/static/images/opus/001-skiplist/skiplists_metro_stations.png" alt="" />
<em>The Purple Line of the Bengaluru Metro, made into a skiplist</em></p>
<p>In this scenario, we consider an arrangement of multiple rail lines (and thus, trains) plying between the same stations. Each station can have multiple trains!</p>
<p>Let us consider that the time taken to travel between any two stops is constant, regardless of how far they are. If I need to get from <em>KSR Station</em> to Central College Station, I am spoilt for choice; I have three train lines to choose from. If I want to get from KSR Station to Cubbon Park Station, I have two choices - I can either use the red line (no stops) or the orange, green, and purple lines (one stop). Clearly, the approach for the red line wins!</p>
<p>Lastly, consider a scenario where I have to go from KSR Station to Trinity Station. It might appear that the default path is 2 stops (KSR -&gt; Central College -&gt; Cubbon Park -&gt; Trinity) - but in reality, I can do this in 1 stop by going from KSR -&gt; Cubbon Park (via the red line), and then going from Cubbon Park -&gt; Trinity (via the purple line). The key learning is that we can greedily adjust our strategy to have the least number of stops, provided that some sort of balance is maintained that doesn't negatively bias this whole process.</p>
<blockquote>
<p>Who decides which lines connect which stations?</p>
</blockquote>
<p>That's an extremely important question, and it's a critical part of what makes skiplists so successful. Keep that question in mind - this is also where probability comes into the picture.</p>
<h3>The Math</h3>
<p>As we discussed before, we rarely have all the data in our dataset present beforehand, which is why our skiplist must tolerate insertions, updates, and deletions, all while maintaining a reasonably low search complexity. In this reality, we can't assume that we know how many nodes there will be or where they'll be positioned, which is why deciding the level of a skiplist node is not trivial.</p>
<p>To ensure that, probabilistically, we have the highest change of skipping the most nodes in our search process, we must reduce the number of nodes in each level as we move from the lowest to the highest level. We can do this by</p>
<ol>
<li>Randomly setting the level of a node at insert time and</li>
<li>controlling the rate of increase of the level of a node</li>
</ol>
<p>To control the rate of increase, we use a fraction <code>p</code> - As Wikipedia puts it,</p>
<blockquote>
<p>&quot;... where an element in layer i appears in layer i + 1  with some fixed probability p&quot;</p>
</blockquote>
<p>And as the paper puts it,</p>
<blockquote>
<p>&quot;To get away from magic constants, we say that a fraction <code>p</code> of the nodes with level <code>i</code> pointers also have level <code>i+1</code> pointers.&quot;</p>
</blockquote>
<p><img src="/static/images/opus/001-skiplist/skiplist-levelgen.png" alt="" /></p>
<p>Choosing <code>p</code> well is a matter of tuning, and the paper has some recommendations to make, given that it basically trades off space for accuracy.</p>
<p><img src="/static/images/opus/001-skiplist/skiplist-p-choices.png" alt="" /></p>
<p>The paper has a nice proof that fashions search instead as &quot;climbing out of a list&quot;, and proves that search complexity is equal to <code>O(log(n))</code>. Wikipedia has a nice proof<sup class="footnote-ref"><a href="#fn4" id="fnref4">4</a></sup> of the search cost being <code>O(log(n))</code> as well.</p>
<h2>Maintenance of a skiplist</h2>
<p>Given that we understand that the skiplist is just an <em>enhancement of a sorted linked list that has been augmented with more pointers</em>, we see that every algorithm that modifies the skiplist (insertion, deletion, update) has the same rough structure:</p>
<pre><code>mutate(skiplist):
	find element to modify / insert after
	update existing element OR create/delete the element
	maintain the chain of pointers
</code></pre>
<p>The first two steps are rather straightforward:
for 1, we use the search algorithm we just learnt about, and 2 is just memory allocation/deallocation.</p>
<p><img src="/static/images/opus/001-skiplist/types-of-mutations-on-skiplist.png" alt="" /></p>
<p>When it comes to pointer maintenance, we use a vector of pointers to the nodes right before the current node, called the <strong>update vector</strong>, to make our lives easier. The Update Vector holds all the predecessor nodes that will need to be updated if a new node is added, and allows us to maintain &quot;pointer continuity&quot; in that sense.</p>
<h2>Advantages over other data structures</h2>
<ul>
<li>Skiplists don't require expensive rebalance operations (see red-black trees)</li>
<li>Skiplists make it very easy to do sequential scans on data (see red-black trees)</li>
<li>Skiplists can be more easily modeled as concurrent data structures using atomic pointer operations <sup class="footnote-ref"><a href="#fn5" id="fnref5">5</a></sup></li>
</ul>
<h2>Implementing a skiplist</h2>
<p>Fair warnings:</p>
<ol>
<li>Using <code>clang</code> is frustrating sometimes. And Don't get me started on <code>CMake</code>.</li>
<li>The paper itself is 1-indexed, and I hope all your code is 0-indexed - this tripped me up for a while and caused some really funny segfaults, so be careful lol</li>
<li>Everything in every class is <code>public</code> - I know, this is very C-style programming, but I couldn't be bothered to write the appropriate accessors.</li>
</ol>
<h3>Defining the skiplist node</h3>
<pre><code class="language-cpp">class SkiplistNode {

public:
  std::vector&lt;SkiplistNode *&gt; links;

  // Constructor
  SkiplistNode(int current_level, std::string key, std::string value)
      : current_level(current_level), key(std::move(key)),
        value(std::move(value)) {

    links = std::vector&lt;SkiplistNode *&gt;(current_level);
    for (int i = current_level - 1; i &gt;= 0; i--) {
      links[i] = nullptr;
    }
  }

  void DUMP() {
    std::cout &lt;&lt; fmt::format(&quot;SkiplistNode[{}] [{}:{}] AT {} &quot;, current_level,
                             key, value, fmt::ptr(this))
              &lt;&lt; std::endl;
    for (int i = current_level - 1; i &gt;= 0; i--) {
      std::cout &lt;&lt; fmt::format(&quot;\tNode at level {} pointing to {}&quot;, i,
                               fmt::ptr(links[i]))
                &lt;&lt; std::endl;
    }
  }

  int current_level;
  std::string key;
  std::string value;
};

</code></pre>
<h3>Defining the Skiplist Class</h3>
<p>I decided to explore <code>std::optional&lt;T&gt;</code> this time as a cure for my rust hangover; It works very nicely! the lack of a <code>Result&lt;T&gt;</code> equivalent made me sad but I hear that that's in the works too for C++23 with <code>std::expected&lt;T, E&gt;</code>. I've made up for this with <code>std::pair&lt;T, SkiplistError&gt;</code>, leading to some fairly go-like C++ code.</p>
<pre><code class="language-cpp">
class SkiplistError {

public:
  enum ErrorVariant { BAD_ACCESS, ALLOC_FAIL, KEY_NOT_FOUND, NOERR };
  ErrorVariant e = ErrorVariant::NOERR;
  std::string message;

  SkiplistError(ErrorVariant e, std::string message = &quot;&quot;)
      : e(e), message(message){};

  operator bool() { return e != NOERR; }
};


class Skiplist {

public:
  // constructor
  Skiplist(int max_level);

  // Get the value associated with a particular key
  std::optional&lt;std::string&gt; Search(std::string Key);

  // Insert a key-value pair
  std::pair&lt;std::string, SkiplistError&gt; Insert(const std::string &amp;Key,
                                               const std::string &amp;Value);

  // Delete a key and get the value associated with it on successful delete
  std::pair&lt;std::string, SkiplistError&gt; Delete(std::string Key);

  // Do a full scan of the skiplist
  // TODO see if we can replace this with an iterator
  std::pair&lt;std::vector&lt;std::pair&lt;std::string, std::string&gt;&gt;, SkiplistError&gt;
  Scan();

  // Find the point to insert a new skiplist node
  std::pair&lt;std::pair&lt;SkiplistNode *, std::vector&lt;SkiplistNode *&gt;&gt;,
            SkiplistError&gt;
  identifyPredecessorNode(std::string key);

  void DUMP();

  ~Skiplist();

  SkiplistNode *START;
  SkiplistNode *END;
  int max_level;
  int current_max_level;
  // p: a tunable factor for the number of elements you want in the skiplist
  float p;
  std::mt19937 rng;
  std::uniform_real_distribution&lt;double&gt; distribution;

  int getRandomLevel();
};
</code></pre>
<h3>Constructing a skiplist</h3>
<p>This is the test that we wanted to pass:</p>
<pre><code class="language-cpp">TEST(SkiplistTest, test_init) {
  // Initialize a Skiplist
  const int max_level = 5;
  auto sl = Skiplist(max_level);
  for (int i = 0; i &lt; max_level; i++) {
    ASSERT_EQ(sl.START-&gt;links[i], sl.END);
  }
}
</code></pre>
<p>and here's the constructor:</p>
<pre><code class="language-cpp">Skiplist::Skiplist(int max_level) : max_level(max_level) {
  p = 0.5;
  rng = std::mt19937(std::time(nullptr)); // fixed seed as of now
  distribution = std::uniform_real_distribution&lt;double&gt;(0.0, 1.0);
  // Initialize the sentinel nodes
  START = new SkiplistNode(max_level, &quot;START_KEY&quot;, &quot;START_VALUE&quot;);
  END = new SkiplistNode(max_level, &quot;END_KEY&quot;, &quot;END_VALUE&quot;);
  // Connect the start and end nodes
  for (int i = 0; i &lt; max_level; i++) {
    START-&gt;links[i] = END;
  }
}
</code></pre>
<p>Here, <code>START</code> and <code>END</code> are special nodes, which are the sentinel nodes of the skiplist. In all honesty, it would probably be &quot;cleaner&quot; to use a <code>nullptr</code> to terminate the node pointers, but I felt like having a dedicated <code>END</code> node would be a better option. This would also help maintain the invariant that there will never be a <code>nullptr</code> in the search path, as we will see later.</p>
<h3>Implementing Insertion</h3>
<p>Given that insertion and search are two really tightly coupled algorithms (you need search to implement insertion, but you also need insertion to be able to test search?), I was forced to implement both at once.</p>
<p>Here's the test we wanted to pass (and there's another one that tests search later as well).</p>
<pre><code class="language-cpp">TEST(SkiplistTest, test_insert) {

  auto linear_search = [](std::string key,
                          Skiplist *sl) -&gt; SkiplistError::ErrorVariant {
    auto curr_node = sl-&gt;START;
    while (curr_node != sl-&gt;END) {
      if (curr_node-&gt;key == key) {
        return SkiplistError::ErrorVariant::NOERR;
      } else {
        curr_node = curr_node-&gt;links[0];
      }
    }
    return SkiplistError::ErrorVariant::KEY_NOT_FOUND;
  };

  // Check the monotonicity of the keys
  auto monotonicity_check = [](Skiplist *sl) -&gt; SkiplistError::ErrorVariant {
    auto currentNode = sl-&gt;START-&gt;links[0];
    std::string prevText = &quot;&quot;;
    std::string currentText = &quot;&quot;;
    while (currentNode != sl-&gt;END) {
      prevText = currentText;
      currentText = currentNode-&gt;key;
      if (!(currentText &gt; prevText)) {
        return SkiplistError::BAD_ACCESS;
      }
      currentNode = currentNode-&gt;links[0];
    }
    return SkiplistError::ErrorVariant::NOERR;
  };

  const int max_level = 5;
  std::cout &lt;&lt; fmt::format(&quot;initializing skiplist with level {}&quot;, max_level)
            &lt;&lt; std::endl;
  auto sl = new Skiplist(max_level);

  std::vector&lt;std::tuple&lt;std::string, std::string&gt;&gt; kvPairs = {
      {&quot;hello&quot;, &quot;world&quot;},   {&quot;something&quot;, &quot;else&quot;},
      {&quot;enter&quot;, &quot;sandman&quot;}, {&quot;the struts&quot;, &quot;could have been me&quot;},
      {&quot;hello&quot;, &quot;world2&quot;},  {&quot;END_KEY&quot;, &quot;SYSTEM BROKEN!!!&quot;}};

  for (auto [key, value] : kvPairs) {
    // Insert an element and search for it
    auto [retval, err] = sl-&gt;Insert(key, value);
    ASSERT_EQ(err.e, SkiplistError::NOERR);
    sl-&gt;DUMP();
    // Do a simple linear search
    ASSERT_EQ(linear_search(key, sl), SkiplistError::NOERR);
    // Ensure that monotonicity is maintained
    ASSERT_EQ(monotonicity_check(sl), SkiplistError::NOERR);
  }
}
</code></pre>
<p>Here's the insertion code, beginning with identifying the predecessor node. This code is common across all mutating functions, and will be reused in other places, so I've made it a function of its own; it returns the pointer to the predecessor node or the node itself, and the update vector of nodes that we used to reach the predecessor node.</p>
<pre><code class="language-cpp">std::pair&lt;std::pair&lt;SkiplistNode *, std::vector&lt;SkiplistNode *&gt;&gt;, SkiplistError&gt;
Skiplist::identifyPredecessorNode(std::string key) {

  // Initialize the update vector
  auto update = std::vector&lt;SkiplistNode *&gt;(max_level, nullptr);

  // See if the node already exists
  auto current_node = START;
  auto next_node = START;

  auto max_search_level = max_level - 1;
  for (int i = max_search_level; i &gt;= 0; i--) {
    // Check if the next node in the level has a key that comes before our
    // search key

    // See if the next node is a sentinel element
    next_node = current_node-&gt;links[i];
    while (next_node-&gt;key &lt; key &amp;&amp; next_node != END) {
      current_node = next_node;
      next_node = current_node-&gt;links[i];
    }
    update[i] = current_node;
  }
  current_node = current_node-&gt;links[0];
  return std::make_pair(std::make_pair(current_node, update),
                        SkiplistError(SkiplistError::ErrorVariant::NOERR));
}

</code></pre>
<p>And finally, the insertion code itself:</p>
<pre><code class="language-cpp">// This is called insert but it has upsert semantics
std::pair&lt;std::string, SkiplistError&gt;
Skiplist::Insert(const std::string &amp;key, const std::string &amp;value) {

  // Figure out where to insert the node: this is either the node with the same
  // key, so we can update the value, or we found the node right before the
  // insertion point so that we can insert after it
  auto [meta, error] = identifyPredecessorNode(key);
  if (error.e != SkiplistError::NOERR) {
    return std::make_pair(&quot;&quot;, error);
  }
  std::cout &lt;&lt; &quot;Predecessor Found!&quot; &lt;&lt; std::endl;

  auto [current_node, update] = meta;
  if (current_node-&gt;key == key) {

    // If the key exists at the current node, update its value
    std::cout &lt;&lt; fmt::format(&quot;BAZINGA! Key {} found with value {}&quot;,
                             current_node-&gt;key, current_node-&gt;value)
              &lt;&lt; std::endl;
    auto oldVal = current_node-&gt;value;
    current_node-&gt;value = value;
    return std::make_pair(value, SkiplistError::NOERR);

  } else {

    // If they key doesn't exist at the current node, insert it
    std::cout &lt;&lt; &quot;Node not found&quot; &lt;&lt; std::endl;
    // The node doesn't exist;
    // Construct a new node
    int level = getRandomLevel();

    // WARN
    // Not doing this rn: need to update the max level of the list if this
    // happens I doubt it will but the paper says we need to do it so ¯\_(ツ)_/¯
    if (level &gt; max_level) {
    }

    auto new_node = new SkiplistNode(level, key, value);
    for (int i = 0; i &lt; level; i++) {
      new_node-&gt;links[i] = update[i]-&gt;links[i];
      update[i]-&gt;links[i] = new_node;
    }

    // Update all the pointers in the reachability chain to reach this node
    return std::make_pair(value, SkiplistError::NOERR);
  }
}
</code></pre>
<p>As we discussed before, the level of a new skiplist node is governed by a random number generator. The <code>p</code> factor here is extremely crucial to the &quot;balancing&quot; of your skiplist;</p>
<pre><code class="language-cpp">int Skiplist::getRandomLevel() {
  int level = 1;
  while ((distribution(rng) &lt; p) &amp;&amp; (level &lt; max_level)) {
    level += 1;
  }
  return level;
}
</code></pre>
<h3>Implementing Search</h3>
<p>Test to pass:</p>
<pre><code class="language-cpp">TEST(SkiplistTest, test_insert_and_search) {
  const int max_level = 5;
  std::cout &lt;&lt; fmt::format(&quot;initializing skiplist with level {}&quot;, max_level)
            &lt;&lt; std::endl;
  auto sl = Skiplist(max_level);
  sl.DUMP();

  // Load some data into the skiplist
  std::vector&lt;std::tuple&lt;std::string, std::string&gt;&gt; kvPairs = {
      {&quot;hello&quot;, &quot;world&quot;},
      {&quot;something&quot;, &quot;else&quot;},
      {&quot;enter&quot;, &quot;sandman&quot;},
      {&quot;martin garrix&quot;, &quot;under pressure&quot;},
      {&quot;the smiths&quot;, &quot;please please please let me get what I want&quot;},
      {&quot;the struts&quot;, &quot;could have been me&quot;},
      {&quot;hello&quot;, &quot;world2&quot;}};
  for (auto [key, value] : kvPairs) {
    auto [retval, err] = sl.Insert(key, value);
    ASSERT_EQ(err.e, SkiplistError::NOERR);
  }

  // Search for elements in the skiplist
  std::vector&lt;std::tuple&lt;std::string, std::optional&lt;std::string&gt;&gt;&gt; testcases = {
      {&quot;something&quot;, &quot;else&quot;},
      {&quot;enter&quot;, &quot;sandman&quot;},
      {&quot;the struts&quot;, &quot;could have been me&quot;},
      {&quot;martin garrix&quot;, &quot;under pressure&quot;},
      {&quot;this isn't there&quot;, std::nullopt},
      {&quot;the smiths&quot;, &quot;please please please let me get what I want&quot;},
      {&quot;hello&quot;, &quot;world2&quot;},
      {&quot;nonexistent&quot;, std::nullopt}};
  for (auto [key, searchResult] : testcases) {
    ASSERT_EQ(sl.Search(key), searchResult);
  }
}
</code></pre>
<p>And the algorithm, most of which has been taken from the <code>identifyPredecessorNode</code> function, <em>both</em> of which are based on the paper:</p>
<pre><code class="language-cpp">std::optional&lt;std::string&gt; Skiplist::Search(std::string Key) {
  std::cout &lt;&lt; &quot;Searching for -&gt; &quot; &lt;&lt; Key &lt;&lt; std::endl;
  auto current = START;
  auto max_search_level = max_level - 1;
  for (int i = max_search_level; i &gt;= 0; i--) {
    while (current-&gt;links[i]-&gt;key &lt; Key &amp;&amp; current-&gt;links[i] != END) {
      std::cout &lt;&lt; fmt::format(&quot;Exploring key -&gt; {}&quot;, current-&gt;links[i]-&gt;key)
                &lt;&lt; std::endl;
      current = current-&gt;links[i];
    }
  }
  current = current-&gt;links[0];
  if (current-&gt;key == Key) {
    return std::optional&lt;std::string&gt;{current-&gt;value};
  } else {
    return std::nullopt;
  }
}
</code></pre>
<h3>Implementing Delete</h3>
<p>This also followed the same pattern as the insert operation as before, just that it now frees memory as well as deleting everything else.</p>
<p>Here is the test we need to pass:</p>
<pre><code class="language-cpp">TEST(SkiplistTest, test_delete) {
  const int max_level = 5;
  std::cout &lt;&lt; fmt::format(&quot;initializing skiplist with level {}&quot;, max_level)
            &lt;&lt; std::endl;
  auto sl = Skiplist(max_level);

  // Load some data into the skiplist
  std::vector&lt;std::tuple&lt;std::string, std::string&gt;&gt; kvPairs = {
      {&quot;hello&quot;, &quot;world&quot;},
      {&quot;something&quot;, &quot;else&quot;},
      {&quot;enter&quot;, &quot;sandman&quot;},
      {&quot;martin garrix&quot;, &quot;under pressure&quot;},
      {&quot;the smiths&quot;, &quot;please please please let me get what I want&quot;},
      {&quot;the struts&quot;, &quot;could have been me&quot;},
      {&quot;hello&quot;, &quot;world2&quot;}};
  for (auto [key, value] : kvPairs) {
    auto [retval, err] = sl.Insert(key, value);
    ASSERT_EQ(err.e, SkiplistError::NOERR);
  }
  // Delete some elements
  std::vector&lt;std::tuple&lt;std::string, SkiplistError::ErrorVariant&gt;&gt; testcases =
      {{&quot;hello&quot;, SkiplistError::NOERR},
       {&quot;enter&quot;, SkiplistError::NOERR},
       {&quot;this will break&quot;, SkiplistError::KEY_NOT_FOUND}};

  for (auto [key, err] : testcases) {
    std::cout &lt;&lt; fmt::format(&quot;Deleting {}&quot;, key) &lt;&lt; std::endl;
    auto [retval, reterr] = sl.Delete(key);
    sl.DUMP();
    ASSERT_EQ(reterr.e, err);
  }
}
</code></pre>
<p>And here's the implementation:</p>
<pre><code class="language-cpp">std::pair&lt;std::string, SkiplistError&gt; Skiplist::Delete(std::string Key) {
  std::string oldVal;
  // Figure out where to delete the node: this is either the node with the same
  // key, so we can update the value, or we found the node right before the
  // insertion point so that we can insert after it
  auto [meta, error] = identifyPredecessorNode(Key);
  if (error.e != SkiplistError::NOERR) {
    return std::make_pair(&quot;&quot;, error);
  }
  auto [nodePtr, update] = meta;
  if (nodePtr-&gt;key == Key) {
    // We have found the right node to delete
    // update all the necessary pointers
    for (int i = 0; i &lt; max_level; i++) {
      if (update[i]-&gt;links[i] != nodePtr) {
        break;
      }
      update[i]-&gt;links[i] = nodePtr-&gt;links[i];
    }
    oldVal = nodePtr-&gt;value;
    delete nodePtr;
    // TODO update overall list level
    return std::make_pair(oldVal,
                          SkiplistError(SkiplistError::ErrorVariant::NOERR));
  } else {
    return std::make_pair(
        &quot;&quot;, SkiplistError(SkiplistError::ErrorVariant::KEY_NOT_FOUND));
  }

  return std::make_pair(&quot;&quot;,
                        SkiplistError(SkiplistError::ErrorVariant::BAD_ACCESS));
}
}
</code></pre>
<h3>Implementing Scan</h3>
<p>This one is pretty simple. We only need to read all key-value pairs into an array. There's probably a better way of doing this, but this will suffice for now.</p>
<p>The test we want to pass:</p>
<pre><code class="language-cpp">TEST(SkiplistTest, test_fullscan) {
  const int max_level = 5;
  std::cout &lt;&lt; fmt::format(&quot;initializing skiplist with level {}&quot;, max_level)
            &lt;&lt; std::endl;
  auto sl = Skiplist(max_level);

  // Load some data into the skiplist
  std::vector&lt;std::pair&lt;std::string, std::string&gt;&gt; kvPairs = {
      {&quot;something&quot;, &quot;else&quot;},
      {&quot;enter&quot;, &quot;sandman&quot;},
      {&quot;martin garrix&quot;, &quot;under pressure&quot;},
      {&quot;the smiths&quot;, &quot;please please please let me get what I want&quot;},
      {&quot;the struts&quot;, &quot;could have been me&quot;},
      {&quot;hello&quot;, &quot;world2&quot;}};
  for (auto [key, value] : kvPairs) {
    auto [retval, err] = sl.Insert(key, value);
    ASSERT_EQ(err.e, SkiplistError::NOERR);
  }

  // Copy over the kv pairs
  std::vector&lt;std::pair&lt;std::string, std::string&gt;&gt; kvPairsSorted = kvPairs;

  std::sort(kvPairsSorted.begin(), kvPairsSorted.end(),
            [](std::pair&lt;std::string, std::string&gt; &amp;a,
               std::pair&lt;std::string, std::string&gt; &amp;b) -&gt; bool {
              return std::get&lt;0&gt;(a) &lt; std::get&lt;0&gt;(b);
            });
  std::cout &lt;&lt; &quot;Printing out sorted list: &quot; &lt;&lt; std::endl;
  for (auto k : kvPairsSorted) {
    std::cout &lt;&lt; fmt::format(&quot;Key {} Value {}&quot;, std::get&lt;0&gt;(k), std::get&lt;1&gt;(k))
              &lt;&lt; std::endl;
  }

  auto [res, err] = sl.Scan();
  ASSERT_EQ(err.e, SkiplistError::NOERR);
  ASSERT_EQ(res, kvPairsSorted);
}
</code></pre>
<p>and here's the implementation:</p>
<pre><code class="language-cpp">std::pair&lt;std::vector&lt;std::pair&lt;std::string, std::string&gt;&gt;, SkiplistError&gt;
Skiplist::Scan() {
  std::vector&lt;std::pair&lt;std::string, std::string&gt;&gt; answer = {};
  auto current_node = START-&gt;links[0];
  while (current_node != END) {
    answer.push_back(std::make_pair(current_node-&gt;key, current_node-&gt;value));
    current_node = current_node-&gt;links[0];
  }
  return std::make_pair(answer, SkiplistError(SkiplistError::NOERR));
}
</code></pre>
<h3>Destructor</h3>
<p>There has to be a better way to do this, but I wanted to be safe.</p>
<pre><code class="language-cpp">Skiplist::~Skiplist() {
  std::cout &lt;&lt; &quot;Destructing Skiplist&quot; &lt;&lt; std::endl;
  auto [res, err] = Scan();
  if (err.e != SkiplistError::NOERR) {
  } else {
    for (auto [k, _] : res) {
      auto [res1, err1] = Delete(k);
      if (err1.e != SkiplistError::NOERR) {
        std::cout &lt;&lt; fmt::format(&quot;Failed to delete key {}&quot;, res1) &lt;&lt; std::endl;
      }
    }
  }
}
</code></pre>
<h2>Notes</h2>
<ul>
<li>you can find the code for <code>DUMP</code>, as well as the CMakeLists.txt file on Github.</li>
<li>My rule of not looking at another implementation might have harmed me more than it helped me!</li>
<li>only a few null pointers were harmed in the making of this post.</li>
</ul>
<h2>Acknowledgements</h2>
<p>I'd really like to thank <a href="https://eatonphil.com/">Phil Eaton</a>, whose practice of writing monthly technical blog posts has been a long-standing inspiration of mine, and thanks to whose encouragement this blog post exists.</p>
<p>I would also like to thank <a href="https://navinshrinivas.com/">Navin Shrinivas</a> for being my accountability buddy throughout this process, and <a href="https://www.linkedin.com/in/achyuthyogeshsosale/">Achyut Yogesh Sosale</a>, <a href="https://github.com/ad-chaos">Kiran Rajpurohit</a>, <a href="https://www.siddharthtewari.me">Siddarth Tewari</a>, and <a href="https://sudhir.live/">Anirudh Sudhir</a> for their suggestions on how I could make this post better. Thank you folks so, so much!</p>
<p>And lastly, to everyone on twitter who has been listening along on my journey and supporting me with encouraging comments and feedback - thank you!</p>
<p>And thank you, dear reader, for reading :D if you have any feedback (suggestions, comments, corrections), you can find me on <a href="https://x.com/anirudhRowjee">twitter</a> or <a href="mailto:ani.rowjee@gmail.com">email</a>.</p>
<h2>References</h2>
<section class="footnotes">
<ol>
<li id="fn1">
<p><a href="https://github.com/anirudhRowjee/skiplist-cpp">anirudhRowjee/skiplist-cpp</a> <a href="#fnref1" class="footnote-backref">↩</a></p>
</li>
<li id="fn2">
<p><a href="https://15721.courses.cs.cmu.edu/spring2018/papers/08-oltpindexes1/pugh-skiplists-cacm1990.pdf">Skip Lists: A Probabilistic Alternative to Balanced Trees (Pugh, 1990)</a> <a href="#fnref2" class="footnote-backref">↩</a></p>
</li>
<li id="fn3">
<p><a href="https://youtu.be/2g9OSRKJuzM?si=dnHc179OWEMg84aU&amp;t=1776">MIT 6.046J Design and Analysis of Algorithms, Spring 2015 - Lecture 7, Randomization</a> <a href="#fnref3" class="footnote-backref">↩</a></p>
</li>
<li id="fn4">
<p><a href="https://en.wikipedia.org/wiki/Skip_list#:~:text=The%20expected%20number,against%20storage%20costs.">Skip Lists: Wikipedia</a> <a href="#fnref4" class="footnote-backref">↩</a></p>
</li>
<li id="fn5">
<p><a href="https://15721.courses.cs.cmu.edu/spring2016/papers/pugh-skiplists1990.pdf">CONCURRENT MAINTENANCE OF SKIP LISTS (Pugh, 1990)</a> <a href="#fnref5" class="footnote-backref">↩</a></p>
</li>
</ol>
</section>
]]></content:encoded></item><item><title>The Warp Web Framework</title><link>https://rowjee.com/blog/rust-warp.html</link><description><![CDATA[Learn about the Warp Web Framework in Rust]]></description><author>null</author><pubDate>Sun, 19 Jul 2020 09:00:00 +0000</pubDate><content:encoded><![CDATA[<ul>
<li>
<p>The Warp Web Framework #rust #warp</p>
<ul>
<li>
<p>This is a web framework written in Rust, built on top of <code>Tokio</code> and <code>hyper</code>.</p>
</li>
<li>
<p><a href="https://docs.rs/warp/latest/warp/">https://docs.rs/warp/latest/warp/</a>, reference video for this * <a href="https://www.youtube.com/watch?v=HNnbIW2Kzbc">https://www.youtube.com/watch?v=HNnbIW2Kzbc</a></p>
</li>
<li>
<p>It uses the standard Tokio async runtime</p>
</li>
<li>
<h2>Hello World Snippet</h2>
<pre><code class="language-rust">		  use warp::Filter;

		  #[tokio::main]
		  async fn main() {

		      let hello_world = warp::path::end()
		          .and(warp::get())
		          .map(|| &quot;hello world from root!&quot;);

		      let hi = warp::path(&quot;hi&quot;)
		          .and(warp::get())
		          .map(|| &quot;Hello from Hi!&quot;);

		      // combine the two filters
		      let routes = hello_world.or(hi);

		      println!(&quot;starting the web server...&quot;);
		      warp::serve(routes).run(([127, 0, 0, 1], 8000)).await;
		  }
</code></pre>
</li>
<li>
<p>Filters</p>
<ul>
<li>
<p>The core idea behind Warp's composable architecture is the concept of a Filter (<code>warp::Filter</code>). Filters are inherently composable, and can be used to define</p>
<ul>
<li>Collections of Routes
<ul>
<li><code>warp::path(&quot;&lt;path here&gt;&quot;)</code> to signify the path segment to match</li>
<li><code>warp::path::end()</code> to signify that the path is over * Not using this will allow for first prefix match wildcard routing</li>
</ul>
</li>
<li>Pattern*matching Mechanisms
<ul>
<li>HTTP headers and Methods
<ul>
<li>See <code>.and(warp::get())</code> * without this, all HTTP Methods will give you the same response on the route.
<ul>
<li>Once these method filters are used, you get an <em>HTTP 405 Method Not Allowed</em> response on an unspecified method.</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>Multiple Filters can be combined to add more routes/functionality to the current route being served. See <code>let routes = hello_world.or(hi)</code>.</p>
<ul>
<li>This combines the <code>hello_world</code> and the <code>hi</code> filter as one of many routes that the server can serve.</li>
<li>for path filters *&gt; <strong>The order in which filters are called are also the order in which they're applied on the request.</strong></li>
</ul>
</li>
<li>
<h2>Static files can also be served as a filter.</h2>
<pre><code class="language-rust">			  const WEB_FOLDER: &amp;str = &quot;static/&quot;;
			  #[tokio:main]
			  async fn main() {
			    ...
			    let staticfiles = warp::fs::dir(WEB_FOLDER);

			    let routes = hello_world.or(hi).or(staticfiles)
			    ...
			  }
</code></pre>
<ul>
<li>This takes care of making sure the folder is there, etc</li>
</ul>
</li>
</ul>
</li>
<li>
<p>Implementing your own Filter</p>
<ul>
<li>
<p>A Filter can look like this, to be imported from another file *</p>
<pre><code class="language-rust">		  use warp::Filter;

		  // implement a custom filter or set of methods for the URL API Methods

		  pub fn todos_filter() *&gt; impl Filter&lt;Extract = impl warp::Reply, Error = warp::Rejection&gt; + Clone {
		      warp::path(&quot;urls&quot;)
		          .and(warp::get())
		          .and(warp::path::end())
		          .map(|| &quot;get all URLs!&quot;)
		  }
</code></pre>
</li>
<li>
<p>DONE Figure out why that type signature is required</p>
<ul>
<li>Filters have two primary reasons they need to be so composable * to filter, and to augment. Thus, a filter needs to implement an <code>Extract</code> trait, which tells every other filter <em>what it takes out from previous filters</em> and <em>what it gives back</em>, and the <code>Error</code> trait tells the type system what will be returned in the event of a failure.</li>
<li>In this case, the signature roughly says *
<ul>
<li>TODO this</li>
</ul>
</li>
</ul>
</li>
<li>
<p>To pass an Async function into the closure (as we often need to do for database calls), and because the async closure is unstable, we can pass a closure with an async block into <code>.and_then()</code>.</p>
<ul>
<li>Primitively,
<pre><code class="language-rust">			      warp::path(&quot;urls&quot;)
			          .and(warp::get())
			          .and(warp::path::end())
			          .and_then(|| async { Ok::&lt;&amp;str, warp::Rejection&gt;(&quot;Will get all URLs&quot;) })
</code></pre>
</li>
<li>The turbofish is necessary as it is a closure. Regular functions are fully typed, and hence, don't need to ask the user for any help inferring their return types.</li>
<li>It's trivial to refactor that into a function *
<pre><code class="language-rust">			  async fn urls_list() *&gt; Result&lt;String, warp::Rejection&gt; {
			      Ok(&quot;Listing all URLs in async&quot;.to_string())
			  }
			  // in the handler
			          .and_then(urls_list)
</code></pre>
And this now can hold whatever connection logic you wish to pass it.</li>
</ul>
</li>
<li>
<p>Route Pattern</p>
<ul>
<li>A simple pattern is to organize your routes around a base route, and use the <code>or</code> composition to create routing.</li>
<li>
<pre><code class="language-rust">			  pub fn todos_filter() *&gt; impl Filter&lt;Extract = impl warp::Reply, Error = warp::Rejection&gt; + Clone {

			    	// base route defined
			      let urls_base = warp::path(&quot;urls&quot;);

			      let list = urls_base
			          .and(warp::get())
			          .and(warp::path::end())
			          .and_then(urls_list);

			      let single_list = urls_base
			          .and(warp::get())
			          .and(warp::path::end())
			          .and_then(urls_list);

			      list.or(single_list) // composition used
			  }
</code></pre>
</li>
</ul>
</li>
<li>
<p>JSON for Replies</p>
<ul>
<li>We can use <code>serde_json</code> along with <code>warp::Reply::Json</code> to return JSON from a function.</li>
<li>Fetch JSON _&gt; Convert to JSON with <code>warp::reply::json()</code> _&gt; return as <code>Ok()</code></li>
<li>
<pre><code class="language-rust">			  async fn urls_list() *&gt; Result&lt;Json, warp::Rejection&gt; {

			      // TODO get from DB
			      let todos = json!([
			          {&quot;id&quot;: &quot;1&quot;, &quot;url&quot;: &quot;abc&quot;, &quot;shortcode&quot;: &quot;def&quot;},
			          {&quot;id&quot;: &quot;2&quot;, &quot;url&quot;: &quot;wxy&quot;, &quot;shortcode&quot;: &quot;xyz&quot;},
			      ]);

			      let urls_json = warp::reply::json(&amp;todos);
			      Ok(urls_json)
			  }
</code></pre>
</li>
</ul>
</li>
<li>
<p>Adding a path parameter</p>
<ul>
<li>we can use the <code>warp::path::param()</code> with an <code>and</code> to add a path parameter at some point.</li>
<li>
<pre><code class="language-rust">			      let single_list = urls_base
			          .and(warp::get())
			          // add a path parameter * This type is inferred based on the first argument that the
			          // handler function takes.
			          // Parse Failures are 404s.
			          .and(warp::path::param())
			          .and(warp::path::end())
			          .and_then(urls_single_list);
</code></pre>
</li>
<li>The type of the parameter is inferred and parsed based on the argument of the handler function. 😍</li>
</ul>
</li>
<li>
<h2>Reading request body</h2>
<pre><code class="language-rust">			  	// in the route declaration phase
			  	let create_url = urls_base
			          .and(warp::post())
			          .and(warp::body::json())
			          .and_then(urls_create);

			  // handler function
			  async fn urls_create(req_body: Value) *&gt; Result&lt;Json, warp::Rejection&gt; {
			      // TODO add a new URL to the Database.
			      let url_new = req_body;
			      let url_json = warp::reply::json(&amp;url_new);
			      Ok(url_json)
			  }
</code></pre>
</li>
</ul>
</li>
<li>
<h2>Auth as a custom filter</h2>
<pre><code class="language-rust">		  use warp::Filter;

		  const HEADER_XAUTH: &amp;str = &quot;X*Auth*Token&quot;;

		  pub fn check_auth() *&gt; impl Filter&lt;Extract = ((),), Error = warp::Rejection&gt; + Clone {
		      // implement custom auth to check the header to see if we're authenticated our not

		      // implement a blank filter
		      warp::any()
		          .and(warp::header::&lt;String&gt;(HEADER_XAUTH))
		          .and_then(|xauth: String| async move {
		              // trivial auth check
		              if !xauth.ends_with(&quot;.exp.signature&quot;) {
		                  return Err(warp::reject::custom(FailAuth));
		              }

		              Ok::&lt;(), warp::Rejection&gt;(())

		          })
		  }

		  // this is a custom error type, better to use this
		  // than to panic or do something similar
		  #[derive(Debug)]
		  pub struct FailAuth;
		  impl warp::reject::Reject for FailAuth {}

</code></pre>
<ul>
<li><code>warp::any</code> can be thought of as a blank filter that implements nothing</li>
</ul>
</li>
<li>
<p>Auth as a custom Filter * Returning the User Context</p>
</li>
<li>
<p>Sharing State with Filters</p>
<ul>
<li>Shared State (Database Pool, etc) can be implemented as a custom filter that extracts nothing, but passes along a new, reference*counted version of a method to access shared state into the handler function parameters.</li>
</ul>
</li>
</ul>
</li>
</ul>
]]></content:encoded></item><item><title>LevelDB Braindump</title><link>https://rowjee.com/blog/leveldb_overview.html</link><description><![CDATA[A brain dump of all the things I find interesting from my read of the LevelDB Codebase]]></description><author>null</author><pubDate>Mon, 23 Mar 2026 09:00:00 +0000</pubDate><content:encoded><![CDATA[<p>This is a brain dump of My thoughts and things I thought were interesting about the levelDB codebase.</p>
<p><strong>levelDB doesn't have any external synchronization primitives</strong> - it's interesting to note this as a design primitive, where the write path handles queueing/waiting for access to the internal store. I found it fascinating that your current write is just in queue waiting to get access, and this happens without the user being aware of any queueing.</p>
<p><strong>fsync is optional</strong> - the write options have a way to let you skip fsyncing the write, leaving it potentially stuck in the pagecache. I don't think this is a good thing, and I'm struggling to see a usecase for when I might want to skip fsync.</p>
<p><strong>there are two memtables</strong> - this is common and back when it was designed i'm sure IO slowness would have warranted it, because you usually have two memtables when flushing a memtable to disk takes a lot of time. In the grand scheme of things it still does - one in-memory IO vs one disk IO.</p>
<p><strong>the WAL is represented as a log, which has its own internal buffering</strong>.</p>
<p><strong>Having an overloaded <code>Status</code> for returning errors is actually good. Saves you a lot of guessing</strong>.</p>
<p><strong>the <code>Env</code> functionality lets the end-user override a lot of codepaths (especially with regard to filesystem operations)</strong> - this allows for some really cool customizations and feature injections. For example, one might wish to print something/log something every time a file is deleted. This is also great for wrangling cross-platform file management quirks and implementations.</p>
<p>see: <a href="https://github.com/google/leveldb/blob/a6b3a2012e9c598258a295aef74d88b796c47a2b/include/leveldb/env.h#L51">https://github.com/google/leveldb/blob/a6b3a2012e9c598258a295aef74d88b796c47a2b/include/leveldb/env.h#L51</a></p>
<p><strong>LevelDB also implements writer throttling</strong> - In LSM Trees, the number of files present in L0 are a dominating factor in read latency. In the tiered compaction strategy, we can enforce the invariant that due to non-overlapping SSTable ranges in every level, for every level except level 0, we can guarantee that only one SSTable file has the key we're concerned about, so the IO load scales proportionally with the number of levels. This doesn't apply to L0, where all the memtables are flushed directly in potentially overlapping ranges, so we may need to read more than one file on every read.</p>
<p>When the number of files in l0 go above a certain threshold, we throttle writes by trying to compact, or delay, within the writer.</p>
<p>see: <a href="https://github.com/google/leveldb/blob/4a0c572440c7df2f56a6f5fb5aec9e366d522edb/db/db_impl.cc#L1331">https://github.com/google/leveldb/blob/4a0c572440c7df2f56a6f5fb5aec9e366d522edb/db/db_impl.cc#L1331</a></p>
<p><strong>Manual reference counting</strong> - the memtable (at least; i've seen it in other places too, namely, the <code>Version</code> ) in LevelDB re-implements <code>std::shared_ptr&lt;T&gt;</code> semantics with manual reference counting. This is very curious and interesting to see - the git blame (possibly from a refactoring /sync commit) comes from 2011, which is also exactly when <code>std::shared_ptr&lt;T&gt;</code> was introduced as a part of C++11. Internal methods manually increase the reference count of this memtable.</p>
<p>The only scenario I can think of is that if some iterator is reading from the memtable and for whatever reason the memtable is flushed, we don't want the memtable to disappear.</p>
<pre><code class="language-cpp">class MemTable {  
 public:  
  // MemTables are reference counted.  The initial reference count  
  // is zero and the caller must call Ref() at least once.  explicit MemTable(const InternalKeyComparator&amp; comparator);  
  ...
  
  // Increase reference count.  
  void Ref() { ++refs_; }  
  
  // Drop reference count.  Delete if no more references exist.  
  void Unref() {  
    --refs_;  
    assert(refs_ &gt;= 0);  
    if (refs_ &lt;= 0) {  
      delete this;  
    }  
  }
  ...
}
</code></pre>
<p><strong>the Read codepath also can cause a compaction</strong> - it looks like whenever the snapshot is edited due to a compaction, it computes a recommendation for the next level to compact. The read codepath then tries to schedule a compaction (if recommended -  measured via <code>Version::compaction_score_</code>)  in the <code>DBImpl::MaybeScheduleCompaction</code> codepath.</p>
<p><a href="https://github.com/google/leveldb/blob/4a0c572440c7df2f56a6f5fb5aec9e366d522edb/db/db_impl.cc#L668">https://github.com/google/leveldb/blob/4a0c572440c7df2f56a6f5fb5aec9e366d522edb/db/db_impl.cc#L668</a></p>
]]></content:encoded></item><item><title>What Running 500 Kilometres Taught Me</title><link>https://rowjee.com/blog/running500km.html</link><description><![CDATA[...]]></description><author>null</author><pubDate>Sun, 5 Oct 2025 09:00:00 +0000</pubDate><content:encoded><![CDATA[<p>I decided to start running (after much encouragement from some very special people) in December 2024. On the 2nd of September, 2025, I crossed a lifetime running mileage of 500km. The sheer insanity of this milestone isn't something I can get past anytime soon; 500km is roughly six trips to the Kempegowda International Airport and back.</p>
<p>Growing up, I was far from an athletic kid; The fact that I was pretty chubby didn't help my case either, as the difference made itself felt during sports. I tried taking part in many, many different sports (Basketball, Football, Badminton, Cricket, even) but none of them really clicked until I started running. I distinctly remember being the last one to finish in the 400m/800m running trials in school multiple times over. Outside of that, I absolutely hated running; It made absolutely no sense to me that people would enjoy the constant torture of being out of breath and struggling to move even an inch more without wanting to lie down. Fast forward five years, and I've accomplished what I think is a pretty neat thing. How did we get here?</p>
<p>Though I'd started with the &quot;Couch to 5k&quot; program in December 2024, I bought my first pair of running shoes in Jan 2025 and was keeping to a very unstructured, c25k-ish program just based on vibes. Around February of 2025, I experienced a loved one having a pretty severe health scare; this was an inflection point for me, because I realised that if I didn't change how I was living my life, I would be next. Cardiovascular fitness became a top priority for me. I took the plunge and bought a Garmin Forerunner 265, and decided to take my running more seriously.</p>
<p>Discovering Interval running (more along the style of the Run-Walk-Run method) was a life-changer for me. It suddenly made it possible for me to consider that I could do more than a 2km long walk (my longest yet) - and soon I was doing 3km in intervals, running for three minutes, walking for 30 seconds, and so on until I hit the mark. I slowly pushed that to 5km, and before I knew it, in April 2025, I ran my 5K PR time at the IIITB Miles 4 Meals race. This was my first organized race, and I chose to ran it in intervals. The elevation changes along the electronic city course were a sure surprise; I never expected to struggle so much. I ended up finishing that race in around 37 Minutes. Not a fantastic time by any means (I see you, sub-30 and sub-20 folks), but it was <strong>my first race</strong>. I had voluntarily signed up to do sports, paid money, even, to do sports, and not only did I do it, I did it a little quicker than I thought I could! This was insane. It set forth a chain of events that would eventually lead to me signing up for the Wipro Bengaluru Marathon, setting myself an ambitious goal of a sub 1:10 10k.</p>
<h2>Running</h2>
<p>Distance running is an endurance sport. Why is it an endurance sport? Because to do it, you have to endure your mind screaming at you for half an hour / one hour / etc and be in a state of consistent physical exertion</p>
<h2>What I learnt</h2>
<p><strong>showing up matters. showing up is where it starts</strong>. This is the chief learning I've had from this entire experience. It does not matter how bad your day went. It does not matter if you're tired and want to lie down. If your workout for the day is a 7:00min/km 3k interval workout, and all you can manage is a 13:00min/km 1k, or lesser, or if you go a few steps and turn back, that's still a win. You got out of whatever you were doing, got ready, stretched, whatever you had to do, and you started. That's the win.</p>
<p>The converse is also true; some workouts you think will be really hard, or that you'll end up being unable to do, turn out to be not so hard or not as bad, simply because you didn't let the fear/anticipation of the workout change how you showed up. You prepared yourself, laced your shoes, and presented yourself to the challenge, ready to take it on. This is the win.</p>
<p><strong>You can do so much more than you think you can</strong> - If you had told me five years ago that I walked (let alone ran) 10km, I'd have laughed - but I did do it! I ended up beating my adjusted expectation of a 1:20 finish by about 3 minutes, only because I put in the work, week after week, workout after workout, stretching myself a little more each and every time. You cannot start running 10km overnight; first you run 3 (two weeks in a row), 3.5 (two weeks in a row), 4 (two weeks in a row)... all the way until you reach 10km, multiple weeks in a row. <strong>gradual progressive overload is key</strong>.</p>
<p>Personally, I'm rather unimpressed by people who manage to pull off impressive race times without training. If it was just about the race, then you wouldn't have to worry about methodology, sustainability, anything of that sort - the people who followed a training plan, showed up day after day, week after week, are the ones who impress me. It takes grit, and sometimes, you acquire said grit, by following a training course.</p>
<p><strong>easy workouts are extremely important</strong> - my longest run of the week is a zone 2 run. I've stopped measuring my long run by pace and instead measure by heart rate instead. This has made a massive difference.</p>
]]></content:encoded></item><item><title>Understanding Raft</title><link>https://rowjee.com/blog/papers/raft.html</link><description><![CDATA[Taking a look at Raft, a distributed consensus protocol]]></description><author>null</author><pubDate>Sun, 18 Sep 2022 09:00:00 +0000</pubDate><content:encoded><![CDATA[<blockquote>
<p>Hello! You're probably here through a link in a lab manual. This post is WIP, but I hope I've managed to motivate the need for this algorithm, as well as providing you with a few questions to think about what's really going on under the hood.</p>
</blockquote>
<p>You'll find the original research paper for the Raft Consensus Algorithm <a href="https://raft.github.io/raft.pdf">here</a> - and <a href="https://raft.github.io/">the website</a> lists a bunch of amazing resources you can refer to as well.</p>
<p>We know that Raft is a Distributed Consensus Algorithm, and while that's right, it's a way of looking at Raft from a &quot;What does it do&quot; perspective, not from a &quot;what problem does it solve&quot; perspective. Keep reading - I'll briefly motivate the problem, and you'll see why this algorithm is necessary.</p>
<h2>Motivating the problem</h2>
<p>Let's consider that you're the lead backend developer for a hot new startup, <code>fonbuk</code>, which provides people with an online phonebook.</p>
<p>The storage backend for your application is a python dictionary, quite literally this -</p>
<pre><code class="language-python">{
  &quot;user1&quot;: {
    &quot;ABC&quot;: 123567890,
    &quot;DEF&quot;: 123567890,
    &quot;GHI&quot;: 123567890,
  },
  &quot;user2&quot;: {
    &quot;ABC&quot;: 123567890,
    &quot;DEF&quot;: 123567890,
    &quot;GHI&quot;: 123567890,
  },
  ...
  # And so on
}
</code></pre>
<p>The operations on this look as follows - any user can only set and get phone numbers, and these are the interfaces that any sort of clients will use.</p>
<pre><code class="language-python">
def set(phonebook, user_name: str, contact_name: str, number: int):
  ...

def get(phonebook, user_name: str, contact_name: str):
  ...
</code></pre>
<p>We'll walk through the stages of growth that your company goes through, and the journey you face as the developer.</p>
<h3>Stage 0: Single User, Single Machine</h3>
<ul>
<li>Scale: 10 users per day</li>
</ul>
<p><img src="/static/images/raft/su_ss_fonbook_example.png" alt="" /></p>
<p>In this part, you're the only user - you get to handle all the queries to read and write numbers, since you're an early-stage startup - so there's nobody else to do this. Your scale, 10 users per day, means you can very comfortably manage to run this on a single machine.</p>
<p>Since you're the only user, you get to decide what order you save phone numbers in - if two people want to change the same contact at once, you decide whose write &quot;wins&quot;. This is important.</p>
<h3>Stage 1: Multiple User, Single Machine</h3>
<ul>
<li>Scale: 100 users per day</li>
</ul>
<p><img src="/static/images/raft/mu_ss_fonbook_example.png" alt="" /></p>
<p>Let's say you manage to make it to TechCrunch Disrupt, displacing title favorites &quot;Pied Piper&quot; for the prize - your innovation has won hearts!
People start running toward your app for their phone needs. At this point, you need to handle multiple users!</p>
<p>So, small problem. While your single-user, single-computer model worked fine, you've now got multiple people reading from and writing to the same piece of data. This introduces a bunch of problems, such as -</p>
<ol>
<li>If two people write to the same sub-item at the same time, whose write is correct?</li>
<li>If Person A did <code>set(pb, &quot;anirudh&quot;, &quot;somesh&quot;, 123)</code> and <em>then</em> person B did <code>set(pb, &quot;anirudh&quot;, &quot;somesh&quot;, 123)</code> but Person B's write reached the server before Person A's did, whose write should be considered?</li>
</ol>
<p>The answer to this is that for &quot;consistency&quot; (i.e. having the &quot;right&quot; value in place according to some scheme), it's necessary for someone to mediate these <em>conflicts</em>. Since you're a capable developer, you implement this - but soon enough, you begin to see some problems.</p>
<h3>Stage 1.5: Single User, Multiple Machines</h3>
<p><img src="/static/images/raft/su_ms_fonbook_example.png" alt="" /></p>
<ul>
<li>Scale: 1000 Read-Heavy users per day (reads outnumber writes in a 100:1 ratio)</li>
</ul>
<p>You decide to carry out an experiment. Computers have limits - there's only so many requests one computer can handle, so you decide - since most people on this application are reading phone numbers rather than writing, I can have multiple computers to handle the read requests.</p>
<p>So, There's only one user who gets to decide the order of things (that's you) and who gets to mediate all conflicts - that's also you.</p>
<p>However, here's a small problem - if you update a value by using <code>set</code> on one computer, how do all the other computers know this value has been updated? Think about it as cache invalidation of a sort. Since there's only one user, you can do what's known as <em>replication</em> to let all the other computers know that something's been changed here, and what it's been changed to.</p>
<p>The challenge here is maintaining <em>consistency</em> between the different copies of state on various machines so that they can handle all these queries independently. Since there's only one person deciding what's being written (you), there are no problems.</p>
<h3>Stage 2: Multiple Users, Multiple Machines</h3>
<p>Scale: 100,000 Users</p>
<p><img src="/static/images/raft/mu_ms_fonbook_example.png" alt="" /></p>
<p>Congratulations on hitting scale! Your startup now needs the best, fastest data solution you have without burning a hole in their pockets - And you come to the rescue! Since you want to take advantage of parallelism, you have multiple machines, and since you don't want to make any user (or human) being the bottleneck, you let people access these machines in parallel.</p>
<p>This brings you to a <em>host</em> of interesting problems, such as -</p>
<ol>
<li>There's no centralized conflict resolution - multiple computers may see multiple values as the correct value at any given point in time, leading to inconsistency</li>
<li>There's no easy way to tell whether or not a computer has the latest value</li>
<li>If you write a value, there's no guarantee that all computers have recieved that write - maybe some overwrote it!</li>
</ol>
<h3>The Solution</h3>
<p>A bunch of really smart people a while back figured out that a good and acceptable (not perfect) solution was to <strong>get the computers to agree on whether or not a value should be accepted by everyone as the truth</strong>.</p>
<p>This works, because</p>
<ol>
<li>All Computers agree on the ordering of writes, so there are no conflicts - taking care of our <em>conflict resolution</em> problem</li>
<li>All Computers have the same state post-agreement, taking care of our <em>replication</em> problem</li>
<li>All Computers will only give back the latest value it has, and because all computers agree on any new values, this will be the latest value overall.</li>
</ol>
<p>Awesome! We now understand what Distributed Consensus is - it's when a bunch of computers agree on a certain decision that should be taken, which all other computers agree to.</p>
<h2>What's Raft, then?</h2>
<p>Raft is a distributed consensus algorithm that works by separating the problem of distributed consensus into three distinct parts (Leader Election, Log Replication, Safety), and solving each on its own.</p>
<p>The TL;DR Black Box overview is this - Any group of computers using Raft for consensus on any decision will be able to guarantee (<a href="https://en.wikipedia.org/wiki/Eventual_consistency">within reasonable bounds</a>) that all computers have the same decisions in the same order.</p>
<p>To put it more formally, we can think of Raft as an algorithm to manage a replicated log. If we look at our Phonebook example, it turns out that we can represent operations on that phonebook as a series of log entries.</p>
<pre><code>SET ANIRUDH ABC 123
SET SOMESH DEF 134
</code></pre>
<p>The Log above, when applied in the same order, gives you -&gt;</p>
<pre><code class="language-python">{
  &quot;ANIRUDH&quot;: {
    &quot;ABC&quot;: 123,
  },
  &quot;SOMESH&quot;: {
    &quot;DEF&quot;: 134,
  },
}
</code></pre>
<p>The Key realization is that the log is a series of actions, and when applied to any FSM, you'll end up in the same state for a deterministic FSM - so if you manage to &quot;replicate&quot; the log of actions keeping the order intect, technically... you've managed to replicate the state! <a href="https://en.wikipedia.org/wiki/State_machine_replication#:~:text=ordering%20inputs%5Bedit%5D">Even then, that has some limitations, but we won't go there today</a>.</p>
<p>Not to mention the fact that you can represent a <em>lot</em> of data structures like this, including databases :)</p>
<h2>How does Raft Work?</h2>
<p><img src="https://www.eecs.berkeley.edu/~rcs/research/raft_fsm.png" alt="" />
<em>Raft Node States: <a href="https://www.eecs.berkeley.edu/~rcs/research/raft_fsm.png">https://www.eecs.berkeley.edu/~rcs/research/raft_fsm.png</a></em></p>
<p>Let's assume a cluster of five nodes, each of them in the Follower state by default.</p>
<p>Each of them is equipped with a randomized timer, ticking down from the time the node comes alive. Eventually, someone's timer expires, and when that timer expires, the node (say node 1) stands for election by moving to a candidate state.</p>
<p>All the other nodes in the cluster look at this and go &quot;hey, someone who wants to do my work for me :D let's vote them in&quot;, thus, appointing a &quot;term leader&quot; for the current &quot;Term&quot; of the cluster. A term is just a <a href="https://en.wikipedia.org/wiki/Logical_clock">logical clock</a>, and you can think of it as a number that increases every time a new leader is elected.</p>
<p>So, once this leader is voted in a by a majority of the servers, it now has full control - i.e. all writes that all followers need to do get forwarded to the leader, whereas the followers can serve read queries. Remember - All Writes Flow to the Leader! There can only be one leader per term.</p>
<p>Once the leader recieves a write and has made sure that a majority of nodes in the cluster have recieved that write, it can then &quot;commit&quot; that write, which means it's safe for all other machines in the cluster to assume that the value will not be lost in the event the leader crashes.</p>
<h3>What happens if the leader crashes?</h3>
<p>The leader checks the status of each node in the cluster every once in a while. Every time a node hears from the leader, its timer gets reset.</p>
<p>Simple! Another node whose ranom timer expired will stand for election, become the next term leader, and will then guide the rest of the nodes on what to do. This is why Raft is Fault-Tolerant.</p>
<h3>What's the decomposition?</h3>
<ol>
<li>Log Replication -
Leaders have rules on when they can and can't tell nodes to write a value. Nodes, too, will perform a bunch of checks - to see if they request to write is one that's consistent with the previous history of the node.</li>
<li>Leader Election -
Raft's use of timers makes it particularly safe from split-brain problems, such as two leaders in the same cluster - the timers also allow for deterministic randomization of the leader election process.</li>
<li>Safety - Nodes will only vote a candidate into leadership if the candidate has the latest data. This is determined using term numbers.</li>
</ol>
<p>This decomposition not only makes the algorithm easier to understand but also easier to build.</p>
<h3>Overarching WIP Conclusion</h3>
<p>You definitely should learn Raft. <a href="https://raft.github.io/">This visualization</a> is an <em>excellent</em> one - try doing all sorts of things like stopping nodes, adding new ones, and so on.</p>
<hr />
<p>We start clean.</p>
<p>Hello! Let's understand the Raft Consensus Protocol.</p>
<p>You might be asking - What the hell is Raft? And you're right. there's no clear answer. Is it a boat? is it a plane? Is it Batman? And these are all valid questions. There's only one right answer, which is that Raft is a distributed Consensus Algorithm.</p>
<p>Sometimes just one computer isn't enough, ya know? Sometimes you just need more juice. Sometimes your one dusty old AMD Athlon Dual Core from 2006 can't keep up with the load, which means you need to <em>scale</em>.</p>
<p>Scaling, our benevolent dictator, manifests for computers in two ways - Vertical Scaling and Horizontal Scaling.</p>
<ul>
<li>Vertical Scaling
Increasing the amount of CPU, RAM, Disk Space available on each machine - as you go higher, this becomes prohibitively expensive</li>
<li>Horizontal Scaling
Get a bunch of lower-power machines and make them process the data in parallel! It's automatically parallel without switching overhead et al and is also cheaper.</li>
</ul>
<p>Since vertical scaling has its limits and is expensive, horizontal scaling is often the recommended option. You'll see some notable holdouts to this - such as Stackoverflow - but I digress.</p>
<p>Ultimately, if you want multiple machines coordinating on some single task, you'll need them to talk to each other to figure out what to do and how to do it! In some cases you usually have someone coordinating these things (a la Zookeeper, HDFS, etc) or you have these decisions being made autonomously amongst these machines themselves, without you having to appoint a &quot;leader&quot;.</p>
<p>Getting machines separated by a network, all working towards the same thing (i.e. a Distributed System) to agree on a descision (one thing) is known as a consensus algorithm.</p>
]]></content:encoded></item><item><title>You Wouldn&apos;t Unlock a Mutex!</title><link>https://rowjee.com/blog/you_wouldnt_unlock_a_mutex_in_rust.html</link><description><![CDATA[Did you know that you can't unlock a Mutex in Rust?]]></description><author>null</author><pubDate>Fri, 14 Apr 2023 09:00:00 +0000</pubDate><content:encoded><![CDATA[<p>Did you know that you cannot unlock a Mutex Lock in Rust?</p>
<p>Mutex unlocks are implicit in rust - this means that you cannot explicitly unlock a mutex in Rust, and that the <code>Mutex&lt;T&gt;</code> does not implement an unlock method in Rust.
This means that you need to be <em>really</em> careful about how you lock and unlock Mutexes in Rust.</p>
<p>Consider the usecase of a threadpool - threadpools use parallelism to increase throughput in a request-based system like a web server. This allows the server to work on multiple requests in Parallel. So this usually requires incoming requests to be placed into a queue for workers to consume from.</p>
<p>In a multi-threaded system, each worker thread must occasionally check this queue to see if there are jobs on the queue, and must pick one job off the queue to handle. This allows multiple threads to work on jobs in parallel, reasonably(include amdahl's law ref) increasing throughput of the system.</p>
<p>To do this, we use a Mutex lock on the queue, to prevent that only one worker can remove one job from the queue at any given time. We do not care which worker thread it is, so long as there is only one. If we do not do this, we can have messy situations like two worker threads picking up the same job and working on it, leading to a host of problems like overwritten responses, processing it twice overall, etc -&gt; so the worker thread's job is basically</p>
<ul>
<li>Check the queue - see if there are any jobs for me</li>
<li>Pick the first available job for me</li>
<li>process the job</li>
</ul>
<p>At times like this, the question of &quot;how long does a worker thread hold the lock on the queue&quot; is of crucial importance to the throughput of the system. Ideally, you want to unlock as soon as you finish reading the task (usually a function pointer or the task description) from the queue. Especially in a web server, since it isn't easy to predict how long each request will take, it's better to unlock as soon as you consume the job, and not hold the lock until the job is done executing. This allows other threads to get their jobs faster.</p>
<p>Funny thing is in Rust there's no way to explicitly unlock a mutex - it automatically unlocks when the lock falls out of scope. Now you might say -</p>
<blockquote>
<p>That's stupid!</p>
</blockquote>
<p>And I'll agree with you - but idiomatic rust will show up and smack us both on the head. Idiomatic Rust basically encourages this to happen implicitly.</p>
<pre><code class="language-rust">// This causes contention! `held_lock` is not going to be left alone until the task is
// finished.
let held_lock = channel.lock().unwrap();
let current_task = held_lock.recv().unwrap();
</code></pre>
<p>In this case, the lock denoted by <code>held_lock</code> is going to remain held until it reaches the end of the current scope. Assuming you'll be executing your task after this, it's bad news for the performance.</p>
<p>But.. what if you could somehow <em>fool</em> the lock into thinking it's reached the end of the scope? Rust also allows you to do this by introducing arbitrary scoped blocks wherever you want. So we just put the lock into the scoped block, and the mutex is released once the block is dropped!</p>
<pre><code class="language-rust">// This does not cause contention! You're using a custom scope block to drop the
// MutexGuard, hence unlocking the channel consumer, and letting other threads get jobs
let current_task: Job = {
	let held_lock = channel.lock().unwrap();
	held_lock.recv().unwrap()
};
</code></pre>
<p>Without proper error handling, you're more likely to do this when you're prototyping. If you haven't read <a href="https://blog.burntsushi.net/rust-error-handling/">this</a> then you're also likely to put it in prod.</p>
<pre><code class="language-rust">// This is the same as the above - because the MutexGuard that's returned after the
// first unwrap is dropped after the recv method
let current_task = channel.lock().unwrap().recv().unwrap();
</code></pre>
<p>idk why this decision was taken. It appears to be <a href="https://github.com/rust-lang/book/issues/1871">something of minor controversy</a>, but as of right now the correct way (sans <code>.unwrap</code>) appears to be the scoped block option.</p>
]]></content:encoded></item><item><title>Refactoring a Large Function in Go</title><link>https://rowjee.com/blog/refactor_large_functions_in_go.html</link><description><![CDATA[I write about a challenge I faced at work, and how I solved it]]></description><author>null</author><pubDate>Sat, 23 Mar 2024 09:00:00 +0000</pubDate><content:encoded><![CDATA[<p>So, you've finally done it! You've shipped a PoC, and the dust has finally
settled; your boss, your boss's boss, and your boss's boss's boss, all like what
you're working on. Hooray!</p>
<p>You can now finally stop thinking about your code as a pencil sketch, and you
can now finally start painting. You can afford to now pay attention to the finer
details - what is this function supposed to be called? Do we really need all the
data in this struct? In other words, you can now start refactoring.</p>
<blockquote>
<p><em>“Any fool can write code that a computer can understand. Good programmers
write code that humans can understand.” ~ Martin Fowler</em></p>
</blockquote>
<p>A well-timed refactor can save you a lot of time and energy; we've all been in
the situation where we've looked at code we've written a while back, and have
understood <em>none</em> of it. Context - the reason you can get away with giving
variables single-letter names - fades, and leaves behind a massive void. By
refactoring your code for maintainability, you're looking out for your future
self!</p>
<p>Now that you've been convinced that this is important, let's walk through an
example. The goal here is as follows -</p>
<blockquote>
<p>Refactor a large function into multiple smaller functions with minimal
breakage</p>
</blockquote>
<h2>The Problem</h2>
<p>Consider the following function, built by our friendly neighbourhood widget
factory elves. As you can see, they're doing their best, but in the interest of
iteration, they've ended up with a massive, massive function.</p>
<pre><code class="language-go">// main.go
package main

import &quot;fmt&quot;

func FizzBuzzOverkill(limit int) ([]string, error) {
	var final_output []string
	for i := 0; i &lt; limit; i++ {
		output := &quot;&quot;
		div_three := false
		div_five := false
		// check for divisibility of three
		if i%3 == 0 {
			div_three = true
			output += &quot;fizz&quot;
		}
		if i%5 == 0 {
			div_five = true
			output += &quot;buzz&quot;
		}
		if !div_three &amp;&amp; !div_five {
			output += fmt.Sprintf(&quot;%d&quot;, i)
		}
		final_output = append(final_output, output)
	}
	return final_output, nil
}
</code></pre>
<h2>The Solution</h2>
<p>How messy! Certainly, we can do better than this - but refactoring without
tests, atleast end-to-end tests, is a bad idea! you need some baseline for
correctness.</p>
<pre><code class="language-go">// main_test.go
package main

import (
	&quot;testing&quot;
	&quot;golang.org/x/exp/slices&quot;
)

func TestFizzBuzz(t *testing.T) {
	sample_output := []string{&quot;fizzbuzz&quot;, &quot;1&quot;, &quot;2&quot;, &quot;fizz&quot;}
	generated_output, err := FizzBuzzOverkill(4)
	if err != nil {
		t.Fatal(err)
	}
	if !slices.Equal(sample_output, generated_output) {
		t.Errorf(&quot;Fizzbuzz Failed!&quot;)
	}
}
</code></pre>
<p>There we go! Given that this test passes, we can now start to begin refactoring.</p>
<p>The guiding idea here is that you <em>do not</em> attempt to rewrite it all at once;
you isoldate the codebase into blocks, convert each function into a block one by
one, and then slowly repalce blocks with their respective code. At the end of
each integration, run the test to see if it's working well!</p>
<p>Let's see how we do that. In the example here, we can see three major tasks
within the loop:</p>
<ol>
<li>Check for divisibility by 3, and</li>
<li>Check for divisibility by 5, and</li>
<li>Add the number itself if it isn't divisible by either</li>
</ol>
<p>Given that we've identified the major working units of the code, let's begin by
pulling out these parts.</p>
<p>An important point to note is these refactors work better, and more
specifically, this method, works better when you majorly deal with pure
functions. We don't want any ghosts in the shell!</p>
<pre><code class="language-go">// Function to add &quot;fizz&quot; to the string if it's divisible by 3
func ModifyStringIfDivisibleBy3(number int, output_string *string) {
	if number%3 == 0 {
		*output_string += &quot;fizz&quot;
	}
}
</code></pre>
<p>This is slightly convoluted, but you get the idea. Let's now integrate it into
the main codebase.</p>
<pre><code class="language-go">// main.go
func FizzBuzzOverkill(limit int) ([]string, error) {
	var final_output []string
	for i := 0; i &lt; limit; i++ {

		output := &quot;&quot;
		div_three := false
		div_five := false

		// check for divisibility of three, set div_three to true if it works
		div_three = ModifyStringIfDivisibleBy3(i, &amp;output)

		if i%5 == 0 {
			div_five = true
			output += &quot;buzz&quot;
		}

		if !div_three &amp;&amp; !div_five {
			output += fmt.Sprintf(&quot;%d&quot;, i)
		}

		final_output = append(final_output, output)
	}
	return final_output, nil
}
</code></pre>
<p>running our tests -</p>
<pre><code class="language-shell">anirudh@shatterdome:~/projects/fizzbuzz-overkill  $ go test .
ok      fbo     0.001s
</code></pre>
<p>We are now good to go! We can now shamelessly refactor. Let's change, piecewise,
the other two segments of the program.</p>
<p>Let's remove the divisibility check for five -</p>
<pre><code class="language-go">// Function to add &quot;fizz&quot; to the string if it's divisible by 3
func ModifyStringIfDivisibleBy3(number int, output_string *string) bool {
	if number%3 == 0 {
		*output_string += &quot;fizz&quot;
		return true
	}
	return false
}

// Function to add &quot;fizz&quot; to the string if it's divisible by 3
func ModifyStringIfDivisibleBy5(number int, output_string *string) bool {
	if number%5 == 0 {
		*output_string += &quot;buzz&quot;
		return true
	}
	return false
}

func FizzBuzzOverkill(limit int) ([]string, error) {

	var final_output []string

	for i := 0; i &lt; limit; i++ {
		output := &quot;&quot;
		div_three := false
		div_five := false

		// check for divisibility of three
		div_three = ModifyStringIfDivisibleBy3(i, &amp;output)
		div_five = ModifyStringIfDivisibleBy5(i, &amp;output)

		if !div_three &amp;&amp; !div_five {
			output += fmt.Sprintf(&quot;%d&quot;, i)
		}
		final_output = append(final_output, output)
	}
	return final_output, nil
}
</code></pre>
<pre><code>anirudh@shatterdome:~/projects/fizzbuzz-overkill  $ go test .
ok      fbo     0.002s
</code></pre>
<p>And now, lastly, the catch-all.</p>
<p>At this point, it's a slightly controversial take - I think it's okay to leave
it as is at this point. This may be biased by this particular example, but i'm
trying to promote responsible refactoring and I think you shouldn't do anything
more to this. It's <em>okay</em>.</p>
<p>Eliminating the redundant variables, we get:</p>
<pre><code class="language-go">func FizzBuzzOverkill(limit int) ([]string, error) {
	var final_output []string

	for i := 0; i &lt; limit; i++ {
		output := &quot;&quot;

		div_three := ModifyStringIfDivisibleBy3(i, &amp;output)
		div_five := ModifyStringIfDivisibleBy5(i, &amp;output)
		if !div_three &amp;&amp; !div_five {
			output += fmt.Sprintf(&quot;%d&quot;, i)
		}

		final_output = append(final_output, output)
	}
	return final_output, nil
}
// The line breaks added are purely for aesthetic reasons!
</code></pre>
<h2>Why this?</h2>
<p>The advantage of this approach is that you get to keep your refactoring process
alive, while also ensuring your code works. It seems somewhat rudimentary now,
but it was definitely helpful to me when I had to refactor a large function.</p>
<p>Happy Refactoring!</p>
<pre><code>\m/
</code></pre>
]]></content:encoded></item><item><title>A summary of &quot;Error Handling in Rust&quot;</title><link>https://rowjee.com/blog/rust_error_handling.html</link><description><![CDATA[I attempt to summarize an article by @burntsushi5]]></description><author>null</author><pubDate>Tue, 30 May 2023 09:00:00 +0000</pubDate><content:encoded><![CDATA[<p><a href="https://blog.burntsushi.net/rust-error-handling/">Here's</a> the article -</p>
<h2>Summary: Error Handling in Rust</h2>
<ul>
<li>Panics are bad!</li>
<li>Rust enables us to use <strong>errors as variables and types</strong> as opposed to errors
as exceptions. This is better for composability.</li>
<li>using <code>.unwrap()</code> is almost always not a good idea, errors can be handled so
much better -&gt; this is because <code>.unwrap()</code> forces a <code>panic!()</code>.</li>
<li>It's good to know what <code>Option&lt;T&gt;</code> and <code>Result&lt;T, E&gt;</code> look like internally as
this knowledge allows us to handle errors so much better.</li>
<li>It's also good to know that <code>Option&lt;T&gt; == Result&lt;T, ()&gt;</code>, and that you can
convert between the two</li>
<li>If you're using <code>.unwrap()</code> multiple times in a row, it's probably a better
idea to use a special <em>combinator</em> type to allow composition.
<ul>
<li>These combinators (<code>.and_then()</code>, <code>.unwrap_or()</code>, <code>.or_else()</code>, <code>.map()</code>)
allow us to elide repetitive stuff from the code and make the code easier to
read.</li>
</ul>
</li>
<li>Combinators are sometimes hard to read and reason about (especially if you
don't know the underlying implementation) -&gt; this is why it's usually more
readable (in my opinion) to stick to implicit case analysis by using things
like the <code>try!()</code> macro and the <code>?</code> operator in conjunction with <em>early
returns</em></li>
<li>If you're combining error types, you can explore multiple options, including,
but not limited to -
<ul>
<li>Converting every error to a <code>String</code> - you lose contextual information like
<code>io::ErrorKind</code></li>
<li>Using <code>Box&lt;dyn Err [+ Send + Sync]&gt;</code> - same problem - because we're treating
the error here as a Trait Object, we lose all other information about it.</li>
</ul>
</li>
<li>The best option for library authors is to define their custom error types (see
stdlib definitions of errors as a template), which also define
<code>std::from::From&lt;T&gt;</code> on the error types that they depend on
<ul>
<li>this is so that you can easily use the <code>?</code> operator to handle unwraps and
result err matches</li>
<li>Any other error type will try to be converted to your custom return error
type, so it's good to define the <code>std::from::From&lt;E: Err&gt;</code> trait on your
custom error type.</li>
</ul>
</li>
<li>Lastly, levels of error handling ordered by difficulty of understandability
<ul>
<li>TODO: Code Examples</li>
<li>Combinators</li>
<li>Explicit Case Analysis</li>
<li>Inline implicit unwraps but top-level catch</li>
</ul>
</li>
</ul>
]]></content:encoded></item><item><title>LevelDB Read and Write Path Outlines</title><link>https://rowjee.com/blog/leveldb_readwrite_path.html</link><description><![CDATA[An outline of the read and write paths of LevelDB]]></description><author>null</author><pubDate>Thu, 2 Apr 2026 09:00:00 +0000</pubDate><content:encoded><![CDATA[<h2>Write Path</h2>
<p>in <code>DBImpl::Write</code></p>
<ul>
<li>create a <code>Writer</code> object that holds the write batch</li>
<li>lock the mutex (RAII Scoped Lock) and wait for the current write to be at the front of the writer queue
<ul>
<li>how do multiple writers lock the same lock at the same time? or do they all contend on the <code>MutexLock l(&amp;mutex_)</code>?</li>
</ul>
</li>
<li>if we need to throttle, then we wait - <code>DBImpl::MakeRoomForWrite</code></li>
<li>get the last sequence number</li>
<li>the Writer now holds responsibility for locking the memtable and WAL, so the main writer thread can release the lock</li>
<li>WAL commit: <a href="https://github.com/google/leveldb/blob/4a0c572440c7df2f56a6f5fb5aec9e366d522edb/db/db_impl.cc#L1236">https://github.com/google/leveldb/blob/4a0c572440c7df2f56a6f5fb5aec9e366d522edb/db/db_impl.cc#L1236</a></li>
<li>if WAL write was successful, write to memtable <a href="https://github.com/google/leveldb/blob/4a0c572440c7df2f56a6f5fb5aec9e366d522edb/db/db_impl.cc#L1245">https://github.com/google/leveldb/blob/4a0c572440c7df2f56a6f5fb5aec9e366d522edb/db/db_impl.cc#L1245</a></li>
<li>Lock the mutex again, and update the high sequence number</li>
<li>remove the writers from the queue (why does this happen?)</li>
<li>signal the waiting head of the write queue</li>
</ul>
<h2>Read Path</h2>
<p>in <code>DBImpl::Get</code></p>
<ul>
<li>Create a scoped RAII Lock for the mutex</li>
<li>Get the snapshot (as determined by a Sequence Number) for the read (either via the <code>ReadOptions</code> or by getting the latest Sequence Number)</li>
<li>Create a <code>LookupKey</code></li>
<li>refcount the latest memtable, immutable memtable, and <code>Version</code>
<ul>
<li>what is <code>Version</code>? <code>Version</code> appears to be how LevelDB represents an on-disk snapshot. Conversely, <code>VersionSet</code> is what appears to represent a &quot;snapshot manager&quot;
<ul>
<li>Each <code>Version</code> is a node in a linked list; it links to the next and previous version as well</li>
<li>It has a list of files in each level</li>
</ul>
</li>
<li>what is the <code>manifest_file_number_</code>? Looks to be the snapshot ID, so that we have clear versioning amongst the snapshots.</li>
</ul>
</li>
<li>Unlock the mutex and lookup the key in the memtable</li>
<li>If the key is not present in the memtable, look up the immutable memtable</li>
<li>if the key is not present in the immutable memtable, look up the current snapshot (see <code>Version::Get</code>)
<ul>
<li>create a tracker struct for lookup information</li>
<li>call into <code>Version::ForEachOverlapping</code> - takes a callback to run on every file whose key range matches the lookup key
<ul>
<li>the callback does the actual work of opening the file and reading from it - when the callback returns <code>false</code>, the search stops and returns</li>
<li>first search level 0 in order of newest to oldest - process all level 0 files that overlap and run the callback on them</li>
<li>if the callback doesn't return, proceed to search lower levels (1, 2, 3, 4)
<ul>
<li>important to note: the guarantee we have now is that only one file will contain the information for any key - so first we search for the first overlapping file within the level, then we search within the file</li>
<li>for each level, binary search the files until we find the first file whose high key is greater than the lookup key
<ul>
<li>if the lookup key is smaller than the low key of the file, then we skip the file</li>
<li>the lookup key is larger than the low key of the file, we can look within the file</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>Within the callback:
<ul>
<li>update internal stats tracking for what the last file read in the read was</li>
<li>look up the <code>TableCache</code> within the <code>VersionSet</code>
<ul>
<li>TableCache is an LRU Cache (exposed as the interface <code>Cache</code>) that maps keys to values, and maintains some other metadata for a key in a <code>Handle</code>. The <code>TableCache</code> exists because opening an SSTable (lots of work regarding decoding the index blocks, filters, etc) is an expensive process, so we can cache the file metadata in a cache and avoid having to read it on every read for the key.</li>
<li>This opens the file (if not already open) and searches the contents of the <code>Table</code> in a block-wise fashion (<code>Table::InternalGet</code>) - if the search is rejected by the bloom filter, return early</li>
<li>Once the block is found, seek to the key</li>
<li>if the key is present (i.e key we seeked to is equal to search key, not greater/lesser), and not corrupt, and not a deletion, then capture the value
<ul>
<li>and return false</li>
</ul>
</li>
</ul>
</li>
<li>notes: seeing a lot of &quot;charge against/for&quot;, wonder what this means (could this be the usage count?)</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3>LookupKey</h3>
<p>This is a special class used to make lookup operations easier in LevelDB. When the user performs a lookup with <code>DB::Get()</code>, the key as the end user sees it - a <code>Slice</code> - is converted into a <code>LookupKey</code>. This serves multiple purposes - you have only one type of key that you use for every operation here on out, whether it's the memtable lookup or the lookup from the files on disk.</p>
<p>An interesting thing to note is the small key optimization, which allocates the key on the stack within the <code>LookupKey</code> class itself if the key is smaller than 200 characters. This prevents a trip to the allocator and the memory subsystem, which generally protects performance at the cost of slightly higher memory usage - but given that the <code>LookupKey</code> is relatively ephemeral and the allocation is fast (i.e. stack frame allocation speed), this temporary wastage is an okay tradeoff to make. It's no doubt that this exact number is based on a combination of heuristics and statistics from the experience of running levelDB in prod for many workloads.</p>
<p>We also see that the <code>SequenceNumber</code> (that we now know to denote a snapshot boundary) is encoded within the byte stream of the key. This is a helpful way of tracking the snapshot we're using for lookup, and, potentially, filtering out results that exist past that snapshot boundary.</p>
<p><a href="https://github.com/google/leveldb/blob/863f185970eff21e826e5fe1164a6215a515c23b/db/dbformat.h#L183">https://github.com/google/leveldb/blob/863f185970eff21e826e5fe1164a6215a515c23b/db/dbformat.h#L183</a></p>
<pre><code class="language-cpp">// A helper class useful for DBImpl::Get()  
class LookupKey {  
 public:  
  // Initialize *this for looking up user_key at a snapshot with  
  // the specified sequence number.  
  LookupKey(const Slice&amp; user_key, SequenceNumber sequence);  
  
  LookupKey(const LookupKey&amp;) = delete;  
  LookupKey&amp; operator=(const LookupKey&amp;) = delete;  
  
  ~LookupKey();  
  
  // Return a key suitable for lookup in a MemTable.  
  Slice memtable_key() const { return Slice(start_, end_ - start_); }  
  
  // Return an internal key (suitable for passing to an internal iterator)  
  Slice internal_key() const { return Slice(kstart_, end_ - kstart_); }  
  
  // Return the user key  
  Slice user_key() const { return Slice(kstart_, end_ - kstart_ - 8); }  
  
 private:  
  // We construct a char array of the form:  
  //    klength  varint32               &lt;-- start_  
  //    userkey  char[klength]          &lt;-- kstart_  
  //    tag      uint64  
  //                                    &lt;-- end_  
  // The array is a suitable MemTable key.  
  // The suffix starting with &quot;userkey&quot; can be used as an InternalKey.  
  const char* start_;  
  const char* kstart_;  
  const char* end_;  
  char space_[200];  // Avoid allocation for short keys  
};
</code></pre>
<p>Looking at the constructor gives us a little more of a hint as to what's going on, and how exactly the sequence number is encoded.</p>
<pre><code class="language-cpp">LookupKey::LookupKey(const Slice&amp; user_key, SequenceNumber s) {
  size_t usize = user_key.size();
  size_t needed = usize + 13;  // A conservative estimate
  char* dst;
  if (needed &lt;= sizeof(space_)) {
    dst = space_;
  } else {
    dst = new char[needed];
  }
  start_ = dst;
  dst = EncodeVarint32(dst, usize + 8);
  kstart_ = dst;
  std::memcpy(dst, user_key.data(), usize);
  dst += usize;
  EncodeFixed64(dst, PackSequenceAndType(s, kValueTypeForSeek));
  dst += 8;
  end_ = dst;
}
</code></pre>
<p>Over here we see that when LevelDB is deciding whether or not it needs to allocate more space for the key to be stored overall, they compute <code>size_t needed = usize + 13</code> - possibly accounting 5 bytes for the <code>Varint32</code> and 8 bytes for the <code>Fixed64</code> that the key size and the sequence number are stored as, respectively, but I was confused as to where the extra 1 is coming from (i.e. why we're using 5 bytes to store a 4-byte integer). Looking at other functions for <code>Varint32</code>, we see that even in <code>PutVarint32()</code> the authors use 5 bytes as a default storage length, confirming that this isn't an approximation of some sort. Let's see why this number is 5.</p>
<pre><code class="language-cpp">void PutVarint32(std::string* dst, uint32_t v) {  
  char buf[5];  
  char* ptr = EncodeVarint32(buf, v);  
  dst-&gt;append(buf, ptr - buf);  
}
</code></pre>
<p>A small note on <code>varint</code>s later - <a href="https://samliu.github.io/2016/10/15/varints.html">https://samliu.github.io/2016/10/15/varints.html</a> I see that one bit in every byte, the most significant bit is used to indicate whether or not there's another byte coming. While this is a part of the encoding format, this is not the number itself, so the space it takes doesn't count to the value of the number. The contract that the <code>Varint32</code> maintains is that you can store a 32 bit unsigned integer i.e. 0 to  2^32 - 1 . The effective capacity of 4 bytes of <code>Varint32</code> is - considering only the bits available for encoding - 7 + 7 + 7 + 7 = 28, which effectively lets us store 0 to 2^28 - 1, which prevents us from storing numbers in the range 2^28 to 2^32 - 1. This is why we need the extra byte, illustrating that there's no such thing as a free lunch - adopting varints as the sole representation sometimes means that you need to use 5 bytes to store a 4-bit unsigned integer :D</p>
<p>My personal preference is to minimize the use of magic numbers as much as possible - for someone who's reading the code at a glance, it's much harder to understand <em>why</em> there's an 8 or a 13 in the places that it exists. I would create an <code>inline const</code> variable with a comment on top of it explaining why it exists.</p>
<p>Another interesting thing to note is that the encoded length of the key has 8 added to it. I couldn't figure out why this happens.</p>
<p>Now what we know how the encoding works, we can safely summarize that the <code>internal_key_</code> is simply the key the user passed in for the lookup, with the sequence number appended to it.</p>
]]></content:encoded></item><item><title>The Mechanix of Software Engineering</title><link>https://rowjee.com/blog/musings/mechanix.html</link><description><![CDATA[Connecting the dots (or rather, the parts)]]></description><author>null</author><pubDate>Thu, 23 May 2024 09:00:00 +0000</pubDate><content:encoded><![CDATA[<p>Someone said that</p>
<blockquote>
<p>&quot;if you want to find your purpose in life, think of what you did as a child
that made the hours feel like seconds&quot;</p>
</blockquote>
<p>and that got me thinking; what made me feel like that as a child?</p>
<p>What comes to mind immediately is Mechanix. Mechanix was a &quot;build it yourself&quot;
toolbox, which gave you parts, and an instruction manual that showed you how to
use those parts to build, step by step, a lot of things: a car, a bike, a
helicopter, etc.</p>
<p>What I'm now thinking about is: though I really like software engineering, I'd
love to bring about this feeling, the sheer joy of play, into my field. How do I
do that? Some particular things stand out to me.</p>
<ol>
<li>
<p>Having a clear end goal of what the finished product looks like: this gave an
immediate justification to why I was doing what I was doing</p>
</li>
<li>
<p>Knowing what the parts are and how the parts fit in with each other: this was
really important because once you had decided to build a smaller
sub-component of the project, you had to know what parts to use, and you
would have to know how the smallest most atomic parts would be combined.
Where would the screws go? Could you position it in that particular way? What
about the other way?</p>
</li>
<li>
<p>Using your tools well: almost every join required that you use a screw and a
bolt to hold the parts together. Though it would have sufficed to know how to
just &quot;make it happen&quot;, for some joins you had to hold the spanner in one
particular way, for the other you needed to know which hand to use for the
screwdriver and which hand for the spanner for the easiest way to put that
screw in place without messing things up</p>
</li>
<li>
<p>Being able to test the joins: once you put a screw and bolt in place, it was
super important to jiggle the parts a little to see if the join was done
correctly. i remember this being an important part of making sure that the
thing you build wouldn't just fall apart.</p>
</li>
<li>
<p>The joy of seeing it finally working: once it was done, being able to see all
the parts you put in place work in harmony to make the final build work was
awesome. Building it and showing it off was a viscerally happy part of the
entire process.</p>
</li>
<li>
<p>Cleanup: working on a sheet on the floor or on a table meant that I was able
to easily spot screws that I've dropped and could easily find the stuff I put
down.</p>
</li>
</ol>
]]></content:encoded></item><item><title>Saaru - Rust India May 2023 Meetup</title><link>https://rowjee.com/blog/saaru.html</link><description><![CDATA[this is about Saaru, a Static Site Generator]]></description><author>null</author><pubDate>Sat, 27 May 2023 09:00:00 +0000</pubDate><content:encoded><![CDATA[<h2><code>$ whoami</code></h2>
<p>Hello, I'm Anirudh!</p>
<ul>
<li>Backend and Distributed Systems Dev</li>
<li>Databases Enthusiast (I call feb 14th WALentine's day)</li>
<li>3.99999th Year CSE @ PES</li>
<li>Lab Head, PES Innovation Lab</li>
<li>Rust, Golang</li>
<li>accidental poet</li>
</ul>
<h2>What is Saaru?</h2>
<ul>
<li>
<p>Saaru is a Static Site Generator -&gt; In simple words, it takes in Markdown files, a bunch of templates, and spits out a whole website.</p>
</li>
<li>
<p>Why is it called Saaru?</p>
<ul>
<li>Saaru is the Kannada word for rasam and I love rasam</li>
</ul>
</li>
</ul>
<h3>Architecture</h3>
<pre><code class="language-text">                         STATIC SITE GENERATOR INTERNALS

      ┌────────────────┐
      │ Markdown Files │--------\
      └────────────────┘        |
                                v
      ┌────────────────┐     ┌─────────────┐    ┌──────────────────┐
      │ Template Rules │----&gt;│ Static Site │---&gt;│ Full Built HTML  │
      └────────────────┘     │ Generator   │    │ and CSS Website  │
                             └─────────────┘    └──────────────────┘
      ┌────────────────┐        ^
      │ Additional     │--------|
      │ Metadata       │
      └────────────────┘

</code></pre>
<ul>
<li>
<p>The content lives as <code>.md</code> files enriched with <em>frontmatter</em>, which is YAML, TOML or JSON that you can add at the top of a markdown file.</p>
</li>
<li>
<p>You combine these files with a template, which can literally be a string with special blocks to substitute text in, or a more specialized dialect of templating like Jinja, Nunjucks, etc</p>
</li>
<li>
<p>Saaru is built on combining a Markdown Engine, a Templating Engine, and Content Injection</p>
</li>
</ul>
<pre><code class="language-text">

                      MARKDOWN ENGINE

         ┌────────┐      ┌────────┐        ┌─────────────┐
         │.md file│-----&gt;│Markdown│-------&gt;│compiled HTML│
         └────────┘      │Engine  │        │output       │
                         └────────┘        └─────────────┘

</code></pre>
<ul>
<li>Saaru is a static site generator in Rust
<ul>
<li>(show docs folder) Give it a folder with Markdown Content and Some Templates, Saaru will render a website (show <code>build</code> folder)</li>
<li>Deep Data Merge: You can have content show up in indices like tags and collections, so that you can reference them in other parts of the site</li>
</ul>
</li>
</ul>
<ul>
<li>
<p>Switches from a one-pass traversal of the dir tree to a two-pass approach (or does it? :sus:)</p>
<ul>
<li>The frontmatter</li>
<li>Controls the title, a bunch of other stuff - here's what the frontmatter looks like</li>
</ul>
</li>
<li>
<p>Frontmatter is metadata you can control. Here's what this looks like -&gt; Each <code>.md</code> file has this, and this can control how it gets rendered.</p>
</li>
</ul>
<pre><code class="language-rust">#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct FrontMatter {
	pub title: Option&lt;String&gt;,
	pub description: Option&lt;String&gt;,
	pub date: Option&lt;String&gt;,
	pub tags: Option&lt;Vec&lt;String&gt;&gt;,
	pub collections: Option&lt;Vec&lt;String&gt;&gt;,
	pub wip: Option&lt;bool&gt;,
	pub template: Option&lt;String&gt;,
	pub link: Option&lt;String&gt;,
	pub meta: Option&lt;Value&gt;,
}
</code></pre>
<h2>Live Demo (with my website)</h2>
<ul>
<li>Here's a working Saaru website. See the structure, and the frontmatter.</li>
</ul>
<h2>commit walkthrough from the bottom up</h2>
<ul>
<li>Frontmatter struct represents a file, content, tags and all</li>
</ul>
<h3>talk about how initial R&amp;D Happened</h3>
<ul>
<li>
<p>Genesis commit - (<a href="https://github.com/anirudhRowjee/saaru/commit/974009349027a4f14a69959b8a1e87c74dcad0b4">https://github.com/anirudhRowjee/saaru/commit/974009349027a4f14a69959b8a1e87c74dcad0b4</a>)</p>
<ul>
<li>Goal: Render a single file of markdown using a template engine</li>
<li>was still using <code>pulldown-cmark</code> at that time</li>
<li>no focus on dynamic naming</li>
<li><code>include_str!()</code> to read the file!</li>
</ul>
</li>
<li>
<p>Attempting to remove the frontmatter myself</p>
<ul>
<li>learnt about <code>.map()</code> and <code>.fold()</code> used together (thanks Kiran!)</li>
<li>Added a minimal test suite</li>
<li>Led me to think &quot;damn, i really need to reorganize this codebase&quot; (<a href="https://github.com/anirudhRowjee/saaru/commit/bd03832a571c77cd32a2ec14627f94c42a75141f">https://github.com/anirudhRowjee/saaru/commit/bd03832a571c77cd32a2ec14627f94c42a75141f</a>)</li>
</ul>
</li>
<li>
<p>Load the template name dynamically based on frontmatter</p>
<ul>
<li>Learnt more about minijina environments and how they can pick up templates automatically based on directory</li>
<li>coming up with ONE function to render a single markdown file based on its templates</li>
</ul>
</li>
<li>
<p>How easy it was to move from the system-native directory walker (<code>fs::read-dir</code> to the <code>WalkDir::new</code> walker (which is recursive)</p>
<ul>
<li>Most satisfying refactor of my life -&gt; single-line change and required <em>nothing</em> else to be touched</li>
<li><a href="https://github.com/anirudhRowjee/saaru/commit/c1553cad881013eef1d8aa2151f9488265ffaec4">https://github.com/anirudhRowjee/saaru/commit/c1553cad881013eef1d8aa2151f9488265ffaec4</a></li>
</ul>
</li>
<li>
<p>Early Deep Data Merge (<a href="https://github.com/anirudhRowjee/saaru/commit/b2185239455ec3386ba2f503409281a0ad066f69">https://github.com/anirudhRowjee/saaru/commit/b2185239455ec3386ba2f503409281a0ad066f69</a>)</p>
<ul>
<li>Conversion from a one-pass operation to a two-pass operation (introducing <code>alternate_render_pipeline()</code>)</li>
<li>DO NOT WALK THE FS TWICE -&gt; INDIRECT TO <code>tag_map</code>, <code>collection_map</code>, <code>frontmatter_map</code></li>
<li>Read the FS into these maps, and iterate over <code>frontmatter_map</code> to render</li>
<li>These maps can pull from <code>tag_map</code> and <code>collection_map</code> to render more content based on tags and collections</li>
<li>Added proper structured logging here</li>
</ul>
</li>
<li>
<p>Deep Data merge (<a href="https://github.com/anirudhRowjee/saaru/commit/badcac9b06f44731c3a8cac5abf00cdf21914f10">https://github.com/anirudhRowjee/saaru/commit/badcac9b06f44731c3a8cac5abf00cdf21914f10</a>)</p>
<ul>
<li>More refactoring! refactor all the things!</li>
<li>moved most stuff from <code>src/main.rs</code> to <code>src/saaru.rs</code></li>
<li>premanently switched to two-pass render pipeline (<code>instance.recursively_render_from_directory()</code> -&gt; <code>instance.alternate_render_pipeline()</code>)</li>
<li>Load data into the <code>tag_map</code>, pass it to template rendering context</li>
<li>move to <code>thin_augmented_frontmatter</code> -&gt; frontmatter without the raw markdown content</li>
<li>Lastly added collections (alternate ontology to tags) and a system-generated default tags page</li>
</ul>
</li>
<li>
<p>markdown engine switch</p>
<ul>
<li>Added Command-line Arguments (<a href="https://github.com/anirudhRowjee/saaru/commit/feab2d7f2224bcd920ef95d1bd774c5c18b01cf6">https://github.com/anirudhRowjee/saaru/commit/feab2d7f2224bcd920ef95d1bd774c5c18b01cf6</a>)</li>
</ul>
</li>
<li>
<p>Switched from <code>pulldown-cmark</code> to <code>comrak</code> (<a href="https://github.com/anirudhRowjee/saaru/commit/2d60a103e72e6e38d5460ba77ef1aeca212a9c1f">https://github.com/anirudhRowjee/saaru/commit/2d60a103e72e6e38d5460ba77ef1aeca212a9c1f</a>)</p>
<ul>
<li>Why?
<ul>
<li>more features -&gt; Footnotes, etc</li>
<li>Better compatibility with common requests to deviate from GFM Spec</li>
<li>more configurability and much simpler API, although less suitable for user-injected AST-level customizations</li>
<li>Automatically ignores frontmatter!</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3>adding other features</h3>
<ul>
<li>Static folder copying for CSS, etc (<code>src/utils.rs</code> -&gt; <code>pub fn copy_recursively</code>, shamelessly stolen from the internet)
<ul>
<li>Basic JSON content injection</li>
<li>Skipping non-markdown files in the parse tree</li>
<li>Yet Another Refactor!</li>
</ul>
</li>
</ul>
<h3>live reload</h3>
<ul>
<li>
<p>Added basic live reload with <code>notify</code> -&gt; loop {poll_fs, if file changes, rerender that file in-place with <code>instance.render_individual_file(filepath)</code>} (<a href="https://github.com/anirudhRowjee/saaru/commit/c612455d70d911897742456dd0951c8b91dbcb1c">https://github.com/anirudhRowjee/saaru/commit/c612455d70d911897742456dd0951c8b91dbcb1c</a>)</p>
<ul>
<li>had to manually refresh browser and external web server!</li>
<li>Initially architectured it as a separate module that consumed the <code>saaru</code> instance</li>
<li>Re-architected when adding a live server (<a href="https://github.com/anirudhRowjee/saaru/commit/b5f96fff134ab1db656872de8e19a7acb196becb">https://github.com/anirudhRowjee/saaru/commit/b5f96fff134ab1db656872de8e19a7acb196becb</a>)</li>
<li>New Fancy Diagram!</li>
<li>crossbeam MPMC channels, watching for fs change, waiting to notify Saaru Instance as well as web server (for live reload) -&gt; <code>notify</code>, <code>tower</code>, <code>tower-http-livereload</code></li>
</ul>
</li>
<li>
<p>2 lightweight threads (watcher thread, message passing thread)</p>
<ul>
<li>Hid these configurations behind command line flags</li>
<li>Specific actions for different types of files</li>
<li><code>.md</code> -&gt; Optimized re-render of single file only</li>
<li><code>.css</code> -&gt; separate watcher thread for static files, copy that specific file over into the build folder if there's a change</li>
</ul>
</li>
</ul>
<h2>future plans</h2>
<ul>
<li>DAG solver -&gt; more fine-grained re-rendering to check what templates etc need to be re-loaded</li>
<li>maybe add template livereload live while i have time</li>
</ul>
]]></content:encoded></item><item><title>Sketch your side projects</title><link>https://rowjee.com/blog/sketching.html</link><description><![CDATA[A cautionary tale warning against common pitfalls while building side projects]]></description><author>null</author><pubDate>Sun, 13 Oct 2024 09:00:00 +0000</pubDate><content:encoded><![CDATA[<blockquote>
<p><em>Start now. Optimize Later. Imperfect starts can always be improved. Obsessing over a perfect plan will never take you anywhere on its own.</em><br />
~ <a href="https://jamesclear.com/quotes/start-now-optimize-later-imperfect-starts-can-always-be-improved-obsessing-over-a-perfect-plan-will-never-take-you-anywhere-on-its-own">James Clear</a></p>
</blockquote>
<p>I'm currently experimenting with a new way of working on side projects, where I give myself a month to work on it, and write a blog post about it, holding myself publicly accountable via twitter.</p>
<p>This experience has made me realise just how untenably high my standards are - and while this has its positives, the negative is that it often tends to stifle the act and art of building with concerns that are largely useless for learning-oriented projects.</p>
<p>For example, if I'm <a href="https://rowjee.com/blog/skiplists">implementing a skiplist</a> - coming from the perspective of someone who doesn't know what a skiplist is - it's much more important that I understand the core algorithms and data structures of the skiplist, than it is to carve out the perfect abstractions and object-oriented design for the nodes and the skiplist themselves.</p>
<p>Case in point - I ultimately decided to not have any private data members in the classes, and chose to leave out some bookkeeping, and didn't handle all my errors - but I can confidently say that this is the reason I now have a working project, and was able to learn how a skiplist works.</p>
<p>To do this is to sketch, like artists do. We can aggressively whittle down the idea to its very core functionality, and implement that - nothing more, nothing less. This lets us establish a foundation, a feedback loop, upon which we can then &quot;paint in the finer details&quot; - refactor to meet our standards - in an iterative fashion.</p>
<p>In doing so, we choose a project that is imperfect but exists today, as opposed to a project that does not exist yet, but is eventually consistent with some ideal of perfection.</p>
<blockquote>
<p><em>... Kids just plunge in and build their treehouse without worrying about whether they're wasting their time, or how it compares to other treehouses. And frankly we could learn a lot from kids here. The high standards most grownups have for &quot;real&quot; work do not always serve us well.</em><br />
<em>... The most important phase in a project of one's own is at the beginning: when you go from thinking it might be cool to do x to actually doing x. And at that point high standards are not merely useless but positively harmful.</em><br />
~ <a href="https://www.paulgraham.com/own.html">A Project of One's Own - Paul Graham</a></p>
</blockquote>
<p>At this scale, It is much easier and cheaper to go from imperfect to perfect than it is to go from non-existent to perfect. <strong>Projects that do not exist cannot be improved or learnt from.</strong></p>
]]></content:encoded></item><item><title>How do LSM Trees work?</title><link>https://rowjee.com/blog/lsmtrees.html</link><description><![CDATA[Let's understand more about LSM Trees.]]></description><author>null</author><pubDate>Sat, 9 Aug 2025 09:00:00 +0000</pubDate><content:encoded><![CDATA[<p>(this post originated in <a href="/the_replicated_log/0002.html">The second edition of my newsletter</a>)</p>
<p>The goal of this post is to walk everyone through how an LSM Tree works. I'll do my best to not wander off into philosophical questions and trying to reason about &quot;why&quot; it is the way it is, and focus more on &quot;so what&quot;. I promise that i'll get to the rambling at some point though :D</p>
<h2>Table of Contents</h2>
<ol>
<li>What is an LSM Tree?</li>
<li>Immutability as an operating principle</li>
<li>The LSM Tree Architecture</li>
<li>The Write Path</li>
<li>The Read Path</li>
<li>Formalizing our understanding of performance</li>
<li>Why is too many files bad, and how do we fix it?</li>
<li>Compaction</li>
</ol>
<h2>What is an LSM Tree?</h2>
<p>An LSM Tree is a disk-based data structure that's used to store key-value pairs on disk. A disk-backed KV (Key-Value) Store is a foundational building block for many applications, so doing this well is important. The main focus of a key-value store is persistence, so it's obvious that the disk, and files, are involved somewhere - we're going to see how and where.</p>
<p>Simply put, the job of the LSM Tree (and most other storage structures) is to obey the following interface, while ensuring that the data you've added or modified stays as it is between crashes and restarts.</p>
<pre><code>LSMTree {
	fn Upsert(String key, String value)
	fn Delete(String key)
	fn Get(String key) -&gt; Option&lt;String&gt;
}
</code></pre>
<p>That's the functional requirement. Non-functional requirements are varied and many, including, but not limited to:</p>
<ol>
<li>Take as less disk space as possible</li>
<li>Make the reads fast</li>
<li>Make the writes fast</li>
<li>Make everything fast (Jury's out on whether or not this is a functional requirement, but for the sake of this post, let's assume it isn't)</li>
</ol>
<p>Compared to other, more conventional data structures that also do the same thing (for example, B-Trees), the LSM Tree really shines when used with write-heavy workloads. An important distinction is that LSM Trees do not update data in-place - <strong>data, once written to disk, is guaranteed to be immutable</strong>, and will only ever be rewritten into a new file at some point. This lets LSM Trees have much cheaper writes than other data structures that perform in-place modifications to existing data files.</p>
<p>Let's first explore what out-of-place updates are, and then we'll investigate how the core data structure actually works.</p>
<h2>An Immutable Database? What the hell is that?</h2>
<p>The fundamental question remains - <strong>how do you overwrite or delete data if the on-disk data structure is immutable</strong>? The simplest answer is - to create another on-disk data structure that represents this, and ensure you read all data on disk in the order of newest to oldest.</p>
<p>To motivate this idea, let's take an example of a simple JSON file (<code>data.1</code>) where we have three key-value pairs. The filename is important as it helps us gain an understanding of the order in which the files were written.</p>
<pre><code class="language-json">// data.1
{
	&quot;backstreet boys&quot;: &quot;show me the meaning of being lonely&quot;,
	&quot;metallica&quot;: &quot;one&quot;,
	&quot;charanjit singh&quot;: &quot;kalavati&quot;,
}
</code></pre>
<p>Let us say that we wish update the value of the key <code>&quot;metallica&quot;</code> to <code>&quot;for whom the bell tolls&quot;</code> under the specific constraint that we cannot modify the file itself which has the key stored in it.</p>
<p>To show that data has been modified, if I can guarantee that <code>data.(x+1)</code> is always read before <code>data.(x)</code> where <code>x + 1 &gt; x</code>, then I can &quot;modify&quot; the data present by writing <code>data.(x+1)</code> as follows:</p>
<pre><code class="language-json">// data.2
{
	&quot;metallica&quot;: &quot;for whom the bell tolls&quot;
}
</code></pre>
<p>Consider our read algorithm to be as follows, essentially enforcing the principles we mentioned earlier:</p>
<pre><code class="language-python">read(key):
	for file in files.sort(newest_to_oldest):
		for kvpair in file:
			if kvpair.key == key:
				return (true, kvpair.value)
	return (false, &quot;&quot;)
</code></pre>
<p>In this way, we can guarantee that our reader will <em>always</em> first see the latest value of the key, hence performing a &quot;modification&quot;.</p>
<p>Now, you might ask - this works for overwrites; but how does one delete a record? The way we do this is that we frame our deletes as overwrites. If I wish to delete the key <code>&quot;backstreet boys&quot;</code>, all I need to do is create a new data file:</p>
<pre><code class="language-json">// data.3
{
	&quot;backstreet boys&quot;: DELETED
}
</code></pre>
<p>This is known as a &quot;tombstone&quot;.  Our reader is then modified to be as follows:</p>
<pre><code class="language-python">read(key):
	for file in files.sort(newest_to_oldest):
		for kvpair in file:
			if kvpair.key == key:
				if kvpair.value == TOMBSTONE:
					return (false, value)
				return (true, value)
	return (false, &quot;&quot;)
</code></pre>
<p>Yay! We now have an immutable database. Now, you may be wondering: if all we ever do is write new files, even if data is deleted, how do we avoid making the disk full? Hold on to that question, and let's explore how an LSM Tree actually works.</p>
<h2>What makes an LSM Tree?</h2>
<p><img src="/static/images/lsmtree/lsm101_highlevelarch.png" alt="" /></p>
<p>A <strong>record</strong> is a pair of strings/bytes, usually both key and value. We can consider a tombstone/deletion marker to be some type of string/byte stream as well, recognized throughout the LSM Tree. We can consider the LSM Tree to be a collection of records, such that you perform CRUD operations via the key. Hence, the atomic unit of data in this LSM Tree is the record.</p>
<p>The LSM Tree usually has three components.</p>
<p><strong>Memtable</strong>: This is the in-memory component of the LSM Tree. It's usually a fixed size, and all records end up here first, ensuring faster writes. It's usually a hashmap-type data structure that lets us access data via a key-value interface.</p>
<p><strong>Write-Ahead Log (WAL)</strong>: this disk-based component ensures that we don't encounter data loss if some records are only in the memtable and haven't made it to disk yet. All records are written to the WAL first, and then to the memtable, to ensure that the records have been persisted to disk - this ensures that we're able to recover memtable data if we crash without writing the memtable to disk. We won't be exploring this too much in this post, but it's important to know why it exists.</p>
<p><strong>SSTable files</strong>: This is the largest component of the LSM Tree. When a memtable is full, it is &quot;flushed&quot; onto disk, to create one SSTable file. Each file contains a set of records stored in a sorted manner.</p>
<p>this isn't a component, but an important concept nonetheless - <strong>Levels</strong>: LSM Trees usually store these SSTable files in levels. There are usually five levels, and each level has a cap on the maximum size of an SSTable that can be present inside the levels. There's often a strategy to how we organize the levels themselves, and what heuristics let us keep data in one level vs move it to the other; For now, we can assume that the higher levels have smaller files, fresher data, and that the lower levels have larger files with older, staler data.</p>
<h2>Writing some data to an LSM Tree</h2>
<p><img src="/static/images/lsmtree/lsm101_writepath.png" alt="" /></p>
<pre><code>write(key, value)
	write to WAL
	write to memtable
	if memtable is full, write it to disk as an SSTable
</code></pre>
<p>Given that the memtable is in-memory, but we still need to offer durability as a guarantee, we will make use of a write-ahead log where all data is persisted to disk by default, and only once it persists in the WAL will we write it to the memtable. This ensures that if the machine crashes after the write completes to the memtable, but isn't present on disk, we do not lose our data.</p>
<h2>Flushing Data to disk</h2>
<p><img src="/static/images/lsmtree/lsm101_flushpath.png" alt="" /></p>
<p>We know that we don't have infinite memory available! Once the memtable goes beyond a defined capacity (either in terms of number of records or in terms of size), the memtable is <strong>flushed</strong> to disk, as an SSTable. This frees up the memtable to accept new writes, and ensures that our data is reliably persisted to storage.</p>
<p>Most implementations will usually have a second immutable memtable, which is present as a standby; this is because flushing is an operation that involves a disk IO, which, in computer time, can be pretty slow. If we block the memtable from accepting any writes during the flush, it'll be a performance penalty that we don't really want to pay - hence, flushing allows us to swap the active memtable out, replace it with an empty one (thus making the full memtable immutable), and operate on the immutable memtable.</p>
<p>For now, let's assume that we do nothing apart from flushing our memtable into a new SSTable. This means that every time we fill our memetable's memory quota, we will create a new file on disk. This also means that our disk size will grow in a rate proportional to the write rate.</p>
<p>All memtables are flushed into Level 0 of the LSM Tree, without further organization.</p>
<h2>Reading some data from an LSM Tree</h2>
<p><img src="/static/images/lsmtree/lsm101_readpath.png" alt="" /></p>
<pre><code>search_table(table, key)
	for record in table:
		if key == record.key:
			return found
	return not found

read(key):
	read the memtable
	if found, return
	for level in levels:
		for table in level.sort_by_recent():
			search_table(table, key)
	return &quot;not found&quot;
</code></pre>
<p>Let us first understand that to read some data, we need to know where it is. This is the majority of the problem; as data structure designers, we need to ensure that all keys are found (or if not found, confirmed as such) as quickly as possible. Once we know where the data is, reading the disk block is a relatively fixed-time operation.</p>
<p>To find a key, we first search both memtables. If the key was written fairly recently, we maximize our chances of a very cheap read, as we perform no disk IO for this read.</p>
<p>This approach does introduce some problems. Since we don't really maintain any other index, for every key we search, we need to go through every SSTable file. Relatively speaking, opening a file and reading from it is an expensive operation; We would ideally like to minimize this as much as possible. So we need to figure out some way to make reads cheaper, and to avoid opening a large number of files for each read.</p>
<p>To do this, an invariant we maintain is that <strong>all levels of the LSM Tree (apart from Level 0) maintain SSTables that comprise only of non-overlapping key ranges</strong>. This means that when we look at a particular level, a key can belong <strong>only to one SSTable in that level</strong> - this is to ensure that we have at most one file to search through in each level of the tree.</p>
<p>Once we do this, our algorithm looks something like this:</p>
<pre><code class="language-python">search_table(table, key)
	for record in table:
		if key == record.key:
			return found
	return not found

read(key):
	read the memtable
	if found, return
	for level in levels:
		if level == level0:
			for table in level.sort_by_recent():
				search_table(table, key)
		else:
			table = find_table() # there can only be one
			search_table(table, key)
	return &quot;not found&quot;
</code></pre>
<h2>Formalizing our understanding of performance</h2>
<p>let's look at two new terms that'll help us understand how to reason about performance in storage systems. We can use them as a sort of Big-O notation to reason about which storage algorithms are efficient, and which aren't.</p>
<p><strong>read amplification</strong>: the ratio of bytes read to read a value, to the size of the value<br />
<strong>write amplification</strong>: the ratio of bytes written to write a value, to the size of the value<br />
<strong>space amplification</strong>: &quot;Space-amp is the ratio of the size of the database to the size of the data in the database&quot;<sup class="footnote-ref"><a href="#fn1" id="fnref1">1</a></sup></p>
<p>In a B-Tree, <strong>write amplification</strong> is high, because every write causes you to potentially split the parent node pages into new ones, which you will also have to write to disk. Conversely, the read amplification is pretty low, because you only need to read log_2(branch_factor) disk pages due to the nature of the B-Tree.</p>
<p>Let's go back to our case of the LSM Tree so far; we don't do anything to the SSTable files once they're written as a part of the flush process. This means that if we have a key that we want to look up, we're going to have to basically read through most, if not all, of the files present - this causes <strong>massive</strong> read amplification. This is also harmful from a performance point of view, because what could have been 1 I/O syscall with an in-memory index, is now a potentially unbounded number.</p>
<p>Given that the LSM Tree <strong>we know in this article so far</strong>, is optimized for low write amplification - a near perfect factor of 1, because we write all data to a new file regardless - we now need to figure out how to reduce the read amplification of the LSM Tree to make sure it's <em>reasonably</em> performant.</p>
<h2>Why is too many files bad, and how can we fix it?</h2>
<p>As of right now, we already know that we have a pretty fast write path; data hits memory first and stays in memory until the memtable is full, beyond which point we need to flush the data to disk. Measured by I/O, these writes are very, very cheap (1 I/O), but in the grand scheme of things, not <strong>free</strong> - we will now incur a cost that we need to pay on every read.</p>
<p>The situation we are at so far is as follows:</p>
<ol>
<li>If we need to read the disk, we need to read all SSTables as they all have overlapping ranges, so we can't really say which specific table has data. It could be present in multiple tables. <strong>This will incur high read amplification</strong>.</li>
<li>Our SSTables will each be as small as the memtable size quota, which means that we'll need to be doing a large number of random disk IOs to read all the SSTables. This is the exact problem that manifests in B-Trees that we are trying to avoid (Modern day SSDs handle this much better, but we'll get to that in another post).</li>
<li>We are never reclaiming data from the disk; no matter how many times a record is overwritten or deleted, the original record still exists on disk. This is wasteful.</li>
</ol>
<p>Given the problems, we need to implement some kind of solution that</p>
<ol>
<li>Keeps the disk size from growing at an unbounded rate</li>
<li>Limits the number of small files present by grouping more data on disk together, allowing for a larger file to be created and accessed with fewer IOs</li>
<li>Maintains the invariant of non-overlapping ranges</li>
</ol>
<p>Let's go back to the previous example we have.</p>
<pre><code class="language-json">// data.1
{
	&quot;backstreet boys&quot;: &quot;show me the meaning of being lonely&quot;,
	&quot;metallica&quot;: &quot;one&quot;,
	&quot;charanjit singh&quot;: &quot;kalavati&quot;,
}
// data.2
{
	&quot;metallica&quot;: &quot;for whom the bell tolls&quot;
}
// data.3
{
	&quot;backstreet boys&quot;: DELETED
}
// let's add some more music here
// data.4
{
	&quot;moblack&quot;: &quot;yamore&quot;,
	&quot;AC/DC&quot;: &quot;thunderstruck&quot;,
	&quot;gorillaz&quot;: &quot;feel good inc&quot;,
}
// data.5
{
	&quot;charanjit singh&quot;: &quot;raag bhairav&quot;,
	&quot;death cab for cutie&quot;: &quot;i'll follow you into the dark&quot;,
	&quot;daft punk&quot;: &quot;instant crush&quot;
}
</code></pre>
<p>We can clearly see that we're overwriting some data - remember, we need only keep the latest value - and we're deleting some data too.</p>
<p>What if we were able to pre-compute the outcome of a read (per the algorithm we described) for this set of files? If we can ensure that the read function works the same on two different representations of data, while one takes lesser space, and is correct (in that we get the same values we stored per key, for every key), would it not be better to perform said computation in the background?</p>
<p>By doing this, we have solved a bunch of the problems we have, by:</p>
<ol>
<li>Converting many small files into one large file</li>
<li>Removing overwritten/deleted data</li>
<li>Can also move the data into clear non-overlapping ranges</li>
</ol>
<p>This is the process known as compaction, which is a background task that works to maintain the disk space used by frequently merging SSTable files such that disk usage is kept at a reasonable level, while lookup latency is also managed by ensuring that we don't have too many small files to open.</p>
<h2>Compaction: Merging multiple SSTable files into one</h2>
<p>So, let's start with the following motivation:</p>
<blockquote>
<p>What do I need to do to maintain the same data, but with lesser space?</p>
</blockquote>
<pre><code class="language-json">// data.1
{
	&quot;backstreet boys&quot;: &quot;show me the meaning of being lonely&quot;,
	&quot;metallica&quot;: &quot;one&quot;,
	&quot;charanjit singh&quot;: &quot;kalavati&quot;,
}
// data.2
{
	&quot;metallica&quot;: &quot;for whom the bell tolls&quot;
}
// data.3
{
	&quot;backstreet boys&quot;: DELETED
}
// let's add some more music here
// data.4
{
	&quot;moblack&quot;: &quot;yamore&quot;,
	&quot;AC/DC&quot;: &quot;thunderstruck&quot;,
	&quot;gorillaz&quot;: &quot;feel good inc&quot;,
}
// data.5
{
	&quot;charanjit singh&quot;: &quot;raag bhairav&quot;,
	&quot;death cab for cutie&quot;: &quot;i'll follow you into the dark&quot;,
	&quot;daft punk&quot;: &quot;instant crush&quot;
}
</code></pre>
<p>A good place to start is to know that we need to maintain key ordering even in the final table, so let's start by ordering all the data (across files) by key; Since the order of writing (AKA the SSTable ID) is important, we'll keep that around, too.</p>
<pre><code class="language-json">[
	{&quot;tableID&quot;: 1, &quot;data&quot;: {&quot;backstreet boys&quot;: &quot;show me the meaning of being lonely&quot;}},
	{&quot;tableID&quot;: 1, &quot;data&quot;: {&quot;metallica&quot;: &quot;one&quot;}},
	{&quot;tableID&quot;: 1, &quot;data&quot;: {&quot;charanjit singh&quot;: &quot;kalavati&quot;}},
	{&quot;tableID&quot;: 2, &quot;data&quot;: {&quot;metallica&quot;: &quot;for whom the bell tolls&quot;}},
	{&quot;tableID&quot;: 3, &quot;data&quot;: {&quot;backstreet boys&quot;: DELETED}},
	{&quot;tableID&quot;: 4, &quot;data&quot;: {&quot;moblack&quot;: &quot;yamore&quot;}},
	{&quot;tableID&quot;: 4, &quot;data&quot;: {&quot;AC/DC&quot;: &quot;thunderstruck&quot;}},
	{&quot;tableID&quot;: 4, &quot;data&quot;: {&quot;gorillaz&quot;: &quot;feel good inc&quot;}},
	{&quot;tableID&quot;: 5, &quot;data&quot;: {&quot;charanjit singh&quot;: &quot;raag bhairav&quot;}},
	{&quot;tableID&quot;: 5, &quot;data&quot;: {&quot;death cab for cutie&quot;: &quot;i'll follow you into the dark&quot;}},
	{&quot;tableID&quot;: 5, &quot;data&quot;: {&quot;daft punk&quot;: &quot;instant crush&quot;}}
]
</code></pre>
<p>First ordering by key:</p>
<pre><code class="language-json">[
	{&quot;tableID&quot;: 4, &quot;data&quot;: {&quot;AC/DC&quot;: &quot;thunderstruck&quot;}},
	{&quot;tableID&quot;: 1, &quot;data&quot;: {&quot;backstreet boys&quot;: &quot;show me the meaning of being lonely&quot;}},
	{&quot;tableID&quot;: 3, &quot;data&quot;: {&quot;backstreet boys&quot;: DELETED}},
	{&quot;tableID&quot;: 1, &quot;data&quot;: {&quot;charanjit singh&quot;: &quot;kalavati&quot;}},
	{&quot;tableID&quot;: 5, &quot;data&quot;: {&quot;charanjit singh&quot;: &quot;raag bhairav&quot;}},
	{&quot;tableID&quot;: 5, &quot;data&quot;: {&quot;daft punk&quot;: &quot;instant crush&quot;}},
	{&quot;tableID&quot;: 5, &quot;data&quot;: {&quot;death cab for cutie&quot;: &quot;i'll follow you into the dark&quot;}},
	{&quot;tableID&quot;: 4, &quot;data&quot;: {&quot;gorillaz&quot;: &quot;feel good inc&quot;}},
	{&quot;tableID&quot;: 1, &quot;data&quot;: {&quot;metallica&quot;: &quot;one&quot;}},
	{&quot;tableID&quot;: 2, &quot;data&quot;: {&quot;metallica&quot;: &quot;for whom the bell tolls&quot;}},
	{&quot;tableID&quot;: 4, &quot;data&quot;: {&quot;moblack&quot;: &quot;yamore&quot;}},
]
</code></pre>
<p>Then, ordering from most recently written to least:</p>
<pre><code class="language-json">[
	{&quot;tableID&quot;: 4, &quot;data&quot;: {&quot;AC/DC&quot;: &quot;thunderstruck&quot;}},
	{&quot;tableID&quot;: 3, &quot;data&quot;: {&quot;backstreet boys&quot;: DELETED}},
	{&quot;tableID&quot;: 1, &quot;data&quot;: {&quot;backstreet boys&quot;: &quot;show me the meaning of being lonely&quot;}},
	{&quot;tableID&quot;: 5, &quot;data&quot;: {&quot;charanjit singh&quot;: &quot;raag bhairav&quot;}},
	{&quot;tableID&quot;: 1, &quot;data&quot;: {&quot;charanjit singh&quot;: &quot;kalavati&quot;}},
	{&quot;tableID&quot;: 5, &quot;data&quot;: {&quot;daft punk&quot;: &quot;instant crush&quot;}},
	{&quot;tableID&quot;: 5, &quot;data&quot;: {&quot;death cab for cutie&quot;: &quot;i'll follow you into the dark&quot;}},
	{&quot;tableID&quot;: 4, &quot;data&quot;: {&quot;gorillaz&quot;: &quot;feel good inc&quot;}},
	{&quot;tableID&quot;: 2, &quot;data&quot;: {&quot;metallica&quot;: &quot;for whom the bell tolls&quot;}},
	{&quot;tableID&quot;: 1, &quot;data&quot;: {&quot;metallica&quot;: &quot;one&quot;}},
	{&quot;tableID&quot;: 4, &quot;data&quot;: {&quot;moblack&quot;: &quot;yamore&quot;}},
]
</code></pre>
<p>Now that we have the most recent data in a sorted fashion, we can delete all outdated and overwritten copies;</p>
<pre><code class="language-json">[
	{&quot;tableID&quot;: 4, &quot;data&quot;: {&quot;AC/DC&quot;: &quot;thunderstruck&quot;}},
	{&quot;tableID&quot;: 3, &quot;data&quot;: {&quot;backstreet boys&quot;: DELETED}},
	{&quot;tableID&quot;: 5, &quot;data&quot;: {&quot;charanjit singh&quot;: &quot;raag bhairav&quot;}},
	{&quot;tableID&quot;: 5, &quot;data&quot;: {&quot;daft punk&quot;: &quot;instant crush&quot;}},
	{&quot;tableID&quot;: 5, &quot;data&quot;: {&quot;death cab for cutie&quot;: &quot;i'll follow you into the dark&quot;}},
	{&quot;tableID&quot;: 4, &quot;data&quot;: {&quot;gorillaz&quot;: &quot;feel good inc&quot;}},
	{&quot;tableID&quot;: 2, &quot;data&quot;: {&quot;metallica&quot;: &quot;for whom the bell tolls&quot;}},
	{&quot;tableID&quot;: 4, &quot;data&quot;: {&quot;moblack&quot;: &quot;yamore&quot;}},
]
</code></pre>
<p>Which brings our final data sstable back to:</p>
<pre><code class="language-json">// data.6
{&quot;AC/DC&quot;: &quot;thunderstruck&quot;},
{&quot;backstreet boys&quot;: DELETED},
{&quot;charanjit singh&quot;: &quot;raag bhairav&quot;},
{&quot;daft punk&quot;: &quot;instant crush&quot;},
{&quot;death cab for cutie&quot;: &quot;i'll follow you into the dark&quot;},
{&quot;gorillaz&quot;: &quot;feel good inc&quot;},
{&quot;metallica&quot;: &quot;for whom the bell tolls&quot;},
{&quot;moblack&quot;: &quot;yamore&quot;},
</code></pre>
<p>We went from 11 rows and 5 files, to 8 rows and 1 file! This is the magic of compaction. Though the improvement seems small right now, this scales with your data size, and this means there are huge benefits to doing this correctly. We can now safely delete the older files, because the latest file will now answer all queries correctly in its place!</p>
<h3>How is Compaction Implemented?</h3>
<p>Now that we know what compaction is, let's explore how it happens in LSM Trees. This usually depends heavily on the implementation, but the general practice is that we pick a file from a level and merge it into files that have an overlapping key range in the next level. This is to maintain the invariant we spoke about in the previous part, where each level has only one file that a key can be in, which is why we need to have non-overlapping key ranges.</p>
<p>First, we identify the target SSTables that we need to compact. How we decide which sstables we want to compact also has massive performance implications. For now, we will just pick an overlapping key range.</p>
<p><img src="/static/images/lsmtree/lsm101_compaction_1.png" alt="" /></p>
<p>Fundamentally, <strong>compaction is a merge sort</strong> on two very large sorted arrays of key-value pairs, where we maintain the hierarchy of sorting order as first by key, and then by creation time; then we simply discard all the values for a key except for the first one. This gives us an SSTable in memory!</p>
<p>Most LSM Trees usually use some sort of iterators to do this, as it is memory-efficient to page disk blocks in one by one, and because each table is sorted, we know that the iterator will move in only one direction. RocksDB for example has a <code>MergingIterator</code><sup class="footnote-ref"><a href="#fn2" id="fnref2">2</a></sup> that helps them do this, with the examples provided.</p>
<p>Conversely, if we wish to iterate over two SSTables with a non-overlapping key range in the same level, we can simply &quot;join&quot; (read one after the other) the two SSTables. We accomplish this via a Join Iterator.</p>
<p>The final iterator construction looks something like this:</p>
<p><img src="/static/images/lsmtree/lsm101_compaction_iterator_assembly.png" alt="" /></p>
<p>I won't cover the implementation details of the iterators now, but the output it gives us is similar to the hand-computed output we found at the end of our last example.</p>
<p>Once we've selected our candidate SSTables, we assemble the iterators, which materialize the output SSTable. As we read through the iterator, we write the new SSTable into the larger level of the levels we've chosen.</p>
<p><img src="/static/images/lsmtree/lsm101_compaction_2.png" alt="" /></p>
<p>Then, we save the new state of the LSM Tree (level information, etc) in some kind of persistent state, such that the tree knows to look for this SSTable as a part of this level.</p>
<p><img src="/static/images/lsmtree/lsm101_compaction_3.png" alt="" /></p>
<p>Lastly, we delete the old SSTables in the tree to reclaim our disk space.</p>
<p><img src="/static/images/lsmtree/lsm101_compaction_4.png" alt="" /></p>
<p>As you can see, we've maintained the invariant that all levels of the table (except for level 0) have non-overlapping key ranges.</p>
<p>A good question to think about is: what do we do with tombstones during the compaction process?</p>
<h2>Types of compaction</h2>
<p>Now that we know that compaction also serves the process of maintaining not just data size but also the index by which we serve reads (the files, the levels, and the metadata for each level), there are multiple types of compaction strategies, each of which have different effects on lookup cost. There are multiple strategies present today, and this is an area of active research;  A paper titled &quot;Constructing and Analyzing the LSM Compaction Design Space&quot;<sup class="footnote-ref"><a href="#fn3" id="fnref3">3</a></sup> dives much deeper into this topic, and would be a good read for anyone trying to get a comprehensive view.</p>
<p>I won't be covering more compaction strategies in this post, but i'll probably expand upon this later.</p>
<h1>Footnotes</h1>
<section class="footnotes">
<ol>
<li id="fn1">
<p><a href="https://smalldatum.blogspot.com/2015/11/read-write-space-amplification-pick-2_23.html">https://smalldatum.blogspot.com/2015/11/read-write-space-amplification-pick-2_23.html</a> <a href="#fnref1" class="footnote-backref">↩</a></p>
</li>
<li id="fn2">
<p><a href="https://github.com/facebook/rocksdb/wiki/Iterator-Implementation#mergingiterator">https://github.com/facebook/rocksdb/wiki/Iterator-Implementation#mergingiterator</a> <a href="#fnref2" class="footnote-backref">↩</a></p>
</li>
<li id="fn3">
<p><a href="https://vldb.org/pvldb/vol14/p2216-sarkar.pdf">https://vldb.org/pvldb/vol14/p2216-sarkar.pdf</a> <a href="#fnref3" class="footnote-backref">↩</a></p>
</li>
</ol>
</section>
]]></content:encoded></item><item><title>Why Systems?</title><link>https://rowjee.com/blog/musings/why_systems.html</link><description><![CDATA[What draws me towards systems?]]></description><author>null</author><pubDate>Sun, 28 Jul 2024 09:00:00 +0000</pubDate><content:encoded><![CDATA[<p>I remembered what drew me to systems. I used to think it was not a good motive but now I realize;</p>
<p>It's symphony. In a database, all the components need to work together at extremely high speed and in exactly the right way; the kernel, the RAM, the network sockets, the buffers, the consensus algorithm, the scheduler, the parser, the compiler... It's like an orchestra, or a death metal band</p>
<p>There's a certain beauty to the unseen demons that show up at high speeds; high concurrency and high throughput is like the scream of the wind in your ears when you push your bike past 100.</p>
<p>And sometimes the most unlikely shit works; think of the Cannons, in Tchaikovsky's 1812 overture. No orchestra was supposed to have Cannons, but it works!</p>
]]></content:encoded></item><item><title>Understanding MapReduce</title><link>https://rowjee.com/blog/papers/mapreduce.html</link><description><![CDATA[Exploring MapReduce, a foundational computing paradigm for Big Data]]></description><author>null</author><pubDate>Sun, 31 Jul 2022 09:00:00 +0000</pubDate><content:encoded><![CDATA[<p>In this article, I'll be going through the paper &quot;<a href="http://static.googleusercontent.com/media/research.google.com/es/us/archive/mapreduce-osdi04.pdf">MapReduce: Simplified Data Processing on Large Clusters</a>&quot; by Jeffery Dean and Sanjay Ghemawat.</p>
<p>This paper deals with <em>MapReduce</em>, a way to perform large-scale computations with the fundamental operations (and functional primitives) <code>map</code> and <code>reduce</code>, in a distributed, parallelized manner.</p>
<p>If you're unfamiliar with the concepts here, I introduce the problem, and some basic primitives, in the <a href="#background">background</a> section - Keep reading for that! If you already know what these primitives are, and want to get to the paper directly, <a href="#the-papers-contributions">click here</a>.</p>
<h5>Table of Contents</h5>
<ol>
<li><a href="#background">Background</a></li>
<li><a href="#the-papers-contributions">The Paper's Contributions</a></li>
<li><a href="#implementation">Implementation</a></li>
<li><a href="#execution">Execution</a></li>
<li><a href="#metrics-benchmarks-and-performance">Metrics, Benchmarks and Performance</a></li>
<li><a href="#mapreduce-at-google">MapReduce at Google</a></li>
<li><a href="#assumptions-and-limitations">Assumptions and limitations</a></li>
<li><a href="#conclusion">Conclusion</a></li>
</ol>
<h2>Background</h2>
<p>To search the World Wide Web, one must first know what's on the web. To that end, one must generate a <code>index</code> - a data structure that holds both an item, and it's location.</p>
<p>In the same way the index of a book holds the name of the chapter and the page that chapter begins on, if one wishes to search the big ol' WWW by, say, a word, they'll need to maintain an <em>index</em> consisting of the item (the word being searched for) and its location (the URL of the page where the word is found).</p>
<p>Thus, creating an index of the WWW is the problem that Google faced in their early days. Way back then, the internet was (and mostly still is) a collection of HTML Documents maintaining links to each other by means of Hyperlinks (now URLs).</p>
<p>To generate an index, Google had to process as many parts of the web (as many documents) as they could, and perform said computations on it. However, this wasn't an easy problem at all - by around 1998, when Google launched, 512MB of RAM was a luxury, and there were already 2.4 Million websites on the internet <sup class="footnote-ref"><a href="#fn1" id="fnref1">1</a></sup>. There were a <em>lot</em> of documents to process.</p>
<p>At the time, Engineers at Google had already solved this problem, by hand-rolling their own, case-specific implementations of a computing system for each task (indexing the web, running the PageRank Algorithm, and so on). They'd get hundreds or thousands of computers together on a network, get them to talk to each other, and co-ordinate these large tasks.</p>
<p>This was the only way these tasks would finish in a reasonable amount of time. However, these implementations ran into various issues, such as frequently failing disks, tasks that took too long to run, and so on.</p>
<p>Around this time, the authors of the paper noticed that most such compute-intensive tasks they ran at Google comprised of a series of <code>map</code> and <code>reduce</code> operations.</p>
<p>Map and Reduce are both primitives of Functional Programming, that allow you to use functions as arguments to another function.</p>
<h3>Map</h3>
<p>The <code>map</code> function takes in an array and a function, and it <strong>applies that function to every element of the array</strong>.</p>
<pre><code class="language-python">&gt;&gt;&gt; def square(x):
...     return x * x
&gt;&gt;&gt; a = [1, 2, 3, 4, 5]
&gt;&gt;&gt; list(map(square, a))
[1, 4, 9, 16, 25]
</code></pre>
<h3>Reduce</h3>
<p>The <code>reduce</code> function takes an array and a function, and <strong>uses that function to combine the elements of the array</strong>.</p>
<pre><code class="language-python">&gt;&gt;&gt; from functools import reduce
&gt;&gt;&gt; def add(a, b):
...     return a + b
...
&gt;&gt;&gt; a = [1, 2, 3, 4, 5]
&gt;&gt;&gt; reduce(add, a)
15
</code></pre>
<p>Keep in mind that these <code>map</code> and <code>reduce</code> functions can be anything!</p>
<h2>The Paper's Contributions</h2>
<p>I see the paper having made two major contributions -</p>
<ol>
<li>Programs written as a series of Map and Reduce operations (and there are many that can be written this way) can be easily parallelized and distributed while maintaining an easy, simple interface</li>
</ol>
<p><img src="/static/images/mapreduce/mapreduce_for_lists.png" alt="" />
<em>A simple example of a MapReduce program to find the sum of twice a list of numbers</em></p>
<p><img src="/static/images/mapreduce/mapreduce_cluster_impl.png" alt="" />
<em>An example of how individual computers can handle <em>map</em> and <em>reduce</em> operations, enabling high parallelism and performance</em></p>
<p>Note that this paradigm consists of users only providing the functions that will be run on the <code>map</code> and <code>reduce</code> operations. They needn't worry about how or where it's implemented - they were inherently parallelizeable and could be distributed across multiple computers.</p>
<ol start="2">
<li>A high-performance implementation of this interface on a large cluster of commodity computers</li>
</ol>
<p>Google implemented a MapReduce system on a large cluster of commodity computers (~1000), which allowed them to complete indexing the internet in only seven passes of MapReduce.</p>
<h3>The Programming Model</h3>
<p>For the <code>map</code> function, the user provided a function that would take in some arguments (usually two, referred to as an &quot;input pair&quot;), and which <strong>emitted a series of intermediate key-value pairs</strong>.</p>
<p>The <code>reduce</code> function must then be written to accept a specific key, and a set of values for that key. It then reduces them to either form</p>
<ol>
<li>A smaller set of values, or</li>
<li>One output value</li>
</ol>
<p>From the paper, this can be summarized as the following -
<img src="/static/images/mapreduce/mapreduce_types.png" alt="" />
<em>MapReduce functions described as types, taken from the paper</em></p>
<p>The map function takes in a key-value pair, and emits a list of key-value pairs. One can think of these keys emitted as &quot;intermediate keys&quot;.</p>
<p>The reduce function takes in an intermediate key and a list of values for the same key, and emits a list of values (which can also have only one value).</p>
<blockquote>
<p>The intermediate keys serve to help create a separation between different reduce jobs. Reduce Jobs for the Intermediate key &quot;I&quot; will only handle values coming which have been emitted with the Intermediate key &quot;I&quot;.</p>
</blockquote>
<p>Google's implementation uses some very clever tricks to make sure that intermediate key-value pairs for a key <code>A</code> emitted by the <code>map</code> job end up at the system running the <code>reduce</code> job for the same key. We'll explore this some more soon.</p>
<h3>Some Programs in Google's MapReduce</h3>
<p>Let's start off with the simplest example.</p>
<p><img src="/static/images/mapreduce/mapreduce_wordcount_example.png" alt="" />
<em>A MapReduce program to count the number of times each word occurs in a document or a string, taken from the paper</em></p>
<p>Here, <code>emitIntermediate</code> is a function used to emit an Intermediate key-value pair. <code>emit</code> is used to emit the final value after the <code>reduce</code> job completes.</p>
<p>Note that it's possible to perform many, many types of computation with this model. The paper mentions a few, one of which we'll take a look at here. Apart from the one here, the paper also mentions</p>
<ul>
<li>A Distributed Text Search</li>
<li>URL Access Frequency Count</li>
<li>Term-Vector per Host</li>
<li>Inverted Index</li>
<li>Distributed Sorting</li>
</ul>
<h4>Reverse Web-Link Graph with MapReduce</h4>
<p><img src="/static/images/mapreduce/weblink-graph.png" alt="" />
<em>A simple example of four webpages, linking to each other</em></p>
<p>Let's say you want to find a list of pages (call them <code>sources</code>) that link to a specific page (the <code>target</code>) via a hyperlink.</p>
<p>This might be important, because you might want to rank how &quot;important&quot; a page is by seeing how many people link to it. Let's look at the page <code>C</code>.</p>
<p>Here's what our <code>map</code> function looks like -</p>
<pre><code class="language-python">def map(target, webpage):
  # check all the links in the page
  for link in webpage:
      # emitIntermediate(target, source)
      emitIntermediate(link, webpage.url)
</code></pre>
<p>Here are our intermediate values generated -</p>
<pre><code class="language-python">[(b, a), (d, a), (c, a), (c, b), (c, d), (a, c)]
</code></pre>
<p>Note that we're using the target link as our intermediate key.</p>
<p>Implementation Magic makes available to each <code>reduce</code> function the following -</p>
<pre><code class="language-python">[
(b, [a]),
(a, [c]),
(c, [a, b, d]),
(d, [a]),
]
</code></pre>
<p>Now, here's <code>reduce</code> -</p>
<pre><code class="language-python">def reduce(key, list):
    count = 0
    for link in list:
        count += 1
    emit(key, count)
</code></pre>
<p>Once <code>reduce</code> is called for all pages <code>[a, b, c, d]</code>, we get -</p>
<pre><code class="language-python">[
(c, 3),
(a, 1),
(b, 1),
(c, 1)
]
</code></pre>
<p>This tells us that <code>c</code> is the most &quot;important&quot; page.</p>
<h2>Implementation</h2>
<p><img src="/static/images/mapreduce/mapreduce_impl_google.png" alt="" />
<em>Google's implementation, as suggested in the paper</em></p>
<p>Now that we know how to implement some common programs in MapReduce, let's see how Google did it.</p>
<p>It all starts with a <em>Master</em> (we'll call this the <em>Supervisor</em>) process or worker node, which controls, schedules and orchestrates multiple MapReduce jobs on a cluster of commodity machines, each known as a <em>worker node</em> (we'll call these the <em>Task nodes</em>).</p>
<blockquote>
<p>The task nodes can be threads, processes, or independent computers. For now, we'll only consider the last aspect.</p>
</blockquote>
<p>Every task node is considered a generic task node, and isn't limited to being used only in the <code>map</code> or <code>reduce</code> phases of the entire process.</p>
<p>The supervisor node controls what job to schedule on which task node and when, so a node which ran <code>map</code>, if free, can also be used to run <code>reduce</code>.</p>
<p>The Scheduler also takes care of splitting data by key, or by size, to make it easier to transfer.</p>
<h2>Execution</h2>
<p><img src="/static/images/mapreduce/mapreduce_execution_desc.png" alt="" />
<em>Execution overview - MapReduce</em></p>
<ol>
<li>
<p>The data to be processed is split into <code>M</code> pieces of 16Mb - 64Mb blocks. This block size can be controlled by the user. At this same time, the program is started on multiple machines.</p>
</li>
<li>
<p>The Supervisor program is spawned, and it assigns work to all the Task Nodes. The Supervisor node also assigns idle nodes to different MapReduce Jobs.</p>
</li>
</ol>
<p><img src="/static/images/mapreduce/supervisor_node_desc.png" alt="" />
<em>Execution Overview - Supervisor Node</em></p>
<ol start="3">
<li>On the <code>map</code> side
<ol>
<li>Read the input assigned</li>
<li>Parse the Key-Value pairs and pass them to the <code>map</code> function</li>
<li>Intermediate key-value pairs are buffered in-memory</li>
<li>These buffered pairs are written to disk at regular intervals, and they're written partitioned by the key.</li>
<li>The locations of these pairs are passed to the Supervisor node.</li>
</ol>
</li>
</ol>
<p><img src="/static/images/mapreduce/map_task_node.png" alt="" />
<em>Execution Overview - Map Task Node</em></p>
<ol start="4">
<li>On the <code>reduce</code> side
<ol>
<li>When a <code>reduce</code> job is scheduled, it uses a <strong>Remote Procedure Call</strong> to fetch the buffered pairs from the <code>map</code> worker.</li>
<li>Once all intermediate data has been obtained, the <code>reduce</code> task node <strong>SORTS the key-value pairs to ensure the same pairs are grouped together</strong>.</li>
<li>The task node iterates over the data, passes the key-value pairs to the reduce function.</li>
<li>The Output from this is appended to the final output file.</li>
</ol>
</li>
</ol>
<p><img src="/static/images/mapreduce/reduce_task_node.png" alt="" />
<em>Execution Overview - Reduce Task Node</em></p>
<ol start="5">
<li>The task is complete!</li>
</ol>
<h3>A (very cool) Note on Locality</h3>
<p>One of Google's primary concerns was the reduction of network bandwidth being used, as it was a bottleneck. To prevent this from happening, Google implemented the Google File System (the GFS) - a distributed filesystem - to ensure that data was where it needed to be before it was required.</p>
<p>How the GFS works internally is perhaps the subject of another blog post itself, so we won't be concerning ourselves with that right now.</p>
<p>GFS (Google File System) handles moving files around, with a focus on ensuring that as much data is stored locally as possible. It also relies on replication - by dividing each file into 64MB Blocks, and ensuring that (typically) at least 3 copies of each block are available on different Task Nodes.</p>
<p>The coolest part is that the <strong>Supervisor Node is aware of this replication and locality</strong> effort, and <strong>attempts to schedule tasks on Task Nodes which already have the input files necessary to run the task</strong>. If one isn't available, it places the task on a machine close to (i.e. in the same network as) the replica. This way, bandwidth doesn't need to be wasted to move blocks back and forth.</p>
<h4>A smaller note on Supervisor Data Structures</h4>
<p>The supervisor node utilizies a number of data structures to keep track of all the tasks present in the cluster.</p>
<p>For every <code>map</code> and <code>reduce</code> task, it stores state (<code>IDLE</code> | <code>INPROGRESS</code> | <code>COMPLETED</code>) and the identity of task nodes.</p>
<p>For every completed <code>map</code> task, it stores the locations and sizes of the intermediate files produced. Updates happen as and when files are added or written to, and the same information is propagated to the <code>reduce</code> workers (this is only for <code>INPROGRESS</code> tasks)</p>
<h3>Fault Tolerance</h3>
<p>Such massive distributed systems can fail for multiple reasons. These are known as &quot;Failure Modes&quot;, and there are many possible cases. The sheer scale of operation implies that failure is a regular event, not a special edge case.</p>
<h4>If A Task Node Fails...</h4>
<p>The Supervisor pings every Task Node periodically to ensure it's still alive. This is standard practice in most systems like this.</p>
<p>If a Task Node is dead (i.e. has not responded to the Supervisor after a threshold amount of time), all the tasks assigned (both <code>completed</code> and <code>inprogress</code>) to it get marked as <code>IDLE</code> and thus become eligible to be allocated to another node in the cluster.</p>
<p>Even the <code>COMPLETED</code> tasks are marked as <code>IDLE</code> as a Task Node stores its intermediate K-V pairs locally - if the node is inaccessible, then so are these intermediate K-V Pairs.</p>
<h4>If The Supervisor Node Fails...</h4>
<p>The Supervisor data structures written to disk, if a master fails, it's easy to restore it at that current state</p>
<h3>Atomicity and Semantics in the presence of failures</h3>
<p>The paper makes the following statement -</p>
<blockquote>
<p>&quot;When the user-supplied map and reduce operators are deterministic functions of their input values, our distributed implementation produces the same output as would have been produced by a non-faulting sequential execution of the entire program.&quot;</p>
</blockquote>
<p>This is accomplished by using periodic, atomic writes of both the map and reduce outputs on their Task Nodes.</p>
<p>Since the writes are atomic, and the outputs are deterministic, even multiple runs of the same task will give the same output. Since it's atomic, there won't be any partial writes.</p>
<h3>Dealing with Slower Machines by using Backup Tasks</h3>
<p>Some nodes are stragglers - they become slow and hold up the scheduling queue, while <em><em>completing one or a few of the LAST COUPLE OF REMAINING</em></em> MapReduce tasks in a cluster. This slows down completion.</p>
<p>When an operation is close to completion, the Supervisor system schedules backup executions of the remaining <code>INPROGRESS</code> tasks, so at least one version will complete. This scheduling is tuned to increase the overall workload by only a few per cent.</p>
<h2>Metrics, Benchmarks and Performance</h2>
<p>The benchmarks for this cluster were considered mainly for two tasks -</p>
<ol>
<li>Sorting 1 TB of Data</li>
<li>Searching 1 TB of Data</li>
</ol>
<p>They were performed on a MapReduce cluster comprising roughly 1,800 computers, with 2x 2GHz Intel Xeon Processors, 4GB RAM, and 160GB IDE Disks per machine.</p>
<p>They find that Backup task handling proves to be crucial, absence of which proves to increase time by an average of 44%. The cluster is relatively resistant (only 5% delay) to machine failures, or process failures, at an 11.454% failure rate.</p>
<h3>Sorting</h3>
<p><img src="/static/images/mapreduce/mapreduce_sort_bench.png" alt="" /></p>
<p><em>Data Transfer rate as a heuristic to estimate throughput of task completion. Notice how not having backup tasks makes a significant difference - Taken from the Paper</em></p>
<p>Each row in the above graph represents a different phase of the sorting task, where</p>
<ul>
<li><em>input</em> - data read into the map jobs</li>
<li><em>shuffle</em> - buffered pairs from map jobs sent to reduce jobs and sorted</li>
<li><em>output</em> - reduce tasks complete and write to final output files</li>
</ul>
<p>And each column corresponds to a different cluster configuration, being</p>
<ul>
<li>Normal Execution (with Backup Tasks)</li>
<li>No Backup Tasks</li>
<li>With 200 Tasks being killed off</li>
</ul>
<p>This was modelled after the TeraSort benchmark. Since the reduce function sorts by keys, the Distributed Sort is fairly simple to implement in MapReduce.</p>
<p>Assuming a pre-existing knowledge of key distribution, Google's sorting implementation completed the sorting in 891 Seconds, as opposed to the current record, coming in at 1057 Seconds.</p>
<h3>Searching</h3>
<p>The implementation searched through 1 TB data in ~150 seconds, including around 60 seconds of startup time. Some delays and overheads were due to -</p>
<ul>
<li>Interactions with GFS</li>
<li>getting the program to newly assigned machines</li>
</ul>
<h2>MapReduce at Google</h2>
<p><img src="/static/images/mapreduce/mapreduce_over_time_google.png" alt="" />
<em>A graph showing the usage of MapReduce at Google</em></p>
<p>This was obviously a game-changer for Google, who began to use MapReduce not only for their production indexing system, but also for multiple other Google Services.</p>
<p>Their production indexing system crawled through 20TB of Data using MapReduce, with around Five to Ten passes of MapReduce for the entire indexing job. The same system went from around 3800 Lines of C++, to around 700 Lines when used with MapReduce.</p>
<p>Lastly, it allowed programmers who aren't distributed systems experts to take advantage of the infrastructure that Google has, as it abstracts away all the fault tolerance, parallelization, locality optimization and load-balancing details from the programmer. Since it focussed specifically on reducing network bandwidth usage, it allowed Google to deploy it at the scale we saw it run.</p>
<h2>Assumptions and Limitations</h2>
<ul>
<li>Assume functions being passed as Map and Reduce functions do not depend on Global State</li>
<li>Assumes the Supervisor node can make scheduling decisions at scale - must make <code>O(M + R)</code> scheduling decisions, and must keep <code>O(M * R)</code> state in memory.</li>
<li>Google's implementation assumes the task is restarted if the Supervisor Node fails</li>
<li>No guarantee of similar performance in sorting benchmarks without prior knowledge of key distribution</li>
</ul>
<h2>Conclusion</h2>
<p>This was my first ever paper review! If you liked it, do reach out to me to let me know what you think about it. I've got papers in mind that I want to cover, but if you find a paper you think is really cool, please don't hesitate to reach out!</p>
<p>As a paper, this one was super well-written, and I found my questions having been answered before I knew I had them. 10/10 recommended read.</p>
<section class="footnotes">
<ol>
<li id="fn1">
<p><a href="https://www.internetlivestats.com/total-number-of-websites/#trend">https://www.internetlivestats.com/total-number-of-websites/#trend</a> <a href="#fnref1" class="footnote-backref">↩</a></p>
</li>
</ol>
</section>
]]></content:encoded></item><item><title>Git Up and Running (Git 101)</title><link>https://rowjee.com/blog/git_up_and_running.html</link><description><![CDATA[This post is my best attempt to put into words a talk I gave (by the same title) at my University a couple of times. Grab a cup of coffee/tea/water, and let's learn about Git!]]></description><author>null</author><pubDate>Wed, 2 Feb 2022 09:00:00 +0000</pubDate><content:encoded><![CDATA[<p>This post is my best attempt to put into words a talk I gave (by the same title) at my University a couple of times. Grab a cup of coffee/tea/water, and let's learn about Git!</p>
<h1>Why?</h1>
<p>Git is an essential, industry-standard <a href="https://en.wikipedia.org/wiki/Version_control">Version Control System (VCS)</a> - In essence, Git is how you work on the same thing (usually a document or a bunch of files) with multiple people at the same time, without needing to break your head about it.</p>
<p>Remember when you'd save your work at different points in time by naming things roughly this way?</p>
<p><img src="/static/images/git_up_and_running/how_not_to.png" alt="" /></p>
<p>Fear not, my friend! Gone are the days where you'd have to do this. Pick your jaws off the floor, and let me introduce you to Git, which saves you from having to resort to this... practice :D</p>
<h1>What is Git?</h1>
<p>Git is a Distributed (i.e. can run on multiple connected computers) Version Control System (VCS) that helps you</p>
<ul>
<li><strong>track changes</strong> made to your files</li>
<li><strong>work on multiple different versions</strong> of your files at once (no, I'm not kidding! You can!)</li>
<li><strong>revert to an old version</strong> of your files</li>
<li><strong>collaborate with other people</strong> on the same files</li>
</ul>
<p>For all practical means and purposes, Git itself is a piece of software that was written to help the maintainers of the large software projects (Git was written by <a href="https://en.wikipedia.org/wiki/Linus_Torvalds">Linus Torvalds </a> and <a href="https://simple.wikipedia.org/wiki/Junio_Hamano">Junio Hamano</a>, both prominent maintainers of the Linux Kernel) do their work easily.</p>
<p>Isn't it amazing that you can use this tool regardless of the scale of your project?</p>
<p>Let's get learning!</p>
<h1>Let's Talk about Maggi.</h1>
<p>I like Maggi Noodles.. I think they're excellent, not just as noodles, but as a &quot;meal platform&quot; of sorts - it's a base on which you can build a lot more dishes, much like bread.</p>
<p>The beauty of Maggi (and most noodles) is that we can either have them plain, or we can customize them by adding our own ingredients.</p>
<p><strong><em>In the quest for the ultimate Maggi Noodle recipe, you begin to keep track of all the recipes you try out.</em></strong></p>
<p>To do this, you take the help of a Logbook, and a rough piece of paper.</p>
<p><img src="/static/images/git_up_and_running/maggi_components.png" alt="" /></p>
<h2>Some Basic Rules</h2>
<p>Before we embark on the wild quest to find the best maggi recipe known to humankind, we set the following rules in place -</p>
<ol>
<li>
<p><strong>Consider a Recipe to be a Collection of Ingredients and Quantities.</strong></p>
<p>This helps us quantify things! A recipe can look like this -</p>
<pre><code>NOODLES   10
MASALA    20
TOMATOES  03
</code></pre>
</li>
<li>
<p><strong>Each and Every change you make is either an ADDITION (+), DELETION (-) or a MODIFICATION (~) of said ingredients and their quantities.</strong></p>
<p>Let's say I started off with the recipe above. I liked it, but I felt like it could use more spice and one less tomato, so the difference would look like this -</p>
<pre><code>+ SPICE   10
- TOMATO  01
</code></pre>
<p>Applying this transformation on the recipe, the new recipe looks something like this -</p>
<pre><code>NOODLES   10
MASALA    20
TOMATOES  02
SPICE     10
</code></pre>
</li>
<li>
<p><strong>The Logbook keeps tracks of versions of recipes by <em>keeping track of the changes</em>.</strong></p>
<p>At this point, it's easy to see how to undo a change, simply by inverting the signs! Let's say we want to get back to the previous version. The Difference for this change looks like this -</p>
<pre><code>- SPICE   10
+ TOMATO  01
</code></pre>
<p>Magic! We're back where we started.</p>
<p>The logbook only <u>keeps track of all your changes</u>.</p>
</li>
<li>
<p><strong>When you're done fiddling around with a recipe on the rough sheet, you write down the changes in the logbook, recording it as a version.</strong></p>
<p>Once we're done experimenting, and we like how a certain change to the recipe tastes, we write it down in the logbook!</p>
<p>Our logbook looks something like this -</p>
<pre><code># Initial Version
+ NOODLES   10
+ MASALA    20
+ TOMATOES  03

# version 1
+ SPICE   10
- TOMATO  01
</code></pre>
</li>
</ol>
<p>Whew! That was a bit. Take a minute to make sure your understand what's going on! You'll thank yourself later, I promise you.</p>
<p>It's time to get cooking!</p>
<h2>What's Going On!?</h2>
<p>So, you're making a batch of the latest experimental Maggi. You'd obviously want to know how you're changing your recipe, so you write down all your changes on the rough paper before you decide you like it, and so that you can easily modify it.</p>
<p>At any given point of time during our experimentation process, we want to see what's going on in our recipe book. Here's what this set of actions looks like -</p>
<table>
<thead>
<tr>
<th align="center">Action</th>
<th align="center">What it Does</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center">Make a new logbook!</td>
<td align="center">starts an empty logbook to immortalize your tastiest experiments</td>
</tr>
<tr>
<td align="center">Look at the rough paper</td>
<td align="center">helps you see what momentary changes you've made</td>
</tr>
<tr>
<td align="center">Look at the Logbook</td>
<td align="center">see all the past changes you made</td>
</tr>
</tbody>
</table>
<h2>Eventual Comfort</h2>
<p>So you've decided on a set of tasty tweaks you want! Here's the rough procedure you follow to immortalize the latest reels-compliant and SEO-Friendly version of your Maggi -</p>
<table>
<thead>
<tr>
<th align="center">Step #</th>
<th align="center">Action</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center">1</td>
<td align="center">Confirm that your rough sheet has all the changes you want to add to the logbook</td>
</tr>
<tr>
<td align="center">2</td>
<td align="center">Copy all these changes to the logbook, and give this recipe a name!</td>
</tr>
</tbody>
</table>
<h2>The Situation So Far</h2>
<p>After a fun day of experimentation, here's what your logbook may look like.</p>
<p><img src="/static/images/git_up_and_running/git-1.png" alt="" /></p>
<p>I could say more about Tobasco sauce (but I digress).</p>
<h2>A Friend wants in!</h2>
<p>Having heard about all the fun you've been having with your Maggi, a friend - Ramesh - Wants in!</p>
<p>You tell Ramesh -</p>
<blockquote>
<p>Hey, Here's the system I'm following! Here's why it works, and here's how you can use it!</p>
</blockquote>
<p>So, in the true spirit of Open Source, you decide to give Ramesh your recipe collection and logbook to start his own, so that he can see your history of experimentation, and add his own changes, too!</p>
<p>Since you trust Ramesh to make some nice additions, you decide to frequently incorporate his changes to the recipe into yours.</p>
<p>Together, your recipes shall <strong>RULE THE WORLD!</strong> (and also only incidentally taste very nice)</p>
<p>Now, since your logbook is just paper, you can simply copy it all at once, and have multiple copies exist at the same time!</p>
<p>Let's see how we'd go about making a copy and working on it in parallel -</p>
<table>
<thead>
<tr>
<th align="center">Step #</th>
<th align="center">Action</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center">1</td>
<td align="center">Make a Xerox Copy of the Logbook</td>
</tr>
<tr>
<td align="center">2</td>
<td align="center">Make Changes on both copies simultaneously</td>
</tr>
<tr>
<td align="center">3</td>
<td align="center">Combine both these versions to get a new version!</td>
</tr>
</tbody>
</table>
<p>Solid. Both Ramesh and you can work on the logbook at the same time (in essence) and merge your changes later to reach a new, final version.</p>
<p>A Bird's eye view of this process looks something like this -</p>
<p><img src="/static/images/git_up_and_running/git-2.png" alt="" /></p>
<p>Keep in mind that you can have any copy of the logbook be the starting point of an entirely different copy, so it's possible for us to have an arbitrary configuration of these &quot;branches&quot;. Think of all the cool diagrams!</p>
<h3>How does this combination happen?</h3>
<p><strong>Since both versions originate from a single point, there's always the possibility you can combine the changes from both versions!</strong> (remember this.)</p>
<p>Your change can look like this -</p>
<pre><code>+ SPICE   10
- TOMATO  1
</code></pre>
<p>And Ramesh's, like this -</p>
<pre><code>+ SALT     11
</code></pre>
<p>Combined, these changes will work the same way!</p>
<pre><code>+ SPICE    10
- TOMATO   1
+ SALT     11
</code></pre>
<p><strong>What if you both make try to change the same thing?</strong></p>
<p>What if you said</p>
<pre><code>+ SALT     10
</code></pre>
<p>And Ramesh Said -</p>
<pre><code>- SALT     5
</code></pre>
<p>Our Logbook doesn't know how to combine these two changes! You'll have to either give it an entirely new value, or you'll need to decide whose version to keep, and whose version to discard.</p>
<blockquote>
<p>Communication is important.</p>
</blockquote>
<p>This leaves us in a funny place - <em><strong>unless you and Ramesh agree upon what to change and what not to change beforehand</strong></em>, this situation is common!</p>
<p>It's formally known as a <a href="https://css-tricks.com/merge-conflicts-what-they-are-and-how-to-deal-with-them/">merge conflict</a>, and can be pretty scary, but fear not! You'll get used to resolving them in no time.</p>
<h1>Congratulations! You know know how Git Works.</h1>
<p>🥳️</p>
<blockquote>
<p>Wait, what?</p>
</blockquote>
<p>Let's make things simpler - it turns out we were talking about code and version control all along!</p>
<p>Let's map our example to Git.</p>
<table>
<thead>
<tr>
<th align="center">Our Name</th>
<th align="center">What it's called in Git</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center">Logbook</td>
<td align="center">Repository</td>
</tr>
<tr>
<td align="center">Recipe</td>
<td align="center">Any Collection of Files</td>
</tr>
<tr>
<td align="center">Rough Paper</td>
<td align="center">Staging Area</td>
</tr>
<tr>
<td align="center">A Specific version of the logbook</td>
<td align="center">Commit</td>
</tr>
<tr>
<td align="center">The Xerox Copy of the Logbook</td>
<td align="center">Branch</td>
</tr>
<tr>
<td align="center">To Combine the copy and your version</td>
<td align="center">Merge</td>
</tr>
</tbody>
</table>
<p>In reality, you can apply more or less the same recipe-and-logbook model to software, code and anything you can represent in files!</p>
<p>So far, using this model, we learnt how to -</p>
<ul>
<li><strong>track changes</strong> made to your <del>recipes</del> files</li>
<li><strong>work on multiple different versions</strong> of your <del>recipes</del> files at once</li>
<li><strong>revert to an old version</strong> of your <del>recipes</del> files</li>
<li><strong>collaborate with other people</strong> on the same <del>recipes</del> files</li>
</ul>
<blockquote>
<p>Sure, okay, I have something of an understanding - but how do I use it? Isn't Git a Command Line tool?</p>
</blockquote>
<p>Fear Not! Let's see how these procedures we learnt for our logbook map to Git.</p>
<p>So, Get out your terminals (Git Bash, or just your regular terminal), and make sure you've got git installed. You can download and install it from <a href="https://git-scm.com/downloads">here</a> - the default options are almost always good enough.</p>
<h2>What's Going On!?</h2>
<p>Here's how you can understand what's going on in your repository, in the same context of the cookbook -</p>
<table>
<thead>
<tr>
<th align="center">Action</th>
<th align="center">Git Command</th>
<th align="center">What it Does (in real life)</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center">Make a new logbook!</td>
<td align="center"><code>git init</code></td>
<td align="center">creates an empty Git repository</td>
</tr>
<tr>
<td align="center">Look at the rough paper</td>
<td align="center"><code>git status</code></td>
<td align="center">tells you which branch you're on, which files are being tracked, and which changes have been considered</td>
</tr>
<tr>
<td align="center">Look at the Logbook</td>
<td align="center"><code>git log</code></td>
<td align="center">look at all the previous commits</td>
</tr>
</tbody>
</table>
<p>Let's start off by creating an empty repository. We'll then add just one file to it for now, and we'll make changes in it.</p>
<p><strong>NOTE: Anything after the <code>$</code> is a command.</strong></p>
<pre><code class="language-shell">$ git init
Initialized empty Git repository in /home/anirudh/git_101/.git/

$ ls -la
total 20
drwxrwxr-x   3 anirudh anirudh  4096 Feb  2 00:08 .
drwxr-x--- 131 anirudh anirudh 12288 Feb  1 23:26 ..
drwxrwxr-x   7 anirudh anirudh  4096 Feb  2 00:08 .git
</code></pre>
<p>We see that the folder is entirely blank, save for the <code>.git</code> folder. This is where the entire repository lives, and it holds everything git needs to know about your repository. Let's see what's going on -</p>
<pre><code class="language-shell">$ git status
On branch main

No commits yet

nothing to commit (create/copy files and use &quot;git add&quot; to track)
</code></pre>
<p><code>git status</code> (a very useful command to use as often as you want) is telling us what we know - this is a blank folder. We're on the default branch <code>main</code>, and we have no commits yet. So let's do what it says, and add a file.</p>
<pre><code class="language-shell">$ echo &quot;Hello, world!&quot; &gt; hello.txt
$ cat hello.txt
Hello, world!
</code></pre>
<p>The first command just creates a file called <code>hello.txt</code> and writes &quot;Hello, world!&quot; into it. We then use the program <code>cat</code> (might not be available on windows, so use a text editor of your choice!) to verify as much. Let's run <code>git status</code> to see if that had any effect -</p>
<pre><code class="language-shell">$ git status
On branch main

No commits yet

Untracked files:
  (use &quot;git add &lt;file&gt;...&quot; to include in what will be committed)
        hello.txt

nothing added to commit but untracked files present (use &quot;git add&quot; to track)
</code></pre>
<p>Now Git <strong>knows</strong> that there's a file (or a potential recipe on the rough sheet), and it hasn't been written to the logbook yet! We'll fix this right now.</p>
<h2>Eventual Comfort</h2>
<p>Once you've messed around with your code enough, and you think it's time to save it as a version, you can use these commands!</p>
<table>
<thead>
<tr>
<th align="center">Step #</th>
<th align="center">Action</th>
<th align="center">Git Command</th>
<th align="center">What it Does (in real life)</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center">1</td>
<td align="center">Confirm that your rough sheet has all the changes you want to add to the logbook</td>
<td align="center"><code>git add -A</code></td>
<td align="center">adds all the files in your repository to the staging area</td>
</tr>
<tr>
<td align="center">2</td>
<td align="center">Copy all these changes to the logbook, and give this recipe a name!</td>
<td align="center"><code>git commit -m &quot;&lt;message&gt;&quot;</code></td>
<td align="center">Saves the current version of your repository as a commit</td>
</tr>
</tbody>
</table>
<p>Let's start off by running <code>git log</code> wherever we left our repository.</p>
<pre><code class="language-shell">$ git log
fatal: your current branch 'main' does not have any commits yet
</code></pre>
<p>Spot on! We'll fix this by following the steps above.</p>
<pre><code class="language-shell">$ git status
On branch main

No commits yet

Untracked files:
  (use &quot;git add &lt;file&gt;...&quot; to include in what will be committed)
        hello.txt

nothing added to commit but untracked files present (use &quot;git add&quot; to track)
</code></pre>
<p>The output here's remained the same. Let's now use the <code>git add</code> command to add all our files to the staging area.</p>
<pre><code class="language-shell">$ git add -A
$ git status
On branch main

No commits yet

Changes to be committed:
  (use &quot;git rm --cached &lt;file&gt;...&quot; to unstage)
        new file:   hello.txt
</code></pre>
<p>We use the <code>-A</code> flag here to specify that we want to track all files, so as to not have to specify files individually (you can do that if you wish to!).</p>
<p>Running <code>git status</code> immediately after shows us that Git knows, and is keeping track of, this file. It's now ready to write it into our logbook at any point of time.</p>
<p>Let's do that!</p>
<pre><code class="language-shell">$ git commit -m &quot;first commit!&quot;
[main (root-commit) 39e97d5] first commit!
 1 file changed, 1 insertion(+)
 create mode 100644 hello.txt
</code></pre>
<p>A little involved here - the <code>-m &quot;&lt;message here&gt;&quot;</code> is followed to create a &quot;commit message&quot; for each commit. Think of it as leaving a note explaining your changes to anyone else who sees this.</p>
<p><strong>DO NOT WORRY if the numbers you see aren't the same - they're what are known as <a href="https://www.mikestreety.co.uk/blog/the-git-commit-hash/">commit hashes</a> and aren't supposed to be the same <em>by design</em></strong>. Commit hashes are unique to a commit, and can be used to identify individual commits.</p>
<p>So you can now see that Git is, indeed, keeping track of insertions and deletions.</p>
<p>Lastly, let's run <code>git log</code> to see what's going on -</p>
<pre><code class="language-shell">$ git log
commit 39e97d52c2c0c26a1c3a314c040c0f01f476a3bc (HEAD -&gt; main)
Author: Anirudh Rowjee &lt;ani.rowjee@gmail.com&gt;
Date:   Wed Feb 2 00:11:40 2022 +0530

    first commit!
</code></pre>
<p>Here we can see the full commit hash, as well as the details of the person who made the commit - here's where the commit message comes into play! We can also see that the <code>HEAD</code> commit (the latest commit) is on the <code>main</code> branch.</p>
<p><strong>Writing a detailed commit message is always helpful, and is a good practice</strong>.</p>
<p>You can have as many commits as you want!</p>
<h2>A Friend wants in!</h2>
<p>Here's how we'll deal with collaboration and multiple versions existing at once -</p>
<table>
<thead>
<tr>
<th align="center">Step #</th>
<th align="center">Action</th>
<th align="center">Git Command</th>
<th align="center">What it Does (In Real Life)</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center">1</td>
<td align="center">Make a Xerox Copy of the Logbook</td>
<td align="center"><code>git branch</code></td>
<td align="center">creates a new branch from your current branch</td>
</tr>
<tr>
<td align="center">2</td>
<td align="center">Make changes on both copies of the logbook simultaneously</td>
<td align="center"><code>git status</code>, <code>git add</code>, <code>git commit</code></td>
<td align="center">add files to the staging area and make a new commit with these files</td>
</tr>
<tr>
<td align="center">3</td>
<td align="center">Combine both these versions to get a new version!</td>
<td align="center"><code>git merge</code></td>
<td align="center">merges both the branches into one branch</td>
</tr>
</tbody>
</table>
<p>Let's start off by <strong>listing all the branches our repository has</strong>.</p>
<pre><code class="language-shell">$ git branch --list
* main
</code></pre>
<p>The <code>*</code> tells us which branch we're currently on. Good IDEs, good text editors and good shells will go out of their way to make the current git branch obvious to you.</p>
<p>At the moment, only one, but who said we're done? Let's <strong>make a new branch</strong>.</p>
<pre><code class="language-shell">$ git branch new_branch
$ git branch --list
* main
  new_branch
</code></pre>
<p>Let's <strong>switch to the branch we just created</strong>.</p>
<pre><code class="language-shell">$ git checkout new_branch
Switched to branch 'new_branch'
$ git branch --list
  main
* new_branch
</code></pre>
<p><em>Psst! You can both create and switch to a new branch at the same time by using the <code>-b</code> flag with <code>git checkout</code> - the above could've been accomplished with just <code>git checkout -b new_branch</code></em></p>
<p>Now we make some changes to this file, and add a commit or two.</p>
<pre><code class="language-shell">$ nano hello.txt

$ cat hello.txt
Hello, world!


This is Anirudh, from the new branch!
</code></pre>
<p>Once we've made changes to the file, we're ready to commit.</p>
<pre><code class="language-shell">$ git add -A

$ git status
On branch new_branch
Changes to be committed:
  (use &quot;git restore --staged &lt;file&gt;...&quot; to unstage)
        modified:   hello.txt

$ git commit -m &quot;made change on branch&quot;
[new_branch 5128622] made change on branch
 1 file changed, 3 insertions(+)
</code></pre>
<p>Let's switch to our <code>main</code> branch, and take a look at the file.</p>
<pre><code class="language-shell">$ git checkout main
Switched to branch 'main'

$ cat hello.txt
Hello, world!
</code></pre>
<p>No change! We now have two versions of our file existing at the same time on two branches.</p>
<pre><code class="language-shell">$ git checkout new_branch
Switched to branch 'new_branch'

$ cat hello.txt
Hello, world!

This is me from a new branch. HI!
</code></pre>
<p>Quickly checking <code>git log</code> tells us the story of two branches -</p>
<pre><code class="language-shell">$ git log
commit 51286222c4e75db4ffe04f9284d4af19f348a56f (HEAD -&gt; new_branch)
Author: Anirudh Rowjee &lt;ani.rowjee@gmail.com&gt;
Date:   Wed Feb 2 00:16:12 2022 +0530

    made change on branch

commit 39e97d52c2c0c26a1c3a314c040c0f01f476a3bc (main)
Author: Anirudh Rowjee &lt;ani.rowjee@gmail.com&gt;
Date:   Wed Feb 2 00:11:40 2022 +0530

    first commit!
</code></pre>
<p><code>HEAD</code> points to the latest commit in the repository. For the <code>new_branch</code> branch, it's the one where we made a change!</p>
<pre><code class="language-shell">$ git checkout main
Switched to branch 'main'

$ git log
commit 39e97d52c2c0c26a1c3a314c040c0f01f476a3bc (HEAD -&gt; main)
Author: Anirudh Rowjee &lt;ani.rowjee@gmail.com&gt;
Date:   Wed Feb 2 00:11:40 2022 +0530

    first commit!
</code></pre>
<p>Since <code>main</code> doesn't know about the changes made in <code>new_branch</code>, <code>HEAD</code> points to a different, earlier commit.</p>
<p>How all this works under the hood is the combination of years of experience and some frankly amazing engineering, which is most definitely out of the scope of this article. Maybe Another Day!</p>
<p>Now, Let's <strong>merge our two branches</strong> to see what happens to the file!</p>
<pre><code class="language-shell">$ git merge new_branch
Updating 39e97d5..5128622
Fast-forward
 hello.txt | 3 +++
 1 file changed, 3 insertions(+)
</code></pre>
<p>WOOHOO! Just a quick check confirms that the changes from our branch, have, indeed, been updated into the main branch -</p>
<pre><code class="language-shell">$ cat hello.txt
Hello, world!


This is Anirudh, from the new branch!
</code></pre>
<p>Taking a look at <code>git log</code> confirms as much.</p>
<pre><code class="language-shell">$ git log
commit 51286222c4e75db4ffe04f9284d4af19f348a56f (HEAD -&gt; main, new_branch)
Author: Anirudh Rowjee &lt;ani.rowjee@gmail.com&gt;
Date:   Wed Feb 2 00:16:12 2022 +0530

    made change on branch

commit 39e97d52c2c0c26a1c3a314c040c0f01f476a3bc
Author: Anirudh Rowjee &lt;ani.rowjee@gmail.com&gt;
Date:   Wed Feb 2 00:11:40 2022 +0530

    first commit!
</code></pre>
<p>As we can see, <code>HEAD</code> points to both our branches.</p>
<p>If we're on the branch we want to merge into, we simply need to use this command to merge changes from the branch to the main branch.</p>
<pre><code class="language-shell">$ git merge &lt;branch_name&gt;
</code></pre>
<p>This about sums it up! You're now ready to use Git, and you know enough to debug any issues you might run into with this, or at least use google to debug better!</p>
<h2>The Git Cheat Sheet by Anirudh Rowjee</h2>
<p>Feel free to take a screenshot of this, and keep it around as a handy reference :D</p>
<table>
<thead>
<tr>
<th align="center">Action</th>
<th align="center">Git Command</th>
<th align="center">What it Does (in Git)</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center">Make a new logbook!</td>
<td align="center"><code>git init</code></td>
<td align="center">creates an empty Git repository</td>
</tr>
<tr>
<td align="center">Look at the rough paper</td>
<td align="center"><code>git status</code></td>
<td align="center">tells you which branch you're on, which files are being tracked, and which changes have been considered</td>
</tr>
<tr>
<td align="center">Look at the Logbook</td>
<td align="center"><code>git log</code></td>
<td align="center">look at all the previous commits</td>
</tr>
<tr>
<td align="center">Confirm that your rough sheet has all the changes you want to add to the logbook</td>
<td align="center"><code>git add -A</code></td>
<td align="center">adds all the files in your repository to the staging area</td>
</tr>
<tr>
<td align="center">Copy all these changes to the logbook, and give this recipe a name!</td>
<td align="center"><code>git commit -m &quot;&lt;message&gt;&quot;</code></td>
<td align="center">Saves the current version of your repository as a commit</td>
</tr>
<tr>
<td align="center">Make a Xerox Copy of the Logbook</td>
<td align="center"><code>git branch &lt;branch name&gt;</code></td>
<td align="center">creates a new branch from your current branch</td>
</tr>
<tr>
<td align="center">Look at a different Xerox Copy of the logbook</td>
<td align="center"><code>git checkout &lt;branch name&gt;</code></td>
<td align="center">switches to a different branch of the repository</td>
</tr>
<tr>
<td align="center">Make a new copy and look at the copy of the logbook</td>
<td align="center"><code>git checkout -b &lt;branch name&gt;</code></td>
<td align="center">creates a new branch from your current branch and switches to it</td>
</tr>
<tr>
<td align="center">Combine both these versions to get a new version!</td>
<td align="center"><code>git merge &lt;branch name&gt;</code></td>
<td align="center">merges both the branches into one branch</td>
</tr>
</tbody>
</table>
<h1>What now?</h1>
<p>You've made it this far, so pat yourself on the back!</p>
<p>If you want to learn more, the <a href="https://git-scm.com/doc">official documentation</a> is an excellent place to start.</p>
<p>I hope you now have some idea of how to use Git, given that you have some understanding of what's going on apart from the output you see from the commands. I'd urge you to use Git for your personal projects (if you don't already), so you can get used to using this awesome tool!</p>
<p>Thank you for your time, and I hope I've been of some help!</p>
<p>Oh, and don't shy away from telling your friends about this if it helped you :D</p>
<p>PS: I'd love to hear from you if you found this helpful - reach out through my twitter or email (found at the bottom of the page). Thank you!</p>
]]></content:encoded></item></channel></rss>