-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
fix(redis-worker): stop fair queue leaking concurrency slots #4540
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: main
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 |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@trigger.dev/redis-worker": patch | ||
| --- | ||
|
|
||
| Fair queue consumers no longer leak concurrency slots. A slot is now always released when a message completes or is put back on the queue, even when its in-flight record has already gone. Leaked slots were never reclaimed, so enough of them would permanently stall every queue belonging to that tenant. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1245,22 +1245,20 @@ export class FairQueue<TPayloadSchema extends z.ZodTypeAny = z.ZodUnknown> { | |
| } | ||
| } | ||
|
|
||
| const descriptor: QueueDescriptor = storedMessage | ||
| ? (this.queueDescriptorCache.get(queueId) ?? { | ||
| id: queueId, | ||
| tenantId: storedMessage.tenantId, | ||
| metadata: storedMessage.metadata ?? {}, | ||
| }) | ||
| : { id: queueId, tenantId: this.keys.extractTenantId(queueId), metadata: {} }; | ||
|
|
||
| // Complete in visibility manager | ||
| await this.visibilityManager.complete(messageId, queueId); | ||
| const descriptor: QueueDescriptor = this.queueDescriptorCache.get(queueId) ?? { | ||
| id: queueId, | ||
| tenantId: storedMessage?.tenantId ?? this.keys.extractTenantId(queueId), | ||
| metadata: storedMessage?.metadata ?? {}, | ||
| }; | ||
|
Comment on lines
+1248
to
+1252
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift Preserve the descriptor that reserved each message slot.
Store the reservation descriptor by Based on learnings, Also applies to: 1301-1305 Source: Learnings |
||
|
|
||
| // Release concurrency | ||
| if (this.concurrencyManager && storedMessage) { | ||
| if (this.concurrencyManager) { | ||
| await this.concurrencyManager.release(descriptor, messageId); | ||
| } | ||
|
matt-aitken marked this conversation as resolved.
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| // Complete in visibility manager | ||
| await this.visibilityManager.complete(messageId, queueId); | ||
|
|
||
| // Update both old and new indexes, clean up caches if queue is empty | ||
| const { queueEmpty } = await this.#updateAllIndexesAfterDequeue(queueId, descriptor.tenantId); | ||
| if (queueEmpty) { | ||
|
|
@@ -1300,13 +1298,16 @@ export class FairQueue<TPayloadSchema extends z.ZodTypeAny = z.ZodUnknown> { | |
| } | ||
| } | ||
|
|
||
| const descriptor: QueueDescriptor = storedMessage | ||
| ? (this.queueDescriptorCache.get(queueId) ?? { | ||
| id: queueId, | ||
| tenantId: storedMessage.tenantId, | ||
| metadata: storedMessage.metadata ?? {}, | ||
| }) | ||
| : { id: queueId, tenantId: this.keys.extractTenantId(queueId), metadata: {} }; | ||
| const descriptor: QueueDescriptor = this.queueDescriptorCache.get(queueId) ?? { | ||
| id: queueId, | ||
| tenantId: storedMessage?.tenantId ?? this.keys.extractTenantId(queueId), | ||
| metadata: storedMessage?.metadata ?? {}, | ||
| }; | ||
|
|
||
| // Release concurrency | ||
| if (this.concurrencyManager) { | ||
| await this.concurrencyManager.release(descriptor, messageId); | ||
| } | ||
|
matt-aitken marked this conversation as resolved.
|
||
|
|
||
| // Release back to queue (visibility manager updates dispatch indexes atomically) | ||
| // Dispatch shard is tenant-based, not queue-based | ||
|
|
@@ -1324,11 +1325,6 @@ export class FairQueue<TPayloadSchema extends z.ZodTypeAny = z.ZodUnknown> { | |
| Date.now() // Put at back of queue | ||
| ); | ||
|
|
||
| // Release concurrency | ||
| if (this.concurrencyManager && storedMessage) { | ||
| await this.concurrencyManager.release(descriptor, messageId); | ||
| } | ||
|
|
||
| this.logger.debug("Message released", { | ||
| messageId, | ||
| queueId, | ||
|
|
@@ -1411,6 +1407,11 @@ export class FairQueue<TPayloadSchema extends z.ZodTypeAny = z.ZodUnknown> { | |
| attempt: storedMessage.attempt + 1, | ||
| }; | ||
|
|
||
| // Release concurrency | ||
| if (this.concurrencyManager) { | ||
| await this.concurrencyManager.release(descriptor, storedMessage.id); | ||
| } | ||
|
|
||
| // Release with delay, passing the updated message data so the Lua script | ||
| // atomically writes the incremented attempt count when re-queuing. | ||
| const tenantQueueIndexKey = this.keys.tenantQueueIndexKey(descriptor.tenantId); | ||
|
|
@@ -1427,11 +1428,6 @@ export class FairQueue<TPayloadSchema extends z.ZodTypeAny = z.ZodUnknown> { | |
| JSON.stringify(updatedMessage) | ||
| ); | ||
|
|
||
| // Release concurrency | ||
| if (this.concurrencyManager) { | ||
| await this.concurrencyManager.release(descriptor, storedMessage.id); | ||
| } | ||
|
|
||
| this.telemetry.recordRetry(); | ||
|
|
||
| this.logger.debug("Message scheduled for retry", { | ||
|
|
||
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.
🔍 Reserve and release build the queue descriptor from different sources
Reservation builds the descriptor as
queueDescriptorCache.get(queueId) ?? { tenantId, metadata: {} }(packages/redis-worker/src/fair-queue/index.ts:1091-1095) — it never falls back to the stored message metadata — whilecompleteMessage/releaseMessagenow fall back tostoredMessage.metadata. If the cache misses at claim time but hits (or resolves differently) at completion time, the SADD and SREM target different Redis sets for metadata-derived concurrency groups. Aligning both paths on a single descriptor source (or recording the reserved group keys with the reservation) would remove this class of asymmetry entirely.Was this helpful? React with 👍 or 👎 to provide feedback.