Skip to content

Protect Cassandra snapshot writes from stale writers (full compare-and-set fence) - #834

Draft
tobiajo wants to merge 3 commits into
evolution-gaming:masterfrom
tobiajo:tj/address-partition-ownership-overlap-possiblity-cassandra
Draft

Protect Cassandra snapshot writes from stale writers (full compare-and-set fence)#834
tobiajo wants to merge 3 commits into
evolution-gaming:masterfrom
tobiajo:tj/address-partition-ownership-overlap-possiblity-cassandra

Conversation

@tobiajo

@tobiajo tobiajo commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Stacked on (merge first): #838

Problem

The persist-only fence (#838) leaves delete last-write-wins, so a stale writer can still revive a just-deleted key. Events recovery has its own replay-window exposure: a journal fold could regress a key below an existing snapshot's offset and revive it.

Fix

Extend the compare-and-set to the full fence: persist and delete are offset-fenced. A fenced delete always writes an offset-carrying tombstone — even for a never-persisted key — so a later replay of an already-deleted record cannot revive it. Events recovery seeds the replay-window floor from the snapshot store and routes initPersisted through the monotonic cell, closing the journal revive. A guard-expired row is read as absent and repaired via IF offset = null.

The store API changes (breaking): the persist/delete write pair and the get/recover read pair collapse into one Stored[S] ADT — a live snapshot or an offset-carrying tombstone — so the read, the write and the per-key buffer reason about one monotonic cell.

Tests and docs

The compare-and-set suite, the replay-fencing spec (a replayed delete after its tombstone was TTL-reaped stays a fence), the per-key serialization race (the flushCell lost-write), the read-state floor gate, and the TTL-edge timing; the Cassandra ITs pin the image above the mode's version floor. The Cassandra design doc is made self-consistent for the full fence — including why the consumer generation cannot replace the offset as the fence token; persistence.md and the kafka design doc get matching updates.

@tobiajo
tobiajo force-pushed the tj/address-partition-ownership-overlap-possiblity-cassandra branch 11 times, most recently from f5dda65 to 6e8480b Compare June 22, 2026 23:21
@tobiajo

tobiajo commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Liveness gap: a tombstoned key self-fences in the CAS replay window

The replay-window fix needs the recovered offset X to seed the buffer's high-water. A tombstone doesn't give one — decode reads null value back as None, and Persistence.read only initPersisteds a Some — so the buffer starts empty, SnapshotFold's record.offset > snapshot.offset is bypassed on None, and a flush mid-replay persists at Oᵢ < X. The IF offset <= :offset guard rejects it → SnapshotWriteConflict out of the PartitionFlow, the owner re-recovers None on restart and loops. Safety holds (X never regresses); liveness only.

Confirmed with a TestControl repro extending SnapshotReplayFencingSpec (branch claude/tombstone-replay-liveness-s2cy1y, b1eba24): the same flush-during-replay that passes for a live snapshot self-fences for a tombstone; a reaped-tombstone control pins the cause to the lingering offset, not the None recovery. One correction — the delete path is shadowed (after None recovery nothing is persisted, so the tick-delete goes out persist=false and never hits the store); the persist path is the only one that fires.

Cassandra.tla (#835) hides this: OwnerRecover/Handover set recoveredAt = store.offset unconditionally, so a Tomb(o)-recovered owner already "knows" X. Giving a tombstone recovery no floor should make RefLive fail, like cassandra_replay_fixoff. A fix would span the stack — the model in #835 and the recovery code here.

@tobiajo

tobiajo commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

The Cassandra part that initially looked trivial with lightweight transactions turned out to have many pitfalls. What makes it harder and a fix much more invasive is the lost offset when deleting. There are possible workarounds, but not without changing the APIs and likely also having an inner ADT representing snapshot/deleted with offset/null. Different paths for persist and delete also make it more difficult.

I'm starting to consider less safety as an alternative and skip asserting offset on deletes. This is starting to spiral out of control with all the invasive changes. Maybe better to wait for https://cwiki.apache.org/confluence/display/KAFKA/KIP-939%3A+Support+Participation+in+2PC and in the meantime compare-and-set (CAS) only for persist, or simply hold back.

@tobiajo
tobiajo force-pushed the tj/address-partition-ownership-overlap-possiblity-cassandra branch 12 times, most recently from d65d74f to 52aae03 Compare June 27, 2026 13:27
tobiajo added a commit to tobiajo/kafka-flow that referenced this pull request Jun 27, 2026
…nt fixes

Follow-up fixes for the Cassandra compare-and-set review:

- Skip the events-recovery snapshot-store floor read for unfenced
  (last-write-wins) buffers: add `Snapshots.fenced` and gate the
  3-arg `ReadState` on it, so only a fencing buffer pays the per-key
  read at recovery (#1).
- Deserialize snapshots with the snapshot's topic in
  `CassandraSnapshots.decode`, symmetric with the persist path (#4).
- Soften the spurious-first-write-conflict comment to say it fails and
  re-recovers, and is effectively unreachable in pure CAS mode (#2).
- Fix the IT comment referencing a non-existent "CasFirstWrite model"
  and a stale `SnapshotDatabase.recover`/`Recovered.Deleted` reference (#6).
- Add unit tests for equal-offset re-persist on a fenced cell (#7) and
  an at/above-floor events-recovery counterpart (#8).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHaexsSAhmWVb2M9wQf6Bd
@tobiajo tobiajo changed the title Protect Cassandra snapshot writes from stale writers (compare-and-set) Protect Cassandra snapshot writes from stale writers (compare-and-set) [WITH SAFE RESURRECTION; MOST PROBABLY REJECTED] Jun 27, 2026
@tobiajo tobiajo changed the title Protect Cassandra snapshot writes from stale writers (compare-and-set) [WITH SAFE RESURRECTION; MOST PROBABLY REJECTED] Protect Cassandra snapshot writes from stale writers (compare-and-set) [WITH SAFE RESURRECTION; SKIP] Jun 27, 2026
@tobiajo
tobiajo force-pushed the tj/address-partition-ownership-overlap-possiblity-cassandra branch from 72760ae to 52c96ce Compare June 27, 2026 20:31
tobiajo added a commit to tobiajo/kafka-flow that referenced this pull request Jul 3, 2026
The PR history's core learning (evolution-gaming#834): the
conditional write looked trivial, the pitfalls were all in not fencing
the legitimate owner. Say that up front so the doc's weight makes sense.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGxViqVaLYb1xQyveED6Hs
tobiajo added a commit to tobiajo/kafka-flow that referenced this pull request Jul 3, 2026
The PR history's core learning (evolution-gaming#834): the
conditional write looked trivial, the pitfalls were all in not fencing
the legitimate owner. Say that up front so the doc's weight makes sense.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGxViqVaLYb1xQyveED6Hs
tobiajo added a commit to tobiajo/kafka-flow that referenced this pull request Jul 3, 2026
The PR history's core learning (evolution-gaming#834): the
conditional write looked trivial, the pitfalls were all in not fencing
the legitimate owner. Say that up front so the doc's weight makes sense.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGxViqVaLYb1xQyveED6Hs
tobiajo added a commit to tobiajo/kafka-flow that referenced this pull request Jul 4, 2026
The PR history's core learning (evolution-gaming#834): the
conditional write looked trivial, the pitfalls were all in not fencing
the legitimate owner. Say that up front so the doc's weight makes sense.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGxViqVaLYb1xQyveED6Hs
tobiajo added a commit to tobiajo/kafka-flow that referenced this pull request Jul 4, 2026
The PR history's core learning (evolution-gaming#834): the
conditional write looked trivial, the pitfalls were all in not fencing
the legitimate owner. Say that up front so the doc's weight makes sense.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGxViqVaLYb1xQyveED6Hs
tobiajo added a commit to tobiajo/kafka-flow that referenced this pull request Jul 4, 2026
Restructure docs/cassandra-single-writer-design.md to focus on why over
what: brief mechanics, explicit design choices, and the learnings from
the deferred full solution, mirroring the Kafka design doc's shape
(Problem, mechanism, Testing, Rejected alternatives, Forward-looking):

- Problem now explains why the Kafka fix does not transfer to Cassandra
  (no transaction to bind the offset commit into, no ownership
  authority), motivating a per-write, per-key fence.
- The mechanism is kept brief and its three load-bearing choices are
  called out explicitly: per-key granularity, `<=` over `<`, and
  ordering by data rather than identity.
- "Why the store is the whole change" makes the SnapshotFold replay
  dedup explicit as the property that lets persist-only ship without
  core changes.
- A Compatibility section collects the no-API-change / no-migration /
  write-side-only-fence consequences.
- The deferred full solution (offset-gated deletes, PR evolution-gaming#834) is retold
  as a forced chain - tombstone, breaking delete(key, offset) API,
  monotonic buffer, tombstone-floor recovery read, independent
  events-recovery seeding - with a livelock diagram, the TLA+ results
  (all defects were liveness, never safety; models in PR evolution-gaming#835), and the
  explicit deferral rationale (cost/benefit and KIP-939 timing).

Also make the rolling-deploy clock-skew caveat in persistence.md
concrete (coordinator vs client write timestamps), and update a test
comment referencing a renamed doc section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018jfVxUNrjxpcSFF2hgiar5
tobiajo added a commit to tobiajo/kafka-flow that referenced this pull request Jul 4, 2026
The PR history's core learning (evolution-gaming#834): the
conditional write looked trivial, the pitfalls were all in not fencing
the legitimate owner. Say that up front so the doc's weight makes sense.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGxViqVaLYb1xQyveED6Hs
tobiajo added a commit to tobiajo/kafka-flow that referenced this pull request Jul 4, 2026
The PR history's core learning (evolution-gaming#834): the
conditional write looked trivial, the pitfalls were all in not fencing
the legitimate owner. Say that up front so the doc's weight makes sense.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGxViqVaLYb1xQyveED6Hs
tobiajo added a commit to tobiajo/kafka-flow that referenced this pull request Jul 4, 2026
- §1.1 'What ships: the persist-only subset': the study audits the full
  (deferred, evolution-gaming#834) fence, but the shipped artifact is persist-only - persist
  offset-fenced, delete plain LWW; evolution-gaming#732 closed for persists, left open for
  deletes (the documented residual, modelled by cassandra_notomb + tested by
  SnapshotSpec). Records what actually ships, not only the deferred design.
- claims R6: SnapshotFold's filter is the *primary* liveness mechanism in the
  shipped persist-only mode (no monotonic buffer), not belt-and-suspenders, and
  it is unit-tested (SnapshotFoldSpec) - graded code+test.
tobiajo added a commit to tobiajo/kafka-flow that referenced this pull request Jul 4, 2026
The PR history's core learning (evolution-gaming#834): the
conditional write looked trivial, the pitfalls were all in not fencing
the legitimate owner. Say that up front so the doc's weight makes sense.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGxViqVaLYb1xQyveED6Hs
tobiajo added a commit to tobiajo/kafka-flow that referenced this pull request Jul 4, 2026
- §1.1 'What ships: the persist-only subset': the study audits the full
  (deferred, evolution-gaming#834) fence, but the shipped artifact is persist-only - persist
  offset-fenced, delete plain LWW; evolution-gaming#732 closed for persists, left open for
  deletes (the documented residual, modelled by cassandra_notomb + tested by
  SnapshotSpec). Records what actually ships, not only the deferred design.
- claims R6: SnapshotFold's filter is the *primary* liveness mechanism in the
  shipped persist-only mode (no monotonic buffer), not belt-and-suspenders, and
  it is unit-tested (SnapshotFoldSpec) - graded code+test.
tobiajo added a commit to tobiajo/kafka-flow that referenced this pull request Jul 4, 2026
- §1.1 'What ships: the persist-only subset': the study audits the full
  (deferred, evolution-gaming#834) fence, but the shipped artifact is persist-only - persist
  offset-fenced, delete plain LWW; evolution-gaming#732 closed for persists, left open for
  deletes (the documented residual, modelled by cassandra_notomb + tested by
  SnapshotSpec). Records what actually ships, not only the deferred design.
- claims R6: SnapshotFold's filter is the *primary* liveness mechanism in the
  shipped persist-only mode (no monotonic buffer), not belt-and-suspenders, and
  it is unit-tested (SnapshotFoldSpec) - graded code+test.
tobiajo added a commit to tobiajo/kafka-flow that referenced this pull request Jul 4, 2026
- §1.1 'What ships: the persist-only subset': the study audits the full
  (deferred, evolution-gaming#834) fence, but the shipped artifact is persist-only - persist
  offset-fenced, delete plain LWW; evolution-gaming#732 closed for persists, left open for
  deletes (the documented residual, modelled by cassandra_notomb + tested by
  SnapshotSpec). Records what actually ships, not only the deferred design.
- claims R6: SnapshotFold's filter is the *primary* liveness mechanism in the
  shipped persist-only mode (no monotonic buffer), not belt-and-suspenders, and
  it is unit-tested (SnapshotFoldSpec) - graded code+test.
tobiajo added a commit to tobiajo/kafka-flow that referenced this pull request Jul 4, 2026
Restructure docs/cassandra-single-writer-design.md to focus on why over
what: brief mechanics, explicit design choices, and the learnings from
the deferred full solution, mirroring the Kafka design doc's shape
(Problem, mechanism, Testing, Rejected alternatives, Forward-looking):

- Problem now explains why the Kafka fix does not transfer to Cassandra
  (no transaction to bind the offset commit into, no ownership
  authority), motivating a per-write, per-key fence.
- The mechanism is kept brief and its three load-bearing choices are
  called out explicitly: per-key granularity, `<=` over `<`, and
  ordering by data rather than identity.
- "Why the store is the whole change" makes the SnapshotFold replay
  dedup explicit as the property that lets persist-only ship without
  core changes.
- A Compatibility section collects the no-API-change / no-migration /
  write-side-only-fence consequences.
- The deferred full solution (offset-gated deletes, PR evolution-gaming#834) is retold
  as a forced chain - tombstone, breaking delete(key, offset) API,
  monotonic buffer, tombstone-floor recovery read, independent
  events-recovery seeding - with a livelock diagram, the TLA+ results
  (all defects were liveness, never safety; models in PR evolution-gaming#835), and the
  explicit deferral rationale (cost/benefit and KIP-939 timing).

Also make the rolling-deploy clock-skew caveat in persistence.md
concrete (coordinator vs client write timestamps), and update a test
comment referencing a renamed doc section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018jfVxUNrjxpcSFF2hgiar5
tobiajo added a commit to tobiajo/kafka-flow that referenced this pull request Jul 4, 2026
The PR history's core learning (evolution-gaming#834): the
conditional write looked trivial, the pitfalls were all in not fencing
the legitimate owner. Say that up front so the doc's weight makes sense.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGxViqVaLYb1xQyveED6Hs
tobiajo added a commit to tobiajo/kafka-flow that referenced this pull request Jul 4, 2026
- §1.1 'What ships: the persist-only subset': the study audits the full
  (deferred, evolution-gaming#834) fence, but the shipped artifact is persist-only - persist
  offset-fenced, delete plain LWW; evolution-gaming#732 closed for persists, left open for
  deletes (the documented residual, modelled by cassandra_notomb + tested by
  SnapshotSpec). Records what actually ships, not only the deferred design.
- claims R6: SnapshotFold's filter is the *primary* liveness mechanism in the
  shipped persist-only mode (no monotonic buffer), not belt-and-suspenders, and
  it is unit-tested (SnapshotFoldSpec) - graded code+test.
tobiajo added a commit to tobiajo/kafka-flow that referenced this pull request Jul 4, 2026
- §1.1 'What ships: the persist-only subset': the study audits the full
  (deferred, evolution-gaming#834) fence, but the shipped artifact is persist-only - persist
  offset-fenced, delete plain LWW; evolution-gaming#732 closed for persists, left open for
  deletes (the documented residual, modelled by cassandra_notomb + tested by
  SnapshotSpec). Records what actually ships, not only the deferred design.
- claims R6: SnapshotFold's filter is the *primary* liveness mechanism in the
  shipped persist-only mode (no monotonic buffer), not belt-and-suspenders, and
  it is unit-tested (SnapshotFoldSpec) - graded code+test.
tobiajo added a commit to tobiajo/kafka-flow that referenced this pull request Jul 4, 2026
Restructure docs/cassandra-single-writer-design.md to focus on why over
what: brief mechanics, explicit design choices, and the learnings from
the deferred full solution, mirroring the Kafka design doc's shape
(Problem, mechanism, Testing, Rejected alternatives, Forward-looking):

- Problem now explains why the Kafka fix does not transfer to Cassandra
  (no transaction to bind the offset commit into, no ownership
  authority), motivating a per-write, per-key fence.
- The mechanism is kept brief and its three load-bearing choices are
  called out explicitly: per-key granularity, `<=` over `<`, and
  ordering by data rather than identity.
- "Why the store is the whole change" makes the SnapshotFold replay
  dedup explicit as the property that lets persist-only ship without
  core changes.
- A Compatibility section collects the no-API-change / no-migration /
  write-side-only-fence consequences.
- The deferred full solution (offset-gated deletes, PR evolution-gaming#834) is retold
  as a forced chain - tombstone, breaking delete(key, offset) API,
  monotonic buffer, tombstone-floor recovery read, independent
  events-recovery seeding - with a livelock diagram, the TLA+ results
  (all defects were liveness, never safety; models in PR evolution-gaming#835), and the
  explicit deferral rationale (cost/benefit and KIP-939 timing).

Also make the rolling-deploy clock-skew caveat in persistence.md
concrete (coordinator vs client write timestamps), and update a test
comment referencing a renamed doc section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018jfVxUNrjxpcSFF2hgiar5
tobiajo added a commit to tobiajo/kafka-flow that referenced this pull request Jul 4, 2026
The PR history's core learning (evolution-gaming#834): the
conditional write looked trivial, the pitfalls were all in not fencing
the legitimate owner. Say that up front so the doc's weight makes sense.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGxViqVaLYb1xQyveED6Hs
tobiajo added a commit to tobiajo/kafka-flow that referenced this pull request Jul 4, 2026
- §1.1 'What ships: the persist-only subset': the study audits the full
  (deferred, evolution-gaming#834) fence, but the shipped artifact is persist-only - persist
  offset-fenced, delete plain LWW; evolution-gaming#732 closed for persists, left open for
  deletes (the documented residual, modelled by cassandra_notomb + tested by
  SnapshotSpec). Records what actually ships, not only the deferred design.
- claims R6: SnapshotFold's filter is the *primary* liveness mechanism in the
  shipped persist-only mode (no monotonic buffer), not belt-and-suspenders, and
  it is unit-tested (SnapshotFoldSpec) - graded code+test.
tobiajo added a commit to tobiajo/kafka-flow that referenced this pull request Jul 4, 2026
- §1.1 'What ships: the persist-only subset': the study audits the full
  (deferred, evolution-gaming#834) fence, but the shipped artifact is persist-only - persist
  offset-fenced, delete plain LWW; evolution-gaming#732 closed for persists, left open for
  deletes (the documented residual, modelled by cassandra_notomb + tested by
  SnapshotSpec). Records what actually ships, not only the deferred design.
- claims R6: SnapshotFold's filter is the *primary* liveness mechanism in the
  shipped persist-only mode (no monotonic buffer), not belt-and-suspenders, and
  it is unit-tested (SnapshotFoldSpec) - graded code+test.
tobiajo added a commit to tobiajo/kafka-flow that referenced this pull request Jul 4, 2026
- §1.1 'What ships: the persist-only subset': the study audits the full
  (deferred, evolution-gaming#834) fence, but the shipped artifact is persist-only - persist
  offset-fenced, delete plain LWW; evolution-gaming#732 closed for persists, left open for
  deletes (the documented residual, modelled by cassandra_notomb + tested by
  SnapshotSpec). Records what actually ships, not only the deferred design.
- claims R6: SnapshotFold's filter is the *primary* liveness mechanism in the
  shipped persist-only mode (no monotonic buffer), not belt-and-suspenders, and
  it is unit-tested (SnapshotFoldSpec) - graded code+test.
tobiajo added a commit to tobiajo/kafka-flow that referenced this pull request Jul 5, 2026
Restructure docs/cassandra-single-writer-design.md to focus on why over
what: brief mechanics, explicit design choices, and the learnings from
the deferred full solution, mirroring the Kafka design doc's shape
(Problem, mechanism, Testing, Rejected alternatives, Forward-looking):

- Problem now explains why the Kafka fix does not transfer to Cassandra
  (no transaction to bind the offset commit into, no ownership
  authority), motivating a per-write, per-key fence.
- The mechanism is kept brief and its three load-bearing choices are
  called out explicitly: per-key granularity, `<=` over `<`, and
  ordering by data rather than identity.
- "Why the store is the whole change" makes the SnapshotFold replay
  dedup explicit as the property that lets persist-only ship without
  core changes.
- A Compatibility section collects the no-API-change / no-migration /
  write-side-only-fence consequences.
- The deferred full solution (offset-gated deletes, PR evolution-gaming#834) is retold
  as a forced chain - tombstone, breaking delete(key, offset) API,
  monotonic buffer, tombstone-floor recovery read, independent
  events-recovery seeding - with a livelock diagram, the TLA+ results
  (all defects were liveness, never safety; models in PR evolution-gaming#835), and the
  explicit deferral rationale (cost/benefit and KIP-939 timing).

Also make the rolling-deploy clock-skew caveat in persistence.md
concrete (coordinator vs client write timestamps), and update a test
comment referencing a renamed doc section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018jfVxUNrjxpcSFF2hgiar5
tobiajo added a commit to tobiajo/kafka-flow that referenced this pull request Jul 5, 2026
The PR history's core learning (evolution-gaming#834): the
conditional write looked trivial, the pitfalls were all in not fencing
the legitimate owner. Say that up front so the doc's weight makes sense.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGxViqVaLYb1xQyveED6Hs
tobiajo added a commit to tobiajo/kafka-flow that referenced this pull request Jul 5, 2026
- §1.1 'What ships: the persist-only subset': the study audits the full
  (deferred, evolution-gaming#834) fence, but the shipped artifact is persist-only - persist
  offset-fenced, delete plain LWW; evolution-gaming#732 closed for persists, left open for
  deletes (the documented residual, modelled by cassandra_notomb + tested by
  SnapshotSpec). Records what actually ships, not only the deferred design.
- claims R6: SnapshotFold's filter is the *primary* liveness mechanism in the
  shipped persist-only mode (no monotonic buffer), not belt-and-suspenders, and
  it is unit-tested (SnapshotFoldSpec) - graded code+test.
tobiajo and others added 3 commits July 22, 2026 21:47
…st-only

Snapshot persistence for the transactional Kafka mode was last-write-wins, so
a stale writer (a zombie that lost its partition but is still flushing) could
overwrite a newer snapshot. Guard the persist path with a compare-and-set on
the offset: a write lands only if it advances the stored offset, so a stale
writer's flush is rejected rather than clobbering the owner's state. This is
the persist-only slice; delete stays plain last-write-wins.

A guard-expired row (present with a null offset, left by a partial write whose
TTL expired) is repaired by a persist claiming it via IF offset = null, which
breaks the INSERT-IF-NOT-EXISTS conflict-forever deadlock.

The design doc (docs/cassandra-single-writer-design.md, titled ": persist
only") is rewritten around the choices and their learnings: why the offset,
not the consumer generation, is the fence token, and where the subtlety lives.
FlowSpec is adapted to the PartitionAssignment API.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGxViqVaLYb1xQyveED6Hs
…d-set fence)

Extend the persist-only compare-and-set to the full snapshot fence: both
persist and delete are offset-fenced, closing the stale-writer overwrite for
deletes as well. A fenced delete always writes a tombstone, even for a
never-persisted key, so a later replay of an already-deleted record cannot
revive it.

Events recovery is guarded against the journal revive: the replay-window floor
is seeded from the snapshot store and initPersisted is routed through the
monotonic cell, so a recovery that replays events cannot regress the floor
below an existing snapshot's offset. The guard-expired row is read as absent
and repaired via IF offset = null.

The Cassandra design doc is made self-consistent for the full fence (why the
consumer generation cannot replace the offset in the token, and where the
subtlety lives); persistence.md and the kafka design doc get the matching
updates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGxViqVaLYb1xQyveED6Hs
…key serialization

Cover the full-fence behaviour: the compare-and-set test suite, the replay-
fencing spec (a replayed delete after its tombstone was TTL-reaped stays a
fence), the per-key serialization race (the flushCell lost-write), the
read-state floor gate, and the TTL-edge timing. The Cassandra integration
tests pin the image above the mode's version floor and harden the TTL-edge
timing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGxViqVaLYb1xQyveED6Hs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant