@coda-console/kernel 0.0.0 → 0.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 +17 -2
- package/docs/design.md +402 -0
- package/kernel.mjs +1 -0
- package/package.json +20 -3
- package/sim/host.mjs +256 -0
- package/sim/scenarios.mjs +484 -0
- package/sim/solo.mjs +47 -0
- package/sim/world.mjs +793 -0
- package/src/kernel.mjs +1038 -0
- package/src/schema.mjs +169 -0
- package/src/transaction.mjs +75 -0
package/README.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
-
#
|
|
1
|
+
# coda-kernel
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Coda 的调用内核:嵌在宿主进程里的库,只管幂等调用与责任转移。
|
|
4
|
+
|
|
5
|
+
- 它做什么、怎么做:[`docs/design.md`](docs/design.md)。
|
|
6
|
+
- 为什么有它、在 Coda 里怎么接:[coda#2449](https://github.com/coda-console/coda/issues/2449)。
|
|
7
|
+
|
|
8
|
+
状态:0.1。设计里的接口都已实现:事务与输出门、表与迁移、调用的主路径、状态更新、调用树与根运行、取消、重读、放弃、回答、定时、`when` 与 `pending`,都有模拟测试守着。设计第 13 节列的现在不做。
|
|
9
|
+
|
|
10
|
+
## 开发
|
|
11
|
+
|
|
12
|
+
需要 Node 24。没有依赖,没有构建步骤。
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm test
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
改动之前先读 [`AGENTS.md`](AGENTS.md)。
|
package/docs/design.md
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
# 调用内核设计
|
|
2
|
+
|
|
3
|
+
状态:0.x 草案。带「定」字的条目是 [coda#2449](https://github.com/coda-console/coda/issues/2449) 里用户确认过的,改它们须用户认可;其余可以改,改时在 PR 里写明理由。实现与本文不一致时,以本文为准;要改行为,先改本文。
|
|
4
|
+
|
|
5
|
+
## 1. 它做什么
|
|
6
|
+
|
|
7
|
+
让「谁请谁做了什么」落库并且可追责。任何时刻都答得出:谁请的谁、责任现在在谁手里、结果是什么。崩溃、重启、离线、升级都不丢,也不重做。
|
|
8
|
+
|
|
9
|
+
它不做,这些都在它外面:
|
|
10
|
+
|
|
11
|
+
- 重试与否的决定、权限判断、寻址与网络。
|
|
12
|
+
- 记录与 watch。宿主读自己的业务数据不经过内核。
|
|
13
|
+
- 按键排队、任何业务概念。
|
|
14
|
+
|
|
15
|
+
## 2. 词
|
|
16
|
+
|
|
17
|
+
| 词 | 含义 |
|
|
18
|
+
|---|---|
|
|
19
|
+
| 地址 | 一个参与方的名字,不透明的字符串。内核只比较是否相等。地址是参与方这一级的;它名下具体哪条记录,写在调用的内容里 |
|
|
20
|
+
| 实例 | 绑在一条 SQLite 连接上的一份内核 |
|
|
21
|
+
| 本地地址 | 在这个实例上登记了处理方或结果处理函数的地址 |
|
|
22
|
+
| 调用 | 一个地址请另一个地址做一个操作。身份是(调用方地址,请求号) |
|
|
23
|
+
| 运行 | 被调用方收下一次调用之后的那条记录。身份同它的调用 |
|
|
24
|
+
| 根运行 | 不是谁请的、处理方自己起的运行(定时、用户、外部事件引起的)。身份是(自己的地址,自己起的号) |
|
|
25
|
+
| 提问号 | 运行每提一次问,加一。回答带着它,说明答的是哪一次 |
|
|
26
|
+
| 信封 | 实例之间递的一条消息 |
|
|
27
|
+
| 回执 | 收信的一方对一封信的当场答复 |
|
|
28
|
+
| 通道 | 调用方给调用起的一个名字。同一个实例里、同一个通道的调用,按写下的先后送达 |
|
|
29
|
+
| 宿主 | 嵌着内核的那个程序 |
|
|
30
|
+
|
|
31
|
+
## 3. 形态与前提
|
|
32
|
+
|
|
33
|
+
形态(定):
|
|
34
|
+
|
|
35
|
+
- 内核是库,嵌在宿主进程里。一个实例对应一条连接,表建在宿主的库里,表名以 `kernel_` 开头。这些表是内核私有的,宿主只经内核的函数读写。
|
|
36
|
+
- 宿主给它:数据库连接(`node:sqlite` 的 `DatabaseSync`,或同样形状的对象)、时钟 `now()`(毫秒)、送信 `send(地址, 信封)`,以及可选的 `onError`、`wake`、`schedule`。
|
|
37
|
+
- 内核只认识自己的表、地址、不透明的内容和时钟。「本机」「跨机」不是它的词:往哪里送、怎么送,是 `send` 背后的事。
|
|
38
|
+
- 没有依赖,只用 Node 自带的模块。
|
|
39
|
+
|
|
40
|
+
内核依赖宿主守住的前提:
|
|
41
|
+
|
|
42
|
+
- **P1** 同一时刻,一个地址只在一个实例上登记处理方。地址搬到另一个实例,0.x 不支持。
|
|
43
|
+
- **P2** 请求号由发起的一方随机生成(如 UUID),不用会回退的计数器。
|
|
44
|
+
- **P3** 交给 `receive` 的信封,`from` 已经由外面核对过确实是对方的地址。内核相信它。
|
|
45
|
+
- **P4** `send` 返回 Promise,并且一定会结束:成功给回执,送不到就拒绝。等多久算送不到由 `send` 自己定。
|
|
46
|
+
- **P5** 处理方都登记完,才调用 `recover()`。在那之前到的信,不论哪一种,`receive` 都回 `not_ready`。
|
|
47
|
+
- **P6** 这条连接上的事务都经 `transaction()` 开,宿主不自己写 `BEGIN` 或 `SAVEPOINT`。
|
|
48
|
+
|
|
49
|
+
## 4. 事务与输出门
|
|
50
|
+
|
|
51
|
+
- **T1 唯一入口。** `transaction(fn)` 是改表的唯一入口:`BEGIN IMMEDIATE`,同步执行 `fn`,`COMMIT`。`fn` 返回 thenable 就抛错并回滚。嵌套调用是保存点:里层抛错只回滚里层,再把错抛给外层。
|
|
52
|
+
- **T2 只在事务里改表。** 内核里会改表的函数,不在事务里调用就抛错。宿主在同一个事务里改自己的表,两边一起提交。反过来,`receive`、`answer`、`inquire`、`pump`、`tick`、`recover` 自己开事务,在事务里调用它们就抛错。
|
|
53
|
+
- **T3 输出门。** 效果只在最外层提交之后放出,回滚则丢弃,里层回滚丢弃里层登记的。放出的有两样:
|
|
54
|
+
- `afterCommit(fn)` 登记的函数:提交之后、`transaction()` 返回之前,按登记的先后同步执行。其中一个抛错不影响其余的,也不从 `transaction()` 抛出,交给 `onError`。
|
|
55
|
+
- 信封:提交之后由内核的发送循环送出。循环在下一个微任务开始,从发件箱这张表里读要送的,不靠内存里的登记。
|
|
56
|
+
- **T4 放出的东西不落库。** 提交之后才做的事只存在内存里,崩溃就没了。凡是崩溃后仍然必须发生的事,都要能从表里重新推出来:内核自己的靠发件箱和定时;宿主的靠启动时重扫自己名下的运行与调用(`runs()`、`calls()`)。
|
|
57
|
+
- **T5 提交失败等于没发生。** `COMMIT` 抛错时什么都不放出,`transaction()` 把错抛给宿主。
|
|
58
|
+
- **T6 回调抛错不拖垮记账。** 内核调用宿主登记的函数时,把它放在保存点里。它抛出意料之外的错:它自己的改动回滚,内核自己的记账照常提交,错交给 `onError`。各个函数抛错之后怎么办,写在下面各条规则里。
|
|
59
|
+
|
|
60
|
+
一起提交的是「我的业务数据,加上我欠出去的东西」。双方的业务数据从不在同一个事务里改,即使它们在同一个实例上。
|
|
61
|
+
|
|
62
|
+
发给本地地址的信封同样先写进发件箱。发送循环交付它时开一个事务:删掉发件箱里这一行,把信交给本地的处理方,并照回执更新发信一方在内核里的那一行,三件事一起提交。这个事务里只调用收信一方的处理函数:对方当场给了结局时,发信一方的调用先记成 `open`,结局由结局的信在它自己的事务里送达;对方不收(`conflict`、`unsupported`)时,对方的处理函数没有被调用,发信一方照常写下失败并调用结果处理函数。循环逐封交付,不递归;每交付若干封经 `schedule` 让出一次事件循环。`idle()` 返回一个 Promise,在循环没有可送的信时完成。
|
|
63
|
+
|
|
64
|
+
## 5. 一次调用的一生
|
|
65
|
+
|
|
66
|
+
```mermaid
|
|
67
|
+
sequenceDiagram
|
|
68
|
+
participant A as 调用方
|
|
69
|
+
participant B as 被调用方
|
|
70
|
+
A->>B: 调用
|
|
71
|
+
B-->>A: 回执:收下了
|
|
72
|
+
B-->>A: 状态,可以有很多次
|
|
73
|
+
B-->>A: 结局
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 5.1 调用方一侧
|
|
77
|
+
|
|
78
|
+
`call()` 在调用方的事务里写一行调用,并在发件箱里欠下一封信。
|
|
79
|
+
|
|
80
|
+
| 状态 | 含义 |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `sending` | 还不知道对方收下没有 |
|
|
83
|
+
| `open` | 对方收下了。另记最近听到的状态:`working` 或 `input-required` |
|
|
84
|
+
| `completed`、`failed`、`canceled` | 结局。只写一次 |
|
|
85
|
+
|
|
86
|
+
### 5.2 被调用方一侧
|
|
87
|
+
|
|
88
|
+
信封到了,内核开一个事务,先写下运行这一行,再把调用交给登记的处理方的 `accept`。交给它的内容是:`from`、`id`、`to`、`op`、`input`、`awaited`、`parent`。处理方在这个事务里做自己的记账,并给出四种回应之一:
|
|
89
|
+
|
|
90
|
+
| 回应 | 怎么表示 | 之后 |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| 给结局 | 返回一个结局 | 运行直接是结局。快的操作 |
|
|
93
|
+
| 留着 | 返回 `undefined` | 运行停在 `working`,之后由处理方在别的事务里推进 |
|
|
94
|
+
| 拒绝 | 抛出 `Rejected`(带 `code`、`message`) | 处理方的记账回滚;留下一条已结局的运行:`failed`,`effects` 为 `none`。同一请求再来,回这一条 |
|
|
95
|
+
| 暂时不收 | 抛出 `Busy` | 什么都不留。回执是 `busy`,信留在对方的发件箱里,以后再送 |
|
|
96
|
+
|
|
97
|
+
处理方抛出别的错:记账回滚,留下一条 `failed` 的运行(`code` 为 `handler_error`,`effects` 为 `none`),错交给 `onError`。没有登记处理方的地址或操作:留下一条 `failed` 的运行(`code` 为 `no_handler`,`effects` 为 `none`)。
|
|
98
|
+
|
|
99
|
+
| 状态 | 含义 |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `working` | 在做 |
|
|
102
|
+
| `input-required` | 在等外面的回答。带一段不透明的说明 `asking`(问的是什么)和提问号 |
|
|
103
|
+
| `completed`、`failed`、`canceled` | 结局。只写一次 |
|
|
104
|
+
|
|
105
|
+
### 5.3 规则
|
|
106
|
+
|
|
107
|
+
- **C1 幂等。** 同一(调用方地址,请求号)只算一次。
|
|
108
|
+
- 调用方重复发起:比 `to`、`op`、`input`、`parent` 四样。相同就返回已有的那一行,它可能已经有结局;不同就抛错。别的参数以第一次为准。
|
|
109
|
+
- 被调用方重复收到:不再交给处理方,回这条运行现在的样子;已有结局就把结局一起回。信封带着上面四样的摘要,和记下的不同就回 `conflict`。只因为取消而留下的记录没有摘要,不比,直接按它回。
|
|
110
|
+
- **C2 责任转移。** 对方回执确认之前,信一直留在调用方的发件箱里,反复送。确认的意思是对方的事务已经提交。收下之后,被调用方欠一个结局。任何时刻至少有一方记着这件事没完。
|
|
111
|
+
- **C3 结局唯一。** 结局写下之后,这一行不再变。对已有结局的运行调用 `update`、`settle` 会抛错。
|
|
112
|
+
- **C4 结局的送达(定)。** 被调用方在写下结局的同一个事务里,在发件箱里欠下一封结局的信,送到调用方确认为止。调用方收到时,内核开一个事务,写下结局,并在同一个事务里调用调用方登记的结果处理函数。结局的信带着运行最后的 `progress`。真相始终是被调用方的运行记录。细则:
|
|
113
|
+
- 收到某次调用的状态或结局,就说明对方已经收下:撤掉发件箱里那封调用,放行通道。换成了取消的:收到状态时等取消确认才放行;收到结局时按 C7 撤掉取消,放弃的调用除外。
|
|
114
|
+
- 经 `send` 拿到的回执里带着结局时,和收到结局的信一样处理。本地交付的回执见第 4 节。
|
|
115
|
+
- 结局和状态的信只认这次调用的被调用方发来的;别的地址发来的,回「收到」,丢弃,告诉 `onError`。
|
|
116
|
+
- 已有结局的调用再收到结局、状态或回执:回「收到」,不改这一行,不再调处理函数。放弃的调用例外,见 C12。
|
|
117
|
+
- 不认识的调用收到结局或状态:回「收到」,丢弃,告诉 `onError`。
|
|
118
|
+
- 结局不看版本号。根运行没有调用方,它的结局不送给谁。
|
|
119
|
+
- 结果处理函数抛错(T6):结局照常写下并提交。内核记着这一次还没处理成,在之后每次 `pump()` 和 `recover()` 时再调它,直到它提交。
|
|
120
|
+
- **C5 可重读(定)。** `inquire()` 对 `open` 的调用有用:把原来的调用带着 `confirmed` 再送一遍。这封信不占通道、不落库;没拿到回执,下次 `pump()` 再送。对方按 C1 回它现在的样子;对方没有这条记录就回 `unknown_call`,不执行,调用方把它写成 `failed`,`code` 为 `lost`,`effects` 为 `unknown`。启动恢复时,对所有 `open` 的调用做一次。`sending` 的调用不用重读,它的信还在发件箱里。
|
|
121
|
+
- **C6 状态更新。** 运行每变一次,版本号加一;每次进入 `input-required` 或 `asking` 变了,提问号加一。被调用方把最新的状态欠在发件箱里;同一条运行还没送出的旧状态被新的替换,写下结局时撤掉还没送出的状态。调用方只接受版本号更大的状态。状态可以丢、可以晚,结局不可以。
|
|
122
|
+
- 收下本身不另送状态,回执就是:调用方据回执把调用记成 `open`,状态 `working`,版本号 1。经 `send` 拿到的回执,`view` 的版本号比记下的大时,和收到状态的信一样处理。本地交付的回执不拿 `view` 更新状态:本地的状态信一定会到,交付的事务里也就不必调调用方的函数(S9)。根运行的状态不送给谁。
|
|
123
|
+
- `update` 没给的字段保持原样;`asking` 只在 `input-required` 时有意义,回到 `working` 就清掉。`settle` 没给 `progress` 时保留最后一次的。
|
|
124
|
+
- 状态的信只认这次调用的被调用方发来的,规则同 C4。不认识的 `status` 记成 `working`。
|
|
125
|
+
- 状态处理函数抛错(T6):状态照常记下,不重调。
|
|
126
|
+
- **C7 取消。** 只有调用方能取消自己的调用。
|
|
127
|
+
- 调用还是 `sending`:把发件箱里那封调用换成一封取消,占着它在通道里的位置,送到确认为止。确认之前通道一直堵着,放弃也不解堵。这封取消得到 `busy`、`not_ready` 之外的 `ok: false`,而调用还是 `sending`:调用写成 `failed`,`code` 同回执,`effects` 为 `unknown`(被换掉的那封调用可能已经送到),照常调结果处理函数。
|
|
128
|
+
- 调用是 `open`:欠下一封取消,不占通道。
|
|
129
|
+
- 调用有了结局:撤掉它还没送达的取消。放弃的调用例外,它的取消照常送到确认为止。
|
|
130
|
+
- 被调用方只认这次调用的调用方发来的取消;别的地址发来的,回「收到」,丢弃,告诉 `onError`。被调用方收到取消,看这条运行:
|
|
131
|
+
- 还没有结局:记下「已要求取消」;对它名下「在等」而没结束的调用各欠一封取消;撤掉它名下的全部定时;然后调用处理方的取消函数。
|
|
132
|
+
- 已有结局:回它现在的样子。
|
|
133
|
+
- 没有这条运行:留下一条 `canceled` 的记录,不交给处理方。同样欠一封结局的信。之后那封调用再到,按 C1 回这一条。
|
|
134
|
+
- 运行什么时候、以什么结局结束,由处理方决定。取消可以重复送,重复的不起作用,取消函数只调一次。
|
|
135
|
+
- 处理方自己要停一条运行(比如根运行),用 `stop(run)`:效果同收到取消,但不调用取消函数。之后真的取消到了,算重复。
|
|
136
|
+
- 取消函数抛错(T6):「已要求取消」照常记下,不重调。宿主重扫运行时看得到这个标记。
|
|
137
|
+
- **C8 回答。** 运行在 `input-required` 时,任何一方可以给它送回答。回答不落库,当场送一次:`answer()` 在事务之外调用,返回一个 Promise,值是对方的回执;`send` 被拒绝就照样拒绝,由回答的一方决定要不要用同一个号再送。回答方一侧不写库,不进发件箱,不重送。发给本地地址的回答由内核直接交付,不经 `send`,走和 `receive` 一样的路(没 `recover()` 过就是 `not_ready`)。
|
|
138
|
+
- 回答带着回答方的地址 `from`、回答方起的号 `id`、答的是哪条运行 `call`、提问号 `asked`。信封的 `to` 必须是那条运行的地址。
|
|
139
|
+
- 被调用方收到回答,在一个事务里依次判断:
|
|
140
|
+
|
|
141
|
+
| 情形 | 回执 |
|
|
142
|
+
|---|---|
|
|
143
|
+
| 没有这条运行,或运行的地址不是信封的 `to` | `{ ok: false, code: 'unknown_call' }` |
|
|
144
|
+
| (回答方地址,号)采用过 | `{ ok: true, applied: true, view }`,不再交给处理方 |
|
|
145
|
+
| 运行不在 `input-required`,或提问号不等于运行现在的提问号 | `{ ok: true, applied: false, view }` |
|
|
146
|
+
| 没有登记回答函数 | `{ ok: false, code: 'rejected', error: { code: 'no_handler', message } }` |
|
|
147
|
+
| 回答函数抛 `Rejected` | `{ ok: false, code: 'rejected', error: { code, message } }` |
|
|
148
|
+
| 回答函数抛别的错 | `{ ok: false, code: 'handler_error' }`,错交给 `onError` |
|
|
149
|
+
| 采用 | `{ ok: true, applied: true, view }`,`view` 是回答函数跑完之后的样子 |
|
|
150
|
+
|
|
151
|
+
- 采用:在一个保存点里,先把运行改回 `working`(同 `update`:版本号加一、清掉 `asking`、欠一封状态的信,根运行不欠),记下这个回答的号,再调用回答函数 `answer(run, { from, id, asked, answer })`。`run` 同 `runs()` 的一项,此时已是 `working`。回答函数里可以 `update`、`settle`;要接着问,就再 `update` 成 `input-required`,这是新的一问。它抛错,保存点整个回滚:运行还是提问时的样子,号也没记。所以一次提问至多采用一个回答(S8)。
|
|
152
|
+
- 按(回答方地址,号)去重,只记号码和它答的是哪条运行的第几问,不记内容。回答方的号同 P2:随机生成,不跨运行复用。
|
|
153
|
+
- 回答函数的找法同取消函数:运行的地址和 `op`。
|
|
154
|
+
- 谁有资格回答,内核不管。
|
|
155
|
+
- **C9 调用树。** 一条运行在处理中发出的调用,用 `by` 记着它是由哪条运行发出的,用 `awaited` 记着发出的一方是不是「在等」它。
|
|
156
|
+
- 名下还有「在等」而没结束的调用时,不许给结局:`settle()` 抛错;`accept` 返回结局也算,按 `handler_error` 处理。处理方先等它们结束,或者对它们 `detach()`。
|
|
157
|
+
- `detach()` 表示不等了。对方不知道,照常做完;结局到了照常交给结果处理函数,并标明已经不在等。
|
|
158
|
+
- `by` 必须是这个实例上的一条运行,并且这条运行的地址就是这次调用的 `from`;由已有结局的运行发出「在等」的调用,`call()` 抛错。
|
|
159
|
+
- 已要求取消的运行再发出的调用,一律记成不在等。
|
|
160
|
+
- **C10 通道保序。** 同一个实例里、同一个通道的调用,按写进发件箱的先后送:前一封得到确认之前,后一封不送。不同通道互不影响。状态、结局、不占通道的取消,不被调用挡住。通道的名字由宿主起;把发往不同地址的调用放进同一个通道,它们会互相挡住。
|
|
161
|
+
- 回执是 `busy`、`not_ready`,或者 `send` 被拒绝:这封信留着,等下一次 `pump()` 再送,不立刻重送。
|
|
162
|
+
- **C11 失败要说清做了多少。** 失败的结局带 `code`、`message` 和 `effects`:`none` 表示什么都没发生,可以放心再来;`unknown` 表示说不清;`some` 表示做了一部分。不写按 `unknown`。可以另带 `retryAfterMs`:从调用方写下这个结局的那一刻算起,过多久再试才有意义。内核只存、只传,不换算,也不据此做任何事。
|
|
163
|
+
- 被调用方写下 `failed`(`settle()`,或 `accept` 返回):`error` 没给(`undefined` 或 `null`),按 `{ effects: 'unknown' }`;是对象而没写 `effects`,补上 `unknown`;`effects` 不是 `none`、`unknown`、`some` 之一,或者 `error` 不是对象,抛错。这和不认识的 `outcome` 一样是处理方写错了,`accept` 返回的按 `handler_error` 处理。
|
|
164
|
+
- 调用方记下对方给的 `failed`(结局的信或回执,晚到的结局也一样):`error` 没给(`undefined` 或 `null`),记成 `{ effects: 'unknown' }`;别的不是对象的值,记成 `{ effects: 'unknown', original: 收到的 }`;`effects` 没写或不认识,记成 `unknown`,别的字段原样。这不算内核自己写的,不带 `local`。`retryAfterMs` 不碰。
|
|
165
|
+
- **C12 放弃。** 对方迟迟不回时,调用方可以 `abandon()`:本地把调用写成 `failed`,`code` 为 `abandoned`,`effects` 为 `unknown`,并按 C7 送取消。放弃算结束(C9)。`abandon()` 本身不调用结果处理函数。放弃之后对方的真结局才到,不论是结局的信还是回执带回来的:记在这一行「晚到的结局」里,只记一次,并调用一次结果处理函数里的 `late`;`late` 抛错的处理同 C4 最后一条。什么时候放弃由宿主决定。
|
|
166
|
+
|
|
167
|
+
### 5.4 内核不重跑任何东西(定)
|
|
168
|
+
|
|
169
|
+
处理方的每一次被调用都在一个事务里:要么提交了,要么等于没发生过、信会再送来。所以内核里没有「做到一半」。
|
|
170
|
+
|
|
171
|
+
被调用方在事务之外做的事(起进程、等别人)断了怎么办,是处理方自己的事:它在启动时重扫自己名下还在 `working` 的运行,决定续上、报失败,还是报 `effects: unknown`。内核不按日志重放代码,状态都在行里。
|
|
172
|
+
|
|
173
|
+
## 6. 信封
|
|
174
|
+
|
|
175
|
+
信封是 JSON。公共字段:`v`(现在是 1)、`kind`、`from`、`to`,以及可选的 `must`。
|
|
176
|
+
|
|
177
|
+
| `kind` | 谁发给谁 | 另带的字段 |
|
|
178
|
+
|---|---|---|
|
|
179
|
+
| `call` | 调用方 → 被调用方 | `id`、`op`、`input`、`digest`、`awaited`、`parent`、`confirmed` |
|
|
180
|
+
| `status` | 被调用方 → 调用方 | `call: { from, id }`、`rev`、`status`、`asked`、`asking`、`progress` |
|
|
181
|
+
| `result` | 被调用方 → 调用方 | `call: { from, id }`、`outcome`、`result` 或 `error`、`progress` |
|
|
182
|
+
| `cancel` | 调用方 → 被调用方 | `call: { from, id }`、`reason` |
|
|
183
|
+
| `answer` | 回答方 → 被调用方 | `id`、`call: { from, id }`、`asked`、`answer` |
|
|
184
|
+
|
|
185
|
+
`parent` 是调用方给的一段不透明内容,说明这次调用是替谁发的;内核原样带到被调用方的运行上。`awaited` 告诉被调用方对面是不是在等。`asked` 是提问号。通道不进信封,它只管发的一方的先后。
|
|
186
|
+
|
|
187
|
+
回执:
|
|
188
|
+
|
|
189
|
+
- `{ ok: true, view }`:对方的事务已提交。`view` 是那条运行现在的样子:`status`、`rev`、`asked`、`asking`、`progress`,有结局时带 `outcome` 与 `result` 或 `error`。回答的回执另带 `applied`。
|
|
190
|
+
- `{ ok: false, code }`:这封信没有留下记录。
|
|
191
|
+
|
|
192
|
+
| `code` | 哪种信会得到 | 含义 | 发的一方怎么办 |
|
|
193
|
+
|---|---|---|---|
|
|
194
|
+
| `not_ready` | 任何一种 | 对方还没启动完 | 信留着,下次 `pump()` 再送 |
|
|
195
|
+
| `busy` | `call` | 处理方现在不收 | 同上 |
|
|
196
|
+
| `conflict` | `call` | 同一个号,内容不同 | 调用写成 `failed`,`effects` 为 `none` |
|
|
197
|
+
| `unknown_call` | 带 `confirmed` 的 `call`、`answer` | 对方没有这条记录;`answer` 还包括运行的地址不是信封的 `to` | `call`:调用写成 `failed`,`code` 为 `lost`,`effects` 为 `unknown`。`answer`:交还给回答的一方 |
|
|
198
|
+
| `unsupported` | 任何一种 | 不认识的 `kind`、`v`,或 `must` 里有不认识的字段 | `call`:写成 `failed`,`effects` 为 `none`。`cancel`:它换掉的调用还是 `sending`,调用写成 `failed`,`effects` 为 `unknown`(C7);否则同别的信。别的信:丢弃,告诉 `onError` |
|
|
199
|
+
| `rejected`、`handler_error` | `answer` | 回答没被接受;`rejected` 另带 `error: { code, message }` | 交还给回答的一方 |
|
|
200
|
+
|
|
201
|
+
`send` 被拒绝:没送到,信留在发件箱里,下次 `pump()` 再送。
|
|
202
|
+
|
|
203
|
+
重读得到 `unknown_call` 之外的 `ok: false`(`conflict`、`unsupported` 等):调用不动,告诉 `onError`。
|
|
204
|
+
|
|
205
|
+
**演进规则(定)**:
|
|
206
|
+
|
|
207
|
+
- 只加字段,不改含义。读到不认识的字段就忽略。
|
|
208
|
+
- 新字段如果对方不认识就不能照做,把字段名列进 `must`;`must` 里有不认识的就回 `unsupported`。
|
|
209
|
+
- 不认识的枚举值按最保守的处理:`status` 按 `working`,`effects` 按 `unknown`,`outcome` 按 `failed` 并保留原文,原文放在 `error.original`。
|
|
210
|
+
- `v` 只在不兼容时加一。认 `v: 1` 的实现必须认识上面五种 `kind`。
|
|
211
|
+
- 1.0 冻结的首先是这一节。
|
|
212
|
+
|
|
213
|
+
## 7. 定时
|
|
214
|
+
|
|
215
|
+
`after({ from, id, delayMs, to, op, input, lane, by, awaited, parent, memo })` 在事务里记一行定时:过一段时间之后,请某个地址做某个操作。`delayMs` 须是不小于 0 的有限数,到点时间是登记那一刻的 `now()` 加上 `delayMs`。`by`、`awaited` 的检查同 `call()`(C9,包括已要求取消的运行再登记的一律记成不在等)。返回 `{ from, id, to, op, state: 'scheduled', dueAt, memo, by, awaited }`。
|
|
216
|
+
|
|
217
|
+
- **共用请求号。** `after()` 和 `call()` 都按 C1 同时查定时表和调用表:同号同内容(四样的摘要)就返回已有的那一行,不管它是定时还是调用,别的参数以第一次为准;内容不同就抛错。所以一个号在两张表里至多出现一次。
|
|
218
|
+
- **到点。** `now()` 不小于到点时间就算到点。`tick()` 在事务之外调用,它在一个事务里把所有到点的定时变成调用:删掉定时,按 `call()` 的写法写调用和信,`by`、`awaited`、`lane`、`memo` 原样带过去,这时不再查 `by`。先后按到点时间,同时到点的按登记的先后。这个事务不调用任何处理函数。没有到点的定时就不开事务。之后同第 5 节:到点的事至少被尝试一次,被收下恰好一次。
|
|
219
|
+
- **撤与改。** 还在定时表里的定时:`unschedule(ref)`、`cancel(ref)`、`abandon(ref)` 都是撤掉它,不留痕迹,不调用结果处理函数,这个号可以再用;`detach(ref)` 把它改成不在等;`inquire(ref)` 什么都不做。`unschedule` 对已经变成调用的不动;两张表里都没有,这几个函数都抛错。
|
|
220
|
+
- **调用树。** 标着「在等」、还在定时表里的定时,同「在等」而没结束的调用一样拦着 `settle()`(C9)。运行被要求取消(收到取消或 `stop`)的那个事务里,撤掉它名下(`by` 是它)的全部定时,在等不在等、到没到点都撤(C7)。之后它再登记的定时记成不在等,不受这条影响。
|
|
221
|
+
- **时钟。** 时间只来自宿主给的 `now()`。它须跨重启可比(墙上时钟),因为到点时间落库。时钟回拨只让定时晚到。
|
|
222
|
+
- **唤醒。** 最早的到点时间和上次告诉宿主的不一样时,内核在提交之后调用 `wake(到点时间)`;最后一个定时没了,调用 `wake(null)`。每次 `tick()` 结束时只要还有定时,不论变没变都再调一次:宿主的计时器刚响过,它的时钟也可能早一点。「上次告诉的」只在内存里,初值是没有,同 `null`(宿主手里没有计时器);`recover()` 之前不调,`recover()` 里的 `tick()` 照此。宿主据此设一个内存里的计时器,到时调用 `tick()`。`wake` 抛错交给 `onError`;没给 `wake` 就不调。
|
|
223
|
+
|
|
224
|
+
## 8. 接口(草案)
|
|
225
|
+
|
|
226
|
+
入口是 `kernel.mjs`,导出 `createKernel`、`Rejected`、`Busy`。
|
|
227
|
+
|
|
228
|
+
```js
|
|
229
|
+
const kernel = createKernel({ db, now, send, onError, wake, schedule });
|
|
230
|
+
// onError 默认 console.error;schedule 默认 setImmediate;wake(dueAt | null) 可选(第 7 节)
|
|
231
|
+
|
|
232
|
+
kernel.transaction(() => kernel.migrate()); // 建表或升级 kernel_* 表,库比代码新就抛错(第 9 节);它改表,所以按 T2 在事务里调用
|
|
233
|
+
kernel.transaction(fn); // T1
|
|
234
|
+
kernel.afterCommit(fn); // T3
|
|
235
|
+
|
|
236
|
+
// 被调用方
|
|
237
|
+
kernel.handle(address, op, { accept, cancel, answer });
|
|
238
|
+
// accept(call):call 是 { from, id, to, op, input, awaited, parent }
|
|
239
|
+
// 返回一个结局、或 undefined(留着),或抛 new Rejected(code, message)、new Busy()
|
|
240
|
+
// 结局:{ outcome: 'completed', result } | { outcome: 'failed', error } | { outcome: 'canceled' }
|
|
241
|
+
// cancel(run, reason):run 同 runs() 的一项,reason 是取消的信带来的;在收到取消的事务里、一个保存点里被调用,
|
|
242
|
+
// 每条运行至多一次;抛错不重调(C7)
|
|
243
|
+
// answer(run, { from, id, asked, answer }):run 同 runs() 的一项,此时已是 working;在采用回答的保存点里被调用;
|
|
244
|
+
// 抛 Rejected 是不接受,抛别的错回执是 handler_error,两种都整个回滚(C8)
|
|
245
|
+
kernel.update(run, { status, asking, progress }); // C6;没给的字段保持原样
|
|
246
|
+
kernel.settle(run, outcome, { progress }); // 给结局(C3、C9);outcome 只能是三种结局之一,没给 progress 就保留最后一次的
|
|
247
|
+
kernel.root({ address, op, id, parent }); // 起一条根运行,返回同 runs() 的一项;op 用来找它的回答函数;号已用过就抛错
|
|
248
|
+
kernel.stop(run); // C7:同收到取消,但不调用取消函数;已有结局的运行不受影响,没有这条运行就抛错
|
|
249
|
+
kernel.runs({ address, status }); // 处理方收下过、还没结局的运行和根运行;status 只在 working、input-required 里再筛
|
|
250
|
+
// 每项:{ from, id, to, op, parent, awaited, cancelAsked, status, rev, asked, asking, progress },根运行另带 root: true;
|
|
251
|
+
// cancelAsked 是「已要求取消」
|
|
252
|
+
|
|
253
|
+
// 调用方
|
|
254
|
+
kernel.call({ from, id, to, op, input, lane, by, awaited, parent, memo }); // by 是发出它的运行的 ref(C9)
|
|
255
|
+
// 返回这一行调用:{ from, id, to, op, state, memo, by, awaited };open 时另带最近听到的 status、rev、asked、asking、progress;
|
|
256
|
+
// 有结局时另带 outcome、result 或 error、progress,结局是内核自己写的(不是对方给的)时另带 local: true;
|
|
257
|
+
// 放弃之后对方的结局到了,另带 late: { outcome, result 或 error, progress }
|
|
258
|
+
// 这个号是一个还在定时表里的定时时,返回那个定时(同 after() 的返回值,第 7 节)
|
|
259
|
+
kernel.results(address, { status, result, late }); // 各在自己的事务里被调用
|
|
260
|
+
// status(call):在记下状态的事务里、一个保存点里被调用;抛错不重调(C6)
|
|
261
|
+
// result(call):call 的形状同 call() 的返回值,此时已有结局;在写下结局的事务里、一个保存点里被调用
|
|
262
|
+
// late(call):放弃了的调用,对方的结局晚到时代替 result 被调用,call 另带 late;抛错的处理同 result(C12)
|
|
263
|
+
kernel.calls({ address, state }); // 这个地址发出的、还没结局的调用;state 只在 sending、open 里再筛
|
|
264
|
+
kernel.cancel(ref, reason); // C7;已有结局的调用不受影响;reason 原样交给对方的取消函数;定时:撤掉
|
|
265
|
+
kernel.detach(ref); // C9:不等了;已有结局的调用不受影响;定时:改成不在等
|
|
266
|
+
kernel.abandon(ref); // C12;已有结局的调用不受影响;定时:撤掉
|
|
267
|
+
kernel.inquire(ref); // C5,事务之外;只对 open 的调用有用,定时什么都不做;两张表里都没有就抛错;不返回什么,结果走 results
|
|
268
|
+
await kernel.answer({ from, id, to, call, asked, answer }); // C8,事务之外(在事务里同步抛错);值是回执,send 被拒绝就拒绝
|
|
269
|
+
|
|
270
|
+
// 定时(第 7 节)
|
|
271
|
+
kernel.after({ from, id, delayMs, to, op, input, lane, by, awaited, parent, memo });
|
|
272
|
+
// 事务里;返回 { from, id, to, op, state: 'scheduled', dueAt, memo, by, awaited };号已用过同 call()
|
|
273
|
+
kernel.unschedule(ref); // 事务里;撤掉还在定时表里的定时;已经变成调用的不动;两张表里都没有就抛错
|
|
274
|
+
kernel.tick(); // 事务之外;把到点的定时变成调用
|
|
275
|
+
|
|
276
|
+
// 收发与启停
|
|
277
|
+
kernel.receive(envelope); // 外面来的信封交给内核,同步返回回执
|
|
278
|
+
kernel.pump(); // 每个还没处理成的结果各开一个事务再调,再送没拿到回执的重读,让发送循环把留着的信再送一遍
|
|
279
|
+
kernel.recover(); // 启动时:开始收信,pump,对 open 的调用 inquire,tick
|
|
280
|
+
await kernel.idle(); // 发送循环没有可送的信了
|
|
281
|
+
kernel.close(); // 停手:之后不再读写库,在途的回执作废;事务之外的入口(receive、pump、recover、inquire、tick、answer、when)一律抛错
|
|
282
|
+
|
|
283
|
+
// 观察
|
|
284
|
+
kernel.when(ref, 'accepted' | 'ended'); // 事务之外(在事务里同步抛错);Promise,值同 call() 的返回值,只在内存里
|
|
285
|
+
kernel.pending({ address }); // { calls, runs, timers, letters }:这个地址没结束的事
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
- `ref` 是 `{ from, id }`,调用、运行、定时都用它。
|
|
289
|
+
- 处理函数(`handle` 登记的 `accept`、`cancel`、`answer`,`results` 登记的 `status`、`result`、`late`)都作为登记的那个对象的方法调用,`this` 是那个对象。
|
|
290
|
+
- `memo` 是调用方留给自己的一段不透明内容,不进信封,交给结果处理函数。
|
|
291
|
+
- `input`、`parent` 不给时是 `null`,`awaited` 不给时是 `false`。C1 比较的是信封里的 `digest`:数组 `[to, op, input, parent]` 的规范化文本的 SHA-256。规范化文本同 JSON,但对象的键按字典序排列、值为 `undefined` 的键略去。
|
|
292
|
+
- 内核自己写下的失败,`code` 就是原因。被调用方一侧:`no_handler`、`handler_error`,`effects` 为 `none`。调用方一侧:`conflict`、`unsupported`,`effects` 为 `none`;`unknown_outcome`,`effects` 为 `unknown`,原文在 `error.original`;`lost`(C5)、`abandoned`(C12),`effects` 为 `unknown`;取消的信被拒而它换掉的调用还是 `sending`(C7),`code` 同回执,`effects` 为 `unknown`。调用方一侧写下的这几种,那一行带 `local: true`。
|
|
293
|
+
- 取消的回执:经 `send` 拿到的,`view` 带着结局就写结局,否则把还是 `sending` 的调用记成 `open`,并按 C6 看版本号;本地交付的只记 `open`。
|
|
294
|
+
- 重读发给本地地址时,内核在一个事务里直接交付并照回执更新调用:被调用方对带 `confirmed` 的调用不调用任何函数,所以这个事务里只调调用方的函数(S9)。
|
|
295
|
+
- `send` 同步抛错,算 `send` 被拒绝。
|
|
296
|
+
- `when(ref, what)`:`accepted` 是调用离开了 `sending`,没被收下就有了结局(放弃、对方拒收)也算,值里的 `state` 说明是哪一种;`ended` 是有了结局(内核自己写的、放弃的都算)。先读表:已经满足就立即完成;是还在定时表里的定时,就等它变成调用再往下等;两张表里都没有就拒绝。等着的记在内存里,每次最外层提交之后再看;等的是定时而它被撤掉了,拒绝。`close()` 和崩溃之后不再有下文(R1)。
|
|
297
|
+
- `pending({ address })`:`calls`、`runs` 同 `calls()`、`runs()`;`timers` 是这个地址登记的、还在定时表里的定时,每项同 `after()` 的返回值;`letters` 是发件箱里这个地址发出的信,每项 `{ kind, to, call }`(`call` 是 `{ from, id }`),按发件箱的次序。
|
|
298
|
+
- `pump()` 之外,内核在每次提交之后自己送新写下的信;一封信送成了,接着送它通道里的下一封。送不到的、对方说 `busy` 或 `not_ready` 的,等宿主下一次 `pump()`:宿主在连上对方时和定期调用它。循环每交出 8 封信(本地交付或交给 `send`)经 `schedule` 让出一次。
|
|
299
|
+
- `schedule(fn)` 是内核让出事件循环的办法,默认 `setImmediate`。测试里换成自己的。
|
|
300
|
+
- 名字可以在实现时调整,调整后改这一节。
|
|
301
|
+
|
|
302
|
+
## 9. 表(草案)
|
|
303
|
+
|
|
304
|
+
| 表 | 一行是什么 | 主键 |
|
|
305
|
+
|---|---|---|
|
|
306
|
+
| `kernel_meta` | 表结构的版本(`key` 为 `version`) | `key` |
|
|
307
|
+
| `kernel_calls` | 我发出的一次调用 | `(from_addr, id)` |
|
|
308
|
+
| `kernel_runs` | 我收下的一次调用,或一条根运行(`from_addr` 是它自己的地址),或一条只为去重留下的结局 | `(from_addr, id)` |
|
|
309
|
+
| `kernel_answers` | 我采用过的一次回答的号码 | `(from_addr, id)` |
|
|
310
|
+
| `kernel_outbox` | 我欠别人的一封信 | 自增的序号 |
|
|
311
|
+
| `kernel_timers` | 一个还没变成调用的定时 | `(from_addr, id)` |
|
|
312
|
+
|
|
313
|
+
迁移只向前。`src/schema.mjs` 的 `MIGRATIONS` 第 n 条把表从版本 n 升到 n + 1;第一条是 0.1.0 的基线。发布过的条目不再改,改表只在末尾追加一条。`migrate()` 读 `kernel_meta` 里的版本:比条数小,依次跑后面的条目,再把版本写成条数;相等,什么都不做;比条数大(库是更新的内核写的),抛错,什么都不写。
|
|
314
|
+
|
|
315
|
+
现在的列(`src/schema.mjs`):
|
|
316
|
+
|
|
317
|
+
- `kernel_calls`:`to_addr`、`op`、`digest`、`input`、`parent`、`lane`(重读与取消要用)、`by_from`、`by_id`(发出它的运行,C9)、`awaited`、`memo`、`state`(`sending`、`open` 或结局)、`status`、`rev`、`asked`、`asking`(最近听到的状态,C6)、`result`、`error`、`progress`、`owed`(结局或晚到的结局已写下、处理函数还没提交,C4、C12)、`local`(结局是内核自己写的,不是对方给的)、`abandoned`(调用方放弃了它,C12)、`late`(放弃之后对方的结局,JSON 原文,只写一次)。索引:`owed = 1` 的部分索引;`state = 'open'` 的部分索引给 `recover()` 的重读;`(from_addr, state)` 给 `calls()`;`(by_from, by_id)` 查一条运行名下的调用。
|
|
318
|
+
- `kernel_runs`:`to_addr`(本地地址)、`op`、`digest`(根运行没有)、`parent`、`awaited`、`root`、`cancel_asked`(已要求取消,C7)、`status`、`rev`、`asked`、`asking`、`result`、`error`、`progress`。只因为取消而留下的记录没有 `op`、`parent`、`digest`,靠 `op` 为空和根运行分开。索引:`(to_addr, status)` 给 `runs()`。
|
|
319
|
+
- `kernel_answers`:`run_from`、`run_id`、`asked`(答的是哪条运行的第几问)。索引:`(run_from, run_id, asked)` 唯一,一问至多记一个回答(S8),以后清理也按运行找。
|
|
320
|
+
- `kernel_timers`:`to_addr`、`op`、`digest`、`input`、`parent`、`lane`、`by_from`、`by_id`、`awaited`、`memo`(同调用表,到点时原样搬过去)、`due`(到点时间)。登记的先后按 `rowid`。索引:`due` 找到点的和最早的;`(by_from, by_id)` 查一条运行名下的定时。
|
|
321
|
+
- `kernel_outbox`:`seq`(这封信的身份,不重用)、`pos`(它在发件箱里的次序;换内容的新行沿用旧行的 `pos`,R3)、`from_addr`(发信的地址,给 `pending()`)、`to_addr`、`lane`(不占通道的为 `NULL`;换掉调用的取消沿用调用的通道)、`kind`(`call`、`status`、`result`、`cancel`)、`call_from`、`call_id`(这封信关于哪次调用)、`envelope`(JSON 原文)。在途、留着的信和回执按 `seq` 认,发送循环和通道按 `pos` 排。索引:`pos` 唯一,`(lane, pos)` 找通道的头,`(call_from, call_id)` 撤一次调用的信,`(from_addr, pos)` 按地址列出欠着的信。
|
|
322
|
+
|
|
323
|
+
重读不落库:没拿到回执的,记在内存里等下一次 `pump()`(R1)。
|
|
324
|
+
|
|
325
|
+
内核的每条语句都列在 `src/schema.mjs` 的 `SQL` 里,不扫全表。`test/sql.test.mjs` 对每条检查查询计划,并核对内核实际准备过的语句都在这份清单里。
|
|
326
|
+
|
|
327
|
+
结局已定的行在 0.x 不清理。保留多久、怎么清,1.0 之前定;规则必须保证去重的记录比任何可能晚到的重复信活得久。
|
|
328
|
+
|
|
329
|
+
## 10. 启动与恢复
|
|
330
|
+
|
|
331
|
+
宿主先 `migrate()`,登记完处理方,再 `recover()`。`recover()` 依次做这几件事:开始收信;`pump()`,即再调那些还没处理成的结果处理函数、送发件箱里的信;对 `open` 的调用各 `inquire` 一次;`tick()`,并照第 7 节告诉宿主最早的到点时间。内核没有别的要恢复的。宿主随后重扫自己名下的运行和调用。
|
|
332
|
+
|
|
333
|
+
## 11. 对实现的要求
|
|
334
|
+
|
|
335
|
+
这些是「任意两次提交之间崩溃也正确」带来的要求:
|
|
336
|
+
|
|
337
|
+
- **R1 内存里只放丢了也没关系的东西**:正在送的信、这一轮没送成的信、等着再送的重读、`when` 的等待。崩溃后它们全部清空,行为仍然正确。别的状态都落库:已要求取消、晚到的结局、提问号、还没处理成的结果。
|
|
338
|
+
- **R2 事务里不改内存。** 内存里的状态只在提交之后改,回滚才不会留下痕迹。
|
|
339
|
+
- **R3 发件箱的行不就地改写。** 要换内容(取消换调用、新状态换旧状态),删掉旧行、写一行新的,新行保持旧行在通道里的次序。回执只撤它送出的那一行;那一行已经被换掉,就不撤别的。
|
|
340
|
+
- **R4 旧实例不再写库。** `close()` 之后,还没结束的 `send` 回来也不处理。模拟崩溃靠它。
|
|
341
|
+
- **R5 提交点数得出来。** 最外层的 `COMMIT` 才算一次提交,保存点的释放不算。
|
|
342
|
+
- **R6 让出事件循环只经 `schedule`**,不直接用计时器。
|
|
343
|
+
|
|
344
|
+
## 12. 怎么测
|
|
345
|
+
|
|
346
|
+
测试是确定性的模拟:几个实例跑在一个进程里,送信的口、时钟、`schedule` 都由测试控制;时钟只由测试推进,`tick()` 由测试随时调用。测试辅助代码放 `sim/`。
|
|
347
|
+
|
|
348
|
+
模拟分两层。`sim/world.mjs` 是世界:几个实例、受控的网络与时钟、提交计数、崩溃与提交失败的注入,以及下面的检查。它不认识任何宿主,经 `@coda-console/kernel/sim` 导出,宿主可以把自己真的处理函数接进来撞故障:每一代启动时,世界在迁移的那个事务里调用宿主的 `setup`;`setup` 在事务里,只能在事务外调用的函数放进 `node.recovered(fn)`:世界在这一代的 `recover()` 成功之后立即在事务外调用它,每一代一次,宿主在这里做第 10 节说的重扫。宿主经世界给的连接读写自己的表,这一代死了之后这条连接上的读写一律抛错;经 `node.schedule` 排的事也不再执行,排的事里撞上崩溃或提交失败,由世界接住。世界在宿主登记处理函数时包一层,在处理函数自己的事务里往 `sim_` 开头的表记一行,再照登记的那个对象的方法调用宿主的函数;按处理函数计的检查(S2、S3、S8、S9,取消函数与状态处理函数的次数)读这些表,其余的读内核的表。记账不取决于宿主给了哪些函数:一个地址在这一代登记过任何东西(`handle` 或 `results`),它听到的状态、结局、晚到的结局世界都记一行,宿主给没给对应的函数都一样;取消函数、回答函数会改变内核的回执,给了才包。宿主可以加自己的每步检查和静止点检查。`sim/host.mjs` 是内核自己测试用的宿主。
|
|
349
|
+
|
|
350
|
+
故障模型:
|
|
351
|
+
|
|
352
|
+
- 送信:请求丢了;请求到了、回执丢了;延迟;重复;同一通道里晚到的重复信排到后面的信之后;不同种类的信之间乱序;`send` 很久才结束。
|
|
353
|
+
- 处理方和回调:抛出别的错;一直留着不推进。
|
|
354
|
+
- 进程:在任意两次提交之间崩溃,内存里的东西全丢,库停在最后一次提交;提交本身失败。
|
|
355
|
+
- 启动:处理方还没登记完就有信到。
|
|
356
|
+
- 版本:两个实现互相说话,一方多了字段、多了 `must`、给了不认识的枚举值。
|
|
357
|
+
- 小场景穷举:在每一次提交之后都崩一次。长场景按种子随机,种子写在失败信息里。
|
|
358
|
+
|
|
359
|
+
模型之外、但设计留了防线的:库从旧备份恢复(C5 的 `confirmed`、C4 里对不认识的调用的处理)。模型之外、不防的:P1 到 P6 被违反。
|
|
360
|
+
|
|
361
|
+
每一步之后都要成立的:
|
|
362
|
+
|
|
363
|
+
- **S1** 结局写下之后不再变。
|
|
364
|
+
- **S2** 对同一次调用,处理方的 `accept` 至多提交一次。
|
|
365
|
+
- **S3** 对同一次调用,结果处理函数的 `result`、`late` 各至多提交一次。
|
|
366
|
+
- **S4** 调用方的结局如果来自对方(回执或结局的信),就等于对方运行的结局。放弃的调用拿晚到的结局来比,还没到就不比。
|
|
367
|
+
- **S5** 同一通道里 c1 先于 c2 写进发件箱,并且两者在对方都有了记录(含只为去重留下的),那么 c1 的记录先提交。
|
|
368
|
+
- **S6** 交给 `send` 的每一封信,在交出的那一刻,等于某个已提交的发件箱行,或者是对某个已提交的 `open` 调用的重读。回答除外,它不落库。
|
|
369
|
+
- **S7** 有结局的运行,名下没有「在等」而没结束的调用。
|
|
370
|
+
- **S8** 每个提问号至多采用一个回答。
|
|
371
|
+
- **S9** 一个事务里,内核至多调用一个地址登记的函数。
|
|
372
|
+
- **S10** `sending` 的调用,发件箱里有它的一封调用或取消。
|
|
373
|
+
- 定时从定时表里消失只有三种去向:变成同号的调用(那时 `now()` 不小于到点时间);被 `unschedule`、`cancel`、`abandon` 撤掉;它名下的运行在同一次提交里被要求取消。
|
|
374
|
+
- `recover()` 过的实例,最近一次 `wake` 给的值等于定时表里最早的到点时间(没有定时是 `null`)。
|
|
375
|
+
|
|
376
|
+
到了静止点才检查的。静止点是:不再注入故障、各方都可达、处理方该推进的都推进完了、各方都 `pump()` 和 `tick()` 过、发送循环空了:
|
|
377
|
+
|
|
378
|
+
- **L1** 发件箱是空的。
|
|
379
|
+
- **L2** 被调用方有结局的调用,调用方也有了同样的结局,并且 `result` 恰好提交了一次。放弃的调用:晚到的结局已经记下,`late` 恰好提交了一次。调用方的结局是内核自己写的(`local`,放弃除外)时不比:对方之后给的结局不改这一行(C4)。
|
|
380
|
+
- **L3** 运行 R 被要求取消的那个事务提交时,R 名下「在等」而没结束的每个调用,现在要么有了结局,要么对方记着已要求取消。
|
|
381
|
+
- **L4** 到点的定时都已经变成调用,并且被收下恰好一次。
|
|
382
|
+
|
|
383
|
+
另有两项常设检查:每条 SQL 走索引;源码不出现业务词、不引入依赖(`test/boundary.test.mjs`)。
|
|
384
|
+
|
|
385
|
+
## 13. 现在不做的
|
|
386
|
+
|
|
387
|
+
- **按键串行**(一个键一次只处理一个):由处理方自己排。有第二个需要它的处理方时再加。
|
|
388
|
+
- **等输入沿调用树往上交**:内核只把 `input-required`、`asking` 和提问号告诉调用方。要往上交时,宿主把最初提问的那条运行的地址、引用和提问号放进 `asking` 一起带上去;回答直接发给最初提问的一方,回执也直接回到回答的人。内核不做中途转交。
|
|
389
|
+
- **落库的回答**:回答只当场送。需要对方离线时也能送达的回答,等有了用例再加。
|
|
390
|
+
- **等齐了自动给结局**:C9 只拦着不让早结束,不替处理方结束。
|
|
391
|
+
- **撤回一封没送过的调用而不留痕迹**:取消一律走取消的信。
|
|
392
|
+
- **地址搬迁**:见 P1。
|
|
393
|
+
- **结局记录的清理**:见第 9 节。
|
|
394
|
+
- **权限**:在内核外面的入口处判断。
|
|
395
|
+
|
|
396
|
+
## 14. 到 1.0 的条件
|
|
397
|
+
|
|
398
|
+
- coda 里 15 处跨归属的过界都经它(清单见 coda#2449)。
|
|
399
|
+
- 模拟测试稳定通过。
|
|
400
|
+
- 第 6 节的信封不再改。
|
|
401
|
+
|
|
402
|
+
1.0 之后改内核须用户认可。
|
package/kernel.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Busy, createKernel, Rejected } from './src/kernel.mjs';
|
package/package.json
CHANGED
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coda-console/kernel",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Coda 的调用内核:嵌在宿主进程里的库,只管幂等调用与责任转移",
|
|
5
|
+
"type": "module",
|
|
3
6
|
"license": "UNLICENSED",
|
|
4
7
|
"repository": {
|
|
5
8
|
"type": "git",
|
|
6
9
|
"url": "git+https://github.com/coda-console/coda-kernel.git"
|
|
7
10
|
},
|
|
8
|
-
"
|
|
9
|
-
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=24"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node --test --test-timeout=120000 --test-force-exit"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./kernel.mjs",
|
|
19
|
+
"./sim": "./sim/world.mjs"
|
|
20
|
+
},
|
|
10
21
|
"publishConfig": {
|
|
11
22
|
"access": "public"
|
|
12
|
-
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"kernel.mjs",
|
|
26
|
+
"src/",
|
|
27
|
+
"sim/",
|
|
28
|
+
"docs/"
|
|
29
|
+
]
|
|
13
30
|
}
|