Documentation
¶
Overview ¶
Example 27_concurrent_txn — transactional ISOLATION and ATOMICITY of the WAL-backed Cypher engine, certified under concurrency and the race detector.
A realistic bank clearing-ledger is opened over a write-ahead log. Many writer goroutines move money between accounts while many reader goroutines continuously observe a global invariant that can only hold if the engine isolates in-flight transactions from readers. The whole run doubles as a data-race certification: it is meant to be run under `go test -race`.
Model ¶
(:ACCOUNT {id, balance}) // id is a string account number,
// balance is an integer (cents)
Each account is a node carrying an integer balance in minor currency units, keyed by a string account number backed by a range index for O(log n) lookup. A transfer moves an amount from one account to another: it debits the source and credits the destination by the same amount, so the SUM of all balances is invariant — money is neither created nor destroyed. That conserved total is the observable the readers pin.
The ledger is fully capitalised: initial balances are chosen (and validated) to exceed the largest possible aggregate debit on any single account, so no account can ever go negative. Overdraft protection is therefore not needed and no transfer is ever rejected — every planned transfer commits — which keeps the committed set, and hence the final per-account state, deterministic.
What it certifies ¶
The example exercises, and asserts, three ACID properties under contention:
ISOLATION (the headline). Readers repeatedly compute `sum(balance)` over all accounts, via both cypher.Engine.Run and a read-only cypher.Engine.BeginReadTx transaction. Under correct isolation this sum ALWAYS equals the seeded total: a reader must never observe a debit without its matching credit. A single torn observation is a module isolation bug — the run surfaces it as an error rather than hiding it, and the fact line total_balance_invariant_holds flips to 0.
ATOMICITY. Half the transfers run as MULTI-STATEMENT explicit transactions (cypher.Engine.BeginTx: a debit statement, then a credit statement, then one Commit). The engine holds the visibility barrier for the whole transaction, so a concurrent reader can never slip between the debit and the credit — it sees the whole transaction or none of it. The other half run as SINGLE-STATEMENT autocommit writes (cypher.Engine.RunInTx) that debit and credit in one statement.
CONSISTENCY / no lost updates. Because every transfer is a commutative delta on two accounts, replaying the committed transfers in any order yields the same final per-account balances. The run computes that expected state deterministically up front and, after the concurrent phase, asserts every account matches it. A single mismatch means a read-modify-write interleaving lost an update (a serialisation failure); lost_updates counts them and must be 0.
Isolation model (verified against cypher/exectx.go and cypher/engine) ¶
The engine serialises writers on the store's single-writer mutex, and cypher.Engine.BeginTx additionally holds the graph's visibility write-lock for the transaction's whole lifetime — so writers are strictly serialised and a transactional reader either observes the state before a write transaction began or the fully committed state after it ended, never a partial state (read-committed isolation, cypher/exectx.go). cypher.Engine.BeginReadTx is the read-only path: it takes neither the writer serialisation nor the barrier and so is never blocked by, and never blocks, other transactions.
Scale ¶
The default is small, deterministic, and fast (a few hundred transfers over a few dozen accounts) so the regression test stays well under the 60 s short-layer budget. Every dimension is a flag, so the same binary scales up to where write serialisation and reader throughput are worth observing:
go run ./examples/27_concurrent_txn -accounts 5000 -writers 16 -readers 32 \
-ops-per-writer 5000 -max-amount 1000 -seed 7
(-max-amount is kept small at this scale so the fully-capitalised no-overdraft invariant still holds: min-initial must be >= writers*ops-per-writer*max-amount.)
The deterministic facts (counts, the seeded total, the conservation and no-lost-update invariants) reproduce for a fixed -seed; only the telemetry (lines prefixed with "# ") and the temp directory path vary per run and machine.