Skip to content

Fix rel_id_server counter seeding: three defects, one path - #56

Open
david-w-t wants to merge 1 commit into
davidwt-com:mainfrom
david-w-t:develop
Open

Fix rel_id_server counter seeding: three defects, one path#56
david-w-t wants to merge 1 commit into
davidwt-com:mainfrom
david-w-t:develop

Conversation

@david-w-t

Copy link
Copy Markdown
Contributor

The short version

This was filed on #53 as a one-line typo (mnesia:dirty_foldl/3 doesn'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/0 called mnesia:dirty_foldl/3, which does not exist in OTP 28. Confirmed empirically — undef whether mnesia is running or not. The real function is mnesia:foldl/3, and it must run inside a transaction.

2. The error handler was the actual hazard.

catch
    _:_ -> 1

That swallowed the undef — and every other failure — and returned 1: precisely the value that collides with live #relationship.id primary 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 1 only for a definite {no_exists, relationships}, and logs + exits rel_id_server_seed on 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_server must start before graphdb_mgrgraphdb_bootstrap.erl:565 draws ids from get_id_pair/0 while loading the scaffold.
  • The relationships table isn't created — and mnesia isn't even started — until graphdb_bootstrap:ensure_mnesia/0 runs inside graphdb_mgr:init/1.

So an eager seed always read "no rows" and landed on 1 regardless of how (1) and (2) were fixed. Seeding is now lazy, on the first get_id/get_id_pair call, 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 relationships table survives (restore, data-dir move, partial recovery). The counter no longer restarts at 1 handing out ids that collide with existing primary keys, which mnesia:write would then silently overwrite.

Evidence the tests aren't vacuous

Each new case was revert-tested against the bug it covers:

Reverted Failing case Result
typo + blanket catch seeds_above_existing_relationship_ids expected 4243, got 1
typo + blanket catch refuses_to_seed_when_max_id_undeterminable expected {error, rel_id_server_seed}, got {ok, <0.943.0>}
eager seeding only (typo already fixed) seed_is_deferred_until_first_use expected [], got [{counter,1}]
eager seeding only (typo already fixed) seeds_above_existing_relationship_ids expected 4243, got 1

The last two rows are the proof that the typo fix alone was insufficient.

xref

The {rel_id_server, seed_from_mnesia, 0} xref_ignores entry is removed, not widened — it existed solely to hide this call. rebar3 xref now runs with an empty ignore list and passes.

Test suite fidelity

rel_id_server_SUITE started 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 a seeding group (4 cases).

Verification

  • 555 CT (was 551, +4) + 151 EUnit = 706, all green across 14 suites
  • rebar3 xref clean, empty ignore list
  • Zero warnings in both default and test profiles
  • Hard tabs preserved in source; suite stays space-indented

Docs

TASKS.md (defect → IMPLEMENTED, ordering finding recorded) and a new docs/Architecture.md section on rel_id_server seeding and why it cannot happen at init/1.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EWukKCbrN8GybaScJGU2kF

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant