bunqueue 2.8.17 → 2.8.18

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.
@@ -48,6 +48,7 @@ export declare class Worker<T = unknown, R = unknown> extends EventEmitter {
48
48
  private pendingJobs;
49
49
  private pendingJobsHead;
50
50
  private processingScheduled;
51
+ private pendingPull;
51
52
  private lastDrainedEmit;
52
53
  private stalledUnsubscribe;
53
54
  on(event: 'ready' | 'drained' | 'closed', listener: () => void): this;
@@ -102,6 +102,9 @@ export class Worker extends EventEmitter {
102
102
  pendingJobs = [];
103
103
  pendingJobsHead = 0;
104
104
  processingScheduled = false; // Prevent multiple setImmediate calls
105
+ // Slots reserved by in-flight doPullBatch() calls (Issue #98). Subtracted from
106
+ // free slots so overlapping pulls see each other and do not over-lease.
107
+ pendingPull = 0;
105
108
  // Drained event tracking
106
109
  lastDrainedEmit = 0;
107
110
  // Stalled event subscription (BullMQ v5 compatible)
@@ -703,14 +706,46 @@ export class Worker extends EventEmitter {
703
706
  return null;
704
707
  }
705
708
  async doPullBatch() {
706
- const slots = this.opts.concurrency - this.activeJobs;
709
+ // Issue #98: cap the LEASED count (running + buffered + in-flight pulls) at
710
+ // `concurrency`, not just the running count. The old `concurrency - activeJobs`
711
+ // was read once and the pull leases jobs on the broker across an await, so:
712
+ // 1. several concurrent finally->poll->tryProcess runs each read the same
713
+ // stale count and each pull a full batch, and
714
+ // 2. a job just pulled by one run sits in `pendingJobs` (leased, counted by
715
+ // the heartbeat) but not yet in `activeJobs`, so an overlapping pull does
716
+ // not see it.
717
+ // Both leak: with concurrency=3 the worker ends up holding 5-6 jobs leased.
718
+ // `pulledJobIds.size` is the true leased count (active + buffered; a job is
719
+ // removed only on completion), and `pendingPull` reserves slots for pulls
720
+ // still in flight whose jobs are not yet registered.
721
+ //
722
+ // Exception — group pull-ahead: when a group limiter is set AND the buffer is
723
+ // non-empty here (this branch is reached only after getNextEligibleJob() found
724
+ // nothing runnable, so those buffered jobs are group-blocked), the worker must
725
+ // pull ahead to discover jobs from other, runnable groups — otherwise it would
726
+ // wedge on a buffer full of one blocked group. In that case the blocked
727
+ // buffered jobs are not counted (only the running ones are). This preserves the
728
+ // existing group behavior; the reported over-pull (no group limiter) always
729
+ // uses the strict leased cap.
730
+ const groupBlockedBuffer = this.groupLimiter !== null && this.pendingJobsHead < this.pendingJobs.length;
731
+ const leased = groupBlockedBuffer ? this.activeJobs : this.pulledJobIds.size;
732
+ const slots = this.opts.concurrency - leased - this.pendingPull;
707
733
  const batchSize = Math.min(this.opts.batchSize, slots, 1000);
708
734
  if (batchSize <= 0)
709
735
  return [];
710
736
  const config = this.getPullConfig();
711
- return this.embedded
712
- ? pullEmbedded(config, batchSize)
713
- : pullTcp(config, this.tcp, batchSize, this._closing);
737
+ this.pendingPull += batchSize;
738
+ try {
739
+ return this.embedded
740
+ ? await pullEmbedded(config, batchSize)
741
+ : await pullTcp(config, this.tcp, batchSize, this._closing);
742
+ }
743
+ finally {
744
+ // Release the reservation. The pulled jobs are now registered/buffered (or
745
+ // the pull failed); either way the reservation has served its purpose for
746
+ // the duration of the in-flight pull.
747
+ this.pendingPull -= batchSize;
748
+ }
714
749
  }
715
750
  startJob(job, token) {
716
751
  const jobIdStr = String(job.id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunqueue",
3
- "version": "2.8.17",
3
+ "version": "2.8.18",
4
4
  "description": "High-performance job queue for Bun & AI agents. SQLite persistence, cron scheduling, priorities, retries, DLQ, webhooks, native MCP server. Zero external dependencies.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",