Skip to content

NornicDB Transaction Implementation

Transaction Semantics

What's Implemented

Atomicity - All operations commit together or none do

Operation Buffering - Changes are invisible until commit

Read-Your-Writes - Transaction can see its own uncommitted changes

Rollback - Discard all buffered operations

Closed Transaction Detection - Error if operating on closed tx

Isolation Semantics

NornicDB storage transactions now provide snapshot isolation via MVCC:

  • explicit transactions capture a read snapshot when BeginTransaction() starts
  • all committed reads inside the transaction are filtered to that snapshot
  • uncommitted changes are not visible to other transactions
  • read-your-writes still applies for changes buffered by the current transaction
  • concurrent write-write races fail at commit with a conflict instead of silently overwriting newer state

In practice, all reads within a transaction see a consistent storage-layer view of the graph as of the transaction start, plus the transaction's own pending changes.

Usage Example

engine := storage.NewMemoryEngine()
tx := engine.BeginTransaction()

// Operations are buffered, not yet visible in engine
tx.CreateNode(&Node{ID: "user-1", Labels: []string{"User"}})
tx.CreateNode(&Node{ID: "user-2", Labels: []string{"User"}})
tx.CreateEdge(&Edge{ID: "e1", StartNode: "user-1", EndNode: "user-2", Type: "FOLLOWS"})

// Read-your-writes works
node, _ := tx.GetNode("user-1")  // Returns the buffered node

// Either commit all...
err := tx.Commit()

// ...or rollback all
// tx.Rollback()

Performance Benchmarks

BenchmarkTransaction_CommitNodes-16      129,942   10,618 ns/op   10,737 B/op   96 allocs/op
BenchmarkTransaction_RollbackNodes-16    224,329    5,403 ns/op    6,776 B/op   66 allocs/op

Interpretation:

  • Committing 10 nodes takes ~10.6μs (~1μs per node)
  • Rolling back 10 nodes takes ~5.4μs (faster - no storage writes)
  • Memory overhead: ~1KB per node in transaction buffer

Test Coverage

Package Coverage
pkg/storage 85.2%
pkg/filter 96.0%
pkg/search 89.8%
pkg/decay 87.4%
pkg/inference 85.5%

Test Results

=== RUN   TestTransaction_CreateNode_Basic
--- PASS: TestTransaction_CreateNode_Basic (0.00s)
=== RUN   TestTransaction_Rollback
--- PASS: TestTransaction_Rollback (0.00s)
=== RUN   TestTransaction_Atomicity
--- PASS: TestTransaction_Atomicity (0.00s)
=== RUN   TestTransaction_DeleteNode
--- PASS: TestTransaction_DeleteNode (0.00s)
=== RUN   TestTransaction_UpdateNode
--- PASS: TestTransaction_UpdateNode (0.00s)
=== RUN   TestTransaction_CreateEdge
--- PASS: TestTransaction_CreateEdge (0.00s)
=== RUN   TestTransaction_CreateEdgeWithNewNodes
--- PASS: TestTransaction_CreateEdgeWithNewNodes (0.00s)
=== RUN   TestTransaction_DeleteEdge
--- PASS: TestTransaction_DeleteEdge (0.00s)
=== RUN   TestTransaction_ClosedTransaction
--- PASS: TestTransaction_ClosedTransaction (0.00s)
=== RUN   TestTransaction_IsActive
--- PASS: TestTransaction_IsActive (0.00s)
=== RUN   TestTransaction_Isolation
--- PASS: TestTransaction_Isolation (0.00s)
=== RUN   TestTransaction_MultipleOperationTypes
--- PASS: TestTransaction_MultipleOperationTypes (0.00s)
PASS

ELI12 Explanation

Imagine you're rearranging furniture in your room:

  1. BEGIN = "I'm going to rearrange my room"
  2. Operations = Moving furniture around (but not committing yet)
  3. COMMIT = "Yes! I like this arrangement, keep it!"
  4. ROLLBACK = "Nope, put everything back where it was"

The transaction remembers where everything was before, so if you change your mind (ROLLBACK), everything goes back to the original spots!

Date

Implemented: November 26, 2025