@trigger.dev/redis-worker 4.4.6 → 4.5.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -16354,7 +16354,576 @@ end
16354
16354
  }
16355
16355
  }
16356
16356
  };
16357
+ var BufferEntryStatus = z.enum(["QUEUED", "DRAINING", "FAILED"]);
16358
+ var BufferEntryError = z.object({
16359
+ code: z.string(),
16360
+ message: z.string()
16361
+ });
16362
+ var stringToInt = z.string().transform((v, ctx) => {
16363
+ const n = Number(v);
16364
+ if (!Number.isInteger(n) || n < 0) {
16365
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "expected non-negative integer string" });
16366
+ return z.NEVER;
16367
+ }
16368
+ return n;
16369
+ });
16370
+ var stringToDate = z.string().transform((v, ctx) => {
16371
+ const d = new Date(v);
16372
+ if (Number.isNaN(d.getTime())) {
16373
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "expected ISO date string" });
16374
+ return z.NEVER;
16375
+ }
16376
+ return d;
16377
+ });
16378
+ var stringToError = z.string().transform((v, ctx) => {
16379
+ try {
16380
+ return BufferEntryError.parse(JSON.parse(v));
16381
+ } catch {
16382
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "expected JSON-encoded BufferEntryError" });
16383
+ return z.NEVER;
16384
+ }
16385
+ });
16386
+ var BufferEntrySchema = z.object({
16387
+ runId: z.string().min(1),
16388
+ envId: z.string().min(1),
16389
+ orgId: z.string().min(1),
16390
+ payload: z.string(),
16391
+ status: BufferEntryStatus,
16392
+ attempts: stringToInt,
16393
+ createdAt: stringToDate,
16394
+ lastError: stringToError.optional()
16395
+ });
16396
+ function serialiseSnapshot(snapshot) {
16397
+ return JSON.stringify(snapshot);
16398
+ }
16399
+ function deserialiseSnapshot(serialised) {
16400
+ return JSON.parse(serialised);
16401
+ }
16402
+
16403
+ // src/mollifier/buffer.ts
16404
+ var MollifierBuffer = class {
16405
+ redis;
16406
+ entryTtlSeconds;
16407
+ logger;
16408
+ constructor(options) {
16409
+ this.entryTtlSeconds = options.entryTtlSeconds;
16410
+ this.logger = options.logger ?? new Logger("MollifierBuffer", "debug");
16411
+ this.redis = createRedisClient(
16412
+ {
16413
+ ...options.redisOptions,
16414
+ retryStrategy(times) {
16415
+ const delay = Math.min(times * 50, 1e3);
16416
+ return delay;
16417
+ },
16418
+ maxRetriesPerRequest: 20
16419
+ },
16420
+ {
16421
+ onError: (error) => {
16422
+ this.logger.error("MollifierBuffer redis client error:", { error });
16423
+ }
16424
+ }
16425
+ );
16426
+ this.#registerCommands();
16427
+ }
16428
+ // Returns true if the entry was newly written; false if a duplicate runId
16429
+ // was already buffered (idempotent no-op). Callers can use the boolean to
16430
+ // record a duplicate-accept metric without affecting buffer state.
16431
+ async accept(input) {
16432
+ const entryKey = `mollifier:entries:${input.runId}`;
16433
+ const queueKey = `mollifier:queue:${input.envId}`;
16434
+ const orgsKey = "mollifier:orgs";
16435
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
16436
+ const result = await this.redis.acceptMollifierEntry(
16437
+ entryKey,
16438
+ queueKey,
16439
+ orgsKey,
16440
+ input.runId,
16441
+ input.envId,
16442
+ input.orgId,
16443
+ input.payload,
16444
+ createdAt,
16445
+ String(this.entryTtlSeconds),
16446
+ "mollifier:org-envs:"
16447
+ );
16448
+ return result === 1;
16449
+ }
16450
+ async pop(envId) {
16451
+ const queueKey = `mollifier:queue:${envId}`;
16452
+ const orgsKey = "mollifier:orgs";
16453
+ const entryPrefix = "mollifier:entries:";
16454
+ const encoded = await this.redis.popAndMarkDraining(
16455
+ queueKey,
16456
+ orgsKey,
16457
+ entryPrefix,
16458
+ envId,
16459
+ "mollifier:org-envs:"
16460
+ );
16461
+ if (!encoded) return null;
16462
+ let raw;
16463
+ try {
16464
+ raw = JSON.parse(encoded);
16465
+ } catch {
16466
+ this.logger.error("MollifierBuffer.pop: failed to parse script result", { envId });
16467
+ return null;
16468
+ }
16469
+ const parsed = BufferEntrySchema.safeParse(raw);
16470
+ if (!parsed.success) {
16471
+ this.logger.error("MollifierBuffer.pop: invalid entry shape", {
16472
+ envId,
16473
+ errors: parsed.error.flatten()
16474
+ });
16475
+ return null;
16476
+ }
16477
+ return parsed.data;
16478
+ }
16479
+ async getEntry(runId) {
16480
+ const raw = await this.redis.hgetall(`mollifier:entries:${runId}`);
16481
+ if (!raw || Object.keys(raw).length === 0) return null;
16482
+ const parsed = BufferEntrySchema.safeParse(raw);
16483
+ if (!parsed.success) {
16484
+ this.logger.error("MollifierBuffer.getEntry: invalid entry shape", {
16485
+ runId,
16486
+ errors: parsed.error.flatten()
16487
+ });
16488
+ return null;
16489
+ }
16490
+ return parsed.data;
16491
+ }
16492
+ // Drainer walks these two methods to schedule pops with org-level
16493
+ // fairness: one env per org per tick. The Lua scripts maintain both
16494
+ // sets atomically with the per-env queues, so an org/env appears here
16495
+ // exactly when at least one of its envs has a queued entry.
16496
+ async listOrgs() {
16497
+ return this.redis.smembers("mollifier:orgs");
16498
+ }
16499
+ async listEnvsForOrg(orgId) {
16500
+ return this.redis.smembers(`mollifier:org-envs:${orgId}`);
16501
+ }
16502
+ async ack(runId) {
16503
+ await this.redis.del(`mollifier:entries:${runId}`);
16504
+ }
16505
+ async requeue(runId) {
16506
+ await this.redis.requeueMollifierEntry(
16507
+ `mollifier:entries:${runId}`,
16508
+ "mollifier:orgs",
16509
+ "mollifier:queue:",
16510
+ runId,
16511
+ "mollifier:org-envs:"
16512
+ );
16513
+ }
16514
+ // Returns true if the entry transitioned to FAILED; false if the entry no
16515
+ // longer exists (TTL expired between pop and fail). Caller can use the
16516
+ // boolean to skip downstream FAILED handling for ghost entries.
16517
+ async fail(runId, error) {
16518
+ const result = await this.redis.failMollifierEntry(
16519
+ `mollifier:entries:${runId}`,
16520
+ JSON.stringify(error)
16521
+ );
16522
+ return result === 1;
16523
+ }
16524
+ async getEntryTtlSeconds(runId) {
16525
+ return this.redis.ttl(`mollifier:entries:${runId}`);
16526
+ }
16527
+ async evaluateTrip(envId, options) {
16528
+ const rateKey = `mollifier:rate:${envId}`;
16529
+ const trippedKey = `mollifier:tripped:${envId}`;
16530
+ const result = await this.redis.mollifierEvaluateTrip(
16531
+ rateKey,
16532
+ trippedKey,
16533
+ String(options.windowMs),
16534
+ String(options.threshold),
16535
+ String(options.holdMs)
16536
+ );
16537
+ return { count: result[0], tripped: result[1] === 1 };
16538
+ }
16539
+ async close() {
16540
+ await this.redis.quit();
16541
+ }
16542
+ #registerCommands() {
16543
+ this.redis.defineCommand("acceptMollifierEntry", {
16544
+ numberOfKeys: 3,
16545
+ lua: `
16546
+ local entryKey = KEYS[1]
16547
+ local queueKey = KEYS[2]
16548
+ local orgsKey = KEYS[3]
16549
+ local runId = ARGV[1]
16550
+ local envId = ARGV[2]
16551
+ local orgId = ARGV[3]
16552
+ local payload = ARGV[4]
16553
+ local createdAt = ARGV[5]
16554
+ local ttlSeconds = tonumber(ARGV[6])
16555
+ local orgEnvsPrefix = ARGV[7]
16556
+
16557
+ -- Idempotent: refuse if an entry for this runId already exists in any
16558
+ -- state. Caller-side dedup is also enforced via API idempotency keys,
16559
+ -- but the buffer must not double-enqueue if a caller retries.
16560
+ if redis.call('EXISTS', entryKey) == 1 then
16561
+ return 0
16562
+ end
16563
+
16564
+ redis.call('HSET', entryKey,
16565
+ 'runId', runId,
16566
+ 'envId', envId,
16567
+ 'orgId', orgId,
16568
+ 'payload', payload,
16569
+ 'status', 'QUEUED',
16570
+ 'attempts', '0',
16571
+ 'createdAt', createdAt)
16572
+ redis.call('EXPIRE', entryKey, ttlSeconds)
16573
+ redis.call('LPUSH', queueKey, runId)
16574
+ -- Org-level membership: maintained atomically with the per-env
16575
+ -- queue so the drainer can walk orgs \u2192 envs-for-org and
16576
+ -- schedule one env per org per tick. SADDs are idempotent if the
16577
+ -- org/env are already tracked.
16578
+ redis.call('SADD', orgsKey, orgId)
16579
+ redis.call('SADD', orgEnvsPrefix .. orgId, envId)
16580
+ return 1
16581
+ `
16582
+ });
16583
+ this.redis.defineCommand("requeueMollifierEntry", {
16584
+ numberOfKeys: 2,
16585
+ lua: `
16586
+ local entryKey = KEYS[1]
16587
+ local orgsKey = KEYS[2]
16588
+ local queuePrefix = ARGV[1]
16589
+ local runId = ARGV[2]
16590
+ local orgEnvsPrefix = ARGV[3]
16591
+
16592
+ local envId = redis.call('HGET', entryKey, 'envId')
16593
+ local orgId = redis.call('HGET', entryKey, 'orgId')
16594
+ if not envId then
16595
+ return 0
16596
+ end
16597
+
16598
+ local currentAttempts = redis.call('HGET', entryKey, 'attempts')
16599
+ local nextAttempts = tonumber(currentAttempts or '0') + 1
16600
+
16601
+ redis.call('HSET', entryKey, 'status', 'QUEUED', 'attempts', tostring(nextAttempts))
16602
+ redis.call('LPUSH', queuePrefix .. envId, runId)
16603
+ -- Re-track the org/env: pop may have SREM'd them when the queue
16604
+ -- last emptied. SADDs are idempotent if the values are still
16605
+ -- present.
16606
+ if orgId then
16607
+ redis.call('SADD', orgsKey, orgId)
16608
+ redis.call('SADD', orgEnvsPrefix .. orgId, envId)
16609
+ end
16610
+ return 1
16611
+ `
16612
+ });
16613
+ this.redis.defineCommand("popAndMarkDraining", {
16614
+ numberOfKeys: 2,
16615
+ lua: `
16616
+ local queueKey = KEYS[1]
16617
+ local orgsKey = KEYS[2]
16618
+ local entryPrefix = ARGV[1]
16619
+ local envId = ARGV[2]
16620
+ local orgEnvsPrefix = ARGV[3]
16621
+
16622
+ -- Helper: prune org-level membership when an env's queue empties.
16623
+ -- Called only from the success branch where we know orgId from the
16624
+ -- popped entry. The no-runId branch below can't reach this because
16625
+ -- it has no entry to read orgId from \u2014 accept any stale org-envs
16626
+ -- entries that result (bounded by env count, recovered next accept).
16627
+ local function pruneOrgMembership(orgId)
16628
+ if not orgId then return end
16629
+ local orgEnvsKey = orgEnvsPrefix .. orgId
16630
+ redis.call('SREM', orgEnvsKey, envId)
16631
+ if redis.call('SCARD', orgEnvsKey) == 0 then
16632
+ redis.call('SREM', orgsKey, orgId)
16633
+ end
16634
+ end
16635
+
16636
+ -- Loop to skip orphan queue references \u2014 runIds whose entry hash has
16637
+ -- expired (TTL hit). HSET on a missing key would CREATE a partial
16638
+ -- hash without a TTL, leaking memory. The loop is bounded by queue
16639
+ -- length; entire Lua script remains atomic.
16640
+ while true do
16641
+ local runId = redis.call('RPOP', queueKey)
16642
+ if not runId then
16643
+ -- Queue is empty AND we have no entry to read orgId from, so
16644
+ -- skip org-level cleanup. Stale org-envs entries are bounded
16645
+ -- by env count and recovered on the next accept.
16646
+ return nil
16647
+ end
16648
+
16649
+ local entryKey = entryPrefix .. runId
16650
+ if redis.call('EXISTS', entryKey) == 1 then
16651
+ redis.call('HSET', entryKey, 'status', 'DRAINING')
16652
+ local raw = redis.call('HGETALL', entryKey)
16653
+ local result = {}
16654
+ for i = 1, #raw, 2 do
16655
+ result[raw[i]] = raw[i + 1]
16656
+ end
16657
+ -- Prune org-level membership if this pop drained the queue.
16658
+ -- Atomic with the RPOP above \u2014 a concurrent accept AFTER this
16659
+ -- script will SADD both back along with its LPUSH.
16660
+ if redis.call('LLEN', queueKey) == 0 then
16661
+ pruneOrgMembership(result['orgId'])
16662
+ end
16663
+ return cjson.encode(result)
16664
+ end
16665
+ -- Orphan queue reference: entry TTL expired while runId was queued.
16666
+ -- Discard the reference and loop to the next.
16667
+ end
16668
+ `
16669
+ });
16670
+ this.redis.defineCommand("failMollifierEntry", {
16671
+ numberOfKeys: 1,
16672
+ lua: `
16673
+ local entryKey = KEYS[1]
16674
+ local errorPayload = ARGV[1]
16675
+
16676
+ -- Guard: never create a partial entry. If the hash expired between
16677
+ -- pop and fail, the run is gone \u2014 nothing to mark FAILED.
16678
+ if redis.call('EXISTS', entryKey) == 0 then
16679
+ return 0
16680
+ end
16681
+
16682
+ redis.call('HSET', entryKey, 'status', 'FAILED', 'lastError', errorPayload)
16683
+ return 1
16684
+ `
16685
+ });
16686
+ this.redis.defineCommand("mollifierEvaluateTrip", {
16687
+ numberOfKeys: 2,
16688
+ lua: `
16689
+ local rateKey = KEYS[1]
16690
+ local trippedKey = KEYS[2]
16691
+ local windowMs = tonumber(ARGV[1])
16692
+ local threshold = tonumber(ARGV[2])
16693
+ local holdMs = tonumber(ARGV[3])
16694
+
16695
+ local count = redis.call('INCR', rateKey)
16696
+ if count == 1 then
16697
+ redis.call('PEXPIRE', rateKey, windowMs)
16698
+ end
16699
+
16700
+ if count > threshold then
16701
+ redis.call('PSETEX', trippedKey, holdMs, '1')
16702
+ end
16703
+
16704
+ local tripped = redis.call('EXISTS', trippedKey)
16705
+ return {count, tripped}
16706
+ `
16707
+ });
16708
+ }
16709
+ };
16710
+ var MollifierDrainer = class {
16711
+ buffer;
16712
+ handler;
16713
+ maxAttempts;
16714
+ isRetryable;
16715
+ pollIntervalMs;
16716
+ maxOrgsPerTick;
16717
+ logger;
16718
+ limit;
16719
+ // Rotation state. `orgCursor` advances through the active-orgs list.
16720
+ // Each org has its own internal cursor in `perOrgEnvCursors` for
16721
+ // cycling through that org's envs. Both reset on `start()`.
16722
+ orgCursor = 0;
16723
+ perOrgEnvCursors = /* @__PURE__ */ new Map();
16724
+ isRunning = false;
16725
+ stopping = false;
16726
+ loopPromise = null;
16727
+ constructor(options) {
16728
+ this.buffer = options.buffer;
16729
+ this.handler = options.handler;
16730
+ this.maxAttempts = options.maxAttempts;
16731
+ this.isRetryable = options.isRetryable;
16732
+ this.pollIntervalMs = options.pollIntervalMs ?? 100;
16733
+ this.maxOrgsPerTick = options.maxOrgsPerTick ?? 500;
16734
+ this.logger = options.logger ?? new Logger("MollifierDrainer", "debug");
16735
+ this.limit = pLimit(options.concurrency);
16736
+ }
16737
+ async runOnce() {
16738
+ const orgs = await this.buffer.listOrgs();
16739
+ if (orgs.length === 0) return { drained: 0, failed: 0 };
16740
+ const orgSlice = this.takeOrgSlice(orgs);
16741
+ const envsByOrg = await Promise.all(
16742
+ orgSlice.map((orgId) => this.buffer.listEnvsForOrg(orgId))
16743
+ );
16744
+ const targets = [];
16745
+ for (let i = 0; i < orgSlice.length; i++) {
16746
+ const orgId = orgSlice[i];
16747
+ const envsForOrg = envsByOrg[i];
16748
+ if (envsForOrg.length === 0) continue;
16749
+ const envId = this.pickEnvForOrg(orgId, envsForOrg);
16750
+ targets.push(envId);
16751
+ }
16752
+ const inflight = [];
16753
+ for (const envId of targets) {
16754
+ inflight.push(this.limit(() => this.processOneFromEnv(envId)));
16755
+ }
16756
+ const results = await Promise.all(inflight);
16757
+ return {
16758
+ drained: results.filter((r) => r === "drained").length,
16759
+ failed: results.filter((r) => r === "failed").length
16760
+ };
16761
+ }
16762
+ start() {
16763
+ if (this.isRunning) return;
16764
+ this.isRunning = true;
16765
+ this.stopping = false;
16766
+ this.orgCursor = 0;
16767
+ this.perOrgEnvCursors = /* @__PURE__ */ new Map();
16768
+ this.loopPromise = this.loop();
16769
+ }
16770
+ // Signal the loop to exit (`stopping = true`) and wait for it. With no
16771
+ // timeout, wait indefinitely for the in-flight `runOnce` and its handlers
16772
+ // to settle — same semantic as FairQueue / BatchQueue's `stop()`. With a
16773
+ // timeout, race the loop promise against a deadline so a hung handler
16774
+ // can't wedge the process past its termination grace period.
16775
+ async stop(options = {}) {
16776
+ if (!this.isRunning || !this.loopPromise) return;
16777
+ this.stopping = true;
16778
+ if (options.timeoutMs == null) {
16779
+ await this.loopPromise;
16780
+ return;
16781
+ }
16782
+ const timeoutSentinel = Symbol("mollifier.stop.timeout");
16783
+ let timeoutHandle;
16784
+ const timeoutPromise = new Promise((resolve) => {
16785
+ timeoutHandle = setTimeout(() => resolve(timeoutSentinel), options.timeoutMs);
16786
+ });
16787
+ try {
16788
+ const winner = await Promise.race([
16789
+ this.loopPromise.then(() => "done"),
16790
+ timeoutPromise
16791
+ ]);
16792
+ if (winner === timeoutSentinel) {
16793
+ this.logger.warn(
16794
+ "MollifierDrainer.stop: deadline exceeded; returning while loop iteration is in flight",
16795
+ { timeoutMs: options.timeoutMs }
16796
+ );
16797
+ }
16798
+ } finally {
16799
+ if (timeoutHandle) clearTimeout(timeoutHandle);
16800
+ }
16801
+ }
16802
+ // Transient Redis errors (e.g. a connection blip in `listOrgs` /
16803
+ // `listEnvsForOrg` / `pop`) must not kill the polling loop permanently.
16804
+ // We log each `runOnce` failure, back off so we don't spin tight on a
16805
+ // sustained outage, and resume. The loop only exits when `stop()` flips
16806
+ // `stopping`.
16807
+ async loop() {
16808
+ try {
16809
+ let consecutiveErrors = 0;
16810
+ while (!this.stopping) {
16811
+ try {
16812
+ const result = await this.runOnce();
16813
+ consecutiveErrors = 0;
16814
+ if (result.drained === 0 && result.failed === 0) {
16815
+ await this.delay(this.pollIntervalMs);
16816
+ }
16817
+ } catch (err) {
16818
+ consecutiveErrors += 1;
16819
+ this.logger.error("MollifierDrainer.runOnce failed; backing off", {
16820
+ err,
16821
+ consecutiveErrors
16822
+ });
16823
+ await this.delay(this.backoffMs(consecutiveErrors));
16824
+ }
16825
+ }
16826
+ } finally {
16827
+ this.isRunning = false;
16828
+ }
16829
+ }
16830
+ // Exponential backoff capped at 5s. Keeps the loop responsive after a
16831
+ // brief blip while preventing a tight retry loop during a long Redis
16832
+ // outage. 1 → 200ms, 2 → 400ms, 3 → 800ms, 4 → 1.6s, 5 → 3.2s, 6+ → 5s.
16833
+ backoffMs(consecutiveErrors) {
16834
+ const base = Math.max(this.pollIntervalMs, 100);
16835
+ const capped = Math.min(base * 2 ** (consecutiveErrors - 1), 5e3);
16836
+ return capped;
16837
+ }
16838
+ delay(ms) {
16839
+ return new Promise((resolve) => setTimeout(resolve, ms));
16840
+ }
16841
+ // Take up to `maxOrgsPerTick` orgs starting at the current cursor, with
16842
+ // wrap-around. Cursor advances by 1 each tick so every org reaches
16843
+ // every slot position (0..sliceSize-1) over a full cycle — no
16844
+ // head-of-line bias within the slice. Orgs are sorted before slicing
16845
+ // so rotation is deterministic regardless of Redis SET iteration order.
16846
+ takeOrgSlice(orgs) {
16847
+ const sorted = [...orgs].sort();
16848
+ const n = sorted.length;
16849
+ const sliceSize = Math.min(this.maxOrgsPerTick, n);
16850
+ const start = this.orgCursor % n;
16851
+ this.orgCursor = (this.orgCursor + 1) % Math.max(n, 1);
16852
+ const end = start + sliceSize;
16853
+ if (end <= n) return sorted.slice(start, end);
16854
+ return [...sorted.slice(start), ...sorted.slice(0, end - n)];
16855
+ }
16856
+ // Pick one env from the org's active-envs list, rotating per org via
16857
+ // the per-org cursor. Each org's cursor advances by 1 each visit, so
16858
+ // an org with N envs cycles through them across N visits.
16859
+ pickEnvForOrg(orgId, envsForOrg) {
16860
+ const sorted = [...envsForOrg].sort();
16861
+ const cursor = this.perOrgEnvCursors.get(orgId) ?? 0;
16862
+ const idx = cursor % sorted.length;
16863
+ this.perOrgEnvCursors.set(orgId, (cursor + 1) % sorted.length);
16864
+ return sorted[idx];
16865
+ }
16866
+ // A failure for one env (e.g. a Redis hiccup mid-batch in `pop`, or in
16867
+ // `requeue`/`fail` during error recovery inside `processEntry`) must not
16868
+ // poison the rest of the batch — `Promise.all` would otherwise reject and
16869
+ // bubble all the way to `loop()`. Catch both stages here so the failed env
16870
+ // is just counted as "failed" for this tick and we move on.
16871
+ async processOneFromEnv(envId) {
16872
+ let entry;
16873
+ try {
16874
+ entry = await this.buffer.pop(envId);
16875
+ } catch (err) {
16876
+ this.logger.error("MollifierDrainer.pop failed", { envId, err });
16877
+ return "failed";
16878
+ }
16879
+ if (!entry) return "empty";
16880
+ try {
16881
+ return await this.processEntry(entry);
16882
+ } catch (err) {
16883
+ this.logger.error("MollifierDrainer.processEntry failed", {
16884
+ envId,
16885
+ runId: entry.runId,
16886
+ err
16887
+ });
16888
+ return "failed";
16889
+ }
16890
+ }
16891
+ async processEntry(entry) {
16892
+ try {
16893
+ const payload = deserialiseSnapshot(entry.payload);
16894
+ await this.handler({
16895
+ runId: entry.runId,
16896
+ envId: entry.envId,
16897
+ orgId: entry.orgId,
16898
+ payload,
16899
+ attempts: entry.attempts,
16900
+ createdAt: entry.createdAt
16901
+ });
16902
+ await this.buffer.ack(entry.runId);
16903
+ return "drained";
16904
+ } catch (err) {
16905
+ const nextAttempts = entry.attempts + 1;
16906
+ if (this.isRetryable(err) && nextAttempts < this.maxAttempts) {
16907
+ await this.buffer.requeue(entry.runId);
16908
+ this.logger.warn("MollifierDrainer: retryable error, requeued", {
16909
+ runId: entry.runId,
16910
+ attempts: nextAttempts
16911
+ });
16912
+ return "failed";
16913
+ }
16914
+ const code = err instanceof Error ? err.name : "Unknown";
16915
+ const message = err instanceof Error ? err.message : String(err);
16916
+ await this.buffer.fail(entry.runId, { code, message });
16917
+ this.logger.error("MollifierDrainer: terminal failure", {
16918
+ runId: entry.runId,
16919
+ code,
16920
+ message
16921
+ });
16922
+ return "failed";
16923
+ }
16924
+ }
16925
+ };
16357
16926
 
