better-lock 3.3.0 → 4.0.0
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/README.md +57 -10
- package/dist/async_context.d.ts +27 -0
- package/dist/async_context.js +58 -0
- package/dist/async_context.js.map +1 -0
- package/dist/better_lock.d.ts +21 -1
- package/dist/better_lock.js +157 -36
- package/dist/better_lock.js.map +1 -1
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +19 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/internals.d.ts +10 -0
- package/dist/internals.js +9 -0
- package/dist/internals.js.map +1 -1
- package/dist/options.d.ts +21 -0
- package/package.json +1 -1
- package/spec/async_context.spec.ts +379 -0
- package/src/async_context.ts +86 -0
- package/src/better_lock.ts +174 -42
- package/src/errors.ts +28 -0
- package/src/index.ts +5 -1
- package/src/internals.ts +12 -0
- package/src/options.ts +24 -0
package/dist/errors.js
CHANGED
|
@@ -15,7 +15,7 @@ var __extends = (this && this.__extends) || (function () {
|
|
|
15
15
|
};
|
|
16
16
|
})();
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.isAcquisitionFailureError = exports.BetterLockJobAbortedError = exports.BetterLockQueueOverflowError = exports.BetterLockExecutionTimeoutError = exports.BetterLockWaitTimeoutError = exports.BetterLockInvalidArgumentError = exports.BetterLockInternalError = exports.BetterLockError = void 0;
|
|
18
|
+
exports.isAcquisitionFailureError = exports.BetterLockReentrancyError = exports.BetterLockJobAbortedError = exports.BetterLockQueueOverflowError = exports.BetterLockExecutionTimeoutError = exports.BetterLockWaitTimeoutError = exports.BetterLockInvalidArgumentError = exports.BetterLockInternalError = exports.BetterLockError = void 0;
|
|
19
19
|
var internals_1 = require("./internals");
|
|
20
20
|
var BetterLockError = /** @class */ (function (_super) {
|
|
21
21
|
__extends(BetterLockError, _super);
|
|
@@ -127,6 +127,24 @@ var BetterLockJobAbortedError = /** @class */ (function (_super) {
|
|
|
127
127
|
return BetterLockJobAbortedError;
|
|
128
128
|
}(BetterLockError));
|
|
129
129
|
exports.BetterLockJobAbortedError = BetterLockJobAbortedError;
|
|
130
|
+
var BetterLockReentrancyError = /** @class */ (function (_super) {
|
|
131
|
+
__extends(BetterLockReentrancyError, _super);
|
|
132
|
+
function BetterLockReentrancyError(lockName, keys, heldKeys) {
|
|
133
|
+
var _this = this;
|
|
134
|
+
var describe = function (list) {
|
|
135
|
+
return list.map(function (key) { return (key === null ? '<default>' : "\"".concat(key, "\"")); }).join(', ');
|
|
136
|
+
};
|
|
137
|
+
var message = heldKeys.length < keys.length
|
|
138
|
+
? "Caller holds only some of the requested keys (".concat(describe(heldKeys), " of ").concat(describe(keys), "). Waiting for the rest while holding these could deadlock")
|
|
139
|
+
: "Reentrant acquire of ".concat(describe(keys), " is not allowed (reentrancy: \"error\")");
|
|
140
|
+
_this = _super.call(this, lockName, message) || this;
|
|
141
|
+
_this.keys = keys;
|
|
142
|
+
_this.held_keys = heldKeys;
|
|
143
|
+
return _this;
|
|
144
|
+
}
|
|
145
|
+
return BetterLockReentrancyError;
|
|
146
|
+
}(BetterLockError));
|
|
147
|
+
exports.BetterLockReentrancyError = BetterLockReentrancyError;
|
|
130
148
|
function isAcquisitionFailureError(err) {
|
|
131
149
|
return ((err && err instanceof BetterLockQueueOverflowError) ||
|
|
132
150
|
err instanceof BetterLockWaitTimeoutError ||
|
package/dist/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { BetterLock as BetterLockUnadorned } from './better_lock';
|
|
2
|
-
import { BetterLockError, BetterLockExecutionTimeoutError, BetterLockInternalError, BetterLockInvalidArgumentError, BetterLockJobAbortedError, BetterLockQueueOverflowError, BetterLockWaitTimeoutError } from './errors';
|
|
3
|
-
import { ILockJobOptions as BetterLockJobOptions, IOptions as BetterLockOptions, IQueueEjectionStrategy as BetterLockQueueEjectionStrategy } from './options';
|
|
2
|
+
import { BetterLockError, BetterLockExecutionTimeoutError, BetterLockInternalError, BetterLockInvalidArgumentError, BetterLockJobAbortedError, BetterLockQueueOverflowError, BetterLockReentrancyError, BetterLockWaitTimeoutError } from './errors';
|
|
3
|
+
import { ILockJobOptions as BetterLockJobOptions, IOptions as BetterLockOptions, IQueueEjectionStrategy as BetterLockQueueEjectionStrategy, IReentrancyMode as BetterLockReentrancyMode } from './options';
|
|
4
4
|
import { IErrorName as BetterLockErrorName, IExecutor as BetterLockExecutor, ILockKey as BetterLockKey } from './types';
|
|
5
|
-
export { BetterLockOptions, BetterLockQueueEjectionStrategy, BetterLockJobOptions, BetterLockExecutor, BetterLockKey, BetterLockErrorName, BetterLockError, BetterLockExecutionTimeoutError, BetterLockInternalError, BetterLockInvalidArgumentError, BetterLockJobAbortedError, BetterLockQueueOverflowError, BetterLockWaitTimeoutError, };
|
|
5
|
+
export { BetterLockOptions, BetterLockQueueEjectionStrategy, BetterLockReentrancyMode, BetterLockJobOptions, BetterLockExecutor, BetterLockKey, BetterLockErrorName, BetterLockError, BetterLockExecutionTimeoutError, BetterLockInternalError, BetterLockInvalidArgumentError, BetterLockJobAbortedError, BetterLockQueueOverflowError, BetterLockReentrancyError, BetterLockWaitTimeoutError, };
|
|
6
6
|
export declare const BetterLock: typeof BetterLockUnadorned & {
|
|
7
7
|
BetterLock: typeof BetterLockUnadorned;
|
|
8
8
|
Error: typeof BetterLockError;
|
|
@@ -12,5 +12,6 @@ export declare const BetterLock: typeof BetterLockUnadorned & {
|
|
|
12
12
|
ExecutionTimeoutError: typeof BetterLockExecutionTimeoutError;
|
|
13
13
|
QueueOverflowError: typeof BetterLockQueueOverflowError;
|
|
14
14
|
JobAbortedError: typeof BetterLockJobAbortedError;
|
|
15
|
+
ReentrancyError: typeof BetterLockReentrancyError;
|
|
15
16
|
};
|
|
16
17
|
export default BetterLock;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.BetterLock = exports.BetterLockWaitTimeoutError = exports.BetterLockQueueOverflowError = exports.BetterLockJobAbortedError = exports.BetterLockInvalidArgumentError = exports.BetterLockInternalError = exports.BetterLockExecutionTimeoutError = exports.BetterLockError = void 0;
|
|
3
|
+
exports.BetterLock = exports.BetterLockWaitTimeoutError = exports.BetterLockReentrancyError = exports.BetterLockQueueOverflowError = exports.BetterLockJobAbortedError = exports.BetterLockInvalidArgumentError = exports.BetterLockInternalError = exports.BetterLockExecutionTimeoutError = exports.BetterLockError = void 0;
|
|
4
4
|
var better_lock_1 = require("./better_lock");
|
|
5
5
|
var errors_1 = require("./errors");
|
|
6
6
|
Object.defineProperty(exports, "BetterLockError", { enumerable: true, get: function () { return errors_1.BetterLockError; } });
|
|
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "BetterLockInternalError", { enumerable: true, ge
|
|
|
9
9
|
Object.defineProperty(exports, "BetterLockInvalidArgumentError", { enumerable: true, get: function () { return errors_1.BetterLockInvalidArgumentError; } });
|
|
10
10
|
Object.defineProperty(exports, "BetterLockJobAbortedError", { enumerable: true, get: function () { return errors_1.BetterLockJobAbortedError; } });
|
|
11
11
|
Object.defineProperty(exports, "BetterLockQueueOverflowError", { enumerable: true, get: function () { return errors_1.BetterLockQueueOverflowError; } });
|
|
12
|
+
Object.defineProperty(exports, "BetterLockReentrancyError", { enumerable: true, get: function () { return errors_1.BetterLockReentrancyError; } });
|
|
12
13
|
Object.defineProperty(exports, "BetterLockWaitTimeoutError", { enumerable: true, get: function () { return errors_1.BetterLockWaitTimeoutError; } });
|
|
13
14
|
function attachExports(target, exports) {
|
|
14
15
|
Object.assign(target, exports);
|
|
@@ -23,6 +24,7 @@ exports.BetterLock = attachExports(better_lock_1.BetterLock, {
|
|
|
23
24
|
ExecutionTimeoutError: errors_1.BetterLockExecutionTimeoutError,
|
|
24
25
|
QueueOverflowError: errors_1.BetterLockQueueOverflowError,
|
|
25
26
|
JobAbortedError: errors_1.BetterLockJobAbortedError,
|
|
27
|
+
ReentrancyError: errors_1.BetterLockReentrancyError,
|
|
26
28
|
});
|
|
27
29
|
exports.default = exports.BetterLock;
|
|
28
30
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,6CAAkE;AAClE,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,6CAAkE;AAClE,mCASkB;AAiBhB,gGAzBA,wBAAe,OAyBA;AACf,gHAzBA,wCAA+B,OAyBA;AAC/B,wGAzBA,gCAAuB,OAyBA;AACvB,+GAzBA,uCAA8B,OAyBA;AAC9B,0GAzBA,kCAAyB,OAyBA;AACzB,6GAzBA,qCAA4B,OAyBA;AAC5B,0GAzBA,kCAAyB,OAyBA;AACzB,2GAzBA,mCAA0B,OAyBA;AAG5B,SAAS,aAAa,CACpB,MAAe,EACf,OAAiB;IAEjB,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,OAAO,MAAa,CAAC;AACvB,CAAC;AAEY,QAAA,UAAU,GAAG,aAAa,CAAC,wBAAmB,EAAE;IAC3D,UAAU,EAAE,wBAAmB;IAE/B,KAAK,EAAE,wBAAe;IACtB,aAAa,EAAE,gCAAuB;IACtC,oBAAoB,EAAE,uCAA8B;IACpD,gBAAgB,EAAE,mCAA0B;IAC5C,qBAAqB,EAAE,wCAA+B;IACtD,kBAAkB,EAAE,qCAA4B;IAChD,eAAe,EAAE,kCAAyB;IAC1C,eAAe,EAAE,kCAAyB;CAC3C,CAAC,CAAC;AAEH,kBAAe,kBAAU,CAAC"}
|
package/dist/internals.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { IAsyncResource } from './async_context';
|
|
1
2
|
import { ILockJobOptions } from './options';
|
|
2
3
|
import { ICallback, IExecutor } from './types';
|
|
3
4
|
/**
|
|
@@ -52,6 +53,15 @@ export declare class LockJob<TResult> {
|
|
|
52
53
|
* Saved incoming stack, for the purpose of extending stack traces
|
|
53
54
|
*/
|
|
54
55
|
incoming_stack: string;
|
|
56
|
+
/**
|
|
57
|
+
* Async context of the acquire() call. Executor and callback run in it. Undefined if the runtime doesn't support it.
|
|
58
|
+
*/
|
|
59
|
+
async_context: IAsyncResource | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* Set for reentrant jobs: the executing jobs (from the caller's async context) that hold this job's keys.
|
|
62
|
+
* A reentrant job doesn't enter key queues. It runs while these jobs hold the keys.
|
|
63
|
+
*/
|
|
64
|
+
reentrant_holders: LockJob<any>[] | null;
|
|
55
65
|
constructor(keys: Array<string | null>, executor: IExecutor<TResult>, callback: ICallback<TResult>, options: ILockJobOptions);
|
|
56
66
|
toString(): string;
|
|
57
67
|
}
|
package/dist/internals.js
CHANGED
|
@@ -32,6 +32,15 @@ var LockJob = /** @class */ (function () {
|
|
|
32
32
|
* Saved incoming stack, for the purpose of extending stack traces
|
|
33
33
|
*/
|
|
34
34
|
this.incoming_stack = null;
|
|
35
|
+
/**
|
|
36
|
+
* Async context of the acquire() call. Executor and callback run in it. Undefined if the runtime doesn't support it.
|
|
37
|
+
*/
|
|
38
|
+
this.async_context = undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Set for reentrant jobs: the executing jobs (from the caller's async context) that hold this job's keys.
|
|
41
|
+
* A reentrant job doesn't enter key queues. It runs while these jobs hold the keys.
|
|
42
|
+
*/
|
|
43
|
+
this.reentrant_holders = null;
|
|
35
44
|
LockJob._lastId++;
|
|
36
45
|
this.id = LockJob._lastId;
|
|
37
46
|
this.keys = keys;
|
package/dist/internals.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"internals.js","sourceRoot":"","sources":["../src/internals.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"internals.js","sourceRoot":"","sources":["../src/internals.ts"],"names":[],"mappings":";;;AAIA;;GAEG;AACH;IAyEE,iBACE,IAA0B,EAC1B,QAA4B,EAC5B,QAA4B,EAC5B,OAAwB;QA1E1B,kDAAkD;QAC3C,OAAE,GAAG,SAAS,CAAC;QAuBtB;;WAEG;QACI,gBAAW,GAAS,IAAI,IAAI,EAAE,CAAC;QAEtC;;WAEG;QACI,gBAAW,GAAS,IAAI,CAAC;QAEhC;;WAEG;QACI,aAAQ,GAAS,IAAI,CAAC;QAO7B;;WAEG;QACI,oBAAe,GAAG,IAAI,CAAC;QAE9B;;WAEG;QACI,yBAAoB,GAAG,IAAI,CAAC;QAEnC;;WAEG;QACI,mBAAc,GAAW,IAAI,CAAC;QAErC;;WAEG;QACI,kBAAa,GAA+B,SAAS,CAAC;QAE7D;;;WAGG;QACI,sBAAiB,GAA0B,IAAI,CAAC;QAQrD,OAAO,CAAC,OAAO,EAAE,CAAC;QAClB,IAAI,CAAC,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;QAC1B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,MAAM,CAAC;QACjC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED,0BAAQ,GAAR;QACE,OAAO,eAAQ,IAAI,CAAC,EAAE,eAAK,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAG,CAAC;IACrD,CAAC;IAzFM,eAAO,GAAG,CAAC,CAAC;IA0FrB,cAAC;CAAA,AA3FD,IA2FC;AA3FY,0BAAO;AA6FpB,wHAAwH;AAExH;;GAEG;AACH;IAiBE,kBAAY,GAAY;QAXxB;;;WAGG;QACI,SAAI,GAAmB,EAAE,CAAC;QAEjC;;WAEG;QACI,WAAM,GAAiB,IAAI,CAAC;QAGjC,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;IACjB,CAAC;IAED,2BAAQ,GAAR;QACE,OAAO,IAAI,CAAC,GAAG,KAAK,QAAQ,CAAC,iBAAiB,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,mBAAY,IAAI,CAAC,GAAG,MAAG,CAAC;IACxF,CAAC;IAtBD,yDAAyD;IACzC,0BAAiB,GAAG,yBAAyB,CAAC;IAsBhE,eAAC;CAAA,AAxBD,IAwBC;AAxBY,4BAAQ"}
|
package/dist/options.d.ts
CHANGED
|
@@ -21,8 +21,17 @@ export interface ILockJobOptions {
|
|
|
21
21
|
* Overrides BetterLockOptions value.
|
|
22
22
|
*/
|
|
23
23
|
lock_condition?: (key: ILockKey | Array<ILockKey>) => boolean;
|
|
24
|
+
/**
|
|
25
|
+
* What to do when acquire is called from code that already holds all the requested keys. Overrides BetterLockOptions value.
|
|
26
|
+
*/
|
|
27
|
+
reentrancy?: IReentrancyMode;
|
|
24
28
|
}
|
|
25
29
|
export declare type IQueueEjectionStrategy = 'oldest' | 'newest';
|
|
30
|
+
/**
|
|
31
|
+
* - 'allow': run the executor without waiting for the keys the caller already holds.
|
|
32
|
+
* - 'error': fail with BetterLockReentrancyError.
|
|
33
|
+
*/
|
|
34
|
+
export declare type IReentrancyMode = 'allow' | 'error';
|
|
26
35
|
/**
|
|
27
36
|
* Options that apply on BetterLock instance
|
|
28
37
|
*/
|
|
@@ -49,6 +58,18 @@ export interface IOptions extends ILockJobOptions {
|
|
|
49
58
|
* This can be used to more conveniently opt out of locking, without duplicating or extracting the executor.
|
|
50
59
|
*/
|
|
51
60
|
lock_condition?: (key: ILockKey | Array<ILockKey>) => boolean;
|
|
61
|
+
/**
|
|
62
|
+
* What to do when acquire is called from code that already holds all the requested keys (inside the executor, or
|
|
63
|
+
* in async work the executor started and that runs before it finishes). 'allow' runs the executor without
|
|
64
|
+
* waiting, 'error' fails with BetterLockReentrancyError. Defaults to 'allow'.
|
|
65
|
+
*
|
|
66
|
+
* Holding only some of the requested keys always fails with BetterLockReentrancyError, because waiting for the rest
|
|
67
|
+
* while holding these could deadlock.
|
|
68
|
+
*
|
|
69
|
+
* Requires Node.js AsyncLocalStorage, looked up through process.getBuiltinModule (Node.js 20.16+ or 22.3+).
|
|
70
|
+
* In other runtimes, this option has no effect and nested acquire calls wait in the queue like any other.
|
|
71
|
+
*/
|
|
72
|
+
reentrancy?: IReentrancyMode;
|
|
52
73
|
/**
|
|
53
74
|
* Lock name. This will be written in all logs and error messages, to help you distinguish between different locks
|
|
54
75
|
*/
|
package/package.json
CHANGED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
+
|
|
3
|
+
import BetterLock from '../src/index';
|
|
4
|
+
|
|
5
|
+
describe('BetterLock async context', () => {
|
|
6
|
+
const store = new AsyncLocalStorage<string>();
|
|
7
|
+
|
|
8
|
+
it('runs a contended executor in the context of its own acquire call', async () => {
|
|
9
|
+
const lock = new BetterLock();
|
|
10
|
+
const seen: Record<string, string> = {};
|
|
11
|
+
|
|
12
|
+
await Promise.all([
|
|
13
|
+
store.run('first', () =>
|
|
14
|
+
lock.acquire(async () => {
|
|
15
|
+
await waitPromise(20);
|
|
16
|
+
seen.first = store.getStore();
|
|
17
|
+
})
|
|
18
|
+
),
|
|
19
|
+
store.run('second', () =>
|
|
20
|
+
lock.acquire(async () => {
|
|
21
|
+
seen.second = store.getStore();
|
|
22
|
+
})
|
|
23
|
+
),
|
|
24
|
+
store.run('third', () =>
|
|
25
|
+
lock.acquire(done => {
|
|
26
|
+
seen.third = store.getStore();
|
|
27
|
+
done(null);
|
|
28
|
+
})
|
|
29
|
+
),
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
expect(seen).toEqual({ first: 'first', second: 'second', third: 'third' });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('calls the callback in the context of its own acquire call', testDone => {
|
|
36
|
+
const lock = new BetterLock({ queue_size: 1 });
|
|
37
|
+
|
|
38
|
+
store.run('first', () => lock.acquire(waitCallback(20), () => {}));
|
|
39
|
+
store.run('second', () =>
|
|
40
|
+
lock.acquire(waitCallback(0), err => {
|
|
41
|
+
// Kicked out by the third acquire, from its context
|
|
42
|
+
expect(err).toBeInstanceOf(BetterLock.QueueOverflowError);
|
|
43
|
+
expect(store.getStore()).toEqual('second');
|
|
44
|
+
testDone();
|
|
45
|
+
})
|
|
46
|
+
);
|
|
47
|
+
store.run('third', () => lock.acquire(waitCallback(0), () => {}));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('leaves the uncontended path unchanged', async () => {
|
|
51
|
+
const lock = new BetterLock();
|
|
52
|
+
const startedAt = Date.now();
|
|
53
|
+
|
|
54
|
+
const result = await store.run('only', () => {
|
|
55
|
+
const promise = lock.acquire(async () => store.getStore());
|
|
56
|
+
expect(lock.canAcquire()).toBeFalsy();
|
|
57
|
+
return promise;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(result).toEqual('only');
|
|
61
|
+
expectMs(Date.now() - startedAt, 0);
|
|
62
|
+
expect(lock.canAcquire()).toBeTruthy();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('BetterLock reentrancy', () => {
|
|
67
|
+
it('runs a reentrant acquire immediately, when the caller holds all keys', async () => {
|
|
68
|
+
const lock = new BetterLock({ wait_timeout: 50 });
|
|
69
|
+
const startedAt = Date.now();
|
|
70
|
+
|
|
71
|
+
const result = await lock.acquire(['a', 'b'], async () => {
|
|
72
|
+
expect(lock.canAcquire('a')).toBeTruthy();
|
|
73
|
+
const inner = await lock.acquire(['b', 'a'], async () => {
|
|
74
|
+
return lock.acquire('a', async () => 'innermost');
|
|
75
|
+
});
|
|
76
|
+
const viaCallback = await new Promise((resolve, reject) => {
|
|
77
|
+
lock.acquire(
|
|
78
|
+
'b',
|
|
79
|
+
done => done(null, 'callback'),
|
|
80
|
+
(err, res) => (err ? reject(err) : resolve(res))
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
return [inner, viaCallback];
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(result).toEqual(['innermost', 'callback']);
|
|
87
|
+
expectMs(Date.now() - startedAt, 0);
|
|
88
|
+
expect(lock.canAcquire('a')).toBeTruthy();
|
|
89
|
+
expect(lock.canAcquire('b')).toBeTruthy();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('treats the default key as a key', async () => {
|
|
93
|
+
const lock = new BetterLock({ wait_timeout: 50 });
|
|
94
|
+
|
|
95
|
+
const result = await lock.acquire(() => lock.acquire(() => Promise.resolve('nested')));
|
|
96
|
+
|
|
97
|
+
expect(result).toEqual('nested');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('counts keys held by all executing jobs in the context', async () => {
|
|
101
|
+
const lock = new BetterLock({ wait_timeout: 50 });
|
|
102
|
+
|
|
103
|
+
const result = await lock.acquire('a', () =>
|
|
104
|
+
lock.acquire('b', () => lock.acquire(['a', 'b'], async () => 'both'))
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
expect(result).toEqual('both');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('lets parallel branches of the holder both pass as reentrant', async () => {
|
|
111
|
+
const lock = new BetterLock({ wait_timeout: 50 });
|
|
112
|
+
let running = 0;
|
|
113
|
+
let maxRunning = 0;
|
|
114
|
+
|
|
115
|
+
const branch = () =>
|
|
116
|
+
lock.acquire('a', async () => {
|
|
117
|
+
running++;
|
|
118
|
+
maxRunning = Math.max(maxRunning, running);
|
|
119
|
+
await waitPromise(10);
|
|
120
|
+
running--;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await lock.acquire('a', () => Promise.all([branch(), branch()]));
|
|
124
|
+
|
|
125
|
+
expect(maxRunning).toEqual(2);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('applies the execution timeout to the reentrant job', async () => {
|
|
129
|
+
const lock = new BetterLock({ execution_timeout: 200 });
|
|
130
|
+
|
|
131
|
+
const result = await lock.acquire('a', async () => {
|
|
132
|
+
const err = await lock
|
|
133
|
+
.acquire('a', () => waitPromise(50), { execution_timeout: 10 })
|
|
134
|
+
.catch(err => err);
|
|
135
|
+
return err;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(result).toBeInstanceOf(BetterLock.ExecutionTimeoutError);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('can abort a reentrant job that has not started yet', async () => {
|
|
142
|
+
const lock = new BetterLock();
|
|
143
|
+
let called = false;
|
|
144
|
+
|
|
145
|
+
const result = await lock.acquire('a', async () => {
|
|
146
|
+
const promise = lock.acquire('a', async () => {
|
|
147
|
+
called = true;
|
|
148
|
+
});
|
|
149
|
+
lock.abort('a');
|
|
150
|
+
return promise.catch(err => err);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
expect(result).toBeInstanceOf(BetterLock.JobAbortedError);
|
|
154
|
+
expect(called).toBeFalsy();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('does not share held keys between lock instances', async () => {
|
|
158
|
+
const lock1 = new BetterLock();
|
|
159
|
+
const lock2 = new BetterLock({ wait_timeout: 20 });
|
|
160
|
+
|
|
161
|
+
const blocker = lock2.acquire('a', () => waitPromise(50));
|
|
162
|
+
const result = await lock1.acquire('a', () =>
|
|
163
|
+
lock2.acquire('a', async () => 'ok').catch(err => err)
|
|
164
|
+
);
|
|
165
|
+
await blocker;
|
|
166
|
+
|
|
167
|
+
expect(result).toBeInstanceOf(BetterLock.WaitTimeoutError);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('rejects when the caller holds only some of the requested keys', async () => {
|
|
171
|
+
const lock = new BetterLock({ wait_timeout: 50 });
|
|
172
|
+
let called = false;
|
|
173
|
+
|
|
174
|
+
const err = await lock.acquire('a', () =>
|
|
175
|
+
lock
|
|
176
|
+
.acquire(['a', 'b'], async () => {
|
|
177
|
+
called = true;
|
|
178
|
+
})
|
|
179
|
+
.catch(err => err)
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
expect(err).toBeInstanceOf(BetterLock.ReentrancyError);
|
|
183
|
+
expect(err.keys).toEqual(['a', 'b']);
|
|
184
|
+
expect(err.held_keys).toEqual(['a']);
|
|
185
|
+
expect(err.stack).toContain('async_context.spec');
|
|
186
|
+
expect(called).toBeFalsy();
|
|
187
|
+
expect(lock.canAcquire('a')).toBeTruthy();
|
|
188
|
+
expect(lock.canAcquire('b')).toBeTruthy();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('passes the reentrancy error to the callback, on a later tick', testDone => {
|
|
192
|
+
const lock = new BetterLock();
|
|
193
|
+
|
|
194
|
+
lock.acquire('a', async () => {
|
|
195
|
+
let returned = false;
|
|
196
|
+
lock.acquire(
|
|
197
|
+
['a', 'b'],
|
|
198
|
+
done => done(null),
|
|
199
|
+
err => {
|
|
200
|
+
expect(returned).toBeTruthy();
|
|
201
|
+
expect(err).toBeInstanceOf(BetterLock.ReentrancyError);
|
|
202
|
+
testDone();
|
|
203
|
+
}
|
|
204
|
+
);
|
|
205
|
+
returned = true;
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('rejects acquireOr with the reentrancy error instead of returning the failure result', async () => {
|
|
210
|
+
const lock = new BetterLock();
|
|
211
|
+
|
|
212
|
+
const result = await lock.acquire('a', () =>
|
|
213
|
+
lock.acquireOr(['a', 'b'], 'failure', async () => 'never').catch(err => err)
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
expect(result).toBeInstanceOf(BetterLock.ReentrancyError);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('rejects any reentrant acquire in "error" mode', async () => {
|
|
220
|
+
const lock = new BetterLock({ reentrancy: 'error' });
|
|
221
|
+
|
|
222
|
+
const results = await lock.acquire('a', async () => {
|
|
223
|
+
expect(lock.canAcquire('a')).toBeFalsy();
|
|
224
|
+
return Promise.all([
|
|
225
|
+
lock.acquire('a', async () => null).catch(err => err),
|
|
226
|
+
lock.acquire(['a'], async () => null).catch(err => err),
|
|
227
|
+
// Not reentrant, so allowed
|
|
228
|
+
lock.acquire('b', async () => 'other key'),
|
|
229
|
+
]);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
expect(results[0]).toBeInstanceOf(BetterLock.ReentrancyError);
|
|
233
|
+
expect(results[0].held_keys).toEqual(['a']);
|
|
234
|
+
expect(results[1]).toBeInstanceOf(BetterLock.ReentrancyError);
|
|
235
|
+
expect(results[2]).toEqual('other key');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('can switch to "error" mode per job', async () => {
|
|
239
|
+
const lock = new BetterLock();
|
|
240
|
+
|
|
241
|
+
const err = await lock.acquire('a', () =>
|
|
242
|
+
lock.acquire('a', async () => null, { reentrancy: 'error' }).catch(err => err)
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
expect(err).toBeInstanceOf(BetterLock.ReentrancyError);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('does not count detached work that runs after the holder has finished', async () => {
|
|
249
|
+
const lock = new BetterLock({ wait_timeout: 500 });
|
|
250
|
+
const events: string[] = [];
|
|
251
|
+
let detached: Promise<any>;
|
|
252
|
+
|
|
253
|
+
await lock.acquire('a', async () => {
|
|
254
|
+
setTimeout(() => {
|
|
255
|
+
detached = lock.acquire('a', async () => {
|
|
256
|
+
events.push('detached');
|
|
257
|
+
});
|
|
258
|
+
}, 20);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Someone else grabs the key before the detached work runs
|
|
262
|
+
const other = lock.acquire('a', async () => {
|
|
263
|
+
await waitPromise(50);
|
|
264
|
+
events.push('other');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
await other;
|
|
268
|
+
await detached;
|
|
269
|
+
|
|
270
|
+
expect(events).toEqual(['other', 'detached']);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('queues a reentrant acquire normally if its holder ends before it starts', async () => {
|
|
274
|
+
const lock = new BetterLock({ wait_timeout: 500 });
|
|
275
|
+
const events: string[] = [];
|
|
276
|
+
let unawaited: Promise<any>;
|
|
277
|
+
|
|
278
|
+
const holder = lock.acquire('a', () => {
|
|
279
|
+
// Returns synchronously, so the holder ends before the nested executor starts
|
|
280
|
+
unawaited = lock.acquire('a', async () => {
|
|
281
|
+
events.push('unawaited');
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
const waiter = lock.acquire('a', async () => {
|
|
285
|
+
await waitPromise(20);
|
|
286
|
+
events.push('waiter');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
await Promise.all([holder, waiter]);
|
|
290
|
+
await unawaited;
|
|
291
|
+
|
|
292
|
+
expect(events).toEqual(['waiter', 'unawaited']);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('skips reentrancy when lock_condition rejects the key', async () => {
|
|
296
|
+
const lock = new BetterLock({ reentrancy: 'error', lock_condition: key => key !== 'free' });
|
|
297
|
+
|
|
298
|
+
const result = await lock.acquire('a', () => lock.acquire('free', async () => 'unlocked'));
|
|
299
|
+
|
|
300
|
+
expect(result).toEqual('unlocked');
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
describe('BetterLock without async_hooks', () => {
|
|
305
|
+
let FallbackLock: typeof BetterLock;
|
|
306
|
+
|
|
307
|
+
beforeAll(() => {
|
|
308
|
+
const processObject = globalThis.process as any;
|
|
309
|
+
const original = processObject.getBuiltinModule;
|
|
310
|
+
processObject.getBuiltinModule = () => undefined;
|
|
311
|
+
try {
|
|
312
|
+
jest.isolateModules(() => {
|
|
313
|
+
FallbackLock = require('../src/index').default;
|
|
314
|
+
});
|
|
315
|
+
} finally {
|
|
316
|
+
processObject.getBuiltinModule = original;
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('runs a contended executor in the context of the previous holder, as before', async () => {
|
|
321
|
+
const store = new AsyncLocalStorage<string>();
|
|
322
|
+
const lock = new FallbackLock();
|
|
323
|
+
let seen: string;
|
|
324
|
+
|
|
325
|
+
await Promise.all([
|
|
326
|
+
store.run('first', () => lock.acquire(() => waitPromise(20))),
|
|
327
|
+
store.run('second', () =>
|
|
328
|
+
lock.acquire(async () => {
|
|
329
|
+
seen = store.getStore();
|
|
330
|
+
})
|
|
331
|
+
),
|
|
332
|
+
]);
|
|
333
|
+
|
|
334
|
+
expect(seen).toEqual('first');
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('makes a nested acquire wait for itself, as before', async () => {
|
|
338
|
+
const lock = new FallbackLock({ wait_timeout: 20, reentrancy: 'error' });
|
|
339
|
+
expect(FallbackLock.ReentrancyError).toBeDefined();
|
|
340
|
+
|
|
341
|
+
const result = await lock.acquire('a', async () => {
|
|
342
|
+
expect(lock.canAcquire('a')).toBeFalsy();
|
|
343
|
+
return lock.acquire('a', async () => 'never').catch(err => err);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
expect(result).toBeInstanceOf(FallbackLock.WaitTimeoutError);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('does not reject partially held keys, as before', async () => {
|
|
350
|
+
const lock = new FallbackLock({ wait_timeout: 20 });
|
|
351
|
+
|
|
352
|
+
const result = await lock.acquire('a', () =>
|
|
353
|
+
lock.acquire(['a', 'b'], async () => 'never').catch(err => err)
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
expect(result).toBeInstanceOf(FallbackLock.WaitTimeoutError);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
function waitCallback(ms: number, ...args): (done: (...args: any) => void) => void {
|
|
361
|
+
return function (done) {
|
|
362
|
+
setTimeout(() => {
|
|
363
|
+
done(...args);
|
|
364
|
+
}, ms);
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function waitPromise(ms, result?) {
|
|
369
|
+
return new Promise(resolve => {
|
|
370
|
+
setTimeout(() => {
|
|
371
|
+
resolve(result);
|
|
372
|
+
}, ms);
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function expectMs(actual, expected, lowerBound = 5, upperBound = 80) {
|
|
377
|
+
expect(actual).toBeGreaterThanOrEqual(expected - lowerBound);
|
|
378
|
+
expect(actual).toBeLessThan(expected + upperBound);
|
|
379
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { LockJob } from './internals';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The subset of Node.js AsyncResource we use. Declared locally, so the package (and its type declarations)
|
|
5
|
+
* don't depend on Node.js types and keep working in browsers.
|
|
6
|
+
*/
|
|
7
|
+
export interface IAsyncResource {
|
|
8
|
+
runInAsyncScope<TResult>(fn: (...args: any[]) => TResult, thisArg?: any, ...args: any[]): TResult;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The subset of Node.js AsyncLocalStorage we use.
|
|
13
|
+
*/
|
|
14
|
+
interface IAsyncLocalStorage<TStore> {
|
|
15
|
+
getStore(): TStore | undefined;
|
|
16
|
+
run<TResult>(store: TStore, fn: () => TResult): TResult;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface IAsyncHooksModule {
|
|
20
|
+
AsyncResource: new (type: string) => IAsyncResource;
|
|
21
|
+
AsyncLocalStorage: new <TStore>() => IAsyncLocalStorage<TStore>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Jobs whose executors are currently running, in the current async context. Jobs from all lock instances
|
|
26
|
+
* share one list; a job only ever matches the queues of the lock that created it.
|
|
27
|
+
*/
|
|
28
|
+
type IHeldJobs = ReadonlyArray<LockJob<any>>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Look up Node.js async_hooks at runtime, without a static import, so bundlers and browsers never see it.
|
|
32
|
+
* Requires Node.js 20.16+ or 22.3+. Returns undefined everywhere else.
|
|
33
|
+
*/
|
|
34
|
+
function lookupAsyncHooks(): IAsyncHooksModule | undefined {
|
|
35
|
+
try {
|
|
36
|
+
const asyncHooks = (globalThis as any).process?.getBuiltinModule?.('node:async_hooks');
|
|
37
|
+
if (
|
|
38
|
+
asyncHooks &&
|
|
39
|
+
typeof asyncHooks.AsyncResource === 'function' &&
|
|
40
|
+
typeof asyncHooks.AsyncLocalStorage === 'function'
|
|
41
|
+
) {
|
|
42
|
+
return asyncHooks;
|
|
43
|
+
}
|
|
44
|
+
} catch (err) {
|
|
45
|
+
// Unsupported runtime
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const asyncHooks = lookupAsyncHooks();
|
|
51
|
+
|
|
52
|
+
const lockContext: IAsyncLocalStorage<IHeldJobs> | undefined = asyncHooks
|
|
53
|
+
? new asyncHooks.AsyncLocalStorage<IHeldJobs>()
|
|
54
|
+
: undefined;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Capture the current async context, so a job can later run in it. Undefined if the runtime doesn't support it.
|
|
58
|
+
*/
|
|
59
|
+
export function captureAsyncContext(): IAsyncResource | undefined {
|
|
60
|
+
return asyncHooks ? new asyncHooks.AsyncResource('BetterLock') : undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Jobs that are executing in the current async context (the caller is inside their executors).
|
|
65
|
+
* Ended jobs can still be listed here (eg. detached work that outlived its job), so check `ended_at`.
|
|
66
|
+
*/
|
|
67
|
+
export function getHeldJobs(): IHeldJobs | undefined {
|
|
68
|
+
return lockContext ? lockContext.getStore() : undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Run fn with the given job added to the jobs held in the current async context.
|
|
73
|
+
*/
|
|
74
|
+
export function runHoldingJob<TResult>(job: LockJob<any>, fn: () => TResult): TResult {
|
|
75
|
+
if (!lockContext) {
|
|
76
|
+
return fn();
|
|
77
|
+
}
|
|
78
|
+
return lockContext.run(withHeldJob(lockContext.getStore(), job), fn);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function withHeldJob(heldJobs: IHeldJobs | undefined, job: LockJob<any>): IHeldJobs {
|
|
82
|
+
// Drop ended jobs, so the list doesn't grow when each executor schedules the next acquire
|
|
83
|
+
const result = heldJobs ? heldJobs.filter(heldJob => !heldJob.ended_at) : [];
|
|
84
|
+
result.push(job);
|
|
85
|
+
return result;
|
|
86
|
+
}
|