Overview
ELIPS provides strong guarantees around data persistence and atomicity, allowing you to tune performance versus crash safety according to your application's needs. With the introduction of the F1-F4 storage engine updates, Python developers now have access to robust Write-Ahead Log (WAL) behaviors, reliable undo logs, and secure cross-process access controls.
Durability Levels
Durability levels determine how aggressively ELIPS forces the operating system to flush writes to physical media. You can configure this using the durability() builder on the Config object:
- paranoid:
F_FULLFSYNCon macOS,fdatasyncon Linux before every acknowledgment. Provides maximum crash safety, suitable for financial or medical data where every single write must survive power loss. - standard: OS-synced writes (fsync at checkpoint). This is the default setting and is suitable for most production uses.
- relaxed: Writes directly to the OS page cache. Data will survive a process crash but not an OS crash or power loss. Ideal for staging or batch ingest pipelines where you can afford to re-ingest data if a system goes down.
- ephemeral: No persistence at all (in-memory mode, no WAL). Suitable for unit tests, CI, and in-process search over preloaded data.
import elips
# paranoid = strongest durability (sync every ack)
config = elips.Config().durability("paranoid")
# standard = default
config = elips.Config().durability("standard")
# relaxed = throughput-optimized (page cache)
config = elips.Config().durability("relaxed")
# ephemeral = no persistence
config = elips.Config().durability("ephemeral")You can inspect the currently active durability via properties on the config object: config.durability_enum and config.durability_val.
# Throughput-optimized (relaxed)
config = elips.Config().dimension(128).durability("relaxed")
db = elips.open_with_config("/data/staging", config)
# In-memory (no WAL at all)
db = elips.open(":memory:", dimension=128)
print(db.persistent) # FalseStorage Improvements (F1-F4)
The F1 through F4 patches significantly hardened the ELIPS storage engine, resolving critical issues around atomicity and corruption recovery. It is important for users to understand what these fixes guarantee:
- F1: True Sync Guarantees. The Write-Ahead Log (WAL) now correctly fsyncs (or
F_FULLFSYNCon macOS) before returning from an append. Additionally, snapshot, segment, and manifest writes are published via a durable rename, which syncs both the file and directory metadata. Previously, acknowledged writes could be lost on OS crash or power loss even under the "standard" durability setting. - F2: Bounded Allocations. Length-prefixed payloads (like strings or vectors) are now bounded by the remaining stream length before allocation. This means a corrupt WAL can no longer cause the system to attempt unbounded memory allocations.
- F3: Faster Recovery. WAL replay time was optimized from
O(n*k)toO(n)by avoiding unnecessary tail-copying per record. - F4: Robust Atomicity. The
commit()process now pre-checks writability and maintains an undo log. A failed WAL write automatically restores the prior state. Commits on a read-only vault throw an error before applying anything. Furthermore, the WAL brackets batches withtxn_beginandtxn_commit, ensuring that replay completely drops unterminated transaction windows.
Transaction Semantics
Following the F4 updates, transactions are strictly all-or-nothing:
- All-or-nothing: A failure partway through a transaction uses the undo log to revert any operations that were already applied.
- WAL framing: Transactions are bracketed with
txn_beginandtxn_commitmarkers in the WAL. If a process dies mid-commit, the replay engine will drop the unterminated window. - Pre-checks:
commit()verifies that a vault is not read-only before attempting to apply operations. - Safe Rollbacks: Calling
rollback()is always safe, even after a failedcommit(), since the state will have already been restored by the undo log.
Transaction Examples
import elips
# Configure maximum durability
config = (
elips.Config()
.dimension(384)
.metric("cosine")
.durability("paranoid")
)
db = elips.open_with_config("/data/critical", config)
# Transaction - all or nothing using a context manager
with db.begin_transaction() as txn:
v = txn.vault("docs")
v.place([1.0, 0.0], {"title": "A"})
v.place([0.0, 1.0], {"title": "B"})
# Clean exit: both writes committed atomically
# Exception: both writes rolled back
# Failed commit restores state automatically
txn = db.begin_transaction()
v = txn.vault("docs")
v.place([1.0, 0.0], {"title": "C"})
try:
txn.commit()
except elips.StorageError:
# State restored. Safe to retry or rollback explicitly.
txn.rollback() # Also safe; state already restoredAccess Modes & Locking
ELIPS uses POSIX advisory flock to manage cross-process locking. There are two primary modes for opening a database:
- read_write (default): Acquires an exclusive writer lock. Only one process can open the database in this mode at a time.
- read_only: Acquires a shared advisory lock. Multiple processes can open the database in
read_onlymode. Write operations (like place, erase, rebuild, or compaction) will raise aStorageError.
If a second process attempts to open a database in read_write mode while another process already holds the exclusive lock, ELIPS raises a LockConflict exception. To read from a database while it is being written to by another process, use read_only mode.
Read-Only Serving
The shared-reader pattern allows you to scale read traffic without interfering with a single writer process.
# Read-only serving (shared readers)
# A separate process might hold the exclusive writer lock
reader = elips.open("/data/critical", access_mode="read_only")
results = reader.vault("docs").seek([1.0, 0.0], top=5)Monitoring Durability
You can inspect operations that are currently in the WAL but not yet checkpointed using vault.pending_writes(). This returns a typed list of WalRecord objects, which is extremely useful for monitoring durability lag and deciding when to force a checkpoint.
Crash Recovery
Recovery after an unclean shutdown happens automatically. When you call elips.open(), ELIPS replays the WAL to recover any committed transactions that were not yet checkpointed.
# Simulate unclean shutdown by abandoning the handle
db.abandon() # Leaves WAL on disk, bypassing clean shutdown
# Next open() replays the WAL automatically
db2 = elips.open("/data/critical")Common Pitfalls
- LockConflict in multiprocessing: Remember that only one process can have a
read_writehandle. Useread_onlyfor read replicas in secondary processes. - Unnecessary durability: Using
paranoiddurability introduces significant latency (often 10-20ms per transaction due to syncing physical media). Only use it if losing a single write during power loss is unacceptable. - Leaving transactions open: Long-running transactions delay checkpointing. Keep transactions scoped tightly around the required batch of operations.
Learn more: Check out the Transaction Engine, Storage format, and Lock Manager documentation.