16358
- export { BaseScheduler, BatchedSpanManager, CallbackFairQueueKeyProducer, ConcurrencyManager, CronSchema, CustomRetry, DRRScheduler, DefaultFairQueueKeyProducer, ExponentialBackoffRetry, FairQueue, FairQueueAttributes, FairQueueTelemetry, FixedDelayRetry, ImmediateRetry, LinearBackoffRetry, MasterQueue, MessagingAttributes, NoRetry, NoopScheduler, RoundRobinScheduler, SimpleQueue, TenantDispatch, VisibilityManager, WeightedScheduler, Worker, WorkerQueueManager, createDefaultRetryStrategy, defaultRetryOptions, isAbortError, noopTelemetry };
16927
+ export { BaseScheduler, BatchedSpanManager, BufferEntryError, BufferEntrySchema, BufferEntryStatus, CallbackFairQueueKeyProducer, ConcurrencyManager, CronSchema, CustomRetry, DRRScheduler, DefaultFairQueueKeyProducer, ExponentialBackoffRetry, FairQueue, FairQueueAttributes, FairQueueTelemetry, FixedDelayRetry, ImmediateRetry, LinearBackoffRetry, MasterQueue, MessagingAttributes, MollifierBuffer, MollifierDrainer, NoRetry, NoopScheduler, RoundRobinScheduler, SimpleQueue, TenantDispatch, VisibilityManager, WeightedScheduler, Worker, WorkerQueueManager, createDefaultRetryStrategy, defaultRetryOptions, deserialiseSnapshot, isAbortError, noopTelemetry, serialiseSnapshot };
16359
16928
  //# sourceMappingURL=index.js.map
16360
16929
  //# sourceMappingURL=index.js.map