@coda-console/kernel 0.2.1 → 0.3.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 CHANGED
@@ -5,7 +5,7 @@ Coda 的调用内核:嵌在宿主进程里的库,只管幂等调用与责任
5
5
  - 它做什么、怎么做:[`docs/design.md`](docs/design.md)。
6
6
  - 为什么有它、在 Coda 里怎么接:[coda#2449](https://github.com/coda-console/coda/issues/2449)。
7
7
 
8
- 状态:0.1。设计里的接口都已实现:事务与输出门、表与迁移、调用的主路径、状态更新、调用树与根运行、取消、重读、放弃、回答、定时、`when` 与 `pending`,都有模拟测试守着。设计第 13 节列的现在不做。
8
+ 状态:0.3。设计里的接口都已实现:事务与输出门、表与迁移、调用的主路径、状态更新、调用树与根运行、取消、重读、放弃、回答、定时、`when` 与 `pending`,都有模拟测试守着。设计第 13 节列的现在不做。
9
9
 
10
10
  ## 开发
11
11
 
package/docs/design.md CHANGED
@@ -59,7 +59,7 @@
59
59
 
60
60
  一起提交的是「我的业务数据,加上我欠出去的东西」。双方的业务数据从不在同一个事务里改,即使它们在同一个实例上。
61
61
 
62
- 发给本地地址的信封同样先写进发件箱。发送循环交付它时开一个事务:删掉发件箱里这一行,把信交给本地的处理方,并照回执更新发信一方在内核里的那一行,三件事一起提交。这个事务里只调用收信一方的处理函数:对方当场给了结局时,发信一方的调用先记成 `open`,结局由结局的信在它自己的事务里送达;对方不收(`conflict`、`unsupported`)时,对方的处理函数没有被调用,发信一方照常写下失败并调用结果处理函数。循环逐封交付,不递归;每交付若干封经 `schedule` 让出一次事件循环。`idle()` 返回一个 Promise,在循环没有可送的信时完成。
62
+ 发给本地地址的信封同样先写进发件箱。发送循环交付它时开一个事务:删掉发件箱里这一行,把信交给本地的处理方,并照回执更新发信一方在内核里的那一行,三件事一起提交。这个事务里只调用收信一方的处理函数:对方当场给了结局时,发信一方的调用先记成 `open`,结局由结局的信在它自己的事务里送达;对方不收(`conflict`、`unsupported`)时,对方的处理函数没有被调用,发信一方照常写下失败并调用结果处理函数。对方早就记着这次调用(比如导入的运行,5.5)时,它这次不调用任何函数、也不再欠信,回执就照经 `send` 拿到的处理:带着结局就写结局并调用结果处理函数,否则照 C6 记下状态。取消的信在两种情况下也这样处理:送到时运行已经结束,对方不调用任何函数;或者它换掉了还没确认的调用信,对方早就记着这条运行,调过取消函数之后运行的版本号还和交付之前读到的一样(取消函数没有改动它;导入之后改过的运行,版本号会大于 1,它那时的状态信送到时调用方还没有这次调用,已经丢了),回执只照 C6 记下状态,不调用发信一方的函数。导入的运行的这些不会再有别的信告诉发信一方;已经结束的运行若还欠着结局的信,那封信送到时调用已经有了结局,不再写(C3)。取消函数改动了运行,就照常由它欠下的信送达;调用不是 `sending` 时,取消的回执不改它的状态,免得在调用了对方取消函数的事务里再调用发信一方的函数(C6、C7)。循环逐封交付,不递归;每交付若干封经 `schedule` 让出一次事件循环。`idle()` 返回一个 Promise,在循环没有可送的信时完成。
63
63
 
64
64
  ## 5. 一次调用的一生
65
65
 
@@ -119,7 +119,8 @@ sequenceDiagram
119
119
  - 结果处理函数抛错(T6):结局照常写下并提交。内核记着这一次还没处理成,在之后每次 `pump()` 和 `recover()` 时再调它,直到它提交。
120
120
  - **C5 可重读(定)。** `inquire()` 对 `open` 的调用有用:把原来的调用带着 `confirmed` 再送一遍。这封信不占通道、不落库;没拿到回执,下次 `pump()` 再送。对方按 C1 回它现在的样子;对方没有这条记录就回 `unknown_call`,不执行,调用方把它写成 `failed`,`code` 为 `lost`,`effects` 为 `unknown`。启动恢复时,对所有 `open` 的调用做一次。`sending` 的调用不用重读,它的信还在发件箱里。
121
121
  - **C6 状态更新。** 运行每变一次,版本号加一;每次进入 `input-required` 或 `asking` 变了,提问号加一。被调用方把最新的状态欠在发件箱里;同一条运行还没送出的旧状态被新的替换,写下结局时撤掉还没送出的状态。调用方只接受版本号更大的状态。状态可以丢、可以晚,结局不可以。
122
- - 收下本身不另送状态,回执就是:调用方据回执把调用记成 `open`,状态 `working`,版本号 1。经 `send` 拿到的回执,`view` 的版本号比记下的大时,和收到状态的信一样处理。本地交付的回执不拿 `view` 更新状态:本地的状态信一定会到,交付的事务里也就不必调调用方的函数(S9)。根运行的状态不送给谁。
122
+ - 收下本身不另送状态,回执就是:调用方据回执把调用记成 `open`,状态 `working`,版本号 1。经 `send` 拿到的回执,调用方照 `view` 记下运行现在的样子:版本号是 1 时它就是运行被收下时的样子,整份记下;状态是 `working` 的不调状态处理函数,和收下一样不算一次状态;是 `input-required` 的(只有导入的运行会这样,5.5)照常调一次状态处理函数:这条运行早就在等输入,不会再有它自己的状态信来提这一问。比 1 大时,和收到状态的信一样处理。本地交付的回执一般不拿 `view` 更新状态:本地的状态信一定会到,交付的事务里也就不必调调用方的函数(S9)。例外是对方早就记着这条运行(比如导入的运行):不会再有别的信来,回执照第 4 节处理,版本号 1 的也照上面调或不调状态处理函数。只有本地交付、取消换掉了还没确认的调用信的那一种,不论版本号和状态都不调:那个事务里可能刚调过对方的取消函数(第 4 节),再调调用方的就违反 S9;回执里运行的样子,提问也在内,照样记在调用的行里。根运行的状态不送给谁。
123
+ - 版本号小于 1 的回执不守这一条,调用方把它当成版本号 1 的处理:是运行被收下时的样子,`working` 的不调状态处理函数,回执里的版本号照记,之后版本号更大的状态照常接受。`sending` 的调用收到回执不比版本号:它的调用信已经删了,调用一定离开 `sending`,否则既不重送也不重读(S10)。这一条只对回执:状态的信照旧只接受版本号更大的,版本号不大于调用的行(`sending` 的是 0)的旧状态信什么都不改,调用信照样留着。
123
124
  - `update` 没给的字段保持原样;`asking` 只在 `input-required` 时有意义,回到 `working` 就清掉。`settle` 没给 `progress` 时保留最后一次的。
124
125
  - 状态的信只认这次调用的被调用方发来的,规则同 C4。不认识的 `status` 记成 `working`。
125
126
  - 状态处理函数抛错(T6):状态照常记下,不重调。
@@ -170,6 +171,23 @@ sequenceDiagram
170
171
 
171
172
  被调用方在事务之外做的事(起进程、等别人)断了怎么办,是处理方自己的事:它在启动时重扫自己名下还在 `working` 的运行,决定续上、报失败,还是报 `effects: unknown`。内核不按日志重放代码,状态都在行里。
172
173
 
174
+ ### 5.5 导入
175
+
176
+ 宿主把已有的调用机制搬到内核上的那一刻,有在途的东西:调用方记着「对方已经收下,我在等结果」,被调用方记着「我收下了,还在做」。它们要变成内核的行,才能接着走完。内核的表只经内核的函数写,而现有的两条路都不对:`call()` 欠一封普通的调用信,对方要是没有这条记录就会重新执行一遍;运行只在收到调用信时产生,那会再调一次 `accept`。所以另有两个函数,专门在宿主自己的迁移里用:
177
+
178
+ - `adoptCall({ from, id, to, op, input, parent, lane, by, awaited, memo })` 写下一条 `open` 的调用,状态 `working`,版本号 0,不欠任何信。写成 `open` 而不是 `sending`,是因为 `recover()` 对 `open` 的调用发的是带 `confirmed` 的重读(C5):对方有这条运行,回它现在的样子或结局,照常交给结果处理函数;对方没有,只答 `unknown_call`,不执行,调用写成 `failed`,`code` 为 `lost`,`effects` 为 `unknown`。版本号是 0,对方的运行至少是 1,重读听到的状态一定更新。摘要、号和定时共用(第 7 节)、同号同摘要返回已有的、不同就抛错,都同 `call()`;`by`、`awaited` 的核对同 C9,`by` 指的运行要先导入。返回值同 `call()`。
179
+ - `adoptRun({ from, id, to, op, input, parent, awaited, status, asking, progress, outcome })` 写下一条早就收下的运行,不调任何处理函数,不欠任何信。没给 `outcome`:`status` 是 `working`(默认)或 `input-required`;`input-required` 时存 `asking`,提问号是 1;版本号是 1,同收下时(C6)。给了 `outcome`(三种结局之一,`failed` 的 `error` 照 C11 补齐):就是已经结束的运行,调用方经重读拿到它的结局,不另送结局的信。摘要照 `[to, op, input, parent]` 算,运行这一行存摘要、不存输入;`op` 必须给,没给(`undefined` 或 `null`)就抛错:没有 `op` 的行是只因取消留下的记录,不比摘要(C1),不同内容的调用会拿到它的结局。`to` 不核对是不是本地地址:导入时处理方可能还没登记,地址错了是宿主的事。同号的运行已经有了:摘要相同什么都不做,返回它;不同就抛错。返回值同 `runs()` 的一项,结束的另带 `outcome` 与 `result` 或 `error`。之后对方的调用信或重读到了,按 C1 回这一条,调用方从回执拿到它的结局或状态,本地交付也一样(第 4 节);之后 `settle`,照常欠结局的信。
180
+
181
+ 规矩:
182
+
183
+ - 两个都只在事务里调(T2),并且只在这个实例 `recover()` 之前调,之后调抛错。导入的行由 `recover()` 的重读和之后的信推进;在那之前导入,它们就不会和在途的信、内存里的东西交错(R1、R2)。
184
+ - 两边的摘要必须相同,由宿主保证:两边用同一个函数,从同一份原料算出 `to`、`op`、`input`、`parent`。对不上时,调用方的重读得到 `conflict`:调用不动,告诉 `onError`(第 6 节末尾),这条调用一直是 `open`。
185
+ - 普通 `call()` 命中导入的等输入运行,回执带回提问号 1 的那一问,照 C6 经 `status` 交给调用方一次,跨实例和本地交付都一样;本地交付时同一个事务里随即 `cancel()`、取消换掉了调用信的那一种不调(C6、S9)。两边都导入的,重读听到的版本号 1 比调用的 0 大,同样调一次。
186
+ - 别的在途形状用已有的函数:还没被对方收下的调用用 `call()`,对方如果其实收过、导入了运行,按 C1 去重;没有调用方的运行用 `root()`;已经要求取消、还没送到的,导入之后再 `cancel()`。对方也导入了运行时,可以在同一个事务里 `call()` 再 `cancel()`:取消换掉调用信,运行已经结束的,取消的回执带回结局;还在做的,取消函数照常调用一次,它没改动运行时,回执带回运行收下时的样子(第 4 节)。
187
+ - 导入不改表结构,不加迁移条目。宿主把它写成自己迁移里一次性的一步,和 `migrate()` 在同一个事务里,并在自己的表里记下导入过。
188
+ - 导入时就已结束的运行一直留着,`forget` 不删(第 9 节)。它不欠结局的信,被调用方没有哪一刻能知道调用方已经拿到了结局:调用方随时可能用普通的 `call()` 或重读找上来,回执丢了就再送,它那台晚升级的话,第一封可能在导入很久以后才到。删了它,那一封就会再执行一次。这种行只有迁移那一刻在途的那几条,留着是有界的。没给 `outcome` 导入、之后 `settle` 的,照常写下结局、欠结局的信,按第 9 节清理。
189
+ - 宿主的责任:导入时还在做、之后才结束的运行,没有什么能说调用方那台已经切换到内核,它们照第 9 节清理,不另留标记。路径是:被调用方导入一条在做的运行,之后 `settle`,结局信送到调用方的内核;那台内核已经 `recover()`,但这次调用还在旧机制里、表里没有这一行,按 C4 回「收到」并告诉 `onError`;被调用方删掉这封信,运行开始算安静;过了保留期 `forget` 删掉它;调用方这时才切换过来,用普通的 `call()` 同号同内容找来,被调用方没有这条运行,`accept` 再执行一次。所以对导入过的运行的所有调用方都切换到内核、`recover()` 过之前,宿主不清理导入过的运行;或者保留期长于新旧版本混跑的时间。调用方那台还没有内核收信、或者还没 `recover()` 时,结局信留在发件箱里,不会踩到这一条。
190
+
173
191
  ## 6. 信封
174
192
 
175
193
  信封是 JSON。公共字段:`v`(现在是 1)、`kind`、`from`、`to`,以及可选的 `must`。
@@ -219,7 +237,7 @@ sequenceDiagram
219
237
  - **撤与改。** 还在定时表里的定时:`unschedule(ref)`、`cancel(ref)`、`abandon(ref)` 都是撤掉它,不留痕迹,不调用结果处理函数,这个号可以再用;`detach(ref)` 把它改成不在等;`inquire(ref)` 什么都不做。`unschedule` 对已经变成调用的不动;两张表里都没有,这几个函数都抛错。
220
238
  - **调用树。** 标着「在等」、还在定时表里的定时,同「在等」而没结束的调用一样拦着 `settle()`(C9)。运行被要求取消(收到取消或 `stop`)的那个事务里,撤掉它名下(`by` 是它)的全部定时,在等不在等、到没到点都撤(C7)。之后它再登记的定时记成不在等,不受这条影响。
221
239
  - **时钟。** 时间只来自宿主给的 `now()`。它须跨重启可比(墙上时钟),因为到点时间落库。时钟回拨只让定时晚到。
222
- - **唤醒。** 最早的到点时间和上次告诉宿主的不一样时,内核在提交之后调用 `wake(到点时间)`;最后一个定时没了,调用 `wake(null)`。每次 `tick()` 结束时只要还有定时,不论变没变都再调一次:宿主的计时器刚响过,它的时钟也可能早一点。「上次告诉的」只在内存里,初值是没有,同 `null`(宿主手里没有计时器);`recover()` 之前不调,`recover()` 里的 `tick()` 照此。宿主据此设一个内存里的计时器,到时调用 `tick()`。`wake` 抛错交给 `onError`;没给 `wake` 就不调。
240
+ - **唤醒。** 最早的到点时间和上次告诉宿主的不一样时,内核在提交之后调用 `wake(到点时间)`;最后一个定时没了,调用 `wake(null)`。每次 `tick()` 结束时只要还有定时,不论变没变都再调一次:宿主的计时器刚响过,它的时钟也可能早一点。「上次告诉的」只在内存里,初值是没有,同 `null`(宿主手里没有计时器);`recover()` 之前不调,`recover()` 里的 `tick()` 照此。宿主据此设一个内存里的计时器,到时调用 `tick()`。`wake` 抛错交给 `onError`;没给 `wake` 就不调。模拟世界里由宿主经 `node.ports({ wake })` 接(第 12 节)。
223
241
 
224
242
  ## 8. 接口(草案)
225
243
 
@@ -245,11 +263,18 @@ kernel.handle(address, op, { accept, cancel, answer });
245
263
  kernel.update(run, { status, asking, progress }); // C6;没给的字段保持原样
246
264
  kernel.settle(run, outcome, { progress }); // 给结局(C3、C9);outcome 只能是三种结局之一,没给 progress 就保留最后一次的
247
265
  kernel.root({ address, op, id, parent }); // 起一条根运行,返回同 runs() 的一项;op 用来找它的回答函数;号已用过就抛错
248
- kernel.stop(run); // C7:同收到取消,但不调用取消函数;已有结局的运行不受影响,没有这条运行就抛错
266
+ kernel.stop(run); // C7:同收到取消,但不调用取消函数;已有结局的运行不受影响,没有这条运行就抛错;清掉之后见第 9 节
249
267
  kernel.runs({ address, status }); // 处理方收下过、还没结局的运行和根运行;status 只在 working、input-required 里再筛
250
268
  // 每项:{ from, id, to, op, parent, awaited, cancelAsked, status, rev, asked, asking, progress },根运行另带 root: true;
251
269
  // cancelAsked 是「已要求取消」
252
270
 
271
+ // 导入(5.5):只在事务里、recover() 之前;不欠信,同号同摘要返回已有的,不同就抛错
272
+ kernel.adoptCall({ from, id, to, op, input, lane, by, awaited, parent, memo });
273
+ // 写下一条 open 的调用,状态 working、版本号 0;by、awaited 同 call();返回同 call()
274
+ kernel.adoptRun({ from, id, to, op, input, parent, awaited, status, asking, progress, outcome });
275
+ // 写下一条早就收下的运行,不调处理函数;status 是 working 或 input-required(提问号 1),给了 outcome 就是结束的;
276
+ // 返回同 runs() 的一项,结束的另带 outcome 与 result 或 error
277
+
253
278
  // 调用方
254
279
  kernel.call({ from, id, to, op, input, lane, by, awaited, parent, memo }); // by 是发出它的运行的 ref(C9)
255
280
  // 返回这一行调用:{ from, id, to, op, state, memo, by, awaited };open 时另带最近听到的 status、rev、asked、asking、progress;
@@ -261,9 +286,9 @@ kernel.results(address, { status, result, late }); // 各在自己的事务里
261
286
  // result(call):call 的形状同 call() 的返回值,此时已有结局;在写下结局的事务里、一个保存点里被调用
262
287
  // late(call):放弃了的调用,对方的结局晚到时代替 result 被调用,call 另带 late;抛错的处理同 result(C12)
263
288
  kernel.calls({ address, state }); // 这个地址发出的、还没结局的调用;state 只在 sending、open 里再筛
264
- kernel.cancel(ref, reason); // C7;已有结局的调用不受影响;reason 原样交给对方的取消函数;定时:撤掉
265
- kernel.detach(ref); // C9:不等了;已有结局的调用不受影响;定时:改成不在等
266
- kernel.abandon(ref); // C12;已有结局的调用不受影响;定时:撤掉
289
+ kernel.cancel(ref, reason); // C7;已有结局的调用不受影响(清掉之后见第 9 节);reason 原样交给对方的取消函数;定时:撤掉
290
+ kernel.detach(ref); // C9:不等了;已有结局的调用不受影响(清掉之后见第 9 节);定时:改成不在等
291
+ kernel.abandon(ref); // C12;已有结局的调用不受影响(清掉之后见第 9 节);定时:撤掉
267
292
  kernel.inquire(ref); // C5,事务之外;只对 open 的调用有用,定时什么都不做;两张表里都没有就抛错;不返回什么,结果走 results
268
293
  await kernel.answer({ from, id, to, call, asked, answer }); // C8,事务之外(在事务里同步抛错);值是回执,send 被拒绝就拒绝
269
294
 
@@ -273,6 +298,10 @@ kernel.after({ from, id, delayMs, to, op, input, lane, by, awaited, parent, memo
273
298
  kernel.unschedule(ref); // 事务里;撤掉还在定时表里的定时;已经变成调用的不动;两张表里都没有就抛错
274
299
  kernel.tick(); // 事务之外;把到点的定时变成调用
275
300
 
301
+ // 清理(第 9 节)
302
+ kernel.forget({ before }); // 事务里,recover() 前后都行;删掉已结束、安静下来早于 before、不再用来去重的行
303
+ // 返回 { calls, runs, answers }:删掉的调用、运行、回答号各几行;保留多久由宿主定,before 是它的现在减保留期
304
+
276
305
  // 收发与启停
277
306
  kernel.receive(envelope); // 外面来的信封交给内核,同步返回回执
278
307
  kernel.pump(); // 每个还没处理成的结果各开一个事务再调,再送没拿到回执的重读,让发送循环把留着的信再送一遍
@@ -290,7 +319,7 @@ kernel.pending({ address }); // { calls, runs, timers, letters }
290
319
  - `memo` 是调用方留给自己的一段不透明内容,不进信封,交给结果处理函数。
291
320
  - `input`、`parent` 不给时是 `null`,`awaited` 不给时是 `false`。C1 比较的是信封里的 `digest`:数组 `[to, op, input, parent]` 的规范化文本的 SHA-256。规范化文本同 JSON,但对象的键按字典序排列、值为 `undefined` 的键略去。
292
321
  - 内核自己写下的失败,`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`。
322
+ - 取消的回执:经 `send` 拿到的,`view` 带着结局就写结局,否则把还是 `sending` 的调用记成 `open`,并按 C6 看版本号;本地交付的只记 `open`,除非对方早就记着这条运行(比如导入的运行),那时照第 4 节处理。
294
323
  - 重读发给本地地址时,内核在一个事务里直接交付并照回执更新调用:被调用方对带 `confirmed` 的调用不调用任何函数,所以这个事务里只调调用方的函数(S9)。
295
324
  - `send` 同步抛错,算 `send` 被拒绝。
296
325
  - `when(ref, what)`:`accepted` 是调用离开了 `sending`,没被收下就有了结局(放弃、对方拒收)也算,值里的 `state` 说明是哪一种;`ended` 是有了结局(内核自己写的、放弃的都算)。先读表:已经满足就立即完成;是还在定时表里的定时,就等它变成调用再往下等;两张表里都没有就拒绝。等着的记在内存里,每次最外层提交之后再看;等的是定时而它被撤掉了,拒绝。`close()` 和崩溃之后不再有下文(R1)。
@@ -310,13 +339,15 @@ kernel.pending({ address }); // { calls, runs, timers, letters }
310
339
  | `kernel_outbox` | 我欠别人的一封信 | 自增的序号 |
311
340
  | `kernel_timers` | 一个还没变成调用的定时 | `(from_addr, id)` |
312
341
 
313
- 迁移只向前。`src/schema.mjs` 的 `MIGRATIONS` 第 n 条把表从版本 n 升到 n + 1;第一条是 0.1.0 的基线。发布过的条目不再改,改表只在末尾追加一条。`migrate()` 读 `kernel_meta` 里的版本:比条数小,依次跑后面的条目,再把版本写成条数;相等,什么都不做;比条数大(库是更新的内核写的),抛错,什么都不写。
342
+ 迁移只向前。`src/schema.mjs` 的 `MIGRATIONS` 第 n 条把表从版本 n 升到 n + 1;第一条是 0.1.0 的基线。发布过的条目不再改,改表只在末尾追加一条。`migrate()` 读 `kernel_meta` 里的版本:比条数小,依次跑后面的条目,再把版本写成条数;相等,什么都不做;比条数大(库是更新的内核写的),抛错,什么都不写。条目是 SQL 文本;要用到迁移那一刻的,是一个函数,拿宿主的 `now()` 返回 SQL 文本。
343
+
344
+ - 第二条(0.3.0):`kernel_calls`、`kernel_runs` 各加 `quiet_at` 和它的索引。迁移之前就结束的行没有自己的时刻,按迁移那一刻算:取宿主给内核的 `now()`,不用 SQLite 自己的时钟(模拟世界的钟是假的);当时还欠着的关于它的信,确认时照常再挪。没结束的行留空。版本 1 的库是 0.2.x 写的,里面没有导入的运行:导入从 0.3.0 才有,宿主按第 10 节的顺序先 `migrate()` 再导入,所以这一条的回填不会把导入时就已结束的运行算成安静(5.5)。这一步要把每一行过一遍,和建表一样属于迁移,不在 `SQL` 清单里。
314
345
 
315
346
  现在的列(`src/schema.mjs`):
316
347
 
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),以后清理也按运行找。
348
+ - `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 原文,只写一次)、`quiet_at`(从什么时候起只为去重而留,宿主的 `now()`:结局写下的那一刻,对方给的、内核自己写的、放弃的都算;之后关于它的调用信、取消信每确认一封,挪到那一刻;晚到的结局不改它;没结束的为空)。索引:`owed = 1` 的部分索引;`state = 'open'` 的部分索引给 `recover()` 的重读;`(from_addr, state)` 给 `calls()`;`(by_from, by_id)` 查一条运行名下的调用;`quiet_at` 给 `forget()`。
349
+ - `kernel_runs`:`to_addr`(本地地址)、`op`、`digest`(根运行没有)、`parent`、`awaited`、`root`、`cancel_asked`(已要求取消,C7)、`status`、`rev`、`asked`、`asking`、`result`、`error`、`progress`、`quiet_at`(从什么时候起只为去重而留,宿主的 `now()`:结局写下的那一刻;之后关于它的状态信、结局信每确认一封,挪到那一刻。为空是没结束,或者导入时就已结束:这种运行永远不算安静,见 5.5)。只因为取消而留下的记录没有 `op`、`parent`、`digest`,靠 `op` 为空和根运行分开。索引:`(to_addr, status)` 给 `runs()`;`quiet_at` 给 `forget()`。
350
+ - `kernel_answers`:`run_from`、`run_id`、`asked`(答的是哪条运行的第几问)。索引:`(run_from, run_id, asked)` 唯一,一问至多记一个回答(S8),清理也按运行找。
320
351
  - `kernel_timers`:`to_addr`、`op`、`digest`、`input`、`parent`、`lane`、`by_from`、`by_id`、`awaited`、`memo`(同调用表,到点时原样搬过去)、`due`(到点时间)。登记的先后按 `rowid`。索引:`due` 找到点的和最早的;`(by_from, by_id)` 查一条运行名下的定时。
321
352
  - `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
353
 
@@ -324,11 +355,45 @@ kernel.pending({ address }); // { calls, runs, timers, letters }
324
355
 
325
356
  内核的每条语句都列在 `src/schema.mjs` 的 `SQL` 里,不扫全表。`test/sql.test.mjs` 对每条检查查询计划,并核对内核实际准备过的语句都在这份清单里。
326
357
 
327
- 结局已定的行在 0.x 不清理。保留多久、怎么清,1.0 之前定;规则必须保证去重的记录比任何可能晚到的重复信活得久。
358
+ ### 清理
359
+
360
+ 结局已定的行留着,是为了去重:同号的调用信、取消、回答、重读再来,按它回(C1、C5、C8)。去重的记录要比任何可能晚到的重复信活得久。重复信只在一次 `send` 的时限里出现(P4),发件箱里的重送在对方确认之后就停。所以保留期不从结局写下算起,而从这一行安静下来算起,也就是最后一封关于它的信说完的时候:
361
+
362
+ - 被调用方的运行早就结束了,调用方却可能还在发调用信:调用信的回执丢了,它就一直留在调用方的发件箱里重送,直到调用方拿到结局,也就是被调用方的结局信被确认的时候。一台停机一周,这就是一周以后。所以被调用方的去重记录要活过调用方最后一封可能还在路上的调用信:从结局信确认的那一刻起,再留一个保留期。
363
+ - 调用这边同理:放弃的调用欠着取消信,确认之前对方还可能回它。
364
+ - 所以 `quiet_at` 记「从什么时候起只为去重而留」:写结局时是那一刻;之后关于它的信每离开一次自己的发件箱(收到回执,送出它的一方删掉这封信),挪到那一刻。调用、运行一样。别处删信,要么和写结局在同一个事务里,要么是换成另一封信,不挪。
365
+
366
+ 宿主定一个远长于一次 `send` 时限的保留期,`forget({ before })` 删掉:
367
+
368
+ - 调用:已结束,`quiet_at` 早于 `before`,`owed = 0`(结果处理函数或 `late` 已经提交),发件箱里没有关于它的信(`call_from`、`call_id` 是它的;放弃的调用的取消还没确认就还在)。
369
+ - 运行:已结束,`quiet_at` 早于 `before`,发件箱里没有关于它的信(结局的信还没确认就还在)。根运行、只因取消留下的记录、导入之后才结束的运行都一样。导入时就已结束的运行 `quiet_at` 为空,不删:它不欠结局的信,没有哪一刻能说调用方已经拿到了结局,调用方随时可能再来(5.5)。这种行只有迁移那一刻在途的那几条。
370
+ - 删掉的运行名下采用过的回答的号(`kernel_answers` 里 `run_from`、`run_id` 是它的)一起删。
371
+
372
+ 没结束的行、定时表、发件箱,一律不碰。`forget` 在事务里调(T2),`recover()` 前后都行,返回删了几行。保留多久、多久调一次是宿主的事,内核不定,也不自己定时清理。Coda 的做法是保留 7 天、每天调一次,`before` 是现在减 7 天;这不是内核的要求。
373
+
374
+ 清掉之后会这样,都是规则的一部分,不是缺陷:
375
+
376
+ - 晚到的结局、状态:按 C4 的不认识的调用处理,回「收到」,丢弃,告诉 `onError`。放弃的调用被清掉之后,对方的晚结局不再记下,`late` 不调。晚到的回答:没有这条运行,回 `unknown_call`(C8)。
377
+ - 内核自己内存里记着的号不会因为行被清掉而出错:没拿到回执、等下次 `pump()` 再送的重读(C5),那一行没了或者已经不是 `open`,就不再送,别的照送。
378
+ - 对清掉的号再调函数:内核分不出「清掉了」和「从来没有」,所以和没有这个号一样处理,不再是空操作。
379
+
380
+ | 调用 | 清掉之前 | 清掉之后 |
381
+ |---|---|---|
382
+ | `cancel`、`detach`、`abandon`(调用) | 已有结局的调用不受影响,空操作 | 抛 `there is no call`,在事务里,宿主同一事务里的改动一起回滚 |
383
+ | `inquire`(调用) | 不是 `open` 就什么都不做 | 抛 `there is no call`(它在事务之外,没有什么可回滚) |
384
+ | `stop`(根运行) | 已有结局的运行不受影响 | 抛 `there is no run`,宿主同一事务里的改动一起回滚 |
385
+ | `when()` | 已有结局的调用照常返回 | 拒绝,`there is no call` |
386
+ | `call({ by })`,`by` 是清掉的运行 | 照常 | 抛 `there is no run`,宿主同一事务里的改动一起回滚 |
387
+
388
+ 宿主的责任:只对保留期之内的号调这些函数;或者在自己的事务里先确认那一行还在,再调。
389
+ - 同号的 `call`:调用方的行清掉之后,同号同内容的 `call` 是一条新调用,不再返回旧的;被调用方的运行也清掉了,就会再执行一次。宿主要是用业务键推出调用号,自己保证保留期之后不再用同一个号。
390
+ - 导入时还在做、之后才结束的运行(5.5):结局信送到了已经 `recover()`、但这次调用还没切换过来的调用方内核,按 C4 被丢弃,被调用方照常删信、算安静、过了保留期清掉;调用方这时才用普通的 `call()` 同号同内容找来,`accept` 再执行一次。宿主的责任:对导入过的运行的所有调用方都切换到内核、`recover()` 过之前,不清理导入过的运行;或者保留期长于新旧版本混跑的时间。
391
+ - 从比保留期更早的备份恢复:调用方重读得到 `unknown_call`,调用记成 `lost`,`effects` 为 `unknown`,结果是「不清楚」(C5)。这正是第 12 节说的那道防线该给的答案。
392
+ - 放弃的调用,结局信已经确认,取消信却一直没确认:调用方收到放弃的调用的结局时不撤取消(C4),取消信的回执丢了,调用方又过了保留期才把它再送到(比如一直停着机)。被调用方的运行从结局信确认起已经过了保留期,清掉了;按 C7 当作取消先于调用,留下一条只因取消的记录,回 `canceled` 的结局;调用方的这次调用已有结局,不改(C4)。这条记录到期后照样清掉。
328
393
 
329
394
  ## 10. 启动与恢复
330
395
 
331
- 宿主先 `migrate()`,登记完处理方,再 `recover()`。`recover()` 依次做这几件事:开始收信;`pump()`,即再调那些还没处理成的结果处理函数、送发件箱里的信;对 `open` 的调用各 `inquire` 一次;`tick()`,并照第 7 节告诉宿主最早的到点时间。内核没有别的要恢复的。宿主随后重扫自己名下的运行和调用。
396
+ 宿主先 `migrate()`,有在途的就在同一个事务里导入(5.5),登记完处理方,再 `recover()`。`recover()` 依次做这几件事:开始收信;`pump()`,即再调那些还没处理成的结果处理函数、送发件箱里的信;对 `open` 的调用各 `inquire` 一次;`tick()`,并照第 7 节告诉宿主最早的到点时间。内核没有别的要恢复的。宿主随后重扫自己名下的运行和调用。
332
397
 
333
398
  ## 11. 对实现的要求
334
399
 
@@ -347,23 +412,30 @@ kernel.pending({ address }); // { calls, runs, timers, letters }
347
412
 
348
413
  模拟分两层。`sim/world.mjs` 是世界:几个实例、受控的网络与时钟、提交计数、崩溃与提交失败的注入,以及下面的检查。它不认识任何宿主,经 `@coda-console/kernel/sim` 导出,宿主可以把自己真的处理函数接进来撞故障:每一代启动时,世界在迁移的那个事务里调用宿主的 `setup`;`setup` 在事务里,只能在事务外调用的函数放进 `node.recovered(fn)`:世界在这一代的 `recover()` 成功之后立即在事务外调用它,每一代一次,宿主在这里做第 10 节说的重扫。宿主经世界给的连接读写自己的表,这一代死了之后这条连接上的读写一律抛错;经 `node.schedule` 排的事也不再执行,排的事里撞上崩溃或提交失败,由世界接住。世界在宿主登记处理函数时包一层,在处理函数自己的事务里往 `sim_` 开头的表记一行,再照登记的那个对象的方法调用宿主的函数;按处理函数计的检查(S2、S3、S8、S9,取消函数与状态处理函数的次数)读这些表,其余的读内核的表。记账不取决于宿主给了哪些函数:一个地址在这一代登记过任何东西(`handle` 或 `results`),它听到的状态、结局、晚到的结局世界都记一行,宿主给没给对应的函数都一样;取消函数、回答函数会改变内核的回执,给了才包。宿主可以加自己的每步检查和静止点检查。
349
414
 
350
- 宿主在内核外面有自己一层(收信时核对收发双方、发信时选路、恢复时起自己的重送)时,在 `setup` 里用 `const post = node.ports({ receive, send, recover })` 把这一层接在世界和内核之间。每一代一次,随新的一代重置;哪一样不给,那一样就照世界自己的走法:
415
+ 宿主在内核外面有自己一层(收信时核对收发双方、发信时选路、恢复时起自己的重送、到点靠自己的钟)时,在 `setup` 里用 `const post = node.ports({ receive, send, recover, wake })` 把这一层接在世界和内核之间。每一代一次,随新的一代重置;哪一样不给,那一样就照世界自己的走法:
351
416
 
352
417
  - `receive(envelope, from)`:世界把一封信交给这台时调它,代替直接调 `kernel.receive`,返回回执。`from` 是把这封信放进网络的那一台,由世界作保(P3),不取自信封;测试直接调 `world.receive(to, envelope)` 时取信封的 `from` 所属的那台。这台没起来时不调。
353
418
  - `send(to, envelope)`:这一代的内核交出一封信时调它,返回回执的 Promise。S6 仍在内核交出的那一刻检查。宿主要把信放进网络,调 `node.ports()` 返回的 `post(to, envelope)`;不给 `send` 时内核交出的信就直接 `post`。`post` 绑在返回它的那一代上:这一代死了之后再调,哪怕新的一代已经起来,也抛崩溃,不入网,不占新一代的额度。
354
419
  - `recover()`:世界恢复这一代时调它,代替直接调 `kernel.recover()`。它正常返回,这台才算恢复好,再调 `node.recovered` 登记的函数;它撞上崩溃或提交失败,同 `recover()` 撞上。
420
+ - `wake(due)`:这一代的内核每调一次 `wake`(第 7 节),世界先照旧记下这个值(`node.woke`,给第 7 节那条检查),再原样交给它。给了它,到点就靠宿主自己的钟:宿主设一个计时器,响时在事务之外调 `kernel.tick()`;世界不再替这一台 `tick()`,收尾时也不替(见下),随机场景里不对它调 `world.tick`。宿主经手里的内核调的 `tick()` 世界记作「经宿主的 tick」计数(`ports: a tick through the host`);`recover()` 里内核自己的那次不算。不给的,到点照世界自己的走法。
355
421
 
356
422
  世界对接进来的这一层查四条,违反了记进 `world.violations`:**P-in** 世界的一次转交里,`kernel.receive` 至多被调一次。按来源认,不看信的文本,也不看在宿主的第几层回调里:转交期间,宿主手里的内核(`setup` 拿到的 `kernel`、`node.kernel`)的 `receive` 每被调一次算一次,世界自己走默认路径的那一次也算;宿主把信重建一遍(换字段顺序、增删字段)再交,处理函数提交之后把同一封信再交,都算。内核自己内部的交付不经宿主手里的这个句柄,一律不算,比如 `answer()` 回答本机地址时内核经自己的 `receive` 交付;**P-out** 按信的文本、在一代之内累计:同一封信 `post` 的次数不多于这一代内核到此为止交出它的次数。内核重送同一封信是又交出一次;宿主没 `post` 的那次交出,额度一直留在这一代,之后可以多 `post` 一次。没交出过的信一 `post` 就报;**P-rec** `recover` 正常返回时,这一代的 `kernel.recover()` 恰好被调了一次;**P-receipt** 宿主的 `receive` 交回的回执,世界照网上的样子转一遍(JSON 序列化再解析,什么都没交回的当作 `null`),查的是转出来、交给发信一方的那份回执,它带着发信一方的内核要读的那几样:`ok` 是 `true` 或 `false`;`ok: false` 的带字符串 `code`;`ok: true` 的调用、取消的回执带 `view` 对象,`view` 里要么有结局 `outcome`,要么有整数的 `rev` 和 `asked`(内核拿它们比版本、写进调用的行里不许为空的整数列)。取消信的发信方不是调用方(`from` 不等于 `call.from`)时,内核不读也不带 `view`,回 `ok: true` 就够。`view` 的其余字段、状态与结局的回执,内核都不读,世界也不查。回答的回执原样交给回答的宿主,内核不读,但宿主要读、世界的 C8 也读它的 `applied`,所以 `ok: true` 的回答回执要带布尔的 `applied`。缺了的,世界记一条违反,这张回执当作在路上丢了,不交给发信的一方,世界按回执计数的地方也不读它。宿主写自己「恰好一次」的检查,可以用 `node.taken`(世界转交期间宿主手里的内核的 `receive` 被调的次数,按来源认,同 P-in)和 `node.posted`(这台经 `post` 放进网络的封数),两者都跨代累计。
357
423
 
358
424
  `quiesce` 在第一次静止之后、查各调用的状态之前,先断言 `world.violations` 为空:宿主的错(比如收尾的 drain 里才第一次出现的坏回执)先作为它自己的违反报出来,不报成它留下的状态不一致。
359
425
 
360
- 收尾时世界默认每一轮 `pump` 每台,这会替一个从不重送的宿主补上。宿主自己负责重送时,用 `world.quiesce(advance, { resend })`:`resend()` 每一轮调一次,让各台把留着的信再送一遍(比如把宿主的钟推过一个重送周期),世界不再 `pump`;推进时钟、`tick()` 仍由世界做。静不下来时抛出的错写明每台发件箱里还有几封信、还有几条调用没结束。
426
+ 收尾时世界默认每一轮 `pump` 每台,这会替一个从不重送的宿主补上。宿主自己负责重送时,用 `world.quiesce(advance, { resend })`:`resend()` 每一轮调一次,让各台把留着的信再送一遍(比如把宿主的钟推过一个重送周期),世界不再 `pump`;推进时钟仍由世界做,`tick()` 也由世界做,给了 `wake` 的台除外:它们的计时器在宿主的钟上,`resend()` 推宿主的钟时才响。静不下来时抛出的错写明每台发件箱里还有几封信、还有几条调用没结束。
361
427
 
362
428
  世界的连接活过每一代,而真的进程一死,连接上的临时表、视图、触发器就没了。所以每一代启动、建内核之前,世界把这条连接上宿主建的临时对象全部删掉,第一代也删,宿主在 `open()` 里建的临时对象留不下来;SQLite 自己管的(名字以 `sqlite_` 开头,如 `AUTOINCREMENT` 带出的 `sqlite_sequence`)删不掉,不删;虚拟表的影子表(`PRAGMA temp.table_list` 里 `type` 为 `shadow` 的)随虚拟表一起删,不单独删。注册的 SQL 函数删不掉,宿主每一代重新注册。
363
429
 
364
430
  宿主经 `world.node(name, addresses, setup, { open })` 交自己的库时,库里可能已经有内核的表(比如宿主自己的内核建的)。所以一台「迁移过」,世界看自己的 `sim_` 表在不在,不看内核的表:`sim_` 表和内核的迁移、宿主的 `setup` 在同一个事务里建,这台第一代的这个事务提交过才有。没迁移过的台(比如第一代的这次提交被注入失败),每步检查和按台读表的计数都跳过它;之后哪一代起来,迁移照常再跑。
365
431
 
366
- `sim/host.mjs` 是内核自己测试用的宿主,走世界自己的路;`test/ports.test.mjs` 是一个经 `node.ports` 接进来、只靠自己的钟重送的宿主。
432
+ 导入场景:小场景里有一个,长场景另跑一组种子。各台第一代的 `setup` 在宿主自己的迁移里导入一次(宿主建一张表记着导入过,和导入在同一个事务里提交),按计划导入若干对:两边都有的、只有调用方的、被调用方已经结束的、要等输入的,以及只有被调用方导入、调用方用普通 `call()` 的,其中一些在同一个事务里随即 `cancel()`(同一个实例里和跨实例都有,运行是在做的、要等输入的或已经结束的);之后照常带故障走,收尾 `quiesce`。世界的检查不为导入放宽:被调用方的 `accept` 对导入的运行一次都不调(S2 读的表里没有它),结果处理函数恰好一次(S3、L2),对方给的结局等于运行的结局(S4、L2)。只有调用方的那种,调用记成 `lost`;它之后再被取消,被调用方留下一条只因取消的记录,两者不比,因为 L2 本来就不比内核自己写的结局。不导入的长场景照旧,计数不变。
433
+
434
+ 清理场景:长场景另跑一组种子,宿主带保留期。世界的钟每一步走一下;间或过去「很久」:网络里还在路上的信和回执都已经到了或者丢了(每次 `send` 都已结束,P4),钟走一个保留期那么远;间或一台的宿主在事务里 `forget`,`before` 是现在减保留期。保留期远长于两次「很久」之间钟能走的路,所以长过任何还在路上的信。为了让晚到的重复信真能晚到,网络间或给一封在路上的信留个副本,过些步再送一次,「很久」时副本也一起没了。为了走到上面那个反例,「很久」里每封还在路上的调用信都送到了、回执却丢了;「很久」之后,起着的宿主各 `pump` 一次,调用方重送调用信、被调用方重送结局信,网络给这时的每封调用信留个副本。世界数出「运行早在 `forget` 的 `before` 之前就结束了(按世界第一次看到它结束的时刻)、因为还没安静而留着、这时来了一封调用信」的次数。宿主只在保留期里用同一个号再调用。收尾时先 `quiesce`,再过一会儿各台都 `forget`、把留着的副本送出去、再静止一次,最后过去「很久」,各台再 `forget`;每次之后 `check()`、`checkQuiet()` 都要过。另有一组种子同时带导入和保留期:导入时就已结束的运行一直留着,调用方的普通调用和重读多晚到都按 C1 回它。
435
+
436
+ 世界的检查不为清理放宽,而是按记下的判断。世界包着 `forget`:在宿主的事务里,它删掉的每一行,删的那一刻都要满足上面的条件(按表里的 `quiet_at`);该删而没删的也不行;它返回的行数要等于真删的;发件箱和定时表不变。提交之后,世界记下删掉的行原来的样子。读得到这一行的检查(S1、S4、L2、L3),行没了就读记下的那一份;L2 对现存的运行和清掉的运行一起查,清掉的运行同样要求 `result` 或 `late` 恰好提交一次。被调用方的运行清掉之后又因晚到的取消留下只因取消的记录,按记下的那份比。放弃的调用在晚结局到之前就清掉了,`result`、`late` 都应当一次也没提交。调用方收到关于清掉的调用的结局、状态时告诉 `onError`,这是规则(上面第一条),测试宿主的错误检查认它。
437
+
438
+ `sim/host.mjs` 是内核自己测试用的宿主,走世界自己的路;`test/ports.test.mjs` 是一个经 `node.ports` 接进来、只靠自己的钟重送和到点的宿主:`wake` 给的到点时间设成它钟上的一个计时器(换掉上一个,`null` 就撤掉),响时 `tick()`;它的随机场景登记到本台和到别台的定时,偶尔撤掉。
367
439
 
368
440
  故障模型:
369
441
 
@@ -408,7 +480,6 @@ kernel.pending({ address }); // { calls, runs, timers, letters }
408
480
  - **等齐了自动给结局**:C9 只拦着不让早结束,不替处理方结束。
409
481
  - **撤回一封没送过的调用而不留痕迹**:取消一律走取消的信。
410
482
  - **地址搬迁**:见 P1。
411
- - **结局记录的清理**:见第 9 节。
412
483
  - **权限**:在内核外面的入口处判断。
413
484
 
414
485
  ## 14. 到 1.0 的条件
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coda-console/kernel",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Coda 的调用内核:嵌在宿主进程里的库,只管幂等调用与责任转移",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/sim/host.mjs CHANGED
@@ -154,6 +154,18 @@ export class TestHost {
154
154
  this.spawn({ from: address, id }, address, kids);
155
155
  }
156
156
 
157
+ /**
158
+ * The host imports a run it had taken before its kernel kept runs (导入), and keeps it working unless it has ended.
159
+ * @param {object} kernel
160
+ * @param {{ from: string, id: string, to: string, goal?: string, outcome?: object }} run the rest as adoptRun() takes it
161
+ * @returns {object} what adoptRun() returned
162
+ */
163
+ adopt(kernel, { goal = 'completed', ...run }) {
164
+ const adopted = kernel.adoptRun(run);
165
+ if (!run.outcome) this.q('INSERT INTO host_held (from_addr, id, addr, goal) VALUES (?, ?, ?, ?)').run(run.from, run.id, run.to, goal);
166
+ return adopted;
167
+ }
168
+
157
169
  /** @param {{ from: string, id: string, rev: number, memo?: { statusThrows?: boolean } }} call */
158
170
  status(call) {
159
171
  if (!this.world.calm && call.memo?.statusThrows) throw new Error('status handler blew up');
package/sim/scenarios.mjs CHANGED
@@ -7,6 +7,43 @@ const ENDED = new Set(['completed', 'failed', 'canceled']);
7
7
  // Small scenarios, each run once without a crash and then once per commit with a crash right after it (12. 怎么测).
8
8
  // The world restarts a crashed instance at once, so a scenario is the same script whatever crashed.
9
9
  export const SMALL = {
10
+ // Calls and runs on their way before the kernel kept them, imported by each instance's first generation: on both
11
+ // sides, on the caller's alone, a run that has ended, one waiting for input, and a run awaiting an imported call of its
12
+ // own; and runs imported by the callee alone, then called the plain way, and canceled at once for two of them. Those
13
+ // waiting for input are answered, and one call is canceled.
14
+ async adopt(world) {
15
+ const work = (from, id, to, more = {}) => ({ from, id, to, op: 'work', input: { mode: 'hold', n: id }, ...more });
16
+ const plan = [
17
+ { call: work('a1', 'm1', 'b1'), run: { goal: 'completed', progress: { step: 1 } } },
18
+ { call: work('a1', 'm2', 'b1') },
19
+ { call: work('a1', 'm3', 'b1', { parent: { for: 'm3' } }), run: { outcome: { outcome: 'failed' } } },
20
+ { call: work('b2', 'm4', 'a1', { memo: { resultThrows: 1 } }), run: { status: 'input-required', asking: { question: 1 } } },
21
+ { call: work('a1', 'm5', 'b1'), run: { goal: 'completed' } },
22
+ { call: work('b1', 'm6', 'b2', { by: { from: 'a1', id: 'm5' }, awaited: true }), run: { goal: 'canceled' } },
23
+ // Imported by the callee alone, then called the plain way: from the same instance and from another.
24
+ { call: work('b2', 'm7', 'b1'), run: { outcome: { outcome: 'completed', result: { imported: true } } }, plain: true },
25
+ { call: work('b2', 'm8', 'b1'), run: { status: 'input-required', asking: { question: 1 } }, plain: true },
26
+ { call: work('a1', 'm9', 'b1'), run: { status: 'input-required', asking: { question: 1 } }, plain: true },
27
+ // Its caller had asked to cancel it, and the cancel had not yet reached the callee: call() then cancel().
28
+ { call: work('b2', 'm10', 'b1'), run: { outcome: { outcome: 'completed', result: { imported: true } } }, plain: true, cancel: true },
29
+ { call: work('a1', 'm11', 'b1'), run: { outcome: { outcome: 'completed', result: { imported: true } } }, plain: true, cancel: true },
30
+ ];
31
+ const a = world.node('A', ['a1'], adopting(plan));
32
+ const b = world.node('B', ['b1', 'b2'], adopting(plan));
33
+ world.start();
34
+ await world.drain();
35
+ world.check();
36
+ for (const [node, id, to, call] of [[b, 'n1', 'a1', { from: 'b2', id: 'm4' }], [b, 'n2', 'b1', { from: 'b2', id: 'm8' }], [a, 'n3', 'b1', { from: 'a1', id: 'm9' }]]) {
37
+ const receipt = world.answer(node, { from: call.from, id, to, call, asked: 1, answer: { mode: 'take' } });
38
+ await world.drain();
39
+ await receipt;
40
+ world.check();
41
+ }
42
+ world.act(a, kernel => kernel.cancel({ from: 'a1', id: 'm1' }));
43
+ await world.drain();
44
+ world.check();
45
+ },
46
+
10
47
  // Calls between two addresses of one instance: every letter goes through local delivery. A cancel function there
11
48
  // ends its run at once, so the cancel's own receipt carries the outcome.
12
49
  async local(world) {
@@ -293,14 +330,68 @@ export const SMALL = {
293
330
  await world.drain();
294
331
  world.check();
295
332
  },
333
+
334
+ // Once the retention has passed, each instance forgets what has ended (9): a call and its run, and a run with the
335
+ // number of the answer it took. A call whose result handler has not committed yet stays, as does one still working.
336
+ async forget(world) {
337
+ const a = world.node('A', ['a1'], host);
338
+ const b = world.node('B', ['b1'], host);
339
+ world.start();
340
+ const run = { from: 'a1', id: 'f2' };
341
+ world.act(a, kernel => {
342
+ kernel.call({ from: 'a1', id: 'f1', to: 'b1', op: 'work', input: { mode: 'complete', n: 1 }, memo: { resultThrows: 1 } });
343
+ kernel.call({ ...run, to: 'b1', op: 'work', input: { mode: 'hold', goal: 'completed' } });
344
+ kernel.call({ from: 'a1', id: 'f3', to: 'b1', op: 'work', input: { mode: 'hold', goal: 'failed' } });
345
+ });
346
+ await world.drain();
347
+ world.act(b, kernel => kernel.update(run, { status: 'input-required', asking: { question: 1 } }));
348
+ await world.drain();
349
+ const receipt = world.answer(a, { from: 'a1', id: 'n1', to: 'b1', call: run, asked: 1, answer: { mode: 'settle' } });
350
+ await world.drain();
351
+ await receipt;
352
+ world.time = 100;
353
+ for (const node of [a, b]) world.act(node, kernel => kernel.forget({ before: 50 }));
354
+ await world.drain();
355
+ world.check();
356
+ },
296
357
  };
297
358
 
298
- /** Every error a world's hosts were told of is one its handlers threw on purpose, or the C9 refusal of a hasty accept. */
359
+ /**
360
+ * The setup of a test host that imports, in its own migration and once, what `plan` has on its way to or from its
361
+ * addresses (导入): each call its instance made, and the run its instance took, if any. Runs come first, as a call
362
+ * may be made by one of them. A `plain` call was not yet taken as far as its caller knows: it makes it with call(), and
363
+ * one to `cancel` with cancel() right after, in the same transaction.
364
+ * @param {Array<{ call: object, run?: object, plain?: boolean, cancel?: boolean }>} plan the call as adoptCall()
365
+ * takes it; the run's own part as TestHost.adopt() does
366
+ */
367
+ export function adopting(plan) {
368
+ return context => {
369
+ const it = host(context);
370
+ const { kernel, node } = context;
371
+ if (it.q("SELECT 1 FROM sqlite_master WHERE name = 'host_imported'").get()) return;
372
+ node.db.exec('CREATE TABLE host_imported (done INTEGER)');
373
+ for (const { call, run } of plan) if (run && node.addresses.includes(call.to)) it.adopt(kernel, { ...call, ...run });
374
+ for (const { call, plain, cancel } of plan) {
375
+ if (!node.addresses.includes(call.from)) continue;
376
+ if (plain) kernel.call(call);
377
+ else kernel.adoptCall(call);
378
+ if (cancel) kernel.cancel(call, 'asked before the import');
379
+ }
380
+ };
381
+ }
382
+
383
+ /**
384
+ * Every error a world's hosts were told of is one its handlers threw on purpose, the C9 refusal of a hasty accept, or a
385
+ * status or an outcome for a call its instance had forgotten (9).
386
+ */
299
387
  export function checkErrors(world) {
300
388
  for (const node of world.nodes)
301
- for (const error of node.errors)
389
+ for (const error of node.errors) {
390
+ const unknown = /^(a status|an outcome) arrived for unknown call (\S+)$/.exec(String(error?.message));
391
+ if (unknown && world.forgotten.has(`${node.name} call ${unknown[2]}`)) continue;
302
392
  assert.match(String(error?.message), /^((|result |status |cancel |late |answer )handler blew up|run \S+ still awaits a call it made)$/,
303
393
  `unexpected error on ${node.name}`);
394
+ }
304
395
  }
305
396
 
306
397
  /**
@@ -325,19 +416,68 @@ const GOALS = ['completed', 'failed', 'canceled'];
325
416
  const LANES = [null, 'x', 'y'];
326
417
  const ANSWERS = ['take', 'take', 'take', 'ask', 'settle', 'refuse', 'throw'];
327
418
  const DELAYS = [0, 1, 5, 10, 30];
419
+ // How far the clock moves when a long while passes in a long scenario whose hosts forget (9). Between two long whiles it
420
+ // moves by at most STRIDE a step, so a retention of LONG outlasts any letter or receipt left on its way in up to 476
421
+ // steps, as wander() checks.
422
+ export const LONG = 10_000;
423
+ // How far the clock moves in one step of a long scenario: by one where hosts forget, and by one of MOVES now and then.
424
+ const MOVES = [1, 2, 5, 10, 20];
425
+ const STRIDE = 1 + Math.max(...MOVES);
426
+ const IMPORTS = ['both sides', 'the caller alone', 'a run that has ended', 'a run waiting for input', 'the callee alone, then a plain call',
427
+ 'the callee alone, then a plain call and its cancel'];
428
+ const ENDINGS = [{ outcome: 'completed', result: { imported: true } }, { outcome: 'failed', error: { code: 'gave_up', message: 'gave up', effects: 'some' } },
429
+ { outcome: 'failed' }, { outcome: 'canceled' }];
430
+
431
+ /**
432
+ * What a long scenario that starts with imports has on its way when it starts: a few calls between any two addresses,
433
+ * each of one kind of IMPORTS.
434
+ * @param {() => number} rng
435
+ * @param {number} seed
436
+ * @param {string[]} addresses
437
+ */
438
+ function imports(rng, seed, addresses) {
439
+ const pick = list => list[Math.floor(rng() * list.length)];
440
+ return Array.from({ length: 2 + Math.floor(rng() * 4) }, (_, index) => {
441
+ const kind = pick(IMPORTS);
442
+ const call = { from: pick(addresses), id: `${seed}-m${index}`, to: pick(addresses), op: 'work', input: { mode: 'hold', n: index },
443
+ parent: rng() < 0.5 ? { imported: index } : null, memo: { resultThrows: rng() < 0.2 ? 1 : 0 } };
444
+ const goal = pick(GOALS);
445
+ // The callee alone has imported any run; its caller has not, and calls the plain way (C1). Its cancel, if any, was
446
+ // asked for and not yet delivered: it comes right after the call, in the same transaction.
447
+ const cancel = kind === 'the callee alone, then a plain call and its cancel';
448
+ const plain = cancel || kind === 'the callee alone, then a plain call';
449
+ const shape = plain ? pick(['a run that has ended', 'a run waiting for input', 'both sides']) : kind;
450
+ const run = kind === 'the caller alone' ? undefined
451
+ : shape === 'a run that has ended' ? { outcome: pick(ENDINGS) }
452
+ : shape === 'a run waiting for input' ? { status: 'input-required', asking: { question: 1 }, goal }
453
+ : { goal, progress: rng() < 0.5 ? { step: 1 } : undefined };
454
+ return { kind, call, run, plain, cancel };
455
+ });
456
+ }
328
457
 
329
458
  /**
330
459
  * A long scenario chosen by `seed`: three instances calling each other under every fault of the model, checked after
331
- * every step and at rest.
460
+ * every step and at rest. With `adopt`, each instance's first generation imports what is on its way (see imports()).
461
+ * With `retain`, now and then a long while passes, and a host forgets what ended more than `retain` ago (9); a host
462
+ * calls again with the same id only within `retain`, and once at rest the hosts forget all they may.
332
463
  * @param {number} seed
333
464
  * @param {number} steps
465
+ * @param {{ adopt?: boolean, retain?: number }} [options]
334
466
  */
335
- export async function wander(seed, steps) {
467
+ export async function wander(seed, steps, { adopt = false, retain } = {}) {
468
+ // A retention no longer than the clock may move over the whole scenario lets a host forget what a letter still on its
469
+ // way is about: that would show as a broken invariant of the kernel, when it is the scenario's.
470
+ if (retain !== undefined && retain <= STRIDE * steps)
471
+ throw new Error(`a retention of ${retain} does not outlast ${steps} steps of up to ${STRIDE} each: take fewer steps or a longer retention`);
336
472
  const rng = random(seed);
337
473
  const pick = list => list[Math.floor(rng() * list.length)];
338
474
  const world = new World({ rng });
339
475
  world.faults = { dropRequest: 0.1, dropReceipt: 0.1, duplicate: 0.1, commitFail: 0.03 };
340
- const nodes = [world.node('A', ['a1', 'a2'], host), world.node('B', ['b1'], host), world.node('C', ['c1'], host)];
476
+ const plan = adopt ? imports(rng, seed, ['a1', 'a2', 'b1', 'c1']) : [];
477
+ const setup = adopt ? adopting(plan) : host;
478
+ const nodes = [world.node('A', ['a1', 'a2'], setup), world.node('B', ['b1'], setup), world.node('C', ['c1'], setup)];
479
+ for (const { kind, call, plain } of plan)
480
+ world.count(`import: ${kind}${plain ? (world.owners.get(call.from) === world.owners.get(call.to) ? ', on the same instance' : ', from another instance') : ''}`);
341
481
  const addresses = nodes.flatMap(node => node.addresses);
342
482
  world.start();
343
483
  const issued = [];
@@ -358,9 +498,46 @@ export async function wander(seed, steps) {
358
498
  return depth === 1 && rng() < 0.15 ? { ...kid, mode: 'parent', kids: kids(2) } : kid;
359
499
  });
360
500
  const unended = node => node.q("SELECT from_addr, id, state FROM kernel_calls WHERE state IN ('sending', 'open') ORDER BY rowid").all();
501
+ /** @type {Map<object, number>} when each call issued was first made */
502
+ const made = new Map();
503
+ /** copies of letters that went on their way, which the network may bring again until a long while has passed */
504
+ const copies = [];
505
+ const forget = node => world.act(node, kernel => kernel.forget({ before: world.time - retain }));
361
506
  for (let step = 0; step < steps; step++) {
362
- const roll = rng();
363
- if (roll < 0.16) {
507
+ // Where hosts forget, every step takes a moment: what ended a step ago is older than a retention of 0, while its
508
+ // letters may still be on their way.
509
+ if (retain !== undefined) world.time += 1;
510
+ const roll = retain !== undefined && rng() < 0.1 ? -1 : rng();
511
+ if (roll < 0) {
512
+ const which = rng();
513
+ if (which < 0.1) {
514
+ // A long while passes: every letter and receipt still on its way has arrived or is lost by now, as each send()
515
+ // has ended (P4), copies and all. Each call letter arrives, and its receipt is lost: the caller keeps it.
516
+ for (let index = world.net.length - 1; index >= 0; index--)
517
+ if (world.net[index].request && JSON.parse(world.net[index].envelope).kind === 'call') world.deliver(index);
518
+ await flush();
519
+ for (const event of world.net.splice(0)) event.reject(new Error('not delivered'));
520
+ copies.length = 0;
521
+ world.time += LONG;
522
+ world.count('forget: a long while passed');
523
+ // The hosts that are up meet again and send what they kept: a call letter whose receipt was lost and the outcome
524
+ // letter its run owes go out together, long after the run ended. The network may bring each call letter again
525
+ // later, after the outcome letter has been confirmed (9).
526
+ for (const node of nodes) world.pump(node);
527
+ await flush();
528
+ for (const event of world.net)
529
+ if (event.request && JSON.parse(event.envelope).kind === 'call') copies.push({ ...event, resolve() {}, reject() {} });
530
+ } else if (which < 0.6) {
531
+ if (forget(pick(nodes))) world.count('forget: a host forgot');
532
+ } else if (which < 0.85) {
533
+ // A copy of a letter on its way, to arrive much later: a duplicate as late as the network may make one.
534
+ const event = pick(world.net.filter(event => event.request));
535
+ if (event) copies.push({ ...event, resolve() {}, reject() {} });
536
+ } else if (copies.length > 0) {
537
+ world.net.push(copies.splice(Math.floor(rng() * copies.length), 1)[0]);
538
+ world.count('forget: a late copy of a letter on its way again');
539
+ }
540
+ } else if (roll < 0.16) {
364
541
  const node = pick(nodes);
365
542
  const call = {
366
543
  from: pick(node.addresses), id: `${seed}-${issued.length}`, to: pick(addresses), op: rng() < 0.05 ? 'nope' : 'work',
@@ -370,16 +547,18 @@ export async function wander(seed, steps) {
370
547
  delayMs: rng() < 0.25 ? pick(DELAYS) : undefined,
371
548
  };
372
549
  issued.push(call);
550
+ made.set(call, world.time);
373
551
  world.act(node, kernel => call.delayMs === undefined ? kernel.call(call) : kernel.after(call));
374
552
  } else if (roll < 0.18) {
375
553
  const node = pick(nodes);
376
554
  const run = { address: pick(node.addresses), id: `${seed}-r${roots++}`, goal: pick(GOALS), kids: kids() };
377
555
  world.act(node, kernel => node.host.root(kernel, run));
378
556
  } else if (roll < 0.22 && issued.length > 0) {
379
- // The same request again, as a call or as a timer, whichever it was first: the two share their ids (7).
380
- const call = pick(issued);
381
- const how = rng() < 0.5 ? 'call' : 'after';
382
- const seen = world.act(world.owners.get(call.from), kernel => kernel[how]({ ...call, delayMs: call.delayMs ?? 5 }));
557
+ // The same request again, as a call or as a timer, whichever it was first: the two share their ids (7). A host that
558
+ // forgets calls again only within its retention: past it, the same id would be a new call (9).
559
+ const call = pick(retain === undefined ? issued : issued.filter(call => made.get(call) > world.time - retain));
560
+ const how = call && rng() < 0.5 ? 'call' : 'after';
561
+ const seen = call && world.act(world.owners.get(call.from), kernel => kernel[how]({ ...call, delayMs: call.delayMs ?? 5 }));
383
562
  if (seen) assert.deepEqual([seen.to, seen.op], [call.to, call.op]);
384
563
  if (seen) world.count(`same id again: ${call.delayMs === undefined ? 'call' : 'after'} then ${how}, finds a ${seen.state === 'scheduled' ? 'timer' : 'call'}`);
385
564
  } else if (roll < 0.45 && world.net.length > 0) world.deliver(Math.floor(rng() * world.net.length), true);
@@ -393,7 +572,7 @@ export async function wander(seed, steps) {
393
572
  world.inquire(node, ref);
394
573
  world.count('timer: inquire');
395
574
  } else if (ref && how !== 'inquire' && world.act(node, kernel => (kernel[how](ref), true))) world.count(`timer: ${how}`);
396
- } else if (roll < 0.485) world.time += pick([1, 2, 5, 10, 20]);
575
+ } else if (roll < 0.485) world.time += pick(MOVES);
397
576
  else if (roll < 0.50) {
398
577
  const node = pick(nodes);
399
578
  if (node.up && node.woke !== null && node.woke <= world.time) world.count('tick: the host\'s timer went off');
@@ -479,6 +658,21 @@ export async function wander(seed, steps) {
479
658
  await world.quiesce(() => advance(world));
480
659
  world.check();
481
660
  world.checkQuiet();
661
+ if (retain !== undefined) {
662
+ // At rest, a moment later, every host forgets what it may, and the network brings the copies it kept once more.
663
+ // Then a long while passes, and every host forgets all it keeps.
664
+ world.time += 1;
665
+ for (const node of nodes) forget(node);
666
+ world.check();
667
+ world.net.push(...copies.splice(0));
668
+ await world.quiesce(() => advance(world));
669
+ world.check();
670
+ world.checkQuiet();
671
+ world.time += LONG;
672
+ for (const node of nodes) forget(node);
673
+ world.check();
674
+ world.checkQuiet();
675
+ }
482
676
  checkErrors(world);
483
677
  return { world, issued: issued.length };
484
678
  }