@minnowdb/core 0.6.9 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/block-format/block.js +254 -325
- package/dist/block-format/checksum.js +30 -52
- package/dist/block-format/codecs.js +112 -125
- package/dist/block-format/column.js +187 -205
- package/dist/block-format/index.js +5 -1
- package/dist/block-format/physical.js +371 -387
- package/dist/block-format/types.js +4 -1
- package/dist/block-format/unicode.js +30 -39
- package/dist/date-value.d.ts +5 -0
- package/dist/date-value.js +71 -41
- package/dist/engine/artifact-cache.js +106 -116
- package/dist/engine/batch.js +36 -44
- package/dist/engine/buffered-writer.js +156 -158
- package/dist/engine/byte-estimates.js +20 -22
- package/dist/engine/cache-limits.js +24 -25
- package/dist/engine/cancellation.js +5 -3
- package/dist/engine/catalog.js +64 -73
- package/dist/engine/client.js +715 -780
- package/dist/engine/database.js +15952 -18465
- package/dist/engine/defaults.js +104 -111
- package/dist/engine/errors.js +113 -128
- package/dist/engine/fts.js +233 -280
- package/dist/engine/group-index.js +287 -321
- package/dist/engine/index.js +23 -12
- package/dist/engine/join-index.js +177 -186
- package/dist/engine/keyed-live.js +168 -173
- package/dist/engine/live-api.js +0 -1
- package/dist/engine/live-equal.js +32 -37
- package/dist/engine/live.js +603 -647
- package/dist/engine/memory.js +109 -111
- package/dist/engine/optimizer.d.ts +19 -1
- package/dist/engine/optimizer.js +2642 -2515
- package/dist/engine/point-read.d.ts +4 -2
- package/dist/engine/point-read.js +121 -171
- package/dist/engine/query-api.js +12 -3
- package/dist/engine/query-cache.js +61 -69
- package/dist/engine/query.d.ts +45 -0
- package/dist/engine/query.js +8506 -9375
- package/dist/engine/result-wire.js +221 -252
- package/dist/engine/schema-wire.js +97 -112
- package/dist/engine/schema.js +1099 -1254
- package/dist/engine/sort-keys.js +315 -389
- package/dist/engine/sql-domains.d.ts +7 -1
- package/dist/engine/sql-domains.js +648 -734
- package/dist/engine/sql-driver.js +0 -1
- package/dist/engine/sql-functions.d.ts +11 -0
- package/dist/engine/sql-functions.js +1092 -0
- package/dist/engine/sql-json.js +199 -218
- package/dist/engine/sql-semantics.d.ts +25 -4
- package/dist/engine/sql-semantics.js +579 -513
- package/dist/engine/typed-live.js +271 -293
- package/dist/engine/vector.d.ts +7 -1
- package/dist/engine/vector.js +4513 -4580
- package/dist/engine/worker-host.d.ts +8 -63
- package/dist/engine/worker-host.js +28 -1130
- package/dist/engine/worker-indexeddb.d.ts +1 -0
- package/dist/engine/worker-indexeddb.js +3 -0
- package/dist/engine/worker-memory.d.ts +1 -0
- package/dist/engine/worker-memory.js +3 -0
- package/dist/engine/worker-opfs.d.ts +1 -0
- package/dist/engine/worker-opfs.js +3 -0
- package/dist/engine/worker-server.d.ts +93 -0
- package/dist/engine/worker-server.js +999 -0
- package/dist/engine/worker-store-indexeddb.d.ts +2 -0
- package/dist/engine/worker-store-indexeddb.js +11 -0
- package/dist/engine/worker-store-memory.d.ts +2 -0
- package/dist/engine/worker-store-memory.js +6 -0
- package/dist/engine/worker-store-opfs.d.ts +2 -0
- package/dist/engine/worker-store-opfs.js +9 -0
- package/dist/engine/worker.js +0 -12
- package/dist/engine/write-block-planner.js +93 -100
- package/dist/plan/index.js +15 -16
- package/dist/plan/model.d.ts +9 -1
- package/dist/plan/model.js +20 -27
- package/dist/storage/index.js +0 -12
- package/dist/storage/indexeddb.js +11776 -13171
- package/dist/storage/memory.js +1198 -1241
- package/dist/storage/opfs/files.js +238 -295
- package/dist/storage/opfs/index.js +9 -3
- package/dist/storage/opfs/leader.js +4386 -4856
- package/dist/storage/opfs/rpc.js +223 -259
- package/dist/storage/opfs/snapshot-ledger.js +219 -260
- package/dist/storage/opfs/store.js +884 -1003
- package/dist/storage/persistence.js +59 -74
- package/dist/storage/snapshot-stream.js +788 -826
- package/dist/storage/snapshot.js +36 -37
- package/dist/storage/toolkit/extents.js +444 -520
- package/dist/storage/toolkit/index.js +28 -29
- package/dist/storage/toolkit/record-core.js +5792 -6377
- package/dist/storage/toolkit/sync-file.js +34 -42
- package/dist/storage/toolkit/wal.js +92 -127
- package/dist/storage/toolkit/wire.js +232 -256
- package/dist/storage/types.js +2952 -3228
- package/dist/testing/block-store-conformance.js +1556 -1629
- package/dist/testing/index.js +310 -291
- package/dist/testing/opfs-shim.js +280 -312
- package/dist/testing/simulator.js +496 -534
- package/dist/testing/sqllogictest.js +425 -365
- package/dist/transactions/index.js +1464 -1712
- package/dist/worker-protocol/index.js +70 -69
- package/package.json +20 -2
- package/postgres-feature-profile.json +15 -5
- package/sql-feature-matrix.json +252 -2
|
@@ -3,592 +3,554 @@ import { MemoryBlockStore } from "../storage/index.js";
|
|
|
3
3
|
import { FaultInjectingBlockStore } from "./index.js";
|
|
4
4
|
import { md5Hex } from "./sqllogictest.js";
|
|
5
5
|
async function collectStoragePages(read) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
const records = [];
|
|
7
|
+
let cursor = null;
|
|
8
|
+
for (; ; ) {
|
|
9
|
+
const page = await read(cursor);
|
|
10
|
+
records.push(...page.records);
|
|
11
|
+
if (page.nextCursor === null)
|
|
12
|
+
return records;
|
|
13
|
+
cursor = page.nextCursor;
|
|
14
|
+
}
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
throw new RangeError("Simulator trace limit must be a non-negative whole number");
|
|
27
|
-
}
|
|
28
|
-
this.#random = mulberry32(seed);
|
|
29
|
-
this.#traceLimit = traceLimit;
|
|
30
|
-
}
|
|
31
|
-
get highWater() {
|
|
32
|
-
return this.#highWater;
|
|
33
|
-
}
|
|
34
|
-
get trace() {
|
|
35
|
-
return this.#trace;
|
|
16
|
+
class DeterministicScheduler {
|
|
17
|
+
#random;
|
|
18
|
+
#traceLimit;
|
|
19
|
+
#pending = [];
|
|
20
|
+
#trace = [];
|
|
21
|
+
#sequence = 0;
|
|
22
|
+
#highWater = 0;
|
|
23
|
+
constructor(seed, traceLimit = 256) {
|
|
24
|
+
if (!Number.isSafeInteger(traceLimit) || traceLimit < 0) {
|
|
25
|
+
throw new RangeError("Simulator trace limit must be a non-negative whole number");
|
|
36
26
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
emptyTurns = 0;
|
|
70
|
-
const index = Math.floor(this.#random() * pending);
|
|
71
|
-
const operation = this.#pending.splice(index, 1)[0];
|
|
72
|
-
if (operation === undefined)
|
|
73
|
-
continue;
|
|
74
|
-
try {
|
|
75
|
-
const value = await operation.run();
|
|
76
|
-
operation.resolve(value);
|
|
77
|
-
this.#record(operation.label, "resolved");
|
|
78
|
-
}
|
|
79
|
-
catch (error) {
|
|
80
|
-
operation.reject(error);
|
|
81
|
-
this.#record(operation.label, "rejected");
|
|
82
|
-
}
|
|
27
|
+
this.#random = mulberry32(seed);
|
|
28
|
+
this.#traceLimit = traceLimit;
|
|
29
|
+
}
|
|
30
|
+
get highWater() {
|
|
31
|
+
return this.#highWater;
|
|
32
|
+
}
|
|
33
|
+
get trace() {
|
|
34
|
+
return this.#trace;
|
|
35
|
+
}
|
|
36
|
+
schedule(label, run) {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
this.#pending.push({ label, run, resolve, reject });
|
|
39
|
+
this.#highWater = Math.max(this.#highWater, this.#pending.length);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
async settle(promises) {
|
|
43
|
+
const state = { complete: false };
|
|
44
|
+
const settled = Promise.allSettled(promises).then((results) => {
|
|
45
|
+
state.complete = true;
|
|
46
|
+
return results;
|
|
47
|
+
});
|
|
48
|
+
let emptyTurns = 0;
|
|
49
|
+
while (!state.complete) {
|
|
50
|
+
await Promise.resolve();
|
|
51
|
+
const pending = this.#pending.length;
|
|
52
|
+
if (pending === 0) {
|
|
53
|
+
await nextEventLoopTurn();
|
|
54
|
+
if (this.#pending.length > 0) {
|
|
55
|
+
emptyTurns = 0;
|
|
56
|
+
continue;
|
|
83
57
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
58
|
+
emptyTurns++;
|
|
59
|
+
if (emptyTurns > 100) {
|
|
60
|
+
throw new Error("Deterministic simulator deadlocked with no scheduled storage operation");
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
emptyTurns = 0;
|
|
65
|
+
const index = Math.floor(this.#random() * pending);
|
|
66
|
+
const operation = this.#pending.splice(index, 1)[0];
|
|
67
|
+
if (operation === void 0)
|
|
68
|
+
continue;
|
|
69
|
+
try {
|
|
70
|
+
const value = await operation.run();
|
|
71
|
+
operation.resolve(value);
|
|
72
|
+
this.#record(operation.label, "resolved");
|
|
73
|
+
} catch (error) {
|
|
74
|
+
operation.reject(error);
|
|
75
|
+
this.#record(operation.label, "rejected");
|
|
76
|
+
}
|
|
93
77
|
}
|
|
78
|
+
return settled;
|
|
79
|
+
}
|
|
80
|
+
#record(operation, outcome) {
|
|
81
|
+
this.#sequence++;
|
|
82
|
+
if (this.#traceLimit === 0)
|
|
83
|
+
return;
|
|
84
|
+
this.#trace.push({ sequence: this.#sequence, operation, outcome });
|
|
85
|
+
if (this.#trace.length > this.#traceLimit)
|
|
86
|
+
this.#trace.shift();
|
|
87
|
+
}
|
|
94
88
|
}
|
|
95
89
|
function nextEventLoopTurn() {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
/** Wraps every asynchronous BlockStore entry point in the deterministic completion scheduler. */
|
|
110
|
-
export function scheduledBlockStore(store, scheduler) {
|
|
111
|
-
return new Proxy(store, {
|
|
112
|
-
get(target, property) {
|
|
113
|
-
const value = Reflect.get(target, property, target);
|
|
114
|
-
if (typeof value !== "function")
|
|
115
|
-
return value;
|
|
116
|
-
const method = value;
|
|
117
|
-
if (property === "close")
|
|
118
|
-
return (...arguments_) => method.apply(target, arguments_);
|
|
119
|
-
return (...arguments_) => scheduler.schedule(String(property), async () => method.apply(target, arguments_));
|
|
120
|
-
},
|
|
121
|
-
});
|
|
90
|
+
if (typeof MessageChannel === "undefined") {
|
|
91
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
92
|
+
}
|
|
93
|
+
return new Promise((resolve) => {
|
|
94
|
+
const channel = new MessageChannel();
|
|
95
|
+
channel.port1.onmessage = () => {
|
|
96
|
+
channel.port1.close();
|
|
97
|
+
channel.port2.close();
|
|
98
|
+
resolve();
|
|
99
|
+
};
|
|
100
|
+
channel.port2.postMessage(void 0);
|
|
101
|
+
});
|
|
122
102
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (!Number.isSafeInteger(clients) || clients < 1 || clients > 32) {
|
|
134
|
-
throw new RangeError("Simulator clients must be between 1 and 32");
|
|
103
|
+
function scheduledBlockStore(store, scheduler) {
|
|
104
|
+
return new Proxy(store, {
|
|
105
|
+
get(target, property) {
|
|
106
|
+
const value = Reflect.get(target, property, target);
|
|
107
|
+
if (typeof value !== "function")
|
|
108
|
+
return value;
|
|
109
|
+
const method = value;
|
|
110
|
+
if (property === "close")
|
|
111
|
+
return (...arguments_) => method.apply(target, arguments_);
|
|
112
|
+
return (...arguments_) => scheduler.schedule(String(property), async () => method.apply(target, arguments_));
|
|
135
113
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function generateSimulationPlan(seed, options = {}) {
|
|
117
|
+
if (!Number.isSafeInteger(seed)) {
|
|
118
|
+
throw new RangeError("Simulator seed must be a whole number");
|
|
119
|
+
}
|
|
120
|
+
const rounds = options.rounds ?? 48;
|
|
121
|
+
const clients = options.clients ?? 4;
|
|
122
|
+
const keySpace = options.keySpace ?? 32;
|
|
123
|
+
if (!Number.isSafeInteger(rounds) || rounds < 1 || rounds > 1e5) {
|
|
124
|
+
throw new RangeError("Simulator rounds must be between 1 and 100,000");
|
|
125
|
+
}
|
|
126
|
+
if (!Number.isSafeInteger(clients) || clients < 1 || clients > 32) {
|
|
127
|
+
throw new RangeError("Simulator clients must be between 1 and 32");
|
|
128
|
+
}
|
|
129
|
+
if (!Number.isSafeInteger(keySpace) || keySpace < 1 || keySpace > 1e4) {
|
|
130
|
+
throw new RangeError("Simulator key space must be between 1 and 10,000");
|
|
131
|
+
}
|
|
132
|
+
if (keySpace < clients) {
|
|
133
|
+
throw new RangeError("Simulator key space must be at least the client count");
|
|
134
|
+
}
|
|
135
|
+
const random = mulberry32(seed);
|
|
136
|
+
const steps = [];
|
|
137
|
+
const faultPoints = [
|
|
138
|
+
"beforeBlockWrite",
|
|
139
|
+
"afterBlockWrite",
|
|
140
|
+
"beforeTransactionCommit",
|
|
141
|
+
"afterTransactionCommit"
|
|
142
|
+
];
|
|
143
|
+
for (let round = 0; round < rounds; round++) {
|
|
144
|
+
const operations = [];
|
|
145
|
+
const start = Math.floor(random() * keySpace);
|
|
146
|
+
let stride = Math.floor(random() * keySpace) + 1;
|
|
147
|
+
while (greatestCommonDivisor(stride, keySpace) !== 1)
|
|
148
|
+
stride++;
|
|
149
|
+
for (let client = 0; client < clients; client++) {
|
|
150
|
+
const key = (start + client * stride) % keySpace + 1;
|
|
151
|
+
if (random() < 0.18)
|
|
152
|
+
operations.push({ kind: "delete", client, key });
|
|
153
|
+
else
|
|
154
|
+
operations.push({ kind: "put", client, key, amount: Math.floor(random() * 1e6) });
|
|
141
155
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
// the reference model one exact answer instead of guessing which same-key promise linearized
|
|
159
|
-
// last. Contention still happens at the shared manifest/transaction boundary.
|
|
160
|
-
const key = ((start + client * stride) % keySpace) + 1;
|
|
161
|
-
if (random() < 0.18)
|
|
162
|
-
operations.push({ kind: "delete", client, key });
|
|
163
|
-
else
|
|
164
|
-
operations.push({ kind: "put", client, key, amount: Math.floor(random() * 1_000_000) });
|
|
165
|
-
}
|
|
166
|
-
steps.push({ kind: "concurrent", operations });
|
|
167
|
-
if (round % 4 === 3)
|
|
168
|
-
steps.push({ kind: "checkpoint" });
|
|
169
|
-
if (round % 12 === 11)
|
|
170
|
-
steps.push({ kind: "maintenance" });
|
|
171
|
-
if (round % 20 === 19) {
|
|
172
|
-
const point = faultPoints[Math.floor(random() * faultPoints.length)] ?? "beforeBlockWrite";
|
|
173
|
-
steps.push({
|
|
174
|
-
kind: "fault",
|
|
175
|
-
point,
|
|
176
|
-
occurrence: 1,
|
|
177
|
-
operation: {
|
|
178
|
-
kind: "put",
|
|
179
|
-
client: Math.floor(random() * clients),
|
|
180
|
-
key: Math.floor(random() * keySpace) + 1,
|
|
181
|
-
amount: Math.floor(random() * 1_000_000),
|
|
182
|
-
},
|
|
183
|
-
});
|
|
156
|
+
steps.push({ kind: "concurrent", operations });
|
|
157
|
+
if (round % 4 === 3)
|
|
158
|
+
steps.push({ kind: "checkpoint" });
|
|
159
|
+
if (round % 12 === 11)
|
|
160
|
+
steps.push({ kind: "maintenance" });
|
|
161
|
+
if (round % 20 === 19) {
|
|
162
|
+
const point = faultPoints[Math.floor(random() * faultPoints.length)] ?? "beforeBlockWrite";
|
|
163
|
+
steps.push({
|
|
164
|
+
kind: "fault",
|
|
165
|
+
point,
|
|
166
|
+
occurrence: 1,
|
|
167
|
+
operation: {
|
|
168
|
+
kind: "put",
|
|
169
|
+
client: Math.floor(random() * clients),
|
|
170
|
+
key: Math.floor(random() * keySpace) + 1,
|
|
171
|
+
amount: Math.floor(random() * 1e6)
|
|
184
172
|
}
|
|
173
|
+
});
|
|
185
174
|
}
|
|
186
|
-
|
|
187
|
-
|
|
175
|
+
}
|
|
176
|
+
steps.push({ kind: "checkpoint" }, { kind: "maintenance" }, { kind: "checkpoint" });
|
|
177
|
+
return { version: 1, seed, clients, keySpace, steps };
|
|
188
178
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
validateSteps(plan.steps, plan.clients ?? 0, plan.keySpace ?? 0);
|
|
213
|
-
return plan;
|
|
179
|
+
function parseSimulationPlan(source) {
|
|
180
|
+
const value = JSON.parse(source);
|
|
181
|
+
if (typeof value !== "object" || value === null)
|
|
182
|
+
throw new TypeError("Simulation plan must be an object");
|
|
183
|
+
const plan = value;
|
|
184
|
+
if (plan.version !== 1)
|
|
185
|
+
throw new TypeError("Simulation plan version must be 1");
|
|
186
|
+
if (!Number.isSafeInteger(plan.seed))
|
|
187
|
+
throw new TypeError("Simulation seed must be a whole number");
|
|
188
|
+
if (!Number.isSafeInteger(plan.clients) || (plan.clients ?? 0) < 1 || (plan.clients ?? 0) > 32) {
|
|
189
|
+
throw new TypeError("Simulation client count is invalid");
|
|
190
|
+
}
|
|
191
|
+
if (!Number.isSafeInteger(plan.keySpace) || (plan.keySpace ?? 0) < 1 || (plan.keySpace ?? 0) > 1e4) {
|
|
192
|
+
throw new TypeError("Simulation key space is invalid");
|
|
193
|
+
}
|
|
194
|
+
if ((plan.keySpace ?? 0) < (plan.clients ?? 0)) {
|
|
195
|
+
throw new TypeError("Simulation key space must be at least the client count");
|
|
196
|
+
}
|
|
197
|
+
if (!Array.isArray(plan.steps) || plan.steps.length > 2e5) {
|
|
198
|
+
throw new TypeError("Simulation steps must be a bounded array");
|
|
199
|
+
}
|
|
200
|
+
validateSteps(plan.steps, plan.clients ?? 0, plan.keySpace ?? 0);
|
|
201
|
+
return plan;
|
|
214
202
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (/manifest changed|conflict/iu.test(errorMessage(error))) {
|
|
253
|
-
rejectedConflicts++;
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
throw error;
|
|
257
|
-
}));
|
|
258
|
-
assertSettled(await scheduler.settle(operations));
|
|
259
|
-
continue;
|
|
203
|
+
async function runSimulation(plan, options = {}) {
|
|
204
|
+
parseSimulationPlan(JSON.stringify(plan));
|
|
205
|
+
const base = new MemoryBlockStore();
|
|
206
|
+
const faults = new FaultController();
|
|
207
|
+
const scheduler = new DeterministicScheduler(plan.seed ^ 1597463007, options.traceEvents ?? 256);
|
|
208
|
+
const store = scheduledBlockStore(new FaultInjectingBlockStore(base, (point) => faults.inject(point)), scheduler);
|
|
209
|
+
let clock = Date.parse("2026-01-01T00:00:00.000Z");
|
|
210
|
+
let id = 0;
|
|
211
|
+
const createDatabase = () => new MinnowDatabase(store, {
|
|
212
|
+
compression: "raw",
|
|
213
|
+
rowsPerBlock: 8,
|
|
214
|
+
maxCommitRetries: 16,
|
|
215
|
+
now: () => new Date(clock),
|
|
216
|
+
createId: () => `sim-${String(id++)}`,
|
|
217
|
+
bufferPoolBytes: 0,
|
|
218
|
+
autoCompact: false,
|
|
219
|
+
autoCollect: false
|
|
220
|
+
});
|
|
221
|
+
const setup = createDatabase();
|
|
222
|
+
await drive(scheduler, setup.execute("CREATE TABLE items (id INTEGER PRIMARY KEY, client INTEGER NOT NULL, amount INTEGER NOT NULL)"));
|
|
223
|
+
await drive(scheduler, setup.close());
|
|
224
|
+
const clients = Array.from({ length: plan.clients }, createDatabase);
|
|
225
|
+
const model = /* @__PURE__ */ new Map();
|
|
226
|
+
let acceptedMutations = 0;
|
|
227
|
+
let rejectedConflicts = 0;
|
|
228
|
+
let injectedFaults = 0;
|
|
229
|
+
let checkpoints = 0;
|
|
230
|
+
for (const step of plan.steps) {
|
|
231
|
+
clock += 1e3;
|
|
232
|
+
if (step.kind === "concurrent") {
|
|
233
|
+
const operations = step.operations.map((operation) => executeMutation(requireClient(clients, operation.client), operation).then(() => {
|
|
234
|
+
applyMutation(model, operation);
|
|
235
|
+
acceptedMutations++;
|
|
236
|
+
}).catch((error) => {
|
|
237
|
+
if (/manifest changed|conflict/iu.test(errorMessage(error))) {
|
|
238
|
+
rejectedConflicts++;
|
|
239
|
+
return;
|
|
260
240
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
if (step.kind === "maintenance") {
|
|
267
|
-
const database = clients[0] ?? setup;
|
|
268
|
-
await drive(scheduler, database.compactTable("items"));
|
|
269
|
-
// One pass can remove obsolete manifests/segments while their transaction journals are
|
|
270
|
-
// still provenance roots. The next pass can reclaim those now-unreferenced journals.
|
|
271
|
-
await drive(scheduler, database.collectGarbage());
|
|
272
|
-
await drive(scheduler, database.collectGarbage());
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
const before = new Map(model);
|
|
276
|
-
const after = new Map(model);
|
|
277
|
-
applyMutation(after, step.operation);
|
|
278
|
-
faults.arm(step.point, step.occurrence);
|
|
279
|
-
const result = await scheduler.settle([
|
|
280
|
-
executeMutation(requireClient(clients, step.operation.client), step.operation),
|
|
281
|
-
]);
|
|
282
|
-
if (!faults.fired)
|
|
283
|
-
throw new Error(`Simulation fault was not reached: ${step.point}`);
|
|
284
|
-
injectedFaults++;
|
|
285
|
-
faults.disarm();
|
|
286
|
-
const failedClient = requireClient(clients, step.operation.client);
|
|
287
|
-
await drive(scheduler, failedClient.close());
|
|
288
|
-
const replacement = createDatabase();
|
|
289
|
-
clients[step.operation.client] = replacement;
|
|
290
|
-
const actual = await readRows(scheduler, replacement);
|
|
291
|
-
if (sameRows(actual, before)) {
|
|
292
|
-
if (result[0]?.status === "fulfilled") {
|
|
293
|
-
throw new Error(`Fault ${step.point} returned success but did not persist its mutation`);
|
|
294
|
-
}
|
|
295
|
-
replaceModel(model, before);
|
|
296
|
-
}
|
|
297
|
-
else if (sameRows(actual, after))
|
|
298
|
-
replaceModel(model, after);
|
|
299
|
-
else
|
|
300
|
-
throw new Error(`Fault ${step.point} left a state that was neither before nor after the mutation`);
|
|
241
|
+
throw error;
|
|
242
|
+
}));
|
|
243
|
+
assertSettled(await scheduler.settle(operations));
|
|
244
|
+
continue;
|
|
301
245
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
// leases. A fresh maintenance client can then collapse history without an artificial pin from
|
|
307
|
-
// the workload whose final state has already been checked.
|
|
308
|
-
const maintenance = createDatabase();
|
|
309
|
-
const compacted = await compactToFixedPoint(scheduler, maintenance, plan.steps.length);
|
|
310
|
-
const maintenanceCompactions = compacted.passes;
|
|
311
|
-
const compactionStop = compacted.stop;
|
|
312
|
-
const collected = await collectToFixedPoint(scheduler, maintenance, plan.steps.length);
|
|
313
|
-
let collectionPasses = collected.passes;
|
|
314
|
-
const collectionTail = [...collected.tail];
|
|
315
|
-
await assertModel(scheduler, maintenance, model);
|
|
316
|
-
await drive(scheduler, maintenance.close());
|
|
317
|
-
const sweeper = createDatabase();
|
|
318
|
-
const swept = await collectToFixedPoint(scheduler, sweeper, plan.steps.length);
|
|
319
|
-
collectionPasses += swept.passes;
|
|
320
|
-
collectionTail.push(...swept.tail);
|
|
321
|
-
if (collectionTail.length > 8)
|
|
322
|
-
collectionTail.splice(0, collectionTail.length - 8);
|
|
323
|
-
await drive(scheduler, sweeper.close());
|
|
324
|
-
const rows = sortedRows(model);
|
|
325
|
-
const [segments, transactions, manifests, currentManifest, leases, compactions, storageBytes] = await Promise.all([
|
|
326
|
-
collectStoragePages((cursor) => base.listSegmentPage(cursor, 256)),
|
|
327
|
-
collectStoragePages((cursor) => base.listTransactionPage(cursor, 256)),
|
|
328
|
-
collectStoragePages((cursor) => base.listManifestPage(cursor, 256)),
|
|
329
|
-
base.getCurrentManifest(),
|
|
330
|
-
base.listLeases(),
|
|
331
|
-
base.listCompactionJobs(),
|
|
332
|
-
base.getLogicalStorageBytes(),
|
|
333
|
-
]);
|
|
334
|
-
const blockCount = currentManifest?.liveBlockCount ?? 0;
|
|
335
|
-
const blockLimit = plan.keySpace * 16 + 64;
|
|
336
|
-
const segmentLimit = plan.keySpace + 16;
|
|
337
|
-
const transactionLimit = segmentLimit + 16;
|
|
338
|
-
if (blockCount > blockLimit ||
|
|
339
|
-
segments.length > segmentLimit ||
|
|
340
|
-
transactions.length > transactionLimit) {
|
|
341
|
-
const transactionStates = transactions.reduce((counts, transaction) => {
|
|
342
|
-
counts[transaction.status] = (counts[transaction.status] ?? 0) + 1;
|
|
343
|
-
return counts;
|
|
344
|
-
}, {});
|
|
345
|
-
const compactionStates = compactions.reduce((counts, compaction) => {
|
|
346
|
-
counts[compaction.state] = (counts[compaction.state] ?? 0) + 1;
|
|
347
|
-
return counts;
|
|
348
|
-
}, {});
|
|
349
|
-
throw new Error(`Storage did not compact to a bound: ${String(blockCount)}/${String(blockLimit)} live blocks, ` +
|
|
350
|
-
`${String(segments.length)}/${String(segmentLimit)} segments, ` +
|
|
351
|
-
`${String(transactions.length)}/${String(transactionLimit)} transactions; ` +
|
|
352
|
-
`${JSON.stringify(transactionStates)} transaction states, ` +
|
|
353
|
-
`${JSON.stringify(compactionStates)} compaction states, ` +
|
|
354
|
-
`${String(currentManifest?.liveBlockCount ?? 0)} current blocks, compaction stopped at ${compactionStop}, ` +
|
|
355
|
-
`${String(manifests.length)} manifests (${String(manifests.filter((manifest) => manifest.prunedAt !== undefined).length)} pruned) remain after ${String(maintenanceCompactions)} compactions ` +
|
|
356
|
-
`and ${String(collectionPasses)} collection passes; collection tail ${JSON.stringify(collectionTail)}`);
|
|
246
|
+
if (step.kind === "checkpoint") {
|
|
247
|
+
await assertModel(scheduler, clients[0] ?? setup, model);
|
|
248
|
+
checkpoints++;
|
|
249
|
+
continue;
|
|
357
250
|
}
|
|
358
|
-
if (
|
|
359
|
-
|
|
251
|
+
if (step.kind === "maintenance") {
|
|
252
|
+
const database = clients[0] ?? setup;
|
|
253
|
+
await drive(scheduler, database.compactTable("items"));
|
|
254
|
+
await drive(scheduler, database.collectGarbage());
|
|
255
|
+
await drive(scheduler, database.collectGarbage());
|
|
256
|
+
continue;
|
|
360
257
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
258
|
+
const before = new Map(model);
|
|
259
|
+
const after = new Map(model);
|
|
260
|
+
applyMutation(after, step.operation);
|
|
261
|
+
faults.arm(step.point, step.occurrence);
|
|
262
|
+
const result = await scheduler.settle([
|
|
263
|
+
executeMutation(requireClient(clients, step.operation.client), step.operation)
|
|
264
|
+
]);
|
|
265
|
+
if (!faults.fired)
|
|
266
|
+
throw new Error(`Simulation fault was not reached: ${step.point}`);
|
|
267
|
+
injectedFaults++;
|
|
268
|
+
faults.disarm();
|
|
269
|
+
const failedClient = requireClient(clients, step.operation.client);
|
|
270
|
+
await drive(scheduler, failedClient.close());
|
|
271
|
+
const replacement = createDatabase();
|
|
272
|
+
clients[step.operation.client] = replacement;
|
|
273
|
+
const actual = await readRows(scheduler, replacement);
|
|
274
|
+
if (sameRows(actual, before)) {
|
|
275
|
+
if (result[0]?.status === "fulfilled") {
|
|
276
|
+
throw new Error(`Fault ${step.point} returned success but did not persist its mutation`);
|
|
277
|
+
}
|
|
278
|
+
replaceModel(model, before);
|
|
279
|
+
} else if (sameRows(actual, after))
|
|
280
|
+
replaceModel(model, after);
|
|
281
|
+
else
|
|
282
|
+
throw new Error(`Fault ${step.point} left a state that was neither before nor after the mutation`);
|
|
283
|
+
}
|
|
284
|
+
await assertModel(scheduler, clients[0] ?? setup, model);
|
|
285
|
+
const closeResults = await scheduler.settle(clients.map((client) => client.close()));
|
|
286
|
+
assertSettled(closeResults);
|
|
287
|
+
const maintenance = createDatabase();
|
|
288
|
+
const compacted = await compactToFixedPoint(scheduler, maintenance, plan.steps.length);
|
|
289
|
+
const maintenanceCompactions = compacted.passes;
|
|
290
|
+
const compactionStop = compacted.stop;
|
|
291
|
+
const collected = await collectToFixedPoint(scheduler, maintenance, plan.steps.length);
|
|
292
|
+
let collectionPasses = collected.passes;
|
|
293
|
+
const collectionTail = [...collected.tail];
|
|
294
|
+
await assertModel(scheduler, maintenance, model);
|
|
295
|
+
await drive(scheduler, maintenance.close());
|
|
296
|
+
const sweeper = createDatabase();
|
|
297
|
+
const swept = await collectToFixedPoint(scheduler, sweeper, plan.steps.length);
|
|
298
|
+
collectionPasses += swept.passes;
|
|
299
|
+
collectionTail.push(...swept.tail);
|
|
300
|
+
if (collectionTail.length > 8)
|
|
301
|
+
collectionTail.splice(0, collectionTail.length - 8);
|
|
302
|
+
await drive(scheduler, sweeper.close());
|
|
303
|
+
const rows = sortedRows(model);
|
|
304
|
+
const [segments, transactions, manifests, currentManifest, leases, compactions, storageBytes] = await Promise.all([
|
|
305
|
+
collectStoragePages((cursor) => base.listSegmentPage(cursor, 256)),
|
|
306
|
+
collectStoragePages((cursor) => base.listTransactionPage(cursor, 256)),
|
|
307
|
+
collectStoragePages((cursor) => base.listManifestPage(cursor, 256)),
|
|
308
|
+
base.getCurrentManifest(),
|
|
309
|
+
base.listLeases(),
|
|
310
|
+
base.listCompactionJobs(),
|
|
311
|
+
base.getLogicalStorageBytes()
|
|
312
|
+
]);
|
|
313
|
+
const blockCount = currentManifest?.liveBlockCount ?? 0;
|
|
314
|
+
const blockLimit = plan.keySpace * 16 + 64;
|
|
315
|
+
const segmentLimit = plan.keySpace + 16;
|
|
316
|
+
const transactionLimit = segmentLimit + 16;
|
|
317
|
+
if (blockCount > blockLimit || segments.length > segmentLimit || transactions.length > transactionLimit) {
|
|
318
|
+
const transactionStates = transactions.reduce((counts, transaction) => {
|
|
319
|
+
counts[transaction.status] = (counts[transaction.status] ?? 0) + 1;
|
|
320
|
+
return counts;
|
|
321
|
+
}, {});
|
|
322
|
+
const compactionStates = compactions.reduce((counts, compaction) => {
|
|
323
|
+
counts[compaction.state] = (counts[compaction.state] ?? 0) + 1;
|
|
324
|
+
return counts;
|
|
325
|
+
}, {});
|
|
326
|
+
throw new Error(`Storage did not compact to a bound: ${String(blockCount)}/${String(blockLimit)} live blocks, ${String(segments.length)}/${String(segmentLimit)} segments, ${String(transactions.length)}/${String(transactionLimit)} transactions; ${JSON.stringify(transactionStates)} transaction states, ${JSON.stringify(compactionStates)} compaction states, ${String(currentManifest?.liveBlockCount ?? 0)} current blocks, compaction stopped at ${compactionStop}, ${String(manifests.length)} manifests (${String(manifests.filter((manifest) => manifest.prunedAt !== void 0).length)} pruned) remain after ${String(maintenanceCompactions)} compactions and ${String(collectionPasses)} collection passes; collection tail ${JSON.stringify(collectionTail)}`);
|
|
327
|
+
}
|
|
328
|
+
if (manifests.length > 2) {
|
|
329
|
+
throw new Error(`Garbage collection left ${String(manifests.length)} manifests and ${String(leases.length)} leases`);
|
|
330
|
+
}
|
|
331
|
+
if (leases.length !== 0) {
|
|
332
|
+
throw new Error(`Simulation clients leaked ${String(leases.length)} leases`);
|
|
333
|
+
}
|
|
334
|
+
const trace = [...scheduler.trace];
|
|
335
|
+
return {
|
|
336
|
+
seed: plan.seed,
|
|
337
|
+
acceptedMutations,
|
|
338
|
+
rejectedConflicts,
|
|
339
|
+
injectedFaults,
|
|
340
|
+
checkpoints,
|
|
341
|
+
maintenanceCompactions,
|
|
342
|
+
compactionStop,
|
|
343
|
+
collectionPasses,
|
|
344
|
+
collectionTail,
|
|
345
|
+
rows,
|
|
346
|
+
blockCount,
|
|
347
|
+
segmentCount: segments.length,
|
|
348
|
+
transactionCount: transactions.length,
|
|
349
|
+
manifestCount: manifests.length,
|
|
350
|
+
leaseCount: leases.length,
|
|
351
|
+
storageBytes,
|
|
352
|
+
schedulerHighWater: scheduler.highWater,
|
|
353
|
+
trace,
|
|
354
|
+
traceDigest: md5Hex(JSON.stringify(trace))
|
|
355
|
+
};
|
|
386
356
|
}
|
|
387
357
|
class FaultController {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
358
|
+
#point;
|
|
359
|
+
#occurrence = 0;
|
|
360
|
+
#seen = 0;
|
|
361
|
+
fired = false;
|
|
362
|
+
arm(point, occurrence) {
|
|
363
|
+
this.#point = point;
|
|
364
|
+
this.#occurrence = occurrence;
|
|
365
|
+
this.#seen = 0;
|
|
366
|
+
this.fired = false;
|
|
367
|
+
}
|
|
368
|
+
disarm() {
|
|
369
|
+
this.#point = void 0;
|
|
370
|
+
}
|
|
371
|
+
inject(point) {
|
|
372
|
+
if (point !== this.#point || this.fired)
|
|
373
|
+
return;
|
|
374
|
+
this.#seen++;
|
|
375
|
+
if (this.#seen !== this.#occurrence)
|
|
376
|
+
return;
|
|
377
|
+
this.fired = true;
|
|
378
|
+
throw new Error(`injected ${point} #${String(this.#occurrence)}`);
|
|
379
|
+
}
|
|
410
380
|
}
|
|
411
381
|
async function drive(scheduler, promise) {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
382
|
+
const result = (await scheduler.settle([promise]))[0];
|
|
383
|
+
if (result?.status === "fulfilled")
|
|
384
|
+
return result.value;
|
|
385
|
+
throw result?.reason ?? new Error("Scheduled operation did not settle");
|
|
416
386
|
}
|
|
417
387
|
function assertSettled(results) {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
388
|
+
const failed = results.find((result) => result.status === "rejected");
|
|
389
|
+
if (failed?.status === "rejected")
|
|
390
|
+
throw failed.reason;
|
|
421
391
|
}
|
|
422
392
|
function executeMutation(database, operation) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
"ON CONFLICT (id) DO UPDATE SET client = excluded.client, amount = excluded.amount", [operation.key, operation.client, operation.amount]);
|
|
393
|
+
if (operation.kind === "delete") {
|
|
394
|
+
return database.execute("DELETE FROM items WHERE id = ?", [operation.key]);
|
|
395
|
+
}
|
|
396
|
+
return database.execute("INSERT INTO items (id, client, amount) VALUES (?, ?, ?) ON CONFLICT (id) DO UPDATE SET client = excluded.client, amount = excluded.amount", [operation.key, operation.client, operation.amount]);
|
|
428
397
|
}
|
|
429
398
|
function applyMutation(model, operation) {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
399
|
+
if (operation.kind === "delete")
|
|
400
|
+
model.delete(operation.key);
|
|
401
|
+
else
|
|
402
|
+
model.set(operation.key, {
|
|
403
|
+
id: operation.key,
|
|
404
|
+
client: operation.client,
|
|
405
|
+
amount: operation.amount
|
|
406
|
+
});
|
|
438
407
|
}
|
|
439
408
|
async function assertModel(scheduler, database, model) {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
409
|
+
const actual = await readRows(scheduler, database);
|
|
410
|
+
if (!sameRows(actual, model)) {
|
|
411
|
+
throw new Error(`Model mismatch:
|
|
412
|
+
expected ${JSON.stringify(sortedRows(model))}
|
|
413
|
+
received ${JSON.stringify(actual)}`);
|
|
414
|
+
}
|
|
444
415
|
}
|
|
445
416
|
async function readRows(scheduler, database) {
|
|
446
|
-
|
|
447
|
-
|
|
417
|
+
const result = await drive(scheduler, database.query("SELECT id, client, amount FROM items ORDER BY id", { memoize: false }));
|
|
418
|
+
return result.rows;
|
|
448
419
|
}
|
|
449
420
|
function sameRows(rows, model) {
|
|
450
|
-
|
|
421
|
+
return JSON.stringify(rows) === JSON.stringify(sortedRows(model));
|
|
451
422
|
}
|
|
452
423
|
function sortedRows(model) {
|
|
453
|
-
|
|
424
|
+
return [...model.values()].sort((left, right) => left.id - right.id);
|
|
454
425
|
}
|
|
455
426
|
function replaceModel(target, source) {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
427
|
+
target.clear();
|
|
428
|
+
for (const [key, value] of source)
|
|
429
|
+
target.set(key, value);
|
|
459
430
|
}
|
|
460
431
|
function requireClient(clients, index) {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
432
|
+
const client = clients[index];
|
|
433
|
+
if (client === void 0)
|
|
434
|
+
throw new RangeError(`Simulation client is out of range: ${String(index)}`);
|
|
435
|
+
return client;
|
|
465
436
|
}
|
|
466
437
|
function validateSteps(steps, clients, keySpace) {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
}
|
|
494
|
-
if (operation.kind === "put" &&
|
|
495
|
-
(typeof operation.amount !== "number" || !Number.isSafeInteger(operation.amount))) {
|
|
496
|
-
throw new TypeError("Simulation put has an invalid amount");
|
|
497
|
-
}
|
|
498
|
-
};
|
|
499
|
-
for (const value of steps) {
|
|
500
|
-
if (typeof value !== "object" || value === null) {
|
|
501
|
-
throw new TypeError("Simulation step must be an object");
|
|
502
|
-
}
|
|
503
|
-
const step = value;
|
|
504
|
-
if (step.kind === "concurrent") {
|
|
505
|
-
if (!Array.isArray(step.operations) || step.operations.length > clients * 4) {
|
|
506
|
-
throw new TypeError("Simulation concurrent step is unbounded");
|
|
507
|
-
}
|
|
508
|
-
step.operations.forEach(validateMutation);
|
|
509
|
-
}
|
|
510
|
-
else if (step.kind === "fault") {
|
|
511
|
-
if (typeof step.point !== "string" || !validPoint.has(step.point)) {
|
|
512
|
-
throw new TypeError("Simulation fault point is invalid");
|
|
513
|
-
}
|
|
514
|
-
const occurrence = step.occurrence;
|
|
515
|
-
if (typeof occurrence !== "number" || !Number.isSafeInteger(occurrence) || occurrence < 1) {
|
|
516
|
-
throw new TypeError("Simulation fault occurrence is invalid");
|
|
517
|
-
}
|
|
518
|
-
validateMutation(step.operation);
|
|
519
|
-
}
|
|
520
|
-
else if (step.kind !== "checkpoint" && step.kind !== "maintenance") {
|
|
521
|
-
throw new TypeError("Simulation step kind is invalid");
|
|
522
|
-
}
|
|
438
|
+
const validPoint = /* @__PURE__ */ new Set([
|
|
439
|
+
"beforeBlockWrite",
|
|
440
|
+
"afterBlockWrite",
|
|
441
|
+
"beforeBlockRead",
|
|
442
|
+
"afterBlockRead",
|
|
443
|
+
"beforeTransactionCommit",
|
|
444
|
+
"afterTransactionCommit"
|
|
445
|
+
]);
|
|
446
|
+
const validateMutation = (value) => {
|
|
447
|
+
if (typeof value !== "object" || value === null) {
|
|
448
|
+
throw new TypeError("Simulation mutation must be an object");
|
|
449
|
+
}
|
|
450
|
+
const operation = value;
|
|
451
|
+
if (operation.kind !== "put" && operation.kind !== "delete") {
|
|
452
|
+
throw new TypeError("Simulation mutation kind is invalid");
|
|
453
|
+
}
|
|
454
|
+
const client = operation.client;
|
|
455
|
+
if (typeof client !== "number" || !Number.isSafeInteger(client) || client < 0 || client >= clients) {
|
|
456
|
+
throw new TypeError("Simulation mutation has an invalid client");
|
|
457
|
+
}
|
|
458
|
+
const key = operation.key;
|
|
459
|
+
if (typeof key !== "number" || !Number.isSafeInteger(key) || key < 1 || key > keySpace) {
|
|
460
|
+
throw new TypeError("Simulation mutation has an invalid key");
|
|
461
|
+
}
|
|
462
|
+
if (operation.kind === "put" && (typeof operation.amount !== "number" || !Number.isSafeInteger(operation.amount))) {
|
|
463
|
+
throw new TypeError("Simulation put has an invalid amount");
|
|
523
464
|
}
|
|
465
|
+
};
|
|
466
|
+
for (const value of steps) {
|
|
467
|
+
if (typeof value !== "object" || value === null) {
|
|
468
|
+
throw new TypeError("Simulation step must be an object");
|
|
469
|
+
}
|
|
470
|
+
const step = value;
|
|
471
|
+
if (step.kind === "concurrent") {
|
|
472
|
+
if (!Array.isArray(step.operations) || step.operations.length > clients * 4) {
|
|
473
|
+
throw new TypeError("Simulation concurrent step is unbounded");
|
|
474
|
+
}
|
|
475
|
+
step.operations.forEach(validateMutation);
|
|
476
|
+
} else if (step.kind === "fault") {
|
|
477
|
+
if (typeof step.point !== "string" || !validPoint.has(step.point)) {
|
|
478
|
+
throw new TypeError("Simulation fault point is invalid");
|
|
479
|
+
}
|
|
480
|
+
const occurrence = step.occurrence;
|
|
481
|
+
if (typeof occurrence !== "number" || !Number.isSafeInteger(occurrence) || occurrence < 1) {
|
|
482
|
+
throw new TypeError("Simulation fault occurrence is invalid");
|
|
483
|
+
}
|
|
484
|
+
validateMutation(step.operation);
|
|
485
|
+
} else if (step.kind !== "checkpoint" && step.kind !== "maintenance") {
|
|
486
|
+
throw new TypeError("Simulation step kind is invalid");
|
|
487
|
+
}
|
|
488
|
+
}
|
|
524
489
|
}
|
|
525
490
|
function errorMessage(error) {
|
|
526
|
-
|
|
491
|
+
return error instanceof Error ? error.message : String(error);
|
|
527
492
|
}
|
|
528
493
|
async function compactToFixedPoint(scheduler, database, workloadSteps) {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
494
|
+
const maximumPasses = Math.min(1e4, workloadSteps + 128);
|
|
495
|
+
for (let pass = 0; pass < maximumPasses; pass++) {
|
|
496
|
+
const result = await drive(scheduler, database.compactTable("items"));
|
|
497
|
+
if (!result.compacted)
|
|
498
|
+
return { passes: pass, stop: result.skipReason ?? "unknown" };
|
|
499
|
+
}
|
|
500
|
+
throw new Error(`Compaction did not converge within ${String(maximumPasses)} passes`);
|
|
536
501
|
}
|
|
537
502
|
async function collectToFixedPoint(scheduler, database, workloadSteps) {
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
if (emptyPasses === 2)
|
|
556
|
-
return { passes: pass, tail };
|
|
557
|
-
}
|
|
558
|
-
else
|
|
559
|
-
emptyPasses = 0;
|
|
560
|
-
}
|
|
561
|
-
throw new Error(`Garbage collection did not converge within ${String(maximumPasses)} passes`);
|
|
503
|
+
const maximumPasses = Math.min(1e4, workloadSteps + 128);
|
|
504
|
+
let emptyPasses = 0;
|
|
505
|
+
const tail = [];
|
|
506
|
+
for (let pass = 1; pass <= maximumPasses; pass++) {
|
|
507
|
+
const result = await drive(scheduler, database.collectGarbage());
|
|
508
|
+
tail.push(summarizeCollection(result));
|
|
509
|
+
if (tail.length > 8)
|
|
510
|
+
tail.shift();
|
|
511
|
+
const changed = result.prunedManifestCount + result.reclaimedSegmentCount + result.reclaimedBlockCount + result.reclaimedTransactionCount;
|
|
512
|
+
if (changed === 0) {
|
|
513
|
+
emptyPasses++;
|
|
514
|
+
if (emptyPasses === 2)
|
|
515
|
+
return { passes: pass, tail };
|
|
516
|
+
} else
|
|
517
|
+
emptyPasses = 0;
|
|
518
|
+
}
|
|
519
|
+
throw new Error(`Garbage collection did not converge within ${String(maximumPasses)} passes`);
|
|
562
520
|
}
|
|
563
521
|
function summarizeCollection(result) {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
522
|
+
return {
|
|
523
|
+
prunedManifests: result.prunedManifestCount,
|
|
524
|
+
retainedManifests: result.retainedManifestCount,
|
|
525
|
+
reclaimedSegments: result.reclaimedSegmentCount,
|
|
526
|
+
retainedSegments: result.retainedSegmentCount,
|
|
527
|
+
reclaimedBlocks: result.reclaimedBlockCount,
|
|
528
|
+
retainedBlocks: result.retainedBlockCount,
|
|
529
|
+
reclaimedTransactions: result.reclaimedTransactionCount,
|
|
530
|
+
retainedTransactions: result.retainedTransactionCount
|
|
531
|
+
};
|
|
574
532
|
}
|
|
575
|
-
// Stream-identical copy of `mulberry32` in ./seeds.ts. This module ships in the tarball and
|
|
576
|
-
// seeds.ts does not (it reads MINNOW_SEED and the unpublished regression-seeds.json), so the
|
|
577
|
-
// published simulator cannot import the canonical copy.
|
|
578
533
|
function mulberry32(seed) {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
534
|
+
let state = seed >>> 0;
|
|
535
|
+
return () => {
|
|
536
|
+
state = state + 1831565813 >>> 0;
|
|
537
|
+
let mixed = state;
|
|
538
|
+
mixed = Math.imul(mixed ^ mixed >>> 15, mixed | 1);
|
|
539
|
+
mixed ^= mixed + Math.imul(mixed ^ mixed >>> 7, mixed | 61);
|
|
540
|
+
return ((mixed ^ mixed >>> 14) >>> 0) / 4294967296;
|
|
541
|
+
};
|
|
587
542
|
}
|
|
588
543
|
function greatestCommonDivisor(left, right) {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
544
|
+
let a = left;
|
|
545
|
+
let b = right;
|
|
546
|
+
while (b !== 0)
|
|
547
|
+
[a, b] = [b, a % b];
|
|
548
|
+
return a;
|
|
594
549
|
}
|
|
550
|
+
export {
|
|
551
|
+
DeterministicScheduler,
|
|
552
|
+
generateSimulationPlan,
|
|
553
|
+
parseSimulationPlan,
|
|
554
|
+
runSimulation,
|
|
555
|
+
scheduledBlockStore
|
|
556
|
+
};
|