-
Notifications
You must be signed in to change notification settings - Fork 365
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(worker): replace Promise.race with efficient an async fifo
- Loading branch information
Showing
6 changed files
with
272 additions
and
36 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
/** | ||
* AsyncFifoQueue | ||
* | ||
* A minimal FIFO queue for asyncrhonous operations. Allows adding asynchronous operations | ||
* and consume them in the order they are resolved. | ||
* | ||
*/ | ||
|
||
export class AsyncFifoQueue<T> { | ||
private queue: T[] = []; | ||
|
||
private nextPromise: Promise<T> | undefined; | ||
private resolve: ((value: T | undefined) => void) | undefined; | ||
private reject: ((reason?: any) => void) | undefined; | ||
private pending = new Set<Promise<T>>(); | ||
|
||
constructor(private ignoreErrors = false) { | ||
this.newPromise(); | ||
} | ||
|
||
public add(promise: Promise<T>) { | ||
this.pending.add(promise); | ||
|
||
promise | ||
.then(job => { | ||
this.pending.delete(promise); | ||
|
||
if (this.queue.length === 0) { | ||
this.resolvePromise(job); | ||
} | ||
this.queue.push(job); | ||
}) | ||
.catch(err => { | ||
// Ignore errors | ||
if (this.ignoreErrors) { | ||
this.queue.push(undefined); | ||
} | ||
this.pending.delete(promise); | ||
this.rejectPromise(err); | ||
}); | ||
} | ||
|
||
public async waitAll() { | ||
await Promise.all(this.pending); | ||
} | ||
|
||
public numTotal() { | ||
return this.pending.size + this.queue.length; | ||
} | ||
|
||
public numPending() { | ||
return this.pending.size; | ||
} | ||
|
||
public numQueued() { | ||
return this.queue.length; | ||
} | ||
|
||
private resolvePromise(job: T) { | ||
this.resolve(job); | ||
this.newPromise(); | ||
} | ||
|
||
private rejectPromise(err: any) { | ||
this.reject(err); | ||
this.newPromise(); | ||
} | ||
|
||
private newPromise() { | ||
this.nextPromise = new Promise<T>((resolve, reject) => { | ||
this.resolve = resolve; | ||
this.reject = reject; | ||
}); | ||
} | ||
|
||
private async wait() { | ||
return this.nextPromise; | ||
} | ||
|
||
public async fetch(): Promise<T | void> { | ||
if (this.pending.size === 0 && this.queue.length === 0) { | ||
return; | ||
} | ||
while (this.queue.length === 0) { | ||
try { | ||
await this.wait(); | ||
} catch (err) { | ||
// Ignore errors | ||
if (!this.ignoreErrors) { | ||
console.error('Unexpected Error in AsyncFifoQueue', err); | ||
} | ||
} | ||
} | ||
return this.queue.shift(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import { expect } from 'chai'; | ||
import { AsyncFifoQueue } from '../src/classes/async-fifo-queue'; | ||
|
||
describe('AsyncFIFOQueue', () => { | ||
it('add several promises and wait for them to complete', async () => { | ||
const asyncFifoQueue = new AsyncFifoQueue<number>(); | ||
const promises = [1, 2, 3, 4, 5].map( | ||
i => | ||
new Promise<number>(resolve => { | ||
setTimeout(() => resolve(i), i * 100); | ||
}), | ||
); | ||
promises.forEach(p => asyncFifoQueue.add(p)); | ||
|
||
expect(asyncFifoQueue.numPending()).to.be.eql(promises.length); | ||
expect(asyncFifoQueue.numQueued()).to.be.eql(0); | ||
expect(asyncFifoQueue.numTotal()).to.be.eql(promises.length); | ||
|
||
await asyncFifoQueue.waitAll(); | ||
expect(asyncFifoQueue.numPending()).to.be.eql(0); | ||
expect(asyncFifoQueue.numQueued()).to.be.eql(promises.length); | ||
expect(asyncFifoQueue.numTotal()).to.be.eql(promises.length); | ||
}); | ||
|
||
it('add several promises and wait for them to complete in order', async () => { | ||
const asyncFifoQueue = new AsyncFifoQueue<number>(); | ||
const promises = [1, 2, 3, 4, 5].map( | ||
i => | ||
new Promise<number>(resolve => { | ||
setTimeout(() => resolve(i), i * 100); | ||
}), | ||
); | ||
promises.forEach(p => asyncFifoQueue.add(p)); | ||
|
||
expect(asyncFifoQueue.numPending()).to.be.eql(promises.length); | ||
expect(asyncFifoQueue.numQueued()).to.be.eql(0); | ||
expect(asyncFifoQueue.numTotal()).to.be.eql(promises.length); | ||
|
||
const results: number[] = []; | ||
for (let i = 0; i < promises.length; i++) { | ||
results.push((await asyncFifoQueue.fetch())!); | ||
} | ||
|
||
expect(results).to.be.eql([1, 2, 3, 4, 5]); | ||
}); | ||
|
||
it('add several promises with random delays and wait for them to complete in order', async () => { | ||
const asyncFifoQueue = new AsyncFifoQueue<number>(); | ||
|
||
const randomDelays = [250, 100, 570, 50, 400, 10, 300, 125, 460, 200]; | ||
|
||
const promises = randomDelays.map( | ||
i => | ||
new Promise<number>(resolve => { | ||
setTimeout(() => resolve(i), i); | ||
}), | ||
); | ||
promises.forEach(p => asyncFifoQueue.add(p)); | ||
|
||
expect(asyncFifoQueue.numPending()).to.be.eql(promises.length); | ||
expect(asyncFifoQueue.numQueued()).to.be.eql(0); | ||
expect(asyncFifoQueue.numTotal()).to.be.eql(promises.length); | ||
|
||
const results: number[] = []; | ||
for (let i = 0; i < promises.length; i++) { | ||
results.push((await asyncFifoQueue.fetch())!); | ||
} | ||
|
||
expect(results).to.be.eql(randomDelays.sort((a, b) => a - b)); | ||
}); | ||
|
||
it('add several promises while fetching them concurrently', async () => { | ||
const asyncFifoQueue = new AsyncFifoQueue<number>(); | ||
|
||
const randomDelays = [ | ||
250, 100, 570, 50, 400, 10, 300, 125, 460, 200, 60, 100, | ||
]; | ||
const results: number[] = []; | ||
const concurrency = 3; | ||
|
||
for (let i = 0; i < randomDelays.length; i++) { | ||
const delay = randomDelays[i]; | ||
asyncFifoQueue.add( | ||
new Promise<number>(resolve => { | ||
setTimeout(() => resolve(delay), delay); | ||
}), | ||
); | ||
|
||
if ((i + 1) % concurrency === 0) { | ||
for (let j = 0; j < concurrency; j++) { | ||
results.push((await asyncFifoQueue.fetch())!); | ||
} | ||
} | ||
} | ||
|
||
const expected = [100, 250, 570, 10, 50, 400, 125, 300, 460, 60, 100, 200]; | ||
|
||
expect(results).to.be.eql(expected); | ||
}); | ||
|
||
it("should handle promises that get rejected and don't block the queue", async () => { | ||
const asyncFifoQueue = new AsyncFifoQueue<number>(true); | ||
|
||
const randomDelays = [250, 100, 570, 50, 400, 10, 300, 125, 460, 200]; | ||
|
||
for (let i = 0; i < randomDelays.length; i++) { | ||
asyncFifoQueue.add( | ||
new Promise<number>((resolve, reject) => { | ||
setTimeout(() => reject(new Error(`${randomDelays[i]}`)), i); | ||
}), | ||
); | ||
} | ||
|
||
const results: number[] = []; | ||
for (let i = 0; i < randomDelays.length; i++) { | ||
results.push((await asyncFifoQueue.fetch())!); | ||
} | ||
|
||
expect(results).to.be.eql(randomDelays.map(() => void 0)); | ||
}); | ||
}); |
Oops, something went wrong.