lakutata 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 +50 -30
- package/README.zh-CN.md +130 -0
- package/dist/cjs/exports/QueueComponent.d.ts +1 -0
- package/dist/cjs/exports/QueueComponent.js +20 -0
- package/dist/esm/exports/QueueComponent.js +1 -0
- package/dist/types/exports/QueueComponent.d.ts +1 -0
- package/doc/en/Application.md +107 -0
- package/doc/en/CLI.md +274 -0
- package/doc/en/Cache.md +379 -0
- package/doc/en/Command.md +110 -0
- package/doc/en/ContainerdIntegration.md +17 -16
- package/doc/en/Controllers.md +256 -0
- package/doc/en/DBus.md +291 -0
- package/doc/en/DTOValidation.md +72 -43
- package/doc/en/Database.md +188 -0
- package/doc/en/DatabaseDrivers.md +287 -0
- package/doc/en/Decorator.md +52 -40
- package/doc/en/DockerIntegration.md +7 -6
- package/doc/en/Entities.md +240 -0
- package/doc/en/Entrypoint.md +216 -0
- package/doc/en/Exceptions.md +174 -0
- package/doc/en/GRPC.md +251 -0
- package/doc/en/GettingStarted.md +165 -0
- package/doc/en/HTTP.md +507 -0
- package/doc/en/HelperFunctions.md +11 -11
- package/doc/en/I18n.md +119 -115
- package/doc/en/Injection.md +144 -0
- package/doc/en/Installers.md +280 -0
- package/doc/en/Logger.md +206 -0
- package/doc/en/MCP.md +293 -0
- package/doc/en/Middlewares.md +213 -0
- package/doc/en/Migrations.md +202 -0
- package/doc/en/MongoEmbedded.md +149 -0
- package/doc/en/Monitor.md +164 -0
- package/doc/en/Mutex.md +27 -25
- package/doc/en/NATS.md +246 -0
- package/doc/en/NativeAddons.md +212 -0
- package/doc/en/Objects.md +143 -0
- package/doc/en/Overview.md +62 -85
- package/doc/en/Packaging.md +193 -0
- package/doc/en/ProjectStructure.md +124 -0
- package/doc/en/Queue.md +269 -0
- package/doc/en/Schedules.md +217 -0
- package/doc/en/Security.md +10 -9
- package/doc/en/ServiceCalls.md +504 -0
- package/doc/en/Time.md +19 -20
- package/doc/en/UpgradingFrom1.md +435 -0
- package/doc/en/UpgradingFrom2.md +315 -0
- package/doc/en/Workspaces.md +189 -0
- package/doc/en/examples/BackgroundJobs.md +677 -0
- package/doc/en/examples/CommandLineTool.md +664 -0
- package/doc/en/examples/DesktopApp.md +809 -0
- package/doc/en/examples/DeviceService.md +607 -0
- package/doc/en/examples/MCPServer.md +653 -0
- package/doc/en/examples/README.md +16 -0
- package/doc/en/examples/RestAPI.md +490 -0
- package/doc/en/examples/ServiceCalls.md +829 -0
- package/doc/en/examples/WebApp.md +568 -0
- package/doc/zh/Application.md +75 -0
- package/doc/zh/CLI.md +250 -0
- package/doc/zh/Cache.md +328 -0
- package/doc/zh/Command.md +78 -0
- package/doc/zh/ContainerdIntegration.md +9 -9
- package/doc/zh/Controllers.md +228 -0
- package/doc/zh/DBus.md +257 -0
- package/doc/zh/DTOValidation.md +49 -19
- package/doc/zh/Database.md +175 -0
- package/doc/zh/DatabaseDrivers.md +261 -0
- package/doc/zh/Decorator.md +25 -18
- package/doc/zh/DockerIntegration.md +6 -6
- package/doc/zh/Entities.md +224 -0
- package/doc/zh/Entrypoint.md +193 -0
- package/doc/zh/Exceptions.md +158 -0
- package/doc/zh/GRPC.md +226 -0
- package/doc/zh/GettingStarted.md +148 -0
- package/doc/zh/HTTP.md +445 -0
- package/doc/zh/HelperFunctions.md +2 -2
- package/doc/zh/I18n.md +26 -26
- package/doc/zh/Injection.md +129 -0
- package/doc/zh/Installers.md +232 -0
- package/doc/zh/Logger.md +188 -0
- package/doc/zh/MCP.md +265 -0
- package/doc/zh/Middlewares.md +198 -0
- package/doc/zh/Migrations.md +178 -0
- package/doc/zh/MongoEmbedded.md +118 -0
- package/doc/zh/Monitor.md +145 -0
- package/doc/zh/Mutex.md +10 -10
- package/doc/zh/NATS.md +207 -0
- package/doc/zh/NativeAddons.md +180 -0
- package/doc/zh/Objects.md +119 -0
- package/doc/zh/Overview.md +46 -71
- package/doc/zh/Packaging.md +154 -0
- package/doc/zh/ProjectStructure.md +102 -0
- package/doc/zh/Queue.md +214 -0
- package/doc/zh/Schedules.md +193 -0
- package/doc/zh/Security.md +6 -6
- package/doc/zh/ServiceCalls.md +459 -0
- package/doc/zh/Time.md +2 -2
- package/doc/zh/UpgradingFrom1.md +347 -0
- package/doc/zh/UpgradingFrom2.md +230 -0
- package/doc/zh/Workspaces.md +150 -0
- package/doc/zh/examples/BackgroundJobs.md +618 -0
- package/doc/zh/examples/CommandLineTool.md +618 -0
- package/doc/zh/examples/DesktopApp.md +748 -0
- package/doc/zh/examples/DeviceService.md +546 -0
- package/doc/zh/examples/MCPServer.md +604 -0
- package/doc/zh/examples/README.md +14 -0
- package/doc/zh/examples/RestAPI.md +458 -0
- package/doc/zh/examples/ServiceCalls.md +765 -0
- package/doc/zh/examples/WebApp.md +520 -0
- package/package.json +46 -17
- package/doc/en/ActionPattern.md +0 -408
- package/doc/en/Core.md +0 -312
- package/doc/en/ORMIntegration.md +0 -341
- package/doc/zh/ActionPattern.md +0 -334
- package/doc/zh/Core.md +0 -256
- package/doc/zh/ORMIntegration.md +0 -319
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Lakutata 嵌入式 MongoDB
|
|
2
|
+
|
|
3
|
+
`mongo-embedded` 驱动让应用在自己的进程里拥有一个 MongoDB:使用 TypeORM 中 MongoDB 的实体、仓储和查询操作符,文档保存在一个目录的文件中。不需要安装或运行服务器,适合桌面应用、设备、命令行工具和测试。
|
|
4
|
+
|
|
5
|
+
它是 lakutata 的一部分,不属于 TypeORM。查询由 [mingo](https://github.com/kofrasa/mingo) 执行,它是 MongoDB 查询语言的 JavaScript 实现。
|
|
6
|
+
|
|
7
|
+
## 1. 第一个数据库
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install lakutata mingo bson
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import {Application} from 'lakutata'
|
|
15
|
+
import {buildDatabaseOptions, type Database} from 'lakutata/com/database'
|
|
16
|
+
import {Column, Entity, Index, ObjectIdColumn} from 'lakutata/decorator/orm'
|
|
17
|
+
import type {MongoRepository, ObjectId} from 'lakutata/orm'
|
|
18
|
+
|
|
19
|
+
@Entity('notes')
|
|
20
|
+
class Note {
|
|
21
|
+
@ObjectIdColumn()
|
|
22
|
+
public id: ObjectId
|
|
23
|
+
|
|
24
|
+
@Index()
|
|
25
|
+
@Column()
|
|
26
|
+
public folder: string
|
|
27
|
+
|
|
28
|
+
@Column()
|
|
29
|
+
public title: string
|
|
30
|
+
|
|
31
|
+
@Column()
|
|
32
|
+
public words: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
Application
|
|
36
|
+
.run(() => ({
|
|
37
|
+
id: 'notes.app',
|
|
38
|
+
name: 'Notes',
|
|
39
|
+
components: {
|
|
40
|
+
db: buildDatabaseOptions({
|
|
41
|
+
type: 'mongo-embedded',
|
|
42
|
+
//一个目录,不存在时会自动创建
|
|
43
|
+
database: 'data/notes',
|
|
44
|
+
entities: [Note],
|
|
45
|
+
//创建实体的索引
|
|
46
|
+
synchronize: true
|
|
47
|
+
})
|
|
48
|
+
},
|
|
49
|
+
bootstrap: ['db']
|
|
50
|
+
}))
|
|
51
|
+
.onLaunched(async (app: Application): Promise<void> => {
|
|
52
|
+
const db: Database = await app.getObject<Database>('db')
|
|
53
|
+
const notes: MongoRepository<Note> = db.getMongoRepository(Note)
|
|
54
|
+
await notes.save([
|
|
55
|
+
{folder: 'work', title: 'Plan', words: 120},
|
|
56
|
+
{folder: 'work', title: 'Review', words: 40},
|
|
57
|
+
{folder: 'home', title: 'Shopping', words: 15}
|
|
58
|
+
])
|
|
59
|
+
//MongoDB 的查询操作符:["Plan"]
|
|
60
|
+
console.log((await notes.find({where: {folder: 'work', words: {$gte: 50}}})).map((note: Note): string => note.title))
|
|
61
|
+
//聚合:[{_id: 'home', words: 15}, {_id: 'work', words: 160}]
|
|
62
|
+
console.log(await notes.aggregate([{$group: {_id: '$folder', words: {$sum: '$words'}}}, {$sort: {_id: 1}}]).toArray())
|
|
63
|
+
await notes.updateMany({folder: 'home'}, {$set: {folder: 'personal'}})
|
|
64
|
+
})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
实体、仓储和查询都与 TypeORM 中的 MongoDB 相同(见 [MongoDB](DatabaseDrivers.md#mongodb)):用 `lakutata/orm` 的 `ObjectId` 作为类型的 `@ObjectIdColumn()`,带有 MongoDB 方法的 `MongoRepository`。只改连接选项,应用就能换成 MongoDB 服务器。
|
|
68
|
+
|
|
69
|
+
## 2. 选项
|
|
70
|
+
|
|
71
|
+
| 选项 | 默认值 | 含义 |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| `database` | `':memory:'` | 数据库所在的目录,或者 `':memory:'` 表示不保存的数据库 |
|
|
74
|
+
| `sync` | `'interval'` | 写入什么时候刷到磁盘:`'interval'`、`'always'` 或 `'never'`(第 3 节) |
|
|
75
|
+
| `syncInterval` | `100` | `sync: 'interval'` 的刷盘间隔,单位毫秒 |
|
|
76
|
+
| `cacheSize` | 64 MiB | 读过的文档保留在内存中所用的内存,单位字节;`0` 表示不保留 |
|
|
77
|
+
| `compaction` | `{minimumSize: 16 MiB, ratio: 2}` | 什么时候压缩集合的文件;`false` 表示从不压缩(第 3 节) |
|
|
78
|
+
| `snapshot` | `{minimumSize: 64 MiB, ratio: 0.25}` | 集合打开期间什么时候保存它的索引;`false` 表示只在关闭时保存(第 3 节) |
|
|
79
|
+
| `worker` | `true` | 在 worker 线程中运行数据库(第 4 节) |
|
|
80
|
+
| `workerResourceLimits` | 无 | worker 线程的内存限制(第 4 节) |
|
|
81
|
+
|
|
82
|
+
所有数据库通用的选项(`entities`、`synchronize`、`logging`……)同样适用。
|
|
83
|
+
|
|
84
|
+
## 3. 文档如何存储
|
|
85
|
+
|
|
86
|
+
- 每个集合在目录中有自己的文件。一次写入只追加一次文件;文档通过保存在内存中(JavaScript 堆之外)的索引按 `_id` 查找。
|
|
87
|
+
- **持久性。** 使用 `sync: 'interval'` 时,写入每 100 毫秒刷到磁盘一次:已经完成的写入在应用崩溃后依然存在,但如果计算机崩溃或断电,可能丢失最后一个间隔内的写入。`'always'` 在每次写入完成前刷盘(同时进行的写入共用一次刷盘);`'never'` 把刷盘交给操作系统。
|
|
88
|
+
- **崩溃。** 被崩溃打断的写入,在数据库再次打开时会被忽略:数据库就是这次写入之前的样子。
|
|
89
|
+
- **压缩。** 更新或删除会把文档的旧版本留在文件里。当文件达到 `minimumSize`,并且是有效文档的 `ratio` 倍时,就会压缩:有效文档被复制到一个新文件,读写照常进行。
|
|
90
|
+
- **快照。** 索引在数据库关闭时保存到快照;数据库打开期间,自上次快照以来写入了 `minimumSize` 字节并达到文件的 `ratio` 比例时也会保存。打开数据库时先读取快照,再只读取快照之后写入的记录。没有快照时(比如在第一次快照之前崩溃),索引要从整个文件重建:两个索引、1000 万个文档大约需要 60 秒。
|
|
91
|
+
- **只能由一个进程打开。** 目录会被打开它的进程锁住。另一个进程打开它会失败,错误信息中会写明占用它的进程。
|
|
92
|
+
|
|
93
|
+
## 4. worker 线程
|
|
94
|
+
|
|
95
|
+
默认情况下,数据库运行在一个 worker 线程中:它的查询、写入和文件操作都不会阻塞应用的事件循环。每次调用及其结果在线程之间以 BSON 格式传递,每次调用大约多花 20 微秒。
|
|
96
|
+
|
|
97
|
+
- `worker: false` 让数据库运行在应用自己的线程中。这样调用更快,查询也可以使用 JavaScript 函数(`$where`、`$function`、`$accumulator`),这些函数无法发送到 worker 线程。
|
|
98
|
+
- 除非用 `workerResourceLimits` 设置了自己的限制(`maxOldGenerationSizeMb`、`maxYoungGenerationSizeMb`、`codeRangeSizeMb`、`stackSizeMb`),worker 线程沿用应用的内存限制。超出限制时,被停止的是 worker 线程而不是应用:它的调用会失败,它的目录被解锁。在 Node.js 24 上,256 MB 或更小的限制反而可能让整个进程停止;Bun 会忽略这些限制。
|
|
99
|
+
|
|
100
|
+
## 5. 大型集合
|
|
101
|
+
|
|
102
|
+
- **耗时的写入。** `insertMany`、`updateMany`、`deleteMany` 以及在已有文档上创建索引,都按每批 1000 个文档执行,批与批之间让出事件循环。与没有事务的 MongoDB 相同,某个文档写入失败时,它之前的文档仍然保持已写入。
|
|
103
|
+
- **`insertMany`。** 与 MongoDB 相同,它在遇到第一个重复键时停止;设置 `ordered: false` 时则继续插入其他文档。之后它会带着 `writeErrors`、`insertedCount` 和 `insertedIds` 失败。
|
|
104
|
+
- **游标。** 不带排序的游标(`createCursor`、`createEntityCursor`)用 `next()`、`hasNext()` 或 `for await` 读取时,每次读取 1000 个文档;`toArray()` 则一次读取全部。
|
|
105
|
+
- **排序。** 没有索引的排序会把所有参与排序的文档放在内存中:给大型集合中用于排序的字段建索引。带 `limit` 的排序会按索引的顺序读取。
|
|
106
|
+
- **分组。** 位于管道开头、只跟在逐个处理文档的阶段(`$match`、`$project`、`$unwind`……)之后的 `$group`,按批读取文档:它占用的内存只是各个分组的内存。
|
|
107
|
+
- **索引键**最多大约 1000 字节:写入更大的键会在写入之前失败,错误码与 MongoDB 的 17280 相同。
|
|
108
|
+
- 超过 2^53 的整数以 `Long` 保存。在内存中完成的排序里(没有索引,或者没有 `limit`),它们排在其他数字之后,按文本顺序排列。
|
|
109
|
+
|
|
110
|
+
## 6. 不支持的功能
|
|
111
|
+
|
|
112
|
+
与 TypeORM 中的 MongoDB 服务器相同:
|
|
113
|
+
|
|
114
|
+
- 没有关系,没有查询构造器;
|
|
115
|
+
- 没有事务:**`transaction()` 会在没有事务的情况下执行它的函数**;
|
|
116
|
+
- `synchronize` 只创建索引,迁移需要手写(见 [Migrations](Migrations.md))。
|
|
117
|
+
|
|
118
|
+
id 使用的 `ObjectId` 是 `lakutata/orm` 导出的那个。查询也接受 `bson` 包的 `ObjectId`,但 `instanceof` 会区分它们:从数据库读出的 id 不是 `import {ObjectId} from 'bson'` 得到的 `ObjectId` 的实例。
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Lakutata 监控
|
|
2
|
+
|
|
3
|
+
`lakutata/com/monitor` 中的监控器测量应用所在的进程:CPU 使用率、内存、事件循环的延迟,以及 HTTP 请求的耗时。每个监控器都是一个组件,在固定大小的内存中保存它自创建以来(或者自上次 `reset()` 以来)各次测量的统计。除了 `lakutata`,不需要安装其他包。
|
|
4
|
+
|
|
5
|
+
## 1. 声明和读取监控器
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import {Application, type ILogger} from 'lakutata'
|
|
9
|
+
import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
|
|
10
|
+
import {GET} from 'lakutata/decorator/ctrl'
|
|
11
|
+
import {Inject} from 'lakutata/decorator/di'
|
|
12
|
+
import {expressHTTPEntrypoint} from 'lakutata/adapter/express'
|
|
13
|
+
import {CpuMonitor, EventLoopMonitor, HttpRequestMonitor, MemoryMonitor} from 'lakutata/com/monitor'
|
|
14
|
+
|
|
15
|
+
class MetricsController extends Controller {
|
|
16
|
+
@Inject('cpu')
|
|
17
|
+
protected readonly cpu: CpuMonitor
|
|
18
|
+
|
|
19
|
+
@Inject('memory')
|
|
20
|
+
protected readonly memory: MemoryMonitor
|
|
21
|
+
|
|
22
|
+
@Inject('eventLoop')
|
|
23
|
+
protected readonly eventLoop: EventLoopMonitor
|
|
24
|
+
|
|
25
|
+
@Inject('httpRequests')
|
|
26
|
+
protected readonly httpRequests: HttpRequestMonitor
|
|
27
|
+
|
|
28
|
+
@GET('/metrics')
|
|
29
|
+
public async metrics(): Promise<Record<string, unknown>> {
|
|
30
|
+
return {
|
|
31
|
+
cpu: this.cpu.statistics,
|
|
32
|
+
memory: this.memory.statistics,
|
|
33
|
+
eventLoop: this.eventLoop.statistics,
|
|
34
|
+
httpRequests: this.httpRequests.statistics
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
Application
|
|
40
|
+
.run(() => ({
|
|
41
|
+
id: 'api.app',
|
|
42
|
+
name: 'API',
|
|
43
|
+
components: {
|
|
44
|
+
cpu: {class: CpuMonitor},
|
|
45
|
+
memory: {class: MemoryMonitor},
|
|
46
|
+
eventLoop: {class: EventLoopMonitor},
|
|
47
|
+
httpRequests: {class: HttpRequestMonitor},
|
|
48
|
+
entrypoint: buildEntrypoints({
|
|
49
|
+
controllers: [MetricsController],
|
|
50
|
+
http: expressHTTPEntrypoint({port: 8080})
|
|
51
|
+
})
|
|
52
|
+
},
|
|
53
|
+
//在应用启动时创建,从一开始就测量
|
|
54
|
+
bootstrap: ['cpu', 'memory', 'eventLoop', 'httpRequests', 'entrypoint']
|
|
55
|
+
}))
|
|
56
|
+
.onLaunched(async (app: Application, log: ILogger): Promise<void> => {
|
|
57
|
+
const cpu: CpuMonitor = await app.getObject<CpuMonitor>('cpu')
|
|
58
|
+
const eventLoop: EventLoopMonitor = await app.getObject<EventLoopMonitor>('eventLoop')
|
|
59
|
+
//每分钟记录这一分钟的统计,然后开始新的一段(/metrics 也随之从零开始)
|
|
60
|
+
setInterval((): void => {
|
|
61
|
+
log.info({cpuAvg: cpu.statistics.usageAvg, eventLoopMax: eventLoop.statistics.max}, 'the last minute')
|
|
62
|
+
cpu.reset()
|
|
63
|
+
eventLoop.reset()
|
|
64
|
+
}, 60000)
|
|
65
|
+
})
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- 用 `class` 声明需要的每个监控器,名字自己选。监控器是单例,注入它的所有对象共用同一份统计。
|
|
69
|
+
- 监控器从创建时开始测量:要把它列在 `bootstrap` 里。否则它在第一次被用到时才创建,从那时起才开始测量。
|
|
70
|
+
- 监控器没有选项。给它的选项会被忽略,并在应用启动时给出警告。
|
|
71
|
+
- 每次读取 `statistics` 时都会重新计算。`reset()` 开始新的一段:之后的统计只包含此后的测量。
|
|
72
|
+
- 监控器的定时器不会让进程保持运行;应用停止时监控器也随之停止。
|
|
73
|
+
|
|
74
|
+
## 2. 测量什么
|
|
75
|
+
|
|
76
|
+
| 监控器 | 测量 | 采样 | 单位 |
|
|
77
|
+
|---|---|---|---|
|
|
78
|
+
| `CpuMonitor` | 进程的 CPU 时间 | 每秒一次 | 单个核心的百分比 |
|
|
79
|
+
| `MemoryMonitor` | 进程的内存:常驻内存、堆、外部内存 | 创建时一次,之后每秒一次 | 字节 |
|
|
80
|
+
| `EventLoopMonitor` | 事件循环的延迟,以及它忙碌的时间占比 | 持续 | 毫秒、百分比 |
|
|
81
|
+
| `HttpRequestMonitor` | 进程响应的 HTTP 请求的耗时 | 每个请求 | 毫秒 |
|
|
82
|
+
|
|
83
|
+
CPU、内存和 HTTP 请求的统计包含最新值、最小值、最大值和平均值,这些都是精确值;另外还有三位有效数字的百分位数(P50、P90、P95、P99)。CPU 和 HTTP 请求的统计在第一次测量之前全都是 0。
|
|
84
|
+
|
|
85
|
+
## 3. `CpuMonitor`
|
|
86
|
+
|
|
87
|
+
| 字段 | 含义 |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `usage` | 最新一次采样 |
|
|
90
|
+
| `usageMin`、`usageMax`、`usageAvg` | 各次采样的最小值、最大值和平均值 |
|
|
91
|
+
| `usageP50`、`usageP90`、`usageP95`、`usageP99` | 各次采样的百分位数 |
|
|
92
|
+
|
|
93
|
+
- 使用率是整个进程的 CPU 时间(用户态和内核态,包括所有线程)除以经过的时间,以单个核心的百分比表示:100 表示一个核心完全占满,使用多个核心的进程(worker 线程、Node.js 的线程池)会超过 100。数值保留 5 位小数。
|
|
94
|
+
- 第一次采样在监控器创建一秒后进行。
|
|
95
|
+
- 事件循环被阻塞时,下一次采样会延迟,并覆盖整个阻塞的时间。这次采样按它覆盖的秒数计入,所以平均值和百分位数按时长反映忙碌的时段。
|
|
96
|
+
|
|
97
|
+
## 4. `MemoryMonitor`
|
|
98
|
+
|
|
99
|
+
| 字段 | 含义 |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `physicalTotal` | 机器的内存(`os.totalmem()`),不是容器的内存限制 |
|
|
102
|
+
| `physicalUsed`、`physicalUsage` | 进程的常驻内存(`rss`),以及它占 `physicalTotal` 的百分比 |
|
|
103
|
+
| `heapTotal`、`heapUsed`、`heapUsage` | V8 的堆和其中已使用的部分,以及两者之比(百分比) |
|
|
104
|
+
| `externalUsed` | 绑定到 JavaScript 对象上的 C++ 对象占用的内存,包括 `Buffer` |
|
|
105
|
+
| `physicalUsedMin`、`…Max`、`…Avg`、`…P50`、`…P90`、`…P95`、`…P99` | `physicalUsed` 的统计 |
|
|
106
|
+
| `heapUsedMin`、`…Max`、`…Avg`、`…P50`、`…P90`、`…P95`、`…P99` | `heapUsed` 的统计 |
|
|
107
|
+
| `externalUsedMin`、`…Max`、`…Avg`、`…P50`、`…P90`、`…P95`、`…P99` | `externalUsed` 的统计 |
|
|
108
|
+
|
|
109
|
+
- 数值的单位是字节;平均值和两个百分比保留 2 位小数。
|
|
110
|
+
- `heapUsage` 比较的是已使用的堆和目前已分配的堆,而不是堆的上限:这个值高本身不说明有问题。内存泄漏的表现是 `heapUsedMin` 一段比一段高。
|
|
111
|
+
- `reset()` 保留最新值(`physicalUsed`、`heapUsed`……);其他统计重新开始。
|
|
112
|
+
|
|
113
|
+
## 5. `EventLoopMonitor`
|
|
114
|
+
|
|
115
|
+
| 字段 | 含义 |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `min`、`max`、`avg`、`stdDev` | 延迟的最小值、最大值、平均值和标准差 |
|
|
118
|
+
| `p50`、`p90`、`p95`、`p99` | 延迟的百分位数 |
|
|
119
|
+
| `utilRate` | 事件循环忙碌的时间占比,单位为百分比 |
|
|
120
|
+
|
|
121
|
+
- 延迟是一个 1 毫秒定时器两次运行之间的间隔:事件循环空闲时大约是 1 毫秒;同步的工作(耗时的计算、同步 I/O、很大的 JSON)占住事件循环时会变长。`max` 就是最长的一次阻塞。
|
|
122
|
+
- `utilRate` 覆盖自创建或上次 `reset()` 以来的整段时间:接近 100 时,事件循环已经没有空闲时间。刚调用 `reset()` 之后它是 100,因为事件循环正忙于运行调用它的代码。
|
|
123
|
+
- 这些数值来自 Node.js 的直方图(`monitorEventLoopDelay`):不做舍入,并且都是近似值,最小值、最大值和平均值也一样。
|
|
124
|
+
- 在应用空闲时,1 毫秒的定时器大约占用单个核心的 1.5 %:在用电池供电的设备上,只在需要时才声明这个监控器。
|
|
125
|
+
|
|
126
|
+
## 6. `HttpRequestMonitor`
|
|
127
|
+
|
|
128
|
+
| 字段 | 含义 |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `count` | 已响应的请求数 |
|
|
131
|
+
| `min`、`max`、`avg`、`stdDev` | 请求耗时的最小值、最大值、平均值和标准差 |
|
|
132
|
+
| `p50`、`p90`、`p95`、`p99` | 请求耗时的百分位数 |
|
|
133
|
+
|
|
134
|
+
- 它统计进程中所有 HTTP 服务器响应的请求(HTTP 适配器、HTTP 服务、基于 HTTP 的 JSON-RPC、Socket.IO 的服务器、你自己的服务器),包括出错的请求,也包括 HTTPS。它不统计 HTTP/2 的请求、进程自己发出的请求,以及从未得到响应的请求。
|
|
135
|
+
- 它不按路由或状态码细分。
|
|
136
|
+
- 它只统计在它创建之后开始的请求:创建时正在处理的请求不计入。在动作中读取时,统计不包含正在响应的这个请求。
|
|
137
|
+
- 耗时的单位是毫秒,保留 2 位小数。
|
|
138
|
+
|
|
139
|
+
## 7. worker 线程
|
|
140
|
+
|
|
141
|
+
在 worker 线程中声明的监控器测量的是:
|
|
142
|
+
|
|
143
|
+
- 它自己的事件循环的延迟和忙碌时间(`EventLoopMonitor`);
|
|
144
|
+
- 它自己的 HTTP 服务器的请求(`HttpRequestMonitor`);
|
|
145
|
+
- 整个进程的 CPU 和常驻内存(`CpuMonitor`、`MemoryMonitor` 的 `physicalUsed`),但堆是它自己的(`heapUsed`)。
|
package/doc/zh/Mutex.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Lakutata 互斥锁
|
|
2
2
|
|
|
3
|
-
**Mutex 组件**(`lakutata/com/mutex`,`@lakutata/mutex`
|
|
3
|
+
**Mutex 组件**(`lakutata/com/mutex`,`@lakutata/mutex` 包)为应用提供具名的锁:受同一把锁保护的工作,同一时间只在一处执行。按所用的驱动,锁可以由一个进程内的各组件共享,由一台机器上的各进程共享,或者由应用的所有实例共享。这个组件还支撑 `@Synchronized`(让控制器动作在锁的保护下执行)和 `exclusive` 定时任务。更多细节见 `@lakutata/mutex` 的 README。
|
|
4
4
|
|
|
5
5
|
## 1. 配置
|
|
6
6
|
|
|
@@ -30,12 +30,12 @@ Application.run(() => ({
|
|
|
30
30
|
| 驱动 | 共享范围 | 说明 |
|
|
31
31
|
|---|---|---|
|
|
32
32
|
| `{type: 'memory'}`(默认) | 一个进程内的各组件 | 开发、测试、单实例 |
|
|
33
|
-
| `{type: 'file', directory}` |
|
|
33
|
+
| `{type: 'file', directory?}` | 一台机器上的各进程 | 以独占方式创建锁文件(不指定 `directory` 时,放在系统临时目录下的 `lakutata-mutex` 中);已退出的进程持有的锁会被立即接管 |
|
|
34
34
|
| `{type: 'redis', url}` | 所有实例 | Redis 4+ 或 Valkey,需要 `redis` 包;有防护令牌;请使用 `noeviction`(被淘汰的键就是丢失的锁) |
|
|
35
|
-
| `{type: 'postgres', host, database, username, password, mode}` | 所有实例 | 需要 `pg` 包;`mode: 'advisory'
|
|
36
|
-
| `{type: 'mysql', host, database, username, password, mode}` | 所有实例 | 需要 `mysql2` 包;`mode: 'advisory'`(默认):在独占的连接上使用 `GET_LOCK
|
|
35
|
+
| `{type: 'postgres', host, database, username, password, mode}` | 所有实例 | 需要 `pg` 包;`mode: 'advisory'`(默认):在独占的连接上使用咨询锁,连接关闭时释放,不能通过事务模式的 PgBouncer 使用;`mode: 'table'`:表中的行,会过期,有防护令牌 |
|
|
36
|
+
| `{type: 'mysql', host, database, username, password, mode}` | 所有实例 | 需要 `mysql2` 包;`mode: 'advisory'`(默认):在独占的连接上使用 `GET_LOCK`,集群的各节点之间不共享;`mode: 'table'`:表中的行,会过期,有防护令牌 |
|
|
37
37
|
|
|
38
|
-
客户端(`redis`、`pg`、`mysql2`)是可选的 peer
|
|
38
|
+
客户端(`redis`、`pg`、`mysql2`)是可选的 peer 依赖。驱动在第一次获取锁时才建立连接;缺少它所需的包时,这次获取会以 `MutexDriverNotFoundException` 失败。
|
|
39
39
|
|
|
40
40
|
## 2. 使用
|
|
41
41
|
|
|
@@ -79,13 +79,13 @@ class Orders extends Component {
|
|
|
79
79
|
- **持有者**:获取锁时使用一个随机令牌;只有持有者能释放或续期它。
|
|
80
80
|
- **过期**:会过期的驱动按 `ttl` 持有锁,组件每过三分之一的时间续期一次,所以崩溃的持有者最多再占用一个 `ttl`。PostgreSQL 和 MySQL 的咨询锁不会过期:它们随连接一起结束。
|
|
81
81
|
- **锁丢失**:续期一直失败直到锁过期,或者持有咨询锁的连接关闭时,`lock.held` 变为 `false`,`lock.signal` 以 `MutexLostException` 中止。受保护的工作不会被打断:它应当关注这个信号。
|
|
82
|
-
- **防护令牌**:`lock.fence` 随每次获取递增(内存、Redis、`table`
|
|
82
|
+
- **防护令牌**:`lock.fence` 随每次获取递增(内存、Redis、`table` 模式):在锁的保护下写入的存储,可以用它拒绝旧持有者的写入。
|
|
83
83
|
- **不可重入**:获取组件已持有的锁时,和其他持有者一样要等待它被释放。
|
|
84
84
|
- **生命周期**:组件被销毁时(应用停止时)会释放它持有的锁并关闭驱动。
|
|
85
85
|
|
|
86
86
|
## 3. 同步的动作
|
|
87
87
|
|
|
88
|
-
`@Synchronized(name, options?)`
|
|
88
|
+
`@Synchronized(name, options?)` 让控制器动作在锁的保护下执行:锁在输入校验和访问控制通过之后获取,动作结束后释放。需要同一把锁的请求,在所有实例中逐个执行,无论通过哪种传输方式;其他请求并行执行。名称是一个模板,其中的 `{field}` 取自校验后的输入;也可以是函数 `(input, context) => string`。选项有 `mutex`(组件名,默认 `'mutex'`)、`timeout`、`wait` 和 `ttl`。没有及时获得锁的请求以 `MutexTimeoutException`(HTTP 409)失败;请求被中止时,它不再等待。
|
|
89
89
|
|
|
90
90
|
```typescript
|
|
91
91
|
import {Controller} from 'lakutata/com/entrypoint'
|
|
@@ -116,7 +116,7 @@ class OrderController extends Controller {
|
|
|
116
116
|
|
|
117
117
|
## 4. 独占的定时任务
|
|
118
118
|
|
|
119
|
-
声明为 `exclusive`
|
|
119
|
+
声明为 `exclusive` 的定时动作,同一时间只在应用的一个实例中执行:每次执行都尝试从组件 `mutex`(或者 `exclusive: {mutex, key}` 指定的组件)获取锁 `schedule:<key>`(key 默认为动作的名字),没有获得锁的实例跳过这次执行。间隔按时钟对齐(每 5 分钟就是 :00、:05、:10……),这样所有实例安排的时间都相同。执行结束后,锁会一直保留到计划时间之后半个周期(最多 30 秒),这样即使某个实例的时钟稍慢,也不会把任务再执行一遍。没有锁组件时,这个动作不会执行,错误会写入日志。
|
|
120
120
|
|
|
121
121
|
```typescript
|
|
122
122
|
import {Controller} from 'lakutata/com/entrypoint'
|
|
@@ -135,11 +135,11 @@ class ReportController extends Controller {
|
|
|
135
135
|
}
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
-
入口需要配置定时适配器:`buildEntrypoints({controllers: [ReportController], schedule: scheduleEntrypoint()})`(见 [
|
|
138
|
+
入口需要配置定时适配器:`buildEntrypoints({controllers: [ReportController], schedule: scheduleEntrypoint()})`(见 [Schedules](Schedules.md))。
|
|
139
139
|
|
|
140
140
|
## 5. 异常
|
|
141
141
|
|
|
142
|
-
| 异常 |
|
|
142
|
+
| 异常 | 什么时候 |
|
|
143
143
|
|---|---|
|
|
144
144
|
| `MutexTimeoutException`(HTTP 409) | 超时前没有获得锁;`@Synchronized` 动作的锁被其他请求持有 |
|
|
145
145
|
| `MutexLostException` | 不会被抛出:锁丢失时作为 `lock.signal` 的中止原因 |
|
package/doc/zh/NATS.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Lakutata NATS
|
|
2
|
+
|
|
3
|
+
`@lakutata/nats` 包把应用连接到 [NATS](https://nats.io) 服务器。它提供:一个用来发布、订阅和发送请求的组件;一个通过 NATS 提供应用服务动作的服务适配器;一个从其他应用调用这些动作的服务代理。同一个服务的多个实例会分担它的请求,不需要负载均衡器。`lakutata` 不包含这个包,需要另外安装:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install lakutata @lakutata/nats
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
它需要 NATS 服务器 2.2 或更高版本;超过服务器大小上限的数据(第 7 节)还需要启用 JetStream(`nats-server -js`):
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
docker run -d --name nats -p 4222:4222 nats:2.12-alpine -js
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 1. NATS 组件
|
|
16
|
+
|
|
17
|
+
用 `buildNatsClientOptions` 声明组件。组件在创建时连接服务器。连不上时,创建会在 `timeout`(默认 20 秒)之后失败;如果组件是在应用启动过程中创建的(列在 `bootstrap` 里,或者被这样的对象用到),应用的启动也会随之失败。
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import {Application} from 'lakutata'
|
|
21
|
+
import {buildNatsClientOptions, type NATS} from '@lakutata/nats'
|
|
22
|
+
|
|
23
|
+
Application
|
|
24
|
+
.run(() => ({
|
|
25
|
+
id: 'pricing.app',
|
|
26
|
+
name: 'Pricing',
|
|
27
|
+
components: {
|
|
28
|
+
nats: buildNatsClientOptions({servers: 'nats://127.0.0.1:4222', name: 'pricing'})
|
|
29
|
+
},
|
|
30
|
+
bootstrap: ['nats']
|
|
31
|
+
}))
|
|
32
|
+
.onLaunched(async (app: Application): Promise<void> => {
|
|
33
|
+
const nats: NATS = await app.getObject<NATS>('nats')
|
|
34
|
+
//应答者:返回值就是请求的响应;同一个队列组里的实例分担请求
|
|
35
|
+
nats.subscribe('price.get', async (input: { sku: string }): Promise<number> => input.sku.length * 10, {queue: 'pricing'})
|
|
36
|
+
//带通配符的订阅者:第二个参数是消息的主题
|
|
37
|
+
nats.subscribe('price.changed.*', (price: number, subject: string): void => console.log(subject, price))
|
|
38
|
+
nats.publish('price.changed.eu', 42)
|
|
39
|
+
//5 秒内的响应
|
|
40
|
+
console.log(await nats.request('price.get', {sku: 'ABC'}, 5000)) //30
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- `publish(subject, payload)` 把消息发给该主题的所有订阅(每个队列组只发给其中一个订阅)。没有人订阅时,消息就丢失了。
|
|
45
|
+
- `subscribe(subject, callback, options?)` 支持通配符 `*`(一段)和 `>`(剩下的所有段)。选项有 `queue`(队列组:每条消息只发给其中一个成员)、`max`(收到这么多条后结束)、`iterator`(逐条处理消息)和 `errorResponse`(见下文)。它返回 NATS 的订阅对象(`unsubscribe()`、`drain()`)。
|
|
46
|
+
- `request(subject, payload, timeout?)` 返回第一个响应。timeout 以毫秒为单位,默认 10 分钟。应答者的回调抛出错误时,请求得不到响应,最终超时;除非订阅设置了 `errorResponse: (error) => value` 来回复它。
|
|
47
|
+
- 连接断开后会在后台重新连接(每个服务器 10 次,`maxReconnectAttempts: -1` 表示一直重试)。
|
|
48
|
+
- 应用停止时,组件会排空它的连接:正在处理的消息处理完,待发送的消息发送出去。
|
|
49
|
+
- 无法抛给调用者的失败(回调抛错、消息无法解码),组件会发出 `'error'` 事件:`nats.on('error', listener)`。
|
|
50
|
+
|
|
51
|
+
`buildNatsClientOptions` 的选项:
|
|
52
|
+
|
|
53
|
+
| 选项 | 默认值 | 含义 |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `servers` | 必填 | 服务器地址,或地址列表(`'nats://127.0.0.1:4222'`) |
|
|
56
|
+
| `user`、`pass` / `token` | | 认证信息 |
|
|
57
|
+
| `name` | | 连接的名字,服务器上可以看到 |
|
|
58
|
+
| `codec` | `messagePackCodec()` | 数据的编码方式(第 6 节) |
|
|
59
|
+
| `timeout` | `20000` | 第一次连接最多等待多少毫秒 |
|
|
60
|
+
| `reconnect`、`maxReconnectAttempts` | `true`、`10` | 是否重连,以及每个服务器最多重连几次 |
|
|
61
|
+
| `pingInterval`、`maxPingOut` | `120000`、`2` | 多久 ping 一次服务器,以及最多允许几次 ping 没有回应 |
|
|
62
|
+
| `noEcho` | `false` | 不接收本连接自己发布的消息 |
|
|
63
|
+
| `noRandomize` | `false` | 按顺序而不是随机地尝试服务器 |
|
|
64
|
+
| `debug` | `false` | 打印协议内容 |
|
|
65
|
+
| `bulk`、`bulkBucket`、`bulkTTL`、`bulkReplicas` | | 大数据传输(第 7 节) |
|
|
66
|
+
|
|
67
|
+
组件不支持 TLS,也不支持 NKey 或 JWT 认证。
|
|
68
|
+
|
|
69
|
+
## 2. 提供服务动作
|
|
70
|
+
|
|
71
|
+
`natsServiceEntrypoint(natsComponentName, options?)` 是入口的一种 `service` 适配器:它接收发给本应用的请求,调用应用里的 `@ServiceAction` 方法。
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import {Application, DTO} from 'lakutata'
|
|
75
|
+
import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
|
|
76
|
+
import {ServiceAction} from 'lakutata/decorator/ctrl'
|
|
77
|
+
import {Expect} from 'lakutata/decorator/dto'
|
|
78
|
+
import {buildNatsClientOptions, natsServiceEntrypoint} from '@lakutata/nats'
|
|
79
|
+
|
|
80
|
+
class GetOrderDTO extends DTO {
|
|
81
|
+
@Expect(DTO.Number().integer().required())
|
|
82
|
+
public id: number
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
class OrderController extends Controller {
|
|
86
|
+
@ServiceAction({cmd: 'order.get'}, GetOrderDTO)
|
|
87
|
+
public async get(input: GetOrderDTO): Promise<{ id: number, total: number }> {
|
|
88
|
+
return {id: input.id, total: 42}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
Application.run(() => ({
|
|
93
|
+
//请求的主题,也是代理使用的 serviceId
|
|
94
|
+
id: 'order.service',
|
|
95
|
+
name: 'OrderService',
|
|
96
|
+
components: {
|
|
97
|
+
nats: buildNatsClientOptions({servers: 'nats://127.0.0.1:4222'}),
|
|
98
|
+
entrypoint: buildEntrypoints({
|
|
99
|
+
controllers: [OrderController],
|
|
100
|
+
service: natsServiceEntrypoint('nats')
|
|
101
|
+
})
|
|
102
|
+
},
|
|
103
|
+
bootstrap: ['entrypoint']
|
|
104
|
+
}))
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- 第一个参数是 NATS 组件的名字。
|
|
108
|
+
- 请求发到以应用 id 命名的主题上(`order.service`);`subject` 选项可以换成别的主题。
|
|
109
|
+
- 服务的各个实例加入同一个队列组(`order.service.serviceQueue`,或 `queue` 选项指定的名字),所以每个请求只由一个实例处理。
|
|
110
|
+
- 不等待响应、直接发布的消息也会执行对应的动作,只是不会有回复。
|
|
111
|
+
- 应用停止时,适配器不再接收新请求,正在处理的请求会处理完。调用正在重启的服务时,调用方会重试请求(见第 3 节)。
|
|
112
|
+
|
|
113
|
+
适配器还接受 `middlewares`(在入口的中间件之后执行)和 `name`。
|
|
114
|
+
|
|
115
|
+
## 3. 调用服务:服务代理
|
|
116
|
+
|
|
117
|
+
`ServiceProxy` 用来调用某个远端服务的动作,并监听它的事件。它是一个单例提供者:每个服务用 `buildServiceProxy` 声明一个,放在 `providers` 下。
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import {Application} from 'lakutata'
|
|
121
|
+
import {
|
|
122
|
+
buildNatsClientOptions,
|
|
123
|
+
buildServiceProxy,
|
|
124
|
+
NatsRequestTimeoutException,
|
|
125
|
+
ServiceInvokeException,
|
|
126
|
+
type ServiceProxy
|
|
127
|
+
} from '@lakutata/nats'
|
|
128
|
+
|
|
129
|
+
Application
|
|
130
|
+
.run(() => ({
|
|
131
|
+
id: 'billing.app',
|
|
132
|
+
name: 'Billing',
|
|
133
|
+
components: {
|
|
134
|
+
nats: buildNatsClientOptions({servers: 'nats://127.0.0.1:4222'})
|
|
135
|
+
},
|
|
136
|
+
providers: {
|
|
137
|
+
orderService: buildServiceProxy({natsComponentName: 'nats', serviceId: 'order.service'})
|
|
138
|
+
}
|
|
139
|
+
}))
|
|
140
|
+
.onLaunched(async (app: Application): Promise<void> => {
|
|
141
|
+
const orderService: ServiceProxy = await app.getObject<ServiceProxy>('orderService')
|
|
142
|
+
try {
|
|
143
|
+
//动作的模式和输入放在同一个对象里,超时 30 秒
|
|
144
|
+
console.log(await orderService.invoke({cmd: 'order.get', id: 1}, 30000))
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (error instanceof ServiceInvokeException) console.error(error.service, error.errno, error.message)
|
|
147
|
+
else if (error instanceof NatsRequestTimeoutException) console.error('No response in time')
|
|
148
|
+
else throw error
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`invoke(input, timeout)` 返回动作的结果。timeout 默认 10 分钟;服务没有运行时调用会立即失败,所以 timeout 只用来限制太慢的动作。
|
|
154
|
+
|
|
155
|
+
- 如果服务没有任何实例收到请求(服务没有运行,或者正在重启),代理会重新发送,最多 `maxRetries` 次(默认 3 次),每次间隔 `retryDelay` 毫秒(默认 100),之后抛出 `NatsNoRespondersAvailableException`。
|
|
156
|
+
- 超时和动作失败都不会重试:动作可能已经执行过了,不能执行两次。
|
|
157
|
+
- **超时不会中止服务端的动作**:调用方不再等待,但动作会一直执行完。这一点与 gRPC 不同,gRPC 的截止时间会中止动作。
|
|
158
|
+
|
|
159
|
+
`buildServiceProxy` 的选项有 `natsComponentName`、`serviceId`、`maxRetries` 和 `retryDelay`。
|
|
160
|
+
|
|
161
|
+
## 4. 事件
|
|
162
|
+
|
|
163
|
+
服务可以向监听它的代理发送事件:
|
|
164
|
+
|
|
165
|
+
- NATS 组件的 `nats.emitServiceEvent(event, ...args)`,在应用 id 下发送;
|
|
166
|
+
- `entrypoint.broadcast(event, ...args)`,在适配器的主题下发送。它返回 0:NATS 不会告诉发送方有多少人收到了消息。如果适配器的 `subject` 选项不是应用 id,`serviceId` 为应用 id 的代理只会收到 `emitServiceEvent` 发出的事件。
|
|
167
|
+
|
|
168
|
+
`entrypoint.notify(clientId, ...)` 到达不了 NATS 的调用方,因为它们没有客户端 id。
|
|
169
|
+
|
|
170
|
+
另一端用 `orderService.on(event, listener)`、`once` 和 `off` 监听服务的事件。用 `on(event, listener, true)` 注册时,在所有应用中这样注册的监听器里,每个事件只发给其中一个(队列组)。事件最多送达一次:发送时没有连接的应用会错过它。代理被销毁时,它注册的监听器也会被移除。
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
import type {ServiceProxy} from '@lakutata/nats'
|
|
174
|
+
|
|
175
|
+
export function watchPayments(orderService: ServiceProxy): void {
|
|
176
|
+
orderService.on('orderPaid', (id: number, total: number): void => console.log(`Order ${id} paid: ${total}`))
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## 5. 错误
|
|
181
|
+
|
|
182
|
+
| 异常 | errno | statusCode | 什么时候 |
|
|
183
|
+
|---|---|---|---|
|
|
184
|
+
| `ServiceInvokeException` | 远端错误的 errno | 远端错误的 statusCode | 动作失败。消息和属性来自远端错误,`service` 是服务 id |
|
|
185
|
+
| `NatsNoRespondersAvailableException` | `E_NATS_NO_RESPONDERS_AVAILABLE` | 503 | 重试之后仍然没有实例收到请求 |
|
|
186
|
+
| `NatsRequestTimeoutException` | `E_NATS_REQUEST_TIMEOUT` | 408 | 超时前没有收到响应 |
|
|
187
|
+
| `NatsBulkException` | `E_NATS_BULK` | 502 | 大数据无法存入或读取(第 7 节) |
|
|
188
|
+
|
|
189
|
+
它们都由 `@lakutata/nats` 导出。远端错误带着它的全部属性,包括调用栈。
|
|
190
|
+
|
|
191
|
+
## 6. 编码方式
|
|
192
|
+
|
|
193
|
+
所有数据都用组件的 `codec` 编码,同一个主题的两端必须相同:
|
|
194
|
+
|
|
195
|
+
- `messagePackCodec()`,默认值:MessagePack,紧凑而且快;
|
|
196
|
+
- `jsonCodec()`:JSON 文本,方便其他语言写的客户端使用。
|
|
197
|
+
|
|
198
|
+
两者都会先把值转成 JSON,让所有客户端收到的都是普通的 JSON 数据:`Date` 变成 ISO 字符串,`Buffer` 变成 `{type: 'Buffer', data: [...]}`,类的实例变成普通对象,`undefined` 变成 `null`。`bigint` 无法发送。任何带有 `encode(value): Uint8Array` 和 `decode(bytes)` 的对象都可以作为编码方式,比如用于文本数据的 `StringCodec()`。
|
|
199
|
+
|
|
200
|
+
## 7. 大数据传输
|
|
201
|
+
|
|
202
|
+
一条 NATS 消息的大小不能超过服务器的 `max_payload`,默认是 1 MiB。达到这个大小 90% 的请求和响应会被存进 JetStream 的对象存储,消息里只带一个引用;接收方读取对象后把它删除。回调、动作和 `invoke()` 拿到的始终是完整的值。
|
|
203
|
+
|
|
204
|
+
- 这个通道默认开启(`bulk: true`),需要 JetStream。没有 JetStream 时,组件会打印一条警告,然后不带它启动:这时只有超过 `max_payload` 的数据会失败。
|
|
205
|
+
- 互相传输大数据的应用必须使用同一个对象存储:给它们设置相同的 `bulkBucket`。默认的名字取决于 `name` 选项(`lkt-bulk-<name>`,没有 name 时是 `lkt-bulk-default`),所以 name 不同的两个应用读不到对方的对象。
|
|
206
|
+
- 没有被读取的对象会在 `bulkTTL`(默认 5 分钟)后过期。`bulkReplicas` 设置集群中的副本数。这两项在创建对象存储时生效。
|
|
207
|
+
- 发布的消息和事件从不经过对象存储:超过 `max_payload` 时,它们会以 NATS 的错误 `MAX_PAYLOAD_EXCEEDED` 失败。
|