Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -539,13 +539,13 @@ A group id must outlive the replica object that minted it — a receiver buckets

**A filter that withholds a member destrands the survivors.** Redaction is per op: a doc-ACL read verdict, a zone scope, or a migration rewrite drops individual members out of a batch. The rest then carry a count their bucket can never reach, so a recipient holds them against a member that will never arrive — invisible to it forever, and still counted among the ids it holds. Every seam that withholds a member therefore delivers the group's survivors untagged, so they merge standalone. Delivering them is the convergence requirement: every op a recipient may receive has to reach it, or it diverges from the correct projection of the sender's state. The atomic view is lost at such a recipient, unavoidably — it cannot see the member that was withheld — and the ops still merge. A group a filter carries whole keeps its tags and stays atomic. One seam cannot follow the rule: a read projection of a snapshot has no per-op verdict to apply to buffered ops, whose paths may not resolve at all, so it drops the buffer entire rather than destranding it — the survivors go with the withheld member.

**A rewritten `count` is a memory-retention instruction, and the answer to it is eviction, not a repair rule.** `count` tells the receiver to hold the group's members until that many arrive, so a size no arrival meets tells it to hold them for the life of the replica — and the buffer rides the state encoding, so the next replica holds them too. Two judgements over a member's own envelope are safe to make locally. A size outside the cap is **refused** where it arrives — at the decode boundary, and at the apply seam an in-process caller reaches without crossing one — because no honest sender mints one, the judgement is on that member alone, and refusing holds nothing. And a group's size is what its members *agree* it is: read off whichever member the buffer happens to hold first, a rewritten count decides when the group commits, so a bucket whose members disagree names no group and is never complete. Unanimity is judged over the members that have *arrived*, so it bounds a rewrite rather than closing it — a unanimous **subset** can reach its own declared count before the dissenting member lands, which is a further shape of the same defect the record below closes for its own three.
**A rewritten `count` is a memory-retention instruction, and the answer to it is eviction, not a repair rule.** `count` tells the receiver to hold the group's members until that many arrive, so a size no arrival meets tells it to hold them for the life of the replica — and the buffer rides the state encoding, so the next replica holds them too. Two judgements over a member's own envelope are safe to make locally. A size outside the cap is **refused** where it arrives — at the decode boundary, and at the apply seam an in-process caller reaches without crossing one — because no honest sender mints one, the judgement is on that member alone, and refusing holds nothing. And a group's size is what its members *agree* it is: read off whichever member the buffer happens to hold first, a rewritten count decides when the group commits, so a bucket whose members disagree names no group and can never complete. Holding such a bucket is not the answer to that, because unanimity is judged over the members that have *arrived*: a unanimous **subset** reaches its own declared count and commits before the dissenting member lands, so whether a bucket ever holds the disagreement at all belongs to the arrival order, and one op set folds to two states. A disagreement therefore **spends the bucket key** exactly where a commit spends it (the record below) — the members it holds are released untagged, and every member still to come is a stray of a resolved group. What that costs is the atomic view of a group a rewrite has already made unservable, which is the price the record pays for the same reason.

Neither of those reaches a rewrite consistent across every member: a group of three retagged to declare two is, to a receiver, an honest group of two followed by a stray. It commits at the size it was told, and *which* members that is belongs to the arrival order, so the third is left holding a size its bucket has already met and no arrival can meet again. Two more shapes land in the same place: an unrelated op of the same author carrying a live group's id, and two copies of one op id carrying envelopes that disagree — in group id or in declared size — where the bucket reads whichever the dedup kept. What is common to them is that a bucket key is *consumed* when it resolves, so a late member of a resolved group is indistinguishable from the first member of a fresh one — and the replica folds one op set to two states.

So the judgement that closes them is over the group rather than the member: **record the key**. A `(author, group id)` set marks a bucket resolved, and a member arriving under a resolved key is untagged and merges standalone. A key is spent at each of the four points a bucket resolves: when it **commits**; when the author **mints** the group, since the author applies its own edits as it makes them and buckets nothing, so without this it would hold a stray every receiver merged; when **eviction** gives up on it, or a member arriving after one would wait on a group the replica has already released, and two replicas on one policy would disagree over nothing but which had ticked first; and when a member arrives naming a group other than the one the buffer is holding that same id under, which spends **both** — only one of the two can ever hold the id, and which one is the arrival order's. Each of the three shapes then lands the same op set from every arrival order, which is what the law asks; what it costs is the atomic *view* of a group a rewrite has already made unservable, and only for the members that follow the commit. The record is persisted, carried in the state encoding beside the buffer it rules — a group resolved before a restart is one whose stray still has to land after it — and every entry is charged to a bucket the replica held, committed, evicted or minted, so it is bounded by the ops it holds rather than by what arrives.
So the judgement that closes them is over the group rather than the member: **record the key**. A `(author, group id)` set marks a bucket resolved, and a member arriving under a resolved key is untagged and merges standalone. A key is spent at each of the five points a bucket resolves: when it **commits**; when its members **disagree**, since a bucket without unanimity completes on no delivery (above); when the author **mints** the group, since the author applies its own edits as it makes them and buckets nothing, so without this it would hold a stray every receiver merged; when **eviction** gives up on it, or a member arriving after one would wait on a group the replica has already released, and two replicas on one policy would disagree over nothing but which had ticked first; and when a member arrives naming a group other than the one the buffer is holding that same id under, which spends **both** — only one of the two can ever hold the id, and which one is the arrival order's. Each of the three shapes then lands the same op set from every arrival order, which is what the law asks; what it costs is the atomic *view* of a group a rewrite has already made unservable — for the members that follow a commit, and for every member of a bucket that disagrees. The record is persisted, carried in the state encoding beside the buffer it rules — a group resolved before a restart is one whose stray still has to land after it — and every entry is charged to a bucket the replica held, committed, evicted or minted. Its bound is the **dedup set**, not the buffer: a key outlives the ops that earned it exactly as a `seen` entry does, so an eviction empties the buffer and keeps the key, and what caps the record is that each entry costs the sender fresh op ids it can never re-spend.

