Repository navigation
fix(runtime,cli): refuse an uncreatable SQLite database directory at once instead of hanging in recursive mkdir - #23851
Conversation
…once instead of looping in recursive mkdir createStandaloneStack's sqlite and sqlite-wasm arms and os dev's unified-default prep called Node's recursive mkdirSync, which never returns when a level answers ENOENT under an existing parent (/proc/<missing>/...). The host now runs the bounded walk driver-sql's connect runs and rejects at once, naming the database file, the directory and the OS error code. Claude-Session: https://claude.ai/code/session_01PFB9ZGRWYvjhTXsd8i6KBp Co-authored-by: Claude <noreply@anthropic.com>
…atabase directory The /proc shape runs in a node --import tsx child bounded by spawnSync's timeout with SIGKILL, so a reintroduced synchronous recursive-mkdir loop fails at the bound instead of hanging the worker. Controls: a creatable nested path is made in both SQLite arms; the read-only boot still makes no directory; os dev leaves every non-default source to its serve child. Claude-Session: https://claude.ai/code/session_01PFB9ZGRWYvjhTXsd8i6KBp Co-authored-by: Claude <noreply@anthropic.com>
…rectory refusal Claude-Session: https://claude.ai/code/session_01PFB9ZGRWYvjhTXsd8i6KBp Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 44 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin baa489a8e034f845c6165b0bc8306cc817ee3f16 && git checkout baa489a8e034f845c6165b0bc8306cc817ee3f16
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 66d456d01663b32014c9ea562f4ba3054ea7024c 70777e1be27906fd8dc7732823b149347b93eac8 && git checkout -B drift-repro 66d456d01663b32014c9ea562f4ba3054ea7024c && git merge --no-ff 70777e1be27906fd8dc7732823b149347b93eac8
node scripts/docs-audit/affected-docs.mjs --json 66d456d01663b32014c9ea562f4ba3054ea7024c
|
✅ ACCEPT: PR #23851 at
|
Fixes #23737
Clause-②: no
What was wrong
createStandaloneStackprepared the directory of a file-backed SQLitedefaultwith Node's recursivemkdirSync, in both thesqliteand thesqlite-wasmarm. Node's recursive mkdir (v22.22.0, sync and async alike) never returns when a level answersENOENTalthough its parent exists. ForOS_DATABASE_URL=file:/proc/os-23737/ctl.dbit loopsmkdir("/proc/os-23737") = ENOENT,mkdir("/proc") = EEXIST, retry, holding the event loop. Soos startprinted→ Starting server...and hung: no plugin timeout could fire, and the SQL driver's bounded refusal from PR #23730 was never reached.os devhad the same shape in its own process (dev.ts, the unified-default directory prep).What changed
packages/runtime/src/standalone-stack.ts: both arms nowawait makeSqliteDatabaseDirectory(filename). This is a module-private copy of the bounded walkdriver-sql's connect runs (makeDatabaseDirectory):mkdir;EEXISTon a created level (a concurrent creator);SqliteDatabaseDirectoryErrorcarryingcode,filename,directoryandcause.The message names the database file, the directory and the OS error, never the URL. The read-only boot's skip (
sqliteAbsentFile: 'empty-in-memory'over an absent file) is unchanged. The directory prep stays on the host, as ADR-0062's ADR-0062 D1 收尾:defaultdriver 的 connect 与失败判决仍是第二份实现(阻塞点已定位) #3826 resolution decides ("URL→config translation andmkdirstay host concerns"). The card's option of dropping it in favour of the driver's connect is not taken.packages/cli/src/commands/dev.ts: theunified-defaultprep goes through a new module-levelprepareDevDatabaseDirectory(resolvedDb), which runs the same walk.os devprints the refusal plus a one-line remedy (pointOS_HOMEat a directory that can be created, or pass--database) and exits 1.No export is added to any package entry (
@objectstack/runtime's index names its exports explicitly;@objectstack/cli's exports onlyDevCommand). Nopackages/specedit..changeset/23737-uncreatable-db-dir-refusal.md:@objectstack/runtimepatch,@objectstack/clipatch.Measured at the public door
Built CLI, this container, Node v22.22.0. Each run is in its own process group with a hard outer kill.
origin/main43567a832f287c4e, the fix; same source as head)OS_DATABASE_URL=file:/proc/os-23737/ctl.db os start(no config)→ Starting server...✗ [StandaloneStack] SQLite database file '/proc/os-23737/ctl.db' cannot be created: its directory '/proc/os-23737' could not be made: ENOENT: no such file or directory, mkdir '/proc/os-23737'wasm-sqlite:///proc/os-23737/ctl.dbcreateStandaloneStackalone looped (14,537 failingmkdirin 8 s)OS_DATABASE_URL=file:/proc/os-23737/ctl.db os dev(examples/app-todo)mkdircalls in about 25 sOS_HOME=/proc/os-23737 os dev(examples/app-todo, unified default)os devprocess itself (64,440 failing calls in 25 s; theEnvironment IDrow after the prep never printed)✗ The default database '/proc/os-23737/data/objectstack.db' cannot be created: its directory '/proc/os-23737/data' could not be made: ENOENT: ...plus the remedy lineOS_DATABASE_URL=file:SCRATCH/ctl-db/a/b/c/ctl.db os start✓ Server is ready;a/b/c/made,ctl.dbwrittencreateStandaloneStack({ databaseUrl: 'file:/proc/os-23737/ctl.db', sqliteAbsentFile: 'empty-in-memory' })(built runtime)createStandaloneStackalone, built,file:/proc/os-23737/ctl.db, onorigin/main: 19,640 failingmkdir("/proc/os-23737")calls in 10 s understrace, killed by the bound.Pins
packages/runtime/src/standalone-stack-uncreatable-db-directory.test.ts(localproject):/procshape in both arms runscreateStandaloneStackfrom source in anode --import tsxchild, bounded byspawnSync's timeout (30 s, SIGKILL). The loop was synchronous, so an in-process case could not be bounded. It asserts the child settled, the code isENOENT, and the message names the file and the directory. Runs where/proc/selfexists.ENOTDIR(a path through a regular file) andEEXIST(a file holding the directory's name), in-process.packages/cli/src/commands/dev-database-directory.test.ts(unittier by the predicate):dev.tscosts about 7 s of the child's wall. The vitest worker never imports it./procgivesENOENTnaming the file and the directory; a path through a file givesENOTDIR; CONTROL, a creatable unified-default path is made; CONTROL, any other source is left to the serve child (nothing made).Negative control (ablation)
Both legs were run on the committed fix through
scripts/ablation-replace.mjs, inside a trap-guarded script, under the verify lock. The subject resolves to source in both runners: a relative import in vitest, and a.tsfile URL in the tsx child. So there is nodist/leg.await makeSqliteDatabaseDirectory(filename);(anchor x2 → x0) became(await import('node:fs')).mkdirSync(resolvePath(filename, '..'), { recursive: true });(x0 → x2), which is the original call.ca7e1c0c79a4→76537e4c8f4a./proccase reportedcreateStandaloneStack had not settled after 30057 ms (child killed by SIGKILL). The ENOTDIR case failed withexpected 'ENOTDIR: not a directory, mkdir ...' to contain '...plain/sub/ctl.db'. Both CONTROLs stayed green.ca7e1c0c79a4,git diff HEADempty.prepareDevDatabaseDirectory,const missing: string[] = [];(x1 → x0) becamefs.mkdirSync(directory, { recursive: true }); return;(x0 → x1).c31d6cd08223→89ecc6801be3.beforeAllthrewprepareDevDatabaseDirectory had not settled after 45080 ms (child killed by SIGKILL)and its 4 cases were skipped.c31d6cd08223,git diff HEADempty.Tests and gates
Local readings are on head
70777e1be2.pnpm --filter @objectstack/runtime exec vitest run --project local: 400 files passed, 5,983 tests passed, 19 skipped, exit 0.pnpm --filter @objectstack/cli exec vitest run --project unit: 298 files passed, 4,438 tests passed, exit 0.pnpm --filter @objectstack/runtime --filter @objectstack/cli run typecheck: exit 0.check:test-typecheckis OK for both, with no new test-layer debt.pnpm exec eslint --no-inline-config --max-warnings=0over the four changed.tsfiles: exit 0.node scripts/pm/dispatch-gates.mjs --commandsderived 65 commands.--ranreconciliation: 63 run with exit 0, 2 NOT MEASURED (exit 3, PREREQUISITE NOT MET), 0 unrun.check:dual-build-cjs-loads: needs every package'sdist/; nine packages outside this card's build closure are unbuilt here.check:i18n-coverage:examples/app-showcaseimports@objectstack/connector-slack, which has no build here.Lint & Repo Gates.check-plugin-teardown-shape.mjs --self-testneeded a pinned commit that this shallow clone lacked; it was fetched and the gate exits 0.check:type-check-debthad queued behind this card's own test run on the verify lock; re-run under the lock, it exits 0.packages/cli'sintegrationtier is left to CI: no integration-tier file or spawn entry is touched. Runtime'stest:repoproject is left to CI too.Acceptance notes
driver-sql'smakeDatabaseDirectory, the runtime'smakeSqliteDatabaseDirectory, and the CLI'sprepareDevDatabaseDirectory. None of them is on a package entry, and this card is fenced from adding an export. Consolidating them, for example as one exported helper in a shared package, would widen a published surface and is a separate decision.os devawaits, not the call inDevCommand.run(). The call site is held by the public-door measurement above.os startrefusal arrives after both processes have loaded (about 9 s wall here). The walk itself answers in under 10 ms.OS_HOME=/proc/os-23737-home os start(no config) hangs in theos startprocess itself: 176,004 failingmkdircalls in 20 s, atpackages/cli/src/commands/start.ts'sfs.mkdirSync(path.join(homeDir, 'data'), { recursive: true }).mkdirSynccalls onOS_HOME-derived paths sit on the same boot path, read but not measured:start.ts(the home dir),default-host.ts(the empty-boot stub artifact),package-state-store.ts,artifact-reference.tsandserve.ts.standalone-stack.ts(theskipSchemaSyncfield and its pass-through) are not touched by these hunks.Generated by Claude Code