@coda-console/kernel 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -2
- package/docs/design.md +402 -0
- package/kernel.mjs +1 -0
- package/package.json +20 -3
- package/sim/host.mjs +256 -0
- package/sim/scenarios.mjs +484 -0
- package/sim/solo.mjs +47 -0
- package/sim/world.mjs +793 -0
- package/src/kernel.mjs +1038 -0
- package/src/schema.mjs +169 -0
- package/src/transaction.mjs +75 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { advance, CHANGES, held, host } from './host.mjs';
|
|
3
|
+
import { flush, NO_FAULTS, random, World } from './world.mjs';
|
|
4
|
+
|
|
5
|
+
const ENDED = new Set(['completed', 'failed', 'canceled']);
|
|
6
|
+
|
|
7
|
+
// Small scenarios, each run once without a crash and then once per commit with a crash right after it (12. 怎么测).
|
|
8
|
+
// The world restarts a crashed instance at once, so a scenario is the same script whatever crashed.
|
|
9
|
+
export const SMALL = {
|
|
10
|
+
// Calls between two addresses of one instance: every letter goes through local delivery. A cancel function there
|
|
11
|
+
// ends its run at once, so the cancel's own receipt carries the outcome.
|
|
12
|
+
async local(world) {
|
|
13
|
+
const a = world.node('A', ['a1', 'a2'], host);
|
|
14
|
+
world.start();
|
|
15
|
+
world.act(a, kernel => kernel.call({ from: 'a1', id: 'l1', to: 'a2', op: 'work', input: { mode: 'complete', n: 1 }, lane: 'x' }));
|
|
16
|
+
world.act(a, kernel => kernel.call({ from: 'a1', id: 'l2', to: 'a2', op: 'work', input: { mode: 'hold', goal: 'failed' }, lane: 'x',
|
|
17
|
+
memo: { resultThrows: 1 } }));
|
|
18
|
+
await world.drain();
|
|
19
|
+
world.check();
|
|
20
|
+
world.act(a, kernel => kernel.call({ from: 'a2', id: 'l3', to: 'a1', op: 'nope' }));
|
|
21
|
+
await world.drain();
|
|
22
|
+
world.check();
|
|
23
|
+
world.inquire(a, { from: 'a1', id: 'l2' });
|
|
24
|
+
world.check();
|
|
25
|
+
world.act(a, kernel => kernel.call({ from: 'a1', id: 'l4', to: 'a2', op: 'work', input: { mode: 'hold', cancelSettles: true } }));
|
|
26
|
+
await world.drain();
|
|
27
|
+
world.act(a, kernel => kernel.cancel({ from: 'a1', id: 'l4' }));
|
|
28
|
+
await world.drain();
|
|
29
|
+
world.check();
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
// One lane to another instance, behind a busy callee, and a host that issues the same call again after a restart.
|
|
33
|
+
async lane(world) {
|
|
34
|
+
const a = world.node('A', ['a1'], host);
|
|
35
|
+
const b = world.node('B', ['b1'], host);
|
|
36
|
+
world.start();
|
|
37
|
+
const first = { from: 'a1', id: 'r1', to: 'b1', op: 'work', input: { mode: 'busy', n: 1 }, lane: 'x' };
|
|
38
|
+
world.act(a, kernel => {
|
|
39
|
+
kernel.call(first);
|
|
40
|
+
kernel.call({ from: 'a1', id: 'r2', to: 'b1', op: 'work', input: { mode: 'hold', goal: 'completed' }, lane: 'x' });
|
|
41
|
+
});
|
|
42
|
+
world.act(a, kernel => kernel.call({ from: 'a1', id: 'r3', to: 'b1', op: 'work', input: { mode: 'fail' }, lane: 'x',
|
|
43
|
+
memo: { resultThrows: 2 } }));
|
|
44
|
+
await world.drain();
|
|
45
|
+
world.check();
|
|
46
|
+
world.act(a, kernel => kernel.call(first));
|
|
47
|
+
world.pump(a);
|
|
48
|
+
await world.drain();
|
|
49
|
+
world.check();
|
|
50
|
+
world.act(b, kernel => {
|
|
51
|
+
for (const row of held(b)) b.host.finish(kernel, row);
|
|
52
|
+
});
|
|
53
|
+
await world.drain();
|
|
54
|
+
world.check();
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// Calls both ways between two instances, with the ways a callee can refuse.
|
|
58
|
+
async across(world) {
|
|
59
|
+
const a = world.node('A', ['a1'], host);
|
|
60
|
+
const b = world.node('B', ['b1'], host);
|
|
61
|
+
world.start();
|
|
62
|
+
world.act(a, kernel => kernel.call({ from: 'a1', id: 'x1', to: 'b1', op: 'work', input: { mode: 'reject' } }));
|
|
63
|
+
world.act(b, kernel => kernel.call({ from: 'b1', id: 'x2', to: 'a1', op: 'work', input: { mode: 'throw' }, lane: 'y' }));
|
|
64
|
+
world.act(b, kernel => kernel.call({ from: 'b1', id: 'x3', to: 'a1', op: 'work', input: { mode: 'hold', goal: 'canceled' }, lane: 'y' }));
|
|
65
|
+
await world.drain();
|
|
66
|
+
world.check();
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
// A run reports its status as it goes, makes calls it awaits and one it does not, waits for one and stops waiting for
|
|
70
|
+
// the other, and ends with its last progress. Its caller's status handler throws while the world is not calm.
|
|
71
|
+
async tree(world) {
|
|
72
|
+
const a = world.node('A', ['a1'], host);
|
|
73
|
+
const b = world.node('B', ['b1', 'b2'], host);
|
|
74
|
+
world.start();
|
|
75
|
+
const kids = [
|
|
76
|
+
{ to: 'a1', mode: 'hold', goal: 'completed', awaited: true },
|
|
77
|
+
{ to: 'b2', mode: 'hold', goal: 'canceled', awaited: true },
|
|
78
|
+
{ to: 'b2', mode: 'complete', awaited: false },
|
|
79
|
+
];
|
|
80
|
+
world.act(a, kernel => kernel.call({ from: 'a1', id: 't1', to: 'b1', op: 'work', input: { mode: 'parent', goal: 'failed', kids },
|
|
81
|
+
memo: { statusThrows: true } }));
|
|
82
|
+
await world.drain();
|
|
83
|
+
world.check();
|
|
84
|
+
for (const change of [CHANGES[1], CHANGES[2], CHANGES[4], CHANGES[3]]) {
|
|
85
|
+
for (const node of [a, b]) for (const row of held(node)) world.act(node, kernel => node.host.nudge(kernel, row, change));
|
|
86
|
+
await world.drain();
|
|
87
|
+
world.check();
|
|
88
|
+
}
|
|
89
|
+
const parent = () => held(b).filter(row => row.id === 't1');
|
|
90
|
+
for (const row of parent()) world.act(b, kernel => b.host.finish(kernel, row));
|
|
91
|
+
for (const row of held(a)) world.act(a, kernel => a.host.finish(kernel, row));
|
|
92
|
+
await world.drain();
|
|
93
|
+
world.check();
|
|
94
|
+
for (const row of parent()) world.act(b, kernel => b.host.finish(kernel, row, { detach: true }));
|
|
95
|
+
await world.drain();
|
|
96
|
+
world.check();
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
// A root run reports its status to no one, makes calls it awaits, and ends once they have.
|
|
100
|
+
async root(world) {
|
|
101
|
+
const a = world.node('A', ['a1'], host);
|
|
102
|
+
const b = world.node('B', ['b1'], host);
|
|
103
|
+
world.start();
|
|
104
|
+
world.act(a, kernel => a.host.root(kernel, { address: 'a1', id: 'r1', goal: 'canceled', kids: [
|
|
105
|
+
{ to: 'b1', mode: 'hold', goal: 'completed', awaited: true },
|
|
106
|
+
{ to: 'a1', mode: 'complete', awaited: true },
|
|
107
|
+
] }));
|
|
108
|
+
await world.drain();
|
|
109
|
+
world.check();
|
|
110
|
+
for (const node of [a, b]) for (const row of held(node)) world.act(node, kernel => node.host.nudge(kernel, row, CHANGES[2]));
|
|
111
|
+
await world.drain();
|
|
112
|
+
world.check();
|
|
113
|
+
for (const node of [a, b]) for (const row of held(node)) world.act(node, kernel => node.host.finish(kernel, row));
|
|
114
|
+
await world.drain();
|
|
115
|
+
world.check();
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
// Cancels at each point of a call's life: while sending, with another call behind it in its lane; in flight, with the
|
|
119
|
+
// cancel delivered first; open, where the cancel function throws; and ended, where it does nothing.
|
|
120
|
+
async cancel(world) {
|
|
121
|
+
const a = world.node('A', ['a1'], host);
|
|
122
|
+
const b = world.node('B', ['b1'], host);
|
|
123
|
+
world.start();
|
|
124
|
+
const work = (id, input, more = {}) => ({ from: 'a1', id, to: 'b1', op: 'work', input, ...more });
|
|
125
|
+
world.act(a, kernel => {
|
|
126
|
+
kernel.call(work('s1', { mode: 'hold', goal: 'completed' }, { lane: 'x' }));
|
|
127
|
+
kernel.call(work('s2', { mode: 'hold', goal: 'completed' }, { lane: 'x' }));
|
|
128
|
+
});
|
|
129
|
+
world.act(a, kernel => kernel.cancel({ from: 'a1', id: 's1' }, 'no longer needed'));
|
|
130
|
+
await world.drain();
|
|
131
|
+
world.check();
|
|
132
|
+
world.act(a, kernel => kernel.call(work('f1', { mode: 'hold', goal: 'completed' })));
|
|
133
|
+
await flush();
|
|
134
|
+
world.act(a, kernel => kernel.cancel({ from: 'a1', id: 'f1' }));
|
|
135
|
+
await flush();
|
|
136
|
+
const overtaking = world.net.findIndex(event => event.request && JSON.parse(event.envelope).kind === 'cancel');
|
|
137
|
+
if (overtaking >= 0) world.deliver(overtaking);
|
|
138
|
+
await world.drain();
|
|
139
|
+
world.check();
|
|
140
|
+
world.act(a, kernel => {
|
|
141
|
+
kernel.call(work('o1', { mode: 'hold', goal: 'completed', cancelThrows: true }));
|
|
142
|
+
kernel.call(work('e1', { mode: 'complete' }));
|
|
143
|
+
});
|
|
144
|
+
await world.drain();
|
|
145
|
+
world.act(a, kernel => {
|
|
146
|
+
kernel.cancel({ from: 'a1', id: 'o1' });
|
|
147
|
+
kernel.cancel({ from: 'a1', id: 'e1' });
|
|
148
|
+
});
|
|
149
|
+
await world.drain();
|
|
150
|
+
world.check();
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
// A cancel goes down a call tree three levels deep, through a cancel function that throws; the run asked to cancel
|
|
154
|
+
// makes one more call, which it cannot await. A host stops its root run, and the call that run awaits is canceled.
|
|
155
|
+
async halt(world) {
|
|
156
|
+
const a = world.node('A', ['a1'], host);
|
|
157
|
+
const b = world.node('B', ['b1', 'b2'], host);
|
|
158
|
+
world.start();
|
|
159
|
+
const kids = [{ to: 'a1', mode: 'parent', goal: 'canceled', awaited: true, cancelThrows: true,
|
|
160
|
+
kids: [{ to: 'b2', mode: 'hold', goal: 'completed', awaited: true }] }];
|
|
161
|
+
world.act(a, kernel => kernel.call({ from: 'a1', id: 'h1', to: 'b1', op: 'work', input: { mode: 'parent', goal: 'completed', kids } }));
|
|
162
|
+
world.act(a, kernel => a.host.root(kernel, { address: 'a1', id: 'r1', goal: 'canceled', kids: [{ to: 'b1', mode: 'hold', goal: 'completed', awaited: true }] }));
|
|
163
|
+
await world.drain();
|
|
164
|
+
world.check();
|
|
165
|
+
world.act(a, kernel => {
|
|
166
|
+
kernel.cancel({ from: 'a1', id: 'h1' }, 'stop it all');
|
|
167
|
+
kernel.stop({ from: 'a1', id: 'r1' });
|
|
168
|
+
});
|
|
169
|
+
await world.drain();
|
|
170
|
+
world.check();
|
|
171
|
+
world.act(b, kernel => kernel.call({ from: 'b1', id: 'after', to: 'a1', op: 'work', input: { mode: 'complete' }, by: { from: 'a1', id: 'h1' }, awaited: true }));
|
|
172
|
+
await world.drain();
|
|
173
|
+
world.check();
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
// A caller abandons an open call and one whose call letter is on its way, with another call behind it in its lane.
|
|
177
|
+
// The real outcomes come late: by result letter, through a late handler that throws once, and by receipt.
|
|
178
|
+
async abandon(world) {
|
|
179
|
+
const a = world.node('A', ['a1'], host);
|
|
180
|
+
const b = world.node('B', ['b1'], host);
|
|
181
|
+
world.start();
|
|
182
|
+
const work = (id, input, more = {}) => ({ from: 'a1', id, to: 'b1', op: 'work', input, ...more });
|
|
183
|
+
world.act(a, kernel => kernel.call(work('g1', { mode: 'hold', goal: 'completed' }, { memo: { resultThrows: 1 } })));
|
|
184
|
+
await world.drain();
|
|
185
|
+
world.act(a, kernel => {
|
|
186
|
+
kernel.call(work('g2', { mode: 'complete', n: 2 }, { lane: 'x' }));
|
|
187
|
+
kernel.call(work('g3', { mode: 'complete', n: 3 }, { lane: 'x' }));
|
|
188
|
+
});
|
|
189
|
+
await flush();
|
|
190
|
+
world.act(a, kernel => {
|
|
191
|
+
kernel.abandon({ from: 'a1', id: 'g1' });
|
|
192
|
+
kernel.abandon({ from: 'a1', id: 'g2' });
|
|
193
|
+
});
|
|
194
|
+
await world.drain();
|
|
195
|
+
world.check();
|
|
196
|
+
world.act(b, kernel => {
|
|
197
|
+
for (const row of held(b)) b.host.finish(kernel, row);
|
|
198
|
+
});
|
|
199
|
+
await world.drain();
|
|
200
|
+
world.check();
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
// Inquiries find what letters lost on the way would have said: a newer status, and an outcome. Every crash restarts
|
|
204
|
+
// an instance, whose recover() inquires about its open calls.
|
|
205
|
+
async inquire(world) {
|
|
206
|
+
const a = world.node('A', ['a1'], host);
|
|
207
|
+
const b = world.node('B', ['b1'], host);
|
|
208
|
+
world.start();
|
|
209
|
+
world.act(a, kernel => {
|
|
210
|
+
kernel.call({ from: 'a1', id: 'q1', to: 'b1', op: 'work', input: { mode: 'hold', goal: 'completed' } });
|
|
211
|
+
kernel.call({ from: 'a1', id: 'q2', to: 'b1', op: 'work', input: { mode: 'hold', goal: 'completed' } });
|
|
212
|
+
});
|
|
213
|
+
await world.drain();
|
|
214
|
+
world.act(b, kernel => kernel.update({ from: 'a1', id: 'q1' }, { progress: { step: 1 } }));
|
|
215
|
+
for (const row of held(b)) if (row.id === 'q2') world.act(b, kernel => b.host.finish(kernel, row));
|
|
216
|
+
await flush();
|
|
217
|
+
// The letters are lost: they wait in B's outbox for its next pump().
|
|
218
|
+
world.net = world.net.map(event => event.request ? { resolve: event.resolve, reject: event.reject } : event);
|
|
219
|
+
await world.drain();
|
|
220
|
+
world.inquire(a, { from: 'a1', id: 'q1' });
|
|
221
|
+
world.inquire(a, { from: 'a1', id: 'q2' });
|
|
222
|
+
await world.drain();
|
|
223
|
+
world.check();
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
// Answers to a run waiting for input, from another instance and from the run's own: one taken that asks again, the
|
|
227
|
+
// same number again, a stale question, a refusal, a handler that throws, one taken that ends the run, and answers to
|
|
228
|
+
// a run that has ended, a run that never was, and the wrong address. An answer lost in a crash is not sent again.
|
|
229
|
+
async answer(world) {
|
|
230
|
+
const a = world.node('A', ['a1'], host);
|
|
231
|
+
const b = world.node('B', ['b1', 'b2'], host);
|
|
232
|
+
world.start();
|
|
233
|
+
const run = { from: 'a1', id: 'q1' };
|
|
234
|
+
world.act(a, kernel => kernel.call({ ...run, to: 'b1', op: 'work', input: { mode: 'hold', goal: 'completed' } }));
|
|
235
|
+
await world.drain();
|
|
236
|
+
world.act(b, kernel => kernel.update(run, { status: 'input-required', asking: { question: 1 } }));
|
|
237
|
+
await world.drain();
|
|
238
|
+
world.check();
|
|
239
|
+
const asked = () => b.q('SELECT asked FROM kernel_runs WHERE from_addr = ? AND id = ?').get(run.from, run.id)?.asked ?? 0;
|
|
240
|
+
const answer = (from, id, mode, more = {}) => ({ from, id, to: 'b1', call: run, asked: asked(), answer: { mode }, ...more });
|
|
241
|
+
const first = answer('a1', 'n1', 'ask');
|
|
242
|
+
for (const sent of [first, first, answer('a1', 'n2', 'take', { asked: asked() - 1 })]) {
|
|
243
|
+
const receipt = world.answer(a, sent);
|
|
244
|
+
await world.drain();
|
|
245
|
+
await receipt;
|
|
246
|
+
world.check();
|
|
247
|
+
}
|
|
248
|
+
for (const mode of ['refuse', 'throw', 'settle']) {
|
|
249
|
+
await world.answer(b, answer('b2', `n-${mode}`, mode));
|
|
250
|
+
world.check();
|
|
251
|
+
}
|
|
252
|
+
for (const sent of [answer('a1', 'n3', 'take'), answer('a1', 'n4', 'take', { call: { from: 'a1', id: 'none' } }), answer('a1', 'n5', 'take', { to: 'b2' })]) {
|
|
253
|
+
const receipt = world.answer(a, sent);
|
|
254
|
+
await world.drain();
|
|
255
|
+
await receipt;
|
|
256
|
+
}
|
|
257
|
+
await world.drain();
|
|
258
|
+
world.check();
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
// Timers: two come due at once and become calls in the order they were registered; one is unscheduled, one
|
|
262
|
+
// canceled, one abandoned and its id used again for another; a root run cannot end while it awaits a timer, until it
|
|
263
|
+
// detaches it; a run stopped loses its timers, due or not.
|
|
264
|
+
async timer(world) {
|
|
265
|
+
const a = world.node('A', ['a1'], host);
|
|
266
|
+
const b = world.node('B', ['b1'], host);
|
|
267
|
+
world.start();
|
|
268
|
+
const timer = (id, delayMs, more = {}) => ({ from: 'a1', id, delayMs, to: 'b1', op: 'work', input: { mode: 'complete', n: id }, lane: 'x', ...more });
|
|
269
|
+
world.act(a, kernel => {
|
|
270
|
+
for (const [id, delayMs] of [['t1', 10], ['t2', 10], ['t3', 20], ['t4', 5], ['t5', 5]]) kernel.after(timer(id, delayMs));
|
|
271
|
+
a.host.root(kernel, { address: 'a1', id: 'r1', goal: 'completed', kids: [{ to: 'b1', mode: 'complete', awaited: true, delayMs: 30 }] });
|
|
272
|
+
a.host.root(kernel, { address: 'a1', id: 'r2', goal: 'canceled', kids: [{ to: 'b1', mode: 'complete', awaited: true, delayMs: 0 },
|
|
273
|
+
{ to: 'b1', mode: 'hold', awaited: false, delayMs: 40 }] });
|
|
274
|
+
});
|
|
275
|
+
world.check();
|
|
276
|
+
world.act(a, kernel => {
|
|
277
|
+
kernel.unschedule({ from: 'a1', id: 't3' });
|
|
278
|
+
kernel.cancel({ from: 'a1', id: 't4' });
|
|
279
|
+
kernel.abandon({ from: 'a1', id: 't5' });
|
|
280
|
+
kernel.after(timer('t5', 1, { input: { mode: 'hold', goal: 'completed' } }));
|
|
281
|
+
kernel.stop({ from: 'a1', id: 'r2' });
|
|
282
|
+
});
|
|
283
|
+
world.time = 10;
|
|
284
|
+
world.tick(a);
|
|
285
|
+
await world.drain();
|
|
286
|
+
world.check();
|
|
287
|
+
const r1 = () => held(a).filter(row => row.id === 'r1');
|
|
288
|
+
for (const row of r1()) world.act(a, kernel => a.host.finish(kernel, row));
|
|
289
|
+
world.act(a, kernel => kernel.detach({ from: 'a1', id: 'a1:r1>0' }));
|
|
290
|
+
for (const row of r1()) world.act(a, kernel => a.host.finish(kernel, row));
|
|
291
|
+
world.time = 30;
|
|
292
|
+
world.tick(a);
|
|
293
|
+
await world.drain();
|
|
294
|
+
world.check();
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
|
|
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. */
|
|
299
|
+
export function checkErrors(world) {
|
|
300
|
+
for (const node of world.nodes)
|
|
301
|
+
for (const error of node.errors)
|
|
302
|
+
assert.match(String(error?.message), /^((|result |status |cancel |late |answer )handler blew up|run \S+ still awaits a call it made)$/,
|
|
303
|
+
`unexpected error on ${node.name}`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Runs a small scenario to rest and checks it.
|
|
308
|
+
* @param {(world: World) => Promise<void>} scenario
|
|
309
|
+
* @param {number} crashAt the commit after which an instance crashes, or Infinity
|
|
310
|
+
*/
|
|
311
|
+
export async function play(scenario, crashAt) {
|
|
312
|
+
const world = new World({ restart: true });
|
|
313
|
+
world.crashAt = crashAt;
|
|
314
|
+
await scenario(world);
|
|
315
|
+
await world.quiesce(() => advance(world));
|
|
316
|
+
world.check();
|
|
317
|
+
world.checkQuiet();
|
|
318
|
+
checkErrors(world);
|
|
319
|
+
return world;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const MODES = ['complete', 'complete', 'fail', 'hold', 'reject', 'busy', 'throw', 'parent', 'hold', 'hasty'];
|
|
323
|
+
const KID_MODES = ['complete', 'fail', 'hold', 'reject', 'busy', 'throw'];
|
|
324
|
+
const GOALS = ['completed', 'failed', 'canceled'];
|
|
325
|
+
const LANES = [null, 'x', 'y'];
|
|
326
|
+
const ANSWERS = ['take', 'take', 'take', 'ask', 'settle', 'refuse', 'throw'];
|
|
327
|
+
const DELAYS = [0, 1, 5, 10, 30];
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* 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.
|
|
332
|
+
* @param {number} seed
|
|
333
|
+
* @param {number} steps
|
|
334
|
+
*/
|
|
335
|
+
export async function wander(seed, steps) {
|
|
336
|
+
const rng = random(seed);
|
|
337
|
+
const pick = list => list[Math.floor(rng() * list.length)];
|
|
338
|
+
const world = new World({ rng });
|
|
339
|
+
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)];
|
|
341
|
+
const addresses = nodes.flatMap(node => node.addresses);
|
|
342
|
+
world.start();
|
|
343
|
+
const issued = [];
|
|
344
|
+
let roots = 0;
|
|
345
|
+
let later = 0;
|
|
346
|
+
/** every answer sent, those whose receipt never came, and those taken: an answerer may send one again, with its number (C8) */
|
|
347
|
+
const answers = [];
|
|
348
|
+
const unanswered = [];
|
|
349
|
+
const taken = [];
|
|
350
|
+
const answer = (node, sent) => world.answer(node, sent).then(receipt => {
|
|
351
|
+
if (!receipt) unanswered.push({ node, sent });
|
|
352
|
+
else if (receipt.applied) taken.push({ node, sent });
|
|
353
|
+
});
|
|
354
|
+
// Now and then a kid is a parent of its own, and the call tree is three levels deep.
|
|
355
|
+
const kids = (depth = 1) => Array.from({ length: 1 + Math.floor(rng() * 2) }, () => {
|
|
356
|
+
const kid = { to: pick(addresses), mode: pick(KID_MODES), goal: pick(GOALS), awaited: rng() < 0.6, cancelThrows: rng() < 0.2, cancelSettles: rng() < 0.2,
|
|
357
|
+
delayMs: rng() < 0.2 ? pick(DELAYS) : undefined };
|
|
358
|
+
return depth === 1 && rng() < 0.15 ? { ...kid, mode: 'parent', kids: kids(2) } : kid;
|
|
359
|
+
});
|
|
360
|
+
const unended = node => node.q("SELECT from_addr, id, state FROM kernel_calls WHERE state IN ('sending', 'open') ORDER BY rowid").all();
|
|
361
|
+
for (let step = 0; step < steps; step++) {
|
|
362
|
+
const roll = rng();
|
|
363
|
+
if (roll < 0.16) {
|
|
364
|
+
const node = pick(nodes);
|
|
365
|
+
const call = {
|
|
366
|
+
from: pick(node.addresses), id: `${seed}-${issued.length}`, to: pick(addresses), op: rng() < 0.05 ? 'nope' : 'work',
|
|
367
|
+
input: { mode: pick(MODES), goal: pick(GOALS), n: issued.length, kids: kids(), cancelThrows: rng() < 0.2, cancelSettles: rng() < 0.2 },
|
|
368
|
+
lane: pick(LANES),
|
|
369
|
+
memo: { resultThrows: rng() < 0.2 ? 1 + Math.floor(rng() * 2) : 0, statusThrows: rng() < 0.1 },
|
|
370
|
+
delayMs: rng() < 0.25 ? pick(DELAYS) : undefined,
|
|
371
|
+
};
|
|
372
|
+
issued.push(call);
|
|
373
|
+
world.act(node, kernel => call.delayMs === undefined ? kernel.call(call) : kernel.after(call));
|
|
374
|
+
} else if (roll < 0.18) {
|
|
375
|
+
const node = pick(nodes);
|
|
376
|
+
const run = { address: pick(node.addresses), id: `${seed}-r${roots++}`, goal: pick(GOALS), kids: kids() };
|
|
377
|
+
world.act(node, kernel => node.host.root(kernel, run));
|
|
378
|
+
} 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 }));
|
|
383
|
+
if (seen) assert.deepEqual([seen.to, seen.op], [call.to, call.op]);
|
|
384
|
+
if (seen) world.count(`same id again: ${call.delayMs === undefined ? 'call' : 'after'} then ${how}, finds a ${seen.state === 'scheduled' ? 'timer' : 'call'}`);
|
|
385
|
+
} else if (roll < 0.45 && world.net.length > 0) world.deliver(Math.floor(rng() * world.net.length), true);
|
|
386
|
+
else if (roll < 0.47) {
|
|
387
|
+
// A host does what it may to a timer not yet due.
|
|
388
|
+
const node = pick(nodes);
|
|
389
|
+
const timer = node.migrated() ? pick(node.q('SELECT from_addr, id FROM kernel_timers ORDER BY rowid').all()) : undefined;
|
|
390
|
+
const how = pick(['unschedule', 'cancel', 'abandon', 'detach', 'inquire']);
|
|
391
|
+
const ref = timer && { from: timer.from_addr, id: timer.id };
|
|
392
|
+
if (ref && how === 'inquire' && node.up) {
|
|
393
|
+
world.inquire(node, ref);
|
|
394
|
+
world.count('timer: inquire');
|
|
395
|
+
} 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]);
|
|
397
|
+
else if (roll < 0.50) {
|
|
398
|
+
const node = pick(nodes);
|
|
399
|
+
if (node.up && node.woke !== null && node.woke <= world.time) world.count('tick: the host\'s timer went off');
|
|
400
|
+
world.tick(node);
|
|
401
|
+
}
|
|
402
|
+
else if (roll < 0.57) {
|
|
403
|
+
// Someone answers a run: mostly one waiting for input, now and then with a stale question, or a run that has
|
|
404
|
+
// ended, a run that never was, or the wrong address.
|
|
405
|
+
const node = pick(nodes);
|
|
406
|
+
const runs = status => nodes.filter(node => node.migrated())
|
|
407
|
+
.flatMap(node => node.q(`SELECT from_addr, id, to_addr, asked FROM kernel_runs WHERE status ${status} ORDER BY rowid`).all());
|
|
408
|
+
const run = (rng() < 0.85 ? pick(runs("= 'input-required'")) : undefined) ?? (rng() < 0.9 ? pick(runs('IS NOT NULL')) : undefined)
|
|
409
|
+
?? { from_addr: 'a1', id: 'none', to_addr: pick(addresses), asked: 1 };
|
|
410
|
+
const sent = { from: pick(node.addresses), id: `${seed}-n${answers.length}`, to: rng() < 0.05 ? pick(addresses) : run.to_addr,
|
|
411
|
+
call: { from: run.from_addr, id: run.id }, asked: run.asked - (rng() < 0.1 ? 1 : 0), answer: { mode: pick(ANSWERS) } };
|
|
412
|
+
answers.push({ node, sent });
|
|
413
|
+
answer(node, sent);
|
|
414
|
+
} else if (roll < 0.585 && answers.length > 0) {
|
|
415
|
+
// Mostly one whose receipt was lost; now and then one taken already, or any, as an answerer unsure of what it sent.
|
|
416
|
+
const which = rng();
|
|
417
|
+
const { node, sent } = which < 0.5 && unanswered.length > 0 ? unanswered.splice(Math.floor(rng() * unanswered.length), 1)[0]
|
|
418
|
+
: which < 0.8 && taken.length > 0 ? pick(taken) : pick(answers);
|
|
419
|
+
world.count('answer sent again with its number');
|
|
420
|
+
answer(node, sent);
|
|
421
|
+
}
|
|
422
|
+
else if (roll < 0.64 && world.tasks.length > 0) world.tasks.shift()();
|
|
423
|
+
else if (roll < 0.71) world.pump(pick(nodes));
|
|
424
|
+
else if (roll < 0.74) {
|
|
425
|
+
const node = pick(nodes);
|
|
426
|
+
const row = node.up ? pick(held(node)) : undefined;
|
|
427
|
+
const detach = rng() < 0.3;
|
|
428
|
+
if (row) world.act(node, kernel => node.host.finish(kernel, row, { detach }));
|
|
429
|
+
} else if (roll < 0.79) {
|
|
430
|
+
const node = pick(nodes);
|
|
431
|
+
const row = node.up ? pick(held(node)) : undefined;
|
|
432
|
+
// Now and then twice, with the send loop let run in between: an older status may still be on its way. Now and
|
|
433
|
+
// then the run ends right after, so that its outcome may overtake its status.
|
|
434
|
+
for (let times = rng() < 0.5 ? 2 : 1; row && times > 0; times--) {
|
|
435
|
+
const change = pick(CHANGES);
|
|
436
|
+
world.act(node, kernel => node.host.nudge(kernel, row, change));
|
|
437
|
+
await flush();
|
|
438
|
+
}
|
|
439
|
+
if (row && rng() < 0.3) world.act(node, kernel => node.host.finish(kernel, row));
|
|
440
|
+
} else if (roll < 0.83) {
|
|
441
|
+
// Any call a node made, whatever its state.
|
|
442
|
+
const node = pick(nodes);
|
|
443
|
+
const call = node.migrated() ? pick(node.q('SELECT from_addr, id, state FROM kernel_calls ORDER BY rowid').all()) : undefined;
|
|
444
|
+
if (call && world.act(node, kernel => (kernel.cancel({ from: call.from_addr, id: call.id }, 'changed my mind'), true)))
|
|
445
|
+
world.count(`cancel a call that is ${ENDED.has(call.state) ? 'ended' : call.state}`);
|
|
446
|
+
} else if (roll < 0.85) {
|
|
447
|
+
const node = pick(nodes);
|
|
448
|
+
const call = node.migrated() ? pick(unended(node)) : undefined;
|
|
449
|
+
if (call && world.act(node, kernel => (kernel.abandon({ from: call.from_addr, id: call.id }), true))) world.count(`abandon a call that is ${call.state}`);
|
|
450
|
+
} else if (roll < 0.86) {
|
|
451
|
+
const node = pick(nodes);
|
|
452
|
+
const row = node.up ? pick(held(node)) : undefined;
|
|
453
|
+
if (row && world.act(node, kernel => (kernel.stop({ from: row.from_addr, id: row.id }), true)))
|
|
454
|
+
world.count(row.from_addr === row.addr ? 'stop a root run' : 'stop a run');
|
|
455
|
+
} else if (roll < 0.88) {
|
|
456
|
+
const node = pick(nodes);
|
|
457
|
+
const call = node.up ? pick(unended(node).filter(call => call.state === 'open')) : undefined;
|
|
458
|
+
if (call) world.inquire(node, { from: call.from_addr, id: call.id });
|
|
459
|
+
} else if (roll < 0.89) {
|
|
460
|
+
// A run the host keeps working makes another call it means to await.
|
|
461
|
+
const node = pick(nodes);
|
|
462
|
+
const row = node.up ? pick(held(node)) : undefined;
|
|
463
|
+
const asked = row && node.q('SELECT cancel_asked FROM kernel_runs WHERE from_addr = ? AND id = ?').get(row.from_addr, row.id).cancel_asked === 1;
|
|
464
|
+
const call = row && { from: row.addr, id: `${seed}-k${later++}`, to: pick(addresses), op: 'work', input: { mode: pick(KID_MODES), goal: pick(GOALS) },
|
|
465
|
+
by: { from: row.from_addr, id: row.id }, awaited: true, delayMs: rng() < 0.5 ? pick(DELAYS) : undefined };
|
|
466
|
+
if (call && world.act(node, kernel => call.delayMs === undefined ? kernel.call(call) : kernel.after(call)) && asked)
|
|
467
|
+
world.count(`${call.delayMs === undefined ? 'call' : 'timer'} from a run asked to cancel`);
|
|
468
|
+
} else if (roll < 0.92) {
|
|
469
|
+
const node = pick(nodes.filter(node => node.up).concat([undefined]));
|
|
470
|
+
node?.down();
|
|
471
|
+
} else if (roll < 0.945) pick(nodes.filter(node => !node.up).concat([undefined]))?.boot({ recover: false });
|
|
472
|
+
else if (roll < 0.97) pick(nodes.filter(node => node.up && !node.ready).concat([undefined]))?.recover();
|
|
473
|
+
else if (roll < 0.99) world.crashAt = world.commits + 1 + Math.floor(rng() * 4);
|
|
474
|
+
await flush();
|
|
475
|
+
world.check();
|
|
476
|
+
}
|
|
477
|
+
world.crashAt = Infinity;
|
|
478
|
+
world.faults = NO_FAULTS;
|
|
479
|
+
await world.quiesce(() => advance(world));
|
|
480
|
+
world.check();
|
|
481
|
+
world.checkQuiet();
|
|
482
|
+
checkErrors(world);
|
|
483
|
+
return { world, issued: issued.length };
|
|
484
|
+
}
|
package/sim/solo.mjs
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { createKernel } from '../kernel.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One kernel on its own, for tests that play the other side by hand. Its connection logs every statement it executes
|
|
6
|
+
* and can be told to fail the next COMMIT. Its `send` records each letter and answers with `answer(to, envelope)`;
|
|
7
|
+
* by default it never answers. Its clock reads `clock.now`, and what it gives `wake` is kept in `woken`.
|
|
8
|
+
* @param {{ answer?: (to: string, envelope: object) => Promise<object>, schedule?: (fn: () => void) => void,
|
|
9
|
+
* wake?: (at: number | null) => void }} [options]
|
|
10
|
+
*/
|
|
11
|
+
export function solo({ answer = () => new Promise(() => {}), schedule = fn => fn(), wake } = {}) {
|
|
12
|
+
const raw = new DatabaseSync(':memory:');
|
|
13
|
+
const log = [];
|
|
14
|
+
const db = {
|
|
15
|
+
failCommit: false,
|
|
16
|
+
get isTransaction() {
|
|
17
|
+
return raw.isTransaction;
|
|
18
|
+
},
|
|
19
|
+
exec(sql) {
|
|
20
|
+
log.push(sql);
|
|
21
|
+
if (sql === 'COMMIT' && db.failCommit) {
|
|
22
|
+
db.failCommit = false;
|
|
23
|
+
throw new Error('disk I/O error');
|
|
24
|
+
}
|
|
25
|
+
raw.exec(sql);
|
|
26
|
+
},
|
|
27
|
+
prepare: sql => raw.prepare(sql),
|
|
28
|
+
};
|
|
29
|
+
const sent = [];
|
|
30
|
+
const errors = [];
|
|
31
|
+
const clock = { now: 0 };
|
|
32
|
+
const woken = [];
|
|
33
|
+
const kernel = createKernel({
|
|
34
|
+
db,
|
|
35
|
+
now: () => clock.now,
|
|
36
|
+
send: (to, envelope) => {
|
|
37
|
+
sent.push({ to, envelope });
|
|
38
|
+
return answer(to, envelope);
|
|
39
|
+
},
|
|
40
|
+
onError: error => errors.push(error),
|
|
41
|
+
wake: wake ?? (at => woken.push(at)),
|
|
42
|
+
schedule,
|
|
43
|
+
});
|
|
44
|
+
kernel.transaction(() => kernel.migrate());
|
|
45
|
+
return { kernel, raw, db, log, sent, errors, clock, woken, row: (sql, ...args) => raw.prepare(sql).get(...args),
|
|
46
|
+
rows: (sql, ...args) => raw.prepare(sql).all(...args).map(row => ({ ...row })) };
|
|
47
|
+
}
|