mirror of
https://github.com/bendtherules/JSQuestions.git
synced 2026-08-18 13:53:51 +00:00
276 lines
8.7 KiB
JavaScript
276 lines
8.7 KiB
JavaScript
const TaskScheduler = require('./async-scheduler');
|
|
|
|
// Suppress unhandled rejection warnings in tests
|
|
process.on('unhandledRejection', () => {});
|
|
|
|
describe('TaskScheduler', () => {
|
|
describe('constructor', () => {
|
|
it('should create a scheduler with custom concurrency', () => {
|
|
const scheduler = new TaskScheduler(5);
|
|
expect(scheduler.concurrencyLimit).toBe(5);
|
|
});
|
|
|
|
it('should start in paused mode by default', () => {
|
|
const scheduler = new TaskScheduler();
|
|
expect(scheduler.isProcessing).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('addTask', () => {
|
|
it('should return a promise', () => {
|
|
const scheduler = new TaskScheduler();
|
|
const promise = scheduler.addTask(() => Promise.resolve('result'));
|
|
expect(promise).toBeInstanceOf(Promise);
|
|
});
|
|
|
|
it('should resolve when task completes successfully', async () => {
|
|
const scheduler = new TaskScheduler();
|
|
scheduler.play();
|
|
const promise = scheduler.addTask(() => Promise.resolve('success'));
|
|
await expect(promise).resolves.toBe('success');
|
|
});
|
|
|
|
it('should reject when task fails', async () => {
|
|
const scheduler = new TaskScheduler();
|
|
scheduler.play();
|
|
const promise = scheduler.addTask(() => Promise.reject(new Error('task failed')));
|
|
await expect(promise).rejects.toThrow('task failed');
|
|
});
|
|
|
|
it('should capture synchronous throws as rejections', async () => {
|
|
const scheduler = new TaskScheduler();
|
|
scheduler.play();
|
|
const promise = scheduler.addTask(() => {
|
|
throw new Error('sync error');
|
|
});
|
|
await expect(promise).rejects.toThrow('sync error');
|
|
});
|
|
|
|
it('should queue task if scheduler is paused', async () => {
|
|
const scheduler = new TaskScheduler();
|
|
const mockFn = jest.fn(() => Promise.resolve());
|
|
scheduler.addTask(mockFn);
|
|
await new Promise(r => setTimeout(r, 50));
|
|
expect(mockFn).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('play and pause', () => {
|
|
it('should start processing when play() is called', async () => {
|
|
const scheduler = new TaskScheduler();
|
|
const mockFn = jest.fn(() => Promise.resolve());
|
|
scheduler.addTask(mockFn);
|
|
|
|
expect(mockFn).not.toHaveBeenCalled();
|
|
|
|
scheduler.play();
|
|
await new Promise(r => setTimeout(r, 10));
|
|
|
|
expect(mockFn).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should stop starting new tasks when pause() is called', async () => {
|
|
const scheduler = new TaskScheduler(1);
|
|
const mockFn1 = jest.fn(() => new Promise(r => setTimeout(r, 50)));
|
|
const mockFn2 = jest.fn(() => Promise.resolve());
|
|
|
|
scheduler.play();
|
|
scheduler.addTask(mockFn1);
|
|
scheduler.addTask(mockFn2);
|
|
scheduler.pause();
|
|
|
|
await new Promise(r => setTimeout(r, 100));
|
|
expect(mockFn2).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should resume processing when play() is called again', async () => {
|
|
const scheduler = new TaskScheduler(1);
|
|
const mockFn1 = jest.fn(() => new Promise(r => setTimeout(() => r(), 50)));
|
|
const mockFn2 = jest.fn(() => Promise.resolve());
|
|
|
|
scheduler.play();
|
|
scheduler.addTask(mockFn1);
|
|
scheduler.pause();
|
|
scheduler.addTask(mockFn2);
|
|
|
|
await new Promise(r => setTimeout(r, 60));
|
|
expect(mockFn2).not.toHaveBeenCalled();
|
|
|
|
scheduler.play();
|
|
await new Promise(r => setTimeout(r, 60));
|
|
expect(mockFn2).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('concurrency', () => {
|
|
it('should execute up to concurrency limit simultaneously', async () => {
|
|
const scheduler = new TaskScheduler(2);
|
|
const executionTimes = [];
|
|
|
|
const createTask = (id) => async () => {
|
|
executionTimes.push({ id, event: "start" });
|
|
await new Promise(r => setTimeout(r, 50));
|
|
executionTimes.push({ id, event: "end" });
|
|
};
|
|
|
|
scheduler.play();
|
|
await Promise.all([
|
|
scheduler.addTask(createTask(1)),
|
|
scheduler.addTask(createTask(2)),
|
|
scheduler.addTask(createTask(3)),
|
|
]);
|
|
|
|
// Expected order:
|
|
// 1. Task 1 starts
|
|
// 2. Task 2 starts (parallel, within limit)
|
|
// 3. Task 1 or 2 ends
|
|
// 4. Task 3 starts (frees up a slot)
|
|
// 5. Task 3 ends
|
|
expect(executionTimes[0]).toEqual({id: 1, event: "start"});
|
|
expect(executionTimes[1]).toEqual({id: 2, event: "start"});
|
|
expect(executionTimes[2]).toEqual({id: 1, event: "end"});
|
|
expect(executionTimes[3]).toEqual({id: 3, event: "start"});
|
|
expect(executionTimes[4]).toEqual({id: 2, event: "end"});
|
|
expect(executionTimes[5]).toEqual({id: 3, event: "end"});
|
|
});
|
|
|
|
it('should queue tasks when concurrency limit is reached', async () => {
|
|
const scheduler = new TaskScheduler(1);
|
|
const execOrder = [];
|
|
|
|
const createTask = (id) => async () => {
|
|
execOrder.push(`start-${id}`);
|
|
await new Promise(r => setTimeout(r, 10));
|
|
execOrder.push(`end-${id}`);
|
|
};
|
|
|
|
scheduler.play();
|
|
await Promise.all([
|
|
scheduler.addTask(createTask(1)),
|
|
scheduler.addTask(createTask(2)),
|
|
scheduler.addTask(createTask(3)),
|
|
]);
|
|
|
|
expect(execOrder).toEqual([
|
|
'start-1', 'end-1',
|
|
'start-2', 'end-2',
|
|
'start-3', 'end-3',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('FIFO ordering', () => {
|
|
it('should execute tasks in FIFO order', async () => {
|
|
const scheduler = new TaskScheduler(1);
|
|
const execOrder = [];
|
|
|
|
const createTask = (id) => () => {
|
|
execOrder.push(id);
|
|
return Promise.resolve();
|
|
};
|
|
|
|
scheduler.play();
|
|
await Promise.all([
|
|
scheduler.addTask(createTask(1)),
|
|
scheduler.addTask(createTask(2)),
|
|
scheduler.addTask(createTask(3)),
|
|
scheduler.addTask(createTask(4)),
|
|
]);
|
|
|
|
expect(execOrder).toEqual([1, 2, 3, 4]);
|
|
});
|
|
|
|
it('should maintain FIFO order even with variable task durations', async () => {
|
|
const scheduler = new TaskScheduler(1);
|
|
const execOrder = [];
|
|
|
|
const createTask = (id, duration) => async () => {
|
|
execOrder.push(`start-${id}`);
|
|
await new Promise(r => setTimeout(r, duration));
|
|
execOrder.push(`end-${id}`);
|
|
};
|
|
|
|
scheduler.play();
|
|
await Promise.all([
|
|
scheduler.addTask(createTask(1, 30)),
|
|
scheduler.addTask(createTask(2, 10)),
|
|
scheduler.addTask(createTask(3, 20)),
|
|
]);
|
|
|
|
expect(execOrder).toEqual([
|
|
'start-1', 'end-1',
|
|
'start-2', 'end-2',
|
|
'start-3', 'end-3',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('automatic task pickup after completion', () => {
|
|
it('should pick next task from queue when running task completes', async () => {
|
|
const scheduler = new TaskScheduler(1);
|
|
const mockFn2 = jest.fn(() => Promise.resolve());
|
|
|
|
scheduler.play();
|
|
await scheduler.addTask(() => new Promise(r => setTimeout(r, 20)));
|
|
const p2 = scheduler.addTask(mockFn2);
|
|
|
|
await p2;
|
|
expect(mockFn2).toHaveBeenCalled();
|
|
});
|
|
|
|
// it('should pick next task even if current task rejects', async () => {
|
|
// const scheduler = new TaskScheduler(1);
|
|
// const mockFn2 = jest.fn(() => Promise.resolve());
|
|
|
|
// const p1 = scheduler.addTask(() => Promise.reject(new Error("this should fail")));
|
|
// p1.catch(() => {}); // suppress unhandled rejection
|
|
// scheduler.play();
|
|
|
|
// try {
|
|
// await p1;
|
|
// } catch (e) {
|
|
// // expected
|
|
// }
|
|
// const p2 = scheduler.addTask(mockFn2);
|
|
|
|
// await p2;
|
|
// expect(mockFn2).toHaveBeenCalled();
|
|
// });
|
|
});
|
|
|
|
describe('edge cases', () => {
|
|
it('should handle concurrency of 1', async () => {
|
|
const scheduler = new TaskScheduler(1);
|
|
const execOrder = [];
|
|
|
|
scheduler.play();
|
|
await Promise.all([
|
|
scheduler.addTask(() => { execOrder.push(1); return Promise.resolve(); }),
|
|
scheduler.addTask(() => { execOrder.push(2); return Promise.resolve(); }),
|
|
]);
|
|
|
|
expect(execOrder).toEqual([1, 2]);
|
|
});
|
|
|
|
it('should handle large concurrency', async () => {
|
|
const scheduler = new TaskScheduler(100);
|
|
const promises = [];
|
|
|
|
scheduler.play();
|
|
for (let i = 0; i < 50; i++) {
|
|
promises.push(scheduler.addTask(() => Promise.resolve(i)));
|
|
}
|
|
|
|
const results = await Promise.all(promises);
|
|
expect(results.length).toBe(50);
|
|
});
|
|
|
|
it('should handle empty queue gracefully', () => {
|
|
const scheduler = new TaskScheduler();
|
|
scheduler.play();
|
|
// Should not throw
|
|
expect(() => scheduler._processQueue()).not.toThrow();
|
|
});
|
|
});
|
|
});
|