Skip to content
This repository has been archived by the owner on Oct 25, 2023. It is now read-only.

Commit

Permalink
add getLockFileGuard method to util
Browse files Browse the repository at this point in the history
  • Loading branch information
jlipps committed Apr 22, 2020
1 parent 0e7008a commit 6dfdb05
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 2 deletions.
3 changes: 3 additions & 0 deletions lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ let fs = {
const simples = [
'open', 'close', 'access', 'readFile', 'writeFile', 'write', 'read',
'readlink', 'chmod', 'unlink', 'readdir', 'stat', 'rename', 'lstat',
'appendFile'
];
for (const s of simples) {
fs[s] = B.promisify(_fs[s]);
Expand All @@ -158,6 +159,8 @@ for (const s of simples) {
const syncFunctions = [
'createReadStream',
'createWriteStream',
'writeFileSync',
'statSync'
];
for (const s of syncFunctions) {
fs[s] = _fs[s];
Expand Down
61 changes: 60 additions & 1 deletion lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
v1 as uuidV1, v3 as uuidV3,
v4 as uuidV4, v5 as uuidV5
} from 'uuid';
import { waitForCondition } from 'asyncbox';


const W3C_WEB_ELEMENT_IDENTIFIER = 'element-6066-11e4-a52e-4f735466cecf';
Expand Down Expand Up @@ -443,11 +444,69 @@ async function toInMemoryBase64 (srcPath, opts = {}) {
return Buffer.concat(resultBuffers);
}

/**
* Create an async function which, when called, will not proceed until a certain file is no
* longer present on the system. This allows for preventing concurrent behavior across processes
* using a known lockfile path.
*
* @param {string} lockFile The full path to the file used for the lock
* @param {number} timeout [120] The max time in seconds to wait for the lock
* @returns {AsyncFunction} async function that takes another async function defining the locked
* behavior
*/
function getLockFileGuard (lockFile, timeout = 120) {
const waitForNoLock = async function waitForNoLock () {
try {
await waitForCondition(async () => {
try {
await fs.stat(lockFile);
return false;
} catch (ign) {
return true;
}
}, {
waitMs: timeout * 1000,
intervalMs: 750
});
} catch (ign) {
throw new Error(`Could not acquire lock on file '${lockFile}', after ` +
`timeout of ${timeout} seconds`);
}
};

return async (behavior) => {
let lockExists = false;
try {
// we first want to use sync commands to check if the lockfile already exists, so that no
// other call or process can jump in on this with us
fs.statSync(lockFile);
lockExists = true;
} catch (ign) {}

if (lockExists) {
// if the lockfile does exist, then start async spinning until it doesn't
await waitForNoLock();
}

// at this point the lockfile is gone, either because it never was, or because we waited for
// another instance of the lock to clear. now write the lock file (again, do it sync so that in
// the initial case, it happens on the same spin of the event loop)
fs.writeFileSync(lockFile, 'appium lock');

try {
return await behavior();
} finally {
// whether the behavior succeeded or not, get rid of the lock
await fs.unlink(lockFile);
}
};
}

export {
hasValue, escapeSpace, escapeSpecialChars, localIp, cancellableDelay,
multiResolve, safeJsonParse, wrapElement, unwrapElement, filterObject,
toReadableSizeString, isSubPath, W3C_WEB_ELEMENT_IDENTIFIER,
isSameDestination, compareVersions, coerceVersion, quote, unleakString,
jsonStringify, pluralize, GiB, MiB, KiB, toInMemoryBase64,
uuidV1, uuidV3, uuidV4, uuidV5, shellParse,
uuidV1, uuidV3, uuidV4, uuidV5, shellParse, getLockFileGuard
};
102 changes: 101 additions & 1 deletion test/util-e2e-specs.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import B from 'bluebird';
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import path from 'path';
import * as util from '../lib/util';
import { tempDir, fs } from '../index';


chai.should();
chai.use(chaiAsPromised);

describe('#util', function () {
Expand Down Expand Up @@ -33,4 +34,103 @@ describe('#util', function () {
});
});

describe('getLockFileGuard()', function () {
let tmpRoot;
let lockFile;
let testFile;

async function lockExists () {
try {
await fs.stat(lockFile);
return true;
} catch (ign) {
return false;
}
}

async function guardedBehavior (text, msBeforeActing) {
await B.delay(msBeforeActing);
await fs.appendFile(testFile, text, 'utf8');
return text;
}

async function testFileContents () {
return (await fs.readFile(testFile)).toString('utf8');
}

beforeEach(async function () {
tmpRoot = await tempDir.openDir();
lockFile = path.resolve(tmpRoot, 'test.lock');
testFile = path.resolve(tmpRoot, 'test');
await fs.writeFile(testFile, 'a', 'utf8');
});

afterEach(async function () {
try {
await fs.unlink(lockFile);
await fs.unlink(testFile);
} catch (ign) {}
});

it('should lock a file during the given behavior', async function () {
const guard = util.getLockFileGuard(lockFile);
await lockExists().should.eventually.be.false;
const guardPromise = guard(async () => await guardedBehavior('b', 500));
await B.delay(200);
await lockExists().should.eventually.be.true;
await guardPromise;
await lockExists().should.eventually.be.false;
await testFileContents().should.eventually.eql('ab');
});

it('should block other behavior until the lock is released', async function () {
// first prove that without a lock, we get races
await testFileContents().should.eventually.eql('a');
const unguardedPromise1 = guardedBehavior('b', 500);
const unguardedPromise2 = guardedBehavior('c', 100);
await unguardedPromise1;
await unguardedPromise2;
await testFileContents().should.eventually.eql('acb');

// now prove that with a lock, we don't get any interlopers
const guard = util.getLockFileGuard(lockFile);
const guardPromise1 = guard(async () => await guardedBehavior('b', 500));
const guardPromise2 = guard(async () => await guardedBehavior('c', 100));
await guardPromise1;
await guardPromise2;
await testFileContents().should.eventually.eql('acbbc');
});

it('should return the result of the guarded behavior', async function () {
const guard = util.getLockFileGuard(lockFile);
const guardPromise1 = guard(async () => await guardedBehavior('hello', 500));
const guardPromise2 = guard(async () => await guardedBehavior('world', 100));
const ret1 = await guardPromise1;
const ret2 = await guardPromise2;
ret1.should.eql('hello');
ret2.should.eql('world');
});

it('should time out if the lock is not released', async function () {
this.timeout(5000);
const guard = util.getLockFileGuard(lockFile, 0.5);
const p1 = guard(async () => await guardedBehavior('hello', 1200));
const p2 = guard(async () => await guardedBehavior('world', 10));
await p2.should.eventually.be.rejectedWith(/not acquire lock/);
await p1.should.eventually.eql('hello');
});

it('should still release lock if guarded behavior fails', async function () {
this.timeout(5000);
const guard = util.getLockFileGuard(lockFile);
const p1 = guard(async () => {
await B.delay(500);
throw new Error('bad');
});
const p2 = guard(async () => await guardedBehavior('world', 100));
await p1.should.eventually.be.rejectedWith(/bad/);
await p2.should.eventually.eql('world');
});
});

});

0 comments on commit 6dfdb05

Please sign in to comment.