fix(db): prevent duplicate event inserts under lock expiry and partial-chunk failure - #2
Closed
raonitimo wants to merge 1 commit into
Closed
fix(db): prevent duplicate event inserts under lock expiry and partial-chunk failure#2raonitimo wants to merge 1 commit into
raonitimo wants to merge 1 commit into
Conversation
…l-chunk failure F7 — lock can expire mid-insert: base-buffer tryFlush() held the flush lock with a 60s TTL, but a ClickHouse insert can run up to 300s (client request_timeout = 300s, insert max_execution_time = 300s). A slow insert outlives the lock, letting a second worker replica acquire it and concurrently flush the same Redis-queued events -> duplicate rows. Raise lockTimeout to 360s so the lock cannot expire while an insert is in flight, and add a deterministic per-chunk insert_deduplication_token as defense-in-depth so ClickHouse rejects a duplicate insert even if the lock assumption is ever violated. F8 — post-loop ltrim replays committed chunks: processBuffer() inserted events in chunks but only ltrimmed the queue after the whole loop. If a later chunk threw, the catch returned without trimming, so the entire batch (including already-inserted chunks) was retried next cycle -> duplicates. Trim each chunk from the front of the queue only after it is safely inserted; on failure we stop, leaving just the untrimmed remainder to be retried. Per-chunk realtime publish keeps committed chunks' notifications intact. Tests: extend event-buffer.test.ts to prove a mid-batch chunk failure does not re-insert committed chunks, and that the dedup token is deterministic across retries. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Owner
Author
|
Superseded by the upstream contribution Openpanel-dev#417 (prod runs the stock upstream image, so the fix must land upstream, not on this fork). Closing here. |
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
Two ways the event-ingestion path can insert duplicate rows into ClickHouse.
eventsis a plain (Shared)MergeTree with no dedup key, and pageviews are the billing metric, so duplicates inflate billing.F7 — flush lock can expire mid-insert
base-buffer.tstryFlush()acquires the Redis flush lock withlockTimeout = 60(seconds). But an insert can run far longer:client.tssetsrequest_timeout = 300_000and the insert proxy setsmax_execution_time: 300. A slow insert outlives the 60s lock, so a second worker replica can acquire the lock and start a concurrent flush of the same queued events → duplicate inserts.F8 — post-loop
ltrimreplays committed chunksevent-buffer.tsprocessBuffer()inserts events in chunks in a loop, but theltrimthat removes them from the Redis queue runs only after the whole loop. If chunk 3 of 4 throws, thecatchlogs and returns without trimming → the entire batch is retried next cycle → chunks 1 & 2 are re-inserted → duplicates.Fixes
lockTimeoutto360s so the lock cannot expire while an insert (max 300s) is in flight. Added a deterministic per-chunkinsert_deduplication_token(sha256 of the chunk payload) as defense-in-depth so ClickHouse rejects a duplicate insert even if the lock assumption is ever violated.created_at); realtimepublishEventmoved per-committed-chunk so a later failure no longer drops earlier chunks' notifications.Tests
Extended
packages/db/src/buffers/event-buffer.test.ts:Ran
pnpm vitest run packages/db/src/buffers/event-buffer.test.ts→ 12 passed. No formatter run (per repo policy).