Skip to content

Consider: a caching framework (TTL/eviction/cache-aside) #119

Description

@Yaraslaut

Part of #115 (userver-vs-morph survey).

Observation

morph has no caching framework — every "cache" hit in include/morph/ is either an unrelated code comment or a function-local static const std::string memoizing a schema string, not a TTL/eviction/cache-aside abstraction. userver bundles cache::CacheUpdateTrait/components::CachingComponentBase with full vs. incremental update modes and cache dumps — persisting a snapshot to survive a failed first cache load on cold start.

Status: speculative, not a confirmed gap

morph has no in-memory read-cache layer today, so there's nothing to add TTL/eviction semantics to yet.

Revisit only if

morph grows an in-memory read-cache layer in front of Lightweight. If it does, the cache-dump idea (persist last-known-good state so a cold start with a broken dependency still boots) is worth considering — it rhymes with SqliteOfflineQueue's durability goal, though it's not directly applicable without a caching framework to attach it to.

Reference

Full comparison: #115, §3.7 and §4 of the attached document (docs/superpowers/findings/userver-vs-morph-2026-08-17.md).


Deep analysis: #119 caching framework

Verification. The prior investigation's findings all hold on current source, and kanban (PR #121) sharpens rather than overturns them. bank::AccountModel remains the only hand-rolled read cache: examples/bank/include/bank/db/row_versions.hpp (a ~40-line version-counter singleton) plus a per-instance memo (_row/_loadedId/_seenVersion, examples/bank/include/bank/models/account_model.hpp) whose lock-free correctness comes from strand serialization. On ladder-kanban-impl, BoardModel::buildState re-queries small indexed tables per call and the hot poll path GetEventsSince is DB-incremental (WHERE id > lastEventId). The one growing cost is GetActivity: it calls FileActionLog::entries() (full file re-read + re-parse, include/morph/journal/file_action_log.hpp:201-233), and the GUI triggers it on every applied board event (board_qml_bridge.cpp::onEventApplied) — O(journal size) per event. Kanban documents this inline as acceptable at rung scale, and rotate() already provides the retention seam.

Design assessment. If the trigger ever fires, the right shape is not userver's CacheUpdateTrait (TTL/eviction/dump lifecycle) but a generalization of what bank already proved: a header-only VersionedCache split the way bank splits it — a locked VersionRegistry<K> (writers bump(key)) plus a per-instance, lock-free Versioned<V> memo (getOrLoad(key, registry, loader)), living in include/morph/util/ beside datetime.hpp/quantity.hpp with a docs/spec/util/ page. TTL and eviction stay out: strand-serialized instances make explicit version stamps strictly better than time-based staleness, and morph's journal already provides natural version sources (LogEntry::seq, kanban's monotonic BoardEventRecord::id — a cached activity view could invalidate off the same cursor GetEventsSince uses). The cache-dump idea stays inapplicable: morph clients' cold-start durability is the local SQLite file plus SqliteOfflineQueue — the authority survives restart, so there is no "last known good snapshot" distinct from the store itself. Server-side, docs/spec/core/shared_instances.md deliberately solves the superset (cross-attacher divergence), and examples/IMPLEMENTATION.md:138 rightly forbids per-rung cache layers.

Cost/benefit. Helper: S (~50-100 lines) but with real carrying cost — Doxygen-gated public API, a spec file, tests — for zero current consumers. Framework: M-L, all risk (staleness is exactly the bug class bank's pattern exists to prevent) and no beneficiary.

Recommendation: defer, unchanged, with sharpened triggers. Revisit when (a) kanban's GetActivity shows measured cost at realistic journal sizes despite rotate(), (b) a rung needs cross-instance read state shared instances can't express, or (c) a third rung reinvents bank's memo+version pattern — two reinventions is a coincidence, three is an API.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions