-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: ensure models are loaded in the same order the add*-functions ar…
…e called (#3956) * fix: ensure models are loaded in the same order the add*-functions are called
- Loading branch information
1 parent
1bb7b10
commit 4963595
Showing
4 changed files
with
181 additions
and
16 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
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,46 @@ | ||
/*! | ||
* Copyright 2023 Cognite AS | ||
*/ | ||
|
||
import { AsyncSequencer } from './AsyncSequencer'; | ||
|
||
import { jest } from '@jest/globals'; | ||
|
||
describe(AsyncSequencer.name, () => { | ||
test("doesn't mix up calls in the right order", async () => { | ||
const asyncSequencer = new AsyncSequencer(); | ||
|
||
const fn = jest.fn<(n: number) => void>(); | ||
|
||
const sequencer1 = asyncSequencer.getNextSequencer(); | ||
const sequencer2 = asyncSequencer.getNextSequencer(); | ||
|
||
await sequencer1(() => fn(1)); | ||
await sequencer2(() => fn(2)); | ||
|
||
expect(fn).toHaveBeenCalledTimes(2); | ||
expect(fn.mock.calls[0][0]).toBe(1); | ||
expect(fn.mock.calls[1][0]).toBe(2); | ||
}); | ||
|
||
test('reverses calls in the wrong order', async () => { | ||
const asyncSequencer = new AsyncSequencer(); | ||
|
||
const fn = jest.fn<(n: number) => void>(); | ||
|
||
const sequencer1 = asyncSequencer.getNextSequencer(); | ||
const sequencer2 = asyncSequencer.getNextSequencer(); | ||
const sequencer3 = asyncSequencer.getNextSequencer(); | ||
|
||
const res2 = sequencer2(() => fn(2)); | ||
const res3 = sequencer3(() => fn(3)); | ||
const res1 = sequencer1(() => fn(1)); | ||
|
||
await Promise.all([res2, res3, res1]); | ||
|
||
expect(fn).toHaveBeenCalledTimes(3); | ||
expect(fn.mock.calls[0][0]).toBe(1); | ||
expect(fn.mock.calls[1][0]).toBe(2); | ||
expect(fn.mock.calls[2][0]).toBe(3); | ||
}); | ||
}); |
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,84 @@ | ||
/*! | ||
* Copyright 2023 Cognite AS | ||
*/ | ||
|
||
export type SequencerFunction<T> = (region: () => Promise<T> | T) => Promise<T>; | ||
|
||
/** | ||
* AsyncSequencer - helper class for making sure a sequence of operations | ||
* are performed in the same order, while permitting dependant computations | ||
* to be performed in arbitrary order. | ||
* See the following example of a function that loads data and puts it in an | ||
* array:. | ||
* | ||
* ``` | ||
* function loadData(id: any): Promise<void> { | ||
* const result = await expensiveFetchOperation(id); | ||
* this._array.push(result); // Shared array used for all retrieved data | ||
* } | ||
* ``` | ||
* | ||
* Calling `loadData` multiple times sequentially without awaiting may | ||
* cause the retrieved data to be pushed to the array in an arbitrary order. | ||
* This may be fine in some cases, but not in others. | ||
* `AsyncSequencer` guarantees the order in the following way: | ||
* | ||
* ``` | ||
* const asyncSequencer = new AsyncSequencer(); | ||
* ... | ||
* // Same function signature as before | ||
* function loadData(id: any): Promise<void> { | ||
* | ||
* // `getNextSequencer` returns a _sequencer_ function that takes another | ||
* // function (the "critical region") as input and ensures it is run | ||
* // after the critical region of the previous _sequencer_ function | ||
* // retrieved from the `asyncSequencer` object with `getNextSequencer`, | ||
* // and before the next such sequencer's critical region | ||
* const sequencer = asyncSequencer.getNextSequencer<void>(); | ||
* | ||
* // The following line still runs and finishes at arbitrary times | ||
* // across different calls to `loadData` ... | ||
* const result = await expensiveFetchOperation(id); | ||
* | ||
* // ... However, the function given to `sequencer` will always | ||
* // run in the same order as the `sequencer`s were created with | ||
* // `getNextSequencer` | ||
* await sequencer(() => { | ||
* this._array.push(result) | ||
* }); | ||
* } | ||
* ``` | ||
* Note that this approach allows `expensiveFetchOperation` to be run in parallel | ||
* while still guaranteeing the order of the results. | ||
* Also, be aware that if `loadData` had been declared `async`, it is not certain | ||
* that the calls to `getNextSequencer` would have been in the same order as | ||
* the calls to the corresponding `loadData`. | ||
*/ | ||
export class AsyncSequencer { | ||
private _currentPromise: Promise<void> = Promise.resolve(); | ||
|
||
/** | ||
* Returns a `sequencer` function that guarantees that the | ||
* function it is called with is run after the previous `sequencer`'s | ||
* function, and before the next one's. | ||
*/ | ||
getNextSequencer<T>(): SequencerFunction<T> { | ||
const lastPromise = this._currentPromise; | ||
let resolver: () => void; | ||
|
||
this._currentPromise = new Promise(res => { | ||
resolver = res; | ||
}); | ||
|
||
const func = async (region: () => T | Promise<T>): Promise<T> => { | ||
await lastPromise; | ||
const result = await region(); | ||
|
||
resolver(); | ||
|
||
return result; | ||
}; | ||
|
||
return func; | ||
} | ||
} |