I am faced with a challenge on my server where I receive concurrent requests, but certain R/W resources cannot be accessed simultaneously. Implementing locks at the database level is not practical for me, so I am looking to create a function that can turn parallel requests into sequential ones.
My idea involves having queues with individual resource IDs. When a request comes in, if the corresponding queue for the ID is empty, the desired function should be executed. If not, the function should be queued to run after the others have finished.
Despite coming up with some code to achieve this, I am encountering issues and need help figuring out why it's not working as expected.
You can view the code on this example fiddle.
export const sleep = (ms: number): Promise<void> =>
new Promise((resolve) => { setTimeout(resolve, ms); });
type AsyncFn = () => Promise<void>;
const locks = new Map<string, AsyncFn[]>();
const unstackLock = async (id: string) => {
const stack = locks.get(id);
if (!stack) {
return;
}
const nextFn = stack.shift();
if (!nextFn) {
return;
}
try {
await nextFn();
} finally {
await unstackLock(id);
}
};
export const withLock = async (id: string, fn: AsyncFn): Promise<void> => {
if (!locks.has(id)) {
locks.set(id, []);
}
const lock = locks.get(id);
if (!lock) {
// never happens but makes TS happy
throw new Error('Lock is not defined');
}
lock.push(fn);
if (lock.length === 1) {
await unstackLock(id);
}
};
const test = async () => {
const results: number[] = [];
const f1 = withLock('lock', async () => {
await sleep(Math.random() * 100);
results.push(1);
});
const f2 = withLock('lock', async () => {
await sleep(Math.random() * 100);
results.push(2);
});
const f3 = withLock('lock', async () => {
await sleep(Math.random() * 100);
results.push(3);
});
await Promise.all([f1, f2, f3]);
if (results[0] !== 1 || results[1] !== 2 || results[2] !== 3) {
console.log('FAILED', results);
} else {
console.log('SUCCESS');
}
};
test();
I aim to have the results
array filled with [1, 2, 3]
in the same order as long as f1
, f2
, and f3
execute. However, the current execution order appears to be random.