Repository navigation
Commit c7dd48a
fix(driver-sqlite-wasm): handle a failing background flush instead of rejecting unhandled (#23643)
Fixes #23635
Clause-②: no
A flush that fails in the background in `driver-sqlite-wasm` is now
handled at the producer: reported once, at `error`, with the database
file named, and the file stays dirty so a later flush retries. It no
longer reaches the process as an unhandled rejection. An awaited
`flush()` / `close()` still throws to its caller.
## What changed
`packages/drivers/driver-sqlite-wasm/src/wasm-connection.ts`, one file:
- The three fire-and-forget flushes (`markDirty` under `on-write`, the
`debounced:N` timer, and the flush deferred past a transaction's close
in `noteTransactionControl`) now call one private helper,
`flushInBackground()`, in place of three bare `void this.flush()`.
- On rejection the helper calls `reportBackgroundFlushFailure()`: one
line at `error` (at `warn` for a logger that has no `error`) naming the
file, the consequence (writes since the last good flush are not on disk
while the database keeps answering from memory and looks healthy) and
the fix. It reports once per failure episode: a latch is set by the
first report and cleared when a flush actually writes, so `on-write`
does not log once per write (AGENTS.md, "Degradation log levels": say it
once, at the first degradation). The reporter never throws, because a
throwing logger inside a rejection handler would recreate the unhandled
rejection.
- `flush()` is unchanged except that it clears the latch after a landed
write. It still re-marks `dirty` and rethrows, so the next flush retries
and an awaited caller still gets the error. `close()` is untouched.
- The exported type surface is unchanged: `error` is read off the
injected logger when it has one (`console` does). Declaring it on the
exported `logger` option type would be a Clause-② widening and a
`minor`; see Acceptance notes.
One patch changeset:
`.changeset/23635-sqlite-wasm-background-flush-failure.md`.
## The pin, and how it asserts "no unhandled rejection"
`src/sqlite-wasm-background-flush-failure.test.ts`, 10 tests. The
assertion is direct: every test attaches its own
`process.on('unhandledRejection')` listener and requires that it saw
nothing once the flush chain has settled. It awaits the chain tail and
then 20 ms of event-loop turn, so no fixed sleep races the flush, and it
does not lean on Vitest's run-level unhandled-error report.
- One test per call site (on-write, debounce timer, transaction close).
Each makes the flush fail by removing the database's directory, as the
dogfood teardown did, and asserts: no unhandled rejection, exactly one
`error` line naming the file, `dirty` still set.
- Retry: once the directory is back, the next write's flush lands, and a
second connection reads back every row written during the failure.
- Once per episode: three failed writes give one line; after a flush
lands, a new failure reports again.
- Report robustness: a `warn`-only logger still hears it, at `warn`; a
logger that throws produces no unhandled rejection.
- CONTROL: an awaited `flush()` rejects with `ENOENT` and does not also
log, `dirty` stays set and a later `flush()` resolves; `close()` rejects
with `ENOENT`.
- Through the driver: the dogfood race itself (a live `SqliteWasmDriver`
on `on-write`, its directory removed, the next insert) is reported, and
the public `driver.flush()` still rejects.
### Ablation
Each leg mutated `wasm-connection.ts` through
`scripts/ablation-replace.mjs` (anchor hit once, blob changed on disk,
restore proven: blob equal to HEAD and `git diff HEAD` empty). The test
imports `src` directly, so no `dist/` rebuild leg applies. The first
attempt of the once-guard leg was refused by the tool as a no-op (its
replacement was a substring of its anchor) and redone with a distinct
replacement.
| Mutation | Result |
|---|---|
| on-write site back to bare `void this.flush()` | the on-write test and
the 5 tests that drive on-write fail (6 of 10); debounce and transaction
tests stay green |
| debounce site back to bare | exactly the debounce test fails (1 of 10)
|
| transaction-close site back to bare | exactly the transaction test
fails (1 of 10) |
| helper body back to bare `void this.flush()` (all three sites) | 8 of
10 fail; the two CONTROL tests stay green |
| once-guard removed | the once-per-episode test fails (1 of 10) |
| latch reset after a landed write removed | the once-per-episode test
fails (1 of 10) |
| logger try/catch removed | the throwing-logger test fails (1 of 10) |
| `warn` fallback removed | the `warn`-only test fails (1 of 10) |
| `this.dirty = true` on failure removed (existing line) | 8 of 10 fail,
including the retry test |
On the unmutated tree the file passes 10 of 10.
## Verification
At `aa42889b36`. The patch round changed one thing: the private `logger`
field's type is now one literal, with `warn` required and `error`
optional. Every run below was repeated at this head.
- `pnpm check:optional-error-sink` (a wide-population family that
`--commands` does not print) was red at `e3b5224c85`, on the old
intersection type. It exits 0 at this head: 37 sinks declare an optional
`error` beside a required `warn`, and 0 are `no-fallback`. Both blocks
`dispatch-gates.mjs` prints on every run were also run, each with its
own command: the artifact-roster block, 53 of 53 exit 0, and the
wide-population block, 11 of 11 exit 0.
- `pnpm --filter @objectstack/driver-sqlite-wasm typecheck`: exit 0.
Whole package suite: 37 files, 685 tests, all pass.
- `node scripts/pm/dispatch-gates.mjs --commands` derived 62 commands
from this tree; all 62 ran at this commit and exited 0, and `--ran`
reconciles 62 derived, 62 run, 0 NOT-MEASURED. One of them,
`check:logger-receiver-detach`, was red on my first spelling (a logger
channel read off `this.logger` into a local) and is fixed in the last
commit. `pnpm check:durability-log-level` is also green.
- eslint with `--no-inline-config` on the two changed source files,
`--format json`: 2 files, 0 errors, 0 warnings. The repo config never
enables type-aware linting (no `parserOptions.project`, no typed rules;
see the note near line 328 of `eslint.config.mjs`), so this diff cannot
move the verdict of an untouched file. The repo-wide `pnpm lint` is left
to CI.
- NOT MEASURED: the Dogfood Regression Gate (2/3). It is CI-owned and
the race is intermittent, so a local run could not prove it; the pin
reproduces the mechanism deterministically instead.
## Measured, out of scope and not fixed here: where the refused boot's
driver is left open
The seat filed this as #23644.
A boot that fails after a plugin's `start()` connected a driver never
tears that driver down.
- Connect: `ObjectQLPlugin.start` (`packages/objectql/src/plugin.ts`,
`await this.ql?.init()`) reaches `driver.connect()` in
`ObjectQLEngine.init` (`packages/objectql/src/engine.ts`, the `for
(const [name, driver] of this.drivers)` loop).
- Failure: the ownership guard throws at `kernel:ready`, Phase 3 of
`ObjectKernel.bootstrap()` (`packages/core/src/kernel.ts`, `await
this.context.trigger('kernel:ready')`).
- Why nothing closes it: `rollbackStartedPlugins()` is called only
inside the Phase 2 branch for a plugin whose `start()` failed. The outer
`catch (error)` of `bootstrap()` sets the state to `stopped`, releases
the signal listeners and rethrows; the comment there states that
`shutdown()` on such a kernel is a no-op. So no plugin `destroy()` runs,
including `DefaultDatasourcePlugin.destroy`
(`packages/runtime/src/default-datasource-plugin.ts`), the path that
disconnects the default datasource. The two in-process callers,
`Runtime.start()` (`packages/runtime/src/runtime.ts`) and `bootStack`
(`packages/verify/src/harness.ts`, `await kernel.bootstrap()`), add no
teardown of their own.
- Probe (core `dist`, scratch script outside the repo): plugin A's
`start()` records a connect and its `destroy()` records a disconnect;
plugin B hooks `kernel:ready` and throws. Result: `["driver.connect()",
"bootstrap rejected: refused at kernel:ready", "shutdown() returned"]`,
with no disconnect. Positive control, same kernel but plugin B's
`start()` throws (Phase 2 rollback): `["driver.connect()",
"driver.disconnect() via destroy()", "bootstrap rejected: ..."]`, so the
probe can see a destroy when one happens.
## Acceptance notes
- Typing `error` on the exported `WasmConnectionOptions['logger']` would
let a typed caller pass a logger with `error` without a cast, and it is
the declared home of this behaviour. It widens a published surface
(Clause-② `yes (widening)`, `minor`), which the card's claim declared
`no`, so it is not done here. The seat can take that as its own card.
- `SqliteWasmDriver.connect()` registers a `beforeExit` handler
(`packages/drivers/driver-sqlite-wasm/src/sqlite-wasm-driver.ts`) that
calls `void this.flush().catch(() => { /* ignore */ })`, so a flush that
fails at process exit is swallowed with no log. Read from source, not
run. It sits outside this card's file surface and is not filed.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 7cb33cf commit c7dd48a
3 files changed
Lines changed: 378 additions & 5 deletions
File tree
- .changeset
- packages/drivers/driver-sqlite-wasm/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 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 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 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 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
0 commit comments