@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 +1 -1
- package/docs/design.md +89 -18
- package/package.json +1 -1
- package/sim/host.mjs +12 -0
- package/sim/scenarios.mjs +206 -12
- package/sim/world.mjs +166 -19
- package/src/kernel.mjs +114 -13
- package/src/schema.mjs +35 -7
package/sim/world.mjs
CHANGED
|
@@ -98,6 +98,7 @@ function connection(node, generation, record) {
|
|
|
98
98
|
if (sql === 'BEGIN IMMEDIATE') {
|
|
99
99
|
node.called = new Set();
|
|
100
100
|
node.intents = new Set();
|
|
101
|
+
node.forgetting = [];
|
|
101
102
|
}
|
|
102
103
|
if (sql === 'COMMIT') node.commit();
|
|
103
104
|
else raw.exec(sql);
|
|
@@ -152,10 +153,13 @@ function post(node, generation, to, envelope) {
|
|
|
152
153
|
* - send: this generation's kernel hands over a letter; the host puts it into the network with the `post` that
|
|
153
154
|
* `node.ports()` returned, or not.
|
|
154
155
|
* - recover: the world recovers this generation; the host calls `kernel.recover()`.
|
|
156
|
+
* - wake: this generation's kernel tells of its earliest due time, after the world has kept it in `node.woke` (7). The
|
|
157
|
+
* host sets its own timer and calls `kernel.tick()` when it goes off; the world no longer ticks this instance.
|
|
155
158
|
* @typedef {{
|
|
156
159
|
* receive?: (envelope: object, from: SimNode) => object,
|
|
157
160
|
* send?: (to: string, envelope: object) => Promise<object>,
|
|
158
161
|
* recover?: () => void,
|
|
162
|
+
* wake?: (due: number | null) => void,
|
|
159
163
|
* }} Ports
|
|
160
164
|
*/
|
|
161
165
|
|
|
@@ -182,6 +186,10 @@ export class SimNode {
|
|
|
182
186
|
* One asked in a savepoint that rolled back, inside a transaction that committed, still counts.
|
|
183
187
|
*/
|
|
184
188
|
intents = new Set();
|
|
189
|
+
/** @type {Array<{ name: string, table: string, row: object }>} the rows forget() took in the open transaction (9) */
|
|
190
|
+
forgetting = [];
|
|
191
|
+
/** the last commit took rows: observe() looks again at every run (9) */
|
|
192
|
+
forgot = false;
|
|
185
193
|
/** whatever the host's setup leaves here for its tests to reach; the world does not look at it */
|
|
186
194
|
host = undefined;
|
|
187
195
|
/** @type {(() => void) | undefined} what this generation's host does once its recover() has succeeded */
|
|
@@ -244,6 +252,7 @@ export class SimNode {
|
|
|
244
252
|
onError: error => this.report(error),
|
|
245
253
|
wake: at => {
|
|
246
254
|
this.woke = at;
|
|
255
|
+
generation.ports.wake?.(at);
|
|
247
256
|
},
|
|
248
257
|
schedule: fn => this.world.tasks.push(fn),
|
|
249
258
|
});
|
|
@@ -256,6 +265,9 @@ export class SimNode {
|
|
|
256
265
|
return fn(ref, ...rest);
|
|
257
266
|
};
|
|
258
267
|
}
|
|
268
|
+
// What forget() takes is checked as it takes it, and kept once it commits (9).
|
|
269
|
+
const forgetting = kernel.forget.bind(kernel);
|
|
270
|
+
kernel.forget = options => this.forgets(() => forgetting(options), options.before);
|
|
259
271
|
// Counted for P-rec.
|
|
260
272
|
const recovering = kernel.recover.bind(kernel);
|
|
261
273
|
kernel.recover = () => {
|
|
@@ -270,9 +282,15 @@ export class SimNode {
|
|
|
270
282
|
if (this.delivering) this.taken += 1;
|
|
271
283
|
return kernel.receive(envelope);
|
|
272
284
|
};
|
|
285
|
+
// A tick of a host given wake is its own, on its own clock; the kernel's recover() ticking itself is not one.
|
|
286
|
+
const tick = () => {
|
|
287
|
+
if (generation.ports.wake) this.world.count('ports: a tick through the host');
|
|
288
|
+
return kernel.tick();
|
|
289
|
+
};
|
|
273
290
|
this.kernel = new Proxy(kernel, {
|
|
274
291
|
get(target, name) {
|
|
275
292
|
if (name === 'receive') return receive;
|
|
293
|
+
if (name === 'tick') return tick;
|
|
276
294
|
const value = Reflect.get(target, name);
|
|
277
295
|
return typeof value === 'function' ? value.bind(target) : value;
|
|
278
296
|
},
|
|
@@ -336,6 +354,50 @@ export class SimNode {
|
|
|
336
354
|
};
|
|
337
355
|
}
|
|
338
356
|
|
|
357
|
+
/**
|
|
358
|
+
* forget() runs in `fn`, in the host's transaction. Every row it takes must have ended and been quiet since before
|
|
359
|
+
* `before`, with no letter about it in the outbox, and a call with its handler committed; an answer's number goes with
|
|
360
|
+
* its run, and only then. What it leaves must not be such a row. The rows taken wait for the commit, which keeps those
|
|
361
|
+
* still gone. A run it leaves that the world saw end before `before`, only as it is not quiet yet, is noted (9).
|
|
362
|
+
* @param {() => { calls: number, runs: number, answers: number }} fn
|
|
363
|
+
* @param {number} before
|
|
364
|
+
*/
|
|
365
|
+
forgets(fn, before) {
|
|
366
|
+
const tables = ['kernel_calls', 'kernel_runs', 'kernel_answers'];
|
|
367
|
+
const read = () => tables.map(table => new Map(this.q(`SELECT * FROM ${table}`).all().map(row => [key(row.from_addr, row.id), row])));
|
|
368
|
+
const owed = () => new Set(this.q('SELECT call_from, call_id FROM kernel_outbox').all().map(row => key(row.call_from, row.call_id)));
|
|
369
|
+
// A row without a quiet time is never quiet: one not ended, or a run imported once ended (5.5).
|
|
370
|
+
const quiet = row => row.quiet_at !== null && row.quiet_at < before;
|
|
371
|
+
const may = (table, name, row) => table === 'kernel_calls' ? ENDED.has(row.state) && quiet(row) && row.owed === 0 && !letters.has(name)
|
|
372
|
+
: ENDED.has(row.status) && quiet(row) && !letters.has(name);
|
|
373
|
+
const untouched = () => JSON.stringify(['kernel_outbox', 'kernel_timers'].map(table => this.q(`SELECT * FROM ${table} ORDER BY rowid`).all()));
|
|
374
|
+
const letters = owed();
|
|
375
|
+
const kept = untouched();
|
|
376
|
+
const was = read();
|
|
377
|
+
const counts = fn();
|
|
378
|
+
const now = read();
|
|
379
|
+
if (untouched() !== kept) this.world.violations.push(`9: forget() on ${this.name} changed its outbox or its timers`);
|
|
380
|
+
const runs = new Set();
|
|
381
|
+
for (const [index, table] of tables.entries()) {
|
|
382
|
+
const taken = [...was[index]].filter(([name]) => !now[index].has(name));
|
|
383
|
+
const told = counts[['calls', 'runs', 'answers'][index]];
|
|
384
|
+
if (told !== taken.length) this.world.violations.push(`9: forget() on ${this.name} said it took ${told} of ${table}, not ${taken.length}`);
|
|
385
|
+
for (const [name, row] of taken) {
|
|
386
|
+
if (table === 'kernel_runs') runs.add(name);
|
|
387
|
+
const fits = table === 'kernel_answers' ? runs.has(key(row.run_from, row.run_id)) : may(table, name, row);
|
|
388
|
+
if (!fits) this.world.violations.push(`9: forget() on ${this.name} took ${table} ${name}, which it may not: ${JSON.stringify(row)}`);
|
|
389
|
+
this.forgetting.push({ name, table, row });
|
|
390
|
+
}
|
|
391
|
+
for (const [name, row] of now[index]) {
|
|
392
|
+
const left = table === 'kernel_answers' ? runs.has(key(row.run_from, row.run_id)) : may(table, name, row);
|
|
393
|
+
if (left) this.world.violations.push(`9: forget() on ${this.name} left ${table} ${name}, which it should take: ${JSON.stringify(row)}`);
|
|
394
|
+
if (table === 'kernel_runs' && ENDED.has(row.status) && !letters.has(name) && this.world.endSeen.get(`${this.name} run ${name}`) < before)
|
|
395
|
+
this.world.spared.add(`${this.name} run ${name}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return counts;
|
|
399
|
+
}
|
|
400
|
+
|
|
339
401
|
recover() {
|
|
340
402
|
const { generation } = this;
|
|
341
403
|
try {
|
|
@@ -404,6 +466,15 @@ export class SimNode {
|
|
|
404
466
|
this.raw.exec('COMMIT');
|
|
405
467
|
world.commits += 1;
|
|
406
468
|
for (const intent of this.intents) world.intents.add(intent);
|
|
469
|
+
// A savepoint rolled back may have put back what forget() took in it.
|
|
470
|
+
for (const { name, table, row } of this.forgetting) {
|
|
471
|
+
if (this.q(`SELECT 1 FROM ${table} WHERE from_addr = ? AND id = ?`).get(row.from_addr, row.id)) continue;
|
|
472
|
+
const what = { kernel_calls: 'call', kernel_runs: 'run', kernel_answers: 'answer number' }[table];
|
|
473
|
+
// The first row taken under a name is the one the checks judge by: a later one is the record a late cancel left (9).
|
|
474
|
+
if (!world.forgotten.has(`${this.name} ${what} ${name}`)) world.forgotten.set(`${this.name} ${what} ${name}`, row);
|
|
475
|
+
world.count(`forget: ${what} taken${world.calm ? ' at rest' : ''}`);
|
|
476
|
+
this.forgot = true;
|
|
477
|
+
}
|
|
407
478
|
world.observe(this);
|
|
408
479
|
if (world.commits === world.crashAt) {
|
|
409
480
|
world.crashed += 1;
|
|
@@ -471,6 +542,15 @@ export class World {
|
|
|
471
542
|
intents = new Set();
|
|
472
543
|
/** @type {Map<string, number>} how often each of the rarer paths was taken */
|
|
473
544
|
tally = new Map();
|
|
545
|
+
/**
|
|
546
|
+
* @type {Map<string, object>} the rows forget() took, as they were then, by `<node> call|run|answer number <key>`: the
|
|
547
|
+
* checks that read a row it may take read this one once it is gone (9)
|
|
548
|
+
*/
|
|
549
|
+
forgotten = new Map();
|
|
550
|
+
/** @type {Map<string, number>} when check() first saw each ended row, by `<node> call|run <key>`: no earlier than it ended */
|
|
551
|
+
endSeen = new Map();
|
|
552
|
+
/** @type {Set<string>} runs, by `<node> run <key>`, that forget() left though they had ended before its `before` (9) */
|
|
553
|
+
spared = new Set();
|
|
474
554
|
violations = [];
|
|
475
555
|
/** @type {Array<() => void>} the hosts' own checks after every step, run by check() */
|
|
476
556
|
steps = [];
|
|
@@ -521,9 +601,13 @@ export class World {
|
|
|
521
601
|
// After every commit: which runs it recorded, and which lane letters it owed, for S5; which runs it asked to cancel,
|
|
522
602
|
// with the calls they awaited then, for L3; and where each timer that left its table went (7).
|
|
523
603
|
observe(node) {
|
|
604
|
+
// Once rows were taken, a new run may get the rowid of one taken (9): every run is looked at once more. A call's
|
|
605
|
+
// first record is the one that counts.
|
|
606
|
+
if (node.forgot) node.seen.run = 0;
|
|
607
|
+
node.forgot = false;
|
|
524
608
|
for (const row of node.q('SELECT rowid, from_addr, id FROM kernel_runs WHERE rowid > ?').all(node.seen.run)) {
|
|
525
609
|
node.seen.run = row.rowid;
|
|
526
|
-
this.order.set(key(row.from_addr, row.id), this.commits);
|
|
610
|
+
if (!this.order.has(key(row.from_addr, row.id))) this.order.set(key(row.from_addr, row.id), this.commits);
|
|
527
611
|
}
|
|
528
612
|
for (const row of node.q('SELECT seq, lane, kind, call_from, call_id FROM kernel_outbox WHERE seq > ?').all(node.seen.seq)) {
|
|
529
613
|
node.seen.seq = row.seq;
|
|
@@ -617,8 +701,14 @@ export class World {
|
|
|
617
701
|
if (moment === 'letter') {
|
|
618
702
|
if (envelope.kind === 'answer') this.noticeAnswer(envelope);
|
|
619
703
|
if (envelope.kind === 'cancel' && !row(envelope.to, 'kernel_runs')) this.count('cancel before its call');
|
|
704
|
+
if (envelope.kind === 'call' && row(envelope.to, 'kernel_runs') && this.spared.has(`${this.owners.get(envelope.to).name} run ${key(call.from, call.id)}`))
|
|
705
|
+
this.count(`forget: a call letter${envelope.confirmed ? ' (confirmed)' : ''} for a run kept, ended before the retention but not quiet`);
|
|
706
|
+
if ((envelope.kind === 'call' || envelope.kind === 'cancel') && !row(envelope.to, 'kernel_runs') && this.owners.get(envelope.to)?.migrated()
|
|
707
|
+
&& this.forgotten.has(`${this.owners.get(envelope.to).name} run ${key(call.from, call.id)}`)) this.count(`forget: a ${envelope.kind} letter${envelope.confirmed ? ' (confirmed)' : ''} for a run taken`);
|
|
620
708
|
if (envelope.kind === 'status' && ENDED.has(row(envelope.to, 'kernel_calls')?.state)) this.count('status after the outcome');
|
|
621
709
|
if (envelope.kind === 'result' && row(envelope.to, 'kernel_calls')?.abandoned === 1) this.count('outcome letter after an abandon');
|
|
710
|
+
if ((envelope.kind === 'status' || envelope.kind === 'result') && this.owners.get(envelope.to)?.migrated() && !row(envelope.to, 'kernel_calls')
|
|
711
|
+
&& this.forgotten.has(`${this.owners.get(envelope.to).name} call ${key(call.from, call.id)}`)) this.count(`forget: a ${envelope.kind} letter for a call taken`);
|
|
622
712
|
return;
|
|
623
713
|
}
|
|
624
714
|
if (!receipt?.ok || (envelope.kind !== 'call' && envelope.kind !== 'cancel')) {
|
|
@@ -637,6 +727,8 @@ export class World {
|
|
|
637
727
|
const node = this.owners.get(envelope.to);
|
|
638
728
|
if (!node?.up || !node.migrated()) return;
|
|
639
729
|
if (node.q('SELECT 1 FROM kernel_answers WHERE from_addr = ? AND id = ?').get(envelope.from, envelope.id)) this.count('answer: its number was taken before');
|
|
730
|
+
else if (this.forgotten.has(`${node.name} run ${key(envelope.call.from, envelope.call.id)}`)
|
|
731
|
+
&& !node.q('SELECT 1 FROM kernel_runs WHERE from_addr = ? AND id = ?').get(envelope.call.from, envelope.call.id)) this.count('forget: an answer to a run taken');
|
|
640
732
|
else if (node.q('SELECT 1 FROM kernel_runs WHERE from_addr = ? AND id = ? AND to_addr = ? AND status = ? AND asked = ?')
|
|
641
733
|
.get(envelope.call.from, envelope.call.id, envelope.to, 'input-required', envelope.asked)) this.count('answer: to its question');
|
|
642
734
|
}
|
|
@@ -728,10 +820,12 @@ export class World {
|
|
|
728
820
|
}
|
|
729
821
|
}
|
|
730
822
|
|
|
731
|
-
/** The host of `node` ticks, as its timer would make it (7). */
|
|
823
|
+
/** The host of `node` ticks, as its timer would make it (7); not one given wake, which ticks on its own clock. */
|
|
732
824
|
tick(node) {
|
|
733
825
|
if (!node.up && this.restart) node.boot();
|
|
734
826
|
if (!node.up) return;
|
|
827
|
+
// Such a tick would be counted as the host's own (see wrap()), and the tally would say what no host did.
|
|
828
|
+
if (node.generation.ports.wake) throw new Error(`the host of ${node.name} was given wake: its clock is the host's, and the world does not tick it`);
|
|
735
829
|
try {
|
|
736
830
|
node.kernel.tick();
|
|
737
831
|
} catch (error) {
|
|
@@ -769,7 +863,8 @@ export class World {
|
|
|
769
863
|
* advance. Otherwise it looks at none of the kernel's tables; what they hold at rest is for checkQuiet() to judge.
|
|
770
864
|
* @param {() => boolean} [advance] the hosts move on what they hold by a step; true while there was something to move
|
|
771
865
|
* @param {{ resend?: () => void }} [options] resend: how the hosts have every instance send what it keeps again, once a
|
|
772
|
-
* round; without it the world pumps every instance itself, which
|
|
866
|
+
* round, and tick an instance whose host was given wake; without it the world pumps every instance itself, which
|
|
867
|
+
* would make up for a host that never does
|
|
773
868
|
*/
|
|
774
869
|
async quiesce(advance = () => false, { resend } = {}) {
|
|
775
870
|
this.calm = true;
|
|
@@ -794,16 +889,43 @@ export class World {
|
|
|
794
889
|
const advanced = advance();
|
|
795
890
|
if (resend) resend();
|
|
796
891
|
else for (const node of this.nodes) this.pump(node);
|
|
797
|
-
// The clock passes every timer, and every instance ticks
|
|
892
|
+
// The clock passes every timer, and every instance ticks, but one whose host was given wake: its own timer, on its
|
|
893
|
+
// own clock, ticks it once its host moves that clock on, which resend() does.
|
|
798
894
|
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));
|
|
799
|
-
for (const node of this.nodes) this.tick(node);
|
|
895
|
+
for (const node of this.nodes) if (!node.generation.ports.wake) this.tick(node);
|
|
800
896
|
await this.drain();
|
|
801
897
|
await Promise.all(this.nodes.filter(node => node.up).map(node => node.kernel.idle()));
|
|
802
898
|
if (this.crashed === crashed && !advanced && this.net.length === 0 && this.tasks.length === 0
|
|
803
899
|
&& this.nodes.every(node => node.ready && node.count('SELECT count(*) AS n FROM kernel_timers') === 0)) return;
|
|
804
900
|
}
|
|
805
901
|
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')} `
|
|
806
|
-
+ `letters in its outbox
|
|
902
|
+
+ `letters in its outbox, ${node.count(`SELECT count(*) AS n FROM kernel_calls WHERE state IN ${UNENDED}`)} calls not ended `
|
|
903
|
+
+ `and ${node.count('SELECT count(*) AS n FROM kernel_timers')} timers`).join(', ')}`);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* A call of `node` as the checks read it: as it is, or as forget() took it (9).
|
|
908
|
+
* @param {SimNode} node
|
|
909
|
+
* @param {string} from
|
|
910
|
+
* @param {string} id
|
|
911
|
+
*/
|
|
912
|
+
call(node, from, id) {
|
|
913
|
+
return node.q('SELECT * FROM kernel_calls WHERE from_addr = ? AND id = ?').get(from, id) ?? this.forgotten.get(`${node.name} call ${key(from, id)}`);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* The run of a call to `to`, as it is or as forget() took it (9).
|
|
918
|
+
* @param {string} from
|
|
919
|
+
* @param {string} id
|
|
920
|
+
* @param {string} to
|
|
921
|
+
*/
|
|
922
|
+
run(from, id, to) {
|
|
923
|
+
const node = this.owners.get(to);
|
|
924
|
+
const row = node.q('SELECT * FROM kernel_runs WHERE from_addr = ? AND id = ?').get(from, id);
|
|
925
|
+
const gone = this.forgotten.get(`${node.name} run ${key(from, id)}`);
|
|
926
|
+
// The cancel of an abandoned call that its caller got through only after the retention finds the run forgotten,
|
|
927
|
+
// and leaves a record of its own (C7); the run's outcome is the one forget() took (9).
|
|
928
|
+
return row && !(row.op === null && gone) ? row : gone;
|
|
807
929
|
}
|
|
808
930
|
|
|
809
931
|
/** At rest, before the held runs finish: a call that has not ended knows the status its run has now (C6). */
|
|
@@ -824,7 +946,7 @@ export class World {
|
|
|
824
946
|
checkHalted() {
|
|
825
947
|
for (const [name, { node, calls }] of this.halted)
|
|
826
948
|
for (const call of calls) {
|
|
827
|
-
if (ENDED.has(
|
|
949
|
+
if (ENDED.has(this.call(node, call.from_addr, call.id).state)) continue;
|
|
828
950
|
const run = this.owners.get(call.to_addr).q('SELECT cancel_asked FROM kernel_runs WHERE from_addr = ? AND id = ?').get(call.from_addr, call.id);
|
|
829
951
|
assert.equal(run?.cancel_asked, 1, `L3: run ${name} was asked to cancel, but the callee of ${key(call.from_addr, call.id)}, which it awaited, was not`);
|
|
830
952
|
}
|
|
@@ -847,10 +969,20 @@ export class World {
|
|
|
847
969
|
for (const row of node.q('SELECT * FROM kernel_calls').all())
|
|
848
970
|
if (ENDED.has(row.state)) now.set(`${node.name} call ${key(row.from_addr, row.id)}`, ending(row, 'state'));
|
|
849
971
|
for (const row of node.q('SELECT * FROM kernel_runs').all())
|
|
850
|
-
if (ENDED.has(row.status))
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
972
|
+
if (ENDED.has(row.status)) {
|
|
973
|
+
const name = `${node.name} run ${key(row.from_addr, row.id)}`;
|
|
974
|
+
// A record a late cancel left: the run's outcome is the one forget() took (see run()).
|
|
975
|
+
now.set(name, ending((row.op === null && this.forgotten.get(name)) || row, 'status'));
|
|
976
|
+
}
|
|
977
|
+
for (const [name, text] of this.ended) {
|
|
978
|
+
if (!name.startsWith(`${node.name} `)) continue;
|
|
979
|
+
const gone = this.forgotten.get(name);
|
|
980
|
+
assert.equal(now.get(name) ?? (gone && ending(gone, name.startsWith(`${node.name} call `) ? 'state' : 'status')), text, `S1: the outcome of ${name} changed`);
|
|
981
|
+
}
|
|
982
|
+
for (const [name, text] of now) {
|
|
983
|
+
if (!this.ended.has(name)) this.endSeen.set(name, this.time);
|
|
984
|
+
this.ended.set(name, text);
|
|
985
|
+
}
|
|
854
986
|
|
|
855
987
|
for (const [table, what] of [['sim_accepted', 'S2: accept'], ['sim_results', 'S3: result'], ['sim_lates', 'S3: late'],
|
|
856
988
|
['sim_cancels', 'the cancel function']]) {
|
|
@@ -867,7 +999,7 @@ export class World {
|
|
|
867
999
|
// An outcome from the callee, or an abandoned call's late one, is the outcome of its run.
|
|
868
1000
|
for (const call of node.q('SELECT * FROM kernel_calls').all()) {
|
|
869
1001
|
if (!ENDED.has(call.state) || (call.local === 1 && call.late === null)) continue;
|
|
870
|
-
const run = this.
|
|
1002
|
+
const run = this.run(call.from_addr, call.id, call.to_addr);
|
|
871
1003
|
assert.ok(run, `S4: call ${key(call.from_addr, call.id)} ended but its callee has no run`);
|
|
872
1004
|
assert.equal(call.late === null ? ending(call, 'state') : lateEnding(call), ending(run, 'status'), `S4: call ${key(call.from_addr, call.id)} ended unlike its run`);
|
|
873
1005
|
}
|
|
@@ -930,16 +1062,31 @@ export class World {
|
|
|
930
1062
|
assert.equal(node.count('SELECT count(*) AS n FROM kernel_calls WHERE owed = 1'), 0, `results of ${node.name} are still owed at rest`);
|
|
931
1063
|
for (const call of node.q('SELECT * FROM kernel_calls').all())
|
|
932
1064
|
assert.ok(ENDED.has(call.state), `call ${key(call.from_addr, call.id)} is still ${call.state} at rest`);
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
if (run.
|
|
1065
|
+
// The runs it has, and those forget() took, which L2 holds for all the same (9).
|
|
1066
|
+
const runs = new Map(node.q('SELECT * FROM kernel_runs').all().map(row => [key(row.from_addr, row.id), row]));
|
|
1067
|
+
for (const [name, row] of this.forgotten)
|
|
1068
|
+
if (name.startsWith(`${node.name} run `) && !runs.has(key(row.from_addr, row.id))) runs.set(key(row.from_addr, row.id), row);
|
|
1069
|
+
for (const [name, row] of runs) {
|
|
1070
|
+
assert.ok(ENDED.has(row.status), `run ${name} is still ${row.status} at rest`);
|
|
1071
|
+
if (row.root === 1) continue;
|
|
1072
|
+
const run = this.run(row.from_addr, row.id, row.to_addr);
|
|
937
1073
|
const caller = this.owners.get(run.from_addr);
|
|
938
|
-
const call =
|
|
1074
|
+
const call = this.call(caller, run.from_addr, run.id);
|
|
939
1075
|
// An abandoned call has its own outcome; the run's comes as the late one, to `late` instead of `result`.
|
|
940
1076
|
const abandoned = call?.abandoned === 1;
|
|
941
|
-
|
|
942
|
-
|
|
1077
|
+
// forget() took an abandoned call before the run's own outcome came: a late outcome is no longer taken (9).
|
|
1078
|
+
if (abandoned && call.late === null && this.forgotten.has(`${caller.name} call ${name}`)) {
|
|
1079
|
+
for (const table of ['sim_results', 'sim_lates'])
|
|
1080
|
+
assert.equal(caller.count(`SELECT count(*) AS n FROM ${table} WHERE from_addr = ? AND id = ?`, run.from_addr, run.id), 0,
|
|
1081
|
+
`L2: ${name} was forgotten before its late outcome came, but a handler took one`);
|
|
1082
|
+
this.count('forget: an abandoned call taken before its late outcome');
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
// An outcome the caller's kernel wrote itself is not compared: the callee's, coming after, does not change it (C4).
|
|
1086
|
+
// Say a call imported open whose callee had no record: lost, and the cancel sent after it left a canceled record.
|
|
1087
|
+
if (!(call?.local === 1 && !abandoned))
|
|
1088
|
+
assert.equal(call && (abandoned ? call.late && lateEnding(call) : ending(call, 'state')), ending(run, 'status'),
|
|
1089
|
+
`L2: the caller of ${name} does not have its ${abandoned ? 'late ' : ''}outcome`);
|
|
943
1090
|
for (const [table, what] of [['sim_results', 'result'], ['sim_lates', 'late']])
|
|
944
1091
|
assert.equal(caller.count(`SELECT count(*) AS n FROM ${table} WHERE from_addr = ? AND id = ?`, run.from_addr, run.id),
|
|
945
1092
|
(what === 'late') === abandoned ? 1 : 0, `L2: ${what} of ${name} was not committed exactly as often as it should`);
|
package/src/kernel.mjs
CHANGED
|
@@ -135,7 +135,7 @@ class Kernel {
|
|
|
135
135
|
// would break the later one.
|
|
136
136
|
if (version > MIGRATIONS.length) throw new Error(`the database is at schema version ${version}, newer than this kernel's ${MIGRATIONS.length}`);
|
|
137
137
|
if (version === MIGRATIONS.length) return;
|
|
138
|
-
for (const step of MIGRATIONS.slice(version)) this.#db.exec(step);
|
|
138
|
+
for (const step of MIGRATIONS.slice(version)) this.#db.exec(typeof step === 'string' ? step : step(this.#now()));
|
|
139
139
|
this.#q(SQL.setVersion).run(MIGRATIONS.length);
|
|
140
140
|
}
|
|
141
141
|
|
|
@@ -171,6 +171,47 @@ class Kernel {
|
|
|
171
171
|
return defined({ from, id, to, op, state: 'sending', memo, by: by && { from: by.from, id: by.id }, awaited });
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Imports a call its callee had already taken before this kernel kept it (导入): it is open, at version 0, and owes no
|
|
176
|
+
* letter. A call letter would have a callee without the record run it again; recover() inquires instead (C5).
|
|
177
|
+
* @param {{ from: string, id: string, to: string, op: string, input?: unknown, lane?: string, by?: Ref,
|
|
178
|
+
* awaited?: boolean, parent?: unknown, memo?: unknown }} call
|
|
179
|
+
*/
|
|
180
|
+
adoptCall({ from, id, to, op, input = null, lane = null, by, awaited = false, parent = null, memo }) {
|
|
181
|
+
this.#adopting();
|
|
182
|
+
const digest = digestOf([to, op, input, parent]);
|
|
183
|
+
const found = this.#found(from, id, digest);
|
|
184
|
+
if (found) return found;
|
|
185
|
+
awaited = this.#awaits(from, by, awaited);
|
|
186
|
+
return callView(this.#q(SQL.adoptCall).get(from, id, to, op, digest, json(input), json(parent), lane, by?.from ?? null, by?.id ?? null,
|
|
187
|
+
awaited ? 1 : 0, json(memo)));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Imports a run taken before this kernel kept it (导入): no handler is called and no letter owed. Without an outcome
|
|
192
|
+
* it is working, or waiting for input on its first question; with one, it has ended.
|
|
193
|
+
* @param {{ from: string, id: string, to: string, op: string, input?: unknown, parent?: unknown, awaited?: boolean,
|
|
194
|
+
* status?: 'working' | 'input-required', asking?: unknown, progress?: unknown, outcome?: Outcome }} run
|
|
195
|
+
*/
|
|
196
|
+
adoptRun({ from, id, to, op, input = null, parent = null, awaited = false, status = 'working', asking, progress, outcome }) {
|
|
197
|
+
this.#adopting();
|
|
198
|
+
// A run without op is the record a cancel leaves, whose digest is never compared (C1).
|
|
199
|
+
if (op == null) throw new Error(`an imported run needs its op: ${from}/${id}`);
|
|
200
|
+
const digest = digestOf([to, op, input, parent]);
|
|
201
|
+
const found = this.#q(SQL.run).get(from, id);
|
|
202
|
+
if (found) {
|
|
203
|
+
if (found.digest !== digest) throw new Error(`run ${from}/${id} already exists with other content`);
|
|
204
|
+
return adoptedView(found);
|
|
205
|
+
}
|
|
206
|
+
if (outcome) {
|
|
207
|
+
if (!ENDED.has(outcome.outcome)) throw new Error(`${outcome.outcome} is not an outcome`);
|
|
208
|
+
status = outcome.outcome;
|
|
209
|
+
} else if (!WORKING.has(status)) throw new Error(`a run cannot be imported as ${status}`);
|
|
210
|
+
const asks = status === 'input-required';
|
|
211
|
+
return adoptedView(this.#q(SQL.adoptRun).get(from, id, to, op, digest, json(parent), awaited ? 1 : 0, status, asks ? 1 : 0,
|
|
212
|
+
asks ? json(asking) : null, json(outcome?.result), outcome?.outcome === 'failed' ? json(written(outcome.error)) : null, json(progress)));
|
|
213
|
+
}
|
|
214
|
+
|
|
174
215
|
/**
|
|
175
216
|
* A call to make once `delayMs` has passed (7). Until then it is a timer, which shares its id with the calls.
|
|
176
217
|
* @param {{ from: string, id: string, delayMs: number, to: string, op: string, input?: unknown, lane?: string, by?: Ref,
|
|
@@ -355,7 +396,25 @@ class Kernel {
|
|
|
355
396
|
const row = this.#made(call);
|
|
356
397
|
if (ENDED.has(row.state)) return;
|
|
357
398
|
this.#recall(row);
|
|
358
|
-
this.#q(SQL.abandonCall).run(json({ code: 'abandoned', message: 'the caller abandoned this call', effects: 'unknown' }),
|
|
399
|
+
this.#q(SQL.abandonCall).run(json({ code: 'abandoned', message: 'the caller abandoned this call', effects: 'unknown' }), this.#now(),
|
|
400
|
+
row.from_addr, row.id);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Deletes what has been quiet since before `before` and is no longer needed to tell a repeat (9): ended calls whose
|
|
405
|
+
* handler has committed, and ended runs, about which no letter is owed and none has left the outbox since, with the
|
|
406
|
+
* numbers of the answers a deleted run took. A repeat comes no later than the host's retention after the last letter
|
|
407
|
+
* about it, which `before` stands for; after it, its id is unknown here.
|
|
408
|
+
* @param {{ before: number }} options
|
|
409
|
+
* @returns {{ calls: number, runs: number, answers: number }} how many rows of each were deleted
|
|
410
|
+
*/
|
|
411
|
+
forget({ before }) {
|
|
412
|
+
this.#inside();
|
|
413
|
+
const calls = this.#q(SQL.forgetCalls).run(before).changes;
|
|
414
|
+
const runs = this.#q(SQL.forgetRuns).all(before);
|
|
415
|
+
let answers = 0;
|
|
416
|
+
for (const run of runs) answers += this.#q(SQL.forgetAnswers).run(run.from_addr, run.id).changes;
|
|
417
|
+
return { calls, runs: runs.length, answers };
|
|
359
418
|
}
|
|
360
419
|
|
|
361
420
|
/**
|
|
@@ -402,7 +461,12 @@ class Kernel {
|
|
|
402
461
|
for (const { from_addr, id } of this.#q(SQL.owedCalls).all()) this.transaction(() => this.#notify(from_addr, id));
|
|
403
462
|
const rereads = [...this.#rereads.values()];
|
|
404
463
|
this.#rereads.clear();
|
|
405
|
-
|
|
464
|
+
// Read the row itself, not through inquire(): forget() may have deleted a call kept here since, and that is no
|
|
465
|
+
// reason to throw and lose the other rereads (9, C5).
|
|
466
|
+
for (const call of rereads) {
|
|
467
|
+
const row = this.#q(SQL.call).get(call.from, call.id);
|
|
468
|
+
if (row?.state === 'open') this.#ask(row);
|
|
469
|
+
}
|
|
406
470
|
this.#kick();
|
|
407
471
|
}
|
|
408
472
|
|
|
@@ -428,6 +492,13 @@ class Kernel {
|
|
|
428
492
|
if (!this.#tx.open) throw new Error('this must be called inside kernel.transaction()');
|
|
429
493
|
}
|
|
430
494
|
|
|
495
|
+
// Imports come before recover(): its first rereads and letters are what moves them on, and no letter or memory of
|
|
496
|
+
// this instance has met them yet (R1, R2).
|
|
497
|
+
#adopting() {
|
|
498
|
+
this.#inside();
|
|
499
|
+
if (this.#started) throw new Error('imports must come before recover()');
|
|
500
|
+
}
|
|
501
|
+
|
|
431
502
|
#outside() {
|
|
432
503
|
if (this.#closed) throw new Error('the kernel is closed');
|
|
433
504
|
if (this.#tx.open) throw new Error('this opens its own transaction and must not be called inside one');
|
|
@@ -573,7 +644,7 @@ class Kernel {
|
|
|
573
644
|
|
|
574
645
|
// Writes a run's outcome and owes its caller a letter carrying it in place of any status not yet confirmed (C3, C4, C6).
|
|
575
646
|
#end(from, id, { outcome, result, error }, progress) {
|
|
576
|
-
const row = this.#q(SQL.endRun).get(outcome, json(result), json(error), json(progress), from, id);
|
|
647
|
+
const row = this.#q(SQL.endRun).get(outcome, json(result), json(error), json(progress), this.#now(), from, id);
|
|
577
648
|
if (row.root === 1) return;
|
|
578
649
|
this.#q(SQL.dropStatusLetter).get(from, id);
|
|
579
650
|
this.#owe(from, null, 'result', { from, id },
|
|
@@ -658,13 +729,24 @@ class Kernel {
|
|
|
658
729
|
* @param {Ref} call
|
|
659
730
|
* @param {string} sender
|
|
660
731
|
* @param {{ rev: number, status: string, asked: number, asking?: unknown, progress?: unknown }} status
|
|
732
|
+
* @param {boolean} [muted] the callee's cancel function may have run in this same transaction (S9)
|
|
733
|
+
* @param {boolean} [receipt] this is the receipt of the letter just dropped from the outbox, not a status letter
|
|
661
734
|
*/
|
|
662
|
-
#hear(call, sender, { rev, status, asked, asking, progress }) {
|
|
735
|
+
#hear(call, sender, { rev, status, asked, asking, progress }, muted = false, receipt = false) {
|
|
663
736
|
const row = this.#known(call, sender, 'a status');
|
|
664
|
-
|
|
737
|
+
// A receipt for a sending call comes as its letter is dropped: whatever version it says, the callee took the call,
|
|
738
|
+
// and the call leaves sending (S10). A status letter at a version not above the row's is an old one, and changes
|
|
739
|
+
// nothing (C6).
|
|
740
|
+
if (!row || ENDED.has(row.state) || (!(receipt && row.state === 'sending') && rev <= row.rev)) return { ok: true };
|
|
665
741
|
if (row.state === 'sending') this.#q(SQL.dropCallLetter).run(call.from, call.id);
|
|
666
742
|
const known = WORKING.has(status) ? status : 'working';
|
|
667
743
|
const now = this.#q(SQL.hearCall).get(known, rev, asked, known === 'input-required' ? json(asking) : null, json(progress), call.from, call.id);
|
|
744
|
+
// The callee's cancel function may have just run in this transaction: the caller's are not called (S9).
|
|
745
|
+
if (muted) return { ok: true };
|
|
746
|
+
// The receipt that takes a call is no status of its own (C6): at version 1, or below it from a callee that breaks
|
|
747
|
+
// C6, it is the run as it was taken. An imported run may have been taken waiting for input (5.5), and no letter of
|
|
748
|
+
// its own will ask: the caller hears the question here.
|
|
749
|
+
if (row.state === 'sending' && rev <= 1 && known === 'working') return { ok: true };
|
|
668
750
|
// T6: a status handler that throws loses its own changes only, and is not called again.
|
|
669
751
|
try {
|
|
670
752
|
this.transaction(() => this.#results.get(call.from)?.status?.(callView(now)));
|
|
@@ -718,7 +800,7 @@ class Kernel {
|
|
|
718
800
|
// confirmed goes too; an abandoned call keeps its cancel until confirmed.
|
|
719
801
|
if (row.state === 'sending') this.#q(SQL.dropCallLetter).run(call.from, call.id);
|
|
720
802
|
this.#q(SQL.dropCancelLetter).run(call.from, call.id);
|
|
721
|
-
this.#q(SQL.endCall).run(outcome, json(result), json(error), json(progress), local ? 1 : 0, call.from, call.id);
|
|
803
|
+
this.#q(SQL.endCall).run(outcome, json(result), json(error), json(progress), local ? 1 : 0, this.#now(), call.from, call.id);
|
|
722
804
|
}
|
|
723
805
|
this.#notify(call.from, call.id);
|
|
724
806
|
return { ok: true };
|
|
@@ -743,9 +825,14 @@ class Kernel {
|
|
|
743
825
|
* @param {object} row
|
|
744
826
|
* @param {Receipt} receipt
|
|
745
827
|
* @param {boolean} [direct] the letter was delivered locally, in this same transaction
|
|
828
|
+
* @param {boolean} [muted] the callee may have run a function of its own in this same transaction (S9)
|
|
746
829
|
*/
|
|
747
|
-
#heard(row, receipt, direct = false) {
|
|
830
|
+
#heard(row, receipt, direct = false, muted = false) {
|
|
748
831
|
this.#q(SQL.dropLetter).run(row.seq);
|
|
832
|
+
// A copy of the letter may still be on its way, and the other side may still send what it answers, until it heard
|
|
833
|
+
// this one: an ended row is kept for a repeat from now on (9). A call or cancel is about the caller's call, a status
|
|
834
|
+
// or an outcome about the callee's run.
|
|
835
|
+
this.#q(row.kind === 'call' || row.kind === 'cancel' ? SQL.quietCall : SQL.quietRun).run(this.#now(), row.call_from, row.call_id);
|
|
749
836
|
const call = { from: row.call_from, id: row.call_id };
|
|
750
837
|
if (!receipt.ok) {
|
|
751
838
|
if (row.kind === 'call') this.#conclude(call, row.to_addr, failure(receipt.code, `${row.to_addr} answered ${receipt.code}`), true);
|
|
@@ -759,10 +846,8 @@ class Kernel {
|
|
|
759
846
|
// A local callee has just run its own functions in this transaction; its outcome and its status come in their own
|
|
760
847
|
// letters (4, C6).
|
|
761
848
|
if (!direct && receipt.view.outcome) this.#conclude(call, row.to_addr, receipt.view);
|
|
762
|
-
else
|
|
763
|
-
|
|
764
|
-
if (!direct) this.#hear(call, row.to_addr, receipt.view);
|
|
765
|
-
}
|
|
849
|
+
else if (direct) this.#q(SQL.openCall).run(call.from, call.id);
|
|
850
|
+
else this.#hear(call, row.to_addr, receipt.view, muted, true);
|
|
766
851
|
}
|
|
767
852
|
|
|
768
853
|
/**
|
|
@@ -884,8 +969,19 @@ class Kernel {
|
|
|
884
969
|
#deliver(row) {
|
|
885
970
|
try {
|
|
886
971
|
const receipt = this.transaction(() => {
|
|
972
|
+
// A callee that took this call before, an imported run say, runs nothing of its own now and owes nothing more:
|
|
973
|
+
// its receipt is all the caller hears, as from a callee elsewhere (5.5, S9). So too a cancel that finds the run
|
|
974
|
+
// ended, and one in the place of the call letter that leaves such a run as it was before the cancel, at the same
|
|
975
|
+
// version: no letter of its own will tell either. The run may have changed since it was imported, its status
|
|
976
|
+
// letter dropped by a caller that had no call yet. A cancel function that changed the run owes its own letters
|
|
977
|
+
// (C6, C7).
|
|
978
|
+
const run = row.kind === 'call' || row.kind === 'cancel' ? this.#q(SQL.run).get(row.call_from, row.call_id) : undefined;
|
|
887
979
|
const receipt = this.#take(JSON.parse(row.envelope));
|
|
888
|
-
|
|
980
|
+
const taken = row.kind === 'call' ? run !== undefined
|
|
981
|
+
: ENDED.has(run?.status) || (run !== undefined && !receipt.view.outcome && receipt.view.rev === run.rev
|
|
982
|
+
&& this.#q(SQL.call).get(row.call_from, row.call_id).state === 'sending');
|
|
983
|
+
// A cancel delivered here may have just run the callee's cancel function: its receipt calls no status (S9).
|
|
984
|
+
if (!HOLD.has(receipt.code)) this.#heard(row, receipt, !taken, row.kind === 'cancel');
|
|
889
985
|
return receipt;
|
|
890
986
|
});
|
|
891
987
|
if (HOLD.has(receipt.code)) this.#held.add(row.seq);
|
|
@@ -1005,6 +1101,11 @@ function runView(row) {
|
|
|
1005
1101
|
...(row.root === 1 && { root: true }) });
|
|
1006
1102
|
}
|
|
1007
1103
|
|
|
1104
|
+
// An imported run as adoptRun() returns it: as runs() lists it, with its outcome once ended.
|
|
1105
|
+
function adoptedView(row) {
|
|
1106
|
+
return defined({ ...runView(row), ...(ENDED.has(row.status) && outcomeOf(row.status, row)) });
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1008
1109
|
// The run that made a call or a timer (C9).
|
|
1009
1110
|
function byOf(row) {
|
|
1010
1111
|
return row.by_from === null ? undefined : { from: row.by_from, id: row.by_id };
|