Two rules the record deliberately does not take. It does not read a member whose id is merely **applied**: a resend is ordinary traffic on every transport that retries, so a delivery that spent a key would make state a function of how often an op arrived rather than of which ops did — the same law, broken in the other dimension. And it does not release a bucket the moment it *looks* unreachable, because whether it looks that way is a function of which members have landed, so replicas served the same ops in different orders would release different sets. What those two leave is three further shapes. Two are order-dependent before the record existed too; the third the record itself opens, because the record is per-replica *evidence* and a destranding seam destroys the evidence at exactly the recipients it serves. First, a second envelope of one id that the buffer holds **nothing tagged** to contradict — because the other copy already committed out of the buffer under a different group, or because it carries **no** tag at all, which is what a filtering seam's destranding produces. The honest group's own member is then left holding on an id that will never join it. Its members converge on eviction; which keys each replica has spent does not, and on some arrival orders the record adds a *third* reading where the un-recorded replica had two — the conflict rule fires on the orders that buffer the disagreeing copy and not on the rest. Measured against the un-recorded replica over 392 forged pools it is better on 156 and worse on 4, so the record improves this shape without closing it. And a **minority** count rewrite, where the members left unanimous are exactly as many as they now declare: that subset commits and the dissenter lands as a stray, or the dissenter arrives first and the bucket names no group at all. Closing the second means a disagreeing bucket spends its key rather than merely never completing, which is a change to what unanimity *is* and wants its own decision. And third: a recipient served a group **destranded** never buckets it, so it never spends the key, while the author spends it at the mint and a whole-delivery recipient spends it on commit — a later stray under that id then merges at those two and is held at the destranded one, where before the record all three held it alike. Eviction closes it; the destranding seams knowing the keys they cut is the other way, and it has to answer what a projection may reveal about a group that straddles its cut. A projection drops the record whole: a key names an author and a group, never a partition, so a kept one would count the groups a withheld partition resolved — the same inference the causal-frontier scrub closes.
Two rules the record deliberately does not take. It does not read a member whose id is merely **applied**: a resend is ordinary traffic on every transport that retries, so a delivery that spent a key would make state a function of how often an op arrived rather than of which ops did — the same law, broken in the other dimension. And it does not release a bucket that merely *looks* unreachable on the count it is short of, because whether the members it lacks will ever come is a function of what has not arrived, so replicas served the same ops in different orders would release different sets. A **disagreement** is not that: two members the buffer already holds contradicting each other is a property of what *has* landed and no arrival repairs it, so spending that key is the rule above rather than an exception to this one. What those two leave is two further shapes. One is order-dependent before the record existed too; the other the record itself opens, because the record is per-replica *evidence* and a destranding seam destroys the evidence at exactly the recipients it serves. First, a second envelope of one id that the buffer holds **nothing tagged** to contradict — because the other copy already committed out of the buffer under a different group; because it carries **no** tag at all, which is what a filtering seam's destranding produces; or because the other copy was **released by a disagreement**, the door the rule above opens, where whether the buffer still holds it when the second envelope lands is the arrival order's. The honest group's own member is then left holding on an id that will never join it. Its members converge on eviction; which keys each replica has spent does not, and on some arrival orders the record adds a *third* reading where the un-recorded replica had two — the conflict rule fires on the orders that buffer the disagreeing copy and not on the rest. Measured against the un-recorded replica over 392 forged pools it is better on 156 and worse on 4, so the record improves this shape without closing it. And second: a recipient served a group **destranded** never buckets it, so it never spends the key, while the author spends it at the mint and a whole-delivery recipient spends it on commit — a later stray under that id then merges at those two and is held at the destranded one, where before the record all three held it alike. Eviction closes it; the destranding seams knowing the keys they cut is the other way, and it has to answer what a projection may reveal about a group that straddles its cut. A projection drops the record whole: a key names an author and a group, never a partition, so a kept one would count the groups a withheld partition resolved — the same inference the causal-frontier scrub closes. A key spent on a **disagreement** is dropped there too, and reaches that recipient without any cut at all, so the same seam owes the same answer for a group that never straddled it.

So the residue is what no local judgement separates: a member that never arrives looks exactly like one still in flight. The replica exposes a way to give up rather than a rule — **eviction** untags every group still waiting, and how long to wait first is the caller's policy, the core reading no clock. Eviction untags rather than discards for the reason a filter destrands its survivors: the members are ops the replica holds and no peer will send again, so dropping them diverges, while untagging costs only the atomic view a group that never completes was never going to deliver. A replica that never evicts holds those members and does not converge with one that does — which is what makes eviction a policy every deployment runs, not an optional cleanup. Eviction **spends the bucket key** it gives up on, which is what makes a bare period over a seam with no notion of a bucket's age safe for a caller that keeps ticking: a tick landing between two members of an *honest* group untags it, and the member that follows is then a stray of a key the replica has already spent rather than the first member of a fresh group, so replicas ticking out of phase converge on the state a reader sees rather than diverging on it. What remains is narrower and still real — a replica that never evicts at all does not converge with one that does; a single tick placement can still leave a group's member unread until the next tick, so it is a policy that repeats rather than one tick that carries the guarantee; and the buffer residue of members untagged but not yet ready still differs by tick placement, so snapshot bytes are not preserved even where the reading is.

Expand Down
Loading
Loading