Skip to content

fix: retry with backoff before wiping the graph on a non-lock-contention open failure - #24

Open
pradeepmouli wants to merge 1 commit into
intuit:mainfrom
pradeepmouli:fix/graph-open-retry-backoff
Open

fix: retry with backoff before wiping the graph on a non-lock-contention open failure#24
pradeepmouli wants to merge 1 commit into
intuit:mainfrom
pradeepmouli:fix/graph-open-retry-backoff

Conversation

@pradeepmouli

Copy link
Copy Markdown
Contributor

Summary

Depends on #20 — this branch is built on top of that PR's fix/lock-contention-wipe, since this fix extends the exact same code path (Infigraph::init()'s error-handling arm right after the lock-contention check). The diff currently includes #20's commit too since it isn't merged yet; it'll shrink to just this PR's own commit automatically once #20 lands.

#20 correctly avoids wiping the graph when Kuzu's open error is lock contention ("Could not set lock on file"), since that just means another live process (a watcher) already has it open. Any other open error — including a transient short read from a concurrent writer mid-checkpoint — fell straight through to wipe_graph with zero retry, on the assumption it must be genuine corruption.

This destroyed a real repo's graph tonight: a manual scip-import ran concurrently with a background watcher/MCP-server process touching the same graph file, producing IO exception: Cannot read from file... 0 bytes on the next open — not a lock message, so it wiped tens of thousands of symbols that were sitting right there on disk moments earlier.

This PR retries with backoff (200ms, 500ms, 1s) before giving up. This self-heals the transient-race case — including slower writers than a single fixed retry would tolerate, e.g. a large SCIP import still mid-checkpoint — while still correctly wiping-and-rebuilding graphs that are durably corrupt, since retries against those keep failing too. If a retry attempt comes back as lock contention specifically, it stops retrying immediately and reports that instead (now unambiguously another live process, not a checkpoint race).

Is there a more principled fix than string-matching a retry threshold?

Investigated this directly before writing the fix:

  • The underlying lbug (Kùzu) crate has no structured error type to match on instead of strings — open failures cross the FFI boundary as an opaque cxx::Exception, so there's no error code available at the Rust binding layer.
  • Kùzu's own concurrency docs don't cover retry or corruption-detection at all. Their documented model is strict single-writer, with concurrent-write access meant to go through one dedicated writer process — the same pattern already used by this codebase's Neo4jBackend, which has no retry/wipe logic of its own since Neo4j handles concurrency server-side.

So this PR is a pragmatic safety net given that constraint, not a substitute for routing all writes through a single process (a much larger architectural change, out of scope here).

Test plan

  • cargo test -p infigraph-core --lib — 3 new regression tests:
    • init_recovers_from_transient_open_failure_without_wiping — a file that heals within the first retry window is not wiped
    • init_recovers_from_slower_transient_failure_via_backoff — heals at ~600ms, past where a single fixed-delay retry would already have given up, proving the backoff schedule (not just one retry) matters
    • init_wipes_and_rebuilds_on_persistent_corruption — durable corruption still correctly falls through to wipe+rebuild
  • Full infigraph-core test suite (271 tests) passes
  • graph_queries integration suite (63 tests) passes

🤖 Generated with Claude Code

https://claude.ai/code/session_01TAXDJyFdnA4BV1U2fxufdC

…ion open failure

Infigraph::init() correctly avoids wiping the graph when Kuzu's open
error is lock contention ("Could not set lock on file"), since that
just means another live process (a watcher) already has it open. Any
other open error -- including a transient short read from a concurrent
writer mid-checkpoint -- fell straight through to wipe_graph with zero
retry, on the assumption it must be genuine corruption.

This destroyed a real repo's graph tonight: a manual `scip-import` ran
concurrently with a background watcher/MCP-server touching the same
graph file, producing "IO exception: Cannot read from file... 0 bytes"
on the next open -- not a lock message, so it wiped tens of thousands
of symbols that were sitting right there on disk moments earlier.

Retry with backoff (200ms, 500ms, 1s) before giving up. This self-heals
the transient-race case -- including slower writers than a single fixed
retry would tolerate, e.g. a large SCIP import still mid-checkpoint --
while still correctly wiping-and-rebuilding graphs that are durably
corrupt, since retries against those keep failing too. If a retry
attempt comes back as lock contention specifically, stop retrying
immediately and report that instead (now unambiguously another live
process, not a checkpoint race).

Investigated whether a more principled fix exists: the underlying
`lbug` (kuzu) crate has no structured error type to match on instead of
strings -- open failures cross the FFI boundary as an opaque
`cxx::Exception`, so there's no error code to check. Kuzu's own
concurrency docs don't cover retry/corruption-detection at all; their
documented model is strict single-writer, with concurrent-write access
meant to go through one dedicated writer process (the same pattern
already used for the Neo4j backend, which has no retry/wipe logic of
its own since Neo4j handles concurrency server-side). This fix is a
pragmatic safety net given that constraint, not a substitute for
routing writes through a single process -- that's a larger, separate
architectural change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TAXDJyFdnA4BV1U2fxufdC
@pradeepmouli
pradeepmouli force-pushed the fix/graph-open-retry-backoff branch from 9f65b77 to 6df2c64 Compare July 27, 2026 01:52
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