Build your own mini stream processor · Stage 8 of 8
Transactional (2PC) sink — end-to-end exactly-once
Exactly-once state (stage 7) is not enough for exactly-once output: the
sink must not publish records that belong to a checkpoint that never completed.
Flink's TwoPhaseCommitSinkFunction ties sink transactions to checkpoints —
pre-commit on the barrier, commit on the checkpoint-complete
notification, and abort anything not yet committed on recovery.
This stage is optional for the learner, but it is the capstone of the EOS story and is fully graded here.
New API this stage
TransactionalSink() # write / pre_commit(cid) / commit(cid) / abort(cid) / abort_open()
run_exactly_once_sink(records, barrier_every, crash_after, commit_lag=0) -> list
This stage's task
TransactionalSink:writebuffers into the open transaction;pre_commit(cid)moves it toprepared[cid](durable, invisible);commit(cid)appendsprepared[cid]to the visiblecommittedlist exactly once;abort(cid)andabort_open()discard.run_exactly_once_sink: on each barrier, pre-commit and queue a pending commit; a commit notification arrivescommit_lagbarriers later. On a clean finish, do a final pre-commit of the trailing open transaction and flush all pending commits.- On crash: abort the open transaction and every prepared-but-unnotified transaction, then replay the source from the last completed checkpoint's offset and commit that tail.
The EOS invariant (how it is graded)
For every crash point and commit lag 0/1, the sorted committed output must
equal the batch oracle — every input appears exactly once, with no duplicates
(uncommitted staging is aborted and replayed) and no loss.
Constraints
- Deterministic crash schedule, idempotent commit, no sleeps. Earlier stages still pass. Suite < 90 s.
Further reading
- Flink 2.x — End-to-end exactly-once / TwoPhaseCommitSinkFunction: https://nightlies.apache.org/flink/flink-docs-release-2.2/docs/dev/datastream/fault-tolerance/
Run it
de start FLK-BYO/08
de test
Hints
Hint 1
TransactionalSink models Flink's TwoPhaseCommitSinkFunction. write() buffers into the OPEN transaction. pre_commit(cid) flushes the open txn into prepared[cid] (durable, invisible) and clears the open txn. commit(cid) appends prepared[cid] to the visible `committed` list EXACTLY ONCE (ignore a duplicate/absent cid). abort(cid) drops prepared[cid]; abort_open() drops the open txn.
Hint 2
run_exactly_once_sink injects barriers; on each Barrier pre_commit the open txn (record the source offset) and queue a pending commit; a commit notification lands commit_lag barriers later (pop the oldest pending and commit it). When the job finishes cleanly, do a FINAL pre_commit of the trailing open txn and flush all pending commits.
Hint 3
On crash: abort the open txn AND any prepared txn whose checkpoint was never notified (aborted = not visible). Then replay the source from the last COMPLETED checkpoint's offset into a fresh sink, pre_commit + commit that tail, and the visible output = committed-before-crash + replayed tail. For every crash point and lag the sorted committed output must equal the batch oracle (each input exactly once).
uv run de start FLK-BYO/08-transactional-sink
uv run de test