TD-6395: Guard checkpoint writes on ownership, not lease_count - #50
Open
tkusumo wants to merge 1 commit into
Open
TD-6395: Guard checkpoint writes on ownership, not lease_count#50tkusumo wants to merge 1 commit into
tkusumo wants to merge 1 commit into
Conversation
order_service logged recurring :update_checkpoint_failed on its Kinesis consumers in prod (~1-3 / 10 min, bursts during deploy-time rebalancing), with lease_owner == current_shard_owner — a conditional-write failure, not an ownership mismatch. Root cause: the Ecto adapter's update_checkpoint did get_shard_lease then a conditional UPDATE via update_shard_lease/3, whose WHERE clause gates on lease_count. Leases renew every 30s (same owner), bumping lease_count. When a renewal landed between the checkpoint's read and write, the UPDATE matched 0 rows and failed, even though this worker still held the lease. A failed checkpoint doesn't advance the shard, causing at-least-once redelivery. Fix: give checkpoint its own ShardLeases.update_checkpoint/3 that writes in a single UPDATE guarded on ownership only (shard_id + app_name + stream_name + lease_owner), never lease_count. This mirrors the Dynamo adapter, whose update_checkpoint conditions only on lease_owner. A lease genuinely taken by another worker still matches 0 rows and surfaces :update_checkpoint_failed; the renew_lease / take_lease optimistic lock on lease_count is untouched. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
tkusumo
force-pushed
the
TD-6395/order-service-kinesis-consumers-log-recurring-upda
branch
from
July 20, 2026 18:18
28098ee to
62083c0
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
order_servicelogs recurring:update_checkpoint_failedon its Kinesis consumers in prod (assessment-event-prod,cognitive-test-event-prod,external-assessment-event-prod) — ~1–3 per 10 min, with heavier bursts during deploy-time lease rebalancing. In the failureslease_owner == current_shard_owner, so it's a checkpoint conditional-write failure against the lease table, not a lease-ownership mismatch. A failed checkpoint doesn't advance the shard, so messages can be re-delivered (at-least-once).Ticket: TD-6395
Root cause
The Ecto adapter's
update_checkpointdid a read (get_shard_lease) then a conditionalUPDATEviaupdate_shard_lease/3, whose WHERE clause (build_where_clause/2) gates onlease_count. TheLeaseprocess renews every 30s (same owner), incrementinglease_count. When a renewal landed between the checkpoint's read and write, theUPDATEmatched 0 rows →{:error, :update_unsuccessful}→:update_checkpoint_failed, even though this worker still held the lease.That explains every symptom: same owner (renewal doesn't change owner); intermittent and low-rate (narrow read→write window); worse during rebalancing (extra
lease_countchurn fromtake_lease); redelivery with no loss.The
lease_countguard is correct forrenew_lease/take_lease(optimistic concurrency) but wrong for checkpointing, which only needs to confirm the lease is still owned. The Dynamo adapter already gets this right — itsupdate_checkpointconditions only onlease_owner.Fix
Give checkpoint its own
ShardLeases.update_checkpoint/3that writes in a singleUPDATE ... RETURNINGguarded on ownership only (shard_id + app_name + stream_name + lease_owner), neverlease_count. This removes the read→write race window entirely and matches the Dynamo adapter's semantics.:update_checkpoint_failed(correct).renew_lease/take_leasekeep theirlease_countoptimistic lock — untouched.Tests
Added a
describe "update_checkpoint ownership guard"block (the existing fakeRepocan't distinguish the WHERE clause, so two small purpose-built stub repos were added):lease_owner, notlease_count— captures the generated query and asserts the WHERE containslease_ownerand notlease_count(fails against the old code).:update_checkpoint_failed— a repo returning{0, []}still maps to the correct error.Full non-integration suite: 51 tests, 0 failures.
Notes
update_checkpoint's path unchanged, so this applies cleanly alongside it.:update_checkpoint_failedatom is Ecto-only, so order_service's lease table is Postgres. Same class of issue, different store — worth correcting on the ticket.🤖 Generated with Claude Code