The Neo4j vs Doublets benchmark suite was failing during the delete benchmark with error NotExists(4000). Root cause analysis revealed that the Transaction wrapper implementation was a non-functional stub that always returned errors for delete operations, causing the benchmark to panic.
| Time | Event |
|---|---|
| 2025-12-22 12:22:51 | First failed run detected (run-20431789056) |
| 2025-12-22 13:20:15 | Second failed run (run-20433102564) |
| 2025-12-22 16:06:33 | Third failed run referenced in issue (run-20437258944) |
All three failures exhibited the same error pattern:
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: NotExists(4000)', benches/benchmarks/delete.rs:23:9
The panic occurred at benches/benchmarks/delete.rs:23, which corresponds to the bench! macro invocation. The actual error was propagated from inside the macro where fork.delete(id)? was called.
The root cause was in rust/src/transaction.rs. The Transaction struct was intended to be a wrapper around the Client to provide transactional Neo4j operations. However, the implementation was a non-functional stub:
Original problematic code:
// transaction.rs lines 55-74
fn create_links(&mut self, _query: &[T], handler: WriteHandler<T>) -> std::result::Result<Flow, Error<T>> {
// Does nothing - just returns empty handler
Ok(handler(Link::nothing(), Link::nothing()))
}
fn delete_links(&mut self, query: &[T], _handler: WriteHandler<T>) -> std::result::Result<Flow, Error<T>> {
// Always returns NotExists error!
Err(Error::NotExists(query[0]))
}-
Create benchmark runs first for both
Neo4j_NonTransactionandNeo4j_TransactionNeo4j_NonTransaction: UsesClientwhich properly creates links in Neo4jNeo4j_Transaction: UsesTransactionwhich silently does nothing (returns empty handler)- Create benchmark "passes" for both because no error is returned
-
Delete benchmark runs second
Neo4j_NonTransaction: Completes successfully (links exist, deletes work)Neo4j_Transaction: Fails immediately because:create_linksdidn't actually create any linksdelete_linksalways returnsErr(Error::NotExists(id))
-
The
tri!macro wraps the benchmark and calls.unwrap()on the result, causing the panic
The delete benchmark loops from ID 4000 down to 3000 in reverse order:
for id in (BACKGROUND_LINKS..=BACKGROUND_LINKS + 1_000).rev() {
let _ = elapsed! {fork.delete(id)?};
}Where BACKGROUND_LINKS = 3000. So the first attempted delete is ID 4000, which immediately returns Err(NotExists(4000)).
The fix properly implements Transaction to delegate all operations to the underlying Client:
-
Made Client API public: Added public accessor methods and made response types public
pub fn host(&self) -> &strpub fn port(&self) -> u16pub fn auth(&self) -> &strpub fn constants(&self) -> &LinksConstants<T>pub fn fetch_next_id(&self) -> i64pub fn execute_cypher(&self, ...) -> Result<CypherResponse>- Made
CypherResponse,QueryResult,RowData,CypherErrorpublic
-
Rewrote Transaction: Full delegation to Client for all Links/Doublets operations
create_links: Now properly creates links via Client's execute_cypherdelete_links: Now properly queries and deletes via Client- All other operations also properly delegated
As the comment in transaction.rs explains:
In the HTTP-based approach using
/db/neo4j/tx/commitendpoint, all requests are auto-committed transactions. This wrapper exists for API compatibility to benchmark "transactional" Neo4j operations.
The Transaction and NonTransaction benchmarks will now produce similar results since the HTTP API auto-commits. For true transactional semantics, multi-request transaction endpoints would need to be used (see Neo4j HTTP Transaction API).
| File | Changes |
|---|---|
rust/src/client.rs |
Made CypherResponse/QueryResult/RowData/CypherError public, added accessor methods |
rust/src/transaction.rs |
Complete rewrite to delegate to Client |
-
Stub implementations should fail explicitly: The original stub silently returned success for creates but explicit failure for deletes, causing confusing behavior.
-
Integration tests needed: Unit tests for individual components would not have caught this since the stub "worked" in isolation.
-
CI logs are essential: The CI logs clearly showed the exact error and location, enabling quick diagnosis.
The following CI run logs have been archived in this case study:
logs/run-20431789056.log- First failurelogs/run-20433102564.log- Second failurelogs/run-20437258944.log- Third failure (referenced in issue)