@lakutata/mutex 3.0.0 → 3.1.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 +61 -53
- package/README.zh-CN.md +192 -0
- package/package.json +25 -3
package/README.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# @lakutata/mutex
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
(
|
|
7
|
-
|
|
8
|
-
`
|
|
3
|
+
English | [中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
The lock component of the lakutata framework: named locks that let one holder at a time do a piece of work. Depending
|
|
6
|
+
on the driver (memory, lock files, Redis, PostgreSQL or MySQL), a lock is shared by the components of a process, by the
|
|
7
|
+
processes of a host, or by every instance of an application. A lock is held with a token and renewed while it is held;
|
|
8
|
+
when its holder can no longer keep it, the lock is lost and its `AbortSignal` is aborted. The package also provides
|
|
9
|
+
`@Synchronized` for controller actions, and the locks of `exclusive` schedules. The umbrella package `lakutata`
|
|
10
|
+
re-exports it as `lakutata/com/mutex`.
|
|
9
11
|
|
|
10
12
|
## Installation
|
|
11
13
|
|
|
@@ -13,25 +15,27 @@ actions and the locks of the `exclusive` schedules. The umbrella package `lakuta
|
|
|
13
15
|
npm install lakutata
|
|
14
16
|
```
|
|
15
17
|
|
|
16
|
-
The memory and file drivers need nothing
|
|
17
|
-
|
|
18
|
+
The memory and file drivers need nothing else. The other drivers rely on optional peer dependencies: install the
|
|
19
|
+
package of the driver you use. When a driver's package is missing, the first acquisition fails with
|
|
20
|
+
`MutexDriverNotFoundException`.
|
|
18
21
|
|
|
19
22
|
| Driver | `type` | Shared by | Install | Expires / renewed | Fencing token |
|
|
20
23
|
|---|---|---|---|---|---|
|
|
21
24
|
| 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;
|
|
23
|
-
| Redis 4+ (or Valkey) | `'redis'` | every instance
|
|
24
|
-
| PostgreSQL, `mode: 'advisory'` (default) | `'postgres'` | every instance
|
|
25
|
-
| PostgreSQL, `mode: 'table'` | `'postgres'` | every instance
|
|
26
|
-
| MySQL / MariaDB, `mode: 'advisory'` (default) | `'mysql'` | every instance
|
|
27
|
-
| MySQL / MariaDB, `mode: 'table'` | `'mysql'` | every instance
|
|
25
|
+
| Lock files | `'file'` | the processes of one host | none | yes; the lock of a process that died is taken over at once | no |
|
|
26
|
+
| Redis 4+ (or Valkey) | `'redis'` | every instance connected to the server or cluster | `redis` (4.6 or 5) | yes | yes |
|
|
27
|
+
| PostgreSQL, `mode: 'advisory'` (default) | `'postgres'` | every instance connected to the database | `pg` (8) | no: released when the connection closes | no |
|
|
28
|
+
| PostgreSQL, `mode: 'table'` | `'postgres'` | every instance connected to the database, also through connection poolers | `pg` (8) | yes | yes |
|
|
29
|
+
| MySQL / MariaDB, `mode: 'advisory'` (default) | `'mysql'` | every instance connected to the server (not across a cluster) | `mysql2` (3) | no: released when the connection closes | no |
|
|
30
|
+
| MySQL / MariaDB, `mode: 'table'` | `'mysql'` | every instance connected to the database, clusters included | `mysql2` (3) | yes | yes |
|
|
28
31
|
|
|
29
32
|
## Usage
|
|
30
33
|
|
|
31
34
|
### The component in an application
|
|
32
35
|
|
|
33
|
-
Register `Mutex` in the `components`
|
|
34
|
-
schedules use by default), then get it with `getObject()` or `@Inject`. Without `driver`, the locks are
|
|
36
|
+
Register `Mutex` in the application's `components` under the name `mutex` (the name that `@Synchronized` and
|
|
37
|
+
`exclusive` schedules use by default), then get it with `getObject()` or `@Inject`. Without `driver`, the locks are kept
|
|
38
|
+
in memory.
|
|
35
39
|
|
|
36
40
|
```typescript
|
|
37
41
|
import {Application} from 'lakutata'
|
|
@@ -82,29 +86,30 @@ Application
|
|
|
82
86
|
})
|
|
83
87
|
```
|
|
84
88
|
|
|
85
|
-
- **Holders**: a lock is acquired with a random token
|
|
86
|
-
|
|
87
|
-
- **Expiration**:
|
|
88
|
-
third of that time, so a crashed
|
|
89
|
-
expire: the database releases them when the component's connection closes.
|
|
90
|
-
- **Lost locks**: when a renewal finds
|
|
91
|
-
until
|
|
92
|
-
`MutexLostException`. The work
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
- **
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
89
|
+
- **Holders**: a lock is acquired with a random token, and only its holder can release or renew it. A lock is released
|
|
90
|
+
only once: later `release()` calls return `false`.
|
|
91
|
+
- **Expiration**: drivers whose locks expire hold a lock for its `ttl` (in milliseconds), and the component renews it
|
|
92
|
+
every third of that time, so a holder that crashed keeps the lock for one `ttl` at most. The advisory locks of
|
|
93
|
+
PostgreSQL and MySQL do not expire: the database releases them when the component's connection closes.
|
|
94
|
+
- **Lost locks**: a lock is lost when a renewal finds it expired or taken by another holder, when renewals keep failing
|
|
95
|
+
(the store is unreachable) until it expires, or when the connection holding an advisory lock closes. `lock.held` then
|
|
96
|
+
becomes `false` and `lock.signal` is aborted with a `MutexLostException`. The protected work is not interrupted, so it
|
|
97
|
+
should watch the signal.
|
|
98
|
+
- **Fencing tokens**: `lock.fence` increases with each acquisition (memory, Redis and the `table` modes). Store it with
|
|
99
|
+
the data the lock protects, so that your data store can reject writes from an older holder.
|
|
100
|
+
- **Not reentrant**: acquiring a lock that the component already holds waits for its release, like any other holder.
|
|
101
|
+
- **Connections**: the driver connects at the first acquisition, not when the component is created. When the lock store
|
|
102
|
+
fails, calls reject with `MutexDriverException`. When the Redis server is unreachable, calls fail: the first
|
|
103
|
+
connection attempt gives up after 5 seconds, and calls made while the client reconnects fail at once.
|
|
104
|
+
- **Lifecycle**: when the component is destroyed (as the application stops), it releases the locks it holds and closes
|
|
105
|
+
its driver.
|
|
101
106
|
|
|
102
107
|
### Synchronized controller actions
|
|
103
108
|
|
|
104
|
-
`@Synchronized` runs a controller action
|
|
105
|
-
passed, released
|
|
106
|
-
transport;
|
|
107
|
-
|
|
109
|
+
`@Synchronized` runs a controller action under a lock. The lock is acquired after the input is validated and access
|
|
110
|
+
control has passed, and released when the action ends. Requests that need the same lock run one at a time across every
|
|
111
|
+
instance, whatever their transport; other requests run in parallel. A request stops waiting when it is aborted (its
|
|
112
|
+
client has gone), and a request that does not get the lock in time fails with `MutexTimeoutException` (HTTP 409).
|
|
108
113
|
|
|
109
114
|
```typescript
|
|
110
115
|
import {Controller} from 'lakutata/com/entrypoint'
|
|
@@ -135,12 +140,13 @@ class OrderController extends Controller {
|
|
|
135
140
|
|
|
136
141
|
### Exclusive schedules
|
|
137
142
|
|
|
138
|
-
A scheduled action declared `exclusive` (`@Schedule`
|
|
139
|
-
application at a time
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
the
|
|
143
|
+
A scheduled action declared `exclusive` (with `@Schedule` from `lakutata/decorator/ctrl`) runs in only one instance of
|
|
144
|
+
the application at a time. Each run tries to acquire the lock `schedule:<key>` (the key defaults to the action name)
|
|
145
|
+
from the `mutex` component (or from the component and key given by `exclusive: {mutex, key}`), and the instances that
|
|
146
|
+
do not get it skip the run. Interval schedules are aligned on multiples of their interval (every 5 minutes means :00,
|
|
147
|
+
:05, :10…), so that every instance plans the same times. After a run, the lock is held until half the period has passed
|
|
148
|
+
since the planned time (30 s at most), so that instances whose clocks are slightly behind skip the run too. When its
|
|
149
|
+
lock component cannot be found, the action does not run, and the error is logged.
|
|
144
150
|
|
|
145
151
|
```typescript
|
|
146
152
|
import {Application} from 'lakutata'
|
|
@@ -180,34 +186,36 @@ Application.run(() => ({
|
|
|
180
186
|
| Export | What it is |
|
|
181
187
|
|---|---|
|
|
182
188
|
| `Mutex` | The component: `tryAcquire`, `acquire`, `synchronized`, `heldLocks`; the options `driver`, `prefix`, `ttl`, `timeout`, `retryDelay` |
|
|
183
|
-
| `MutexLock` | A lock
|
|
189
|
+
| `MutexLock` | A held lock: `name`, `token`, `fence`, `acquiredAt`, `held`, `signal`, `release()`, `extend()`, `await using` |
|
|
184
190
|
| `MutexAcquireOptions` | The `ttl`, `timeout` and abort `signal` of an acquisition |
|
|
185
|
-
| `MutexDriverOptions` | The `driver` option: one of the driver options below,
|
|
191
|
+
| `MutexDriverOptions` | The `driver` option: one of the driver options below, distinguished by `type` |
|
|
186
192
|
| `MemoryMutexOptions`, `FileMutexOptions`, `RedisMutexOptions`, `PostgresMutexOptions`, `MysqlMutexOptions` | The options of each driver |
|
|
187
|
-
| `Synchronized` | Method decorator
|
|
193
|
+
| `Synchronized` | Method decorator that runs a controller action under a lock |
|
|
188
194
|
| `SynchronizedOptions` | The lock component, `timeout`, `wait` and `ttl` of `@Synchronized` |
|
|
189
195
|
| `lockNameOf` | Builds a lock name from a template (`'order:{id}'`) and an input |
|
|
190
196
|
| `MutexDriver`, `DriverLock` | The base class of the drivers, and a lock granted by a driver |
|
|
191
|
-
| `MemoryMutexDriver`, `FileMutexDriver`, `RedisMutexDriver`, `PostgresMutexDriver`, `MysqlMutexDriver` | The drivers,
|
|
192
|
-
| `RedisMutexConnection`, `PostgresMutexConnection`, `MysqlMutexConnection` | The connections
|
|
197
|
+
| `MemoryMutexDriver`, `FileMutexDriver`, `RedisMutexDriver`, `PostgresMutexDriver`, `MysqlMutexDriver` | The drivers, which can be used directly without the component (no waiting, no renewal) |
|
|
198
|
+
| `RedisMutexConnection`, `PostgresMutexConnection`, `MysqlMutexConnection` | The connections passed to the drivers' constructors |
|
|
193
199
|
| `MutexTimeoutException`, `MutexLostException`, `MutexDriverException`, `MutexDriverNotFoundException` | The exceptions |
|
|
194
200
|
|
|
195
|
-
All are exported by `lakutata/com/mutex` and `@lakutata/mutex`. The typings document
|
|
201
|
+
All of them are exported by `lakutata/com/mutex` and `@lakutata/mutex`. The typings document every declaration, with
|
|
202
|
+
examples.
|
|
196
203
|
|
|
197
204
|
## Errors
|
|
198
205
|
|
|
199
206
|
| Exception | When |
|
|
200
207
|
|---|---|
|
|
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
|
|
208
|
+
| `MutexTimeoutException` (`E_MUTEX_TIMEOUT`, HTTP 409) | `acquire()` or `synchronized()` did not get the lock before the timeout; a `@Synchronized` action whose lock is held by another request |
|
|
202
209
|
| `MutexLostException` (`E_MUTEX_LOST`) | Not thrown: the abort reason of `lock.signal` when the lock is lost |
|
|
203
210
|
| `MutexDriverException` (`E_MUTEX_DRIVER`) | The lock store failed (unreachable, a failed query, a file error) during a call |
|
|
204
211
|
| `MutexDriverNotFoundException` (`E_MUTEX_DRIVER_NOT_FOUND`) | The first acquisition, when the driver's package (`redis`, `pg`, `mysql2`) is not installed |
|
|
205
|
-
| `
|
|
212
|
+
| `InvalidObjectOptionsException` (from `lakutata`) | An option of the component has an invalid value; the application fails to start |
|
|
206
213
|
| `TypeError` | An empty lock name; a `@Synchronized` template whose field is missing from the input |
|
|
207
214
|
|
|
208
215
|
## See also
|
|
209
216
|
|
|
210
|
-
-
|
|
211
|
-
-
|
|
212
|
-
- `@lakutata/
|
|
217
|
+
- The guide on the website: [Mutex](https://lakutata.com/guide/Mutex).
|
|
218
|
+
- `doc/en/Mutex.md` in the `lakutata` package (Chinese version in `doc/zh`): the guide to locks.
|
|
219
|
+
- `@lakutata/entrypoint` (`lakutata/com/entrypoint`): controllers, `@Schedule` and the schedule adapter.
|
|
220
|
+
- `@lakutata/core` (`lakutata`): components, `Application` and dependency injection.
|
|
213
221
|
- `npm run test:stores` (in this package) tests the drivers against Redis, PostgreSQL and MySQL in Docker.
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# @lakutata/mutex
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
lakutata 框架的锁组件:具名的锁,同一时间只让一个持有者执行某项工作。按所用的驱动(内存、锁文件、Redis、PostgreSQL 或 MySQL),锁可以由一个进程内的各组件共享,由一台机器上的各进程共享,或者由应用的所有实例共享。持有锁要凭一个令牌,持有期间锁会不断续期;持有者无法再保住锁时,锁就丢失了,锁的 `AbortSignal` 会被中止。这个包还为控制器动作提供 `@Synchronized`,并为 `exclusive` 定时任务提供锁。`lakutata` 包把它重新导出为 `lakutata/com/mutex`。
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
```shell
|
|
10
|
+
npm install lakutata
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
内存驱动和文件驱动不需要再安装其他包。其他驱动所需的包都是可选的 peer 依赖:用哪个驱动,就安装哪个驱动的包。缺少驱动的包时,第一次获取锁会失败,抛出 `MutexDriverNotFoundException`。
|
|
14
|
+
|
|
15
|
+
| 驱动 | `type` | 共享范围 | 需要安装 | 过期 / 续期 | 防护令牌 |
|
|
16
|
+
|---|---|---|---|---|---|
|
|
17
|
+
| 内存(默认) | `'memory'` | 一个进程内的各组件 | 无 | 是 | 是(每个进程单独计数) |
|
|
18
|
+
| 锁文件 | `'file'` | 一台机器上的各进程 | 无 | 是;已退出的进程持有的锁会被立即接管 | 否 |
|
|
19
|
+
| Redis 4+(或 Valkey) | `'redis'` | 连接到同一服务器或集群的所有实例 | `redis`(4.6 或 5) | 是 | 是 |
|
|
20
|
+
| PostgreSQL,`mode: 'advisory'`(默认) | `'postgres'` | 连接到同一数据库的所有实例 | `pg`(8) | 否:连接关闭时释放 | 否 |
|
|
21
|
+
| PostgreSQL,`mode: 'table'` | `'postgres'` | 连接到同一数据库的所有实例,通过连接池代理连接的也可以 | `pg`(8) | 是 | 是 |
|
|
22
|
+
| MySQL / MariaDB,`mode: 'advisory'`(默认) | `'mysql'` | 连接到同一服务器的所有实例(集群的各节点之间不共享) | `mysql2`(3) | 否:连接关闭时释放 | 否 |
|
|
23
|
+
| MySQL / MariaDB,`mode: 'table'` | `'mysql'` | 连接到同一数据库的所有实例,包括集群 | `mysql2`(3) | 是 | 是 |
|
|
24
|
+
|
|
25
|
+
## 用法
|
|
26
|
+
|
|
27
|
+
### 在应用中使用组件
|
|
28
|
+
|
|
29
|
+
在应用的 `components` 中以 `mutex` 为名注册 `Mutex`(`@Synchronized` 和 `exclusive` 定时任务默认使用这个名称),然后用 `getObject()` 或 `@Inject` 获取它。不指定 `driver` 时,锁保存在内存中。
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import {Application} from 'lakutata'
|
|
33
|
+
import {Mutex, type MutexLock} from 'lakutata/com/mutex'
|
|
34
|
+
|
|
35
|
+
Application
|
|
36
|
+
.run(() => ({
|
|
37
|
+
id: 'mutex.app',
|
|
38
|
+
name: 'Mutex',
|
|
39
|
+
components: {
|
|
40
|
+
mutex: {
|
|
41
|
+
class: Mutex,
|
|
42
|
+
driver: {type: 'redis', url: 'redis://127.0.0.1:6379'},
|
|
43
|
+
//不续期时持有 30 秒,持有期间每 10 秒续期一次
|
|
44
|
+
ttl: 30000,
|
|
45
|
+
//最多等待 10 秒
|
|
46
|
+
timeout: 10000,
|
|
47
|
+
prefix: 'lakutata:mutex:'
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}))
|
|
51
|
+
.onLaunched(async (app: Application): Promise<void> => {
|
|
52
|
+
const mutex: Mutex = await app.getObject('mutex')
|
|
53
|
+
|
|
54
|
+
//执行函数前获取,执行后释放,函数抛出异常时也会释放
|
|
55
|
+
await mutex.synchronized('report:daily', async (lock: MutexLock): Promise<void> => {
|
|
56
|
+
//锁丢失时以 MutexLostException 中止
|
|
57
|
+
lock.signal.throwIfAborted()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
//最多等待 3 秒,超时抛出 MutexTimeoutException
|
|
61
|
+
const lock: MutexLock = await mutex.acquire('order:42', {timeout: 3000})
|
|
62
|
+
try {
|
|
63
|
+
console.log(lock.fence)
|
|
64
|
+
} finally {
|
|
65
|
+
await lock.release()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
//只尝试一次:被其他持有者占用时返回 undefined
|
|
69
|
+
const cleanup: MutexLock | undefined = await mutex.tryAcquire('cleanup')
|
|
70
|
+
await cleanup?.release()
|
|
71
|
+
|
|
72
|
+
//在代码块结束时释放
|
|
73
|
+
{
|
|
74
|
+
await using scoped: MutexLock = await mutex.acquire('order:43')
|
|
75
|
+
console.log(scoped.held)
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
- **持有者**:获取锁时使用一个随机令牌,只有持有者能释放或续期它。一把锁只会释放一次:之后再调用 `release()` 会返回 `false`。
|
|
81
|
+
- **过期**:锁会过期的驱动按 `ttl`(毫秒)持有锁,组件每过三分之一的时间续期一次,所以崩溃的持有者最多再占用一个 `ttl`。PostgreSQL 和 MySQL 的咨询锁不会过期:组件的连接关闭时,数据库会释放它们。
|
|
82
|
+
- **锁丢失**:以下情况锁会丢失:续期时发现锁已过期或已被其他持有者拿走;续期一直失败(存储连不上),直到锁过期;持有咨询锁的连接关闭。此时 `lock.held` 变为 `false`,`lock.signal` 以 `MutexLostException` 中止。受保护的工作不会被打断,所以它应当关注这个信号。
|
|
83
|
+
- **防护令牌**:`lock.fence` 随每次获取递增(内存、Redis 和 `table` 模式)。把它和锁保护的数据存在一起,数据存储就能拒绝旧持有者的写入。
|
|
84
|
+
- **不可重入**:获取组件已持有的锁时,和其他持有者一样要等待它被释放。
|
|
85
|
+
- **连接**:驱动在第一次获取锁时才连接,而不是在组件创建时。锁的存储出错时,调用以 `MutexDriverException` 失败。Redis 服务器连不上时,调用会失败:第一次连接在 5 秒后放弃,客户端重连期间的调用会立即失败。
|
|
86
|
+
- **生命周期**:组件被销毁时(应用停止时),会释放它持有的锁并关闭驱动。
|
|
87
|
+
|
|
88
|
+
### 同步的控制器动作
|
|
89
|
+
|
|
90
|
+
`@Synchronized` 让控制器动作在锁的保护下执行。锁在输入校验和访问控制通过之后获取,动作结束后释放。需要同一把锁的请求,无论来自哪个实例、通过哪种传输方式,都逐个执行;其他请求并行执行。请求被中止时(客户端已离开)不再等待;没有及时获得锁的请求以 `MutexTimeoutException`(HTTP 409)失败。
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import {Controller} from 'lakutata/com/entrypoint'
|
|
94
|
+
import {HTTPAction, ServiceAction} from 'lakutata/decorator/ctrl'
|
|
95
|
+
import {Synchronized} from 'lakutata/com/mutex'
|
|
96
|
+
|
|
97
|
+
class OrderController extends Controller {
|
|
98
|
+
//每个订单一把锁:{id} 是校验后输入中的字段
|
|
99
|
+
@HTTPAction('/orders/:id/pay', 'POST')
|
|
100
|
+
@ServiceAction({cmd: 'order.pay'})
|
|
101
|
+
@Synchronized('order:{id}')
|
|
102
|
+
public async pay(input: {id: number}): Promise<void> {
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
//其他请求正在执行时立即失败(HTTP 409)
|
|
106
|
+
@ServiceAction({cmd: 'search.reindex'})
|
|
107
|
+
@Synchronized('search:reindex', {wait: false})
|
|
108
|
+
public async reindex(): Promise<void> {
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
//根据输入计算锁名,使用注册为 "locks" 的锁组件,最多等待 5 秒
|
|
112
|
+
@ServiceAction({cmd: 'user.export'})
|
|
113
|
+
@Synchronized((input: {userId: string}): string => `export:${input.userId}`, {timeout: 5000, mutex: 'locks'})
|
|
114
|
+
public async export(input: {userId: string}): Promise<void> {
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 独占的定时任务
|
|
120
|
+
|
|
121
|
+
声明为 `exclusive` 的定时动作(使用 `lakutata/decorator/ctrl` 的 `@Schedule`),同一时间只在应用的一个实例中执行。每次执行都尝试从 `mutex` 组件(或者 `exclusive: {mutex, key}` 指定的组件和键)获取锁 `schedule:<key>`(键默认为动作的名字),没有获得锁的实例跳过这次执行。按间隔执行的任务对齐到间隔的整数倍(每 5 分钟就是 :00、:05、:10……),这样所有实例安排的时间都相同。执行结束后,锁会一直保留到计划时间之后半个周期(最多 30 秒),这样时钟稍慢的实例也会跳过这次执行。找不到锁组件时,这个动作不会执行,错误会写入日志。
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
import {Application} from 'lakutata'
|
|
125
|
+
import {buildEntrypoints, Controller, scheduleEntrypoint} from 'lakutata/com/entrypoint'
|
|
126
|
+
import {Schedule} from 'lakutata/decorator/ctrl'
|
|
127
|
+
import {Mutex} from 'lakutata/com/mutex'
|
|
128
|
+
|
|
129
|
+
class ReportController extends Controller {
|
|
130
|
+
//每 5 分钟由一个实例执行
|
|
131
|
+
@Schedule({every: '5m', exclusive: true})
|
|
132
|
+
public async build(): Promise<void> {
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
//每天 03:00,使用组件 "locks" 中名为 schedule:purge 的锁
|
|
136
|
+
@Schedule({cron: '0 3 * * *', exclusive: {mutex: 'locks', key: 'purge'}})
|
|
137
|
+
public async purge(): Promise<void> {
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
Application.run(() => ({
|
|
142
|
+
id: 'reports.app',
|
|
143
|
+
name: 'Reports',
|
|
144
|
+
components: {
|
|
145
|
+
mutex: {class: Mutex, driver: {type: 'redis', url: 'redis://127.0.0.1:6379'}},
|
|
146
|
+
locks: {class: Mutex, driver: {type: 'postgres', host: 'db', database: 'app', username: 'app', password: ''}},
|
|
147
|
+
entrypoint: buildEntrypoints({
|
|
148
|
+
controllers: [ReportController],
|
|
149
|
+
schedule: scheduleEntrypoint()
|
|
150
|
+
})
|
|
151
|
+
},
|
|
152
|
+
bootstrap: ['entrypoint']
|
|
153
|
+
}))
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## API
|
|
157
|
+
|
|
158
|
+
| 导出 | 说明 |
|
|
159
|
+
|---|---|
|
|
160
|
+
| `Mutex` | 组件:`tryAcquire`、`acquire`、`synchronized`、`heldLocks`;选项 `driver`、`prefix`、`ttl`、`timeout`、`retryDelay` |
|
|
161
|
+
| `MutexLock` | 已持有的锁:`name`、`token`、`fence`、`acquiredAt`、`held`、`signal`、`release()`、`extend()`、`await using` |
|
|
162
|
+
| `MutexAcquireOptions` | 一次获取的 `ttl`、`timeout` 和用于中止的 `signal` |
|
|
163
|
+
| `MutexDriverOptions` | `driver` 选项:下面某个驱动的选项,以 `type` 区分 |
|
|
164
|
+
| `MemoryMutexOptions`、`FileMutexOptions`、`RedisMutexOptions`、`PostgresMutexOptions`、`MysqlMutexOptions` | 各个驱动的选项 |
|
|
165
|
+
| `Synchronized` | 方法装饰器,让控制器动作在锁的保护下执行 |
|
|
166
|
+
| `SynchronizedOptions` | `@Synchronized` 的锁组件、`timeout`、`wait` 和 `ttl` |
|
|
167
|
+
| `lockNameOf` | 根据模板(`'order:{id}'`)和输入生成锁名 |
|
|
168
|
+
| `MutexDriver`、`DriverLock` | 驱动的基类,以及驱动授予的锁 |
|
|
169
|
+
| `MemoryMutexDriver`、`FileMutexDriver`、`RedisMutexDriver`、`PostgresMutexDriver`、`MysqlMutexDriver` | 各个驱动,可以不通过组件直接使用(不等待,也不续期) |
|
|
170
|
+
| `RedisMutexConnection`、`PostgresMutexConnection`、`MysqlMutexConnection` | 传给驱动构造函数的连接 |
|
|
171
|
+
| `MutexTimeoutException`、`MutexLostException`、`MutexDriverException`、`MutexDriverNotFoundException` | 异常 |
|
|
172
|
+
|
|
173
|
+
以上全部由 `lakutata/com/mutex` 和 `@lakutata/mutex` 导出。类型声明中的每一项都附有说明和示例。
|
|
174
|
+
|
|
175
|
+
## 错误
|
|
176
|
+
|
|
177
|
+
| 异常 | 什么时候 |
|
|
178
|
+
|---|---|
|
|
179
|
+
| `MutexTimeoutException`(`E_MUTEX_TIMEOUT`,HTTP 409) | `acquire()` 或 `synchronized()` 在超时前没有获得锁;`@Synchronized` 动作的锁被其他请求持有 |
|
|
180
|
+
| `MutexLostException`(`E_MUTEX_LOST`) | 不会被抛出:锁丢失时作为 `lock.signal` 的中止原因 |
|
|
181
|
+
| `MutexDriverException`(`E_MUTEX_DRIVER`) | 调用期间锁的存储出错(连不上、查询失败、文件错误) |
|
|
182
|
+
| `MutexDriverNotFoundException`(`E_MUTEX_DRIVER_NOT_FOUND`) | 第一次获取锁时,驱动所需的包(`redis`、`pg`、`mysql2`)没有安装 |
|
|
183
|
+
| `InvalidObjectOptionsException`(来自 `lakutata`) | 组件的某个选项的值不合法;应用启动失败 |
|
|
184
|
+
| `TypeError` | 锁名为空;`@Synchronized` 模板中的字段在输入中不存在 |
|
|
185
|
+
|
|
186
|
+
## 另见
|
|
187
|
+
|
|
188
|
+
- 网站上的指南:[互斥锁](https://lakutata.com/zh/guide/Mutex)。
|
|
189
|
+
- `lakutata` 包中的 `doc/zh/Mutex.md`:锁的指南。
|
|
190
|
+
- `@lakutata/entrypoint`(`lakutata/com/entrypoint`):控制器、`@Schedule` 和定时任务适配器。
|
|
191
|
+
- `@lakutata/core`(`lakutata`):组件、`Application` 和依赖注入。
|
|
192
|
+
- 在这个包中运行 `npm run test:stores`,会在 Docker 中针对 Redis、PostgreSQL 和 MySQL 测试各个驱动。
|
package/package.json
CHANGED
|
@@ -1,7 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lakutata/mutex",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "The locks of the lakutata framework: mutual exclusion between the instances and the processes of an application, over memory, files, Redis, PostgreSQL or MySQL, held by tokens, renewed while they are held.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"lakutata",
|
|
7
|
+
"mutex",
|
|
8
|
+
"lock",
|
|
9
|
+
"distributed-lock",
|
|
10
|
+
"redis",
|
|
11
|
+
"postgresql",
|
|
12
|
+
"mysql",
|
|
13
|
+
"typescript"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://lakutata.com/packages/mutex",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/lakutata/lakutata/issues",
|
|
18
|
+
"email": "admin@lakutata.com"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/lakutata/lakutata.git",
|
|
23
|
+
"directory": "packages/mutex"
|
|
24
|
+
},
|
|
25
|
+
"author": "alex.ma",
|
|
26
|
+
"license": "MIT",
|
|
5
27
|
"type": "module",
|
|
6
28
|
"main": "./dist/cjs/exports/Mutex.js",
|
|
7
29
|
"types": "./dist/cjs/exports/Mutex.d.ts",
|
|
@@ -30,7 +52,7 @@
|
|
|
30
52
|
"test:stores": "sh test-env/run.sh"
|
|
31
53
|
},
|
|
32
54
|
"peerDependencies": {
|
|
33
|
-
"@lakutata/core": "3.
|
|
55
|
+
"@lakutata/core": "3.1.0",
|
|
34
56
|
"mysql2": "^3.0.0",
|
|
35
57
|
"pg": "^8.0.0",
|
|
36
58
|
"redis": "^4.6.0 || ^5.0.0"
|
|
@@ -47,7 +69,7 @@
|
|
|
47
69
|
}
|
|
48
70
|
},
|
|
49
71
|
"engines": {
|
|
50
|
-
"node": ">=
|
|
72
|
+
"node": "^22.13.0 || >=24.11.0"
|
|
51
73
|
},
|
|
52
74
|
"files": [
|
|
53
75
|
"dist",
|