Skip to content

Embedded dual-layer persistent store new

A lightweight, disk-persisted key/value store for the server/HOST's occasionally-referenced, long-lived data (profiles, inventories, session/world metadata). It is self-contained (no external DB) and built on the same worker job pool as the web fetch, so every disk touch runs off the fixed-tick main thread and its results are delivered on the tick thread inside lattice_runner_tick() — the pump never blocks on disk.

Architecture (CQRS + cache-aside):

flowchart LR
  G["lattice_store_get"] -->|memcache HIT| HIT[return same tick<br/>from_cache=1]
  G -->|miss| RD[(durable READ store<br/>replica)]
  P["lattice_store_put / delete"] --> WR[(durable WRITE store<br/>source of truth, fsync'd)]
  WR -->|sync per-key / async-batch| RD
  WR -->|update or evict| MC[in-memory LRU<br/>memcache]
  MC -.front of.-> RD

A GET is a synchronous memcache hit (no disk) or an async read-store read. A PUT/DELETE persists to the write store, syncs the key to the read store, and updates-or-evicts dependent memcache entries — so a read-after-write is coherent (in the default per-key sync mode).

Threading contract

All lattice_store_* calls are main-thread (same thread as lattice_runner_tick). The on_store_result callback and lattice_store_poll only ever surface results on the tick thread.

lattice_store_open / close / configure

LATTICE_API lattice_result lattice_store_open(lattice_runner* r, const lattice_store_config* cfg);
LATTICE_API void           lattice_store_close(lattice_runner* r);
LATTICE_API lattice_result lattice_store_configure(lattice_runner* r, const lattice_store_config* cfg);

open opens the store under cfg->dir, replaying the on-disk logs (data persists across restart; the read replica is reconciled to the write store on reopen). close leaves the data on disk (writes were already fsync'd). configure reconfigures sync mode / write policy / memcache capacity in place (dir is fixed at open). Role: authority. Returns: LATTICE_OK or an error.

lattice_store_config

typedef struct {
    const char*                dir;             /* dir for write.log + read.log; CALLER must create it */
    lattice_store_sync_mode    sync_mode;
    lattice_store_write_policy write_policy;
    uint32_t                   memcache_capacity; /* 0 => a small default */
    double                     compact_ratio;     /* dead-byte fraction to compact; 0 => default */
    uint64_t                   compact_min_bytes; /* gate so a tiny log is never churned */
} lattice_store_config;

typedef enum {
    LATTICE_STORE_SYNC_PER_KEY     = 0, /* mirror on the SAME commit (read-after-write coherent) */
    LATTICE_STORE_SYNC_ASYNC_BATCH = 1  /* mirror in batches (eventual; reads may briefly lag)   */
} lattice_store_sync_mode;

typedef enum {
    LATTICE_STORE_CACHE_EVICT  = 0, /* drop the dependent entry; next read re-fetches (default) */
    LATTICE_STORE_CACHE_UPDATE = 1  /* overwrite a single-key dependent entry in place          */
} lattice_store_write_policy;
/* caller must ensure the dir exists */
lattice_store_config sc = {0};
sc.dir = "/var/lib/mygame/store";
sc.sync_mode = LATTICE_STORE_SYNC_PER_KEY;
sc.write_policy = LATTICE_STORE_CACHE_EVICT;
sc.memcache_capacity = 4096;
lattice_store_open(r, &sc);

lattice_store_get

LATTICE_API uint64_t lattice_store_get(lattice_runner* r, const char* key, int cache_on_read);

Async GET. cache_on_read (1/0): on a read-store read, whether to populate the memcache with the value (tagged with key, so a later write to it invalidates the entry). Returns: a non-zero handle; the result is delivered via on_store_result / lattice_store_poll. A memcache hit still returns a handle and delivers a result (from_cache = 1) — on the same tick, with no disk read.

lattice_store_put / delete

LATTICE_API uint64_t lattice_store_put(lattice_runner* r, const char* key,
                                       const uint8_t* value, uint32_t value_len);
LATTICE_API uint64_t lattice_store_delete(lattice_runner* r, const char* key);

put durably persists the value, syncs the read replica, and updates/evicts dependent cache entries. delete tombstones the key in the write store, mirrors to the read store, and evicts the cache. Returns: a non-zero handle; the result arrives later.

lattice_store_poll

LATTICE_API int lattice_store_poll(lattice_runner* r,
                                   uint64_t* out_handle, int* out_op, int* out_ok, int* out_found,
                                   int* out_from_cache, uint8_t* out_value, uint32_t out_value_cap,
                                   uint32_t* out_value_len);

Main thread: poll the next completed store op (callback-free alternative). Fills the out params (any may be NULL): *out_handle, *out_op (a lattice_store_op), *out_ok, *out_found, *out_from_cache. The GET value is copied into out_value up to out_value_cap and *out_value_len is the full value length. Returns: 1 if a result was dequeued, 0 if none ready. FIFO.

lattice_store_build_key

LATTICE_API uint32_t lattice_store_build_key(uint32_t func_id, uint64_t user_id,
                                             const uint64_t* params, uint32_t param_count,
                                             char* out, uint32_t cap);

Deterministic composite key builder. Builds a stable string key from a function id, a user id, and param_count params, so the same logical read derives a byte-identical key on any peer / any run. Writes up to cap bytes (including the NUL) into out and returns the full key length (excluding the NUL), which may exceed cap-1 (truncated but always NUL-terminated when cap > 0).

char key[128];
uint64_t params[] = { season_id, slot };
lattice_store_build_key(/*func_id*/ FN_INVENTORY, user_id, params, 2, key, sizeof key);
lattice_store_get(r, key, /*cache_on_read*/ 1);

Reacting — on_store_result

void (*on_store_result)(void* user_data, uint64_t handle, int op, int ok, int found,
                        int from_cache, const uint8_t* value, uint32_t value_len);

Fires synchronously inside lattice_runner_tick() on the main thread. handle is the value the call returned; op is a lattice_store_op (GET/PUT/DELETE); ok is 1 if the op completed without an I/O error; found is 1 if the key existed (a GET miss is ok=1, found=0; PUT is always found=1; DELETE reports whether the key had existed); from_cache is 1 if a GET was served from the memcache; value/value_len is the GET value (owned by the core, valid only for the call). A NULL pointer (the default) means "poll for results via lattice_store_poll instead".

typedef enum {
    LATTICE_STORE_GET    = 0,
    LATTICE_STORE_PUT    = 1,
    LATTICE_STORE_DELETE = 2
} lattice_store_op;

Introspection / metrics

LATTICE_API uint64_t lattice_store_read_store_reads(lattice_runner* r); /* real read-store reads done */
LATTICE_API uint64_t lattice_store_cache_hits(lattice_runner* r);
LATTICE_API uint64_t lattice_store_cache_misses(lattice_runner* r);
LATTICE_API uint32_t lattice_store_memcache_size(lattice_runner* r);
LATTICE_API uint64_t lattice_store_write_count(lattice_runner* r);    /* live keys in the write store */
LATTICE_API uint64_t lattice_store_read_count(lattice_runner* r);     /* live keys in the read store  */
LATTICE_API uint64_t lattice_store_compactions(lattice_runner* r);    /* write-store compactions run  */