Skip to content

Commit b03beab

Browse files
committed
improve polling behavior, at least 0.5 if no long polling supported
1 parent 5927524 commit b03beab

3 files changed

Lines changed: 131 additions & 3 deletions

File tree

src/core/queue.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ export abstract class Queue<TJobMap = Record<string, any>, TJobRequest extends B
3333
protected plugins: QueuePlugin[];
3434
protected pluginDisposers: Array<() => Promise<void>> = [];
3535
public readonly name?: string;
36+
37+
/**
38+
* Indicates whether this queue driver supports long polling.
39+
* Drivers that support long polling (like SQS) can efficiently wait for messages.
40+
* Drivers that don't support long polling will have a minimum 0.5s sleep between polls.
41+
*/
42+
protected supportsLongPolling = false;
3643

3744
/**
3845
* Creates a new Queue instance.
@@ -195,8 +202,14 @@ export abstract class Queue<TJobMap = Record<string, any>, TJobRequest extends B
195202
const message = await this.reserve(timeout);
196203
if (!message) {
197204
if (!repeat) break;
198-
if (timeout > 0) {
199-
await this.sleep(timeout * 1000);
205+
206+
// Apply minimum sleep time for drivers that don't support long polling
207+
const sleepMs = this.supportsLongPolling
208+
? timeout * 1000
209+
: Math.max(500, timeout * 1000);
210+
211+
if (sleepMs > 0) {
212+
await this.sleep(sleepMs);
200213
}
201214
continue;
202215
}
@@ -326,7 +339,7 @@ export abstract class Queue<TJobMap = Record<string, any>, TJobRequest extends B
326339
return true; // Job is considered handled (failed)
327340
}
328341

329-
private async sleep(ms: number): Promise<void> {
342+
protected async sleep(ms: number): Promise<void> {
330343
return new Promise(resolve => setTimeout(resolve, ms));
331344
}
332345

src/drivers/sqs.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export class SqsQueue<TJobMap = Record<string, any>> extends Queue<
2828
options: QueueOptions = {}
2929
) {
3030
super(options);
31+
// SQS supports long polling via WaitTimeSeconds
32+
this.supportsLongPolling = true;
3133
}
3234

3335
protected async pushMessage(payload: string, meta: JobMeta): Promise<string> {

tests/core/queue.test.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ class TestQueue extends Queue<TestJobs, DbJobRequest<any>> {
3838
}
3939
}
4040

41+
class TestQueueWithLongPolling extends TestQueue {
42+
constructor(options = {}) {
43+
super(options);
44+
this.supportsLongPolling = true;
45+
}
46+
}
47+
4148
describe('Queue', () => {
4249
let queue: TestQueue;
4350

@@ -221,4 +228,110 @@ describe('Queue', () => {
221228
);
222229
});
223230
});
231+
232+
describe('polling behavior', () => {
233+
// Plugin to track polling times
234+
class PollTrackingPlugin {
235+
public pollTimes: number[] = [];
236+
public pollCount = 0;
237+
238+
async beforePoll(): Promise<'continue' | 'stop'> {
239+
this.pollTimes.push(Date.now());
240+
this.pollCount++;
241+
242+
// Stop after a few polls to prevent infinite loop
243+
if (this.pollCount >= 3) {
244+
return 'stop';
245+
}
246+
return 'continue';
247+
}
248+
249+
getTimeBetweenPolls(index1: number, index2: number): number {
250+
if (index1 >= this.pollTimes.length || index2 >= this.pollTimes.length) {
251+
return -1;
252+
}
253+
return this.pollTimes[index2]! - this.pollTimes[index1]!;
254+
}
255+
}
256+
257+
it('should sleep for at least 500ms when timeout is 0 (non-long-polling)', async () => {
258+
const plugin = new PollTrackingPlugin();
259+
const queue = new TestQueue({ plugins: [plugin] });
260+
queue.onJob('test-job', async () => {});
261+
262+
// Run with repeat=true and timeout=0
263+
await queue.run(true, 0);
264+
265+
// Check time between polls
266+
const timeBetweenPolls = plugin.getTimeBetweenPolls(0, 1);
267+
expect(timeBetweenPolls).toBeGreaterThanOrEqual(500);
268+
expect(timeBetweenPolls).toBeLessThan(600); // Allow some margin
269+
});
270+
271+
it('should sleep for at least 500ms when timeout < 0.5s (non-long-polling)', async () => {
272+
const plugin = new PollTrackingPlugin();
273+
const queue = new TestQueue({ plugins: [plugin] });
274+
queue.onJob('test-job', async () => {});
275+
276+
// Run with timeout=0.2 (200ms)
277+
await queue.run(true, 0.2);
278+
279+
// Should still sleep for 500ms, not 200ms
280+
const timeBetweenPolls = plugin.getTimeBetweenPolls(0, 1);
281+
expect(timeBetweenPolls).toBeGreaterThanOrEqual(500);
282+
expect(timeBetweenPolls).toBeLessThan(600);
283+
});
284+
285+
it('should use timeout value when > 0.5s (non-long-polling)', async () => {
286+
const plugin = new PollTrackingPlugin();
287+
const queue = new TestQueue({ plugins: [plugin] });
288+
queue.onJob('test-job', async () => {});
289+
290+
// Run with timeout=2 (2 seconds)
291+
await queue.run(true, 2);
292+
293+
// Should sleep for 2000ms
294+
const timeBetweenPolls = plugin.getTimeBetweenPolls(0, 1);
295+
expect(timeBetweenPolls).toBeGreaterThanOrEqual(2000);
296+
expect(timeBetweenPolls).toBeLessThan(2100);
297+
});
298+
299+
it('should not sleep when timeout is 0 (long-polling)', async () => {
300+
const plugin = new PollTrackingPlugin();
301+
const queue = new TestQueueWithLongPolling({ plugins: [plugin] });
302+
queue.onJob('test-job', async () => {});
303+
304+
// Run with timeout=0
305+
await queue.run(true, 0);
306+
307+
// With long polling and timeout=0, polls should be immediate
308+
const timeBetweenPolls = plugin.getTimeBetweenPolls(0, 1);
309+
expect(timeBetweenPolls).toBeLessThan(50); // Should be nearly instant
310+
});
311+
312+
it('should use exact timeout value (long-polling)', async () => {
313+
const plugin = new PollTrackingPlugin();
314+
const queue = new TestQueueWithLongPolling({ plugins: [plugin] });
315+
queue.onJob('test-job', async () => {});
316+
317+
// Run with timeout=0.2 (200ms)
318+
await queue.run(true, 0.2);
319+
320+
// Should sleep for exactly 200ms
321+
const timeBetweenPolls = plugin.getTimeBetweenPolls(0, 1);
322+
expect(timeBetweenPolls).toBeGreaterThanOrEqual(200);
323+
expect(timeBetweenPolls).toBeLessThan(250);
324+
});
325+
326+
it('should not sleep in single run mode', async () => {
327+
const plugin = new PollTrackingPlugin();
328+
const queue = new TestQueue({ plugins: [plugin] });
329+
330+
// Run with repeat=false
331+
await queue.run(false, 0);
332+
333+
// Should only poll once
334+
expect(plugin.pollCount).toBe(1);
335+
});
336+
});
224337
});

0 commit comments

Comments
 (0)