-
-
Notifications
You must be signed in to change notification settings - Fork 158
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add beforeAll/afterAll events + file locking (#397)
* Add beforeAll and afterAll hooks * turn off no-promise-executor-return
- Loading branch information
Showing
6 changed files
with
179 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import * as fs from 'fs'; | ||
import * as path from 'path'; | ||
import { Umzug } from './umzug'; | ||
|
||
export interface FileLockerOptions { | ||
path: string; | ||
fs?: typeof fs; | ||
} | ||
|
||
/** | ||
* Simple locker using the filesystem. Only one lock can be held per file. An error will be thrown if the | ||
* lock file already exists. | ||
* | ||
* @example | ||
* const umzug = new Umzug({ ... }) | ||
* FileLocker.attach(umzug, { path: 'path/to/lockfile' }) | ||
* | ||
* @docs | ||
* To wait for the lock to be free instead of throwing, you could extend it (the below example uses `setInterval`, | ||
* but depending on your use-case, you may want to use a library with retry/backoff): | ||
* | ||
* @example | ||
* class WaitingFileLocker extends FileLocker { | ||
* async getLock() { | ||
* return new Promise(resolve => setInterval( | ||
* () => super.getLock().then(resolve).catch(), | ||
* 500, | ||
* ) | ||
* } | ||
* } | ||
* | ||
* const locker = new WaitingFileLocker({ path: 'path/to/lockfile' }) | ||
* locker.attachTo(umzug) | ||
*/ | ||
export class FileLocker { | ||
private readonly lockFile: string; | ||
private readonly fs: typeof fs; | ||
|
||
constructor(params: FileLockerOptions) { | ||
this.lockFile = params.path; | ||
this.fs = params.fs ?? fs; | ||
} | ||
|
||
/** Attach `beforeAll` and `afterAll` events to an umzug instance which use the specified filepath */ | ||
static attach(umzug: Umzug<unknown>, params: FileLockerOptions): void { | ||
const locker = new FileLocker(params); | ||
locker.attachTo(umzug); | ||
} | ||
|
||
/** Attach `beforeAll` and `afterAll` events to an umzug instance */ | ||
attachTo(umzug: Umzug<unknown>): void { | ||
umzug.on('beforeAll', async () => this.getLock()); | ||
umzug.on('afterAll', async () => this.releaseLock()); | ||
} | ||
|
||
private async readFile(filepath: string): Promise<string | undefined> { | ||
return this.fs.promises.readFile(filepath).then( | ||
buf => buf.toString(), | ||
() => undefined | ||
); | ||
} | ||
|
||
private async writeFile(filepath: string, content: string): Promise<void> { | ||
await this.fs.promises.mkdir(path.dirname(filepath), { recursive: true }); | ||
await this.fs.promises.writeFile(filepath, content); | ||
} | ||
|
||
private async removeFile(filepath: string): Promise<void> { | ||
await this.fs.promises.unlink(filepath); | ||
} | ||
|
||
async getLock(): Promise<void> { | ||
const existing = await this.readFile(this.lockFile); | ||
if (existing) { | ||
throw new Error(`Can't acquire lock. ${this.lockFile} exists`); | ||
} | ||
|
||
await this.writeFile(this.lockFile, 'lock'); | ||
} | ||
|
||
async releaseLock(): Promise<void> { | ||
const existing = await this.readFile(this.lockFile); | ||
if (!existing) { | ||
throw new Error(`Nothing to unlock`); | ||
} | ||
|
||
await this.removeFile(this.lockFile); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
export * from './umzug'; | ||
export * from './storage'; | ||
export * from './file-locker'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { JSONStorage, FileLocker, Umzug } from '../src'; | ||
import * as path from 'path'; | ||
import { fsSyncer } from 'fs-syncer'; | ||
import * as pEvent from 'p-event'; | ||
|
||
const names = (migrations: Array<{ name: string }>) => migrations.map(m => m.name); | ||
const delay = async (ms: number) => new Promise(r => setTimeout(r, ms)); | ||
|
||
describe('locks', () => { | ||
const syncer = fsSyncer(path.join(__dirname, 'generated/lock/json'), {}); | ||
syncer.sync(); | ||
|
||
test('file lock', async () => { | ||
const umzug = new Umzug({ | ||
migrations: [1, 2].map(n => ({ | ||
name: `m${n}`, | ||
up: async () => delay(100), | ||
})), | ||
storage: new JSONStorage({ path: path.join(syncer.baseDir, 'storage.json') }), | ||
logger: undefined, | ||
}); | ||
|
||
FileLocker.attach(umzug, { path: path.join(syncer.baseDir, 'storage.json.lock') }); | ||
|
||
expect(syncer.read()).toEqual({}); | ||
|
||
const promise1 = umzug.up(); | ||
await pEvent(umzug, 'migrating'); | ||
const promise2 = umzug.up(); | ||
|
||
await expect(promise2).rejects.toThrowError(/Can't acquire lock. (.*)storage.json.lock exists/); | ||
await expect(promise1.then(names)).resolves.toEqual(['m1', 'm2']); | ||
|
||
expect(names(await umzug.executed())).toEqual(['m1', 'm2']); | ||
expect(syncer.read()).toEqual({ | ||
'storage.json': JSON.stringify(['m1', 'm2'], null, 2), | ||
}); | ||
}); | ||
}); |