fix(github-backend): handle race condition in editorial workflow (#2658)
This commit is contained in:
committed by
Shawn Erquhart
parent
0baf651f33
commit
97f1f84b69
@ -0,0 +1,85 @@
|
||||
import { asyncLock } from '../asyncLock';
|
||||
|
||||
jest.useFakeTimers();
|
||||
jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
describe('asyncLock', () => {
|
||||
it('should be able to acquire a new lock', async () => {
|
||||
const lock = asyncLock();
|
||||
|
||||
const acquired = await lock.acquire();
|
||||
|
||||
expect(acquired).toBe(true);
|
||||
});
|
||||
|
||||
it('should not be able to acquire an acquired lock', async () => {
|
||||
const lock = asyncLock();
|
||||
await lock.acquire();
|
||||
|
||||
const promise = lock.acquire();
|
||||
|
||||
// advance by default lock timeout
|
||||
jest.advanceTimersByTime(15000);
|
||||
|
||||
const acquired = await promise;
|
||||
|
||||
expect(acquired).toBe(false);
|
||||
});
|
||||
|
||||
it('should be able to acquire an acquired lock that was released', async () => {
|
||||
const lock = asyncLock();
|
||||
await lock.acquire();
|
||||
|
||||
const promise = lock.acquire();
|
||||
|
||||
// release the lock in the "future"
|
||||
setTimeout(() => lock.release(), 100);
|
||||
|
||||
// advance to the time where the lock will be released
|
||||
jest.advanceTimersByTime(100);
|
||||
|
||||
const acquired = await promise;
|
||||
|
||||
expect(acquired).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept a timeout for acquire', async () => {
|
||||
const lock = asyncLock();
|
||||
await lock.acquire();
|
||||
|
||||
const promise = lock.acquire(50);
|
||||
|
||||
/// advance by lock timeout
|
||||
jest.advanceTimersByTime(50);
|
||||
|
||||
const acquired = await promise;
|
||||
|
||||
expect(acquired).toBe(false);
|
||||
});
|
||||
|
||||
it('should be able to re-acquire a lock after a timeout', async () => {
|
||||
const lock = asyncLock();
|
||||
await lock.acquire();
|
||||
|
||||
const promise = lock.acquire();
|
||||
|
||||
// advance by default lock timeout
|
||||
jest.advanceTimersByTime(15000);
|
||||
|
||||
let acquired = await promise;
|
||||
|
||||
expect(acquired).toBe(false);
|
||||
|
||||
acquired = await lock.acquire();
|
||||
expect(acquired).toBe(true);
|
||||
});
|
||||
|
||||
it('should suppress "leave called too many times" error', async () => {
|
||||
const lock = asyncLock();
|
||||
|
||||
await expect(() => lock.release()).not.toThrow();
|
||||
|
||||
expect(console.warn).toHaveBeenCalledTimes(1);
|
||||
expect(console.warn).toHaveBeenCalledWith('leave called too many times.');
|
||||
});
|
||||
});
|
41
packages/netlify-cms-lib-util/src/asyncLock.js
Normal file
41
packages/netlify-cms-lib-util/src/asyncLock.js
Normal file
@ -0,0 +1,41 @@
|
||||
import semaphore from 'semaphore';
|
||||
|
||||
export const asyncLock = () => {
|
||||
let lock = semaphore(1);
|
||||
|
||||
const acquire = (timeout = 15000) => {
|
||||
const promise = new Promise(resolve => {
|
||||
// this makes sure a caller doesn't gets stuck forever awaiting on the lock
|
||||
const timeoutId = setTimeout(() => {
|
||||
// we reset the lock in that case to allow future consumers to use it without being blocked
|
||||
lock = semaphore(1);
|
||||
resolve(false);
|
||||
}, timeout);
|
||||
|
||||
lock.take(() => {
|
||||
clearTimeout(timeoutId);
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
const release = () => {
|
||||
try {
|
||||
// suppress too many calls to leave error
|
||||
lock.leave();
|
||||
} catch (e) {
|
||||
// calling 'leave' too many times might not be good behavior
|
||||
// but there is no reason to completely fail on it
|
||||
if (e.message !== 'leave called too many times.') {
|
||||
throw e;
|
||||
} else {
|
||||
console.warn('leave called too many times.');
|
||||
lock = semaphore(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return { acquire, release };
|
||||
};
|
@ -26,6 +26,7 @@ import {
|
||||
} from './backendUtil';
|
||||
import loadScript from './loadScript';
|
||||
import getBlobSHA from './getBlobSHA';
|
||||
import { asyncLock } from './asyncLock';
|
||||
|
||||
export const NetlifyCmsLibUtil = {
|
||||
APIError,
|
||||
@ -77,4 +78,5 @@ export {
|
||||
responseParser,
|
||||
loadScript,
|
||||
getBlobSHA,
|
||||
asyncLock,
|
||||
};
|
||||
|
Reference in New Issue
Block a user