better-lock 1.0.2 → 2.0.2-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +24 -0
- package/.idea/better-lock.iml +1 -7
- package/.idea/codeStyles/Project.xml +336 -5
- package/.idea/compiler.xml +7 -0
- package/.idea/inspectionProfiles/Project_Default.xml +6 -2
- package/.idea/jsLibraryMappings.xml +0 -1
- package/.idea/markdown.xml +9 -0
- package/.idea/modules.xml +1 -1
- package/.idea/prettier.xml +8 -0
- package/.prettierrc.yaml +1 -3
- package/.travis.yml +2 -2
- package/README.md +113 -73
- package/dist/better_lock.d.ts +50 -0
- package/dist/better_lock.js +317 -0
- package/dist/better_lock.js.map +1 -0
- package/dist/errors.d.ts +44 -0
- package/dist/errors.js +131 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/internals.d.ts +75 -0
- package/dist/internals.js +75 -0
- package/dist/internals.js.map +1 -0
- package/dist/options.d.ts +54 -0
- package/dist/options.js +3 -0
- package/dist/options.js.map +1 -0
- package/dist/tools.d.ts +19 -0
- package/dist/tools.js +109 -0
- package/dist/tools.js.map +1 -0
- package/dist/types.d.ts +23 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/jest.config.js +4 -0
- package/package.json +32 -9
- package/spec/better_lock.spec.ts +614 -0
- package/src/better_lock.ts +430 -0
- package/src/errors.ts +124 -0
- package/src/index.ts +54 -0
- package/src/internals.ts +118 -0
- package/src/options.ts +63 -0
- package/src/tools.ts +111 -0
- package/src/types.ts +29 -0
- package/tsconfig.json +13 -0
- package/.idea/markdown-navigator/profiles_settings.xml +0 -3
- package/.idea/markdown-navigator.xml +0 -78
- package/.idea/misc.xml +0 -6
- package/.idea/watcherTasks.xml +0 -8
- package/index.js +0 -19
- package/spec/better_lock.spec.js +0 -602
- package/src/better_lock.js +0 -405
- package/src/errors.js +0 -81
- package/src/options.js +0 -104
- package/src/tools.js +0 -125
- package/src/types.js +0 -110
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@ A (better) node.js lock library.
|
|
|
6
6
|
|
|
7
7
|
### Features
|
|
8
8
|
|
|
9
|
+
- Typescript-ready
|
|
9
10
|
- Named and keyed locks
|
|
10
11
|
- Queue and execution timeouts
|
|
11
12
|
- Queue size limit
|
|
@@ -21,6 +22,8 @@ A (better) node.js lock library.
|
|
|
21
22
|
|
|
22
23
|
```bash
|
|
23
24
|
npm install --save better-lock
|
|
25
|
+
# or
|
|
26
|
+
yarn add better-lock
|
|
24
27
|
```
|
|
25
28
|
|
|
26
29
|
### Usage examples
|
|
@@ -29,28 +32,25 @@ npm install --save better-lock
|
|
|
29
32
|
|
|
30
33
|
```javascript
|
|
31
34
|
const lock = new BetterLock();
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
.acquire(() => {
|
|
35
|
-
// Inside the lock.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return 'my result';
|
|
39
|
-
});
|
|
40
|
-
})
|
|
41
|
-
.then(
|
|
42
|
-
res => {
|
|
43
|
-
// Outside the lock. You will get whatever the promise chain has returned.
|
|
44
|
-
console.log(res); // my result
|
|
45
|
-
},
|
|
46
|
-
err => {
|
|
47
|
-
// Either your or BetterLock's error
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const res = await lock.acquire(async () => {
|
|
38
|
+
// Inside the lock. It will stay closed until the promise you return resolves or rejects.
|
|
39
|
+
await doSomeAsyncTask();
|
|
40
|
+
return 'my result';
|
|
48
41
|
});
|
|
42
|
+
|
|
43
|
+
// Outside the lock. You will get whatever the promise chain has returned.
|
|
44
|
+
console.log(res); // "my result"
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
// Either your or BetterLock's error
|
|
48
|
+
}
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
##### Advanced example
|
|
52
52
|
|
|
53
|
-
```
|
|
53
|
+
```typescript
|
|
54
54
|
const lock = new BetterLock({
|
|
55
55
|
name: 'FileLock', // To be used in error reporting and logging
|
|
56
56
|
log: winstonLogger.debug, // Give it your logger with appropeiate level
|
|
@@ -59,28 +59,30 @@ const lock = new BetterLock({
|
|
|
59
59
|
queue_size: 1, // At most one pending job
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
function processFile(filename
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
});
|
|
68
|
-
}).then(result => {
|
|
69
|
-
callback(null, {
|
|
70
|
-
status: true,
|
|
71
|
-
result,
|
|
62
|
+
async function processFile(filename) {
|
|
63
|
+
try {
|
|
64
|
+
const result = await lock.acquire(filename, async () => {
|
|
65
|
+
const appended = await appendToFile(filename);
|
|
66
|
+
return updateDb(appended);
|
|
72
67
|
});
|
|
73
|
-
|
|
68
|
+
return {
|
|
69
|
+
status: true,
|
|
70
|
+
result
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
74
|
if (err instanceof BetterLock.QueueOverflowError) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
// The job was discarded
|
|
76
|
+
return {
|
|
77
|
+
status: false
|
|
78
|
+
};
|
|
78
79
|
}
|
|
80
|
+
|
|
79
81
|
if (err instanceof BetterLock.ExecutionTimeoutError) {
|
|
80
82
|
winstonLogger.warn('Potential swallowed callback! Stack trace to the entry site:', err.stack);
|
|
81
83
|
}
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
```
|
|
@@ -129,7 +131,7 @@ lock.acquire(done => {
|
|
|
129
131
|
// Outside the lock
|
|
130
132
|
if (err) {
|
|
131
133
|
// Either your or BetterLock's error
|
|
132
|
-
console.error(err);
|
|
134
|
+
console.error(err);
|
|
133
135
|
}
|
|
134
136
|
});
|
|
135
137
|
```
|
|
@@ -139,16 +141,17 @@ You can see a bunch more usage examples in the spec file, [here](spec/better_loc
|
|
|
139
141
|
### API
|
|
140
142
|
|
|
141
143
|
- `new BetterLock(options)`
|
|
142
|
-
Create a new instance of `BetterLock`. Options should match interface
|
|
144
|
+
Create a new instance of `BetterLock`. Options should match interface `BetterLockOptions`. See below for details.
|
|
143
145
|
|
|
144
146
|
- `BetterLock.acquire([key], executor, [callback], [jobOptions])`
|
|
145
|
-
The main method you'll want to call. For each `key`, given `executor` will be called only one at a time.
|
|
147
|
+
The main method you'll want to call. For each `key`, given `executor` will be called only one at a time. If you don't provide `callback`, it will return a promise that will be resolved with whatever `executor` returns.
|
|
148
|
+
|
|
146
149
|
- `key`: Arbitrary string under which to lock. It allows you to use the same lock instance for multiple parallel concerns. Eg. this might be a database record id or filename.
|
|
147
150
|
- `executor`: Function that will be called within the lock. This function should have one of two forms.
|
|
148
|
-
1.
|
|
149
|
-
2.
|
|
151
|
+
1. _Without arguments_, in which case it should return a promise. Lock will remain locked until the promise resolves.
|
|
152
|
+
2. _With single `done`_ argument. In this case, the executor should call `done(err, res)` once it is done. Arguments passed to done will be passed to the callback of the lock.
|
|
150
153
|
- `callback`: Optional callback that will be called once executor exits. Results from executor (resolved/rejected value or arguments given to `done`) will be passed along. This can be used in addition to the returned promise.
|
|
151
|
-
- `jobOptions`: An object that should match interface
|
|
154
|
+
- `jobOptions`: An object that should match interface `BetterLockJobOptions`. A subset of main options that will serve as overrides for this particular job (for example, timeout settings).
|
|
152
155
|
|
|
153
156
|
- `BetterLock.canAcquire([key])`
|
|
154
157
|
Returns true if given key can be acquired.
|
|
@@ -159,10 +162,9 @@ You can see a bunch more usage examples in the spec file, [here](spec/better_loc
|
|
|
159
162
|
- `BetterLock.abortAll()`
|
|
160
163
|
Abort all jobs for all keys. This is suitable to be called during shutdown of your app.
|
|
161
164
|
|
|
162
|
-
|
|
163
165
|
### Options
|
|
164
166
|
|
|
165
|
-
All available options
|
|
167
|
+
All available options can be seen [here](src/options.ts).
|
|
166
168
|
|
|
167
169
|
`BetterLockOptions` are provided when you construct a lock instance. A subset of options given in `LockJobOptions` can be provided when you call `lock.acquire`, as the last argument.
|
|
168
170
|
|
|
@@ -186,58 +188,96 @@ Most commonly used options are:
|
|
|
186
188
|
- `queue_size`
|
|
187
189
|
Max queue size for waiting jobs.
|
|
188
190
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
During runtime, you can change the defaults like this:
|
|
191
|
+
Default options are a static member `DEFAULT_OPTIONS` on the `BetterLock` class. That can be seen [here](src/better_lock.ts). During runtime, you can change the defaults like this:
|
|
192
192
|
|
|
193
193
|
```javascript
|
|
194
|
-
|
|
194
|
+
import BetterLock from 'better-lock';
|
|
195
195
|
|
|
196
196
|
BetterLock.DEFAULT_OPTIONS.wait_timeout = 1000;
|
|
197
|
-
```
|
|
197
|
+
```
|
|
198
198
|
|
|
199
|
-
###
|
|
199
|
+
### When to use
|
|
200
200
|
|
|
201
|
-
|
|
201
|
+
This library is a good fit if:
|
|
202
202
|
|
|
203
|
-
|
|
203
|
+
- You need a local lock in your node.js application.
|
|
204
|
+
- You have some advanced needs, like multiple keys per lock, timeouts, aborting...
|
|
205
|
+
- You like good error messages, with full stack traces.
|
|
206
|
+
- You like good types, either through typescript or JSDoc comments
|
|
204
207
|
|
|
205
|
-
|
|
206
|
-
- Added features (execution timeout, abort jobs...)
|
|
207
|
-
- Extended stack traces (so when you get an error, you have a full stack trace of the original calling code)
|
|
208
|
-
- JSDoc comments (helps if you're using an IDE)
|
|
208
|
+
The library is not a good fit if:
|
|
209
209
|
|
|
210
|
-
|
|
210
|
+
- **You need lock reentrancy.**
|
|
211
|
+
There is no good solution for this in node.js that I know of. And this library doesn't offer any.
|
|
211
212
|
|
|
212
|
-
-
|
|
213
|
-
|
|
214
|
-
- Interface, with executor and the callback function
|
|
215
|
-
- Promises
|
|
216
|
-
- Timeout and queue limit
|
|
213
|
+
- **You need a shared lock between different nodes**
|
|
214
|
+
This library is a single process only. If you need to coordinate multiple apps or services, you need a different library.
|
|
217
215
|
|
|
218
|
-
|
|
216
|
+
### Change log
|
|
219
217
|
|
|
220
|
-
|
|
218
|
+
#### **2.0.0** (_2021/05/30_)
|
|
221
219
|
|
|
222
|
-
|
|
220
|
+
Major update. The entire library was rewritten in typescript, so you should now get typings in most editors. We also had to switch tests from mocha + chai to jest (easier ts integration).
|
|
223
221
|
|
|
224
|
-
|
|
222
|
+
The API and features have remained largely the same, just a bit of a refresh.
|
|
223
|
+
|
|
224
|
+
Non-breaking and internal changes:
|
|
225
|
+
|
|
226
|
+
- We now export all error names as a type script type. A few other types as well.
|
|
227
|
+
- Errors now have `name` parameter, which matches these names.
|
|
228
|
+
|
|
229
|
+
Breaking changes:
|
|
225
230
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
231
|
+
- You can no longer use camel case versions of external-facing objects. Eg. you can no longer pass `waitTimeout` instead of `wait_timeout`. In retrospect, this was a pretty flaky API to maintain.
|
|
232
|
+
|
|
233
|
+
- Internal `LockJob` class is no longer exported.
|
|
234
|
+
|
|
235
|
+
- Also, errors no longer expose internal `LockJob` instances (`err.job`). We now instead provide the most important fields from the job (`id` and `keys`).
|
|
236
|
+
|
|
237
|
+
- Error names have been renamed to have `BetterLock` prefix. Eg. `WaitTimeoutError` -> `BetterLockWaitTimeoutError`. This will influence `err.name` and `err.message` parameters. The idea here is, if you see `BetterLock` error in the wild, you will know what generated it.
|
|
238
|
+
|
|
239
|
+
- We have renamed `BetterLock.BetterLockError` to `BetterLock.BaseError` and `BetterLock.BetterLockInternalError` to `BetterLock.InternalError` to better match the naming scheme.
|
|
240
|
+
|
|
241
|
+
- Since Options are no longer a class but interface, we are no longer exporting them under `BetterLock.Options`. You can do `import {BetterLockOptions} from 'better-lock';` to get the typescript type.
|
|
242
|
+
|
|
243
|
+
#### **1.0.1** (_2019/01/28_)
|
|
244
|
+
|
|
245
|
+
Handle empty key list
|
|
246
|
+
|
|
247
|
+
#### **1.0.0** (_2019/01/28_)
|
|
248
|
+
|
|
249
|
+
Major version bump.
|
|
250
|
+
|
|
251
|
+
- Added multi-key locks and refactored a bunch of internals.
|
|
252
|
+
- Removed `OVERFLOW_STRATEGIES` and related options, which is mostly the reason for the major version bump. The library should otherwise work the same.
|
|
253
|
+
|
|
254
|
+
#### **0.3.1** (_2018/10/01_)
|
|
255
|
+
|
|
256
|
+
Updated CI to use the current node versions (0.8 & 0.10). Older node versions should continue to work, but are no longer tested. Also, README updates.
|
|
257
|
+
|
|
258
|
+
#### **0.3.0** (_2018/10/01_)
|
|
259
|
+
|
|
260
|
+
Can abort jobs waiting in queue.
|
|
261
|
+
|
|
262
|
+
#### **0.2.1** (_2018/09/27_)
|
|
263
|
+
|
|
264
|
+
- Better and customizable Promise detection.
|
|
265
|
+
- Restored DEFAULT_OPTIONS.
|
|
266
|
+
|
|
267
|
+
#### **0.2.0** (_2018/09/27_)
|
|
268
|
+
|
|
269
|
+
Code reformat, better pattern for loading options. No feature upgrades.
|
|
270
|
+
|
|
271
|
+
#### **0.1.1** (_2018/06/04_)
|
|
272
|
+
|
|
273
|
+
You can now use a Number as job name
|
|
235
274
|
|
|
236
275
|
### Development
|
|
237
276
|
|
|
238
277
|
Fork, then git clone. The project is already set up with a WebStorm project, if that's your cup of tee.
|
|
239
278
|
|
|
240
279
|
To run tests, with coverage:
|
|
280
|
+
|
|
241
281
|
```
|
|
242
282
|
npm run test
|
|
243
283
|
```
|
|
@@ -246,4 +286,4 @@ If you want to contribute, create a branch off master, do your work and then mak
|
|
|
246
286
|
|
|
247
287
|
## License
|
|
248
288
|
|
|
249
|
-
[MIT](./LICENSE)
|
|
289
|
+
[MIT](./LICENSE)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ILockJobOptions, IOptions } from './options';
|
|
2
|
+
import { ICallback, ICallbackExecutor, ILockKey, IPromiseExecutor } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Create a better lock object.
|
|
5
|
+
*/
|
|
6
|
+
export declare class BetterLock {
|
|
7
|
+
/**
|
|
8
|
+
* Default options to be used when creating BetterLock instances.
|
|
9
|
+
*/
|
|
10
|
+
static DEFAULT_OPTIONS: IOptions;
|
|
11
|
+
private options;
|
|
12
|
+
private log;
|
|
13
|
+
private queues;
|
|
14
|
+
constructor(options?: IOptions);
|
|
15
|
+
/**
|
|
16
|
+
* Return or create a key queue for given key
|
|
17
|
+
*/
|
|
18
|
+
private getQueue;
|
|
19
|
+
/**
|
|
20
|
+
* Returns true if the caller can *immediately* acquire the lock. There is nothing holding the key and nothing in the queue.
|
|
21
|
+
*/
|
|
22
|
+
canAcquire(key?: string): boolean;
|
|
23
|
+
private normalizeAndValidateKey;
|
|
24
|
+
acquire<TResult>(executor: IPromiseExecutor<TResult>, jobOptions?: ILockJobOptions): Promise<TResult>;
|
|
25
|
+
acquire<TResult>(executor: ICallbackExecutor<TResult>, jobOptions?: ILockJobOptions): Promise<TResult>;
|
|
26
|
+
acquire<TResult>(executor: ICallbackExecutor<TResult>, callback: ICallback<TResult>, jobOptions?: ILockJobOptions): void;
|
|
27
|
+
acquire<TResult>(key: ILockKey | Array<ILockKey>, executor: IPromiseExecutor<TResult>, jobOptions?: ILockJobOptions): Promise<TResult>;
|
|
28
|
+
acquire<TResult>(key: ILockKey | Array<ILockKey>, executor: ICallbackExecutor<TResult>, jobOptions?: ILockJobOptions): Promise<TResult>;
|
|
29
|
+
acquire<TResult>(key: ILockKey | Array<ILockKey>, executor: ICallbackExecutor<TResult>, callback: ICallback<TResult>, jobOptions?: ILockJobOptions): void;
|
|
30
|
+
/**
|
|
31
|
+
* Update state of given list of queues.
|
|
32
|
+
*/
|
|
33
|
+
private update;
|
|
34
|
+
private executeJob;
|
|
35
|
+
private onWaitTimeout;
|
|
36
|
+
private onExecutionTimeout;
|
|
37
|
+
private endJob;
|
|
38
|
+
/**
|
|
39
|
+
* Abort all jobs for a given key (or from the default job queue, if no key is given).
|
|
40
|
+
* Job executors will not be called. Callbacks will be called with JobAbortedError.
|
|
41
|
+
* Currently executing job will not be interrupted.
|
|
42
|
+
* @param [key]
|
|
43
|
+
*/
|
|
44
|
+
abort(key?: ILockKey): void;
|
|
45
|
+
/**
|
|
46
|
+
* Abort all pending jobs from all queues.
|
|
47
|
+
* Currently executing jobs will not be interrupted.
|
|
48
|
+
*/
|
|
49
|
+
abortAll(): void;
|
|
50
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BetterLock = void 0;
|
|
4
|
+
var errors_1 = require("./errors");
|
|
5
|
+
var internals_1 = require("./internals");
|
|
6
|
+
var tools = require("./tools");
|
|
7
|
+
/**
|
|
8
|
+
* Create a better lock object.
|
|
9
|
+
*/
|
|
10
|
+
var BetterLock = /** @class */ (function () {
|
|
11
|
+
function BetterLock(options) {
|
|
12
|
+
this.options = tools.assign({}, [BetterLock.DEFAULT_OPTIONS, options]);
|
|
13
|
+
this.log = tools.makeLog(this.options.name, this.options.log);
|
|
14
|
+
this.queues = {};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Return or create a key queue for given key
|
|
18
|
+
*/
|
|
19
|
+
BetterLock.prototype.getQueue = function (key) {
|
|
20
|
+
if (key === undefined || key === null) {
|
|
21
|
+
key = internals_1.KeyQueue.DEFAULT_QUEUE_KEY;
|
|
22
|
+
}
|
|
23
|
+
var queue = this.queues[key];
|
|
24
|
+
if (!queue) {
|
|
25
|
+
queue = this.queues[key] = new internals_1.KeyQueue(String(key));
|
|
26
|
+
}
|
|
27
|
+
return queue;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Returns true if the caller can *immediately* acquire the lock. There is nothing holding the key and nothing in the queue.
|
|
31
|
+
*/
|
|
32
|
+
BetterLock.prototype.canAcquire = function (key) {
|
|
33
|
+
var queue = this.getQueue(key);
|
|
34
|
+
return !queue.active && !queue.jobs.length;
|
|
35
|
+
};
|
|
36
|
+
BetterLock.prototype.normalizeAndValidateKey = function (key) {
|
|
37
|
+
if (key === undefined) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (key === null) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
if (tools.isString(key)) {
|
|
44
|
+
return key;
|
|
45
|
+
}
|
|
46
|
+
if (tools.isNumber(key)) {
|
|
47
|
+
return String(key);
|
|
48
|
+
}
|
|
49
|
+
// Invalid key value
|
|
50
|
+
throw new errors_1.BetterLockInvalidArgumentError(this.options.name, 'key', 'an instance of ILockKey: string, number, undefined or null', key);
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Acquire the lock for given key or list of keys. If waiting on a list, keys will be acquired in order,
|
|
54
|
+
* as they become free. Any deadlock prevention must be handled by the caller.
|
|
55
|
+
*
|
|
56
|
+
* Once the lock is acquired, we will call the executor function, with a "done" method.
|
|
57
|
+
* You must call "done" to release the lock. Alternatively, return a Promise; lock will be released
|
|
58
|
+
* once promise resolves or rejects.
|
|
59
|
+
*
|
|
60
|
+
* Return values from your executor will be passed to callback (or the resulting promise).
|
|
61
|
+
*
|
|
62
|
+
* @param key Named key or array of keys for this particular call. Calls with different keys will be run in parallel. Not required.
|
|
63
|
+
* @param executor Function that will run inside the lock. Required.
|
|
64
|
+
* @param callback Function to be called after the executor finishes or if we never enter the lock (timeout, queue depletion). Leave out to use promises.
|
|
65
|
+
* @param jobOptions Options to be applied on this job only
|
|
66
|
+
*/
|
|
67
|
+
BetterLock.prototype.acquire = function (key, executor, callback, jobOptions) {
|
|
68
|
+
var _this = this;
|
|
69
|
+
if (tools.isFunction(key) || tools.isObject(key)) {
|
|
70
|
+
// Presume we weren't given a key (first form)
|
|
71
|
+
jobOptions = callback;
|
|
72
|
+
callback = executor;
|
|
73
|
+
executor = key;
|
|
74
|
+
key = null;
|
|
75
|
+
}
|
|
76
|
+
// Repackage "key" into an array of keys
|
|
77
|
+
var keys = [];
|
|
78
|
+
if (Array.isArray(key)) {
|
|
79
|
+
var arrayOfKeys = key;
|
|
80
|
+
// Make sure we don't add duplicates
|
|
81
|
+
var seenKeys = new Set();
|
|
82
|
+
for (var i = 0; i < arrayOfKeys.length; i++) {
|
|
83
|
+
var normalizedKey = this.normalizeAndValidateKey(arrayOfKeys[i]);
|
|
84
|
+
if (!seenKeys.has(normalizedKey)) {
|
|
85
|
+
keys.push(normalizedKey);
|
|
86
|
+
seenKeys.add(normalizedKey);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
keys.push(this.normalizeAndValidateKey(key));
|
|
92
|
+
}
|
|
93
|
+
// Create callback wrapper for promise interface
|
|
94
|
+
if (!tools.isFunction(callback)) {
|
|
95
|
+
if (jobOptions === undefined) {
|
|
96
|
+
jobOptions = callback;
|
|
97
|
+
}
|
|
98
|
+
callback = tools.callbackWithPromise();
|
|
99
|
+
}
|
|
100
|
+
// Validate other options
|
|
101
|
+
if (!tools.isFunction(executor)) {
|
|
102
|
+
throw new errors_1.BetterLockInvalidArgumentError(this.options.name, 'executor', 'a function', executor);
|
|
103
|
+
}
|
|
104
|
+
if (!tools.isFunction(callback)) {
|
|
105
|
+
throw new errors_1.BetterLockInvalidArgumentError(this.options.name, 'callback', 'a function', callback);
|
|
106
|
+
}
|
|
107
|
+
// Prepare job options
|
|
108
|
+
var effectiveJobOptions = jobOptions
|
|
109
|
+
? tools.assign({}, [this.options, jobOptions])
|
|
110
|
+
: // No need to create new object since these will be read-only and are subset of global options
|
|
111
|
+
this.options;
|
|
112
|
+
var job = new internals_1.LockJob(keys, executor, callback, effectiveJobOptions);
|
|
113
|
+
// Set incoming stack
|
|
114
|
+
if (effectiveJobOptions.extend_stack_traces) {
|
|
115
|
+
var tempErr = new Error();
|
|
116
|
+
Error.captureStackTrace(tempErr, this.acquire);
|
|
117
|
+
job.incoming_stack = tempErr.stack;
|
|
118
|
+
}
|
|
119
|
+
// Add job to its key queues
|
|
120
|
+
var queuesToUpdate = [];
|
|
121
|
+
for (var i = 0; i < keys.length; i++) {
|
|
122
|
+
var queue = this.getQueue(keys[i]);
|
|
123
|
+
queue.jobs.push(job);
|
|
124
|
+
queuesToUpdate.push(queue);
|
|
125
|
+
}
|
|
126
|
+
// Start wait timer, if enabled
|
|
127
|
+
if (tools.isNumber(job.options.wait_timeout)) {
|
|
128
|
+
job.wait_timeout_id = setTimeout(function () { return _this.onWaitTimeout(job); }, job.options.wait_timeout);
|
|
129
|
+
// NOTE: timeout of 0 will NOT trigger error if lock can be acquired immediately.
|
|
130
|
+
}
|
|
131
|
+
this.log("Enqueued " + job);
|
|
132
|
+
// Perform update of queues this job has modified. We don't do this in next tick or something,
|
|
133
|
+
// because we want canAcquire to immediately return false
|
|
134
|
+
if (queuesToUpdate.length) {
|
|
135
|
+
this.update(queuesToUpdate);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// Special case. We are given empty list of keys. We will immediately execute jobs like this.
|
|
139
|
+
this.executeJob(job);
|
|
140
|
+
}
|
|
141
|
+
return callback.promise;
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Update state of given list of queues.
|
|
145
|
+
*/
|
|
146
|
+
BetterLock.prototype.update = function (queues) {
|
|
147
|
+
for (var i = 0; i < queues.length; i++) {
|
|
148
|
+
var queue = queues[i];
|
|
149
|
+
// If key is available, have next job grab it
|
|
150
|
+
if (!queue.active && queue.jobs.length) {
|
|
151
|
+
var job = queue.jobs.splice(0, 1)[0];
|
|
152
|
+
queue.active = job;
|
|
153
|
+
job.waiting_count--;
|
|
154
|
+
if (job.waiting_count <= 0) {
|
|
155
|
+
// This job can be executed now.
|
|
156
|
+
this.executeJob(job);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Handle wait queue overflow
|
|
160
|
+
if (tools.isNumber(this.options.queue_size) && this.options.queue_size < queue.jobs.length) {
|
|
161
|
+
// Overflow. Reject the most recent job
|
|
162
|
+
var mostRecentJob = queue.jobs[queue.jobs.length - 1];
|
|
163
|
+
this.log(queue.toString() + " has overflown, so most recent job (" + mostRecentJob + ") was kicked out");
|
|
164
|
+
this.endJob(mostRecentJob, [
|
|
165
|
+
new errors_1.BetterLockQueueOverflowError(this.options.name, queue.key, queue.jobs.length, mostRecentJob),
|
|
166
|
+
]);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
BetterLock.prototype.executeJob = function (job) {
|
|
171
|
+
var _this = this;
|
|
172
|
+
this.log("Executing " + job);
|
|
173
|
+
// Clear wait timeout
|
|
174
|
+
clearTimeout(job.wait_timeout_id);
|
|
175
|
+
job.wait_timeout_id = null;
|
|
176
|
+
// We want to do the rest of this in a separate context, because we don't want user executor code
|
|
177
|
+
// to ever interfere with the code calling acquire().
|
|
178
|
+
setImmediate(function () {
|
|
179
|
+
// Make sure job hasn't been ended in the meantime
|
|
180
|
+
if (job.executed_at || job.ended_at) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// Validate this job is holding all queues it needs
|
|
184
|
+
for (var i = 0; i < job.keys.length; i++) {
|
|
185
|
+
var queue = _this.getQueue(job.keys[i]);
|
|
186
|
+
if (queue.active !== job) {
|
|
187
|
+
// This should never happen
|
|
188
|
+
return _this.endJob(job, [
|
|
189
|
+
new errors_1.BetterLockInternalError(_this.options.name, "Corrupted wait queue state for " + queue.key),
|
|
190
|
+
]);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Mark job as having started executing
|
|
194
|
+
job.executed_at = new Date();
|
|
195
|
+
if (tools.isNumber(job.options.execution_timeout)) {
|
|
196
|
+
job.execution_timeout_id = setTimeout(function () { return _this.onExecutionTimeout(job); }, job.options.execution_timeout);
|
|
197
|
+
}
|
|
198
|
+
var lockDone = function () {
|
|
199
|
+
var args = [];
|
|
200
|
+
for (var _i = 0; _i < arguments.length; _i++) {
|
|
201
|
+
args[_i] = arguments[_i];
|
|
202
|
+
}
|
|
203
|
+
_this.log("Done called for " + job);
|
|
204
|
+
_this.endJob(job, args);
|
|
205
|
+
};
|
|
206
|
+
if (job.executor.length > 0) {
|
|
207
|
+
// Callback interface
|
|
208
|
+
job.executor(lockDone);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
// Promise interface
|
|
212
|
+
var executorResult;
|
|
213
|
+
try {
|
|
214
|
+
executorResult = job.executor();
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
// Error thrown directly from executor
|
|
218
|
+
lockDone(err);
|
|
219
|
+
}
|
|
220
|
+
// Promise sniffing
|
|
221
|
+
if (_this.options.promise_tester(executorResult)) {
|
|
222
|
+
executorResult.then(function (res) { return lockDone(null, res); }, function (err) { return lockDone(err); });
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
// "promise" is just some random value. We don't have to wait
|
|
226
|
+
lockDone(null, executorResult);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
};
|
|
230
|
+
BetterLock.prototype.onWaitTimeout = function (job) {
|
|
231
|
+
this.log(job + " has timed out after waiting in queue for " + (new Date().valueOf() - job.enqueued_at.valueOf()) + "ms");
|
|
232
|
+
this.endJob(job, [new errors_1.BetterLockWaitTimeoutError(this.options.name, job)]);
|
|
233
|
+
};
|
|
234
|
+
BetterLock.prototype.onExecutionTimeout = function (job) {
|
|
235
|
+
this.log(job + " has timed out after executing for " + (new Date().valueOf() - job.executed_at.valueOf()) + "ms");
|
|
236
|
+
this.endJob(job, [new errors_1.BetterLockExecutionTimeoutError(this.options.name, job)]);
|
|
237
|
+
};
|
|
238
|
+
BetterLock.prototype.endJob = function (job, callbackArgs) {
|
|
239
|
+
var _this = this;
|
|
240
|
+
if (job.ended_at) {
|
|
241
|
+
this.log("WARNING: " + job + " is trying to end, but it has already ended at " + job.ended_at.toISOString() + ". Called with arguments: " + callbackArgs);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
clearTimeout(job.wait_timeout_id);
|
|
245
|
+
job.wait_timeout_id = null;
|
|
246
|
+
clearTimeout(job.execution_timeout_id);
|
|
247
|
+
job.execution_timeout_id = null;
|
|
248
|
+
job.ended_at = new Date();
|
|
249
|
+
var queuesToUpdate = [];
|
|
250
|
+
for (var i = 0; i < job.keys.length; i++) {
|
|
251
|
+
var queue = this.getQueue(job.keys[i]);
|
|
252
|
+
queuesToUpdate.push(queue);
|
|
253
|
+
if (queue.active === job) {
|
|
254
|
+
// This job was holding on the queue's key
|
|
255
|
+
queue.active = null;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
// This job was still in wait list for this queue
|
|
259
|
+
var index = queue.jobs.indexOf(job);
|
|
260
|
+
if (index >= 0) {
|
|
261
|
+
// Remove from queue
|
|
262
|
+
queue.jobs.splice(index, 1);
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
// Something is wrong. Where did this job come from?
|
|
266
|
+
this.log("WARNING: " + job + " is ending, but it is not found anywhere in the queue for " + queue.key);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// Call the callback
|
|
271
|
+
try {
|
|
272
|
+
job.callback.apply(null, callbackArgs);
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
// Whatever happens, schedule a queue update
|
|
276
|
+
setImmediate(function () { return _this.update(queuesToUpdate); });
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
/**
|
|
280
|
+
* Abort all jobs for a given key (or from the default job queue, if no key is given).
|
|
281
|
+
* Job executors will not be called. Callbacks will be called with JobAbortedError.
|
|
282
|
+
* Currently executing job will not be interrupted.
|
|
283
|
+
* @param [key]
|
|
284
|
+
*/
|
|
285
|
+
BetterLock.prototype.abort = function (key) {
|
|
286
|
+
var _this = this;
|
|
287
|
+
var queue = this.getQueue(key);
|
|
288
|
+
queue.jobs.slice().forEach(function (job) {
|
|
289
|
+
_this.endJob(job, [new errors_1.BetterLockJobAbortedError(_this.options.name, job)]);
|
|
290
|
+
});
|
|
291
|
+
if (queue.active && !queue.active.executed_at) {
|
|
292
|
+
// If we have a job holding this queue that hasn't been executed yet, abort it as well
|
|
293
|
+
this.endJob(queue.active, [new errors_1.BetterLockJobAbortedError(this.options.name, queue.active)]);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
/**
|
|
297
|
+
* Abort all pending jobs from all queues.
|
|
298
|
+
* Currently executing jobs will not be interrupted.
|
|
299
|
+
*/
|
|
300
|
+
BetterLock.prototype.abortAll = function () {
|
|
301
|
+
this.abort(null);
|
|
302
|
+
for (var key in this.queues) {
|
|
303
|
+
// noinspection JSUnfilteredForInLoop
|
|
304
|
+
this.abort(key);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
/**
|
|
308
|
+
* Default options to be used when creating BetterLock instances.
|
|
309
|
+
*/
|
|
310
|
+
BetterLock.DEFAULT_OPTIONS = {
|
|
311
|
+
extend_stack_traces: true,
|
|
312
|
+
promise_tester: function (p) { return p && typeof p.then === 'function'; },
|
|
313
|
+
};
|
|
314
|
+
return BetterLock;
|
|
315
|
+
}());
|
|
316
|
+
exports.BetterLock = BetterLock;
|
|
317
|
+
//# sourceMappingURL=better_lock.js.map
|