Skip to content

DRY: extract shared DbCore lifecycle scaffolding from DbV0/DbV1 (~70 LOC) #1049

@zancas

Description

@zancas

Parent: #862
Related: #1033 (bug lives inside the duplicated block), #858 / #860 (DbV2 will triplicate it if left alone)

Problem

packages/zaino-state/src/chain_index/finalised_state/db/v0.rs (1062 LOC) and
packages/zaino-state/src/chain_index/finalised_state/db/v1.rs (723 LOC)
contain byte-identical blocks that carry no V0- vs V1-specific logic. Verified
by diff (zero output):

Item v0.rs v1.rs
impl DbCore::shutdown body (incl. notify_one + take() + select!{handle|timeout}) 187–224 153–190
zaino_db_handler_sleep (sleep / maintenance.tick / shutdown_notify) 439–449 577–587
clean_trailing 455–459 690–694
open_or_create_db 464–476 697–709
inherent status() + trait forwarder 178–180, 368–370 149–151, 427–429
wait_until_ready 376–386 436–446

Total: ~70 LOC of pure copy-paste. Adding DbV2 (#860) would triplicate it.

Proposed solution

Per CLAUDE.md ("DRY: deduplicate with functions first") extract to the parent
db.rs module:

  1. A pub(super) trait DbLifecycle with required getters exposing the six
    shared struct fields (env, db_handler, shutdown_notify, status) and
    provided methods for status(), wait_until_ready(), shutdown(),
    clean_trailing(), zaino_db_handler_sleep(). No macros needed — a
    provided-method trait expresses the abstraction.
  2. pub(super) async fn open_or_create_db(env, name, flags) as a free
    function (no self, no need for a trait).

Both DbV0 and DbV1 become implementors by forwarding getters; the
duplicated bodies disappear.

Must resolve alongside the extraction

#1033 — the duplicated shutdown() uses shutdown_notify.notify_one(),
which only stores a single permit. If multiple background tasks need to be
woken, the extra wakes are lost. The DRY'd helper must fix this (widen to
e.g. Notify::notify_waiters, a per-task oneshot, or a count-gated
barrier) rather than ossify the bug into a shared method.

Explicit non-goals

  • Table-layout sharing / struct-field sharing. The table sets of v0 and
    v1 are disjoint in type, not just count:

    Relation v0 v1
    height → hash heights_to_hashes (direct) derived from headers[height].context.index.hash
    hash → height not stored heights (direct, inverse direction from v0)
    hash → compact block hashes_to_blocks (JSON blob) assembled on read from headers/txids/transparent/sapling/orchard (five height-keyed tables, StoredEntry{Var,Fixed}<T> with checksums)

    v0's information is a strict subset of v1's, but the tables are not —
    keys, values, serialization format (JSON wrappers vs StoredEntry*) and
    direction of the hash↔height map all differ. There is no inner-struct
    seam to factor out. This issue is scoped to behavior/lifecycle DRY, not
    schema or table-layout DRY.

  • spawn_handler scaffolding. The outer shape (clone-fields, spawn,
    Ready, 60 s interval, closing-check loop) is parallel, but the inner body
    diverges completely (v0: empty; v1: three initial scans + per-iteration
    validation). No clean seam.

  • Other tokio::select! sites. Five other select! sites exist in the
    tree (finalised_state.rs:555, jsonrpc.rs:103); they are one-offs with
    distinct semantics and are out of scope here.

Acceptance criteria

Out of scope / follow-ups

Metadata

Metadata

Assignees

No one assigned

    Labels

    architectureArchitectural improvementscode-smellCode smells that need attention

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions