flatsql 2.0.3 → 3.0.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/LICENSE +1 -1
- package/README.md +54 -64
- package/dist/artifacts/index.d.ts +1 -3
- package/dist/artifacts/index.d.ts.map +1 -1
- package/dist/artifacts/index.js +1 -3
- package/dist/artifacts/index.js.map +1 -1
- package/dist/response/artifact.d.ts +1 -2
- package/dist/response/artifact.d.ts.map +1 -1
- package/dist/response/artifact.js.map +1 -1
- package/dist/response/types.d.ts +6 -0
- package/dist/response/types.d.ts.map +1 -1
- package/dist/stress/index.d.ts +0 -1
- package/dist/stress/index.d.ts.map +1 -1
- package/dist/stress/index.js +0 -1
- package/dist/stress/index.js.map +1 -1
- package/dist/stress/native-publish-event-workload.d.ts.map +1 -1
- package/dist/stress/native-publish-event-workload.js +14 -4
- package/dist/stress/native-publish-event-workload.js.map +1 -1
- package/dist/stress/smoke-runner.d.ts.map +1 -1
- package/dist/stress/smoke-runner.js +6 -5
- package/dist/stress/smoke-runner.js.map +1 -1
- package/docs/PARTITION-STORE-WASM.md +241 -0
- package/docs/PARTITION-STORE.md +635 -0
- package/package.json +61 -22
- package/wasm/flatsql-io.js +4 -1
- package/wasm/flatsql-ps-threads.wasm +0 -0
- package/wasm/flatsql-wasi-noeh.wasm +0 -0
- package/wasm/flatsql-wasi.wasm +0 -0
- package/wasm/flatsql.js +1 -1
- package/wasm/flatsql.wasm +0 -0
- package/wasm/index.d.ts +2 -0
- package/wasm/index.js +16 -1
- package/wasm/integrity.json +28 -0
- package/wasm/ps-node-fault.d.mts +23 -0
- package/wasm/ps-node-fault.mjs +381 -0
- package/wasm/ps-node-guest.mjs +156 -0
- package/wasm/ps-node-host.d.mts +86 -0
- package/wasm/ps-node-host.mjs +431 -0
- package/wasm/ps-node-imports.mjs +847 -0
- package/wasm/ps.d.ts +21 -0
- package/wasm/ps.js +75 -0
- package/dist/artifacts/builder.d.ts +0 -31
- package/dist/artifacts/builder.d.ts.map +0 -1
- package/dist/artifacts/builder.js +0 -514
- package/dist/artifacts/builder.js.map +0 -1
- package/dist/artifacts/demo-extractors.d.ts +0 -15
- package/dist/artifacts/demo-extractors.d.ts.map +0 -1
- package/dist/artifacts/demo-extractors.js +0 -238
- package/dist/artifacts/demo-extractors.js.map +0 -1
- package/dist/artifacts/standalone-builder.d.ts +0 -62
- package/dist/artifacts/standalone-builder.d.ts.map +0 -1
- package/dist/artifacts/standalone-builder.js +0 -144
- package/dist/artifacts/standalone-builder.js.map +0 -1
- package/dist/artifacts/worker-client.d.ts +0 -32
- package/dist/artifacts/worker-client.d.ts.map +0 -1
- package/dist/artifacts/worker-client.js +0 -415
- package/dist/artifacts/worker-client.js.map +0 -1
- package/dist/core/database.d.ts +0 -86
- package/dist/core/database.d.ts.map +0 -1
- package/dist/core/database.js +0 -900
- package/dist/core/database.js.map +0 -1
- package/dist/core/index.d.ts +0 -3
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/index.js +0 -3
- package/dist/core/index.js.map +0 -1
- package/dist/core/table-store.d.ts +0 -30
- package/dist/core/table-store.d.ts.map +0 -1
- package/dist/core/table-store.js +0 -187
- package/dist/core/table-store.js.map +0 -1
- package/dist/flatbuffer-accessor.d.ts +0 -35
- package/dist/flatbuffer-accessor.d.ts.map +0 -1
- package/dist/flatbuffer-accessor.js +0 -114
- package/dist/flatbuffer-accessor.js.map +0 -1
- package/dist/index.d.ts +0 -15
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -22
- package/dist/index.js.map +0 -1
- package/dist/stress/publish-event-workload.d.ts +0 -3
- package/dist/stress/publish-event-workload.d.ts.map +0 -1
- package/dist/stress/publish-event-workload.js +0 -467
- package/dist/stress/publish-event-workload.js.map +0 -1
- package/wasm/flatsql-artifact.worker.js +0 -847
- package/wasm/standalone.d.ts +0 -122
- package/wasm/standalone.js +0 -914
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
# FlatSQL partition store (format 2): native engine
|
|
2
|
+
|
|
3
|
+
The engine of the partition store program. Part I (§1–§11) is T1: append-only
|
|
4
|
+
partition logs, one writer per partition on a pinned pool, durable acks,
|
|
5
|
+
per-commit indexes, type owners, and durable-tail open. Part II (§12–§21) is
|
|
6
|
+
T2: reader instances and lanes, the snapshot protocol, the SQL surface, the
|
|
7
|
+
fan-out merge, admission and results. The design is the stack's
|
|
8
|
+
`docs/architecture/flatsql-partition-store.md` (§22 overrides §0–§21; §22.4
|
|
9
|
+
holds the owner rulings). This file records what was built, how to run it, the
|
|
10
|
+
measured acceptance, and every place the build departs from the design.
|
|
11
|
+
|
|
12
|
+
Compaction, splits and quota are T3; the wasm artifact is T4; SDN and browser
|
|
13
|
+
integration are T6/T10.
|
|
14
|
+
|
|
15
|
+
## 1. Code map
|
|
16
|
+
|
|
17
|
+
| Path | Contents |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `cpp/include/flatsql/flatsql_io.h`, `cpp/src/flatsql_io_native.cpp` | Seven-import host contract; flags `CREATE_PARENTS` 0x0100, `UNLINK_IF_UNUSED` 0x0200, `OPEN_DEFERRED` 0x0400, status `BUSY` −7 (the module-sdk 0.8.22 values). Native host: lock-free handle table, `F_FULLFSYNC` on darwin, `fdatasync` on Linux, parent-directory fsyncs. |
|
|
20
|
+
| `cpp/include/flatsql/ps/format.h`, `src/ps/format.cpp` | On-disk structs, magics, key encodings, A17 CID sort key, head slots, paths. |
|
|
21
|
+
| `ps/index.h`, `src/ps/index_l0.cpp`, `index_l1.cpp` | Per-commit L0 blocks, L1 runs (4 KiB blocks, fences, blooms, TOC), streaming k-way merge. |
|
|
22
|
+
| `ps/ring.h`, `src/ps/ring.cpp` | 64 KiB slab pool, SPSC rings, page map-ahead, rejects, owner word. |
|
|
23
|
+
| `ps/extract.h`, `src/ps/extract.cpp` | Per-type config (BFBS + rule text), frame checks, key extraction (A19), producer token (A3), sealed envelopes. |
|
|
24
|
+
| `ps/registry.h`, `src/ps/registry.cpp` | Registry frames and head (A10). |
|
|
25
|
+
| `ps/writer.h`, `src/ps/writer_pool.cpp` | Engine, writers, commit rounds, sync pool, mailbox, maintenance, producers. |
|
|
26
|
+
| `src/ps/partition_log.cpp` | Staging, dedupe, RETAG, supersede, TOMB, RECONCILE, TOMB_RANGE, transactions, publish. |
|
|
27
|
+
| `src/ps/merge.cpp` | Warm/cool, merge planning and outputs (A11), manifests. |
|
|
28
|
+
| `src/ps/type_owner.cpp` | Labels, arrivals (A15), cid catalog, REPEAT, REHOME (A14), type-level deletes, notices (A25). |
|
|
29
|
+
| `src/ps/journal.cpp` | Per-writer commit journal (A8 fallback). |
|
|
30
|
+
| `src/ps/open.cpp` | Open, journal replay, durable-tail adoption (A4), registration. |
|
|
31
|
+
| `ps/flatsql_ps.h`, `src/ps/capi_ps.cpp` | C ABI (i32/f64 only). |
|
|
32
|
+
| `schemas/flatsql_attr.fbs` | `RecordAttr` / `SourceTag` (generated with the published flatc-wasm). |
|
|
33
|
+
| `cpp/test/ps/`, `cpp/test/io_fault.cpp` | Tests, fault-injecting host, bench driver, A19 vectors. |
|
|
34
|
+
|
|
35
|
+
## 2. On-disk layout
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
<root>/fsql2/
|
|
39
|
+
STORE MIGRATED registry.fsl registry.fsh
|
|
40
|
+
j/<writer:02x>-{a,b}.fsj commit journal (only with commitJournal)
|
|
41
|
+
p/<pid:08x>/
|
|
42
|
+
h.fsh head, A/B 4 KiB slots
|
|
43
|
+
m-<seg:06x>.fsl meta log, one per data segment (A9)
|
|
44
|
+
d-<seg:06x>.fsd frames, [u32 size][FlatBuffer]
|
|
45
|
+
r-<seg>.fsr a-<seg>.fsa dense rows / attributes (merged ranges)
|
|
46
|
+
x-<seg>-<gen:04x>.fsx L1 runs
|
|
47
|
+
mf-<gen:06x>.fsm segment manifest
|
|
48
|
+
l.fsl lane table
|
|
49
|
+
t/<fid hex8>/
|
|
50
|
+
h.fsh m-<seg>.fsl type head and log (A10 batches)
|
|
51
|
+
g-<seg:06x>.fsg arrivals, 24 B entries (A15 segments)
|
|
52
|
+
g.fsf arrivals gseq fence index (A15)
|
|
53
|
+
x-<gen:06x>.fsx mf-<gen>.fsm cid catalog runs and manifest
|
|
54
|
+
s-<fp:016x>.fsc type config (BFBS + rules)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Byte layouts are the structs in `format.h` (all little-endian, CRC32C).
|
|
58
|
+
|
|
59
|
+
## 3. Write path
|
|
60
|
+
|
|
61
|
+
1. A producer reserves credits (`min(ring cap − used, pool free − reserve)`),
|
|
62
|
+
copies one entry into its ring and rings the owner's doorbell.
|
|
63
|
+
2. The owning writer stages each partition with backlog: verify (size, fid,
|
|
64
|
+
BFBS), CID check, extraction, dedupe (writer cid table, blooms, L0/L1),
|
|
65
|
+
RETAG for a new tag tuple (A2), supersede, lane deltas, the L0 block.
|
|
66
|
+
3. A commit round writes every staged batch and makes it durable:
|
|
67
|
+
- **file mode** (default, §6.4 as amended by A8): pwrite `d`/`l`/`g`/fence,
|
|
68
|
+
one concurrent sync round; pwrite `m` batches, a second sync round. Two
|
|
69
|
+
sync rounds per iteration whatever the number of dirty partitions.
|
|
70
|
+
- **journal mode** (`commitJournal`, §22.4 ruling 5): the same pwrites plus
|
|
71
|
+
one journal record holding their bytes; the journal fsync is the round's
|
|
72
|
+
only sync. A checkpoint thread later syncs the touched files and heads
|
|
73
|
+
(paced, see deviation 13) and truncates the retired journal file.
|
|
74
|
+
4. Publish: state, seqlock publication, head slot pwrite (checkpoint heads are
|
|
75
|
+
synced one round later in file mode, by the journal checkpoint in journal
|
|
76
|
+
mode), acks, notices to the type owners (A25: a full queue drops the
|
|
77
|
+
notice; the owner compares `labeled_through` with each partition's HWM).
|
|
78
|
+
5. Maintenance, bounded per iteration: merge planning (INTENT_MERGE first,
|
|
79
|
+
A11), MERGE_DONE once a helper has written and synced the outputs, seal by
|
|
80
|
+
size/records/age, next-segment pre-creation, idle reclaim, journal
|
|
81
|
+
checkpoints.
|
|
82
|
+
|
|
83
|
+
Mailbox commands (never ring entries, A24): adopt/handoff, rebalance, type
|
|
84
|
+
registration, type-level kills (TOMB_CID fan-out, A14) and `TOMB_RANGE{seg,
|
|
85
|
+
epoch < t}` for the quota planner. TOMB_RANGE runs in bounded steps, spares
|
|
86
|
+
supersede-lane heads and control kinds, and completes when its TOMBs are
|
|
87
|
+
durable.
|
|
88
|
+
|
|
89
|
+
Ownership (A26): owner word `{epoch u32, writer u8, state}`. A rebalance waits
|
|
90
|
+
for any in-flight merge helper, sets HANDOFF, and the target CASes it to
|
|
91
|
+
OWNED with epoch + 1. A helper re-validates `{epoch, OWNED}` before every file
|
|
92
|
+
mutation.
|
|
93
|
+
|
|
94
|
+
## 4. Open and recovery
|
|
95
|
+
|
|
96
|
+
1. `STORE` and `MIGRATED` (refused without the marker).
|
|
97
|
+
2. Registry: frames scanned past the head, truncated at the first bad CRC;
|
|
98
|
+
incarnation = max seen + 1, recorded durably before any batch.
|
|
99
|
+
3. Journal replay (A8): both files of every writer id from 0 until the first
|
|
100
|
+
id without journals; records in `seq` order; parts pwritten into their
|
|
101
|
+
files; the files fsynced; the journals truncated.
|
|
102
|
+
4. Types: head slot, label checkpoint (A10, > 128 pids), tail batches (commit
|
|
103
|
+
chain, incarnation, arrivals CRC, A15 segment switch; a journaled batch only
|
|
104
|
+
if replayed), fence index validation, fsync before adoption.
|
|
105
|
+
5. Partitions: head slot, tail batches across SEAL continuations (same rules),
|
|
106
|
+
fsync `d`/`m`, A11 intent cleanup (`UNLINK_IF_UNUSED`), DURABLE_CKPT head.
|
|
107
|
+
6. Type heads for adopted type tails, written after the partitions are
|
|
108
|
+
attached (their inline labels come from them).
|
|
109
|
+
|
|
110
|
+
Open never reads a `d-*` byte and never parses a frame
|
|
111
|
+
(`openDataBytes = framesParsedAtOpen = 0` in every crash trial).
|
|
112
|
+
|
|
113
|
+
## 5. Extraction rules (A19)
|
|
114
|
+
|
|
115
|
+
A type's extraction is engine data: `TypeConfig` = schema name, fid, BFBS,
|
|
116
|
+
rule text, limits, flags. The rule language is documented in `extract.h`
|
|
117
|
+
(`epoch`, `col`, `epoch_day`, `object`, `require`, `supersede`).
|
|
118
|
+
|
|
119
|
+
The production rule texts for OMM, MPE, OEM, CAT, PNM and RFB are
|
|
120
|
+
`cpp/test/ps/vectors/<TYPE>.rules`. `format_extraction_golden_vectors_A19`
|
|
121
|
+
checks them against rows produced by sdn-server's `extractIndexedFields`,
|
|
122
|
+
`recordIndexArgs` and `recordSupersedeKey` (copied verbatim into
|
|
123
|
+
`vectors/gen/goref.go`, built against the published SDS Go bindings 1.226.0)
|
|
124
|
+
over 420 frames built from the published SDS 1.226.0 schemas with the
|
|
125
|
+
published flatc-wasm: 310 OMM from a Space-Track GP archive day plus edge
|
|
126
|
+
cases (fallbacks, time zones, invalid dates, pre-1970), 44 MPE, 10 OEM, 48 CAT
|
|
127
|
+
(every enum value, out-of-range values, all supersede branches), 4 PNM,
|
|
128
|
+
4 RFB. All 420 match.
|
|
129
|
+
|
|
130
|
+
## 6. C ABI
|
|
131
|
+
|
|
132
|
+
`flatsql_ps_init(role, cfg, len)`, `flatsql_ps_start()`,
|
|
133
|
+
`flatsql_ps_layout(out)`, `flatsql_ps_wake(addr, n)`,
|
|
134
|
+
`flatsql_ps_pump(budget_us)`, `flatsql_ps_stop(deadline_ms)`,
|
|
135
|
+
`flatsql_ps_stats(out, len)`, `flatsql_ps_register_type(cfg, len)`,
|
|
136
|
+
`flatsql_ps_register_partition(peer, len, fid)`, `flatsql_ps_ring(pid)`.
|
|
137
|
+
Config TLV tags are listed in `flatsql_ps.h`.
|
|
138
|
+
|
|
139
|
+
## 7. Configuration (`EngineConfig`)
|
|
140
|
+
|
|
141
|
+
| Field | Default | Meaning |
|
|
142
|
+
|---|---|---|
|
|
143
|
+
| `writers` | 1 | writer threads (production: clamp(cores − 2, 1, 16)) |
|
|
144
|
+
| `syncThreads` | 0 = clamp(writers, 4, 8) | concurrent sync pool, fair per writer |
|
|
145
|
+
| `poolBytes` / `slabBytes` | 192 MiB / 64 KiB | ring slab pool |
|
|
146
|
+
| `defaultRingCap` | 4 MiB | per partition (type config may override) |
|
|
147
|
+
| `commitBytes` / `commitFrames` | 4 MiB / 1024 | per partition per commit |
|
|
148
|
+
| `sealBytes` / `sealRecords` / `sealAgeMs` | 64 MiB / 1 M / 1 h | segment seal |
|
|
149
|
+
| `ckptIntervalMs` / `ckptMetaBytes` | 250 / 64 KiB | head checkpoints (file mode) |
|
|
150
|
+
| `mergeL0Blocks` / `mergeL0Bytes` / `mergeMinL0Bytes` | 16 / 1 MiB / 64 KiB | merge triggers |
|
|
151
|
+
| `mergeHelpers` | 1 | merge builder threads (0: build on the writer) |
|
|
152
|
+
| `zeroFillStep` | 1 MiB | A8 zero-fill ahead |
|
|
153
|
+
| `arrivalsSegBytes` | 64 MiB | A15 arrivals segment |
|
|
154
|
+
| `commitJournal` | false | A8 fallback: per-writer commit journal |
|
|
155
|
+
| `journalCkptBytes` / `journalCkptMs` | 8 MiB / 1000 | journal checkpoint triggers |
|
|
156
|
+
| `noticeQueue` | 1024 | A25 notice queue per type |
|
|
157
|
+
| `typeCommitRows` / `reconcileStep` | 8192 / 4096 | bounded steps |
|
|
158
|
+
|
|
159
|
+
## 8. Tests and benchmarks
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
cmake -S cpp -B cpp/build -DCMAKE_BUILD_TYPE=RelWithDebInfo
|
|
163
|
+
cmake --build cpp/build -j 8 --target flatsql_ps_test flatsql_ps_bench
|
|
164
|
+
cpp/build/flatsql_ps_test # default suite, T1 + T2 (~2 min)
|
|
165
|
+
cpp/build/flatsql_ps_test --test=<name> # one test, slow ones included
|
|
166
|
+
cpp/build/flatsql_ps_test --crash-trials=N --crash-seed=S --test=crash_faults_T1_1
|
|
167
|
+
cpp/build/flatsql_ps_bench --mode=scaling|host02|dirty|soak [--journal=1] [--io=mem|fs] [--dir=D]
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Slow tests: `crash_faults_T1_1_full` (10,000 trials, 64 types, 256
|
|
171
|
+
partitions), `writer_single_writer_under_rebalance_T1_4_full` (10 min),
|
|
172
|
+
`type_notices_never_block_A25_full` (10 min),
|
|
173
|
+
`open_clean_1M_records_256_partitions_T1_2_full` and `_real_fs`.
|
|
174
|
+
|
|
175
|
+
The crash harness (FaultFs) injects: drop all unsynced writes, drop a random
|
|
176
|
+
subset, tear the last write at a 512 B boundary, reorder unsynced writes, and
|
|
177
|
+
kill -9 keeping the page cache; every other trial adds the A4 sequence
|
|
178
|
+
(reopen, resend, publish, second crash dropping everything). Half the stores
|
|
179
|
+
run in journal mode; seals, merges (helper and inline), arrivals seals,
|
|
180
|
+
RECONCILE, TOMB_CID, TOMB_RANGE, transactions and registrations run under the
|
|
181
|
+
crashes. A stalled ack counts as a failure.
|
|
182
|
+
|
|
183
|
+
## 9. Acceptance (§18 T1 as amended)
|
|
184
|
+
|
|
185
|
+
Machines: **Mac** = owner Mac Studio (Darwin 25.3.0, arm64, 28 cores,
|
|
186
|
+
APFS, `F_FULLFSYNC`), shared with other lanes (load average 13–42 during
|
|
187
|
+
these runs). **Docker** = Linux 6.12 linuxkit arm64 in Docker Desktop on that
|
|
188
|
+
Mac (ext4 on a virtual disk), `--cpus`/`--memory` as noted. Neither is
|
|
189
|
+
host-02 or a Linux-8 box; §11 lists what needs one. All numbers come from
|
|
190
|
+
`MEASURED` lines of the tests and `flatsql_ps_bench`, run on task-branch builds from 16a4c63 to 7f941bc.
|
|
191
|
+
|
|
192
|
+
| # | Acceptance | Result | Where |
|
|
193
|
+
|---|---|---|---|
|
|
194
|
+
| 1 | 10,000 injected crashes, 256 partitions, 64 types (OMM/MPE/CAT/IQC shapes); acked bytes identical, gap-free pseqs, counters = recount, no gseq reassigned, 0 `d-*` bytes read by open; A4 crash → reopen → resend → second drop-all crash; A10 type/registry crash points; A11 merge crash points | **Pass.** Final build (7f941bc), Mac: 10,000 trials over the five modes (about 2,000 each: drop all unsynced, drop a subset, tear the last write at 512 B, reorder, kill -9) plus an A4 second drop-all crash every other trial; 3,437,331 acked records verified; 526,105 merges, 1,087 seals, 69,126 arrivals seals, 11,107 TOMB_RANGE commands, 983,753 journal records replayed (half the stores journaled); 243 s. Linux (Docker): 2 × 1,000 trials on the final build, 5 × 1,000 on earlier ones. | Mac `crash_faults_T1_1_full`; Docker `--crash-trials=1000` |
|
|
195
|
+
| 2 | Open, 1 M records in 256 partitions: clean ≤ 150 ms and ≤ 1 MiB I/O; after drop-unsynced ≤ 64 KiB `m` per active partition; 0 frames parsed | **Pass.** Real fs: Docker ext4 39.6 ms, 476 KB read; Mac APFS 34.1 ms, 574 KB read, 1 fsync (the registry incarnation). In-memory host: 4.5 ms, 577 KB. After drop-unsynced: 24 KB of `m` per partition (64 partitions). `framesParsedAtOpen = 0`, `openDataBytes = 0`. | `open_clean_1M_records_256_partitions_T1_2_real_fs`, `_full`, `open_after_drop_unsynced_reads_bounded_meta` |
|
|
196
|
+
| 3 | Resend the last 10 k acked after a crash: 0 new rows, 0 `d-*` bytes read | **Pass.** 0 rows, 0 bytes. | `open_resend_after_crash_dedupes_T1_3` |
|
|
197
|
+
| 4 | Rebalance every 50 ms for 10 min: pseq strictly increasing and gap-free, one writer thread per ownership interval, no overlap; A26: rebalances during merges with helpers stalled 100 ms, 0 non-owner writes | **Pass.** 600 s, 8 writers, 64 partitions, producers paced to ~6,400 records/s: 10,623 moves, 761,217 audited commits, 7,624 ownership intervals, 0 violations (gap, order, overlap, second thread in an interval); 33,511 merges, 5,181 injected 100 ms helper stalls, 5,448 handoffs that waited for an in-flight helper, 0 non-owner helper writes; every sent record present after reopen. | `writer_single_writer_under_rebalance_T1_4_full` (Mac) |
|
|
198
|
+
| 5 | One partition flooded: ring ≤ cap, others' p99 changes ≤ 20%, pool within budget, credits reach 0 and recover, 0 frames lost | **Pass.** Ring max 4,193,680 of 4,194,304 B; others' ack p99 34.5 → 30.4 ms (ratio 0.88; 28 hardware threads); pool peak 111 slabs (≤ total − reserve); 101,094 zero-credit samples and 14,006 credit waits, credits > 0 after; every sent frame present after reopen. The test enforces the ring, pool, credit and frame checks on every machine. The 20% ack-p99 ratio is a Linux-8 acceptance: it is enforced only on machines with ≥ 8 hardware threads and otherwise printed with the thread count (a 3-vCPU CI runner measured 91.6 → 154.9 ms once, then passed on rerun). | `writer_backpressure_flood_T1_5` (Mac) |
|
|
199
|
+
| 6 | Linux-8: N=8/N=1 ≥ 5× in memory, ≥ 3× with real fdatasync; 2 fsyncs per partition commit | **Pass on the proxy.** Docker `--cpus 8`: in memory 154,230 → 829,233 records/s (5.38×); real fs 80,883 → 456,263 records/s (5.64×); `d` 1.000 + `m` 1.000 fsyncs per framed partition commit. Journal mode: 1 journal fsync per committing iteration. | `flatsql_ps_bench --mode=scaling --io=mem|fs --writers=8` |
|
|
200
|
+
| 7 | host-02: N=1 ≥ 5,000 OMM records/s durable over 50 partitions; ack p99 ≤ 40 ms at 2,500/s | **Needs host-02.** Sustained rate passes everywhere measured (Docker 4 vCPU 38,912–100,598 records/s; 1 vCPU 35,788–85,149; Mac 19,920–26,936). Ack p99 at 2,500/s follows the disk's fsync tail and the shared box's load, not the engine: the same Docker 4 vCPU profile gave file mode p50 17–20 ms / p99 153–214 ms and journal mode p50 1.1–4.0 ms / p99 120 ms at load 15–20 (that disk's `fio` fdatasync p50 0.66 ms, p99 27 ms, p99.9 142 ms), and p99 0.4–4.4 s in both modes (an A/B run of the build at da0ad51 gave the same), when the box ran at load 12–42 with other lanes' I/O. Mac APFS (`F_FULLFSYNC`, load 14–22): file p50 757 / p99 998 ms, journal p50 343 / p99 567 ms. A8 at 1/10/50 dirty partitions (Docker 4 vCPU, load 15–20): file p99 23 / 95 / 185 ms, journal 17 / 93 / 170 ms. | `--mode=host02`, `--mode=dirty`, `--journal=0|1` |
|
|
201
|
+
| 8 | 30-min soak: every shared lock max hold ≤ 50 ms, p99.9 ≤ 1 ms; hot-path `malloc` per record = 0 | **Pass.** Mac, 1,800 s, 3,600,000 records over 50 partitions, 2 writers, real fs: hot-path `malloc` 0 per record; partition seqlock writer sections (144,399) max 0.027 ms, p99.9 ≤ 0.001 ms; registration lock (116 holds) max 0.025 ms, p99.9 bucket ≤ 0.033 ms. The other locks are queue mutexes held for one push or pop. `writer_hot_path_zero_malloc_T1_8`: 0 allocations over 4,000 records in file and in journal mode. (Ack p99 in that run was 6.5 s: the Mac's `F_FULLFSYNC` at 2 per partition commit, see row 7.) | `flatsql_ps_bench --mode=soak` (Mac); `writer_hot_path_zero_malloc_T1_8` |
|
|
202
|
+
| 9 | 2,048 partitions, 64 active: writer committed memory ≤ 288 MiB; idle partitions hold 0 slabs | **Pass.** 80.6 MiB committed (pool 25.8 MiB touched of 192 MiB, descriptors 11.9 MiB); 0 idle partitions with slabs. | `writer_memory_2048_partitions_64_active_T1_9` |
|
|
203
|
+
|
|
204
|
+
§22 amendments naming T1:
|
|
205
|
+
|
|
206
|
+
| Amendment | Status |
|
|
207
|
+
|---|---|
|
|
208
|
+
| A2 TAG rows, RETAG, RECONCILE | Built; B1/B2 sequence gives 0 TOMBs and 0 new arrivals, B3 tombstones exactly the removed record (`writer_dedupe_retag_reconcile_A2`); RECONCILE and TAG_TOMB under crashes. |
|
|
209
|
+
| A4 durable tails | Built (fsync before adoption, DURABLE_CKPT head, quarantine on fsync EIO, dedupe acks at durable commit); the crash harness's A4 mode. |
|
|
210
|
+
| A8 sync rounds, zero-fill, journal fallback | Built: 2 sync rounds per iteration (file mode) or 1 (journal mode), zero-fill ahead, A8 ack p99 reported at 1/10/50 dirty partitions. |
|
|
211
|
+
| A9 segmented meta log | Built (`m-<seg>` per data segment). Retirement is T3 (deviation 3). |
|
|
212
|
+
| A10 type-log and registry durability | Built (type batch format, > 128 pids label checkpoints, registry truncation, EXCL heads, pid never reused). |
|
|
213
|
+
| A11 merge intents | Built (INTENT_MERGE, idempotent `r`/`a`, fresh `x`/`mf`, open unlinks orphans). |
|
|
214
|
+
| A14 kills and promotion | Built (TOMB_CID fan-out, REHOME keeps gseq). |
|
|
215
|
+
| A15 arrivals | Segments and fence index built; compaction is T3 (deviation 4). MaxRowID = published `gseq_hi`. |
|
|
216
|
+
| A19 extraction parity, sealed frames | Built; 420/420 golden rows (§5); sealed envelopes checked by magic and size only. |
|
|
217
|
+
| A25 notices never block | Built. 10-minute run (Mac, 2 writers, crossed type ownership, notice queue of 1): 253,777 notices dropped, `labeled_through` lag p99 0.001 ms and max 222 ms, longest producer ack wait 414 ms (0 stalls ≥ 1 s). Under a load that saturates the type owners' labeling, the lag is throughput-bound, not bounded by the commit window. |
|
|
218
|
+
| A26 ownership epochs | Built except stage-1 prep states (deviation 2). |
|
|
219
|
+
| A34 published deps | `flatsql_attr_generated.h` and the A19 vectors come from the published flatc-wasm, SDS npm 1.226.0 and SDS Go 1.226.0; no sibling checkout is read at build time except the flatbuffers sources CMake already used. |
|
|
220
|
+
| A35 host-02 budget | Memory measured against the host-02 profile (row 9); the ops receipt (`nproc`, `free -m`, `df`, `fio`) needs the hosts (§11). |
|
|
221
|
+
|
|
222
|
+
## 10. Design deviations
|
|
223
|
+
|
|
224
|
+
Each entry: what the design says, what was built, why, and the evidence.
|
|
225
|
+
|
|
226
|
+
1. **Merge outputs are built and synced by a maintenance helper thread**
|
|
227
|
+
(design §4.5/§11: the owner merges between commits; helpers only for split
|
|
228
|
+
hot partitions, §12). The owner plans the merge (INTENT_MERGE), a helper
|
|
229
|
+
writes `r`/`a` at plan-derived offsets and fresh `x`/`mf` files, syncs them,
|
|
230
|
+
and the owner commits MERGE_DONE. Why: building a run on the writer stalled
|
|
231
|
+
every partition it owns (maintenance step max at 50 dirty partitions, Linux
|
|
232
|
+
1 vCPU profile: 325.8 ms with merges on the writer, 125.7 ms with the helper
|
|
233
|
+
sharing that one CPU); the owner law puts
|
|
234
|
+
compaction "per partition, off the hot path". The helper writes only new
|
|
235
|
+
files or regions no reader can see before MERGE_DONE, re-validates the owner
|
|
236
|
+
epoch before every mutation (A26), and a HANDOFF waits for it
|
|
237
|
+
(`writer_single_writer_under_rebalance_T1_4`: helpers stalled 100 ms, 0
|
|
238
|
+
non-owner writes). Its syncs no longer ride the commit's first sync round.
|
|
239
|
+
2. **No stage-1 helpers; ring entries carry no prep state** (§5.2, A26 "FREE →
|
|
240
|
+
CLAIMED(epoch) → PREPARED"). The owning writer verifies, extracts and
|
|
241
|
+
checks CIDs itself. Scaling came from partitions spread over writers
|
|
242
|
+
(T1 #6 met without them, see §9), so the extra protocol bought
|
|
243
|
+
nothing measurable. A26's other clauses (owner word, epoch re-validation,
|
|
244
|
+
slab grace) are built.
|
|
245
|
+
3. **A torn-both-heads rebuild replays the meta log from segment 0** (A9:
|
|
246
|
+
"scans only the active m segment plus the manifest"). Sealed `m` segments
|
|
247
|
+
are retired by T3 (A12 lifecycle), which also records `first_live_m_seg`;
|
|
248
|
+
until then segment 0 onward is the only complete source.
|
|
249
|
+
4. **A15 arrivals compaction is not built.** Arrivals are segmented with a
|
|
250
|
+
gseq fence index (`g-<seg>.fsg`, `g.fsf`); dropping dead entries and
|
|
251
|
+
folding DEAD entries out of catalog runs belong with T3's compaction and
|
|
252
|
+
quota planner, which trigger them. MaxRowID is the published `gseq_hi`.
|
|
253
|
+
5. **The A8 commit journal is a configuration mode, off by default.** §22.4
|
|
254
|
+
ruling 5 makes it the fallback "if a host misses T1 #7"; host-02 has not
|
|
255
|
+
been measured (no ssh from this lane). Both modes pass the crash harness
|
|
256
|
+
(half its stores run journaled) and are benchmarked in §9.
|
|
257
|
+
6. **A19 golden vectors come from a local archive day, not the host-02
|
|
258
|
+
inventory sample** (no host access). The frames are real Space-Track GP
|
|
259
|
+
records (`/opt/data/sdn-archive/spacetrack/gp/2026/2026-09-27.json.gz`)
|
|
260
|
+
plus constructed edge cases; the expected rows come from the Go functions
|
|
261
|
+
themselves.
|
|
262
|
+
7. **New rule directive `require <table path>`**: OEM's Go extraction returns
|
|
263
|
+
no keys at all when the first block has no OBJECT; the rule language had no
|
|
264
|
+
way to say that.
|
|
265
|
+
8. **New index kind `TAG_DEAD`** (A2 lists only TAG_TOMB rows). A PUT row is
|
|
266
|
+
also its record's first tag instance; posting its TAG_TOMB under `DEAD`
|
|
267
|
+
would have killed the record. `TAG_DEAD` keys a dead tag instance
|
|
268
|
+
separately from a dead record.
|
|
269
|
+
9. **A full type L0 directory (48 blocks) makes the type owner wait for its
|
|
270
|
+
merge** instead of committing more label batches; the head lists at most
|
|
271
|
+
48 unmerged blocks.
|
|
272
|
+
10. **The registration lock holds no I/O.** A registry frame reserves its
|
|
273
|
+
offset under a short mutex, is pwritten outside every lock, and returns
|
|
274
|
+
once all earlier frames are written (so its fsync covers them); a failed
|
|
275
|
+
pwrite breaks the registry for the run and open truncates at the hole.
|
|
276
|
+
The hint head is encoded under the lock and written after it. A second
|
|
277
|
+
registrant of the same key waits for the first. With the fsyncs inside,
|
|
278
|
+
the 30-minute Mac soak measured a 92 ms hold; with only the pwrite inside,
|
|
279
|
+
1.8 ms (a pwrite queued behind another registrant's `F_FULLFSYNC`).
|
|
280
|
+
11. **In journal mode heads carry no DURABLE_CKPT meaning.** Heads are written
|
|
281
|
+
every commit and made durable by the journal checkpoint; open trusts
|
|
282
|
+
`max(head, replayed journal)` and never adopts a journaled batch its
|
|
283
|
+
journal did not replay.
|
|
284
|
+
12. **Backpressure acceptance "credits go to 0"** is measured as samples with
|
|
285
|
+
less than one entry of credit (< 1 KiB) and producer credit waits during
|
|
286
|
+
the flood, then credits > 0 after it.
|
|
287
|
+
13. **Journal checkpoints run on their own thread and are paced** (a 50% duty
|
|
288
|
+
cycle per file sync). Back-to-back checkpoint fsyncs queued ahead of the
|
|
289
|
+
journal fsyncs acks wait on, and a long checkpoint on the merge helper
|
|
290
|
+
delayed merges until L0 directories filled.
|
|
291
|
+
|
|
292
|
+
## 11. Needs a Linux host or ops
|
|
293
|
+
|
|
294
|
+
Commands for the rows this lane could not measure (run from a flatsql
|
|
295
|
+
checkout at the landed commit, `cmake --build cpp/build -j 8 --target
|
|
296
|
+
flatsql_ps_bench flatsql_ps_test` first):
|
|
297
|
+
|
|
298
|
+
- **T1 #7 on host-02** (4 vCPU / 7.9 GiB), on the store disk, both modes:
|
|
299
|
+
`cpp/build/flatsql_ps_bench --mode=host02 --dir=<store disk>/ps-bench --journal=0` and
|
|
300
|
+
`--journal=1`; `cpp/build/flatsql_ps_bench --mode=dirty --dir=<store disk>/ps-bench --journal=0|1`.
|
|
301
|
+
If file mode misses p99 ≤ 40 ms and journal mode meets it, §22.4 ruling 5
|
|
302
|
+
selects `commitJournal` for that host.
|
|
303
|
+
- **A8/A35 ops receipt** on host-01 and host-02: `nproc; free -m; df -h <store disk>;
|
|
304
|
+
fio --name=fds --rw=write --bs=16k --size=256m --fdatasync=1 --directory=<store disk>`
|
|
305
|
+
(p50/p99 of `sync`).
|
|
306
|
+
- **T1 #6 on a real Linux-8 box**: `cpp/build/flatsql_ps_bench --mode=scaling --io=mem --writers=8`
|
|
307
|
+
and `--io=fs --dir=<ext4 dir>`.
|
|
308
|
+
- **LazyFS** power-loss runs (the crash harness emulates the loss modes in
|
|
309
|
+
memory; a filesystem-level run needs Linux with FUSE):
|
|
310
|
+
`lazyfs` mounted at `<dir>`, then `flatsql_ps_test --test=open_clean_1M_records_256_partitions_T1_2_real_fs --dir=<dir>`
|
|
311
|
+
with `lazyfs::clear-cache` between ingest and open.
|
|
312
|
+
|
|
313
|
+
# Part II: readers and SQL (T2)
|
|
314
|
+
|
|
315
|
+
## 12. Code map
|
|
316
|
+
|
|
317
|
+
| Path | Contents |
|
|
318
|
+
|---|---|
|
|
319
|
+
| `ps/lane.h`, `src/ps/lane.cpp` | Reader instances, lanes, the mailbox ABI (request slots, result rings, parking, cancellation), the native client, `CounterReader` (counters without a lane, A28). |
|
|
320
|
+
| `ps/lane_arena.h`, `src/ps/lane_arena.cpp` | Per-lane TLSF arenas behind `SQLITE_CONFIG_MALLOC`, instrumented SQLite mutexes, the null VFS. |
|
|
321
|
+
| `ps/snapshot.h`, `src/ps/snapshot.cpp` | The reader's view of committed state: registry view, partition and type snapshots, rows, frames, attributes, posting scans and lookups, lazily read L1 runs, the instance's shared index cache, arrivals, GONE counting. |
|
|
322
|
+
| `ps/vtab.h`, `src/ps/vtab_partition.cpp` | Record virtual tables: module, plans (`xBestIndex`), columns, partition-level row sources. |
|
|
323
|
+
| `src/ps/vtab_fanout.cpp` | Type level: pruning, the k-way merge, arrivals order with REHOME/GONE joins, the cid catalog, `<TYPE>_current`, offset paging. |
|
|
324
|
+
| `src/ps/vtab_meta.cpp` | `flatsql_partitions`, `flatsql_lanes`, `flatsql_licences`, `flatsql_arrivals`, `flatsql_types`. |
|
|
325
|
+
| `src/ps/admission.cpp` | Plan admission (bounded bit), the sandbox authorizer. |
|
|
326
|
+
| `ps/result_block.h`, `src/ps/result_block.cpp` | RB1 encoder/decoder, raw-stream framing. |
|
|
327
|
+
| `cpp/test/ps/{lane,fanout_merge,int64_exact,lane_isolation,snapshot,arrivals_paging,reader_concurrency}_test.cpp` | T2 tests; `reader_fixtures.*` helpers; `vectors/rb1_int64.hex` (the RB1 golden vector for T6's Go decoder). |
|
|
328
|
+
|
|
329
|
+
## 13. Reader instances and lanes
|
|
330
|
+
|
|
331
|
+
- A `ReaderInstance` runs L lane threads of one class: **interactive**
|
|
332
|
+
(index-bounded plans only), **bulk** (any plan; `setpriority` nice +10 on
|
|
333
|
+
Linux, QoS utility on darwin) or **sandbox** (untrusted SQL under a work
|
|
334
|
+
budget, A28). Instances share nothing with a writer: they read files only.
|
|
335
|
+
- Each lane owns a SQLite connection (`:memory:`, `SQLITE_OPEN_NOMUTEX`, the
|
|
336
|
+
`flatsql_ps_null` VFS that refuses every file open, `temp_store=MEMORY`), a
|
|
337
|
+
TLSF arena (8 MiB interactive and sandbox, 128 MiB bulk) installed through
|
|
338
|
+
`SQLITE_CONFIG_MALLOC` (thread-bound dispatch), lookaside carved from that
|
|
339
|
+
arena, its own file handles (LRU of host handles) and caches.
|
|
340
|
+
- SQLite is built separately for the readers (`sqlite3_ps`):
|
|
341
|
+
`THREADSAFE=2`, `DEFAULT_MEMSTATUS=0`, `OMIT_WAL`, `TEMP_STORE=3`.
|
|
342
|
+
- Lane threads get explicit stacks (2 MiB default, ≥ 1 MiB enforced) with a
|
|
343
|
+
guard page and a canary word checked every loop iteration (A30).
|
|
344
|
+
- **Mailbox ABI** (what the Go router programs against): a pool of request
|
|
345
|
+
slots in instance memory. A slot is a `SlotHeader` (state, cancel word,
|
|
346
|
+
doorbells, budgets, status, statistics) followed by a request area (SQL,
|
|
347
|
+
then RB1-encoded parameters) and an SPSC result ring. Clients claim a free
|
|
348
|
+
slot by CAS, write the request and queue its index on an MPMC queue; an
|
|
349
|
+
idle lane is woken through its doorbell word.
|
|
350
|
+
- **Parking (A28).** A lane never blocks on output: when a slot's ring is
|
|
351
|
+
full the statement parks (its `sqlite3_stmt` stays resumable) and the lane
|
|
352
|
+
serves other slots, up to 8 parked statements per lane. A client read that
|
|
353
|
+
frees space rings the owning lane.
|
|
354
|
+
- **Cancellation** is the slot's cancel word, polled by the progress handler
|
|
355
|
+
every 4,096 VM steps and by every vtab loop every 4,096 entries (A21, A28).
|
|
356
|
+
The instance's stop word is polled the same way. No deadline, no abandon.
|
|
357
|
+
- **A12 announcements.** Each lane publishes the start time of its oldest
|
|
358
|
+
running (or parked) statement; `ReaderInstance::oldestActiveStart()` is the
|
|
359
|
+
minimum. The reclaimer (T3) may unlink files a SWAP retired at time t once
|
|
360
|
+
that minimum is later than t, checked twice a grace apart.
|
|
361
|
+
|
|
362
|
+
## 14. Snapshot protocol (§8)
|
|
363
|
+
|
|
364
|
+
1. Each statement re-reads the registry head and parses new frames only
|
|
365
|
+
(copy-on-write view; a changed incarnation rebuilds the view).
|
|
366
|
+
2. A partition is snapshotted once per statement at first touch: the valid
|
|
367
|
+
head slot with the highest gen (8 KiB, one pread), then its manifest
|
|
368
|
+
(immutable, cached per lane).
|
|
369
|
+
3. Type-level statements snapshot the type head first (labels inline, or the
|
|
370
|
+
A10 checkpoint plus deltas for > 128 partitions, cached incrementally),
|
|
371
|
+
then each partition. **V_p = min(pseq_hi, labeled_through[p]).** A
|
|
372
|
+
partition head older than `labeled_through` (the writer pwrites the head
|
|
373
|
+
right after the commit its type owner may already have labeled) is re-read,
|
|
374
|
+
bounded (4 ms), so V_p never hides a promotion.
|
|
375
|
+
4. Rows come from `r-<seg>` (merged range, per the manifest) or the meta
|
|
376
|
+
batch the head's L0 directory names; payloads from `d-<seg>` at
|
|
377
|
+
`(off, len)`, CRC-checked against the row's `dataCrc`.
|
|
378
|
+
5. Liveness: a DEAD posting with killer ≤ V_p. Type level: a row is FIRST when
|
|
379
|
+
it has no REPEAT posting (REPEAT is a lookup kind with a bloom; a REPEAT
|
|
380
|
+
label always leaves one), otherwise its latest LABEL decides (A14
|
|
381
|
+
promotions). Readers take no lock shared with a writer.
|
|
382
|
+
6. A file named by the snapshot that fails with ENOENT before the first row
|
|
383
|
+
re-snapshots once; after a row it returns `FLATSQL_SNAPSHOT_GONE`
|
|
384
|
+
(retryable). The announcement protocol makes that happen only after a
|
|
385
|
+
restart.
|
|
386
|
+
|
|
387
|
+
## 15. SQL surface (§9, A17, A18)
|
|
388
|
+
|
|
389
|
+
Virtual tables are created lazily in the lane's temp schema the first time a
|
|
390
|
+
statement names them:
|
|
391
|
+
|
|
392
|
+
| Table | Rows |
|
|
393
|
+
|---|---|
|
|
394
|
+
| `sds_p_<producer>__<TYPE>` | every live PUT of one partition (partition-level visibility) |
|
|
395
|
+
| `<TYPE>` | the fan-out: one row per live CID, FIRST copies only |
|
|
396
|
+
| `<TYPE>@<source>` | `<TYPE>` with a live tag instance of that source |
|
|
397
|
+
| `<TYPE>_current` | latest live record per stored supersede key, else per object key (OBJECT_EPOCH) |
|
|
398
|
+
| `flatsql_partitions`, `flatsql_lanes`, `flatsql_licences`, `flatsql_arrivals`, `flatsql_types` | meta (never public) |
|
|
399
|
+
|
|
400
|
+
Record vtabs expose the root table's fields (BFBS reflection, vtable-slot
|
|
401
|
+
order; sealed rows project NULL) and hidden columns `_pseq _cid _cid_bin
|
|
402
|
+
_epoch _arrival _gseq _producer _source _source_name _provider _batch _peer_id
|
|
403
|
+
_signature _data _offset _len _kind _rowid _pid`. `_source` is
|
|
404
|
+
`'<TYPE>@<source_name>'`; `_rowid` is `_pseq` on a partition and `_gseq` at
|
|
405
|
+
type level.
|
|
406
|
+
|
|
407
|
+
Plans (`xBestIndex`), each recording a bounded bit in its `idxStr`:
|
|
408
|
+
|
|
409
|
+
| Access | Claims | Order |
|
|
410
|
+
|---|---|---|
|
|
411
|
+
| CID | `_cid` / `_cid_bin` EQ (type level: the cid catalog) | – |
|
|
412
|
+
| pseq | `_pseq` / `_rowid` EQ or range (partition) | pseq ± |
|
|
413
|
+
| gseq | `_gseq` / `_rowid` EQ or range (type: arrivals), OFFSET pushdown | gseq ± |
|
|
414
|
+
| COL | indexed column EQ (`col <n> u64pos|str:FIELD` rules) | – |
|
|
415
|
+
| tag | `_provider` / `_batch` / `_peer_id` EQ | – |
|
|
416
|
+
| source | `_source` / `_source_name` EQ (+ `_epoch` range; the alias) | epoch ± |
|
|
417
|
+
| epoch | `_epoch` range or `ORDER BY _epoch` (EPOCH index) | epoch ± |
|
|
418
|
+
| default | none of the above at type level (EPOCH_CID index) | seconds DESC, CID ASC |
|
|
419
|
+
| full | anything else | unbounded |
|
|
420
|
+
|
|
421
|
+
A plan is bounded when its index range is closed or a LIMIT applies to an
|
|
422
|
+
index order the vtab emits. The fan-out prunes by `_producer` EQ, merges
|
|
423
|
+
index-ordered sub-cursors on (key, CID, pid, pseq) with a heap, drops
|
|
424
|
+
adjacent duplicate CIDs, and stops at LIMIT.
|
|
425
|
+
|
|
426
|
+
**Admission.** Interactive lanes read the prepared statement's own program
|
|
427
|
+
(`sqlite3_stmt_explain`): any `VFilter` whose plan is unbounded returns
|
|
428
|
+
`FLATSQL_NEEDS_BULK` before a row is examined. Every statement reports
|
|
429
|
+
rows examined, bytes read, index entries and fence reads.
|
|
430
|
+
|
|
431
|
+
**Untrusted SQL (A28, §22.4 ruling 7).** Sandbox requests keep the SandboxCaps
|
|
432
|
+
contract: one read-only SELECT, the authorizer allow-list mapped onto the
|
|
433
|
+
record vtab names (meta tables never public), `<TYPE>` bounded to the newest N
|
|
434
|
+
arrivals (A18; `OMM` 400,000, other types 10,000), and a work budget — rows
|
|
435
|
+
examined, bytes read, VM steps — that is an admission limit returning the
|
|
436
|
+
existing `timeout` code. Sandbox SQL never runs on the shared bulk lane: it is
|
|
437
|
+
refused `needs-bulk` there, admitted by plan on interactive lanes, and runs
|
|
438
|
+
any plan on a dedicated sandbox lane under the budget.
|
|
439
|
+
|
|
440
|
+
## 16. Results (RB1, raw stream)
|
|
441
|
+
|
|
442
|
+
`ps/result_block.h` has the byte layout. Integers are exact 8-byte int64
|
|
443
|
+
(no float64 rounding above 2^53, §2); REAL, TEXT and BLOB cells are typed per
|
|
444
|
+
cell; an end record carries the status and the work counters. Raw-stream mode
|
|
445
|
+
emits `[u32le size][bytes]` per BLOB cell, so `SELECT _data …` streams the
|
|
446
|
+
stored frames verbatim. `cpp/test/ps/vectors/rb1_int64.hex` is the golden
|
|
447
|
+
vector for T6's Go decoder.
|
|
448
|
+
|
|
449
|
+
## 17. Index access and caches
|
|
450
|
+
|
|
451
|
+
- **L0 blocks** are read whole once (CRC-checked) and kept as a kind
|
|
452
|
+
directory; each kind's section (entries, offsets, bloom) is cached separately
|
|
453
|
+
and binary-searched. A statement resolves each snapshot's sections once per
|
|
454
|
+
kind.
|
|
455
|
+
- **L1 runs** open lazily: footer and TOC, the metadata CRC checked once per
|
|
456
|
+
lane, then a kind's fences on first use and its bloom while it fits.
|
|
457
|
+
- **Ascending scans prime runs lazily.** A run joins a posting merge with a
|
|
458
|
+
lower bound of its first entry (the scan's `lo`, or its first block's fence
|
|
459
|
+
prefix) and reads its first 4 KiB block only when it reaches the top of the
|
|
460
|
+
merge. The default order (`EPOCH_CID`, newest first) and gseq order are
|
|
461
|
+
ascending, so a LIMIT window reads blocks only from the runs it draws rows
|
|
462
|
+
from, not one block from every run of every partition. Descending scans
|
|
463
|
+
prime eagerly (a fence gives no upper bound).
|
|
464
|
+
- **The instance's shared cache** holds these immutable pieces (L0
|
|
465
|
+
directories and sections, fences, blooms) for every lane: 64 shards, each
|
|
466
|
+
only ever try-locked, so a busy shard is a miss and a lane never waits on
|
|
467
|
+
another lane (writers never touch it). A lane-private front map serves
|
|
468
|
+
repeated hits without any lock.
|
|
469
|
+
- Default budgets: 16 MiB shared per instance, 4 MiB private per lane,
|
|
470
|
+
4,096 host handles per lane (the host's handles are virtual; T5 bounds real
|
|
471
|
+
fds with its LRU).
|
|
472
|
+
|
|
473
|
+
## 18. Arrivals, offset paging, dead history (A14, A15)
|
|
474
|
+
|
|
475
|
+
- Arrivals rows merge-join two type postings by gseq: **REHOME** (the latest
|
|
476
|
+
promotion keeps the gseq, A14) and **GONE** (a gseq whose last live copy
|
|
477
|
+
died; new, posted by the type owner). No per-entry lookup.
|
|
478
|
+
- **Offset paging.** `ORDER BY _gseq|_rowid [DESC] LIMIT n OFFSET k` pushes the
|
|
479
|
+
OFFSET into the vtab. Live entries in positions [a, b) = (b − a) − GONE
|
|
480
|
+
gseqs in [g_a, g_b), counted from L1 fences (whole blocks by their counts,
|
|
481
|
+
the two edge blocks read) and the unmerged L0 sections; a binary search over
|
|
482
|
+
positions finds the k-th live entry in O(log n) probes.
|
|
483
|
+
- **Dead runs.** A GONE entry starts a gallop over the same counts to the next
|
|
484
|
+
live position; short runs fall back to per-entry checks.
|
|
485
|
+
- `flatsql_types` exposes MaxRowID (`gseq_hi`), TotalCount
|
|
486
|
+
(`first_live_count`) and the SnapshotID inputs (A16).
|
|
487
|
+
|
|
488
|
+
## 19. Writer-side changes made by T2
|
|
489
|
+
|
|
490
|
+
T2 owns the whole flatsql component; these changes to T1's writer serve the
|
|
491
|
+
readers and are covered by T1's suites as well as T2's.
|
|
492
|
+
|
|
493
|
+
| Change | Why |
|
|
494
|
+
|---|---|
|
|
495
|
+
| Index kind `EPOCH_CID` (19): complemented `floor(epoch_ms/1000)` + the A17 CID key → PUT pseq | A19's default order (seconds DESC, text CID ASC) as one ascending index scan, with copies of a CID adjacent for the merge. |
|
|
496
|
+
| Type kind `GONE` (0x204): a gseq whose last live copy died | Dead arrivals counted by fences: offset paging (T2 #7), dead-run skipping (T2 #8), merge-joined arrivals. |
|
|
497
|
+
| `REPEAT` (0x203) is a lookup kind (bloom) | The type-level FIRST test is a bloom probe for most rows; LABEL is read only for REPEAT copies. |
|
|
498
|
+
| SWAP seam: `Engine::swapSegment`, ctl `SWAP{seg, gen, oldCgen}`, manifest segment `cgen`, `c-<seg>-<gen>.{fsd,fsr,fsa}`; the owner reads compacted files, open replays SWAP | The smallest seam T3's compaction plugs into (§20 #5 uses a verbatim copier through it). |
|
|
499
|
+
| Catalog precedence at equal tcs: DEAD > PROMOTED > REPEAT > FIRST (writer `upsertCopy`, reader `catalog`/`labelOf`; REHOME candidates of one tcs resolved by liveness) | **T1 defect** found by the fan-out oracle: a batch that promoted a REPEAT and then killed it left two entries with one tcs; value order made the copy read as live, and a returning copy was labeled REPEAT instead of FIRST. |
|
|
500
|
+
| A cid's catalog is gathered, resolved per copy and loaded without its dead copies | **T1 defect** found by the A14 reader test: a cid killed and re-added over 1,000 times overflowed the per-batch copy table, dropped its live REPEAT, and died without an heir (63 of 64 promotions at round 1,027). |
|
|
501
|
+
| Writer threads tag themselves for the lock report | Lock-set disjointness instrumentation (T2 #1). |
|
|
502
|
+
| Test fixture `buildRecord` passes a NUL-terminated file identifier | `FinishSizePrefixed` takes a C string; the fixture passed the type's four `fid` bytes, and Debug builds (CI) asserted when the next byte was not zero (T2's type variants). |
|
|
503
|
+
|
|
504
|
+
## 20. Acceptance (§18 T2 as amended)
|
|
505
|
+
|
|
506
|
+
Machines: **Mac** = the owner's Mac Studio (Darwin 25.3.0, arm64, 28 hardware
|
|
507
|
+
threads), shared with other lanes; the 1-minute load average is recorded per
|
|
508
|
+
run (20–46 during these). **Mac mem** runs use the in-memory test host (no
|
|
509
|
+
fsync), **Mac APFS** the native host on APFS (`F_FULLFSYNC`). **Docker** =
|
|
510
|
+
Linux 6.12 arm64 in Docker Desktop on that Mac (a 16-vCPU VM shared with the
|
|
511
|
+
Mac's load), either `--cpuset-cpus 0-7` (8 pinned vCPUs, "Docker-8") or
|
|
512
|
+
`--cpus 8` (a CFS quota over 16 vCPUs, whose throttling periods show up as
|
|
513
|
+
100 ms+ tails; used only for correctness and disjointness); ext4 on the VM's
|
|
514
|
+
disk with `fdatasync`. None is a quiet Linux-8 box: latency bounds are enforced
|
|
515
|
+
by the tests only on one (a release build, 8+ usable hardware threads, load
|
|
516
|
+
below a quarter of them) and reported everywhere else. Numbers come from
|
|
517
|
+
`MEASURED` lines of `flatsql_ps_test` and `flatsql_ps_bench --mode=test` on
|
|
518
|
+
task-branch builds of 2026-09-28; rows 1, 4 and A28 are from the final build
|
|
519
|
+
(lazy run priming, §17).
|
|
520
|
+
|
|
521
|
+
| # | Acceptance | Result | Where |
|
|
522
|
+
|---|---|---|---|
|
|
523
|
+
| 1 | 8 writers saturate 256 partitions, 16 lanes, 10 min: `flatsql_partitions` p99 ≤ 1 ms; window LIMIT 1000 p99 ≤ 10 ms; reader and writer lock sets disjoint; max lane lock wait ≤ 1 ms | **Disjointness and lock wait pass everywhere; the latency bounds are not met on any box measured (all of them CPU-saturated by the test and by other work: needs a quiet Linux-8 box).** Every SQLite mutex and the shared index cache are acquired by lanes only (writer acquisitions 0 in every run); max lane lock wait 0.000–0.022 ms (the cache is only ever try-locked). The engine's own cost, writers stopped and one client (100 statements, 1–4 L0 blocks per partition after merges, 4.7–18 M records): Mac window p50 3.0–8.1 ms, p99 6.3–48 ms following the Mac's load, `flatsql_partitions` p99 0.95–1.3 ms; Docker-8 in-memory window p50 6.3 / p99 60 ms, ext4 p50 14.6 / p99 154 ms. Under saturation the lanes share the CPUs with 8 producer threads, the writers and their merges: Mac mem 40 s at load 46 (15.4 M records): `flatsql_partitions` p99 6.5 ms, window p50 14.5 / p99 91 ms, source window p99 77 ms, counters without a lane (A28) p50 0.42 / p99 2.0 ms. Docker-8 in-memory 30 s at VM load 12 (8.6 M records): `flatsql_partitions` p99 15.7 ms, window p50 75 / p99 259 ms. Docker-8 ext4 60 s at VM load 19 (8.1 M records): `flatsql_partitions` p99 24 ms, window p50 278 / p99 762 ms (an earlier build's run at VM load 18: p50 3.6 / p99 347 ms; host contention decides). Mac APFS 10 min at load 16 (earlier build, 14.8 M records, fsync-bound): 0 errors, window p50 70 / p99 370 ms. | `readers_under_saturating_writers_T2_1[_full]` (`--dir` for a real file system) |
|
|
524
|
+
| 2 | 10,000 randomized workloads (a CID across up to 8 producers, supersede, tombstones, epoch-less types) equal a brute-force reference: one row per CID, FIRST semantics, order; rows examined ≤ LIMIT × (partitions + 1). A14: readers during FIRST deaths never miss a live CID | **Pass.** Mac mem: 10,000 workloads (49,833 promotions, 10,064 type deletes, 22,043 supersedes, 13,119 RECONCILEs), 229 s; every Q1–Q5 check (default order, LIMIT prefix and its examined bound, CID lookups, gseq order, partition counts) against the Inspector's reference. A14: 10 min, 2,979 rounds of FIRST deaths, 190,656 promotions, 5,179 reader statements, 0 missing CIDs, 0 gseq changes. Two T1 defects were found on the way (§19). Docker (`--cpus 8`): 500 workloads pass. | `fanout_randomized_vs_bruteforce_T2_2_full`, `fanout_readers_during_first_deaths_A14_full` |
|
|
525
|
+
| 3 | −2^63, 2^53+1, 2^63−1 round-trip exactly through RB1 (C++ encoder; Go decoder vectors for T6) | **Pass.** Encoder/decoder in any byte split, the golden vector `vectors/rb1_int64.hex`, and parameters and literals through a lane. Docker: pass. | `int64_rb1_encoder_decoder_exact_T2_3`, `int64_exact_through_sql_lane_T2_3` |
|
|
526
|
+
| 4 | Bulk statement past its 128 MiB arena → `SQLITE_NOMEM`, interactive p99 within ±10%, nothing poisoned; unbounded plan on an interactive lane → `FLATSQL_NEEDS_BULK` in ≤ 1 ms with 0 rows examined | **NOMEM, no poisoning and NEEDS_BULK pass; the ±10% ratio holds on Docker-8 and is noise-bound on the Mac.** Every hog returns `kRsNoMem`; the same lane then answers, its arena passes a full consistency check and holds 160 KiB. NEEDS_BULK: lane time p50 0.012–0.016 / p99 0.051–0.099 / max 0.17 ms, 0 rows examined, over 200 unbounded shapes (Mac and Docker). Ratio (p99 with the hog / without, four alternating rounds): Docker-8 at VM load 2.0 and 3.5: 1.030 and 1.087 (rounds 0.95–1.09 and 1.02–1.43); Docker `--cpus 8`: 0.947; Mac control runs (no hog) 1.02–1.05, with the hog 0.94–2.3 at load 20–47 (one busy core streaming 300 MB beside the lanes; no shared lock, see #1). | `lane_isolation_bulk_nomem_T2_4`, `lane_needs_bulk_under_1ms_zero_rows_T2_4` |
|
|
527
|
+
| 5 | 1,000 compaction SWAPs under load: statements started before a SWAP complete with pre-SWAP rows. A12: 10-min bulk export under continuous compaction, 0 SNAPSHOT_GONE | **Pass.** Mac APFS: 1,000 SWAPs while ingest runs, 2,406 partition scans verified row by row against the frames sent, 987 of them spanning a SWAP (up to 28 SWAPs during one statement), 0 errors, every retired file set unlinked once announcements allowed. A12, Mac APFS 10 min: 2,769 type-level exports (84.5 M frames), 8,153 SWAPs, 8,090 retired sets unlinked, 0 SNAPSHOT_GONE. Negative control: unlinking without the announcement check gives SNAPSHOT_GONE. Docker (`--cpus 8`, in-memory): 1,000 SWAPs, 56 partition scans all spanning SWAPs (up to 257 during one), 0 errors; A12 10 min: 18,891 exports (296.5 M frames) across 300 SWAPs (the in-memory cap), 0 SNAPSHOT_GONE. | `snapshot_swaps_under_load_T2_5`, `snapshot_bulk_export_under_compaction_A12_full --dir=…`, `snapshot_unlink_without_announce_is_snapshot_gone` |
|
|
528
|
+
| 6 | Arrivals paging while 8 writers produce: gseq strictly increasing within and across pages; no gseq ≤ the snapshot head appears after the snapshot; union of pages = the FIRST-live set at the snapshot. A14: a CID's gseq never changes while a copy lives | **Pass.** Mac mem: 62 syncs (18,716 pages of 500) against 1.44 M records from 8 producers into 32 partitions; every sync equals the oracle (final arrivals ≤ its MaxRowID), 0 violations, 0 gseq changes. Docker: 46 syncs, 9,648 pages, 921,888 records, 0 violations. | `arrivals_paging_while_writers_produce_T2_6` |
|
|
529
|
+
| 7 | Page 10,000 of 100 rows on a 3 M-record type examines ≤ 2 × 100 rows plus fence reads | **Pass.** Mac mem, 3,000,000 records, 10% scattered deaths: OFFSET 999,900 in gseq order examined 100 rows and 132 arrival entries with 268 fence reads, 2.5 ms; pages equal a full scan's (60 random offsets, ascending and descending, 30% deaths). Docker: 100 rows, 150 entries, 196 fence reads, 7.3 ms. | `offset_paging_page_10000_T2_7_full`, `offset_paging_matches_reference_with_deaths` |
|
|
530
|
+
| 8 | A full sync of a 99%-dead history examines ≤ 2 × (live rows + fence reads) | **Pass.** 100,000-entry history, 1,000 live. Deaths clustered: 1,000 rows and 1,008–1,009 arrival entries examined, 724–1,052 fence reads. Deaths scattered (1 in 100 live): 1,000 rows and 2,001 entries examined, 57,388–67,740 fence reads (deviation 4). Docker: clustered 1,000 rows / 1,008 entries / 707 fence reads; scattered 1,000 / 2,001 / 57,492. | `dead_history_full_sync_T2_8` |
|
|
531
|
+
| A30 | A maximum-depth SQL expression on a lane | **Pass.** A depth-1000 expression (`SQLITE_MAX_EXPR_DEPTH`) runs; depth 1001 is refused; the lane serves on, canary intact. | `lane_max_depth_expression_A30` |
|
|
532
|
+
| A28 | 8 clients reading at 10 KB/s: counter p99 ≤ 10 ms, window p99 ≤ 50 ms. A sandbox cartesian join returns `timeout` while the bulk lane stays free | **Counter bound met on the Mac and Docker-8 in-memory; window bound met only at Mac load 20; the sandbox contract passes.** 8 slow clients park their lanes while others read. Mac mem 40 s: counter p99 1.1 / 4.1 / 2.4 ms and window p99 31.9 / 58 / 64 ms at load 20 (earlier build) / 31 / 46. Docker-8: counter p99 6.4 (in-memory) and 14.1 ms (ext4), window p99 148 and 224 ms at VM load 12 and 19. The join returns `timeout` from the work budget while a bulk statement completes in 1.5–2.3 ms; meta tables are not public; sandbox SQL is refused on the shared bulk lane. | `readers_under_saturating_writers_T2_1_full`, `lane_sandbox_contract_A28` |
|
|
533
|
+
|
|
534
|
+
A12's announcement protocol, A14's promotion reads, A15's page fill and
|
|
535
|
+
MaxRowID, A17's CID order in the default merge, A18's `_source` and bounded
|
|
536
|
+
`<TYPE>`, A21's cancellation polls, A28 and A30 are covered by the rows above
|
|
537
|
+
and `lane_test.cpp`.
|
|
538
|
+
|
|
539
|
+
## 21. Design deviations (T2)
|
|
540
|
+
|
|
541
|
+
Each entry: what the design says, what was built, why, and the evidence.
|
|
542
|
+
|
|
543
|
+
1. **Fan-out sub-cursors run inline; no idle-lane borrowing** (§5.3: "borrows
|
|
544
|
+
idle lanes … as sub-cursor producers … when no lane is idle, the
|
|
545
|
+
sub-cursors run inline"). Only the inline form is built. A partition
|
|
546
|
+
sub-cursor costs its sections' binary searches and one block per L1 run;
|
|
547
|
+
the measured window cost (§20 #1) is dominated by per-row label, liveness
|
|
548
|
+
and row reads that a producer lane would not remove. Borrowing is an
|
|
549
|
+
optimization T6 can add behind the same `RowSource` interface.
|
|
550
|
+
2. **Type-level FIRST test per row, not a REPEAT merge-join** (§8.6). Index
|
|
551
|
+
order is not pseq order, so a merge-join with the REPEAT run applies only to
|
|
552
|
+
pseq scans. Rows test the REPEAT bloom (REPEAT became a lookup kind); only
|
|
553
|
+
rows with a REPEAT posting read their latest LABEL. Arrivals order does
|
|
554
|
+
merge-join (GONE, REHOME).
|
|
555
|
+
3. **Offset paging by GONE counts in arrivals order, not per-block live-FIRST
|
|
556
|
+
counts in L1 fences** (§9, A17). T1's merges write each block's entry count
|
|
557
|
+
as its live count, and liveness changes after a merge; a new type kind,
|
|
558
|
+
GONE, is counted by fences instead (§18). Offset paging is pushed down for
|
|
559
|
+
`_gseq`/`_rowid` order (the data explorer's order); the default and CID
|
|
560
|
+
orders page through SQLite (O(offset)).
|
|
561
|
+
4. **A15 #8 without arrivals compaction.** Arrivals compaction (dropping dead
|
|
562
|
+
entries) is T3's. Dead runs are skipped by galloping over GONE fence counts;
|
|
563
|
+
the bound holds for rows and for arrival entries examined, but a history
|
|
564
|
+
whose deaths are scattered one-in-a-hundred costs about 60 fence probes per
|
|
565
|
+
live row (§20 #8) until T3 compacts it.
|
|
566
|
+
5. **RB1 cells are typed per cell** (§9: a type vector per block). SQLite
|
|
567
|
+
values are dynamically typed per row.
|
|
568
|
+
6. **One result ring per request slot** (256 KiB default), parked per
|
|
569
|
+
statement, rather than one 1 MiB ring per lane (A28 parking is per
|
|
570
|
+
statement either way).
|
|
571
|
+
7. **Sandbox work budget adds VM steps** to rows examined and bytes read, so a
|
|
572
|
+
statement that reads nothing (a recursive CTE) is bounded too; the A18
|
|
573
|
+
window counts the newest N arrivals entries (dead included), not N
|
|
574
|
+
FIRST-live records (one entry read instead of a count).
|
|
575
|
+
8. **`<TYPE>@<source>` matches live tag instances of the FIRST copy's PUT** in
|
|
576
|
+
its partition. An untagged record never matches a source (the legacy
|
|
577
|
+
`_source` falls back to the producer token for untagged rows); T6's
|
|
578
|
+
equivalence run on a migrated copy decides whether that case exists.
|
|
579
|
+
9. **Type-level visibility is labeled rows only**; the A20 read-your-writes
|
|
580
|
+
scan of the unlabeled gap belongs to `GetRecord` (T6), which can use the
|
|
581
|
+
partition vtab (partition-level visibility is the acked HWM).
|
|
582
|
+
10. **Interactive lanes at nice −5 on ≤ 2 vCPU** (A28) needs privileges the
|
|
583
|
+
engine does not have natively; bulk lanes do take nice +10 on Linux. The
|
|
584
|
+
host (T5/T6) sets both.
|
|
585
|
+
11. **Shared index cache, try-locked.** The design budgets a 16 MiB block cache
|
|
586
|
+
per reader instance; the built cache holds parsed index pieces (L0 sections,
|
|
587
|
+
fences, blooms) for all lanes of an instance and is only ever try-locked, so
|
|
588
|
+
lanes never wait on each other. Per-lane budgets (4 MiB) hold manifests and
|
|
589
|
+
run views; 4,096 host handles per lane (virtual handles; T5 bounds real fds).
|
|
590
|
+
12. **SWAP seam built in the writer** (T3's scope). `Engine::swapSegment` copies
|
|
591
|
+
a fully merged sealed segment verbatim to `c-<seg>-<gen>.*`, commits SWAP
|
|
592
|
+
with a new manifest, and reports the retired files; it has no
|
|
593
|
+
INTENT_COMPACT (T3 adds it with crash handling). Rows stay dense by pseq.
|
|
594
|
+
13. **Lock disjointness natively covers SQLite's mutexes and the shared
|
|
595
|
+
cache**; the host handle table is measured under WasmEdge (A29, T5/T6).
|
|
596
|
+
14. **CounterReader reads heads** (O(partitions) preads), the A28 path without
|
|
597
|
+
a lane; the Go snapshot refreshed on every acked commit is T6's.
|
|
598
|
+
|
|
599
|
+
## 22. Running the T2 tests; what needs a Linux-8 box
|
|
600
|
+
|
|
601
|
+
```
|
|
602
|
+
cmake --build cpp/build -j 8 --target flatsql_ps_test flatsql_ps_bench
|
|
603
|
+
cpp/build/flatsql_ps_test --test=lane_ # lanes, parking, sandbox, isolation, A30, C ABI
|
|
604
|
+
cpp/build/flatsql_ps_test --test=fanout_randomized_vs_bruteforce_T2_2_full # 10,000 workloads
|
|
605
|
+
cpp/build/flatsql_ps_test --test=fanout_readers_during_first_deaths_A14_full # 10 min
|
|
606
|
+
cpp/build/flatsql_ps_test --test=snapshot_ [--dir=<real fs dir>]
|
|
607
|
+
cpp/build/flatsql_ps_test --test=snapshot_bulk_export_under_compaction_A12_full --dir=<dir> # 10 min
|
|
608
|
+
cpp/build/flatsql_ps_test --test=offset_paging_page_10000_T2_7_full # 3 M records
|
|
609
|
+
cpp/build/flatsql_ps_bench --mode=test --test=readers_under_saturating_writers_T2_1_full [--dir=<dir>]
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
The default suite runs shortened forms of every slow test. Long runs that
|
|
613
|
+
write a lot use `--dir` (the in-memory test host keeps every file it held).
|
|
614
|
+
|
|
615
|
+
**T2 #1 and #4 on a quiet Linux-8 box** (8 cores, ext4, nothing else
|
|
616
|
+
running), from a checkout at the landed commit:
|
|
617
|
+
|
|
618
|
+
```
|
|
619
|
+
cpp/build/flatsql_ps_bench --mode=test --test=readers_under_saturating_writers_T2_1_full --dir=<ext4 dir>
|
|
620
|
+
cpp/build/flatsql_ps_test --test=lane_isolation_bulk_nomem_T2_4
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
Both enforce their bounds on such a box (a release build, 8+ usable hardware
|
|
624
|
+
threads, 1-minute load below a quarter of them; the load is sampled at the end
|
|
625
|
+
of the run, so the test's own writers count). The disjointness claim itself is
|
|
626
|
+
measured under WasmEdge with the C host module in T5/T6 (A29).
|
|
627
|
+
|
|
628
|
+
What to expect there: the window's own cost is 3–8 ms at p50 (§20 #1), so the
|
|
629
|
+
10 ms p99 bound leaves little room for waiting on a CPU. The test runs 8
|
|
630
|
+
producer threads, the writers and their merges and 16 lanes on 8 cores; when
|
|
631
|
+
they saturate the CPUs, the window p99 is decided by the scheduler, not by the
|
|
632
|
+
engine. If the bound fails there, the next steps are the design's own levers:
|
|
633
|
+
fewer writer threads than cores (design §5.1: `clamp(cores−2, 1, 16)`), interactive
|
|
634
|
+
lanes at a higher priority than writers (design A28 gives nice −5 only on ≤ 2 vCPU),
|
|
635
|
+
and idle-lane sub-cursors (§21 deviation 1).
|