Skip to content

Concurrency

Tom Laird-McConnell edited this page May 24, 2026 · 1 revision

Concurrency

LottaDB provides optimistic concurrency via ETags and a safe read-modify-write primitive via ChangeAsync. Per-key striped locking ensures table writes and Lucene index updates are atomic.

ETag-based optimistic concurrency

Every object returned from LottaDB has an ETag attached automatically (see Object Metadata). When you save an object that has an ETag, LottaDB performs a conditional write -- if another writer modified the entity since your read, the write fails with ConcurrencyException.

var actor = await db.GetAsync<Actor>("alice");
actor.DisplayName = "Alice Updated";

// Conditional write -- throws ConcurrencyException if another writer changed the entity
await db.SaveAsync(actor);

New objects (no ETag) are saved as unconditional upserts:

var doc = new Actor { Username = "bob", DisplayName = "Bob" }
await db.SaveAsync(doc);
var key = doc.GetKey();
var etag = doc.GetETag();
var json = doc.GetJSON();

ConcurrencyException

Thrown when a conditional save fails because the entity was modified by another writer:

try
{
    await db.SaveAsync(actor);
}
catch (ConcurrencyException ex)
{
    // ex.Key -- the entity key
    // ex.EntityType -- the CLR type
    // Re-read and retry, or return 409 Conflict to the client
}

ChangeAsync -- safe read-modify-write

ChangeAsync<T>() reads the current entity, applies a mutation, and saves with ETag concurrency. If another writer sneaks in between the read and write, it re-reads and retries automatically (up to 50 attempts).

Two overloads:

// Action overload -- mutate in place
await db.ChangeAsync<Actor>(key, a => a.FollowerCount++);

// Func overload -- return a (possibly new) object
await db.ChangeAsync<Actor>(key, a =>
{
    var newObj = new Actor() { ... };
    return newObj;
});

The mutation function may be invoked more than once on retry. It must be a pure function of its input -- don't capture external mutable state.

Best practices

Scenario Use
Create a new object SaveAsync (no ETag = unconditional upsert)
Overwrite regardless of current state SaveAsync on a new object instance (no ETag)
Read-modify-write ChangeAsync (handles retries automatically)
Detect conflicts in a web API SaveAsync with client-provided ETag via SetETag()
Parallel counter increments ChangeAsync (per-key locking prevents lost updates)

When to use SaveAsync vs. ChangeAsync

Use SaveAsync when you're creating new objects or when you already have the latest version and want to detect conflicts (e.g., web API with If-Match headers).

Use ChangeAsync when you need to read the current state, modify it, and write it back. It handles the retry loop so you don't have to.

// Bad -- manual retry loop, easy to get wrong
while (true)
{
    var actor = await db.GetAsync<Actor>("alice");
    actor.FollowerCount++;
    try { await db.SaveAsync(actor); break; }
    catch (ConcurrencyException) { continue; }
}

// Good -- ChangeAsync does this for you
await db.ChangeAsync<Actor>("alice", a => a.FollowerCount++);

Clone this wiki locally