HeliosDB MVCC Configuration Guide
HeliosDB MVCC Configuration Guide
Multi-Version Concurrency Control with Serializable Snapshot Isolation
Overview
HeliosDB implements PostgreSQL-compatible MVCC (Multi-Version Concurrency Control) with Serializable Snapshot Isolation (SSI). This guide explains how to correctly configure and use MVCC features, especially for SERIALIZABLE isolation level.
⚠ Critical Configuration Requirements
1. Shared GlobalTransactionCoordinator (REQUIRED)
For SERIALIZABLE isolation to work correctly across connections, you MUST share a single GlobalTransactionCoordinator instance among all PostgresHandler instances.
❌ INCORRECT (Per-Connection Coordinator)
use heliosdb_protocols::postgres::PostgresHandler;use tokio::net::TcpListener;
#[tokio::main]async fn main() { let listener = TcpListener::bind("127.0.0.1:5432").await.unwrap();
loop { let (stream, _) = listener.accept().await.unwrap();
// ❌ WRONG: Each handler gets its own coordinator! let handler = PostgresHandler::new(stream); // This will show a WARNING log
tokio::spawn(async move { handler.run().await.unwrap(); }); }}Problem: Each connection has an isolated coordinator, so cross-connection conflicts are never detected. Two transactions on different connections can both modify the same row and both commit successfully (violating serializability).
CORRECT (Shared Coordinator)
use heliosdb_protocols::postgres::{PostgresHandler, GlobalTransactionCoordinator};use std::sync::Arc;use tokio::net::TcpListener;
#[tokio::main]async fn main() { let listener = TcpListener::bind("127.0.0.1:5432").await.unwrap();
// Create ONE coordinator for the entire server let coordinator = Arc::new(GlobalTransactionCoordinator::new());
loop { let (stream, _) = listener.accept().await.unwrap();
// CORRECT: Share the coordinator with all handlers let handler = PostgresHandler::new(stream) .with_global_txn_coordinator(Arc::clone(&coordinator)); // This will show a SUCCESS log
tokio::spawn(async move { handler.run().await.unwrap(); }); }}Result: All connections share the same coordinator, enabling proper cross-connection conflict detection.
2. Complete Example with Storage
use heliosdb_protocols::postgres::{PostgresHandler, GlobalTransactionCoordinator};use heliosdb_storage::LsmStorageEngine;use std::sync::Arc;use tokio::net::TcpListener;
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { // Initialize storage engine let storage = Arc::new(LsmStorageEngine::new("/tmp/heliosdb_data")?);
// Create shared global transaction coordinator (CRITICAL!) let coordinator = Arc::new(GlobalTransactionCoordinator::new());
// Bind server let listener = TcpListener::bind("127.0.0.1:5432").await?; println!("HeliosDB listening on 127.0.0.1:5432");
// Accept connections loop { let (stream, addr) = listener.accept().await?; println!("New connection from {}", addr);
// Clone Arc references for this connection let storage_clone = Arc::clone(&storage); let coordinator_clone = Arc::clone(&coordinator);
// Create handler with shared components let handler = PostgresHandler::new(stream) .with_storage(storage_clone) .with_global_txn_coordinator(coordinator_clone);
// Spawn connection handler tokio::spawn(async move { if let Err(e) = handler.run().await { eprintln!("Connection error: {}", e); } }); }}Transaction Isolation Levels
HeliosDB supports four PostgreSQL-compatible isolation levels:
| Isolation Level | Snapshot Created? | Reads Tracked? | Writes Tracked? | Conflict Detection | Overhead |
|---|---|---|---|---|---|
| READ UNCOMMITTED | ❌ No | ❌ No | ❌ No | None | ~0% |
| READ COMMITTED | ❌ No | ❌ No | ❌ No | None | ~0% |
| REPEATABLE READ | Yes | ❌ No | ❌ No | None | <1% |
| SERIALIZABLE | Yes | ⚠ Partial | Yes | Full | 2-5% |
Usage Examples
READ COMMITTED (Default)
-- Default isolation levelBEGIN TRANSACTION;-- orBEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- Non-repeatable reads possibleSELECT balance FROM accounts WHERE id = 1; -- Returns 100-- (Another transaction updates balance to 200)SELECT balance FROM accounts WHERE id = 1; -- May return 200
COMMIT;REPEATABLE READ
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- Repeatable reads guaranteedSELECT balance FROM accounts WHERE id = 1; -- Returns 100-- (Another transaction updates balance to 200)SELECT balance FROM accounts WHERE id = 1; -- Still returns 100 (snapshot)
-- Write conflicts NOT detected (both can commit)UPDATE accounts SET balance = balance + 10 WHERE id = 1;
COMMIT;SERIALIZABLE
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- Strict serializabilityUPDATE accounts SET balance = balance + 10 WHERE id = 1;
-- If another transaction modifies id=1 concurrently:COMMIT; -- May fail with "serialization_failure"⚠ Known Limitations (Gap #1)
SELECT Queries Don’t Track Reads
Current Limitation: SELECT statements do NOT track reads in the transaction_read_set, which means read-write conflicts involving SELECT are NOT detected in SERIALIZABLE transactions.
Problem Scenario
-- Transaction 1 (Connection 1)BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;SELECT balance FROM accounts WHERE id = 1; -- Returns 100-- ⚠ READ NOT TRACKED!
-- Transaction 2 (Connection 2)BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;UPDATE accounts SET balance = 200 WHERE id = 1;COMMIT; -- Succeeds
-- Transaction 1 continuesINSERT INTO audit_log (message) VALUES ('balance was 100');COMMIT; -- ❌ BUG: Should fail but succeeds!Expected Behavior: Transaction 1 should abort with serialization_failure because it read data that was later modified.
Actual Behavior: Both transactions commit successfully (INCORRECT).
Workarounds
Option 1: Use SELECT FOR UPDATE (Recommended)
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;-- SELECT FOR UPDATE tracks the read as a writeSELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
-- Now conflicts will be detected-- If another transaction updates id=1, this will fail at COMMITOption 2: Use REPEATABLE READ Instead
-- If full serializability isn't required, use REPEATABLE READBEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;SELECT balance FROM accounts WHERE id = 1; -- Snapshot-consistent
-- You get consistent reads but no conflict detectionOption 3: Use UPDATE to Force Tracking
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;-- Dummy UPDATE to force trackingUPDATE accounts SET balance = balance WHERE id = 1;SELECT balance FROM accounts WHERE id = 1;
-- Now the row is tracked in write_setWhy This Limitation Exists
The execute_select method uses a high-level query executor abstraction (heliosdb_compute::QueryExecutor) that doesn’t provide key-level visibility. Fixing this requires:
- Parser changes: Expose predicates for SELECT queries (like UPDATE/DELETE)
- Storage-backed execution: Direct storage access with key tracking
- Query executor instrumentation: Add read tracking callbacks to compute layer
Status: Mitigation (runtime warnings) implemented. Full fix planned for Phase 2.
See: docs/MVCC_SSI_GAP_ANALYSIS.md for detailed analysis.
Conflict Detection
What Gets Detected
HeliosDB’s SSI implementation detects:
Write-Write Conflicts (UPDATE/DELETE):
-- T1: UPDATE accounts SET balance = balance + 10 WHERE id = 1;-- T2: UPDATE accounts SET balance = balance - 5 WHERE id = 1;-- Result: Second COMMIT fails with serialization_failureRead-Write Conflicts (UPDATE/DELETE):
-- T1: UPDATE accounts SET balance = balance + 10 WHERE id = 1; (reads then writes)-- T2: UPDATE accounts SET balance = balance - 5 WHERE id = 1;-- Result: Second COMMIT fails with serialization_failure❌ Read-Write Conflicts (SELECT + UPDATE): NOT DETECTED (Gap #1)
-- T1: SELECT balance FROM accounts WHERE id = 1;-- T2: UPDATE accounts SET balance = 200 WHERE id = 1; COMMIT;-- T1: COMMIT;-- Result: Both succeed (INCORRECT!)Conflict Resolution Strategy
HeliosDB uses First-Committer-Wins:
- The first transaction to COMMIT succeeds
- Later transactions detect conflicts and abort with
serialization_failure - Matches PostgreSQL behavior
Example: Preventing Lost Updates
-- Account starts with balance = 100
-- Transaction 1 (starts at timestamp 100)BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;UPDATE accounts SET balance = balance + 50 WHERE id = 1;-- Read set: {accounts:1}-- Write set: {accounts:1}
-- Transaction 2 (starts at timestamp 101)BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;UPDATE accounts SET balance = balance - 30 WHERE id = 1;-- Read set: {accounts:1}-- Write set: {accounts:1}COMMIT; -- Succeeds (first committer, timestamp 102)
-- Transaction 1 tries to commitCOMMIT;-- ❌ Fails with:-- ERROR: could not serialize access due to concurrent update on key: accounts:1-- DETAIL: Transaction 2 modified accounts:1 (commit_ts: 102) after our snapshot (ts: 100)
-- Final balance = 70 (100 - 30)-- Transaction 1's +50 update is rejectedPerformance Characteristics
Overhead by Isolation Level
| Operation | READ COMMITTED | REPEATABLE READ | SERIALIZABLE |
|---|---|---|---|
| BEGIN | <1μs | +50μs (snapshot) | +50μs |
| SELECT | 0% overhead | 0% overhead | ⚠ 0% (Gap #1) |
| INSERT | 0% overhead | 0% overhead | +100ns/row |
| UPDATE | 0% overhead | 0% overhead | +150ns/row |
| DELETE | 0% overhead | 0% overhead | +150ns/row |
| COMMIT | <1μs | +10μs | +1-5ms |
Memory Usage (SERIALIZABLE)
- Per transaction: O(R + W) where R = keys read, W = keys written
- Typical small transaction: 10-100 keys = ~1-10 KB
- Global coordinator: ~10,000 recent commits = ~500 KB - 5 MB
When to Use Each Isolation Level
READ COMMITTED (Default)
- Use when: High throughput required, occasional non-repeatable reads acceptable
- Examples: Analytics queries, reporting, read-heavy workloads
- Performance: Best (0% overhead)
REPEATABLE READ
- Use when: Consistent snapshot needed, but write conflicts acceptable
- Examples: Report generation, data exports, long-running reads
- Performance: Near-zero overhead (<1%)
SERIALIZABLE
- Use when: Strict consistency required, lost updates unacceptable
- Examples: Financial transactions, inventory management, concurrent updates
- Performance: 2-5% overhead, potential for serialization_failure retries
- ⚠ Limitation: SELECT conflicts not detected (Gap #1)
Error Handling
Serialization Failures
When a conflict is detected, you’ll receive:
ERROR: could not serialize access due to concurrent update on key: accounts:1SQLSTATE: 40001 (serialization_failure)Recommended Handling:
use heliosdb_protocols::postgres::PgError;
async fn transfer_money(from: i64, to: i64, amount: i64) -> Result<(), Box<dyn std::error::Error>> { let mut retries = 0; const MAX_RETRIES: u32 = 3;
loop { match attempt_transfer(from, to, amount).await { Ok(_) => return Ok(()), Err(e) if is_serialization_failure(&e) && retries < MAX_RETRIES => { retries += 1; // Exponential backoff tokio::time::sleep(Duration::from_millis(10 * 2_u64.pow(retries))).await; continue; } Err(e) => return Err(e), } }}
fn is_serialization_failure(error: &PgError) -> bool { error.sql_state() == SqlState::SerializationFailure}Monitoring and Debugging
Log Messages
When properly configured, you’ll see:
[INFO] Using shared GlobalTransactionCoordinator (correct setup for cross-connection serializability)[INFO] Registered transaction 12345 with global coordinator (start_ts: 1000)[INFO] Registered commit for transaction 12345 with global coordinator (commit_ts: 1005)When misconfigured:
[WARN] ⚠ CRITICAL: Creating per-connection GlobalTransactionCoordinator. This BREAKS cross-connection serializability! For SERIALIZABLE isolation to work correctly across connections, you MUST create a shared coordinator...When using SELECT in SERIALIZABLE:
[WARN] ⚠ CRITICAL GAP: SELECT query in SERIALIZABLE transaction does NOT track reads! Read-write conflicts will NOT be detected. This violates serializability. Use SELECT FOR UPDATE as a workaround...Debugging Conflicts
Enable debug logging to see conflict detection:
use tracing_subscriber;
tracing_subscriber::fmt() .with_max_level(tracing::Level::DEBUG) .init();You’ll see detailed logs:
[DEBUG] Checking for write conflicts (Serializable isolation)[DEBUG] Read set size: 3, Write set size: 2[DEBUG] Checking conflicts for snapshot timestamp 1000[DEBUG] Found 2 transactions committed since our snapshot[WARN] Read-write conflict detected: txn 12345 read key 'accounts:1', but txn 12346 modified it (commit_ts: 1002)Testing
Unit Test Example
#[tokio::test]async fn test_cross_connection_conflict_detection() { use heliosdb_protocols::postgres::{PostgresHandler, GlobalTransactionCoordinator}; use std::sync::Arc;
// Create shared coordinator let coordinator = Arc::new(GlobalTransactionCoordinator::new());
// Create two handlers sharing the coordinator let handler1 = PostgresHandler::new(stream1) .with_global_txn_coordinator(Arc::clone(&coordinator)); let handler2 = PostgresHandler::new(stream2) .with_global_txn_coordinator(Arc::clone(&coordinator));
// Transaction 1: Update account 123 handler1.execute("BEGIN SERIALIZABLE").await?; handler1.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 123").await?;
// Transaction 2: Also update account 123 handler2.execute("BEGIN SERIALIZABLE").await?; handler2.execute("UPDATE accounts SET balance = balance - 50 WHERE id = 123").await?; handler2.execute("COMMIT").await?; // First commit succeeds
// Transaction 1: Try to commit let result = handler1.execute("COMMIT").await; assert!(result.is_err()); // Second commit fails assert_eq!(result.unwrap_err().sql_state(), SqlState::SerializationFailure);}Migration from PostgreSQL
If migrating from PostgreSQL, HeliosDB’s MVCC behavior is compatible with one exception (Gap #1):
Compatible Behavior
Same isolation levels Same conflict detection for UPDATE/DELETE Same first-committer-wins semantics Same error codes (40001 for serialization_failure)
Differences
⚠ SELECT in SERIALIZABLE doesn’t track reads (Gap #1)
- PostgreSQL: Full SSI including SELECT
- HeliosDB: SELECT not tracked (partial SSI)
- Workaround: Use SELECT FOR UPDATE
References
- Gap Analysis: docs/MVCC_SSI_GAP_ANALYSIS.md - Detailed analysis of known limitations
- Implementation Details:
- docs/TRANSACTION_SNAPSHOT_LIFECYCLE.md - Week 3 implementation
- docs/WRITE_CONFLICT_DETECTION.md - Week 4 implementation
- docs/WEEK6_GRANULAR_TRACKING_INTEGRATION.md - Week 6 implementation
- PostgreSQL SSI: https://www.postgresql.org/docs/current/transaction-iso.html
- Academic Paper: “Serializable Snapshot Isolation in PostgreSQL” (Ports & Grittner, 2012)
Support
For questions or issues related to MVCC:
- Check the gap analysis document for known limitations
- Review log messages for configuration warnings
- Enable DEBUG logging for conflict detection details
- File issues at: https://github.com/heliosdb/heliosdb/issues
Version History
- v5.4.0: Initial MVCC SSI implementation (Weeks 1-6)
- v5.4.1: Gap mitigations added (runtime warnings)
- v5.5.0: (Planned) Full SELECT tracking support