fix(github-backend): handle race condition in editorial workflow (#2658)

This commit is contained in:
Erez Rokah
2019-09-09 22:56:47 +03:00
committed by Shawn Erquhart
parent 0baf651f33
commit 97f1f84b69
4 changed files with 160 additions and 3 deletions

View File

@ -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.');
});
});

View 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 };
};

View File

@ -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,
};