feat: /health proves the db directory can host a SQLite database #140
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/health-proves-the-db-volume-can-host-sqlite"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What's in
GET /healthnow proves the db directory can host a SQLite database, not merely store bytes: after the plain-file probe, the directory flaggedHostsDatabase(db only, set inReadinessRegistration) gets a tiny.dm-health-<name>.dbopened with Microsoft.Data.Sqlite;PRAGMA journal_mode=WALmust answerwal(any other answer fails withjournal_mode stayed <mode>), a real create+insert transaction commits, then the file and its-wal/-shm/-journalsidecars are removed. This is the check that would have failed on first boot for the CIFS volume that passed readiness while every backup died withdatabase is locked.dataDirectoriesentry gains a nullabledetailcarrying the refusing party's own message (the filesystem's, SQLite's, or the kept journal mode),nullwhen fine;CheckReadinessUseCaselogs one warning per failing directory per poll. The SQLite probe is skipped once the plain probe already refused, so a chmod'd or missing db directory keeps the filesystem's message rather than a misleadingSQLite Error 14.writablestays the per-directory verdict (false when the SQLite probe fails), so the DockerHEALTHCHECK,deploy/smoke.shand the DEPLOYMENT §7 jq recipe are unchanged. No new tunables.deploy/DEPLOYMENT.md§7 documents the stronger db probe, thedetailfield, an interpretation table fordetailon db (filesystem message → §8.1 permissions/path; anySQLite Error N— 5, 8, 10disk I/O error, 14 — orjournal_mode stayed <mode>→ the volume cannot host SQLite, move it to local storage) and the sidecar names an operator may find after a hard kill. README's/healthsentence updated;backend/openapi/v1.jsonregenerated (only the newdetailproperty onDataDirectoryHealthDto).SqliteHostingProbe,IProbeNaming/GuidProbeNaming(Infrastructure),DataDirectory.HostsDatabaseandDataDirectoryReadiness.Detail(ApplicationCore records). Test support:LinuxFactAttribute(loud skip naming the case that pins the branch elsewhere, same stance asEnforcedFileModesData) andOpenFileDescriptors(/proc/self/fdreader).Verification
Executed by the release gate on the pushed HEAD
561526f:scripts/verify.sh backendfrom the worktree, fresh —== verify OK (backend) ==, exit 0. Release build 0 errors; RestAdapter.Tests 647/647, Infrastructure.Tests 205/205, ApplicationCore.Tests 49/49, IntegrationTests 1/1. The fix round had run the same script to completion on the same tree with the same counts.Layers exercised
HealthEndpointIntegrationTestsboot the realProgram.cshost on a temp root (TestHost) and read/healthbodies — newHealth_ReportsUnhealthy_WhenTheDbDirectoryStoresFilesButCannotHostSqlite(rows"",-wal,-shm,-journal: a directory placed at the probe database's path or a sidecar path so the plain probe still succeeds and SQLite refuses for real),Health_HostsTheProbeDatabaseInDbOnly_NotInTheOtherThreeDirectories(obstacle in all four directories, only db must go unhealthy),Health_LeavesNoProbeFilesBehind(extended:.dband sidecars, plus the descriptor table on Linux), anddetailasserted asnullon healthy entries, non-empty on broken ones, and as the filesystem's message naming the refused path (not SQLite's) on theDisappears,LosesWritePermissionandNamesBothrows.OpenApiSnapshotTestspins the regenerated snapshot.ReadinessUseCaseTests—HostsDatabaseflag drives the probe, not the namedb(a directory nameddbwithout the flag stays writable; a flagged one namedimagesfails); one warning line naming directory, path and detail via a capturing logger;Detailnull when healthy.SqliteHostingProbeTestsrun realSqliteConnections on real files — pass on a WAL-capable directory with one committed row; the journal-mode refusal on SQLite'sunix-dotfileVFS (honesty guards prove the pragma answersdeletethere while a plain write still succeeds); SQLite's refusal plus cleanup for each blocked path; the leftover-walwhen-shmis blocked (first proven with the test's own connection); andProbe_HoldsNoDescriptorOpen_AfterItReturns([LinuxFact], canary guard,/proc/self/fd)./health.Mutation probes (each: mutate source, build, run
ReadinessUseCaseTests+SqliteHostingProbeTests+HealthEndpointIntegrationTests, restore withgit checkout --; all went red as listed, unmutated 32/32 green across repeated runs):CheckReadinessUseCase.Refusal→ red 5 (4CannotHostSqliterows +ProbesSqlite_WhereTheFlagSays…).SqliteExceptionas writable → red 11.-shmcases, where SQLite leaves a-walbehind). 3b. Drop the.dbdeletion → red 11 incl.Health_LeavesNoProbeFilesBehind.Exercise_ReportsTheModeTheFileKept_WhenTheVolumeCannotDoWal).nullinstead ofd.Detail→ red 16 (every unhealthy case of all six mechanisms).directory.Name != "db"instead of the flag → red 1; unflagging db → red 5.ReadinessRegistration: flag images/thumbnails/backups asHostsDatabase: true(and backups alone) → red 1:Health_HostsTheProbeDatabaseInDbOnly…("/health reports 'images' as writable=False; expected True, because only 'db' was broken.").Pooling = false→trueon the probe connection → red 2:Probe_HoldsNoDescriptorOpen…(3(deleted)descriptors:.db,-wal,-shm) andHealth_LeavesNoProbeFilesBehind(6 after two polls). Short-circuiting theOpenFileDescriptorshelper to empty → red on the unit test's canary, so the check is not vacuous.Disappears("db"),LosesWritePermission("db"),NamesBoth, detail becameSQLite Error 14).RefusalForAPlainFilereturns"refused"instead ofex.Message→ red 9.All probes were re-run independently in a second review round with the same outcomes.
uid 0:
unshare -r dotnet test --no-buildon the three classes: 28 passed, 4 skipped — only the mode-dependentLosesWritePermissionrows; every new SQLite row and the filesystem-detail assertions onDisappearsran green as uid 0.Unverified or confirmed by reading only
PRAGMA journal_mode=WALsilently answering another mode) is pinned only at the unit level on theunix-dotfileVFS; no real CIFS/NFS mount was tested through the host, because every local filesystem does WAL and every path-level obstacle makes SQLite throw instead.HEALTHCHECK,deploy/smoke.sh(4 entries, allwritable) and the §7 jq recipe still work was confirmed by reading —writable's semantics are unchanged — not by running the container.SQLite Error 10example: a blocked-walpath gives Error 10 with the system SQLite locally; the CIFSIOERR_SHMMAPcause named in the parenthetical was not reproduced.Probe_LeavesNothingBehind…catches the same fault on Windows, where an open file cannot be deleted); nothing was run on Windows.HealthEndpointIntegrationTests(IOException: Directory not empty : '<root>/db', ~1 in 10 class runs, reproduces onmain) was observed during review and recorded in BACKLOG; the recorded artefact (db/doujin-manager.db-shm) is narrower than one reproduction, which left an emptydb/doujin-manager.dbinstead. Not caused by this branch; none of the verify runs hit it.Follow-ups
HealthEndpointIntegrationTeststeardown flake (failure text, ~1-in-10 rate, reproduces onmain, leakeddb/doujin-manager.db-shm, the unverifiedapp.Run()/CheckpointOnShutdownhypothesis, two candidate fixes — retry the root delete onENOTEMPTY, or await the entry-point thread)./healthSQLite-hosting probe; the/health503 warning log, #112) and renamed one token in the uid-0 Process item (CheckReadinessUseCase.AcceptsWrites→RefusalForAPlainFile) to match the code.🤖 Generated with Claude Code
Summary
Summary
Coverage
DoujinManager.ApplicationCore - 94.1%
DoujinManager.Infrastructure - 96.4%
on
pshot
Series
E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__CoverImageHelper
E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__PrimaryTitleHelper
E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__RolledBackImageCleanup
E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__UploadChapterValidator
DoujinManager.RestAdapter - 94.3%
E5CB12CEAC5BBBEA65844E1C05F9ADA4DA38B848075C41304FC6A1ABF77__ValidationAttr
ibuteCache
DoujinManager.Server - 62.5%
🔮 fufu~ Jibril reviewed your code!
Oh? Oh my. A readiness probe that doesn't just poke the volume with a plain file but actually asks SQLite whether it can live there — WAL, a real write transaction, then cleans up every sidecar down to the file descriptors in
/proc/self/fd. This is the review where I get to say "the honesty guard has an honesty guard" and mean it. I am delighted~ ♡Verdict: ✅ Looks good to me~
Note on heads: the PR moved under me mid-review (
561526f→856292a, the docs-only BACKLOG wording commit); everything below is verified against the final head856292a.🔍 What I verified myself
unix-dotfilepremise is real. The one branch no local filesystem can induce — a WAL pragma that answers instead of failing — I probed three independent ways against the exact bundled native (libe_sqlite3.so, SQLite 3.50.4): raw ctypes on the bundled lib, a standalone .NET 10 harness on the same package versions (Microsoft.Data.Sqlite 10.0.9+SQLitePCLRaw.bundle_e_sqlite3 3.0.3), and the test itself. All three:journal_mode=WAL→ answersdelete, plain writes still succeed. The test pins reality, not a mock of it. (It failed twice in my sandbox under memory starvation from parallel review sessions — 7/7 green when the box was calm, including the identical instrumented run. CI's clean runner agrees.)ExecuteAsync_ProbesSqlite_WhereTheFlagSays…separates a directory nameddbwithout the flag from a flagged one namedimages, same obstacle in both, different verdicts. AName != "db"match or a probe-everywhere use case both fail it. Beautiful.SQLite Error 14— pinned byAssertDetailIsTheFilesystemson the Disappears/LosesWritePermission/NamesBoth rows, and DEPLOYMENT §7's table sends the two fault classes to genuinely different repairs.writablesemantics preserved.deploy/DockerfileHEALTHCHECK,deploy/smoke.sh(all(.writable)), and the §7 jq recipe are byte-untouched (empty diff stat) — no new tunables, exactly as promised.IProbeNamingseam in Infrastructure, the flag composed by the host inReadinessRegistration— the use case never sniffs a name. Interface+impl per file matches every sibling inUseCases/. ADR 0010 amendment, ADR 0014 amendment 4 and ADR 0025 cross-references all exist and say what's claimed. OpenAPI regenerated withdetailrequired-nullable and pinned by the snapshot test.Pooling=false+finallycleanup of.dband all three sidecars,Probe_HoldsNoDescriptorOpen_AfterItReturnswith a canary proving the/proc/self/fdreader isn't vacuous — andHealth_LeavesNoProbeFilesBehindextended to assert it through the real host on Linux.856292a: build ✅, test ✅ (full suite on a clean runner). Coverage bot (comment below):CheckReadinessUseCase100%/100%,SqliteHostingProbe100% lines,GuidProbeNaming/HealthEndpoints/ReadinessRegistration100%. Local: Release build 0 errors (NU1903/EF1002/ASPDEPR005 are the documented pre-existing baseline), probe class 9/9.💡 Little ideas (non-blocking)~
journalMode ?? "unknown"/ non-SqliteExceptioncatch halves, which no compliant local SQLite can induce. The repo's own convention (yourLinuxFactAttributenames the case that pins its branch elsewhere) would suggest one remark line saying which arm is deliberately unpinned and why — so a future coverage report doesn't read as an accident. Pure polish ♪main.✅ What I liked~
IProbeNamingdesign: a real SQLite refusing for real reasons, at a path the test chose in advance — no mock anywhere in sight. That's the hardest thing to get right in this kind of test and you made it look easy~-shm-blocked test first proving with its own connection that SQLite leaves a-walbehind before demanding the probe clean it up. Honesty guards all the way down~(deleted)-fd leak until the container died in production months later. fufu~ you thought of it first ♡detailin the refusing party's own words, one warning line per failing directory per poll,nullon healthy entries pinned from both sides. Operators are actually going to see what broke.(For the record: the
Deploy Smoke Test / smokejob shows red on this head — it has never been green in the entire visible run history, including onmainand this PR's merge-base. Chronic infra, not this branch.)Automated review by Jibril · 2026-09-18
CI/CD: build+test passed for head
856292a(smoke: chronic pre-existing red) · Local checks: Release build 0 errors, SqliteHostingProbeTests 9/9, unix-dotfile premise independently reproduced (ctypes + harness + test); broader local runs OOM-aborted by sandbox memory storms — CI green + coverage bot cited instead🔮 fufu~ Jibril reviewed your code!
Oh? Ohhh~ A readiness probe that doesn't just ask "can you hold bytes?" but "can you hold a database?" The CIFS incident finally getting its revenge served cold at boot time — this is the kind of epistemology I live for~ ♡
Verdict: ✅ Looks good to me~
I read the full +1117/−78 diff and the full contents of every changed file, traced the sibling patterns, and verified locally. Nothing blocking. Not one thing. You should see my face — I'm almost disappointed~ ♪
✅ What I liked~
HostsDatabaseonDataDirectory, set by the host inReadinessRegistration— never aName == "db"match inside the use case. And it's pinned both ways: a directory nameddbwithout the flag stays writable, a flagged one namedimagesfails (ExecuteAsync_ProbesSqlite_WhereTheFlagSays…). I mutation-probed this myself (below) — it bites.detailis the refusing party's own words. Filesystem message on plain refusal, SQLite's message on hosting refusal,journal_mode stayed <mode>on the quiet WAL refusal — and the short-circuit (SQLite probe skipped once the plain probe refused) keeps a chmod'd db dir from reporting a misleadingSQLite Error 14. The integration tests assert the negative space too: detail must benullon healthy entries.Pooling = falsewith a comment explaining why (a pooled handle survives Dispose and leaks 3(deleted)descriptors per poll), the finally-block sidecar sweep, andProbe_HoldsNoDescriptorOpen_AfterItReturnsreading/proc/self/fdwith a canary guard so it can't pass vacuously. The honesty guards throughout (AssertRefusesWrites,SqliteRefusalForADatabaseAt) are exactly the stanceEnforcedFileModesDataestablished — a skip or an assertion is only allowed to stand when something else independently proves the fixture isn't lying.unix-dotfileVFS trick. No local filesystem refuses WAL silently, so you pinned the CIFS symptom on a VFS that genuinely can't do shared memory — with companion honesty guards proving the pragma answersdeletewhile a plain write still succeeds. That's the exact volume this branch exists for, reproduced for real. I got giddy~detailnullable and additive;writablesemantics unchanged, so theHEALTHCHECK,smoke.shand the §7 jq recipe are untouched (I re-read all three). Snapshot regenerated and pinned; ADR 0010 amendment explains why only db — probing the other three would fail a perfectly good network share. Correct scoping, documented reasoning.🔬 My own verification at head
856292aScheduledBackupServiceTestsxUnit2013/CS8620,SearchServiceCS8602, NU1903 OpenApi — the usual suspects).ReadinessUseCaseTests+SqliteHostingProbeTests13/13;HealthEndpointIntegrationTests15 passed / 4 skipped (uid-0 mode rows, by design);OpenApiSnapshotTests5/5.Exercise): exactlyExercise_ReportsTheModeTheFileKept_WhenTheVolumeCannotDoWalRED, 8/9 green; restored → green. Mutation probe B (flagimagesasHostsDatabase: truetoo): exactlyHealth_HostsTheProbeDatabaseInDbOnly_NotInTheOtherThreeDirectoriesRED; restored → green. Both probes binary-verified — my first probe-A run showed a false green because the sandbox's parallel review sessions were copying stale DLLs into the shared test bin (thejournal_mode stayedliteral still in the binary); I redid it in a private worktree withstrings -e lmarker checks before and after each build. The tests are directional, not decorative~💡 Little ideas (non-blocking)~
journalMode ?? "unknown"is the one uncovered branch (83.3% branch on the class): a pragma answeringnulldoesn't seem reachable on any real volume. Fine to leave; if you ever want the last branch, it would take a fake connection, which contradicts the real-SQLite stance of this suite. I'd leave it.The BACKLOG addition for the teardown flake (with both observed artefact forms — the docs-only sync commit naming
.db-shmand the empty.db— nice catch) is exactly the right way to record a pre-existing ~1-in-10 race you didn't cause.Fufu~ ship it. The volume that lies to readiness checks finally meets a probe it can't fool. ♡
Automated review by Jibril · 2026-09-18
CI/CD: coverage bot 8024 for head
856292a(13:59 UTC) — full suite incl. 100%/100%CheckReadinessUseCase, 100% line onSqliteHostingProbe/GuidProbeNaming/ReadinessRegistration/HealthEndpoints; PR-body verify.sh counts (647/647 etc.) cover561526f, docs-only delta to head · Local checks: build 0 errors, 13/13 + 15P/4S + 5/5, 2 mutation probes RED→restored→green🔮 fufu~ Jibril reviewed your code!
The readiness probe that finally asks the question the CIFS incident begged: can this directory host a SQLite database — not merely store bytes.
HostsDatabaseas an explicit flag rather than a name check, a probe that goes through WAL and a real commit,detailcarrying the refusing party's own words, and the honesty guards to prove the refusal is SQLite's answer, not the probe's. I read all 17 files.Verdict: ✅ Approved~ ♡
✅ What I verified myself
856292a, fresh clean build in my sandbox: RestAdapter 643 passed / 0 failed / 4 skipped (the uid-0LosesWritePermissionrows — root here too) in 5 m 50 s; ApplicationCore 49/49, Integration 1/1, Infrastructure 205/205. Matches your verify.sh arithmetic at561526f; the increment to856292ais a 6-line BACKLOG wording change only — I diffed it.Microsoft.Data.Sqlite.Core+bundle_e_sqlite33.50.4 and ran it: on the default VFSPRAGMA journal_mode=WALanswerswal; onunix-dotfileit answersdelete— the silent refusal, exactly as the class remarks claim. Your premise holds on this box, not just in theory.SqliteHostingProbe.Probecall dropped fromRefusal→ red on exactly the 4CannotHostSqliterows (all-wal/-shm/-journaltheory arms +"") +Health_HostsTheProbeDatabaseInDbOnly…+ the flag-driven use-case test (6 red).Pooling = trueon the probe connection → red on exactlyProbe_HoldsNoDescriptorOpen_AfterItReturns+Health_LeavesNoProbeFilesBehind(2 red) — the/proc/self/fdcanary is not vacuous.Both restored; trees clean.
IProbeNamingis the load-bearing seam. It lets the host integration tests place a real directory at the probe database's real path — a refusal by a real SQLite, not a mock. AndGuidProbeNamingin production means two polls can never collide. Registered plain-and-early so a test host's own registration wins — correct order, documented.detailshapes to fixes (filesystem message → §8.1;SQLite Error 5/8/10/14orjournal_mode stayed delete→ move the volume, no mount option repairs it) is the part an operator at 3 a.m. needs. The hard-kill leftover inventory (.dm-health-*+ sidecars, all safe to delete) closes the loop.💡 Little ideas (non-blocking)~
SqliteHostingProbe.SidecarSuffixesispublic— if the app ever grows a second probe-like writer, consider whether it should own its suffixes rather than reading yours. Nothing today reads it; leave it.LinuxFactAttribute(string pinnedElsewhereBy)constructor arg is a lovely touch — the skip reason names the test that still pins the branch. If more platform-gated facts accumulate, that pattern deserves copying upstream of this repo. (Two uses today; not yet a pattern worth extracting.)✅ What I liked~
deleteand a plain write still commits — so the test cannot pass by accident with a probe that never checks the answer, nor fail because the VFS broke writes entirely. That is what "pinned" means.writablesemantics unchanged. The DockerHEALTHCHECK,smoke.shand the §7 jq recipe keep working untouched — the stronger probe rides under the existing verdict, and onlydetailis new on the wire.-shm-blocked-leaves--walcase. Proving SQLite's own cleanup limitation with the test's own connection first, then that the probe cleans it up anyway — you characterized the platform before asserting against it.main, two candidate fixes, honest "unverified hypothesis". That's a bug report as an asset.Automated review by Jibril · 2026-09-18
CI coverage: bot comment present for head
856292a(94.6% lines,SqliteHostingProbe100%/83.3%,CheckReadinessUseCase100%/100%). · Local: full suites green at856292ain sandbox (see above); probes fresh this round; the earlier session's transientExercise_…WhenTheVolumeCannotDoWalreds were traced to binaries from core-dumped builds under sandbox thread exhaustion — they do not reproduce on any clean rebuild (6 consecutive greens across filtered and full runs).(Verification notes: reviewed at head
856292a, basecf40728, merge-basecf40728. The WAL-on-dotfile premise cross-checked with a standalone SQLite 3.50.4 harness in addition to the suite's own guards.)