Repository navigation
fix(storage): fail catalog saves when the directory fsync fails so committed rules stay in the WAL - #333
Merged
Conversation
save_json_atomic ignored the parent-directory fsync error and returned Ok. For a catalog-only commit the WAL then pruned its copy of the changes, so a power loss could undo the rename and silently drop a committed rule or schema. Propagate the error through sync_directory instead, so the WAL keeps the changes until both catalog files are durable. Closes #297
This was referenced Oct 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Intent
I want you to really hyper focus on making the symbolic engine, you know, crisp and really truly production ready. (Context: #297 (audit P1-1): the catalog save discards the parent-directory fsync error, so a committed rule or schema can vanish on power loss. save_json_atomic ignores the directory fsync error and returns Ok; for catalog-only commits the WAL entries are then pruned away, so a power loss can silently drop a committed rule/schema. Fix: propagate the directory fsync error like sync_directory does; add a fault-injection test.)
What Changed
save_json_atomic(src/storage/metadata.rs) no longer ignores errors from opening or fsyncing the parent directory after the rename. It now calls the sharedpersist::sync_directory(nowpub(crate)) and returns its error. A path with no parent or an empty parent falls back to.. The result is that a catalog or schema save whose rename may not be durable counts as a failed save, and its WAL entries are not pruned.savereturns an error when the directory sync fails.rules/directory and on the KG directory, then simulates power loss by deletingrules/catalog.jsonandschema.json. It checks that the committed schema and rule are still in the WAL and come back on reopen.Risk Assessment
✅ Low: The change is small and targeted. save_json_atomic now returns the directory-fsync error through the existing sync_directory helper, and every caller already handles a save error safely: save_catalogs leaves the WAL entries in place, the rule catalog stays dirty, and tombstones are left unchanged in memory. Fault-injection tests cover both the rules directory and the KG directory.
Testing
I ran the two new fault-injection tests and the existing WAL-prune test. All three pass, and the two new tests fail with the pre-fix save_json_atomic. Then I drove the real inputlayer-server and client end to end, using an LD_PRELOAD shim to make directory fsync fail with EIO. With the fix, the commit succeeds and logs catalog_save_failed_wal_keeps_changes, and the WAL keeps the catalog entries. After a kill -9 and deleting the non-durable catalog files, a restart replays rule v and the item int schema: ?v(X) returns 1 and 2, and a string insert is rejected. The pre-fix control logs no warning, prunes the WAL, and comes back with no rules and ?v(X) returning nothing. All four live scenarios passed. The unit-test regression scenario is marked untested in the live sense because it ran only as cargo tests. The target/ build output was removed afterwards.
Evidence: Live transcript, fixed server: fault-injected commit, WAL retained, power-loss simulation, recovery of rule and schema
Evidence: Fixed server log showing injected EIO and catalog_save_failed_wal_keeps_changes warnings
Evidence: Control transcript, pre-fix server: fsync error swallowed, WAL pruned, rule lost after power loss
Evidence: Fault-injection shim source (EIO on directory fsync)
Evidence: Recovery after simulated power loss (fixed vs pre-fix)
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
src/storage/metadata.rs:490- The new testtest_save_fails_when_the_directory_sync_failswas inserted between the existing doc comment/// Regression: Metadata save creates parent directories if they don't exist.and the test it describes,test_metadata_save_creates_parent_dirs. The comment now documents the wrong test. Move the new test above that doc comment, or belowtest_metadata_save_creates_parent_dirs.✅ **Test** - passed
✅ No issues found.
cargo test --lib -- unsynced_catalog_directory_keeps_the_changes_in_the_wal test_save_fails_when_the_directory_sync_fails saved_catalog_changes_leave_the_wal(all pass)Same two new tests run with the base-commit save_json_atomic body temporarily restored: both FAIL (regression reproduced), source then restoredBuilt an LD_PRELOAD shim (failfsync.so) that returns EIO from fsync/fdatasync on chosen directoriesLive:inputlayer-server(fixed) under the shim failing fsync on data/default and data/default/rules;inputlayer-client commit.iqlregisters schemaitem(x: int)and rulev(X) <- item(X); checked server log and WALLive: kill -9 the server, delete default/rules/catalog.json and default/schema.json (renames never made durable), restart without the fault,inputlayer-client check.iql(.rule list, insert, ?v(X)) andschema_check.iql(string into int column)Live control: the same steps on a server binary built from the base commit's metadata.rs✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.