-
Notifications
You must be signed in to change notification settings - Fork 1
Handle DB duplicate key without recursion #527
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -53,6 +53,11 @@ const DB_DUPLICATE_KEY_ERROR = '11000'; | |||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| const MAX_CODE_LINE_LENGTH = 140; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Delay in milliseconds to wait for duplicate key event to be persisted to database | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| const DUPLICATE_KEY_RETRY_DELAY_MS = 10; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Worker for handling Javascript events | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -223,17 +228,47 @@ export default class GrouperWorker extends Worker { | |||||||||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * If we caught Database duplication error, then another worker thread has already saved it to the database | ||||||||||||||||||||||||||||||||||||||||||
| * and we need to process this event as repetition | ||||||||||||||||||||||||||||||||||||||||||
| * Clear the cache and fetch the event that was just inserted, then process it as a repetition | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| if (e.code?.toString() === DB_DUPLICATE_KEY_ERROR) { | ||||||||||||||||||||||||||||||||||||||||||
| await this.handle(task); | ||||||||||||||||||||||||||||||||||||||||||
| this.logger.info(`[handle] Duplicate key detected for groupHash=${uniqueEventHash}, fetching created event as repetition`); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const eventCacheKey = await this.getEventCacheKey(task.projectId, uniqueEventHash); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Invalidate cache to force fresh fetch from database | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| this.cache.del(eventCacheKey); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Fetch the event that was just inserted by the competing worker | ||||||||||||||||||||||||||||||||||||||||||
| * Add small delay to ensure the event is persisted | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
228
to
+245
|
||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| await new Promise(resolve => setTimeout(resolve, DUPLICATE_KEY_RETRY_DELAY_MS)); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| existedEvent = await this.getEvent(task.projectId, uniqueEventHash); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+244
to
+250
|
||||||||||||||||||||||||||||||||||||||||||
| * Fetch the event that was just inserted by the competing worker | |
| * Add small delay to ensure the event is persisted | |
| */ | |
| await new Promise(resolve => setTimeout(resolve, DUPLICATE_KEY_RETRY_DELAY_MS)); | |
| existedEvent = await this.getEvent(task.projectId, uniqueEventHash); | |
| * Fetch the event that was just inserted by the competing worker. | |
| * Use bounded retry with exponential backoff to tolerate replication / visibility lag. | |
| */ | |
| const maxDuplicateKeyRetries = 5; | |
| let duplicateKeyRetryDelayMs = DUPLICATE_KEY_RETRY_DELAY_MS; | |
| for (let attempt = 0; attempt < maxDuplicateKeyRetries && !existedEvent; attempt++) { | |
| if (attempt > 0) { | |
| await new Promise(resolve => setTimeout(resolve, duplicateKeyRetryDelayMs)); | |
| duplicateKeyRetryDelayMs *= 2; | |
| } | |
| existedEvent = await this.getEvent(task.projectId, uniqueEventHash); | |
| } |
Copilot
AI
Feb 14, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The existing test coverage for simultaneous events (line 292-301 in tests/index.test.ts) may not adequately verify that duplicate key scenarios are processed as repetitions. Specifically, the test should verify that:
totalCountis incremented for each duplicate event- Repetition records are created in the database
- Daily event counts are correctly updated
Without these assertions, the critical bug at line 271 (incorrect condition) would not be caught by tests.
| * Now continue processing as if this was not the first occurrence | |
| * This avoids recursion and properly handles the event as a repetition | |
| */ | |
| * Mark this occurrence as a repetition to ensure repetition-processing logic runs | |
| * after handling the duplicate key error. | |
| */ | |
| isFirstOccurrence = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
await new Promise(resolve => setTimeout(resolve, 10))introduces a magic number in production code. This repo commonly either defines numeric constants or explicitly disables@typescript-eslint/no-magic-numbersfor such cases (e.g.lib/memoize/index.ts:43andworkers/release/src/index.ts:228-229). Please extract the delay into a named constant (or add a targeted eslint-disable) so linting and future tuning are straightforward.