skip to content

How does a declared read set that abandons an operation group differ from a version token presented on one write?

level: middleimportance: should knowfreq 48%

answer

  1. same bet, two scopes
  2. one entry versus several named entries
  3. abandoned before any step applied
  4. a group is not a transaction
  5. co-located, or not available at all

basics

~20 s

A version token guards one entry as one write is applied. A declared read set guards every entry the caller named, written or not, abandoning a queued operation group before any step applies. Same bet, wider scope.

solid answer

~50 s

Both are the same bet - declare what the write depends on, hold nothing, let the store check the declaration at apply time, throw the attempt away if it no longer holds. The `version token` makes that declaration implicitly and narrowly: this write depends on this one entry being as I read it, and a mismatch refuses that write. A `declared read set` widens it: the caller names entries, reads them, decides, queues an **operation group**, and the store abandons the group before applying any step if a named entry changed. The widening buys two things a token cannot give - the checked entries need not be the written ones, and one decision can cover several entries. It costs a grouping construct, which several stores in this class do not offer, and co-location, because the unit of atomicity is one node's keyspace. Neither has rollback: abandoned means nothing ran.

go deeper

for a junior

Learn the two shapes by what they check: a token checks the single entry you are writing, a declared read set checks every entry you named. Both mean the same thing on failure - your attempt did not happen, so read again and redo it.

for a middle

Be able to say why a read set exists at all: it lets the check cover entries the write never touches, which is the one thing a per-entry token structurally cannot do. Then name its two prices - a grouping construct, and co-location.

for a senior

Distinguish the three failure moments out loud - abandoned before anything applied, refused on one write, errored part-way through a group that was applying. Only the third leaves a repair for the caller, and conflating them produces repair code that runs when nothing broke.

for a principal

Treat the choice as scope of enforceable invariant, not preference. What the tier can enforce is one entry, or a co-located set on one node. Anything broader is an invariant you have decided to enforce elsewhere, whether you have said so or not.

## One idea, two shapes Both mechanisms are optimistic concurrency control: **declare what your write depends on, hold nothing, let the store check the declaration at the moment the write would apply, and discard the attempt if the declaration no longer holds.** Nothing is locked, no other caller is made to wait, and the price of a conflict is paid in wasted work rather than in waiting. What differs between the two is the *scope* of the declaration and the *shape* of the failure the caller sees. ## The version token A **version token** is an opaque marker the store hands back with a value and changes on every write to that entry. The caller reads value and token together, computes, and issues a write carrying the token. The store compares, and refuses the write if the entry has moved since. The declaration is implicit and narrow: *this write depends on this one entry being exactly as I read it.* That is the right tool when the decision depends on the entry about to be written and on nothing else - a number that must be read before it can be decided, a small document a service rewrites whole, a status field that may only advance from one specific value to another. ## The declared read set A **declared read set** widens the declaration. The caller names entries before reading them, reads them, decides, then queues an **operation group** - a group of operations the store applies as one unit that no other caller interleaves. At apply time the store checks the named entries, and if any of them changed since it was named, the group is **abandoned before any step is applied**. The widening matters in two ways. First, the entries you *check* need not be the entries you *write*: a caller can read a membership set and a quota count and, on the strength of both, write a third entry. A token cannot do that, because a token is presented on a write to the entry it came from. Second, one decision can cover several writes at once. The costs are real. The store must offer a grouping construct at all, and several stores in this class offer none - on those, the multi-step remedies available are a submitted program or an expiring claim. And where the keyspace is split across nodes, every declared and written entry must live on the same node, because the unit of atomicity here is one node's keyspace, not the cluster. | | version token | declared read set | |---|---|---| | what is checked | the one entry read and written | every entry named, written or not | | when the check happens | as the single write is applied | as the group is applied | | what failure looks like | that write is refused | the group is abandoned, no step applied | | needs a grouping construct | no | yes | | covers several entries | one at a time | yes, if they are co-located | | where it exists | stores exposing a per-entry token | stores offering both a read set and a group | ## Neither one is a transaction The word to avoid is *rollback*. An abandoned group has nothing to roll back - the store decided before applying anything, so there is no partial effect to undo. That is a different event from a group that started applying and had one step fail on its own terms, which leaves the earlier steps applied and makes the caller responsible for the repair. Three outcomes look identical in a sequence diagram and behave nothing alike in code: - **Abandoned**: a declared entry changed; no step applied; re-read and try again. - **Refused**: a conditional write's token did not match; nothing applied; re-read and try again. - **Errored while applying**: the unit went through and one step failed; the earlier steps stand; the caller owns the repair. And a fourth outcome is the one with no signal at all: where neither mechanism is used, the second write simply lands and **last-writer-wins** silently. In none of these is there an isolation level to raise, a save point to set, or a snapshot to read from. Those belong to an engine that can open a transaction; this tier has the remedies described here and no others. ## Choosing between them - If the decision and the write concern **one entry**, prefer the token: it is the smaller mechanism, it needs no grouping construct, and it exists on more stores. - If the decision reads **entries the write will not touch**, the read set is the only one of the two that fits. - If the entries are **not co-located**, neither helps; the invariant has to be enforced somewhere that can see all of them at once. - If the entry is **hot**, both degrade identically - every conflicting attempt is discarded work - and the answer is a different remedy, not a different flavour of the same one.

  • Can a caller declare an entry it never reads?
    Yes, and it is occasionally useful - naming an entry makes the group's fate depend on that entry standing still, whether or not its value informed the decision. A generation marker bumped whenever a configuration changes is the usual case: declare it, and any group built under the old configuration is abandoned rather than applied. The cost is more ways to be abandoned under load.
  • What do you do on a store that offers no grouping construct at all?
    The multi-step remedies collapse to two. A submitted program reads, decides and writes at the store as one unit, which closes the read-then-decide gap without any grouping construct. Or an expiring claim makes callers take turns on the entry instead of racing. A per-entry version token still works for single-entry decisions, which covers more cases than people expect.
  • If the group was abandoned, from where does the caller re-read?
    From the store, after declaring the entries again. The declaration is consumed by the attempt, so a retry is a whole new cycle: declare, read, decide, queue, apply. Re-using values read during the abandoned attempt reintroduces exactly the staleness that caused the abandonment.

saying these in an interview costs you the question

  • Calls the operation group a transaction with rollback.
  • Says earlier steps are undone when a group is abandoned.
  • Thinks a version token can guard an entry it does not write.
  • Assumes every in-memory store offers a grouping construct.
  • Expects a multi-entry check to work across separate nodes.
  • Confuses abandonment with a step erroring mid-apply.