Skip to content

Protect Cassandra snapshot writes on persist (compare-and-set) - #838

Draft
tobiajo wants to merge 1 commit into
evolution-gaming:masterfrom
tobiajo:tj/address-partition-ownership-overlap-possiblity-cassandra-persist-only
Draft

tobiajo wants to merge 1 commit into
evolution-gaming:masterfrom
tobiajo:tj/address-partition-ownership-overlap-possiblity-cassandra-persist-only

Conversation

@tobiajo

@tobiajo tobiajo commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Stacked on (merge first): #854

Problem

Consumer-group ownership of the input topic does not extend to the snapshot store (#732): a zombie that lost its partition but is still flushing can overwrite a newer snapshot, and the next recovery loads stale state while resuming from the newer committed offset. The Kafka backend closes this with transactional writes; the Cassandra backend was still last-write-wins.

Fix

Guard the persist path with a compare-and-set on the offset (a lightweight transaction): a write lands only if it advances the stored offset, so a stale writer's flush is rejected (SnapshotWriteConflict) instead of clobbering the owner's state. The first write of a key uses INSERT ... IF NOT EXISTS, and a guard-expired row (present with a null offset after a partial write's TTL expiry) is repaired by claiming it via IF offset = null, so no key can conflict forever. Opt-in: snapshotCompareAndSet on CassandraPersistence, off by default.

This is the persist-only slice: delete stays plain last-write-wins (offset-gating it is an API break, kept for the full compare-and-set mode stacked on this). Accepted residual: a stale writer can revive a just-deleted key. No core change — buffer and recovery stay exactly as on master.

Tests and docs

Store-level compare-and-set suite (SnapshotSpec), TTL edge cases (SnapshotTtlEdgeSpec), and the flow-level corruption reproduction and prevention (FlowSpec). New design doc, docs/cassandra-single-writer-design.md — including why the offset, not the consumer generation, is the fence token here; persistence docs updated.

Comment thread docs/cassandra-single-writer-design.md Outdated
@tobiajo
tobiajo force-pushed the tj/address-partition-ownership-overlap-possiblity-cassandra-persist-only branch 15 times, most recently from c377f7e to dfb7869 Compare July 1, 2026 20:35
tobiajo added a commit to tobiajo/kafka-flow that referenced this pull request Jul 3, 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), and the explicit deferral
  rationale (cost/benefit and KIP-939 timing).
- A process note links the stacked PRs (evolution-gaming#828/evolution-gaming#833/evolution-gaming#834/evolution-gaming#835/evolution-gaming#838) so
  the git history, review conversations and models stay discoverable.

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
tobiajo force-pushed the tj/address-partition-ownership-overlap-possiblity-cassandra-persist-only branch 12 times, most recently from 9d443af to 472228d Compare July 5, 2026 09:12
@tobiajo
tobiajo force-pushed the tj/address-partition-ownership-overlap-possiblity-cassandra-persist-only branch 21 times, most recently from 101b3fc to 1ecdff0 Compare July 8, 2026 13:18
@tobiajo
tobiajo force-pushed the tj/address-partition-ownership-overlap-possiblity-cassandra-persist-only branch from 1ecdff0 to 427f52b Compare July 11, 2026 14:05
@tobiajo tobiajo changed the title Protect Cassandra snapshot writes from stale writers (compare-and-set, persist-only) Protect Cassandra snapshot writes on persist (compare-and-set) Jul 11, 2026
@tobiajo
tobiajo force-pushed the tj/address-partition-ownership-overlap-possiblity-cassandra-persist-only branch 3 times, most recently from 5922e19 to 06c302e Compare July 15, 2026 16:51
…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
@tobiajo
tobiajo force-pushed the tj/address-partition-ownership-overlap-possiblity-cassandra-persist-only branch from 06c302e to 1a8c137 Compare July 22, 2026 21:49
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