Persistence
Persistence
Stoolap provides durable storage through a combination of Write-Ahead Logging (WAL) and periodic snapshots. This architecture ensures data durability while maintaining high performance.
Overview
Stoolap’s persistence layer consists of two main components:
- Write-Ahead Log (WAL): Records all changes before they’re applied to memory
- Snapshots: Periodic full copies of the database state
This dual approach provides:
- Durability: Changes are persisted before acknowledgment
- Fast Recovery: Snapshots reduce recovery time
- Crash Safety: WAL ensures no committed transactions are lost
Enabling Persistence
To enable persistence, use a file:// connection string:
use stoolap::Database;
// In-memory only (no persistence)
let db = Database::open("memory://")?;
// With disk persistence
let db = Database::open("file:///path/to/database")?;
Command line:
# In-memory
stoolap
# With persistence
stoolap --db "file:///path/to/database"
Write-Ahead Log (WAL)
How WAL Works
- When a transaction commits, changes are first written to the WAL file
- The WAL is synced to disk (based on sync_mode)
- Changes are then applied to the in-memory structures
- Transaction is acknowledged to the client
This sequence ensures that committed transactions survive crashes.
WAL Operations
The WAL records these operations:
- INSERT: New row insertions
- UPDATE: Row modifications
- DELETE: Row deletions
- CREATE TABLE: Table creation (DDL)
- DROP TABLE: Table deletion (DDL)
- CREATE INDEX: Index creation (DDL)
- DROP INDEX: Index deletion (DDL)
WAL Configuration
Configure WAL behavior using PRAGMA:
-- Sync mode: 0=None, 1=Normal (default), 2=Full
PRAGMA sync_mode = 1;
-- Number of operations before automatic WAL flush
PRAGMA wal_flush_trigger = 32768;
| Sync Mode | Value | Behavior |
|---|---|---|
| None | 0 | No sync (fastest, but data may be lost on crash) |
| Normal | 1 | Sync on commit (balanced performance and durability) |
| Full | 2 | Sync every operation (slowest, maximum durability) |
WAL Files
WAL files are stored in the database directory:
/path/to/database/
wal/
wal_000001.log
wal_000002.log
...
Old WAL files are automatically cleaned up after successful snapshots.
Snapshots
How Snapshots Work
Snapshots capture the complete database state at a point in time:
- All table data and schema
- All index definitions
- Current transaction state
After a snapshot is created, older WAL entries can be safely deleted.
Snapshot Configuration
-- Interval between automatic snapshots (in seconds, default: 300)
PRAGMA snapshot_interval = 300;
-- Number of snapshots to retain (default: 5)
PRAGMA keep_snapshots = 5;
-- Manually create a snapshot
PRAGMA create_snapshot;
Snapshot Files
Snapshots are stored as binary files:
/path/to/database/
snapshots/
snapshot_1704067200.bin
snapshot_1704067500.bin
...
The filename includes the timestamp of creation.
Recovery Process
When opening a database, Stoolap performs recovery automatically:
- Load Latest Snapshot: Restore base state from most recent snapshot
- Replay WAL: Apply any operations logged after the snapshot
- Verify Consistency: Ensure all data structures are valid
- Ready for Operations: Database is now available
Recovery is transparent to the application.
Recovery Example
// Opening automatically triggers recovery if needed
let db = Database::open("file:///path/to/database")?;
// Database is ready with all committed data restored
let results = db.query("SELECT * FROM users")?;
Configuration Options
Connection String Parameters
file:///path/to/database?sync_mode=2&snapshot_interval=60&keep_snapshots=3
| Parameter | Description | Default |
|---|---|---|
| sync_mode | WAL sync mode (0, 1, 2) | 1 |
| snapshot_interval | Seconds between snapshots | 300 |
| keep_snapshots | Number of snapshots to keep | 5 |
PRAGMA Commands
-- Read current settings
PRAGMA sync_mode;
PRAGMA snapshot_interval;
PRAGMA keep_snapshots;
PRAGMA wal_flush_trigger;
-- Modify settings
PRAGMA sync_mode = 2;
PRAGMA snapshot_interval = 60;
PRAGMA keep_snapshots = 3;
PRAGMA wal_flush_trigger = 10000;
-- Manually trigger snapshot
PRAGMA create_snapshot;
Best Practices
Durability vs Performance
Choose sync_mode based on your requirements:
| Use Case | Recommended sync_mode |
|---|---|
| Development/Testing | 0 (None) |
| General Use | 1 (Normal) |
| Financial/Critical Data | 2 (Full) |
Snapshot Frequency
- Frequent snapshots (low interval): Faster recovery, more disk I/O
- Infrequent snapshots (high interval): Slower recovery, less disk I/O
For databases with high write rates, consider shorter intervals.
Disk Space
Monitor disk usage:
- WAL files grow until the next snapshot
- Old snapshots are retained based on
keep_snapshots - Plan for peak WAL size between snapshots
Backup Strategy
For backups:
- Create a manual snapshot:
PRAGMA create_snapshot; - Copy the entire database directory while the database is idle
- For hot backups, use filesystem snapshots (ZFS, LVM)
Directory Structure
A persistent database creates this directory structure:
/path/to/database/
db.lock # Lock file for single-writer
wal/
wal_NNNNNN.log # WAL segment files
snapshots/
snapshot_TIMESTAMP.bin # Snapshot files
Error Handling
Corrupt WAL
If WAL corruption is detected during recovery:
- Stoolap attempts to recover up to the last valid entry
- Corrupted entries at the end are discarded
- A warning is logged
Disk Full
If disk becomes full:
- WAL writes will fail
- Transactions will be rolled back
- Free disk space before continuing
Lock Contention
Only one process can open a database directory:
- A lock file (
db.lock) prevents concurrent access - If a previous process crashed, the lock is automatically released on open
Example: Complete Configuration
use stoolap::Database;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Open with custom persistence settings
let db = Database::open(
"file:///var/lib/myapp/data?sync_mode=2&snapshot_interval=120"
)?;
// Fine-tune at runtime
db.execute("PRAGMA wal_flush_trigger = 5000")?;
db.execute("PRAGMA keep_snapshots = 7")?;
// Your application logic...
db.execute("CREATE TABLE events (id INTEGER PRIMARY KEY AUTO_INCREMENT, data JSON)")?;
// Force a snapshot before maintenance
db.execute("PRAGMA create_snapshot")?;
Ok(())
}