<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/rss.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>dethlex — digital garden</title><description>Personal site and digital garden of Aleksei Khromenkov — backend engineer working on high-load systems: Go, ClickHouse, Kafka, observability.</description><link>https://dethlex.com/</link><language>en</language><item><title>Kafka partition assignment: Range, RoundRobin, and why joins care</title><link>https://dethlex.com/garden/kafka-partition-assignment/</link><guid isPermaLink="true">https://dethlex.com/garden/kafka-partition-assignment/</guid><description>RangeAssignor looks naive until you join two topics — then its alignment is the whole point. Choosing an assignment strategy is choosing what your consumers are allowed to assume.</description><pubDate>Wed, 22 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;🧪 &lt;strong&gt;Sample seed note.&lt;/strong&gt; Placeholder content demonstrating the garden structure — replace with your own text.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A consumer group is a little scheduler: topics on one side, consumers on the
other, and an &lt;em&gt;assignor&lt;/em&gt; deciding who reads what. Most people meet assignors
the day load lands unevenly on their pods. The more interesting encounter is
the day a stream-stream join silently produces garbage.&lt;/p&gt;
&lt;h2 id=&quot;rangeassignor-skewed-but-aligned&quot;&gt;RangeAssignor: skewed, but aligned&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;RangeAssignor&lt;/code&gt; works &lt;strong&gt;per topic&lt;/strong&gt;: sort the partitions, sort the consumers,
carve the partitions into contiguous ranges. With 8 partitions and 3
consumers you get 3-3-2 — the first consumers always take the extra
partitions, &lt;em&gt;for every topic they subscribe to&lt;/em&gt;. Subscribe one group to five
8-partition topics and consumer 0 carries five extra partitions while
consumer 2 carries none. That is the famous Range skew.&lt;/p&gt;
&lt;p&gt;But look at what the per-topic sorting buys: &lt;strong&gt;partition N of every
subscribed topic lands on the same consumer.&lt;/strong&gt; If &lt;code&gt;impressions&lt;/code&gt; and &lt;code&gt;clicks&lt;/code&gt;
are both keyed by &lt;code&gt;campaign_id&lt;/code&gt;, both have 8 partitions, and both were
written with the same partitioner, then all events for one campaign — from
both topics — arrive at one process. You can join them in local memory with
no shuffle, no external store. This property is called &lt;strong&gt;co-partitioning&lt;/strong&gt;,
and Range-style assignment is the only reason it survives group rebalancing.&lt;/p&gt;
&lt;h2 id=&quot;roundrobin-and-sticky-even-but-blind&quot;&gt;RoundRobin and Sticky: even, but blind&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;RoundRobinAssignor&lt;/code&gt; throws every topic-partition into one pool and deals
them out like cards. Load: even. Alignment: destroyed — &lt;code&gt;impressions-3&lt;/code&gt; and
&lt;code&gt;clicks-3&lt;/code&gt; now live on different consumers, and your in-memory join is
quietly wrong.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;StickyAssignor&lt;/code&gt; and its successor &lt;code&gt;CooperativeStickyAssignor&lt;/code&gt; optimize a
different axis: keep the previous assignment as intact as possible across
rebalances. Cooperative rebalancing additionally kills the stop-the-world
pause — consumers only release the partitions that actually move. For a
plain fan-out consumer group this is what you want in 2026.&lt;/p&gt;
&lt;h2 id=&quot;the-actual-decision-table&quot;&gt;The actual decision table&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You are…&lt;/th&gt;
&lt;th&gt;Pick&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;joining topics on a shared key&lt;/td&gt;
&lt;td&gt;Range (and see checklist)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;running a plain high-volume fan-out&lt;/td&gt;
&lt;td&gt;CooperativeSticky&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;on Kafka Streams&lt;/td&gt;
&lt;td&gt;nothing — it brings its own&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The co-partitioning checklist, all four or nothing:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Same &lt;strong&gt;partition count&lt;/strong&gt; on every joined topic.&lt;/li&gt;
&lt;li&gt;Same &lt;strong&gt;key&lt;/strong&gt; and serialization.&lt;/li&gt;
&lt;li&gt;Same &lt;strong&gt;partitioner&lt;/strong&gt; on every producer.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Range-family assignor&lt;/strong&gt; on the consumer group.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Break any one of them — someone “rebalances” partition counts, one producer
uses murmur2 and another crc32 — and the join degrades without a single error
in any log. The only alarm you’ll get is a business metric drifting.&lt;/p&gt;
&lt;p&gt;In Go this is explicit: &lt;code&gt;franz-go&lt;/code&gt; takes
&lt;code&gt;kgo.Balancers(kgo.CooperativeStickyBalancer())&lt;/code&gt; (or &lt;code&gt;kgo.RangeBalancer()&lt;/code&gt;),
&lt;code&gt;segmentio/kafka-go&lt;/code&gt; takes &lt;code&gt;GroupBalancers&lt;/code&gt;. Choose consciously — the default
depends on the library, and as shown above, the default is a semantic
decision, not a tuning knob.&lt;/p&gt;</content:encoded><category>kafka</category><category>distributed-systems</category><category>go</category></item><item><title>Trident 660, first thousand kilometers</title><link>https://dethlex.com/garden/triumph-trident-660/</link><guid isPermaLink="true">https://dethlex.com/garden/triumph-trident-660/</guid><description>Notes after a month with Triumph&apos;s little triple on Cyprus roads — what the spec sheet doesn&apos;t tell you about heat, hairpins and the sound at 7000 rpm.</description><pubDate>Sat, 20 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;🧪 &lt;strong&gt;Sample seed note.&lt;/strong&gt; Placeholder content demonstrating the garden structure — replace with your own text.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The spec sheet says 660 cc, three cylinders, 81 hp. What it doesn’t say is
what a triple &lt;em&gt;feels&lt;/em&gt; like when you live between a twin and an inline-four in
your head: torque low like the twin, then instead of running out of breath it
just keeps pulling, smoother and angrier, until the shift light. The sound
between 6 and 8 thousand is the reason people keep this bike long after they
can afford a bigger one.&lt;/p&gt;
&lt;h2 id=&quot;what-works&quot;&gt;What works&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The engine is honest.&lt;/strong&gt; No ride-by-wire theatrics in Road mode: throttle
in, drive out. Rain mode exists; on Cyprus it gets used maybe four days a
year.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hairpins.&lt;/strong&gt; On the Troodos switchbacks the low center of mass and the
wide bars make the bike fall into corners with almost no effort. It flatters
you exactly up to the point where it doesn’t — which is a fine teacher.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Commuting.&lt;/strong&gt; Light clutch, no drama in traffic, mirrors that actually
show the road. It disappears under you, in the best sense.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what-the-brochure-skips&quot;&gt;What the brochure skips&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Heat.&lt;/strong&gt; Thirty-eight degrees in Limassol traffic and the triple reminds
you it’s a compact engine in a tight frame. Moving, it’s fine; standing,
you learn to time the lights.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The tank.&lt;/strong&gt; Fourteen liters is optimistic marketing for “range”. Riding
it like it asks to be ridden, the low-fuel bar shows up around 180 km, and
mountain villages do not compete on petrol station density.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wind.&lt;/strong&gt; Naked means naked. Above 120 km/h your neck is the windscreen;
the aftermarket flyscreen moved from “cosmetic” to “ordered” within two
weekends.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;verdict-at-1000-km&quot;&gt;Verdict at 1000 km&lt;/h2&gt;
&lt;p&gt;It’s a first “real” bike that doesn’t feel like a compromise and a second
bike that doesn’t feel like a step down. Servicing is cheap, the triple makes
every tunnel a concert, and the whole machine communicates so clearly that
you end up riding better than you are. The plan was to ride it for a season
and trade up. The plan is now under review.&lt;/p&gt;</content:encoded><category>motorcycles</category><category>offline</category></item><item><title>Why ClickHouse wants your inserts in batches</title><link>https://dethlex.com/garden/clickhouse-batch-inserts/</link><guid isPermaLink="true">https://dethlex.com/garden/clickhouse-batch-inserts/</guid><description>Every INSERT into MergeTree creates a part on disk. Feed it row-by-row and you trade write throughput for a merge storm — batch, or let async inserts batch for you.</description><pubDate>Wed, 18 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;🧪 &lt;strong&gt;Sample seed note.&lt;/strong&gt; Placeholder content demonstrating the garden structure — replace with your own text.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;MergeTree is not a B-tree you poke rows into. Every &lt;code&gt;INSERT&lt;/code&gt; becomes a brand-new
&lt;em&gt;part&lt;/em&gt; — a directory on disk with one file pair per column, plus checksums and
metadata. The table you query is the union of all active parts, and a background
process constantly merges them into bigger ones. That design is why ClickHouse
scans billions of rows per second. It is also why it punishes chatty writers.&lt;/p&gt;
&lt;h2 id=&quot;what-a-tiny-insert-actually-costs&quot;&gt;What a tiny insert actually costs&lt;/h2&gt;
&lt;p&gt;Insert one row and ClickHouse still pays the full price of a part: create the
directory, write every column file, fsync, register the part. On a &lt;code&gt;Replicated*&lt;/code&gt;
table it additionally writes a replication log entry that every replica must
fetch and apply. One row, dozens of filesystem operations, one round of
replication traffic.&lt;/p&gt;
&lt;p&gt;Do that a thousand times a second and two things happen:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Merges fall behind.&lt;/strong&gt; The merge scheduler was designed for parts with
thousands-to-millions of rows, not thousands of parts with one row.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The table throttles you.&lt;/strong&gt; Once a partition accumulates
&lt;code&gt;parts_to_delay_insert&lt;/code&gt; active parts (1000 by default), inserts get slowed
down; at &lt;code&gt;parts_to_throw_insert&lt;/code&gt; (3000) you start catching
&lt;code&gt;TOO_MANY_PARTS&lt;/code&gt; exceptions in production at the worst possible moment.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You can watch the pressure build up:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes phosphor-light phosphor-dark&quot; style=&quot;--shiki-light:#221b3a;--shiki-dark:#ebe5d9;--shiki-light-bg:#fbfafe;--shiki-dark-bg:#1b1730; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#96570A;--shiki-dark:#F2A03D&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;--shiki-light:#96570A;--shiki-dark:#F2A03D&quot;&gt; table&lt;/span&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#6D4B0F;--shiki-dark:#F5D7A3&quot;&gt;count&lt;/span&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt;() &lt;/span&gt;&lt;span style=&quot;--shiki-light:#96570A;--shiki-dark:#F2A03D&quot;&gt;AS&lt;/span&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt; active_parts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#96570A;--shiki-dark:#F2A03D&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;--shiki-light:#8F5406;--shiki-dark:#FFC46B&quot;&gt; system&lt;/span&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;--shiki-light:#8F5406;--shiki-dark:#FFC46B&quot;&gt;parts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#96570A;--shiki-dark:#F2A03D&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt; active&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#96570A;--shiki-dark:#F2A03D&quot;&gt;GROUP BY&lt;/span&gt;&lt;span style=&quot;--shiki-light:#96570A;--shiki-dark:#F2A03D&quot;&gt; table&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#96570A;--shiki-dark:#F2A03D&quot;&gt;ORDER BY&lt;/span&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt; active_parts &lt;/span&gt;&lt;span style=&quot;--shiki-light:#96570A;--shiki-dark:#F2A03D&quot;&gt;DESC&lt;/span&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If &lt;code&gt;active_parts&lt;/code&gt; for a hot table grows monotonically during peak traffic,
your writers are winning against the merges. They will not win for long.&lt;/p&gt;
&lt;h2 id=&quot;the-rule-of-thumb&quot;&gt;The rule of thumb&lt;/h2&gt;
&lt;p&gt;Aim for &lt;strong&gt;one insert per table per second, tens of thousands of rows per
insert&lt;/strong&gt;. The exact numbers are workload-dependent, but the shape of the rule
is not: fewer, fatter inserts are always cheaper than many thin ones. In our
pipelines the sweet spot landed around 50–100k rows or one second of
accumulation, whichever comes first.&lt;/p&gt;
&lt;h2 id=&quot;where-to-batch&quot;&gt;Where to batch&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;In the application.&lt;/strong&gt; A buffered channel and a ticker: flush on
&lt;code&gt;len(buf) &amp;gt;= N&lt;/code&gt; or every second. Full control over durability — you decide
what happens to the buffer on shutdown. This is what we run in the Go
consumers that land &lt;a href=&quot;https://dethlex.com/garden/kafka-partition-assignment/&quot;&gt;Kafka topics&lt;/a&gt;
into ClickHouse.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;async_insert = 1&lt;/code&gt;.&lt;/strong&gt; The server batches for you: inserts land in an
in-memory buffer and are flushed as one part. Turn on
&lt;code&gt;wait_for_async_insert&lt;/code&gt; if you want the acknowledgment to mean “data is in a
part” rather than “data is in a buffer” — with it off, a crash between
buffer and flush loses rows silently.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Buffer engine.&lt;/strong&gt; Same idea, table-shaped, older. It works, but the
buffer lives in RAM of the node and is lost on a hard restart, and it adds
one more table to reason about. I reach for it last.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-part-nobody-tells-you&quot;&gt;The part nobody tells you&lt;/h2&gt;
&lt;p&gt;Batching interacts with &lt;strong&gt;insert deduplication&lt;/strong&gt; on replicated tables: a
retried identical block is dropped by its checksum, which is exactly what you
want from at-least-once consumers — but only if your batches are
deterministic. Build them by offset range, not by wall clock, and a consumer
restart re-produces byte-identical blocks that dedup for free.&lt;/p&gt;
&lt;p&gt;Batch because the storage engine is built from parts. Everything else —
async inserts, Buffer tables, Kafka engine — is just someone else doing the
batching for you, with durability semantics you should read twice.&lt;/p&gt;</content:encoded><category>clickhouse</category><category>performance</category></item><item><title>Lock-free config reloads in Go with atomic.Pointer</title><link>https://dethlex.com/garden/go-atomic-swap-reload/</link><guid isPermaLink="true">https://dethlex.com/garden/go-atomic-swap-reload/</guid><description>When 100k RPS worth of goroutines read a lookup table that reloads every minute, RWMutex is the wrong tool. Build the new snapshot aside and swap one pointer.</description><pubDate>Fri, 15 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;🧪 &lt;strong&gt;Sample seed note.&lt;/strong&gt; Placeholder content demonstrating the garden structure — replace with your own text.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A recurring shape in high-load services: a lookup structure — deduplication
set, targeting rules, a denylist — that is &lt;strong&gt;read on every request and
replaced wholesale every N seconds&lt;/strong&gt; from some source of truth. Hundreds of
goroutines read it; exactly one goroutine writes it; the write is never an
edit, always a full replacement.&lt;/p&gt;
&lt;p&gt;That last property is the key that unlocks everything.&lt;/p&gt;
&lt;h2 id=&quot;why-not-rwmutex&quot;&gt;Why not RWMutex&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;sync.RWMutex&lt;/code&gt; is correct here and wrong anyway. Every &lt;code&gt;RLock&lt;/code&gt; is an atomic
RMW on shared state — at hundreds of thousands of reads per second across
many cores, readers contend with each other on the lock word itself, and the
cache line holding it ping-pongs between cores. You pay this tax on the hot
path to protect against a write that happens once a minute.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sync.Map&lt;/code&gt; solves a different problem (disjoint key sets, incremental
updates). For build-then-replace workloads it buys complexity, not speed.&lt;/p&gt;
&lt;h2 id=&quot;the-pattern&quot;&gt;The pattern&lt;/h2&gt;
&lt;p&gt;Treat the structure as an &lt;strong&gt;immutable snapshot&lt;/strong&gt;. Readers grab the current
one; the reloader builds the next one off to the side and swaps a pointer:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes phosphor-light phosphor-dark&quot; style=&quot;--shiki-light:#221b3a;--shiki-dark:#ebe5d9;--shiki-light-bg:#fbfafe;--shiki-dark-bg:#1b1730; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#96570A;--shiki-dark:#F2A03D&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;--shiki-light:#16655C;--shiki-dark:#8FC7BD&quot;&gt; Snapshot&lt;/span&gt;&lt;span style=&quot;--shiki-light:#96570A;--shiki-dark:#F2A03D&quot;&gt; struct&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt;	seen &lt;/span&gt;&lt;span style=&quot;--shiki-light:#96570A;--shiki-dark:#F2A03D&quot;&gt;map&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;--shiki-light:#96570A;--shiki-dark:#F2A03D&quot;&gt;uint64&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;--shiki-light:#96570A;--shiki-dark:#F2A03D&quot;&gt;struct&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;{}&lt;/span&gt;&lt;span style=&quot;--shiki-light:#8B84A8;--shiki-light-font-style:italic;--shiki-dark:#756C96;--shiki-dark-font-style:italic&quot;&gt; // never mutated after publish&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#96570A;--shiki-dark:#F2A03D&quot;&gt;var&lt;/span&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt; current &lt;/span&gt;&lt;span style=&quot;--shiki-light:#16655C;--shiki-dark:#8FC7BD&quot;&gt;atomic&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;--shiki-light:#16655C;--shiki-dark:#8FC7BD&quot;&gt;Pointer&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;--shiki-light:#16655C;--shiki-dark:#8FC7BD&quot;&gt;Snapshot&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#8B84A8;--shiki-light-font-style:italic;--shiki-dark:#756C96;--shiki-dark-font-style:italic&quot;&gt;// The zero value of atomic.Pointer is nil — seed it before the hot path&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#8B84A8;--shiki-light-font-style:italic;--shiki-dark:#756C96;--shiki-dark-font-style:italic&quot;&gt;// runs, or every request panics until the first successful reload.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#96570A;--shiki-dark:#F2A03D&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6D4B0F;--shiki-dark:#F5D7A3&quot;&gt; init&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt;	current&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6D4B0F;--shiki-dark:#F5D7A3&quot;&gt;Store&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;(&amp;#x26;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#16655C;--shiki-dark:#8FC7BD&quot;&gt;Snapshot&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt;seen&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;--shiki-light:#96570A;--shiki-dark:#F2A03D&quot;&gt; map&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;--shiki-light:#96570A;--shiki-dark:#F2A03D&quot;&gt;uint64&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;--shiki-light:#96570A;--shiki-dark:#F2A03D&quot;&gt;struct&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;{}{}})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#8B84A8;--shiki-light-font-style:italic;--shiki-dark:#756C96;--shiki-dark-font-style:italic&quot;&gt;// Hot path: one atomic load, zero locks, zero contention.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#96570A;--shiki-dark:#F2A03D&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6D4B0F;--shiki-dark:#F5D7A3&quot;&gt; Seen&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt;id &lt;/span&gt;&lt;span style=&quot;--shiki-light:#96570A;--shiki-dark:#F2A03D&quot;&gt;uint64&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;--shiki-light:#96570A;--shiki-dark:#F2A03D&quot;&gt; bool&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt;	_&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt; ok &lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt; current&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6D4B0F;--shiki-dark:#F5D7A3&quot;&gt;Load&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;().&lt;/span&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt;seen&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt;id&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#96570A;--shiki-dark:#F2A03D&quot;&gt;	return&lt;/span&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt; ok&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#8B84A8;--shiki-light-font-style:italic;--shiki-dark:#756C96;--shiki-dark-font-style:italic&quot;&gt;// Cold path: runs in one goroutine, e.g. on a ticker.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#96570A;--shiki-dark:#F2A03D&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6D4B0F;--shiki-dark:#F5D7A3&quot;&gt; reload&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt;ctx &lt;/span&gt;&lt;span style=&quot;--shiki-light:#16655C;--shiki-dark:#8FC7BD&quot;&gt;context&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;--shiki-light:#16655C;--shiki-dark:#8FC7BD&quot;&gt;Context&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt; src &lt;/span&gt;&lt;span style=&quot;--shiki-light:#16655C;--shiki-dark:#8FC7BD&quot;&gt;Source&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;--shiki-light:#96570A;--shiki-dark:#F2A03D&quot;&gt; error&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt;	next&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt; err &lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6D4B0F;--shiki-dark:#F5D7A3&quot;&gt; buildSnapshot&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt;ctx&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt; src&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;--shiki-light:#8B84A8;--shiki-light-font-style:italic;--shiki-dark:#756C96;--shiki-dark-font-style:italic&quot;&gt; // minutes of work allowed here&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#96570A;--shiki-dark:#F2A03D&quot;&gt;	if&lt;/span&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt; err &lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#8F5406;--shiki-dark:#FFC46B&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#96570A;--shiki-dark:#F2A03D&quot;&gt;		return&lt;/span&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt; err &lt;/span&gt;&lt;span style=&quot;--shiki-light:#8B84A8;--shiki-light-font-style:italic;--shiki-dark:#756C96;--shiki-dark-font-style:italic&quot;&gt;// keep serving the old snapshot — stale beats broken&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt;	current&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6D4B0F;--shiki-dark:#F5D7A3&quot;&gt;Store&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt;next&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#96570A;--shiki-dark:#F2A03D&quot;&gt;	return&lt;/span&gt;&lt;span style=&quot;--shiki-light:#8F5406;--shiki-dark:#FFC46B&quot;&gt; nil&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;atomic.Pointer&lt;/code&gt; (Go 1.19+) makes this typed and impossible to get wrong at
the call site. A reader that loaded the old snapshot finishes its request on
the old snapshot — perfectly consistent, just up to one reload stale. The
garbage collector retires the old map once the last such reader returns.&lt;/p&gt;
&lt;h2 id=&quot;the-fine-print&quot;&gt;The fine print&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Immutability is a contract, not a mechanism.&lt;/strong&gt; Nothing stops a confused
reader from writing to the map and racing every other goroutine. Keep the
map unexported, expose only query functions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory doubles during the swap.&lt;/strong&gt; Old and new snapshots coexist while the
build runs. For a 2 GB table, plan 4+ GB — or your “lock-free elegance”
becomes an OOMKilled pod at the worst hour.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Failed reloads must not clear state.&lt;/strong&gt; Return early, keep the old
pointer, and export the moment of last success:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;astro-code astro-code-themes phosphor-light phosphor-dark&quot; style=&quot;--shiki-light:#221b3a;--shiki-dark:#ebe5d9;--shiki-light-bg:#fbfafe;--shiki-dark-bg:#1b1730; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt;lastReload&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6D4B0F;--shiki-dark:#F5D7A3&quot;&gt;Set&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#96570A;--shiki-dark:#F2A03D&quot;&gt;float64&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#221B3A;--shiki-dark:#EBE5D9&quot;&gt;time&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6D4B0F;--shiki-dark:#F5D7A3&quot;&gt;Now&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;().&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6D4B0F;--shiki-dark:#F5D7A3&quot;&gt;Unix&lt;/span&gt;&lt;span style=&quot;--shiki-light:#5A5180;--shiki-dark:#A89FC4&quot;&gt;()))&lt;/span&gt;&lt;span style=&quot;--shiki-light:#8B84A8;--shiki-light-font-style:italic;--shiki-dark:#756C96;--shiki-dark-font-style:italic&quot;&gt; // prometheus gauge&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Alert when &lt;code&gt;time() - last_successful_reload_seconds&lt;/code&gt; exceeds a few reload
intervals — a service quietly serving week-old rules is the failure mode
nobody notices until money moves.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Writes on the hot path change the game.&lt;/strong&gt; If readers must also &lt;em&gt;mark&lt;/em&gt;
ids as seen, a read-only snapshot no longer fits — reach for sharded
mutexes or a pair of rotating generations instead. The atomic-swap trick is
for read-heavy, replace-rarely data. Know when you’ve left its territory.&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>go</category><category>concurrency</category><category>performance</category></item></channel></rss>