Skip to content

MemStore — Async In-Process Cache ​

Aegon ships MemStore as a zero-dependency, async in-process alternative to Redis. It runs on its own dedicated thread (like Redis's single-threaded event loop) and exposes a fully async API: HTTP worker coroutines post requests and co_await results via Linux io_uring eventfd reads — the event loop is never blocked.

Use MemStore when:

  • You want caching without running an external Redis server.
  • You are building a single-node service, embedding Aegon, or writing tests.
  • You need deterministic local cache semantics (no network round-trips, no serialization).

For distributed multi-node deployments, use RedisCacheBackend instead.


Setup ​

cpp
#include <aegon/data/memory/MemStore.h>
#include <aegon/data/memory/MemStoreCacheBackend.h>

using namespace aegon::data::memory;

CMake target ​

cmake
target_link_libraries(my_app PRIVATE Aegon::memory)

Instantiation & Server Registration ​

cpp
// 1. Create the MemStore with optional configuration
//    The background thread starts automatically — no start() needed.
auto store = std::make_shared<MemStore>(MemStoreConfig{
    .max_entries    = 500'000,
    .sweep_interval = std::chrono::milliseconds(500)
});

// 2. Register as a server service
server.provide<MemStore>(store);

// 3. (Optional) attach as the SQL ORM cache backend
auto mem_cache = std::make_shared<MemStoreCacheBackend>(store);
db->set_cache(mem_cache);

NOTE

MemStore starts its background thread in the constructor and stops it in the destructor. No manual lifecycle calls required.


Configuration (MemStoreConfig) ​

FieldTypeDefaultDescription
max_entriessize_t1,000,000Maximum number of live keys. When full, the least-recently-used key is evicted before inserting a new one.
sweep_intervalstd::chrono::milliseconds500msHow often the MemStore thread scans for and purges TTL-expired keys.

Core API ​

All operations below are usable directly on MemStore via MemStoreCacheBackend, or by posting MemRequest structs manually for advanced use cases.

Get ​

cpp
// Returns std::nullopt on cache miss or TTL expiry
std::optional<std::string> val = co_await cache.get("user:session:100");

if (val) {
    std::cout << "Hit: " << *val << "\n";
} else {
    std::cout << "Miss\n";
}

Set (with optional TTL) ​

cpp
// Persistent (no TTL)
co_await cache.set("config:feature_flags", serialized_flags);

// With TTL — auto-evicted after 1 hour
co_await cache.set("user:session:100", "active", std::chrono::seconds(3600));

Delete ​

cpp
bool was_present = co_await cache.del("user:session:100");

Multi-Get ​

cpp
std::vector<std::optional<std::string>> values =
    co_await cache.mget({"k1", "k2", "k3"});

for (size_t i = 0; i < values.size(); ++i) {
    if (values[i]) std::cout << "k" << i << " = " << *values[i] << "\n";
    else           std::cout << "k" << i << " = <miss>\n";
}

Multi-Delete ​

cpp
// Returns the number of keys that existed and were deleted
int64_t deleted_count = co_await cache.del_many({"sess:1", "sess:2", "sess:42"});

Atomic Increment ​

cpp
// Atomically increments an integer key. Creates it at 1 if it doesn't exist.
int64_t views = co_await cache.incr("metrics:page_views");

NOTE

All operations run exclusively on the MemStore thread — reads, writes, increments, and deletes are all serialized through the same event queue. There are no data races and no locks on the hot-path map.


TTL Expiry ​

TTLs are set per-key at write time:

cpp
co_await cache.set("rate_limit:user:42", "10", std::chrono::seconds(60));

Expired keys are lazily evicted on get (if accessed after expiry) and proactively swept in bulk every sweep_interval milliseconds by the MemStore thread. This mirrors Redis's combined lazy + active expiry strategy.


LRU Eviction ​

When the store reaches max_entries, the least-recently-used key is evicted before the new entry is inserted. Both reads (get) and writes (set) count as a "use" and move the key to the front of the LRU list.

cpp
// Low-memory embedded config: cap at 10,000 entries
auto store = std::make_shared<MemStore>(MemStoreConfig{
    .max_entries = 10'000,
});

Using as ORM Cache Backend ​

MemStoreCacheBackend implements the CacheBackend interface and is a drop-in replacement for RedisCacheBackend when used with SqlDatabaseClient:

cpp
#include <aegon/data/memory/MemStoreCacheBackend.h>

// In server on_start or main:
auto mem_cache = std::make_shared<MemStoreCacheBackend>(store);
db->set_cache(mem_cache);

The ORM's normalized pointer caching, query epoch invalidation, and deferred transactional cache ops all work identically with MemStoreCacheBackend. See ORM Cache for full details.


Monitoring & Stats ​

cpp
// Live key count (approximate — read from the MemStore thread's map)
size_t count = store->size();

// Hit / miss counters (atomic — safe to read from any thread)
uint64_t hits   = store->hits();
uint64_t misses = store->misses();

double hit_rate = static_cast<double>(hits) / (hits + misses) * 100.0;
std::println("Cache hit rate: {:.1f}%", hit_rate);

// Reset counters
store->reset_stats();

Architecture ​

HTTP Worker Threads                    MemStore Thread
──────────────────                     ───────────────
co_await cache.get("key")
  │
  ├─ create eventfd (O_NONBLOCK)
  ├─ push MemRequest* to queue  ──────► drain request queue
  ├─ io_uring_prep_read(efd)           process all ops on map
  └─ coroutine suspends                write results to req struct
                                ◄────── write(efd, 1)
  io_uring wakes coroutine             ↓
  read result from req struct          sweep TTL-expired keys
  close(efd)                           evict LRU if over capacity
  co_return result

Key properties:

PropertyDetail
Thread modelOne dedicated thread owns the map — no data locking needed
Worker suspensionio_uring eventfd read — event loop is never blocked
Queue contentionMutex held for ~100ns per enqueue (pointer push + notify)
TTL strategyLazy eviction on get + proactive background sweep
Eviction policyLRU (doubly-linked list + hash map, O(1) per operation)
Per-core variantNot needed — no sockets, no rings, no network overhead

Comparison: MemStore vs Redis ​

MemStoreRedis
DeploymentIn-process, zero setupExternal server required
Network latencyNone~0.1–1ms per op
Data persistenceNone (process lifetime)RDB / AOF snapshots
Data sharingSingle process onlyAny number of services
Max capacityRAM of the host processConfigurable RAM + disk
Operationsget, mget, set, del, del_many, incrFull Redis command set
Best forSingle-node, testing, embeddedProduction, distributed

Released under the MIT License.