Fix rel_id_server counter seeding: three defects, one path - #56
Open
david-w-t wants to merge 1 commit into
Open
Conversation
Filed as a one-line typo; it was three stacked defects, and the typo was
the least important of them.
1. seed_from_mnesia/0 called mnesia:dirty_foldl/3, which does not exist in
OTP 28 (real function is mnesia:foldl/3, and it must run inside a
transaction). Invisible to the compiler.
2. Its blanket `catch _:_ -> 1' swallowed the resulting undef -- and every
other failure -- and returned 1, precisely the value that collides with
live relationship primary keys. The error handler WAS the corruption:
the failure was silent by construction. Now 1 is returned only for a
definite {no_exists, relationships}; anything else logs and exits
rel_id_server_seed.
3. The ordering defect the report missed, and the reason the first two
fixes do not close the hole on their own. Seeding ran from init/1,
where the table is structurally unreadable, circularly so:
rel_id_server must start BEFORE graphdb_mgr (graphdb_bootstrap draws
ids from get_id_pair/0 while loading the scaffold), yet the
relationships table is not created -- and mnesia is not even started --
until graphdb_bootstrap:ensure_mnesia/0 runs inside graphdb_mgr:init/1.
An eager seed therefore always read "no rows" and landed on 1 no matter
how (1) and (2) were fixed. Seeding is now lazy, on the first
get_id/get_id_pair call.
Failure mode closed: DETS file lost while the Mnesia relationships table
survives (restore, data-dir move, partial recovery) no longer restarts the
counter at 1, handing out ids that collide with existing primary keys and
letting mnesia:write silently overwrite those rows.
The {rel_id_server, seed_from_mnesia, 0} xref_ignores entry is removed
rather than widened -- it existed solely to hide this call, so xref now
runs with an empty ignore list.
rel_id_server_SUITE gains a `seeding' group (4 cases) and now starts mnesia
per testcase: starting the server with no mnesia at all, as it used to, is
not a state the system can be in, and is part of why this stayed hidden.
Each new case was revert-tested against the bug it covers; notably, with
the typo fixed but seeding still eager, the corruption test still yields 1.
555 CT + 151 EUnit green; xref clean; zero warnings in both profiles.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWukKCbrN8GybaScJGU2kF
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
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The short version
This was filed on #53 as a one-line typo (
mnesia:dirty_foldl/3doesn't exist). It was three defects stacked in the same path, and the typo was the least important of them — fixing it alone would have left the data-corruption hole fully intact.What was wrong
1. The typo.
rel_id_server:seed_from_mnesia/0calledmnesia:dirty_foldl/3, which does not exist in OTP 28. Confirmed empirically —undefwhether mnesia is running or not. The real function ismnesia:foldl/3, and it must run inside a transaction.2. The error handler was the actual hazard.
That swallowed the
undef— and every other failure — and returned1: precisely the value that collides with live#relationship.idprimary keys. The fallback was the corruption, and it made the failure silent by construction.A "cannot determine the high-water mark" answer must never default to the value that collides. It now returns
1only for a definite{no_exists, relationships}, and logs + exitsrel_id_server_seedon anything else.3. The ordering defect the report missed — and why the first two fixes don't close the hole.
Seeding ran from
init/1, where the table is structurally unreadable, and the constraint is circular:rel_id_servermust start beforegraphdb_mgr—graphdb_bootstrap.erl:565draws ids fromget_id_pair/0while loading the scaffold.relationshipstable isn't created — and mnesia isn't even started — untilgraphdb_bootstrap:ensure_mnesia/0runs insidegraphdb_mgr:init/1.So an eager seed always read "no rows" and landed on
1regardless of how (1) and (2) were fixed. Seeding is now lazy, on the firstget_id/get_id_paircall, by which point bootstrap has run and the table is loaded.I found this because the full suite failed after the first fix: 10 suites died with
node_not_running, disproving the assumption that graphdb's{applications, [... mnesia ...]}entry guaranteed mnesia was up. It doesn't — this codebase bootstraps mnesia manually.Failure mode closed
DETS file lost while the Mnesia
relationshipstable survives (restore, data-dir move, partial recovery). The counter no longer restarts at 1 handing out ids that collide with existing primary keys, whichmnesia:writewould then silently overwrite.Evidence the tests aren't vacuous
Each new case was revert-tested against the bug it covers:
seeds_above_existing_relationship_idsrefuses_to_seed_when_max_id_undeterminable{error, rel_id_server_seed}, got{ok, <0.943.0>}seed_is_deferred_until_first_use[], got[{counter,1}]seeds_above_existing_relationship_idsThe last two rows are the proof that the typo fix alone was insufficient.
xref
The
{rel_id_server, seed_from_mnesia, 0}xref_ignoresentry is removed, not widened — it existed solely to hide this call.rebar3 xrefnow runs with an empty ignore list and passes.Test suite fidelity
rel_id_server_SUITEstarted the server with no mnesia at all, which is not a state the system can be in — part of why this stayed hidden. It now starts mnesia per testcase, and gains aseedinggroup (4 cases).Verification
rebar3 xrefclean, empty ignore listDocs
TASKS.md(defect → IMPLEMENTED, ordering finding recorded) and a newdocs/Architecture.mdsection onrel_id_serverseeding and why it cannot happen atinit/1.🤖 Generated with Claude Code
https://claude.ai/code/session_01EWukKCbrN8GybaScJGU2kF