@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
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023-present Lakutata
|
|
4
|
+
|
|
5
|
+
All rights reserved
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,213 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @lakutata/mutex
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The lock component of the lakutata framework: named locks that give a work to one holder at a time, between the
|
|
4
|
+
components of a process, the processes of a host, or every instance of an application, as its driver shares them
|
|
5
|
+
(memory, lock files, Redis, PostgreSQL or MySQL). The locks are held by a token, renewed while they are held, and lost
|
|
6
|
+
(an `AbortSignal` aborted) when their holder can no longer keep them. It also provides `@Synchronized` for controller
|
|
7
|
+
actions and the locks of the `exclusive` schedules. The umbrella package `lakutata` re-exports it as
|
|
8
|
+
`lakutata/com/mutex`.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```shell
|
|
13
|
+
npm install lakutata
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The memory and file drivers need nothing more; the other drivers use optional peer dependencies, installed when used.
|
|
17
|
+
A driver whose package is missing fails the first acquisition with `MutexDriverNotFoundException`.
|
|
18
|
+
|
|
19
|
+
| Driver | `type` | Shared by | Install | Expires / renewed | Fencing token |
|
|
20
|
+
|---|---|---|---|---|---|
|
|
21
|
+
| Memory (the default) | `'memory'` | the components of one process | none | yes | yes (per process) |
|
|
22
|
+
| Lock files | `'file'` | the processes of one host | none | yes; a lock of a dead process is taken over at once | no |
|
|
23
|
+
| Redis 4+ (or Valkey) | `'redis'` | every instance on the server or cluster | `redis` (4.6 or 5) | yes | yes |
|
|
24
|
+
| PostgreSQL, `mode: 'advisory'` (default) | `'postgres'` | every instance on the database | `pg` (8) | no: released when the connection closes | no |
|
|
25
|
+
| PostgreSQL, `mode: 'table'` | `'postgres'` | every instance on the database, through the poolers | `pg` (8) | yes | yes |
|
|
26
|
+
| MySQL / MariaDB, `mode: 'advisory'` (default) | `'mysql'` | every instance on the server (not the clusters) | `mysql2` (3) | no: released when the connection closes | no |
|
|
27
|
+
| MySQL / MariaDB, `mode: 'table'` | `'mysql'` | every instance on the database, the clusters included | `mysql2` (3) | yes | yes |
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
### The component in an application
|
|
32
|
+
|
|
33
|
+
Register `Mutex` in the `components` of the application, named `mutex` (the name `@Synchronized` and the `exclusive`
|
|
34
|
+
schedules use by default), then get it with `getObject()` or `@Inject`. Without `driver`, the locks are in memory.
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import {Application} from 'lakutata'
|
|
38
|
+
import {Mutex, type MutexLock} from 'lakutata/com/mutex'
|
|
39
|
+
|
|
40
|
+
Application
|
|
41
|
+
.run(() => ({
|
|
42
|
+
id: 'mutex.app',
|
|
43
|
+
name: 'Mutex',
|
|
44
|
+
components: {
|
|
45
|
+
mutex: {
|
|
46
|
+
class: Mutex,
|
|
47
|
+
driver: {type: 'redis', url: 'redis://127.0.0.1:6379'},
|
|
48
|
+
//Held 30 s without a renewal, renewed every 10 s while held
|
|
49
|
+
ttl: 30000,
|
|
50
|
+
//Waited for 10 s at most
|
|
51
|
+
timeout: 10000,
|
|
52
|
+
prefix: 'lakutata:mutex:'
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}))
|
|
56
|
+
.onLaunched(async (app: Application): Promise<void> => {
|
|
57
|
+
const mutex: Mutex = await app.getObject('mutex')
|
|
58
|
+
|
|
59
|
+
//Acquired before the function, released after it, even when it throws
|
|
60
|
+
await mutex.synchronized('report:daily', async (lock: MutexLock): Promise<void> => {
|
|
61
|
+
//Aborted with a MutexLostException when the lock is lost
|
|
62
|
+
lock.signal.throwIfAborted()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
//Waited for up to 3 s, then MutexTimeoutException
|
|
66
|
+
const lock: MutexLock = await mutex.acquire('order:42', {timeout: 3000})
|
|
67
|
+
try {
|
|
68
|
+
console.log(lock.fence)
|
|
69
|
+
} finally {
|
|
70
|
+
await lock.release()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
//Tried once: undefined when another holder has it
|
|
74
|
+
const cleanup: MutexLock | undefined = await mutex.tryAcquire('cleanup')
|
|
75
|
+
await cleanup?.release()
|
|
76
|
+
|
|
77
|
+
//Released at the end of the block
|
|
78
|
+
{
|
|
79
|
+
await using scoped: MutexLock = await mutex.acquire('order:43')
|
|
80
|
+
console.log(scoped.held)
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
- **Holders**: a lock is acquired with a random token; only its holder releases or renews it. A lock is released once:
|
|
86
|
+
the next `release()` calls return `false`.
|
|
87
|
+
- **Expiration**: the expiring drivers hold a lock for its `ttl` (milliseconds), and the component renews it every
|
|
88
|
+
third of that time, so a crashed holder keeps it at most one `ttl`. The advisory locks of PostgreSQL and MySQL do not
|
|
89
|
+
expire: the database releases them when the component's connection closes.
|
|
90
|
+
- **Lost locks**: when a renewal finds the lock expired or taken, when the renewals keep failing (the store unreachable)
|
|
91
|
+
until the lock expires, or when the connection holding an advisory lock closes, the lock is lost: `lock.held` becomes `false` and `lock.signal` is aborted with a
|
|
92
|
+
`MutexLostException`. The work it protects is not interrupted: it should watch the signal.
|
|
93
|
+
- **Fencing tokens**: `lock.fence` increases with each acquisition (memory, Redis, the `table` modes). Write it along
|
|
94
|
+
with the data the lock protects, so that the store refuses the writes of an older holder.
|
|
95
|
+
- **Not reentrant**: acquiring a lock the component already holds waits for its release, like any other holder.
|
|
96
|
+
- **Connections**: the driver connects at the first acquisition, not when the component is created. A store failing
|
|
97
|
+
rejects the calls with `MutexDriverException`; an unreachable Redis server fails the calls (its first connection
|
|
98
|
+
given up after 5 seconds, the calls during a reconnection failing at once).
|
|
99
|
+
- **Lifecycle**: the component releases the locks it holds and closes its driver when it is destroyed (the application
|
|
100
|
+
stopping).
|
|
101
|
+
|
|
102
|
+
### Synchronized controller actions
|
|
103
|
+
|
|
104
|
+
`@Synchronized` runs a controller action holding a lock, acquired once its input is validated and its access control
|
|
105
|
+
passed, released after it. The requests needing the same lock run one at a time in every instance, whatever their
|
|
106
|
+
transport; the others run in parallel. The wait stops when the request is aborted (its client gone); a request not
|
|
107
|
+
getting the lock in time fails with `MutexTimeoutException` (HTTP 409).
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
import {Controller} from 'lakutata/com/entrypoint'
|
|
111
|
+
import {HTTPAction, ServiceAction} from 'lakutata/decorator/ctrl'
|
|
112
|
+
import {Synchronized} from 'lakutata/com/mutex'
|
|
113
|
+
|
|
114
|
+
class OrderController extends Controller {
|
|
115
|
+
//A lock per order: {id} is a field of the validated input
|
|
116
|
+
@HTTPAction('/orders/:id/pay', 'POST')
|
|
117
|
+
@ServiceAction({cmd: 'order.pay'})
|
|
118
|
+
@Synchronized('order:{id}')
|
|
119
|
+
public async pay(input: {id: number}): Promise<void> {
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
//Fails at once (HTTP 409) while another request runs it
|
|
123
|
+
@ServiceAction({cmd: 'search.reindex'})
|
|
124
|
+
@Synchronized('search:reindex', {wait: false})
|
|
125
|
+
public async reindex(): Promise<void> {
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
//A name computed from the input, a lock component registered as "locks", a wait of 5 s
|
|
129
|
+
@ServiceAction({cmd: 'user.export'})
|
|
130
|
+
@Synchronized((input: {userId: string}): string => `export:${input.userId}`, {timeout: 5000, mutex: 'locks'})
|
|
131
|
+
public async export(input: {userId: string}): Promise<void> {
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Exclusive schedules
|
|
137
|
+
|
|
138
|
+
A scheduled action declared `exclusive` (`@Schedule` of `lakutata/decorator/ctrl`) runs in one instance of the
|
|
139
|
+
application at a time: each run tries to acquire the lock `schedule:<key>` (the action name by default) from the
|
|
140
|
+
component `mutex` (or `exclusive: {mutex, key}`), and the instances not getting it skip the run. Its intervals are
|
|
141
|
+
aligned on their multiples so that every instance plans the same times, and its lock is kept until half of its period
|
|
142
|
+
(30 s at most) after a short run, so that the instances whose clocks are late skip it too. Without its lock component,
|
|
143
|
+
the action does not run (the error is logged).
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import {Application} from 'lakutata'
|
|
147
|
+
import {buildEntrypoints, Controller, scheduleEntrypoint} from 'lakutata/com/entrypoint'
|
|
148
|
+
import {Schedule} from 'lakutata/decorator/ctrl'
|
|
149
|
+
import {Mutex} from 'lakutata/com/mutex'
|
|
150
|
+
|
|
151
|
+
class ReportController extends Controller {
|
|
152
|
+
//Every 5 minutes, by one instance
|
|
153
|
+
@Schedule({every: '5m', exclusive: true})
|
|
154
|
+
public async build(): Promise<void> {
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
//At 03:00 every day, the lock named schedule:purge in the component "locks"
|
|
158
|
+
@Schedule({cron: '0 3 * * *', exclusive: {mutex: 'locks', key: 'purge'}})
|
|
159
|
+
public async purge(): Promise<void> {
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
Application.run(() => ({
|
|
164
|
+
id: 'reports.app',
|
|
165
|
+
name: 'Reports',
|
|
166
|
+
components: {
|
|
167
|
+
mutex: {class: Mutex, driver: {type: 'redis', url: 'redis://127.0.0.1:6379'}},
|
|
168
|
+
locks: {class: Mutex, driver: {type: 'postgres', host: 'db', database: 'app', username: 'app', password: ''}},
|
|
169
|
+
entrypoint: buildEntrypoints({
|
|
170
|
+
controllers: [ReportController],
|
|
171
|
+
schedule: scheduleEntrypoint()
|
|
172
|
+
})
|
|
173
|
+
},
|
|
174
|
+
bootstrap: ['entrypoint']
|
|
175
|
+
}))
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## API
|
|
179
|
+
|
|
180
|
+
| Export | What it is |
|
|
181
|
+
|---|---|
|
|
182
|
+
| `Mutex` | The component: `tryAcquire`, `acquire`, `synchronized`, `heldLocks`; the options `driver`, `prefix`, `ttl`, `timeout`, `retryDelay` |
|
|
183
|
+
| `MutexLock` | A lock held: `name`, `token`, `fence`, `acquiredAt`, `held`, `signal`, `release()`, `extend()`, `await using` |
|
|
184
|
+
| `MutexAcquireOptions` | The `ttl`, `timeout` and abort `signal` of an acquisition |
|
|
185
|
+
| `MutexDriverOptions` | The `driver` option: one of the driver options below, told apart by `type` |
|
|
186
|
+
| `MemoryMutexOptions`, `FileMutexOptions`, `RedisMutexOptions`, `PostgresMutexOptions`, `MysqlMutexOptions` | The options of each driver |
|
|
187
|
+
| `Synchronized` | Method decorator running a controller action holding a lock |
|
|
188
|
+
| `SynchronizedOptions` | The lock component, `timeout`, `wait` and `ttl` of `@Synchronized` |
|
|
189
|
+
| `lockNameOf` | Builds a lock name from a template (`'order:{id}'`) and an input |
|
|
190
|
+
| `MutexDriver`, `DriverLock` | The base class of the drivers, and a lock granted by a driver |
|
|
191
|
+
| `MemoryMutexDriver`, `FileMutexDriver`, `RedisMutexDriver`, `PostgresMutexDriver`, `MysqlMutexDriver` | The drivers, usable directly without the component (no waiting, no renewal) |
|
|
192
|
+
| `RedisMutexConnection`, `PostgresMutexConnection`, `MysqlMutexConnection` | The connections given to the drivers' constructors |
|
|
193
|
+
| `MutexTimeoutException`, `MutexLostException`, `MutexDriverException`, `MutexDriverNotFoundException` | The exceptions |
|
|
194
|
+
|
|
195
|
+
All are exported by `lakutata/com/mutex` and `@lakutata/mutex`. The typings document each declaration with examples.
|
|
196
|
+
|
|
197
|
+
## Errors
|
|
198
|
+
|
|
199
|
+
| Exception | When |
|
|
200
|
+
|---|---|
|
|
201
|
+
| `MutexTimeoutException` (`E_MUTEX_TIMEOUT`, HTTP 409) | `acquire()` or `synchronized()` did not get the lock before the timeout; a `@Synchronized` action whose lock another request holds |
|
|
202
|
+
| `MutexLostException` (`E_MUTEX_LOST`) | Not thrown: the abort reason of `lock.signal` when the lock is lost |
|
|
203
|
+
| `MutexDriverException` (`E_MUTEX_DRIVER`) | The lock store failed (unreachable, a failed query, a file error) during a call |
|
|
204
|
+
| `MutexDriverNotFoundException` (`E_MUTEX_DRIVER_NOT_FOUND`) | The first acquisition, when the driver's package (`redis`, `pg`, `mysql2`) is not installed |
|
|
205
|
+
| `InvalidValueException` (from `lakutata`) | The component's options are invalid, when it is created |
|
|
206
|
+
| `TypeError` | An empty lock name; a `@Synchronized` template whose field is missing from the input |
|
|
207
|
+
|
|
208
|
+
## See also
|
|
209
|
+
|
|
210
|
+
- `doc/en/Mutex.md` of the package `lakutata` (Chinese in `doc/zh`): the guide of the locks.
|
|
211
|
+
- `@lakutata/entrypoint` (`lakutata/com/entrypoint`): the controllers, `@Schedule` and the schedule adapter.
|
|
212
|
+
- `@lakutata/core` (`lakutata`): the components, `Application` and the dependency injection.
|
|
213
|
+
- `npm run test:stores` (in this package) tests the drivers against Redis, PostgreSQL and MySQL in Docker.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export * from '../mutex/Mutex.js';
|
|
2
|
+
export * from '../mutex/MutexLock.js';
|
|
3
|
+
export * from '../mutex/drivers/MutexDriver.js';
|
|
4
|
+
export * from '../mutex/drivers/MemoryMutexDriver.js';
|
|
5
|
+
export * from '../mutex/drivers/FileMutexDriver.js';
|
|
6
|
+
export * from '../mutex/drivers/RedisMutexDriver.js';
|
|
7
|
+
export * from '../mutex/drivers/PostgresMutexDriver.js';
|
|
8
|
+
export * from '../mutex/drivers/MysqlMutexDriver.js';
|
|
9
|
+
export * from '../mutex/options/MemoryMutexOptions.js';
|
|
10
|
+
export * from '../mutex/options/FileMutexOptions.js';
|
|
11
|
+
export * from '../mutex/options/RedisMutexOptions.js';
|
|
12
|
+
export * from '../mutex/options/PostgresMutexOptions.js';
|
|
13
|
+
export * from '../mutex/options/MysqlMutexOptions.js';
|
|
14
|
+
export * from '../mutex/exceptions/MutexTimeoutException.js';
|
|
15
|
+
export * from '../mutex/exceptions/MutexLostException.js';
|
|
16
|
+
export * from '../mutex/exceptions/MutexDriverException.js';
|
|
17
|
+
export * from '../mutex/exceptions/MutexDriverNotFoundException.js';
|
|
18
|
+
export * from '../mutex/Synchronized.js';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("../mutex/Mutex.js"), exports);
|
|
18
|
+
__exportStar(require("../mutex/MutexLock.js"), exports);
|
|
19
|
+
__exportStar(require("../mutex/drivers/MutexDriver.js"), exports);
|
|
20
|
+
__exportStar(require("../mutex/drivers/MemoryMutexDriver.js"), exports);
|
|
21
|
+
__exportStar(require("../mutex/drivers/FileMutexDriver.js"), exports);
|
|
22
|
+
__exportStar(require("../mutex/drivers/RedisMutexDriver.js"), exports);
|
|
23
|
+
__exportStar(require("../mutex/drivers/PostgresMutexDriver.js"), exports);
|
|
24
|
+
__exportStar(require("../mutex/drivers/MysqlMutexDriver.js"), exports);
|
|
25
|
+
__exportStar(require("../mutex/options/MemoryMutexOptions.js"), exports);
|
|
26
|
+
__exportStar(require("../mutex/options/FileMutexOptions.js"), exports);
|
|
27
|
+
__exportStar(require("../mutex/options/RedisMutexOptions.js"), exports);
|
|
28
|
+
__exportStar(require("../mutex/options/PostgresMutexOptions.js"), exports);
|
|
29
|
+
__exportStar(require("../mutex/options/MysqlMutexOptions.js"), exports);
|
|
30
|
+
__exportStar(require("../mutex/exceptions/MutexTimeoutException.js"), exports);
|
|
31
|
+
__exportStar(require("../mutex/exceptions/MutexLostException.js"), exports);
|
|
32
|
+
__exportStar(require("../mutex/exceptions/MutexDriverException.js"), exports);
|
|
33
|
+
__exportStar(require("../mutex/exceptions/MutexDriverNotFoundException.js"), exports);
|
|
34
|
+
__exportStar(require("../mutex/Synchronized.js"), exports);
|
|
35
|
+
|
|
36
|
+
//The default of the entry: the entry itself (scripts/package-build/cjs-package.mjs)
|
|
37
|
+
if (!Object.prototype.hasOwnProperty.call(exports, 'default')) Object.defineProperty(exports, 'default', {value: exports})
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { Component } from '@lakutata/core';
|
|
2
|
+
import { MutexLock } from './MutexLock.js';
|
|
3
|
+
import { MemoryMutexOptions } from './options/MemoryMutexOptions.js';
|
|
4
|
+
import { FileMutexOptions } from './options/FileMutexOptions.js';
|
|
5
|
+
import { RedisMutexOptions } from './options/RedisMutexOptions.js';
|
|
6
|
+
import { PostgresMutexOptions } from './options/PostgresMutexOptions.js';
|
|
7
|
+
import { MysqlMutexOptions } from './options/MysqlMutexOptions.js';
|
|
8
|
+
/**
|
|
9
|
+
* The `driver` option of a {@link Mutex}: the store holding the locks, told apart by `type`. The store decides who
|
|
10
|
+
* shares the locks: `'memory'` the components of one process, `'file'` the processes of one host, `'redis'`,
|
|
11
|
+
* `'postgres'` and `'mysql'` every instance connected to the same server.
|
|
12
|
+
*/
|
|
13
|
+
export type MutexDriverOptions = MemoryMutexOptions | FileMutexOptions | RedisMutexOptions | PostgresMutexOptions | MysqlMutexOptions;
|
|
14
|
+
/**
|
|
15
|
+
* The options of {@link Mutex.acquire} and {@link Mutex.synchronized} ({@link Mutex.tryAcquire} only reads `ttl`).
|
|
16
|
+
*/
|
|
17
|
+
export type MutexAcquireOptions = {
|
|
18
|
+
/**
|
|
19
|
+
* How long the lock stays held without a renewal, in milliseconds. The component renews it every third of this time
|
|
20
|
+
* while it is held, so it only expires when its holder is gone or cannot reach the store. Ignored by the PostgreSQL
|
|
21
|
+
* and MySQL advisory locks, which end with their connection instead.
|
|
22
|
+
* @default the component's `ttl` option (30000)
|
|
23
|
+
*/
|
|
24
|
+
ttl?: number;
|
|
25
|
+
/**
|
|
26
|
+
* How long to wait for the lock, in milliseconds: `0` tries once, `Infinity` waits until it is acquired. When the
|
|
27
|
+
* time is up, the call rejects with {@link MutexTimeoutException}. An attempt sent to the store is given at least a
|
|
28
|
+
* second to answer, even past the time: a short timeout (`0` included) is not cut short by the round trip of a store
|
|
29
|
+
* (a file, a network), while a store which does not answer still ends the wait.
|
|
30
|
+
* @default the component's `timeout` option (10000)
|
|
31
|
+
*/
|
|
32
|
+
timeout?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Stops the wait when aborted: the call rejects with the signal's reason. An attempt already sent to the store is not
|
|
35
|
+
* cancelled, and a lock it acquires is returned.
|
|
36
|
+
* @default none (the wait only ends with the lock or the timeout)
|
|
37
|
+
*/
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* The lock component: named locks giving a work to one holder at a time, between the instances and the processes of an
|
|
42
|
+
* application as its driver shares them (see {@link MutexDriverOptions}). Register it in the `components` of an
|
|
43
|
+
* application (`{class: Mutex, driver: {...}}`, named `mutex` for `@Synchronized` and the `exclusive` schedules) and
|
|
44
|
+
* get it with `getObject()` or `@Inject`; it is a singleton. Without a `driver`, the locks are in memory.
|
|
45
|
+
*
|
|
46
|
+
* A lock is acquired with a random token: only its holder ({@link MutexLock}) releases or renews it. The expiring
|
|
47
|
+
* drivers (memory, file, Redis, the `table` mode of PostgreSQL and MySQL) hold it for its `ttl` and the component renews
|
|
48
|
+
* it every third of that time, so a crashed holder keeps it at most one `ttl`. The advisory locks of PostgreSQL and
|
|
49
|
+
* MySQL do not expire: the database releases them when the component's connection closes.
|
|
50
|
+
*
|
|
51
|
+
* A lock is lost when a renewal finds it expired or taken, when a renewal fails (the store unreachable), or when the
|
|
52
|
+
* connection holding an advisory lock closes: its {@link MutexLock.signal} is then aborted with a
|
|
53
|
+
* {@link MutexLostException}, and the work it protects should stop. The locks are not reentrant: acquiring a lock this
|
|
54
|
+
* component already holds waits for its release like any other holder.
|
|
55
|
+
*
|
|
56
|
+
* The driver connects at its first use, not when the component is created: a store which cannot be reached, or whose
|
|
57
|
+
* client package is missing, fails the first acquisition. The component releases the locks it holds and closes the
|
|
58
|
+
* driver when it is destroyed (the application stopping).
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* import {Application} from 'lakutata'
|
|
62
|
+
* import {Mutex, type MutexLock} from 'lakutata/com/mutex'
|
|
63
|
+
*
|
|
64
|
+
* Application
|
|
65
|
+
* .run(() => ({
|
|
66
|
+
* id: 'mutex.app',
|
|
67
|
+
* name: 'Mutex',
|
|
68
|
+
* components: {
|
|
69
|
+
* //The locks shared by every instance connected to this Redis server
|
|
70
|
+
* mutex: {class: Mutex, driver: {type: 'redis', url: 'redis://127.0.0.1:6379'}, ttl: 30000, timeout: 10000}
|
|
71
|
+
* }
|
|
72
|
+
* }))
|
|
73
|
+
* .onLaunched(async (app: Application): Promise<void> => {
|
|
74
|
+
* const mutex: Mutex = await app.getObject('mutex')
|
|
75
|
+
* //Acquired before the function, released after it, even when it throws
|
|
76
|
+
* const total: number = await mutex.synchronized('report:daily', async (lock: MutexLock): Promise<number> => {
|
|
77
|
+
* lock.signal.throwIfAborted()
|
|
78
|
+
* return 42
|
|
79
|
+
* })
|
|
80
|
+
* console.log(total)
|
|
81
|
+
* })
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export declare class Mutex extends Component {
|
|
85
|
+
#private;
|
|
86
|
+
/**
|
|
87
|
+
* The store holding the locks, told apart by `type`: `'memory'` (the components of one process), `'file'` (the
|
|
88
|
+
* processes of one host), `'redis'`, `'postgres'` or `'mysql'` (every instance connected to the server).
|
|
89
|
+
* @default {type: 'memory'}
|
|
90
|
+
*/
|
|
91
|
+
protected readonly driver: MutexDriverOptions;
|
|
92
|
+
/**
|
|
93
|
+
* The prefix added to the lock names in the store, which keeps apart the applications sharing a store. Two
|
|
94
|
+
* components with the same driver and prefix share their locks.
|
|
95
|
+
* @default 'lakutata:mutex:'
|
|
96
|
+
*/
|
|
97
|
+
protected readonly prefix: string;
|
|
98
|
+
/**
|
|
99
|
+
* How long a lock stays held without a renewal, in milliseconds (a positive integer); the default of
|
|
100
|
+
* {@link MutexAcquireOptions.ttl}.
|
|
101
|
+
* @default 30000
|
|
102
|
+
*/
|
|
103
|
+
protected readonly ttl: number;
|
|
104
|
+
/**
|
|
105
|
+
* How long {@link Mutex.acquire} waits for a lock, in milliseconds (`0` tries once, `Infinity` waits without limit);
|
|
106
|
+
* the default of {@link MutexAcquireOptions.timeout}.
|
|
107
|
+
* @default 10000
|
|
108
|
+
*/
|
|
109
|
+
protected readonly timeout: number;
|
|
110
|
+
/**
|
|
111
|
+
* The delay before the second attempt to acquire a lock, in milliseconds. It doubles after each attempt up to one
|
|
112
|
+
* second, each delay randomized between its half and its whole to spread the waiters.
|
|
113
|
+
* @default 50
|
|
114
|
+
*/
|
|
115
|
+
protected readonly retryDelay: number;
|
|
116
|
+
/**
|
|
117
|
+
* Create the driver chosen by the `driver` option; it connects at its first use.
|
|
118
|
+
* @throws {Error} When the `table` option of a PostgreSQL or MySQL driver is not a valid table name.
|
|
119
|
+
* @protected
|
|
120
|
+
*/
|
|
121
|
+
protected init(): Promise<void>;
|
|
122
|
+
/**
|
|
123
|
+
* Release the locks this component holds (their failures ignored), then close the driver's connections.
|
|
124
|
+
* @protected
|
|
125
|
+
*/
|
|
126
|
+
protected destroy(): Promise<void>;
|
|
127
|
+
/**
|
|
128
|
+
* Mark a lock as lost: stop its renewal, forget it, and abort its signal with a {@link MutexLostException}. The lock
|
|
129
|
+
* is not released in the store: it stays there until it expires (or its connection closes).
|
|
130
|
+
* @param lock The lock lost.
|
|
131
|
+
* @param message Why it is lost, added to the exception's message.
|
|
132
|
+
* @protected
|
|
133
|
+
*/
|
|
134
|
+
protected lost(lock: MutexLock, message: string): void;
|
|
135
|
+
/**
|
|
136
|
+
* Call the driver, turning its errors into a {@link MutexDriverException} (a {@link MutexDriverNotFoundException} is
|
|
137
|
+
* rethrown as it is).
|
|
138
|
+
* @param call The driver call.
|
|
139
|
+
* @returns What the driver call returns.
|
|
140
|
+
* @throws {MutexDriverException} When the driver call fails.
|
|
141
|
+
* @throws {MutexDriverNotFoundException} When the driver's client package is not installed.
|
|
142
|
+
* @protected
|
|
143
|
+
*/
|
|
144
|
+
protected call<T>(call: () => Promise<T>): Promise<T>;
|
|
145
|
+
/**
|
|
146
|
+
* Try to acquire a lock once, without waiting. The lock is renewed automatically while it is held (expiring
|
|
147
|
+
* drivers), until it is released or lost.
|
|
148
|
+
* @param name The lock name, a non-empty string; the component's `prefix` is added to it in the store.
|
|
149
|
+
* @param options `ttl`: how long the lock stays held without a renewal, in milliseconds (the component's `ttl` by
|
|
150
|
+
* default).
|
|
151
|
+
* @returns The lock, or `undefined` when another holder has it (this component included: the locks are not
|
|
152
|
+
* reentrant).
|
|
153
|
+
* @throws {TypeError} When the name is not a non-empty string.
|
|
154
|
+
* @throws {MutexDriverException} When the store fails (unreachable, a failed query).
|
|
155
|
+
* @throws {MutexDriverNotFoundException} When the driver's client package (`redis`, `pg`, `mysql2`) is not installed.
|
|
156
|
+
* @example
|
|
157
|
+
* ```typescript
|
|
158
|
+
* import type {Mutex, MutexLock} from 'lakutata/com/mutex'
|
|
159
|
+
*
|
|
160
|
+
* declare const mutex: Mutex
|
|
161
|
+
*
|
|
162
|
+
* //A cleanup run by whichever instance gets there first, skipped by the others
|
|
163
|
+
* const lock: MutexLock | undefined = await mutex.tryAcquire('cleanup')
|
|
164
|
+
* if (lock) {
|
|
165
|
+
* try {
|
|
166
|
+
* console.log('cleaning up')
|
|
167
|
+
* } finally {
|
|
168
|
+
* await lock.release()
|
|
169
|
+
* }
|
|
170
|
+
* }
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
tryAcquire(name: string, options?: Pick<MutexAcquireOptions, 'ttl'>): Promise<MutexLock | undefined>;
|
|
174
|
+
/**
|
|
175
|
+
* Plan the next renewal of a lock of an expiring driver: a third of its time to live (renewed twice before it
|
|
176
|
+
* expires). A renewal failing (the store unreachable) is tried again until the lock expires, then the lock is lost.
|
|
177
|
+
* @param lock The lock held by the component; a lock released or lost is not renewed.
|
|
178
|
+
* @protected
|
|
179
|
+
*/
|
|
180
|
+
protected renew(lock: MutexLock): void;
|
|
181
|
+
/**
|
|
182
|
+
* An attempt of {@link Mutex.acquire}, bounded by its deadline but given at least a second to answer (the round trip
|
|
183
|
+
* of a store is not cut short, a timeout of 0 tries once): an attempt hanging (the store unreachable) fails then, the
|
|
184
|
+
* lock it acquires later released.
|
|
185
|
+
* @param name The lock name.
|
|
186
|
+
* @param options The options of the acquisition (its `ttl`).
|
|
187
|
+
* @param deadline The time the wait ends, in milliseconds since the epoch.
|
|
188
|
+
* @param timeout The timeout of the wait, in milliseconds, for the message of its exception.
|
|
189
|
+
* @returns The lock acquired, or `undefined` when another holder has it.
|
|
190
|
+
* @throws {MutexTimeoutException} When the attempt does not answer before the deadline and its second of grace.
|
|
191
|
+
* @protected
|
|
192
|
+
*/
|
|
193
|
+
protected attempt(name: string, options: MutexAcquireOptions, deadline: number, timeout: number): Promise<MutexLock | undefined>;
|
|
194
|
+
/**
|
|
195
|
+
* Acquire a lock, waiting up to the timeout for its holder to release it (or for it to expire). The store is polled:
|
|
196
|
+
* the first retry comes after `retryDelay` milliseconds, the delay doubling up to one second. The lock is renewed
|
|
197
|
+
* automatically while it is held, until it is released or lost; release it with {@link MutexLock.release}, or
|
|
198
|
+
* declare it with `await using`.
|
|
199
|
+
* @param name The lock name, a non-empty string; the component's `prefix` is added to it in the store.
|
|
200
|
+
* @param options The lock's `ttl`, the `timeout` of the wait and an abort `signal` (the component's options by
|
|
201
|
+
* default).
|
|
202
|
+
* @returns The lock acquired.
|
|
203
|
+
* @throws {MutexTimeoutException} When the lock is not acquired before the timeout (a `timeout` of `0` after one
|
|
204
|
+
* attempt).
|
|
205
|
+
* @throws {TypeError} When the name is not a non-empty string.
|
|
206
|
+
* @throws {MutexDriverException} When the store fails; the wait stops at the first failure.
|
|
207
|
+
* @throws {MutexDriverNotFoundException} When the driver's client package is not installed.
|
|
208
|
+
* @throws The signal's reason (an `AbortError` `DOMException` by default) when the signal is aborted before the lock
|
|
209
|
+
* is acquired.
|
|
210
|
+
* @example
|
|
211
|
+
* ```typescript
|
|
212
|
+
* import type {Mutex, MutexLock} from 'lakutata/com/mutex'
|
|
213
|
+
*
|
|
214
|
+
* declare const mutex: Mutex
|
|
215
|
+
*
|
|
216
|
+
* //Waits up to 3 seconds, then rejects with MutexTimeoutException
|
|
217
|
+
* const lock: MutexLock = await mutex.acquire('order:42', {timeout: 3000})
|
|
218
|
+
* try {
|
|
219
|
+
* console.log(`order 42 is ours, fencing token ${lock.fence}`)
|
|
220
|
+
* } finally {
|
|
221
|
+
* await lock.release()
|
|
222
|
+
* }
|
|
223
|
+
*
|
|
224
|
+
* //Released at the end of the block, even when it throws
|
|
225
|
+
* {
|
|
226
|
+
* await using scoped: MutexLock = await mutex.acquire('order:43', {signal: AbortSignal.timeout(5000)})
|
|
227
|
+
* console.log(scoped.name)
|
|
228
|
+
* }
|
|
229
|
+
* ```
|
|
230
|
+
*/
|
|
231
|
+
acquire(name: string, options?: MutexAcquireOptions): Promise<MutexLock>;
|
|
232
|
+
/**
|
|
233
|
+
* Run a function holding a lock: acquire it as {@link Mutex.acquire} does, run the function, then release the lock
|
|
234
|
+
* whether the function returned or threw (a failure of the release is ignored). When the lock is lost while the
|
|
235
|
+
* function runs, the function is not interrupted: its `lock.signal` is aborted, and it should check it.
|
|
236
|
+
* @param name The lock name, a non-empty string; the component's `prefix` is added to it in the store.
|
|
237
|
+
* @param fn The function to run, given the lock.
|
|
238
|
+
* @param options The lock's `ttl`, the `timeout` of the wait and an abort `signal` (the component's options by
|
|
239
|
+
* default).
|
|
240
|
+
* @returns What the function returns (awaited).
|
|
241
|
+
* @throws {MutexTimeoutException} When the lock is not acquired before the timeout; the function does not run.
|
|
242
|
+
* @throws {MutexDriverException} When the store fails while acquiring the lock.
|
|
243
|
+
* @throws {MutexDriverNotFoundException} When the driver's client package is not installed.
|
|
244
|
+
* @throws The function's error, after the lock is released.
|
|
245
|
+
* @example
|
|
246
|
+
* ```typescript
|
|
247
|
+
* import type {Mutex, MutexLock} from 'lakutata/com/mutex'
|
|
248
|
+
*
|
|
249
|
+
* declare const mutex: Mutex
|
|
250
|
+
* declare function chargeOrder(id: number, signal: AbortSignal): Promise<string>
|
|
251
|
+
*
|
|
252
|
+
* //One payment of order 42 at a time, in every instance sharing the store
|
|
253
|
+
* const receipt: string = await mutex.synchronized('order:42', (lock: MutexLock): Promise<string> => {
|
|
254
|
+
* //Aborted with a MutexLostException when the lock is lost
|
|
255
|
+
* return chargeOrder(42, lock.signal)
|
|
256
|
+
* }, {timeout: 5000})
|
|
257
|
+
* console.log(receipt)
|
|
258
|
+
* ```
|
|
259
|
+
*/
|
|
260
|
+
synchronized<T>(name: string, fn: (lock: MutexLock) => T | Promise<T>, options?: MutexAcquireOptions): Promise<T>;
|
|
261
|
+
/**
|
|
262
|
+
* The locks this component holds now (not released, not lost), in their acquisition order; a new array on each read.
|
|
263
|
+
*/
|
|
264
|
+
get heldLocks(): MutexLock[];
|
|
265
|
+
}
|