@coda-console/kernel 0.1.0 → 0.2.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/docs/design.md +15 -1
- package/package.json +1 -1
- package/sim/world.mjs +149 -20
package/docs/design.md
CHANGED
|
@@ -345,7 +345,21 @@ kernel.pending({ address }); // { calls, runs, timers, letters }
|
|
|
345
345
|
|
|
346
346
|
测试是确定性的模拟:几个实例跑在一个进程里,送信的口、时钟、`schedule` 都由测试控制;时钟只由测试推进,`tick()` 由测试随时调用。测试辅助代码放 `sim/`。
|
|
347
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
|
|
348
|
+
模拟分两层。`sim/world.mjs` 是世界:几个实例、受控的网络与时钟、提交计数、崩溃与提交失败的注入,以及下面的检查。它不认识任何宿主,经 `@coda-console/kernel/sim` 导出,宿主可以把自己真的处理函数接进来撞故障:每一代启动时,世界在迁移的那个事务里调用宿主的 `setup`;`setup` 在事务里,只能在事务外调用的函数放进 `node.recovered(fn)`:世界在这一代的 `recover()` 成功之后立即在事务外调用它,每一代一次,宿主在这里做第 10 节说的重扫。宿主经世界给的连接读写自己的表,这一代死了之后这条连接上的读写一律抛错;经 `node.schedule` 排的事也不再执行,排的事里撞上崩溃或提交失败,由世界接住。世界在宿主登记处理函数时包一层,在处理函数自己的事务里往 `sim_` 开头的表记一行,再照登记的那个对象的方法调用宿主的函数;按处理函数计的检查(S2、S3、S8、S9,取消函数与状态处理函数的次数)读这些表,其余的读内核的表。记账不取决于宿主给了哪些函数:一个地址在这一代登记过任何东西(`handle` 或 `results`),它听到的状态、结局、晚到的结局世界都记一行,宿主给没给对应的函数都一样;取消函数、回答函数会改变内核的回执,给了才包。宿主可以加自己的每步检查和静止点检查。
|
|
349
|
+
|
|
350
|
+
宿主在内核外面有自己一层(收信时核对收发双方、发信时选路、恢复时起自己的重送)时,在 `setup` 里用 `const post = node.ports({ receive, send, recover })` 把这一层接在世界和内核之间。每一代一次,随新的一代重置;哪一样不给,那一样就照世界自己的走法:
|
|
351
|
+
|
|
352
|
+
- `receive(envelope, from)`:世界把一封信交给这台时调它,代替直接调 `kernel.receive`,返回回执。`from` 是把这封信放进网络的那一台,由世界作保(P3),不取自信封;测试直接调 `world.receive(to, envelope)` 时取信封的 `from` 所属的那台。这台没起来时不调。
|
|
353
|
+
- `send(to, envelope)`:这一代的内核交出一封信时调它,返回回执的 Promise。S6 仍在内核交出的那一刻检查。宿主要把信放进网络,调 `node.ports()` 返回的 `post(to, envelope)`;不给 `send` 时内核交出的信就直接 `post`。`post` 绑在返回它的那一代上:这一代死了之后再调,哪怕新的一代已经起来,也抛崩溃,不入网,不占新一代的额度。
|
|
354
|
+
- `recover()`:世界恢复这一代时调它,代替直接调 `kernel.recover()`。它正常返回,这台才算恢复好,再调 `node.recovered` 登记的函数;它撞上崩溃或提交失败,同 `recover()` 撞上。
|
|
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` 放进网络的封数),两者都跨代累计。
|
|
357
|
+
|
|
358
|
+
收尾时世界默认每一轮 `pump` 每台,这会替一个从不重送的宿主补上。宿主自己负责重送时,用 `world.quiesce(advance, { resend })`:`resend()` 每一轮调一次,让各台把留着的信再送一遍(比如把宿主的钟推过一个重送周期),世界不再 `pump`;推进时钟、`tick()` 仍由世界做。静不下来时抛出的错写明每台发件箱里还有几封信、还有几条调用没结束。
|
|
359
|
+
|
|
360
|
+
世界的连接活过每一代,而真的进程一死,连接上的临时表、视图、触发器就没了。所以每一代启动、建内核之前,世界把这条连接上宿主建的临时对象全部删掉,第一代也删,宿主在 `open()` 里建的临时对象留不下来;SQLite 自己管的(名字以 `sqlite_` 开头,如 `AUTOINCREMENT` 带出的 `sqlite_sequence`)删不掉,不删;虚拟表的影子表(`PRAGMA temp.table_list` 里 `type` 为 `shadow` 的)随虚拟表一起删,不单独删。注册的 SQL 函数删不掉,宿主每一代重新注册。
|
|
361
|
+
|
|
362
|
+
`sim/host.mjs` 是内核自己测试用的宿主,走世界自己的路;`test/ports.test.mjs` 是一个经 `node.ports` 接进来、只靠自己的钟重送的宿主。
|
|
349
363
|
|
|
350
364
|
故障模型:
|
|
351
365
|
|
package/package.json
CHANGED
package/sim/world.mjs
CHANGED
|
@@ -7,7 +7,8 @@ import { createKernel } from '../kernel.mjs';
|
|
|
7
7
|
//
|
|
8
8
|
// The world knows no host. Each instance runs whatever handlers its `setup` registers; the world wraps them to record,
|
|
9
9
|
// in its own `sim_` tables and in the handler's own transaction, what committed. Its checks read those tables and the
|
|
10
|
-
// kernel's, so they hold for any host; a host adds its own with onStep() and onQuiet().
|
|
10
|
+
// kernel's, so they hold for any host; a host adds its own with onStep() and onQuiet(). A host with a layer of its own
|
|
11
|
+
// around the kernel puts it between the world and the kernel with node.ports(), and the world checks that layer too.
|
|
11
12
|
|
|
12
13
|
/** The instance died right after a commit: the commit stands, everything in memory is gone. */
|
|
13
14
|
export class Crash extends Error {
|
|
@@ -94,14 +95,53 @@ function connection(node, generation, record) {
|
|
|
94
95
|
});
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
/**
|
|
99
|
+
* A generation's host puts a letter its kernel handed over into the network, for `to`, an address of some instance of
|
|
100
|
+
* the world. P-out counts by the letter's text, over the whole generation: a letter may be posted no more often than
|
|
101
|
+
* this generation's kernel has handed it over so far. A handing over the host did not post stays to its credit, so a
|
|
102
|
+
* letter handed over twice and posted once may be posted again later. Once that generation has died it throws Crash,
|
|
103
|
+
* whatever generation has come since.
|
|
104
|
+
* @param {SimNode} node
|
|
105
|
+
* @param {object} generation
|
|
106
|
+
* @param {string} to
|
|
107
|
+
* @param {object} envelope
|
|
108
|
+
* @returns {Promise<object>} the receipt; rejected when the letter or its receipt is lost
|
|
109
|
+
*/
|
|
110
|
+
function post(node, generation, to, envelope) {
|
|
111
|
+
if (generation.dead) throw new Crash(`${node.name} is down`);
|
|
112
|
+
const { handed } = generation;
|
|
113
|
+
const text = JSON.stringify(envelope);
|
|
114
|
+
const owed = handed.get(text) ?? 0;
|
|
115
|
+
if (owed === 0) node.world.violations.push(`P-out: ${node.name} posted a letter more often than its kernel handed it over: ${text}`);
|
|
116
|
+
else if (owed === 1) handed.delete(text);
|
|
117
|
+
else handed.set(text, owed - 1);
|
|
118
|
+
node.posted += 1;
|
|
119
|
+
return new Promise((resolve, reject) => node.world.net.push({ request: true, from: node, to, envelope: text, resolve, reject }));
|
|
120
|
+
}
|
|
121
|
+
|
|
97
122
|
/**
|
|
98
123
|
* What a host gives an instance: called on every generation inside its first transaction, with migrate(), so what it
|
|
99
124
|
* writes commits with it. It creates the host's own tables and registers its handlers with `kernel.handle` and
|
|
100
125
|
* `kernel.results`. Being inside a transaction, it cannot call what runs only outside one (recover(), tick(), pump(),
|
|
101
|
-
* inquire(), answer(), when()); it hands that to `node.recovered(fn)`.
|
|
126
|
+
* inquire(), answer(), when()); it hands that to `node.recovered(fn)`. A host with a layer of its own around the kernel
|
|
127
|
+
* puts it between the world and the kernel with `node.ports()`.
|
|
102
128
|
* @typedef {(context: { kernel: object, node: SimNode, world: World }) => void} Setup
|
|
103
129
|
*/
|
|
104
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Where a host stands between the world and its kernel, for one generation; each one left out is the world's own way.
|
|
133
|
+
* - 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.
|
|
135
|
+
* - send: this generation's kernel hands over a letter; the host puts it into the network with the `post` that
|
|
136
|
+
* `node.ports()` returned, or not.
|
|
137
|
+
* - recover: the world recovers this generation; the host calls `kernel.recover()`.
|
|
138
|
+
* @typedef {{
|
|
139
|
+
* receive?: (envelope: object, from: SimNode) => object,
|
|
140
|
+
* send?: (to: string, envelope: object) => Promise<object>,
|
|
141
|
+
* recover?: () => void,
|
|
142
|
+
* }} Ports
|
|
143
|
+
*/
|
|
144
|
+
|
|
105
145
|
/** One instance: a database that outlives crashes, and the kernel generation running on it. */
|
|
106
146
|
export class SimNode {
|
|
107
147
|
up = false;
|
|
@@ -129,6 +169,16 @@ export class SimNode {
|
|
|
129
169
|
host = undefined;
|
|
130
170
|
/** @type {(() => void) | undefined} what this generation's host does once its recover() has succeeded */
|
|
131
171
|
afterRecover = undefined;
|
|
172
|
+
/**
|
|
173
|
+
* calls of receive() on the host's handle while the world was handing this instance a letter, over all generations:
|
|
174
|
+
* the world's own call on its way, and every one of the host's, whatever the letter and at whatever depth. The kernel
|
|
175
|
+
* delivering to itself, as answer() does to a local address, does not go through the handle and does not count.
|
|
176
|
+
*/
|
|
177
|
+
taken = 0;
|
|
178
|
+
/** letters put into the network with a generation's post, over all generations */
|
|
179
|
+
posted = 0;
|
|
180
|
+
/** the world is handing this instance a letter */
|
|
181
|
+
delivering = false;
|
|
132
182
|
|
|
133
183
|
/**
|
|
134
184
|
* @param {World} world
|
|
@@ -152,10 +202,25 @@ export class SimNode {
|
|
|
152
202
|
* @param {{ recover?: boolean }} [options]
|
|
153
203
|
*/
|
|
154
204
|
boot({ recover = true } = {}) {
|
|
155
|
-
|
|
205
|
+
// A process that dies takes its connection's temporary tables, views and triggers with it; this connection outlives
|
|
206
|
+
// every generation, so the world drops them itself. The first generation too: nothing a host's open() made in temp
|
|
207
|
+
// is left for it. Triggers go first, as dropping a table drops its own. What SQLite keeps itself, such as the
|
|
208
|
+
// sqlite_sequence of an AUTOINCREMENT table, cannot be dropped and is no host's. Nor are a virtual table's shadow
|
|
209
|
+
// tables, which go with it: SQLite itself tells them apart, asking the table's module, in table_list. SQL functions
|
|
210
|
+
// cannot be taken back: a host registers its own again on every generation.
|
|
211
|
+
for (const { type, name } of this.q(`SELECT type, name FROM sqlite_temp_master AS m
|
|
212
|
+
WHERE type IN ('trigger', 'view', 'table') AND name NOT GLOB 'sqlite_*'
|
|
213
|
+
AND NOT EXISTS (SELECT 1 FROM pragma_table_list AS l WHERE l.schema = 'temp' AND l.name = m.name AND l.type = 'shadow')
|
|
214
|
+
ORDER BY CASE type WHEN 'trigger' THEN 0 WHEN 'view' THEN 1 ELSE 2 END`).all())
|
|
215
|
+
this.raw.exec(`DROP ${type.toUpperCase()} temp."${name.replaceAll('"', '""')}"`);
|
|
216
|
+
// handed: per letter text, how often this generation's kernel handed it over and its host has not posted it yet.
|
|
217
|
+
const generation = { dead: false, /** @type {Ports} */ ports: {}, handed: new Map(), recovers: 0 };
|
|
218
|
+
// Bound to this generation, so that a host of a generation that died cannot post through the next one.
|
|
219
|
+
generation.post = (to, envelope) => post(this, generation, to, envelope);
|
|
220
|
+
this.generation = generation;
|
|
156
221
|
this.db = connection(this, this.generation, false);
|
|
157
222
|
this.woke = null;
|
|
158
|
-
|
|
223
|
+
const kernel = createKernel({
|
|
159
224
|
db: connection(this, this.generation, true),
|
|
160
225
|
now: () => this.world.time,
|
|
161
226
|
send: (to, envelope) => this.world.send(this, to, envelope),
|
|
@@ -168,13 +233,33 @@ export class SimNode {
|
|
|
168
233
|
// What the host asks for, once it commits: a run asked to cancel, and a timer gone from its table without coming
|
|
169
234
|
// due, must be traceable to one of these (C7, 7).
|
|
170
235
|
for (const name of ['cancel', 'abandon', 'stop', 'unschedule']) {
|
|
171
|
-
const fn =
|
|
172
|
-
|
|
236
|
+
const fn = kernel[name].bind(kernel);
|
|
237
|
+
kernel[name] = (ref, ...rest) => {
|
|
173
238
|
this.intents.add(`${name} ${key(ref.from, ref.id)}`);
|
|
174
239
|
return fn(ref, ...rest);
|
|
175
240
|
};
|
|
176
241
|
}
|
|
177
|
-
|
|
242
|
+
// Counted for P-rec.
|
|
243
|
+
const recovering = kernel.recover.bind(kernel);
|
|
244
|
+
kernel.recover = () => {
|
|
245
|
+
generation.recovers += 1;
|
|
246
|
+
return recovering();
|
|
247
|
+
};
|
|
248
|
+
this.wrap(kernel);
|
|
249
|
+
// The handle the host and the world hold, counted for P-in by where a call of receive() comes from, not by the letter
|
|
250
|
+
// it carries: one through the handle during a delivery is taken. Every other function is the kernel's own, bound to
|
|
251
|
+
// it, so the kernel calling its own receive() never comes back through the handle.
|
|
252
|
+
const receive = envelope => {
|
|
253
|
+
if (this.delivering) this.taken += 1;
|
|
254
|
+
return kernel.receive(envelope);
|
|
255
|
+
};
|
|
256
|
+
this.kernel = new Proxy(kernel, {
|
|
257
|
+
get(target, name) {
|
|
258
|
+
if (name === 'receive') return receive;
|
|
259
|
+
const value = Reflect.get(target, name);
|
|
260
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
261
|
+
},
|
|
262
|
+
});
|
|
178
263
|
this.afterRecover = undefined;
|
|
179
264
|
this.up = true;
|
|
180
265
|
this.ready = false;
|
|
@@ -235,8 +320,14 @@ export class SimNode {
|
|
|
235
320
|
}
|
|
236
321
|
|
|
237
322
|
recover() {
|
|
323
|
+
const { generation } = this;
|
|
238
324
|
try {
|
|
239
|
-
|
|
325
|
+
if (generation.ports.recover) {
|
|
326
|
+
this.world.count('ports: a recovery through the host');
|
|
327
|
+
generation.ports.recover();
|
|
328
|
+
if (generation.recovers !== 1)
|
|
329
|
+
this.world.violations.push(`P-rec: the host of ${this.name} recovered, its kernel's recover() called ${generation.recovers} times, not once`);
|
|
330
|
+
} else this.kernel.recover();
|
|
240
331
|
this.ready = true;
|
|
241
332
|
this.afterRecover?.();
|
|
242
333
|
} catch (error) {
|
|
@@ -262,6 +353,17 @@ export class SimNode {
|
|
|
262
353
|
this.afterRecover = fn;
|
|
263
354
|
}
|
|
264
355
|
|
|
356
|
+
/**
|
|
357
|
+
* Puts the host between the world and this generation's kernel (see Ports). Called from the setup, once per
|
|
358
|
+
* generation; the next generation starts without them.
|
|
359
|
+
* @param {Ports} ports
|
|
360
|
+
* @returns {(to: string, envelope: object) => Promise<object>} this generation's post (see post())
|
|
361
|
+
*/
|
|
362
|
+
ports(ports) {
|
|
363
|
+
this.generation.ports = ports;
|
|
364
|
+
return this.generation.post;
|
|
365
|
+
}
|
|
366
|
+
|
|
265
367
|
/**
|
|
266
368
|
* The host's way to do something later: `fn` runs when the world runs its tasks, unless this generation has died by
|
|
267
369
|
* then. A crash or a failed commit in it is the world's business, as in act().
|
|
@@ -438,7 +540,10 @@ export class World {
|
|
|
438
540
|
node.timers = timers;
|
|
439
541
|
}
|
|
440
542
|
|
|
441
|
-
/**
|
|
543
|
+
/**
|
|
544
|
+
* The `send` every instance gets. It checks S6 at the moment of handing over; an answer is no letter of the outbox.
|
|
545
|
+
* The letter then goes to the host's send, or straight into the network.
|
|
546
|
+
*/
|
|
442
547
|
send(node, to, envelope) {
|
|
443
548
|
const call = envelope.kind === 'call' ? envelope : envelope.call;
|
|
444
549
|
const text = JSON.stringify(envelope);
|
|
@@ -448,7 +553,11 @@ export class World {
|
|
|
448
553
|
.get(call.from, call.id, to, envelope.digest) !== undefined
|
|
449
554
|
: node.q('SELECT envelope FROM kernel_outbox WHERE call_from = ? AND call_id = ?').all(call.from, call.id).some(row => row.envelope === text));
|
|
450
555
|
if (!owed) this.violations.push(`S6: ${node.name} sent a letter that is no committed outbox row nor an inquiry about an open call: ${text}`);
|
|
451
|
-
|
|
556
|
+
const { handed, ports } = node.generation;
|
|
557
|
+
handed.set(text, (handed.get(text) ?? 0) + 1);
|
|
558
|
+
if (!ports.send) return node.generation.post(to, envelope);
|
|
559
|
+
this.count('ports: a letter out through the host');
|
|
560
|
+
return ports.send(to, envelope);
|
|
452
561
|
}
|
|
453
562
|
|
|
454
563
|
/**
|
|
@@ -472,7 +581,7 @@ export class World {
|
|
|
472
581
|
if (this.chance(faults.duplicate)) this.net.push({ ...event, resolve() {}, reject() {} });
|
|
473
582
|
const envelope = JSON.parse(event.envelope);
|
|
474
583
|
this.notice('letter', envelope);
|
|
475
|
-
let receipt = this.receive(event.to, envelope);
|
|
584
|
+
let receipt = this.receive(event.to, envelope, event.from);
|
|
476
585
|
if (envelope.kind === 'answer') this.checkAnswer(envelope, receipt);
|
|
477
586
|
this.notice('receipt', envelope, receipt);
|
|
478
587
|
if (this.chance(faults.dropReceipt)) receipt = undefined;
|
|
@@ -545,14 +654,30 @@ export class World {
|
|
|
545
654
|
this.violations.push(`C8: an answer ${key(answer.from, answer.id)} got applied: ${receipt.applied}, but its number was ${taken ? '' : 'not '}taken`);
|
|
546
655
|
}
|
|
547
656
|
|
|
548
|
-
|
|
657
|
+
/**
|
|
658
|
+
* Hands a letter to the instance that owns `to`, through its host's receive when it gave one. The receipt comes back
|
|
659
|
+
* as it would over a wire; undefined when that instance is down or crashed meanwhile.
|
|
660
|
+
* @param {string} to
|
|
661
|
+
* @param {object} envelope
|
|
662
|
+
* @param {SimNode} [from] the instance that put the letter into the network; called directly, the owner of its `from`
|
|
663
|
+
*/
|
|
664
|
+
receive(to, envelope, from = this.owners.get(envelope.from)) {
|
|
549
665
|
const node = this.owners.get(to);
|
|
550
666
|
if (!node?.up) return undefined;
|
|
667
|
+
const { ports } = node.generation;
|
|
668
|
+
const taken = node.taken;
|
|
669
|
+
node.delivering = true;
|
|
551
670
|
try {
|
|
552
|
-
return JSON.parse(JSON.stringify(node.kernel.receive(envelope)));
|
|
671
|
+
if (!ports.receive) return JSON.parse(JSON.stringify(node.kernel.receive(envelope)));
|
|
672
|
+
this.count('ports: a letter in through the host');
|
|
673
|
+
return JSON.parse(JSON.stringify(ports.receive(envelope, from)));
|
|
553
674
|
} catch (error) {
|
|
554
675
|
if (!lost(error)) throw error;
|
|
555
676
|
return undefined;
|
|
677
|
+
} finally {
|
|
678
|
+
node.delivering = false;
|
|
679
|
+
if (node.taken - taken > 1)
|
|
680
|
+
this.violations.push(`P-in: the host of ${node.name} handed one ${envelope.kind} letter to its kernel ${node.taken - taken} times`);
|
|
556
681
|
}
|
|
557
682
|
}
|
|
558
683
|
|
|
@@ -617,19 +742,21 @@ export class World {
|
|
|
617
742
|
* checks their statuses there; then it rests again, calling `advance` every round, until a round finds nothing to
|
|
618
743
|
* advance. Otherwise it looks at none of the kernel's tables; what they hold at rest is for checkQuiet() to judge.
|
|
619
744
|
* @param {() => boolean} [advance] the hosts move on what they hold by a step; true while there was something to move
|
|
745
|
+
* @param {{ resend?: () => void }} [options] resend: how the hosts have every instance send what it keeps again, once a
|
|
746
|
+
* round; without it the world pumps every instance itself, which would make up for a host that never does
|
|
620
747
|
*/
|
|
621
|
-
async quiesce(advance = () => false) {
|
|
748
|
+
async quiesce(advance = () => false, { resend } = {}) {
|
|
622
749
|
this.calm = true;
|
|
623
750
|
// Letters already in flight may still come back refused by faults chosen earlier; the pumps that count come after.
|
|
624
751
|
await this.drain();
|
|
625
|
-
await this.rest(() => false);
|
|
752
|
+
await this.rest(() => false, resend);
|
|
626
753
|
this.checkStatus();
|
|
627
754
|
this.checkHalted();
|
|
628
|
-
await this.rest(advance);
|
|
755
|
+
await this.rest(advance, resend);
|
|
629
756
|
}
|
|
630
757
|
|
|
631
|
-
/** @param {() => boolean} advance */
|
|
632
|
-
async rest(advance) {
|
|
758
|
+
/** @param {() => boolean} advance @param {(() => void) | undefined} resend */
|
|
759
|
+
async rest(advance, resend) {
|
|
633
760
|
for (let round = 0; round < 50; round++) {
|
|
634
761
|
const crashed = this.crashed;
|
|
635
762
|
for (const node of this.nodes) {
|
|
@@ -637,7 +764,8 @@ export class World {
|
|
|
637
764
|
else if (!node.ready) node.recover();
|
|
638
765
|
}
|
|
639
766
|
const advanced = advance();
|
|
640
|
-
|
|
767
|
+
if (resend) resend();
|
|
768
|
+
else for (const node of this.nodes) this.pump(node);
|
|
641
769
|
// The clock passes every timer, and every instance ticks.
|
|
642
770
|
this.time = Math.max(this.time, ...this.nodes.filter(node => node.migrated()).map(node => node.q('SELECT max(due) AS due FROM kernel_timers').get().due ?? 0));
|
|
643
771
|
for (const node of this.nodes) this.tick(node);
|
|
@@ -646,7 +774,8 @@ export class World {
|
|
|
646
774
|
if (this.crashed === crashed && !advanced && this.net.length === 0 && this.tasks.length === 0
|
|
647
775
|
&& this.nodes.every(node => node.ready && node.count('SELECT count(*) AS n FROM kernel_timers') === 0)) return;
|
|
648
776
|
}
|
|
649
|
-
throw new Error(
|
|
777
|
+
throw new Error(`the world did not come to rest: ${this.nodes.map(node => `${node.name} has ${node.count('SELECT count(*) AS n FROM kernel_outbox')} `
|
|
778
|
+
+ `letters in its outbox and ${node.count(`SELECT count(*) AS n FROM kernel_calls WHERE state IN ${UNENDED}`)} calls not ended`).join(', ')}`);
|
|
650
779
|
}
|
|
651
780
|
|
|
652
781
|
/** At rest, before the held runs finish: a call that has not ended knows the status its run has now (C6). */
|