elips/docs
Internals

Lock manager

LockManager enforces ELIPS' single-writer / many-reader contract for on-disk databases through a POSIX advisory file lock. It acquires an exclusive, non-blocking lock at open time and holds it for the lifetime of the ElipsInstance.

Overview

A persistent database directory contains a LOCK file. The first read-write opener takes an exclusive flock(LOCK_EX | LOCK_NB); subsequent writers fail fast with LockConflict. Read-only opens take a shared lock and may coexist with other readers. There is no background coordination — no daemon, no thread pool, just a file descriptor.

Class shape

cpp
// include/elips/kernel/LockManager.hpp
namespace elips {

class LockConflict : public ElipsError {
public:
    using ElipsError::ElipsError;
};

class LockManager {
public:
    explicit LockManager(const std::string& lock_path);  // acquires
    ~LockManager();                                       // releases

    LockManager(const LockManager&)            = delete;
    LockManager& operator=(const LockManager&) = delete;
    LockManager(LockManager&&) noexcept;                  // movable
    LockManager& operator=(LockManager&&)      = delete;

private:
    int fd_{-1};
};

} // namespace elips

Locking semantics

cpp
LockManager::LockManager(const std::string& lock_path) {
    fd_ = ::open(lock_path.c_str(), O_RDWR | O_CREAT, 0644);
    if (fd_ < 0) {
        throw StorageError{"cannot open lock file: " + lock_path};
    }
    if (::flock(fd_, LOCK_EX | LOCK_NB) != 0) {
        ::close(fd_);
        fd_ = -1;
        throw LockConflict{"database is already open by another writer: " + lock_path};
    }
}

LockManager::~LockManager() {
    if (fd_ >= 0) {
        ::flock(fd_, LOCK_UN);
        ::close(fd_);
    }
}
  • Path<db_path>/LOCK. The file is a 0-byte target for flock(2); no data is written.
  • LOCK_EX — exclusive lock. No other process can hold any lock on the file simultaneously.
  • LOCK_NB — non-blocking. Conflicting calls return immediately with EWOULDBLOCK, not a hang.
  • Release on close — POSIX releases every flock held on a file when any descriptor for it is closed.
  • Release on process exit — the kernel cleans up file descriptors, so a crashed writer is automatically unlocked.

Use in open()

cpp
std::unique_ptr<ElipsInstance> open(const std::string& path, const Config& config) {
    // ... path validation ...
    LockManager lock{(fs::path(path) / lock_file).string()};   // single-writer
    // ... identity, snapshot load, WAL replay ...
    auto instance = std::make_unique<ElipsInstance>(path, effective,
                                                    /*persistent=*/true,
                                                    std::move(lock));
    return instance;
}

The lock is created on the stack inside open(), moved into ElipsInstance, and released when the instance is destroyed or close() is called. If any step between acquisition and instance construction throws, stack unwinding releases the lock — there is no leakage path.

RAII guarantees

ScenarioLock state
Normal open → closeAcquired on open, released on close
ElipsInstance destructorReleased via ~LockManager() → flock(LOCK_UN)
Process crash / SIGKILLReleased by the kernel
Exception during open() before instance constructedReleased via stack unwinding
In-memory database (":memory:")No lock created (no filesystem path)

Cross-platform notes

  • POSIX (macOS, Linux) — current implementation, BSD-style flock(2). Lock follows the file descriptor.
  • Windows — planned via LockFileEx with LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY. Path handling differs.

Errors

std::runtime_errorstdelips::ElipsErrorevery elips throw inherits thisLockConflictStorageError
Every lock failure surfaces as a typed ElipsError descendant — never a bare std::runtime_error.
python
try:
    db = elips.open("path/to/db")
except elips.LockConflict as e:
    print(f"Database is locked by another process: {e}")
except elips.StorageError as e:
    print(f"Storage error: {e}")

Related: Architecture · Transaction engine.