You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// jobId: the UUID of the job (the existing one when deduped)
185
+
```
186
+
187
+
### How it works
188
+
189
+
- The dedup ID is automatically prefixed with the job name (`SendInvoiceJob::order-123`), so different job types can reuse the same key.
190
+
- The user-supplied `id` must be ≤ 400 characters, and the combined `<jobName>::<id>` key must be ≤ 510 characters (constrained by the Knex storage column). Both limits are validated at `.dedup()` time.
191
+
-`ttl` accepts a Duration (`'5s'`, `'1m'`) or milliseconds, and must be **positive** when provided. Use `0` or omit `ttl` if you want no expiry — `ttl: 0` is rejected to avoid an ambiguous "expired immediately vs no-expiry" interpretation across engines.
192
+
-`extend` and `replace`**require**`ttl` — calling them without `ttl` throws.
193
+
-`replace` only applies to jobs in `pending` or `delayed` state. Jobs that are active (executing) or retained in history (`completed`/`failed` with retention) are left alone; the dispatch returns `{ deduped: 'skipped' }`.
194
+
-`replace` swaps the **payload only** — priority, queue, delay, groupId, and stored dedup options of the existing job are retained. To change those, use a different dedup id or wait for the TTL to expire.
195
+
-`extend` resets the TTL clock but never changes the window length. The window length is fixed to the `ttl` from the first dispatch that created the dedup slot. Later dispatches that pass a different `ttl` only reset the clock; their `ttl` value is ignored. To resize the window, let the slot expire and start over with a new dispatch.
196
+
-`extend` works in **all states** — even when the existing job is `active` (executing) or retained in history. Unlike `replace` (which is no-op on non-replaceable states), `extend` always refreshes the dedup TTL window. Use this when you want the dedup slot to keep blocking new dispatches for the lifetime of a long-running job.
197
+
-`extend` requires the **first** dispatch to have set a `ttl`. If the slot was created without a `ttl`, later `extend` dispatches have no window to refresh and return `{ deduped: 'skipped' }` instead of `'extended'`.
198
+
-`retryJob` does not touch the dedup entry — a retried job continues to occupy the dedup slot. TTL runs on wall-clock time, so long-running retries may outlive the TTL window. Use a generous TTL or no TTL if retries must stay deduped.
199
+
- Atomic and race-free:
200
+
-**Redis**: a single Lua script per dispatch performs the dedup-key lookup, state check (pending/delayed ZSCORE), payload swap, and TTL refresh atomically.
201
+
-**Knex**: transactional `SELECT ... FOR UPDATE` + insert/update inside a transaction. A nested savepoint catches unique-constraint violations under concurrent inserts and returns `{ deduped: 'skipped' }` pointing at the winner.
202
+
-**SyncAdapter**: executes inline, no dedup support.
203
+
204
+
### Caveats
205
+
206
+
- Without `.dedup()`, jobs use auto-generated UUIDs and are never deduplicated.
207
+
- The **Sync adapter** ignores `.dedup()` entirely — every dispatch executes inline and `deduped` is always `undefined` on the result. Use Redis or Knex if you need real deduplication.
208
+
-`.dedup()` is only available on single dispatch. `dispatchMany` / `pushManyOn` reject jobs with a `dedup` field.
209
+
- Scheduled jobs (`.schedule()`) do not support dedup — each cron/interval fire is an independent dispatch.
210
+
- With no `ttl`, dedup persists until the job is removed (completed/failed without retention). When retention keeps the record, re-dispatch stays blocked until the record is pruned.
211
+
- With `ttl`, dedup expires after the window — a new job (new UUID) is created. The old job still runs.
212
+
- Knex MySQL concurrent race: MySQL does not support partial unique indexes, so two `pushOn` calls with the same dedup id firing at the exact same instant can both succeed. Serialize at the app layer if strict guarantees are required, or use Postgres / SQLite / Redis (all of which serialize correctly via the partial unique index or Lua atomicity).
213
+
134
214
## Job History & Retention
135
215
136
216
Keep completed and failed jobs for debugging:
@@ -536,7 +616,7 @@ import * as boringqueue from '@boringnode/queue'
536
616
537
617
const instrumentation =newQueueInstrumentation({
538
618
messagingSystem: 'boringqueue', // default
539
-
executionSpanLinkMode: 'link', // or 'parent'
619
+
executionSpanLinkMode: 'link', // or 'parent'
540
620
})
541
621
542
622
instrumentation.enable()
@@ -549,19 +629,19 @@ The instrumentation patches `QueueManager.init()` to automatically inject its wr
549
629
550
630
The instrumentation uses standard [OTel messaging semantic conventions](https://opentelemetry.io/docs/specs/semconv/messaging/messaging-spans/) where they map cleanly, plus a few queue-specific custom attributes.
0 commit comments