Skip to content

store: the block progress marker has no reader, and host keys spend the module's quota #276

Description

@mfw78

Decide whether the block progress marker is a primitive or dead weight, and stop the host spending a module's quota without saying so.

What is true today

persist_progress_marker (crates/nexum-runtime/src/supervisor/cursors.rs:98) writes last_dispatched_block:<chain_id> after every successful block dispatch, per module and per chain.

Nothing in the runtime ever reads it back. progress_key has exactly one caller, the write. Contrast chainlog_cursor:<keccak>, which read_chain_log_cursor (cursors.rs:50) does read, and which therefore earns its place.

So on a two-second chain with ten block-subscribed modules the engine performs five redb writes a second for a value with no consumer.

Why this is more than a wasted write

Both host-written keys go through store.module(module), so they land in the module's own local-store namespace, and the namespace footprint is what the quota is measured against (host/local_store_redb.rs:169-183).

Two consequences the tree does not state anywhere.

A module's max_state_bytes is partly spent by the host, on the module's behalf, without the author being able to see or influence it.

A key an author picks can collide with a host key. last_dispatched_block:1 written by a module is the same key the host writes for chain 1.

And the failure mode is asymmetric. persist_progress_marker warns and continues, so a module at its quota silently stops updating a marker nothing reads, which is harmless. commit_chain_log_cursor warns and continues too, so the same module silently stops advancing its chain-log cursor, and on the next start it replays from a stale position. That is a real behaviour, it is quota-dependent, and it is documented nowhere.

Why now

docs/production.md:136 and ADR-0019:81 list both keys as store contents an operator may inspect. Neither says an author may read them, neither says an author must avoid them, and neither says the host spends the module's quota.

This is pre-v1 contract surface. #239 renames the whole trigger vocabulary, and these two keys are named in ADR-0019 alongside it, so settling them costs nothing now and is a breaking change later.

The question to answer

Does a module need to detect a gap? Blocks are deliberately not replayed: a log carries content that exists once, a block trigger is a clock, and replaying a clock after an outage is wrong, since a module doing periodic work wants the current head rather than five hundred historical ticks. That asymmetry is correct and should be recorded in an ADR while this is decided.

But a module that does care can only learn it missed blocks 100 to 500 if something tells it. The marker is already in its namespace, so the cheapest gap primitive is nearly free: document that a module may read last_dispatched_block:<chain_id> from its own store, and the runtime keeps writing it. The alternative is to delete the write and let a module that cares keep its own marker.

Pick one. Do not leave a write with no reader and no documented contract.

Scope

  • Decide: documented gap primitive, or delete the write.
  • If it stays, record the asymmetry (logs backfill, blocks do not replay) in an ADR, and document the key as author-readable.
  • Either way, resolve the namespace question. Host-written keys sharing an author's keyspace needs either a reserved prefix an author cannot write, or a documented rule that these names are taken.
  • Either way, state that host-written keys consume max_state_bytes, and say what happens at the quota, including that a stalled chain-log cursor replays from a stale position on restart.

Done when

No host write to a module namespace lacks a reader or a documented contract.
An author knows which keys are theirs.
The quota interaction is written down, including the stale-cursor consequence.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:runtimeSupervisor, builder, event loop, restart/poison lifecycle, addonsarea:storelocal-store (redb), remote-store, state seam, namespace isolationdebtRefactor/cleanup: typed replacements for stringly code, dedup, right-sizing

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions