R1.03.3 · Write-behind — dive 3
Coalescing writes.
Twenty updates to one key between flushes should cost one database write, not twenty. The async buffer batched by flush; the dirty-set batches by key.
Grounding: echo_cache/journal.ex (record_many/2 group commit, compact/1) · coherence.ex (the 29-byte message) — verbatim. Engine: Valkey.
Keep each key once
write-behind.md.txt · AdvantagesThe async buffer batches by flush. Coalescing batches by key. The trick is the buffer’s
structure. A List records every change in order, so a key updated five times appears five times, and the
flush writes the database five times for that one key. A Set cannot hold a duplicate:
SADD a key that is already present and the Set is unchanged. So a dirty-set of changed keys
holds each key exactly once, no matter how often it changed.
The value itself is not stored on the buffer — only the key is marked dirty. The flush reads the
current value of each dirty key with a normal GET or HGETALL, so it always
carries the latest state, never an intermediate one. Twenty updates to one key leave one entry in the set and
write the database once with the final value.
# write path — mark the key dirty, store the value (the value, not the key, holds state) SET ecc:{limits}:LIM0NgWEfAEJfs {…} # store the latest value SADD dirty LIM0NgWEfAEJfs # mark dirty — a Set keeps it once however often it changes SADD dirty LIM0NgWEfAEJfs # the same key again -> Set unchanged, still one entry # flush path — one write per distinct key, with the current value SMEMBERS dirty # -> {"LIM0NgWEfAEJfs", "LIM0KHTOWnGLuC"} — each once GET ecc:{limits}:LIM0NgWEfAEJfs # read the latest value, then write the database once
- dirty-set
- A Valkey Set of changed keys.
SADDis idempotent, so each key is held once however many times it changed. - coalescing
- Collapsing many updates to one key into a single database write, carrying the latest value.
- latest-wins
- The flush reads the current value, not a queued one — so intermediate states never reach the source.
- write amplification
- The ratio of database writes to updates. A List keeps it at one; the dirty-set drops it toward the count of distinct keys.
Coalescing changes the durability story a little: the window holds only the latest value of each key, so a crash loses the most recent change, not a sequence of them. The trade itself is the one R1.03.2 measured.
Coalesce a stream of updates
write-behind.md.txt · write coalescingFeed updates into the dirty-set one at a time and watch it hold each key once. The keys come from a fixed
repeating stream where one key is hot. Each update does an SADD; flush writes
the database one round per distinct key and clears the set. The readout reports updates seen, the distinct
keys held, and the coalescing ratio — how many updates one database write absorbed.
A hot key updated again and again still costs one database write per flush. The dirty-set turns update volume into a count of distinct keys.
Coalescing on EchoCache — group commit at the writer’s edge
journal.ex:57 · :106 · coherence.ex:35EchoCache coalesces in two places. At the cache, the dirty-set keeps each changed key once — the
Valkey structure above. At the journal, the equivalent is record_many/2: “group commit at
the writer’s edge — record a batch of intents inside one transaction, one WAL append amortized
across the batch.” Newer-wins makes a re-applied version harmless, so two intents on the same name
collapse to one effect: applying the same version twice is a comparison that answers stale the second time.
compact/1 then retires the outbox by coverage — an intent is gone when its name carries an
applied version at least as new.
The coherence message is the smallest possible cargo for this: id <> ":" <> version
— 14 bytes of branded name, a colon, 14 bytes of branded version, 29 bytes total.
# group commit at the writer's edge — one WAL append across the batch {:ok, seqs} = Journal.record_many(:limits_journal, triples) # the 29-byte coherence message — id <> ":" <> version (14 + 1 + 14)
The 29-byte message rides EchoMQ
The coalesced obligation travels as the 29-byte coherence message over EchoMQ’s fair lanes,
at-least-once. The lane mechanics — the braced emq:{q}: keyspace, the claim and complete
verbs — are taught rung by rung in the EchoMQ course.
Mark a key dirty instead of queuing each change; a Set keeps it once. The flush reads the latest value and writes the source one time per distinct key.
record_many/2 group-commits a batch of intents in one WAL append, and newer-wins collapses
repeats on a name to one effect — the 29-byte coherence message is the cargo.
Notes on ValkeySADD returns the count of members actually
added, so a re-add of a present key returns 0 and the Set is unchanged — the idempotence the dirty-set
relies on (valkey.io/commands/sadd).
Coalescing is the second saving of write-behind: the buffer batches by flush, the dirty-set batches by key, and a burst on one record costs one write.
Recap — the whole module
buffer · window · coalescingWrite-behind acknowledges a write once the cache holds it, defers the source write to a background flush (the async buffer), accepts a bounded window where the cache holds the only copy (the durability trade-off), and collapses repeated writes to one key into a single flush (coalescing). A burst that would have been a database write per update becomes one batch, one write per hot key — throughput up front, the cost deferred and bounded. Return to the hub to revisit any dive.
This closes R1.03 · Write-behind. Return to the module hub or the Caching chapter to continue.
References
sources · relatedSources
- Valkey — SADD — adds a member to a Set; idempotent, so a key is held once however often it changes.
- Valkey — SMEMBERS — reads every distinct dirty key on the flush, each once.
- Redis — Documentation — Sets and Lists, and choosing the structure that fits the shape of the problem.
- Sanfilippo, S. — antirez weblog — the Redis creator on choosing the data structure that fits the problem.
Related in this course
- R1.03.2 · The durability trade-offthe previous dive
- R1.03 · Write-behindthe module hub
- R1 · Cachingthe chapter
- /echomqthe fair-lanes job queue the 29-byte message rides