Guides

Concurrency & cancellation

Share one handle safely, understand per-call consistency, coordinate read-modify-write work, and shut down deliberately.

Share one handle per directory

Open the database during application startup and pass the handle to repositories or workers. Do not open/close it per request. The OS lock prevents another handle/process from owning the same directory.

One handle is safe for concurrent goroutines. Commits, flushes, and close acquire the exclusive mutex; lookups, scans, and stats use its shared side. Readers wait for synchronous checkpointing and vice versa.

Consistency boundaries

PatternGuaranteed behavior
One Get / GetRecordA committed record or an error
One ScanA consistent committed collection range
One WriteBatchAtomic validation/publication of that batch
Two Get callsMay observe different commits
Several scan pagesIndependent snapshots
Get then PutNo atomic read-modify-write guarantee
Read Version then writeNo expected-version enforcement

The engine does not expose persistent snapshots or label arbitrary application transactions serializable.

Read-modify-write coordination

Use an application mutex around the complete logical operation, and make every code path that mutates the affected invariant use the same coordination:

mu.Lock()
defer mu.Unlock()

value, err := db.Get(ctx, "counters", []byte("jobs"))
// Decode, validate, and calculate under the same application lock.
// Treat ErrNotFound according to the application's initial-value policy.
_ = value
_ = err

This sketch is not an engine transaction callback. A mutex only coordinates callers that honor it. Other code directly using the shared DB can bypass your application invariant. Keep ownership behind a repository boundary when needed.

Use one WriteBatch for the resulting primary/index mutations so their publication is atomic. The pre-read and the commit are coordinated by the application lock, not by Version.

Contexts and deadlines

Pass a non-nil context to context-aware methods. Use a request context for work that should stop before it begins and an application context for background jobs.

ctx, cancel := context.WithTimeout(parent, 2*time.Second)
defer cancel()
value, err := db.Get(ctx, "users", []byte("1024"))

Import context and time. Context cancellation does not interrupt mutex waits or OS I/O. A request can exceed its deadline while waiting for a lock, then return cancellation when it reaches a check.

Once WAL append or checkpoint publication starts, cancellation cannot undo persistent work. Do not interpret a deadline as proof that no mutation exists. An I/O failure after append can be indeterminate even without cancellation.

Open, Stats, and Close do not accept a context. Plan their lifecycle accordingly.

Avoid carrying work out of a read call

Returned slices belong to the caller and can be changed without changing the DB. The input slices are copied by a write, but callers must not concurrently mutate arguments while the API is still reading them.

A scan materializes bounded results before returning; it is not a lazy cursor retaining locks. Decode expensive application payloads after the API call rather than assuming it exposes a transaction/iterator callback.

Shutdown order

  1. Stop admitting application work.
  2. Cancel background producers where appropriate.
  3. Wait for all workers using the DB to finish.
  4. Call Close and preserve its error.

An explicit Flush before close is optional, not a substitute for stopping writers. Durable commits already rely on the WAL. Closing does sync pending WAL data in either mode.

Use the worker lifecycle recipe for a complete bounded-concurrency example.

Something to improve?Edit this page on GitHub