There's an async method in some external library, which is not thread-safe (corrupts internal state when being called concurrently) and is very unlikely to be ever fixed in the near future. Yet I want the rest of my codebase to freely use it in any possible parallel vs. sequential combinations (in other words, I want my methods that use that method to still be thread-safe).
So I need to wrap that problematic method with some Active Object implementation, so that its invocations are always aligned (executed one after another).
My take on this is to use a chain of Promises:
var workQueue: Promise<any> = Promise.resolve();
function executeSequentially<T>(action: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
workQueue = workQueue.then(() => {
return action().then(resolve, reject);
});
});
}
And then instead of directly calling that method:
const myPromise = asyncThreadUnsafeMethod(123);
I'll be calling it like this:
const myPromise = executeSequentially(() => asyncThreadUnsafeMethod(123));
But am I missing something? Any potential problems with this approach? Any better ideas?
CodePudding user response:
There are no problems with this approach, using a promise queue is a standard pattern that will solve your problem.
While your code does work fine, I would recommend to avoid the Promise
constructor antipattern:
let workQueue: Promise<void> = Promise.resolve();
const ignore = _ => {};
function executeSequentially<T>(action: () => Promise<T>): Promise<T> {
const result = workQueue.then(action);
workQueue = result.then(ignore, ignore);
return result;
}
Also you say that you'd be "calling the method" like executeSequentially(() => asyncThreadUnsafeMethod(123));
, but this does leave the potential for mistakes where you forget the executeSequentially
wrapper and still call the unsafe method directly. Instead I'd recommend to introduce an indirection where you wrap the entire safe call in a function and then export only that, having your codebase only interact with that facade and never with the library itself. To help with the creation of such a facade, I'd curry the executeSequentially
function:
const ignore = _ => {};
function makeSequential<T, A extends unknown[]>(fn: (...args: A) => Promise<T>) => (...args: A) => Promise<T> {
let workQueue: Promise<void> = Promise.resolve();
return (...args) => {
const result = workQueue.then(() => fn(...args));
workQueue = result.then(ignore, ignore);
return result;
};
}
export const asyncSafeMethod = makeSequential(asyncThreadUnsafeMethod);
or just directly implement it for that one case
const ignore = _ => {};
let workQueue: Promise<void> = Promise.resolve();
export function asyncSafeMethod(x: number) {
const result = workQueue.then(() => asyncThreadUnsafeMethod(x));
workQueue = result.then(ignore, ignore);
return result;
}
import { asyncSafeMethod } from …;
asyncSafeMethod(123);