<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
     xmlns:atom="http://www.w3.org/2005/Atom"
     xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Venish Patidar</title>
    <link>https://venish.dev</link>
    <description>Notes on computer science, mathematics, quantum physics — and skydiving in between</description>
    <language>en</language>
    <atom:link href="https://venish.dev/rss.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>How Rust's HashMap Works Internally</title>
      <link>https://venish.dev/blog/how-hashmap-works-internally</link>
      <guid isPermaLink="true">https://venish.dev/blog/how-hashmap-works-internally</guid>
      <pubDate>Sun, 02 Aug 2026 00:00:00 GMT</pubDate>
      <description>Hash builders, control bytes, probing, and why a String-keyed map can still be queried with &amp;str.</description>
      <category>Rust</category>
      <content:encoded><![CDATA[<p>Rust's <code>HashMap</code> looks small from the outside: hash a key, find a slot, return a
value. The current standard-library implementation is built around SwissTable
ideas, which make that lookup less like following a chain and more like scanning
a compact index.</p>
<p>Those internals are not part of Rust's stable API. They can change without
breaking user code. The useful part is understanding the shape of the current
design and the costs it creates.</p>
<h2>1. Hashing is supplied, not hard-coded</h2>
<p><code>HashMap&#x3C;K, V, S></code> stores a hash builder <code>S</code>. Each operation asks it for a
hasher, feeds the key through <code>Hash</code>, and uses the resulting integer to begin a
lookup. The default <code>RandomState</code> is seeded per map, which makes deliberately
constructed collision attacks harder.</p>
<p>The exact default hashing algorithm is deliberately not a contract. Code that
needs a different speed or security trade-off can provide another
<code>BuildHasher</code>, but every key already in a map must have been processed by the
same builder.</p>
<h2>2. Control bytes narrow the search</h2>
<p>The table keeps key-value storage alongside a compact control byte for each
bucket. A control byte records whether the bucket is full, empty, or deleted.
For a full bucket it also keeps a short fingerprint derived from the hash.</p>
<p>The remaining hash bits choose the first group of buckets to inspect. The table
compares the wanted fingerprint against a group of control bytes at once,
using vector instructions where they are available. Only matching fingerprints
become candidates for a full key comparison.</p>
<p>That separation matters. Most misses are rejected by metadata that is much
smaller and denser than the keys themselves.</p>
<h2>3. Collisions probe another group</h2>
<p>This is open addressing: colliding entries live elsewhere in the same table,
not in a linked structure hanging from one bucket. If the first group does not
contain the key, the probe sequence moves to another group and continues.</p>
<p>A genuinely empty bucket ends an unsuccessful lookup. A deleted bucket cannot,
because entries later in the probe sequence may still depend on that path.
Deletion therefore leaves a tombstone until later maintenance can reclaim it.</p>
<h2>4. Capacity leaves room for probing</h2>
<p>The table grows before every bucket is occupied. The SwissTable-style layout
keeps roughly one slot in eight available so lookups usually find an empty
bucket quickly; the exact growth policy remains an implementation detail.</p>
<p>Growing allocates a larger raw table and redistributes the entries. If the
approximate size is already known, <code>with_capacity</code> or <code>reserve</code> avoids repeating
that work.</p>
<pre><code class="language-rust">use std::collections::HashMap;

let mut counts: HashMap&#x3C;String, u64> = HashMap::with_capacity(1_024);

for word in ["timeout", "retry", "timeout"] {
    *counts.entry(word.to_owned()).or_insert(0) += 1;
}

// String implements Borrow&#x3C;str>, so this lookup allocates nothing.
assert_eq!(counts.get("timeout"), Some(&#x26;2));
</code></pre>
<h2>5. Ownership shapes the public API</h2>
<p>Insertion moves owned keys and values into the map. Lookup is more flexible:
<code>get</code> can accept a borrowed form of the key when the stored key implements
<code>Borrow</code> for it and both forms hash and compare the same way. A map keyed by
<code>String</code> can therefore be queried with <code>&#x26;str</code>.</p>
<p>The <code>entry</code> API performs the lookup once and then exposes the occupied or vacant
case. That is why counters and in-place updates use <code>entry</code> instead of a
<code>get</code>-then-<code>insert</code> pair.</p>
<h2>Conclusion</h2>
<p>The useful model is a hash builder feeding an open-addressed table, with compact
metadata filtering candidates before key comparison. Rust then layers ownership,
borrowing, and <code>entry</code> on top so callers can use that table without giving up
the language's usual guarantees.</p>]]></content:encoded>
    </item>
    <item>
      <title>Redis Caching Explained</title>
      <link>https://venish.dev/blog/redis-caching-explained</link>
      <guid isPermaLink="true">https://venish.dev/blog/redis-caching-explained</guid>
      <pubDate>Sat, 04 Jul 2026 00:00:00 GMT</pubDate>
      <description>Cache-aside, stampedes, and an eviction policy you can defend.</description>
      <category>Redis</category>
      <category>Distributed Systems</category>
      <content:encoded><![CDATA[<p>Most caching failures I care about are failures of the miss path. The cache is
empty, slow, or carrying an old value; the rest of the system still has to make
a deliberate choice.</p>
<h2>Cache-aside is the default</h2>
<p>Cache-aside keeps the database authoritative. A read checks Redis first, falls
back to the source on a miss, and stores the result with an expiration. Writes
usually invalidate the key so the next read repopulates it. Updating the cache
directly can be appropriate, but it also makes ordering between two systems part
of the correctness model.</p>
<pre><code class="language-go">func (s *Service) Product(ctx context.Context, id string) (Product, error) {
	key := "product:" + id

	cached, err := s.cache.Get(ctx, key).Bytes()
	if err == nil {
		var product Product
		if json.Unmarshal(cached, &#x26;product) == nil {
			return product, nil
		}
		slog.WarnContext(ctx, "invalid cached product", "key", key)
	} else if !errors.Is(err, redis.Nil) {
		slog.WarnContext(ctx, "redis read failed", "key", key, "error", err)
	}

	product, err := s.products.ByID(ctx, id)
	if err != nil {
		return Product{}, err
	}

	if encoded, err := json.Marshal(product); err == nil {
		if err := s.cache.Set(ctx, key, encoded, 10*time.Minute).Err(); err != nil {
			slog.WarnContext(ctx, "redis write failed", "key", key, "error", err)
		}
	}

	return product, nil
}
</code></pre>
<p><code>redis.Nil</code> is the expected miss. Other Redis errors are observable, but they do
not turn a successful source-of-truth read into an application failure. That
trade-off only makes sense because this Redis instance is a disposable cache.</p>
<h2>Stampedes</h2>
<p>When a hot key expires, every concurrent reader misses at once and they all hit
the database together. TTL jitter prevents related keys from expiring in
lockstep. Request coalescing or a short distributed lock lets one caller
recompute while the rest wait briefly. Systems that can tolerate bounded
staleness can serve the previous value during that refresh instead.</p>
<p>The right mechanism depends on the cost of recomputation and the consequence of
stale data. A lock is not automatically worth adding to every cached read.</p>
<h2>Picking an eviction policy</h2>
<p>For a Redis instance containing only reproducible cache entries, an <code>allkeys-*</code>
policy lets Redis reclaim memory instead of rejecting writes. LRU is a sensible
starting point when recent access predicts reuse; LFU can fit workloads where a
stable hot set matters more.</p>
<p>If Redis contains data that cannot be regenerated, it is no longer merely a
cache. <code>noeviction</code> turns memory pressure into a visible write failure rather
than silently discarding that data, but the larger fix is to separate durable
state from disposable cache entries and operate them with different policies.</p>]]></content:encoded>
    </item>
    <item>
      <title>When Postgres Chooses an Index-Only Scan</title>
      <link>https://venish.dev/blog/postgres-index-only-scans</link>
      <guid isPermaLink="true">https://venish.dev/blog/postgres-index-only-scans</guid>
      <pubDate>Mon, 08 Jun 2026 00:00:00 GMT</pubDate>
      <description>Visibility maps, covering indexes, and reading EXPLAIN without guessing.</description>
      <category>PostgreSQL</category>
      <content:encoded><![CDATA[<p>An index-only scan answers a query from the index alone, never touching the
heap. Postgres will only do it when two conditions hold.</p>
<h2>Condition one: the index covers the query</h2>
<p>Every column referenced — in <code>SELECT</code>, <code>WHERE</code> and <code>ORDER BY</code> — must be present
in the index. <code>INCLUDE</code> adds payload columns without widening the search key:</p>
<pre><code class="language-sql">CREATE INDEX idx_orders_customer
  ON orders (customer_id)
  INCLUDE (status, total_cents);
</code></pre>
<h2>Condition two: the visibility map is current</h2>
<p>The index holds no row visibility information, so Postgres consults the
visibility map to learn whether a page is all-visible. Pages dirtied since the
last vacuum are not, and those rows require a heap fetch anyway.</p>
<h2>Reading the plan</h2>
<p><code>EXPLAIN (ANALYZE, BUFFERS)</code> reports <code>Heap Fetches</code>. Zero means the scan was
genuinely index-only. A large number after a bulk load means the table has not
been vacuumed — run <code>VACUUM ANALYZE</code> and measure again before touching the
index.</p>]]></content:encoded>
    </item>
  </channel>
</rss>