@lakutata/mutex 0.0.0-stage → 3.0.0-beta.1
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/LICENSE +23 -0
- package/README.md +212 -2
- package/dist/cjs/exports/Mutex.d.ts +18 -0
- package/dist/cjs/exports/Mutex.js +37 -0
- package/dist/cjs/mutex/Mutex.d.ts +265 -0
- package/dist/cjs/mutex/Mutex.js +403 -0
- package/dist/cjs/mutex/MutexLock.d.ts +91 -0
- package/dist/cjs/mutex/MutexLock.js +113 -0
- package/dist/cjs/mutex/Synchronized.d.ts +90 -0
- package/dist/cjs/mutex/Synchronized.js +93 -0
- package/dist/cjs/mutex/drivers/FileMutexDriver.d.ts +115 -0
- package/dist/cjs/mutex/drivers/FileMutexDriver.js +230 -0
- package/dist/cjs/mutex/drivers/MemoryMutexDriver.d.ts +60 -0
- package/dist/cjs/mutex/drivers/MemoryMutexDriver.js +95 -0
- package/dist/cjs/mutex/drivers/MutexDriver.d.ts +82 -0
- package/dist/cjs/mutex/drivers/MutexDriver.js +38 -0
- package/dist/cjs/mutex/drivers/MysqlMutexDriver.d.ts +135 -0
- package/dist/cjs/mutex/drivers/MysqlMutexDriver.js +244 -0
- package/dist/cjs/mutex/drivers/PostgresMutexDriver.d.ts +136 -0
- package/dist/cjs/mutex/drivers/PostgresMutexDriver.js +230 -0
- package/dist/cjs/mutex/drivers/RedisMutexDriver.d.ts +129 -0
- package/dist/cjs/mutex/drivers/RedisMutexDriver.js +180 -0
- package/dist/cjs/mutex/exceptions/MutexDriverException.d.ts +12 -0
- package/dist/cjs/mutex/exceptions/MutexDriverException.js +19 -0
- package/dist/cjs/mutex/exceptions/MutexDriverNotFoundException.d.ts +12 -0
- package/dist/cjs/mutex/exceptions/MutexDriverNotFoundException.js +19 -0
- package/dist/cjs/mutex/exceptions/MutexLostException.d.ts +13 -0
- package/dist/cjs/mutex/exceptions/MutexLostException.js +20 -0
- package/dist/cjs/mutex/exceptions/MutexTimeoutException.d.ts +16 -0
- package/dist/cjs/mutex/exceptions/MutexTimeoutException.js +23 -0
- package/dist/cjs/mutex/lib/LoadDriver.d.ts +6 -0
- package/dist/cjs/mutex/lib/LoadDriver.js +64 -0
- package/dist/cjs/mutex/lib/Names.d.ts +14 -0
- package/dist/cjs/mutex/lib/Names.js +26 -0
- package/dist/cjs/mutex/options/FileMutexOptions.d.ts +33 -0
- package/dist/cjs/mutex/options/FileMutexOptions.js +45 -0
- package/dist/cjs/mutex/options/MemoryMutexOptions.d.ts +27 -0
- package/dist/cjs/mutex/options/MemoryMutexOptions.js +41 -0
- package/dist/cjs/mutex/options/MysqlMutexOptions.d.ts +70 -0
- package/dist/cjs/mutex/options/MysqlMutexOptions.js +77 -0
- package/dist/cjs/mutex/options/PostgresMutexOptions.d.ts +83 -0
- package/dist/cjs/mutex/options/PostgresMutexOptions.js +89 -0
- package/dist/cjs/mutex/options/RedisMutexOptions.d.ts +71 -0
- package/dist/cjs/mutex/options/RedisMutexOptions.js +77 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/exports/Mutex.js +18 -0
- package/dist/esm/mutex/Mutex.js +397 -0
- package/dist/esm/mutex/MutexLock.js +109 -0
- package/dist/esm/mutex/Synchronized.js +89 -0
- package/dist/esm/mutex/drivers/FileMutexDriver.js +223 -0
- package/dist/esm/mutex/drivers/MemoryMutexDriver.js +91 -0
- package/dist/esm/mutex/drivers/MutexDriver.js +34 -0
- package/dist/esm/mutex/drivers/MysqlMutexDriver.js +240 -0
- package/dist/esm/mutex/drivers/PostgresMutexDriver.js +226 -0
- package/dist/esm/mutex/drivers/RedisMutexDriver.js +176 -0
- package/dist/esm/mutex/exceptions/MutexDriverException.js +15 -0
- package/dist/esm/mutex/exceptions/MutexDriverNotFoundException.js +15 -0
- package/dist/esm/mutex/exceptions/MutexLostException.js +16 -0
- package/dist/esm/mutex/exceptions/MutexTimeoutException.js +19 -0
- package/dist/esm/mutex/lib/LoadDriver.js +28 -0
- package/dist/esm/mutex/lib/Names.js +21 -0
- package/dist/esm/mutex/options/FileMutexOptions.js +41 -0
- package/dist/esm/mutex/options/MemoryMutexOptions.js +37 -0
- package/dist/esm/mutex/options/MysqlMutexOptions.js +73 -0
- package/dist/esm/mutex/options/PostgresMutexOptions.js +85 -0
- package/dist/esm/mutex/options/RedisMutexOptions.js +73 -0
- package/dist/types/exports/Mutex.d.ts +18 -0
- package/dist/types/mutex/Mutex.d.ts +265 -0
- package/dist/types/mutex/MutexLock.d.ts +91 -0
- package/dist/types/mutex/Synchronized.d.ts +90 -0
- package/dist/types/mutex/drivers/FileMutexDriver.d.ts +115 -0
- package/dist/types/mutex/drivers/MemoryMutexDriver.d.ts +60 -0
- package/dist/types/mutex/drivers/MutexDriver.d.ts +82 -0
- package/dist/types/mutex/drivers/MysqlMutexDriver.d.ts +135 -0
- package/dist/types/mutex/drivers/PostgresMutexDriver.d.ts +136 -0
- package/dist/types/mutex/drivers/RedisMutexDriver.d.ts +129 -0
- package/dist/types/mutex/exceptions/MutexDriverException.d.ts +12 -0
- package/dist/types/mutex/exceptions/MutexDriverNotFoundException.d.ts +12 -0
- package/dist/types/mutex/exceptions/MutexLostException.d.ts +13 -0
- package/dist/types/mutex/exceptions/MutexTimeoutException.d.ts +16 -0
- package/dist/types/mutex/lib/LoadDriver.d.ts +6 -0
- package/dist/types/mutex/lib/Names.d.ts +14 -0
- package/dist/types/mutex/options/FileMutexOptions.d.ts +33 -0
- package/dist/types/mutex/options/MemoryMutexOptions.d.ts +27 -0
- package/dist/types/mutex/options/MysqlMutexOptions.d.ts +70 -0
- package/dist/types/mutex/options/PostgresMutexOptions.d.ts +83 -0
- package/dist/types/mutex/options/RedisMutexOptions.d.ts +71 -0
- package/package.json +57 -4
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MemoryMutexDriver = void 0;
|
|
4
|
+
const MutexDriver_js_1 = require("./MutexDriver.js");
|
|
5
|
+
const Names_js_1 = require("../lib/Names.js");
|
|
6
|
+
/**
|
|
7
|
+
* The locks of the process, shared by its components: the ones of a single instance (its development, its tests)
|
|
8
|
+
*/
|
|
9
|
+
const LOCKS = new Map();
|
|
10
|
+
let FENCE = 0;
|
|
11
|
+
/**
|
|
12
|
+
* The memory driver (`{type: 'memory'}`, the default of a {@link Mutex}): the locks live in the process memory, shared
|
|
13
|
+
* by every component and driver of the process using the same prefix, and invisible to the other processes. Use it for
|
|
14
|
+
* development, tests and an application running as a single process. It needs no package.
|
|
15
|
+
*
|
|
16
|
+
* The locks expire after their `ttl` unless renewed. The fencing token is a counter of the process, starting again at 1
|
|
17
|
+
* when the process restarts. The locks are lost with the process.
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* import {MemoryMutexDriver, type DriverLock} from 'lakutata/com/mutex'
|
|
21
|
+
*
|
|
22
|
+
* const driver: MemoryMutexDriver = new MemoryMutexDriver('test:')
|
|
23
|
+
* const first: DriverLock | undefined = await driver.acquire('job', 1000)
|
|
24
|
+
* console.log(await driver.acquire('job', 1000)) //undefined: held by the first
|
|
25
|
+
* if (first) console.log(await driver.release('job', first)) //true
|
|
26
|
+
* await driver.close()
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
class MemoryMutexDriver extends MutexDriver_js_1.MutexDriver {
|
|
30
|
+
#locks;
|
|
31
|
+
/**
|
|
32
|
+
* Create a driver on the process memory.
|
|
33
|
+
* @param prefix The prefix added to the lock names; the drivers of the process with the same prefix share their
|
|
34
|
+
* locks. `''` by default.
|
|
35
|
+
*/
|
|
36
|
+
constructor(prefix = '') {
|
|
37
|
+
super();
|
|
38
|
+
this.prefix = prefix;
|
|
39
|
+
/**
|
|
40
|
+
* `true`: the locks expire after their `ttl` unless renewed.
|
|
41
|
+
*/
|
|
42
|
+
this.expiring = true;
|
|
43
|
+
this.#locks = LOCKS;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Try to acquire a lock once: granted when nobody holds it or its holder let it expire.
|
|
47
|
+
* @param name The lock name.
|
|
48
|
+
* @param ttl How long the lock stays held without a renewal, in milliseconds.
|
|
49
|
+
* @returns The lock with a fencing token, or `undefined` when another holder has it.
|
|
50
|
+
*/
|
|
51
|
+
async acquire(name, ttl) {
|
|
52
|
+
const key = this.prefix + name;
|
|
53
|
+
const current = this.#locks.get(key);
|
|
54
|
+
if (current && current.expires > Date.now())
|
|
55
|
+
return undefined;
|
|
56
|
+
const lock = { token: (0, Names_js_1.lockToken)(), fence: ++FENCE };
|
|
57
|
+
this.#locks.set(key, { token: lock.token, expires: Date.now() + ttl });
|
|
58
|
+
return lock;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Release a lock still held by this holder.
|
|
62
|
+
* @param name The lock name.
|
|
63
|
+
* @param lock The lock acquired.
|
|
64
|
+
* @returns Whether it was released (`false` when another holder has taken it since).
|
|
65
|
+
*/
|
|
66
|
+
async release(name, lock) {
|
|
67
|
+
if (this.#locks.get(this.prefix + name)?.token !== lock.token)
|
|
68
|
+
return false;
|
|
69
|
+
this.#locks.delete(this.prefix + name);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Renew a lock still held by this holder, for `ttl` milliseconds from now. A lock expired is not renewed (lost),
|
|
74
|
+
* even when no other holder took it yet.
|
|
75
|
+
* @param name The lock name.
|
|
76
|
+
* @param lock The lock acquired.
|
|
77
|
+
* @param ttl How long the lock stays held without another renewal, in milliseconds.
|
|
78
|
+
* @returns Whether it was renewed.
|
|
79
|
+
*/
|
|
80
|
+
async extend(name, lock, ttl) {
|
|
81
|
+
const current = this.#locks.get(this.prefix + name);
|
|
82
|
+
//Expired: lost, even when nobody took it yet
|
|
83
|
+
if (current?.token !== lock.token || current.expires <= Date.now())
|
|
84
|
+
return false;
|
|
85
|
+
current.expires = Date.now() + ttl;
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Do nothing: the locks stay in the process memory until they are released or expire.
|
|
90
|
+
*/
|
|
91
|
+
async close() {
|
|
92
|
+
//The locks of the process kept: the ones of this component are released by it
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.MemoryMutexDriver = MemoryMutexDriver;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A lock granted by a {@link MutexDriver}: what its holder gives back to release or renew it.
|
|
3
|
+
*/
|
|
4
|
+
export type DriverLock = {
|
|
5
|
+
/**
|
|
6
|
+
* The random token identifying the holder (32 hexadecimal characters); the store only releases or renews the lock
|
|
7
|
+
* for it.
|
|
8
|
+
*/
|
|
9
|
+
token: string;
|
|
10
|
+
/**
|
|
11
|
+
* The fencing token: a number increasing with each acquisition in the store, given by the memory and Redis drivers
|
|
12
|
+
* and the `table` mode of PostgreSQL and MySQL; `undefined` for the file driver and the advisory locks.
|
|
13
|
+
*/
|
|
14
|
+
fence?: number;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* The base class of the lock drivers: it acquires, releases and renews the locks in a store shared by their holders.
|
|
18
|
+
* A {@link Mutex} creates its driver from its `driver` option and adds the waiting, the automatic renewal and the lost
|
|
19
|
+
* locks; a custom subclass cannot be given to it. The drivers are exported to use a store directly (scripts, tests):
|
|
20
|
+
* they neither wait nor renew, and their errors are the clients' errors, not a {@link MutexDriverException}.
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* import {MemoryMutexDriver, type DriverLock, type MutexDriver} from 'lakutata/com/mutex'
|
|
24
|
+
*
|
|
25
|
+
* async function once(driver: MutexDriver, name: string, work: () => Promise<void>): Promise<boolean> {
|
|
26
|
+
* const lock: DriverLock | undefined = await driver.acquire(name, 10000)
|
|
27
|
+
* if (!lock) return false
|
|
28
|
+
* try {
|
|
29
|
+
* await work()
|
|
30
|
+
* } finally {
|
|
31
|
+
* await driver.release(name, lock)
|
|
32
|
+
* }
|
|
33
|
+
* return true
|
|
34
|
+
* }
|
|
35
|
+
*
|
|
36
|
+
* const driver: MutexDriver = new MemoryMutexDriver('script:')
|
|
37
|
+
* console.log(await once(driver, 'migrate', async (): Promise<void> => undefined))
|
|
38
|
+
* await driver.close()
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare abstract class MutexDriver {
|
|
42
|
+
/**
|
|
43
|
+
* Whether the locks expire after their `ttl` unless renewed: `true` for the memory, file and Redis drivers and the
|
|
44
|
+
* `table` mode; `false` for the advisory locks of PostgreSQL and MySQL, which end with their connection instead.
|
|
45
|
+
*/
|
|
46
|
+
abstract readonly expiring: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Called with the names of the locks the driver lost at once, when the connection holding advisory locks closes or
|
|
49
|
+
* fails. A {@link Mutex} sets it; it does nothing by default.
|
|
50
|
+
*/
|
|
51
|
+
onLost: (names: string[]) => void;
|
|
52
|
+
/**
|
|
53
|
+
* Try to acquire a lock once, without waiting.
|
|
54
|
+
* @param name The lock name; the driver's prefix is added to it in the store.
|
|
55
|
+
* @param ttl How long the lock stays held without a renewal, in milliseconds (ignored by the advisory locks).
|
|
56
|
+
* @returns The lock granted, or `undefined` when another holder has it.
|
|
57
|
+
* @throws {MutexDriverNotFoundException} When the client package of the driver is not installed.
|
|
58
|
+
*/
|
|
59
|
+
abstract acquire(name: string, ttl: number): Promise<DriverLock | undefined>;
|
|
60
|
+
/**
|
|
61
|
+
* Release a lock, when the store still has it for this holder.
|
|
62
|
+
* @param name The lock name, as acquired.
|
|
63
|
+
* @param lock The lock returned by {@link MutexDriver.acquire}.
|
|
64
|
+
* @returns Whether it was released: `false` when the store no longer has it for this holder (expired, taken, or its
|
|
65
|
+
* connection closed).
|
|
66
|
+
*/
|
|
67
|
+
abstract release(name: string, lock: DriverLock): Promise<boolean>;
|
|
68
|
+
/**
|
|
69
|
+
* Renew a lock for `ttl` milliseconds from now, when the store still has it for this holder; for the advisory locks,
|
|
70
|
+
* check that it is still held.
|
|
71
|
+
* @param name The lock name, as acquired.
|
|
72
|
+
* @param lock The lock returned by {@link MutexDriver.acquire}.
|
|
73
|
+
* @param ttl How long the lock stays held without another renewal, in milliseconds.
|
|
74
|
+
* @returns Whether the lock is still held by this holder.
|
|
75
|
+
*/
|
|
76
|
+
abstract extend(name: string, lock: DriverLock, ttl: number): Promise<boolean>;
|
|
77
|
+
/**
|
|
78
|
+
* Close the driver's connections; the advisory locks they hold are released by the database. The driver is not meant
|
|
79
|
+
* to be used after it.
|
|
80
|
+
*/
|
|
81
|
+
abstract close(): Promise<void>;
|
|
82
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MutexDriver = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The base class of the lock drivers: it acquires, releases and renews the locks in a store shared by their holders.
|
|
6
|
+
* A {@link Mutex} creates its driver from its `driver` option and adds the waiting, the automatic renewal and the lost
|
|
7
|
+
* locks; a custom subclass cannot be given to it. The drivers are exported to use a store directly (scripts, tests):
|
|
8
|
+
* they neither wait nor renew, and their errors are the clients' errors, not a {@link MutexDriverException}.
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import {MemoryMutexDriver, type DriverLock, type MutexDriver} from 'lakutata/com/mutex'
|
|
12
|
+
*
|
|
13
|
+
* async function once(driver: MutexDriver, name: string, work: () => Promise<void>): Promise<boolean> {
|
|
14
|
+
* const lock: DriverLock | undefined = await driver.acquire(name, 10000)
|
|
15
|
+
* if (!lock) return false
|
|
16
|
+
* try {
|
|
17
|
+
* await work()
|
|
18
|
+
* } finally {
|
|
19
|
+
* await driver.release(name, lock)
|
|
20
|
+
* }
|
|
21
|
+
* return true
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* const driver: MutexDriver = new MemoryMutexDriver('script:')
|
|
25
|
+
* console.log(await once(driver, 'migrate', async (): Promise<void> => undefined))
|
|
26
|
+
* await driver.close()
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
class MutexDriver {
|
|
30
|
+
constructor() {
|
|
31
|
+
/**
|
|
32
|
+
* Called with the names of the locks the driver lost at once, when the connection holding advisory locks closes or
|
|
33
|
+
* fails. A {@link Mutex} sets it; it does nothing by default.
|
|
34
|
+
*/
|
|
35
|
+
this.onLost = () => undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.MutexDriver = MutexDriver;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { MutexDriver, type DriverLock } from './MutexDriver.js';
|
|
2
|
+
/**
|
|
3
|
+
* The MySQL (or MariaDB) connection of a {@link MysqlMutexDriver}.
|
|
4
|
+
*/
|
|
5
|
+
export type MysqlMutexConnection = {
|
|
6
|
+
/**
|
|
7
|
+
* The host name or IP address of the server.
|
|
8
|
+
*/
|
|
9
|
+
host: string;
|
|
10
|
+
/**
|
|
11
|
+
* The port of the server.
|
|
12
|
+
* @default 3306
|
|
13
|
+
*/
|
|
14
|
+
port?: number;
|
|
15
|
+
/**
|
|
16
|
+
* The database holding the table of the `table` mode (the named locks are global to the server).
|
|
17
|
+
*/
|
|
18
|
+
database: string;
|
|
19
|
+
/**
|
|
20
|
+
* The user name.
|
|
21
|
+
*/
|
|
22
|
+
username: string;
|
|
23
|
+
/**
|
|
24
|
+
* The password.
|
|
25
|
+
*/
|
|
26
|
+
password: string;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* The MySQL driver (`{type: 'mysql'}`, MySQL or MariaDB): the locks are shared by every instance connected to the same
|
|
30
|
+
* server with the same prefix. It needs the optional peer dependency `mysql2` (3); without it, the first acquisition
|
|
31
|
+
* fails with {@link MutexDriverNotFoundException}. It has two modes:
|
|
32
|
+
* - `'advisory'` (the default): named locks (`GET_LOCK`) taken on one connection of the driver, opened at the first use.
|
|
33
|
+
* They do not expire and need no renewal: MySQL releases them when the connection closes, so a crashed process frees
|
|
34
|
+
* them at once. When the connection closes or fails, every lock it held is lost (a {@link Mutex} aborts their
|
|
35
|
+
* signals) and the next acquisition opens a new connection. A connection cut without the client noticing it is not
|
|
36
|
+
* detected by the renewals. No fencing token. The named locks are not replicated by the clusters (Galera, Group
|
|
37
|
+
* Replication): every holder must connect to the same server.
|
|
38
|
+
* - `'table'`: one row per lock in a table, created at the first use with its counter table `<table>_fence`. The rows
|
|
39
|
+
* expire by the database clock after their `ttl` unless renewed, so it works through the poolers and the clusters, and
|
|
40
|
+
* each acquisition gets a fencing token from the counter. A pool of 4 connections is used.
|
|
41
|
+
* @example
|
|
42
|
+
* ```typescript
|
|
43
|
+
* import {MysqlMutexDriver, type DriverLock} from 'lakutata/com/mutex'
|
|
44
|
+
*
|
|
45
|
+
* const driver: MysqlMutexDriver = new MysqlMutexDriver({
|
|
46
|
+
* host: '127.0.0.1',
|
|
47
|
+
* database: 'app',
|
|
48
|
+
* username: 'app',
|
|
49
|
+
* password: 'secret'
|
|
50
|
+
* }, 'my-app:')
|
|
51
|
+
* const lock: DriverLock | undefined = await driver.acquire('invoice:7', 30000)
|
|
52
|
+
* if (lock) await driver.release('invoice:7', lock)
|
|
53
|
+
* //Closing the connection releases its named locks
|
|
54
|
+
* await driver.close()
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export declare class MysqlMutexDriver extends MutexDriver {
|
|
58
|
+
#private;
|
|
59
|
+
protected readonly connection: MysqlMutexConnection;
|
|
60
|
+
protected readonly prefix: string;
|
|
61
|
+
protected readonly mode: 'advisory' | 'table';
|
|
62
|
+
protected readonly table: string;
|
|
63
|
+
/**
|
|
64
|
+
* `true` in the `table` mode (the rows expire), `false` in the `advisory` mode (the locks end with the connection).
|
|
65
|
+
*/
|
|
66
|
+
readonly expiring: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Create a driver on a MySQL server; it connects at its first use.
|
|
69
|
+
* @param connection The connection to the server.
|
|
70
|
+
* @param prefix The prefix added to the lock names.
|
|
71
|
+
* @param mode `'advisory'` (the default) or `'table'`.
|
|
72
|
+
* @param table The table of the `table` mode, in the connection's database; `'lakutata_mutex'` by default.
|
|
73
|
+
* @throws {Error} When the table is not a valid name (letters, digits and `_`, not starting with a digit, without a
|
|
74
|
+
* database qualifier).
|
|
75
|
+
*/
|
|
76
|
+
constructor(connection: MysqlMutexConnection, prefix: string, mode?: 'advisory' | 'table', table?: string);
|
|
77
|
+
/**
|
|
78
|
+
* The configuration given to `mysql2` for the connections.
|
|
79
|
+
* @returns The `mysql2` connection configuration.
|
|
80
|
+
*/
|
|
81
|
+
protected config(): Record<string, unknown>;
|
|
82
|
+
/**
|
|
83
|
+
* The name of a lock for `GET_LOCK`: `lakutata:` and the start of the SHA-256 digest of the prefixed name, 64
|
|
84
|
+
* characters in all (the limit of MySQL).
|
|
85
|
+
* @param name The lock name.
|
|
86
|
+
* @returns The MySQL lock name.
|
|
87
|
+
*/
|
|
88
|
+
protected lockName(name: string): string;
|
|
89
|
+
/**
|
|
90
|
+
* The connection holding the named locks, opened at the first call; when it ends or fails, its locks are reported
|
|
91
|
+
* lost through {@link MutexDriver.onLost}.
|
|
92
|
+
* @returns The `mysql2` connection.
|
|
93
|
+
* @throws {MutexDriverNotFoundException} When the package `mysql2` is not installed.
|
|
94
|
+
*/
|
|
95
|
+
protected session(): Promise<any>;
|
|
96
|
+
/**
|
|
97
|
+
* The connection pool of the `table` mode, created at the first call along with the tables; created again at the
|
|
98
|
+
* next call when that fails.
|
|
99
|
+
* @returns The `mysql2` pool.
|
|
100
|
+
* @throws {MutexDriverNotFoundException} When the package `mysql2` is not installed.
|
|
101
|
+
*/
|
|
102
|
+
protected pool(): Promise<any>;
|
|
103
|
+
/**
|
|
104
|
+
* Try to acquire a lock once: a named lock of the driver's connection, or a row inserted (or taken over when
|
|
105
|
+
* expired) in the table.
|
|
106
|
+
* @param name The lock name.
|
|
107
|
+
* @param ttl How long the lock stays held without a renewal, in milliseconds (the `table` mode only).
|
|
108
|
+
* @returns The lock (with a fencing token in the `table` mode), or `undefined` when another holder has it, this
|
|
109
|
+
* driver included.
|
|
110
|
+
* @throws {MutexDriverNotFoundException} When the package `mysql2` is not installed.
|
|
111
|
+
*/
|
|
112
|
+
acquire(name: string, ttl: number): Promise<DriverLock | undefined>;
|
|
113
|
+
/**
|
|
114
|
+
* Release a lock still held by this holder: `RELEASE_LOCK`, or delete the row.
|
|
115
|
+
* @param name The lock name.
|
|
116
|
+
* @param lock The lock acquired.
|
|
117
|
+
* @returns Whether it was released (`false` when the row expired and was taken, or the connection was lost).
|
|
118
|
+
*/
|
|
119
|
+
release(name: string, lock: DriverLock): Promise<boolean>;
|
|
120
|
+
/**
|
|
121
|
+
* Renew a lock: in the `table` mode, push back the expiration of its row when it still holds this holder's token; in
|
|
122
|
+
* the `advisory` mode, check that the driver still holds it on its connection, by a query (a connection cut without
|
|
123
|
+
* notice fails it).
|
|
124
|
+
* @param name The lock name.
|
|
125
|
+
* @param lock The lock acquired.
|
|
126
|
+
* @param ttl How long the lock stays held without another renewal, in milliseconds (the `table` mode only).
|
|
127
|
+
* @returns Whether the lock is still held by this holder.
|
|
128
|
+
*/
|
|
129
|
+
extend(name: string, lock: DriverLock, ttl: number): Promise<boolean>;
|
|
130
|
+
/**
|
|
131
|
+
* Close the connection (MySQL releases its named locks) and the pool; the rows of the `table` mode stay until they
|
|
132
|
+
* expire.
|
|
133
|
+
*/
|
|
134
|
+
close(): Promise<void>;
|
|
135
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MysqlMutexDriver = void 0;
|
|
4
|
+
const MutexDriver_js_1 = require("./MutexDriver.js");
|
|
5
|
+
const LoadDriver_js_1 = require("../lib/LoadDriver.js");
|
|
6
|
+
const Names_js_1 = require("../lib/Names.js");
|
|
7
|
+
/**
|
|
8
|
+
* The MySQL driver (`{type: 'mysql'}`, MySQL or MariaDB): the locks are shared by every instance connected to the same
|
|
9
|
+
* server with the same prefix. It needs the optional peer dependency `mysql2` (3); without it, the first acquisition
|
|
10
|
+
* fails with {@link MutexDriverNotFoundException}. It has two modes:
|
|
11
|
+
* - `'advisory'` (the default): named locks (`GET_LOCK`) taken on one connection of the driver, opened at the first use.
|
|
12
|
+
* They do not expire and need no renewal: MySQL releases them when the connection closes, so a crashed process frees
|
|
13
|
+
* them at once. When the connection closes or fails, every lock it held is lost (a {@link Mutex} aborts their
|
|
14
|
+
* signals) and the next acquisition opens a new connection. A connection cut without the client noticing it is not
|
|
15
|
+
* detected by the renewals. No fencing token. The named locks are not replicated by the clusters (Galera, Group
|
|
16
|
+
* Replication): every holder must connect to the same server.
|
|
17
|
+
* - `'table'`: one row per lock in a table, created at the first use with its counter table `<table>_fence`. The rows
|
|
18
|
+
* expire by the database clock after their `ttl` unless renewed, so it works through the poolers and the clusters, and
|
|
19
|
+
* each acquisition gets a fencing token from the counter. A pool of 4 connections is used.
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* import {MysqlMutexDriver, type DriverLock} from 'lakutata/com/mutex'
|
|
23
|
+
*
|
|
24
|
+
* const driver: MysqlMutexDriver = new MysqlMutexDriver({
|
|
25
|
+
* host: '127.0.0.1',
|
|
26
|
+
* database: 'app',
|
|
27
|
+
* username: 'app',
|
|
28
|
+
* password: 'secret'
|
|
29
|
+
* }, 'my-app:')
|
|
30
|
+
* const lock: DriverLock | undefined = await driver.acquire('invoice:7', 30000)
|
|
31
|
+
* if (lock) await driver.release('invoice:7', lock)
|
|
32
|
+
* //Closing the connection releases its named locks
|
|
33
|
+
* await driver.close()
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
class MysqlMutexDriver extends MutexDriver_js_1.MutexDriver {
|
|
37
|
+
#connection;
|
|
38
|
+
#pool;
|
|
39
|
+
#connecting;
|
|
40
|
+
#table;
|
|
41
|
+
#held = new Map();
|
|
42
|
+
/**
|
|
43
|
+
* Create a driver on a MySQL server; it connects at its first use.
|
|
44
|
+
* @param connection The connection to the server.
|
|
45
|
+
* @param prefix The prefix added to the lock names.
|
|
46
|
+
* @param mode `'advisory'` (the default) or `'table'`.
|
|
47
|
+
* @param table The table of the `table` mode, in the connection's database; `'lakutata_mutex'` by default.
|
|
48
|
+
* @throws {Error} When the table is not a valid name (letters, digits and `_`, not starting with a digit, without a
|
|
49
|
+
* database qualifier).
|
|
50
|
+
*/
|
|
51
|
+
constructor(connection, prefix, mode = 'advisory', table = 'lakutata_mutex') {
|
|
52
|
+
super();
|
|
53
|
+
this.connection = connection;
|
|
54
|
+
this.prefix = prefix;
|
|
55
|
+
this.mode = mode;
|
|
56
|
+
this.table = table;
|
|
57
|
+
this.expiring = mode === 'table';
|
|
58
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table))
|
|
59
|
+
throw new Error(`The table of the locks "${table}" is not a name of MySQL`);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The configuration given to `mysql2` for the connections.
|
|
63
|
+
* @returns The `mysql2` connection configuration.
|
|
64
|
+
*/
|
|
65
|
+
config() {
|
|
66
|
+
return { host: this.connection.host, port: this.connection.port ?? 3306, database: this.connection.database, user: this.connection.username, password: this.connection.password };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The name of a lock for `GET_LOCK`: `lakutata:` and the start of the SHA-256 digest of the prefixed name, 64
|
|
70
|
+
* characters in all (the limit of MySQL).
|
|
71
|
+
* @param name The lock name.
|
|
72
|
+
* @returns The MySQL lock name.
|
|
73
|
+
*/
|
|
74
|
+
lockName(name) {
|
|
75
|
+
return `lakutata:${(0, Names_js_1.nameDigest)(this.prefix + name).slice(0, 55)}`;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The connection holding the named locks, opened at the first call; when it ends or fails, its locks are reported
|
|
79
|
+
* lost through {@link MutexDriver.onLost}.
|
|
80
|
+
* @returns The `mysql2` connection.
|
|
81
|
+
* @throws {MutexDriverNotFoundException} When the package `mysql2` is not installed.
|
|
82
|
+
*/
|
|
83
|
+
async session() {
|
|
84
|
+
if (this.#connection)
|
|
85
|
+
return this.#connection;
|
|
86
|
+
this.#connecting ??= (async () => {
|
|
87
|
+
const mysql = await (0, LoadDriver_js_1.loadDriver)('createConnection', 'mysql2/promise');
|
|
88
|
+
const connection = await mysql.createConnection(this.config());
|
|
89
|
+
const lost = () => {
|
|
90
|
+
if (this.#connection !== connection)
|
|
91
|
+
return;
|
|
92
|
+
this.#connection = undefined;
|
|
93
|
+
const names = [...this.#held.keys()];
|
|
94
|
+
this.#held.clear();
|
|
95
|
+
if (names.length)
|
|
96
|
+
this.onLost(names);
|
|
97
|
+
};
|
|
98
|
+
connection.on('error', lost);
|
|
99
|
+
connection.on('end', lost);
|
|
100
|
+
this.#connection = connection;
|
|
101
|
+
return connection;
|
|
102
|
+
})().finally(() => {
|
|
103
|
+
this.#connecting = undefined;
|
|
104
|
+
});
|
|
105
|
+
return await this.#connecting;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The connection pool of the `table` mode, created at the first call along with the tables; created again at the
|
|
109
|
+
* next call when that fails.
|
|
110
|
+
* @returns The `mysql2` pool.
|
|
111
|
+
* @throws {MutexDriverNotFoundException} When the package `mysql2` is not installed.
|
|
112
|
+
*/
|
|
113
|
+
async pool() {
|
|
114
|
+
//Created once: the first uses at the same time share it
|
|
115
|
+
this.#table ??= (async () => {
|
|
116
|
+
const mysql = await (0, LoadDriver_js_1.loadDriver)('createPool', 'mysql2/promise');
|
|
117
|
+
const pool = mysql.createPool({ ...this.config(), connectionLimit: 4 });
|
|
118
|
+
this.#pool = pool;
|
|
119
|
+
await this.#pool.query(`CREATE TABLE IF NOT EXISTS \`${this.table}\` (\`key\` VARCHAR(64) PRIMARY KEY, name TEXT NOT NULL, token VARCHAR(64) NOT NULL, fence BIGINT NOT NULL, expires_at DATETIME(3) NOT NULL)`);
|
|
120
|
+
await this.#pool.query(`CREATE TABLE IF NOT EXISTS \`${this.table}_fence\` (id TINYINT PRIMARY KEY, value BIGINT NOT NULL)`);
|
|
121
|
+
await this.#pool.query(`INSERT IGNORE INTO \`${this.table}_fence\` (id, value) VALUES (1, 0)`);
|
|
122
|
+
})().catch(async (error) => {
|
|
123
|
+
//Created again at the next use: this pool closed
|
|
124
|
+
this.#table = undefined;
|
|
125
|
+
const failed = this.#pool;
|
|
126
|
+
this.#pool = undefined;
|
|
127
|
+
await failed?.end().catch(() => undefined);
|
|
128
|
+
throw error;
|
|
129
|
+
});
|
|
130
|
+
await this.#table;
|
|
131
|
+
return this.#pool;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Try to acquire a lock once: a named lock of the driver's connection, or a row inserted (or taken over when
|
|
135
|
+
* expired) in the table.
|
|
136
|
+
* @param name The lock name.
|
|
137
|
+
* @param ttl How long the lock stays held without a renewal, in milliseconds (the `table` mode only).
|
|
138
|
+
* @returns The lock (with a fencing token in the `table` mode), or `undefined` when another holder has it, this
|
|
139
|
+
* driver included.
|
|
140
|
+
* @throws {MutexDriverNotFoundException} When the package `mysql2` is not installed.
|
|
141
|
+
*/
|
|
142
|
+
async acquire(name, ttl) {
|
|
143
|
+
const token = (0, Names_js_1.lockToken)();
|
|
144
|
+
if (this.mode === 'table') {
|
|
145
|
+
const connection = await (await this.pool()).getConnection();
|
|
146
|
+
try {
|
|
147
|
+
//The fencing token: the counter incremented, read by the connection (LAST_INSERT_ID)
|
|
148
|
+
await connection.query(`UPDATE \`${this.table}_fence\` SET value = LAST_INSERT_ID(value + 1) WHERE id = 1`);
|
|
149
|
+
const [[{ fence }]] = await connection.query('SELECT LAST_INSERT_ID() AS fence');
|
|
150
|
+
const key = (0, Names_js_1.nameDigest)(this.prefix + name);
|
|
151
|
+
//Taken when it is expired: the columns kept otherwise (expires_at assigned last, read before)
|
|
152
|
+
await connection.query(`INSERT INTO \`${this.table}\` (\`key\`, name, token, fence, expires_at) VALUES (?, ?, ?, ?, NOW(3) + INTERVAL ? MICROSECOND)
|
|
153
|
+
ON DUPLICATE KEY UPDATE name = IF(expires_at <= NOW(3), VALUES(name), name), token = IF(expires_at <= NOW(3), VALUES(token), token),
|
|
154
|
+
fence = IF(expires_at <= NOW(3), VALUES(fence), fence), expires_at = IF(expires_at <= NOW(3), VALUES(expires_at), expires_at)`, [key, name, token, fence, Math.ceil(ttl) * 1000]);
|
|
155
|
+
const [rows] = await connection.query(`SELECT token FROM \`${this.table}\` WHERE \`key\` = ?`, [key]);
|
|
156
|
+
return rows[0]?.token === token ? { token: token, fence: Number(fence) } : undefined;
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
connection.release();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (this.#held.has(name))
|
|
163
|
+
return undefined;
|
|
164
|
+
this.#held.set(name, undefined);
|
|
165
|
+
try {
|
|
166
|
+
const [[{ locked }]] = await (await this.session()).query('SELECT GET_LOCK(?, 0) AS locked', [this.lockName(name)]);
|
|
167
|
+
//NULL: an error of the server (the thread killed, out of memory), not a lock held by another
|
|
168
|
+
if (locked === null)
|
|
169
|
+
throw new Error(`GET_LOCK failed for the lock ${name}`);
|
|
170
|
+
if (Number(locked) !== 1) {
|
|
171
|
+
this.#held.delete(name);
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
this.#held.set(name, token);
|
|
175
|
+
return { token: token };
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
this.#held.delete(name);
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Release a lock still held by this holder: `RELEASE_LOCK`, or delete the row.
|
|
184
|
+
* @param name The lock name.
|
|
185
|
+
* @param lock The lock acquired.
|
|
186
|
+
* @returns Whether it was released (`false` when the row expired and was taken, or the connection was lost).
|
|
187
|
+
*/
|
|
188
|
+
async release(name, lock) {
|
|
189
|
+
if (this.mode === 'table') {
|
|
190
|
+
const [result] = await (await this.pool()).query(`DELETE FROM \`${this.table}\` WHERE \`key\` = ? AND token = ?`, [(0, Names_js_1.nameDigest)(this.prefix + name), lock.token]);
|
|
191
|
+
return result.affectedRows === 1;
|
|
192
|
+
}
|
|
193
|
+
if (this.#held.get(name) !== lock.token)
|
|
194
|
+
return false;
|
|
195
|
+
this.#held.delete(name);
|
|
196
|
+
const connection = this.#connection;
|
|
197
|
+
if (!connection)
|
|
198
|
+
return false;
|
|
199
|
+
const [[{ released }]] = await connection.query('SELECT RELEASE_LOCK(?) AS released', [this.lockName(name)]);
|
|
200
|
+
return Number(released) === 1;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Renew a lock: in the `table` mode, push back the expiration of its row when it still holds this holder's token; in
|
|
204
|
+
* the `advisory` mode, check that the driver still holds it on its connection, by a query (a connection cut without
|
|
205
|
+
* notice fails it).
|
|
206
|
+
* @param name The lock name.
|
|
207
|
+
* @param lock The lock acquired.
|
|
208
|
+
* @param ttl How long the lock stays held without another renewal, in milliseconds (the `table` mode only).
|
|
209
|
+
* @returns Whether the lock is still held by this holder.
|
|
210
|
+
*/
|
|
211
|
+
async extend(name, lock, ttl) {
|
|
212
|
+
if (this.mode === 'table') {
|
|
213
|
+
const [result] = await (await this.pool()).query(`UPDATE \`${this.table}\` SET expires_at = NOW(3) + INTERVAL ? MICROSECOND WHERE \`key\` = ? AND token = ?`, [Math.ceil(ttl) * 1000, (0, Names_js_1.nameDigest)(this.prefix + name), lock.token]);
|
|
214
|
+
return result.affectedRows === 1;
|
|
215
|
+
}
|
|
216
|
+
//Held as long as its connection: checked by a query (a connection cut silently fails it)
|
|
217
|
+
const connection = this.#connection;
|
|
218
|
+
if (this.#held.get(name) !== lock.token || !connection)
|
|
219
|
+
return false;
|
|
220
|
+
try {
|
|
221
|
+
await connection.query('SELECT 1');
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Close the connection (MySQL releases its named locks) and the pool; the rows of the `table` mode stay until they
|
|
230
|
+
* expire.
|
|
231
|
+
*/
|
|
232
|
+
async close() {
|
|
233
|
+
const connection = this.#connection;
|
|
234
|
+
this.#connection = undefined;
|
|
235
|
+
this.#held.clear();
|
|
236
|
+
await connection?.end().catch(() => undefined);
|
|
237
|
+
const pool = this.#pool;
|
|
238
|
+
this.#pool = undefined;
|
|
239
|
+
//Created again at the next use
|
|
240
|
+
this.#table = undefined;
|
|
241
|
+
await pool?.end().catch(() => undefined);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
exports.MysqlMutexDriver = MysqlMutexDriver;
|