@coda-console/kernel 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/design.md CHANGED
@@ -353,12 +353,16 @@ kernel.pending({ address }); // { calls, runs, timers, letters }
353
353
  - `send(to, envelope)`:这一代的内核交出一封信时调它,返回回执的 Promise。S6 仍在内核交出的那一刻检查。宿主要把信放进网络,调 `node.ports()` 返回的 `post(to, envelope)`;不给 `send` 时内核交出的信就直接 `post`。`post` 绑在返回它的那一代上:这一代死了之后再调,哪怕新的一代已经起来,也抛崩溃,不入网,不占新一代的额度。
354
354
  - `recover()`:世界恢复这一代时调它,代替直接调 `kernel.recover()`。它正常返回,这台才算恢复好,再调 `node.recovered` 登记的函数;它撞上崩溃或提交失败,同 `recover()` 撞上。
355
355
 
356
- 世界对接进来的这一层查三条,违反了记进 `world.violations`:**P-in** 世界的一次转交里,`kernel.receive` 至多被调一次。按来源认,不看信的文本,也不看在宿主的第几层回调里:转交期间,宿主手里的内核(`setup` 拿到的 `kernel`、`node.kernel`)的 `receive` 每被调一次算一次,世界自己走默认路径的那一次也算;宿主把信重建一遍(换字段顺序、增删字段)再交,处理函数提交之后把同一封信再交,都算。内核自己内部的交付不经宿主手里的这个句柄,一律不算,比如 `answer()` 回答本机地址时内核经自己的 `receive` 交付;**P-out** 按信的文本、在一代之内累计:同一封信 `post` 的次数不多于这一代内核到此为止交出它的次数。内核重送同一封信是又交出一次;宿主没 `post` 的那次交出,额度一直留在这一代,之后可以多 `post` 一次。没交出过的信一 `post` 就报;**P-rec** `recover` 正常返回时,这一代的 `kernel.recover()` 恰好被调了一次。宿主写自己「恰好一次」的检查,可以用 `node.taken`(世界转交期间宿主手里的内核的 `receive` 被调的次数,按来源认,同 P-in)和 `node.posted`(这台经 `post` 放进网络的封数),两者都跨代累计。
356
+ 世界对接进来的这一层查四条,违反了记进 `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
+
358
+ `quiesce` 在第一次静止之后、查各调用的状态之前,先断言 `world.violations` 为空:宿主的错(比如收尾的 drain 里才第一次出现的坏回执)先作为它自己的违反报出来,不报成它留下的状态不一致。
357
359
 
358
360
  收尾时世界默认每一轮 `pump` 每台,这会替一个从不重送的宿主补上。宿主自己负责重送时,用 `world.quiesce(advance, { resend })`:`resend()` 每一轮调一次,让各台把留着的信再送一遍(比如把宿主的钟推过一个重送周期),世界不再 `pump`;推进时钟、`tick()` 仍由世界做。静不下来时抛出的错写明每台发件箱里还有几封信、还有几条调用没结束。
359
361
 
360
362
  世界的连接活过每一代,而真的进程一死,连接上的临时表、视图、触发器就没了。所以每一代启动、建内核之前,世界把这条连接上宿主建的临时对象全部删掉,第一代也删,宿主在 `open()` 里建的临时对象留不下来;SQLite 自己管的(名字以 `sqlite_` 开头,如 `AUTOINCREMENT` 带出的 `sqlite_sequence`)删不掉,不删;虚拟表的影子表(`PRAGMA temp.table_list` 里 `type` 为 `shadow` 的)随虚拟表一起删,不单独删。注册的 SQL 函数删不掉,宿主每一代重新注册。
361
363
 
364
+ 宿主经 `world.node(name, addresses, setup, { open })` 交自己的库时,库里可能已经有内核的表(比如宿主自己的内核建的)。所以一台「迁移过」,世界看自己的 `sim_` 表在不在,不看内核的表:`sim_` 表和内核的迁移、宿主的 `setup` 在同一个事务里建,这台第一代的这个事务提交过才有。没迁移过的台(比如第一代的这次提交被注入失败),每步检查和按台读表的计数都跳过它;之后哪一代起来,迁移照常再跑。
365
+
362
366
  `sim/host.mjs` 是内核自己测试用的宿主,走世界自己的路;`test/ports.test.mjs` 是一个经 `node.ports` 接进来、只靠自己的钟重送的宿主。
363
367
 
364
368
  故障模型:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coda-console/kernel",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Coda 的调用内核:嵌在宿主进程里的库,只管幂等调用与责任转移",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/sim/world.mjs CHANGED
@@ -63,6 +63,23 @@ const lateEnding = row => {
63
63
  };
64
64
  const statusOf = row => JSON.stringify([row.status, row.rev, row.asked, row.asking, row.progress]);
65
65
 
66
+ /**
67
+ * Whether the kernel that sent the letter can read `receipt`, as far as it reads one: a refusal's code, which it
68
+ * compares; and for a call or cancel taken, the view's outcome, and without one the status's rev and asked, integers it
69
+ * compares and writes into its table. It reads nothing else of a view. An answer's receipt goes to its host, and the
70
+ * world reads its applied (C8), so that is a boolean.
71
+ */
72
+ function fits(envelope, receipt) {
73
+ if (receipt?.ok === false) return typeof receipt.code === 'string';
74
+ if (receipt?.ok !== true) return false;
75
+ if (envelope.kind === 'answer') return typeof receipt.applied === 'boolean';
76
+ if (envelope.kind !== 'call' && envelope.kind !== 'cancel') return true;
77
+ // A cancel from someone but the caller is taken without a view (the kernel's #revoke).
78
+ if (envelope.kind === 'cancel' && envelope.from !== envelope.call.from) return true;
79
+ const { view } = receipt;
80
+ return typeof view === 'object' && view !== null && (Boolean(view.outcome) || Number.isInteger(view.rev) && Number.isInteger(view.asked));
81
+ }
82
+
66
83
  // The connection one generation of an instance gets, over the database it runs on: it counts commits and fails them
67
84
  // on purpose, and once that generation dies every call through it, or through a statement it prepared, throws Crash.
68
85
  // Everything else passes through. The kernel's copy also records what it prepares.
@@ -131,7 +148,7 @@ function post(node, generation, to, envelope) {
131
148
  /**
132
149
  * Where a host stands between the world and its kernel, for one generation; each one left out is the world's own way.
133
150
  * - receive: the world hands the host a letter for this instance; `from` is the instance that put it into the network,
134
- * which the world vouches for (P3). Returns the receipt.
151
+ * which the world vouches for (P3). Returns the receipt, as its sending kernel reads one (P-receipt).
135
152
  * - send: this generation's kernel hands over a letter; the host puts it into the network with the `post` that
136
153
  * `node.ports()` returned, or not.
137
154
  * - recover: the world recovers this generation; the host calls `kernel.recover()`.
@@ -405,8 +422,10 @@ export class SimNode {
405
422
  if (this.called.size > 1) this.world.violations.push(`S9: one transaction of ${this.name} called handlers of ${[...this.called].join(', ')}`);
406
423
  }
407
424
 
425
+ // A first generation's migration committed. The world's own tables tell, as they commit with the kernel's: a database
426
+ // the host opened may come with the kernel's tables already made, and without the world's.
408
427
  migrated() {
409
- return this.q("SELECT 1 FROM sqlite_master WHERE name = 'kernel_runs'").get() !== undefined;
428
+ return this.q("SELECT 1 FROM sqlite_master WHERE name = 'sim_accepted'").get() !== undefined;
410
429
  }
411
430
 
412
431
  // The checks' own statements, kept per database; they bypass the generation's connection.
@@ -670,7 +689,14 @@ export class World {
670
689
  try {
671
690
  if (!ports.receive) return JSON.parse(JSON.stringify(node.kernel.receive(envelope)));
672
691
  this.count('ports: a letter in through the host');
673
- return JSON.parse(JSON.stringify(ports.receive(envelope, from)));
692
+ // What goes over the wire is what the sending kernel gets, and what is checked; nothing at all comes as null.
693
+ const receipt = JSON.parse(JSON.stringify(ports.receive(envelope, from)) ?? 'null');
694
+ // The sending kernel could not read it: the world tells of it, and the receipt is lost on its way.
695
+ if (!fits(envelope, receipt)) {
696
+ this.violations.push(`P-receipt: the host of ${node.name} answered a ${envelope.kind} letter with no receipt of the kernel's: ${JSON.stringify(receipt)}`);
697
+ return undefined;
698
+ }
699
+ return receipt;
674
700
  } catch (error) {
675
701
  if (!lost(error)) throw error;
676
702
  return undefined;
@@ -750,6 +776,8 @@ export class World {
750
776
  // Letters already in flight may still come back refused by faults chosen earlier; the pumps that count come after.
751
777
  await this.drain();
752
778
  await this.rest(() => false, resend);
779
+ // A host's fault met on the way is told of first, not as the kernel's invariant it leaves broken.
780
+ assert.deepEqual(this.violations, []);
753
781
  this.checkStatus();
754
782
  this.checkHalted();
755
783
  await this.rest(advance, resend);