Skip to content

Bulk loads exclude regular updates (pometry-storage#425) - #2861

Merged
fabianmurariu merged 1 commit into
bulk-load-exclusionfrom
bulk-load-exclusion-425
Oct 16, 2026
Merged

fabianmurariu merged 1 commit into
bulk-load-exclusionfrom
bulk-load-exclusion-425

Conversation

@fabianmurariu

Copy link
Copy Markdown
Collaborator

Fixes Pometry/pometry-storage#425. See ADR-0001 in pometry-storage.

Summary

 GraphStore
+  bulk_loader_lock: RwLock<()>
+  bulk_load_read()  -> read_recursive guard
+  bulk_load_write() -> write guard
   vacuum()
+    holds bulk_load_read()

 WriteLockedGraph::new            // bulk loads + WAL replay
+  bulk_load_write()              // taken first, released after the segment locks

 DurabilityOps::transaction_manager()
-  -> &TransactionManager
+  -> TransactionGuard { &TransactionManager, read guard }   // Deref<Target = TransactionManager>

 import_node_internal
+  holds transaction_manager()
 load_graph_props_from_df          // graph properties are regular updates
+  holds transaction_manager()

Every regular update (add/delete edge, add node, set node type, node/edge/graph properties and metadata) already calls transaction_manager() first and keeps it to the end, so they now hold the read guard for their full duration with no call-site changes. WAL recovery goes through write_lock() and so holds the write guard.

Evidence

New tests in io_tests/df_loaders.rs:

test_write_locked_graph_excludes_regular_updates_and_vacuum
  hold write_lock()
  add_edge / add_node on new segments, vacuum  -> each still blocked after 200ms
  drop write lock                              -> all three complete

test_regular_updates_during_edge_bulk_load
  edge bulk load (2000 edges, 40 chunks, page len 2)
  || 4 threads x 200: add_edge, add_node(type), set_node_type, add_edge between bulk-loaded nodes
  finishes within 60s; node/edge counts = bulk + regular; props and types intact
  • Before (WriteLockedGraph takes a read guard instead): both fail. The exclusion test fails with "ran while the graph was write locked for a bulk load". The concurrency test fails with wrong node/edge counts in one run and deadlocks (60s timeout) in another.
  • After: both pass. Full suites: test_io 67 passed / 6 ignored, test_lib 113 passed / 3 ignored, test_graphs 200 passed, test_nodes_edges 39 passed / 1 ignored. db4-storage, db4-graph and raphtory-storage tests pass. cargo check --workspace --tests is clean here and in pometry-storage.

Merge Danger

Door: two-way

Revert the commit to restore the old behaviour; nothing is persisted.

Blast Radius: mutations

  • Regular updates now wait for a running bulk load to finish, and a bulk load waits for in-flight regular updates. Because readers use read_recursive, a steady stream of regular updates can delay a bulk load from starting.
  • Calling a regular update (or vacuum) from inside a bulk load, on the same thread, now deadlocks. No such call exists today; the doc comment on transaction_manager() says so.
  • load_graph_props_from_df holds the read guard for the whole load, so a concurrent bulk load waits for it.

🤖 Generated with Claude Code

GraphStore owns a bulk_loader_lock. WriteLockedGraph takes the write guard
for its lifetime, so a bulk load or WAL replay excludes every regular update.
transaction_manager() now returns a TransactionGuard that holds a
read_recursive guard, so regular updates still run concurrently with each
other. import_node_internal and load_graph_props_from_df take it explicitly,
and vacuum takes the read guard.

Fixes Pometry/pometry-storage#425

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment on lines +309 to +318
/// Recursive, so a regular update nested in another does not wait behind a queued bulk load.
pub fn bulk_load_read(&self) -> RwLockReadGuard<'_, ()> {
self.bulk_loader_lock.read_recursive()
}

/// Write guard held for the whole of a bulk load or WAL replay.
pub fn bulk_load_write(&self) -> RwLockWriteGuard<'_, ()> {
self.bulk_loader_lock.write()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor but calling these bulk_load_shared and bulk_load_exclusive conveys the intention more precisely

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe even rename bulk_loader_lock -> write_lock - then write_lock_shared for the regular updates and write_lock_exclusive for the bulk loaders?

@fabianmurariu
fabianmurariu merged commit bdbd6e0 into bulk-load-exclusion Oct 16, 2026
31 checks passed
@fabianmurariu
fabianmurariu deleted the bulk-load-exclusion-425 branch October 16, 2026 08:55
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.

2 participants