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:
- 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.
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
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) andpackages/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):impl DbCore::shutdownbody (incl.notify_one+take()+select!{handle|timeout})zaino_db_handler_sleep(sleep / maintenance.tick / shutdown_notify)clean_trailingopen_or_create_dbstatus()+ trait forwarderwait_until_readyTotal: ~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.rsmodule:pub(super) trait DbLifecyclewith required getters exposing the sixshared struct fields (
env,db_handler,shutdown_notify,status) andprovided methods for
status(),wait_until_ready(),shutdown(),clean_trailing(),zaino_db_handler_sleep(). No macros needed — aprovided-method trait expresses the abstraction.
pub(super) async fn open_or_create_db(env, name, flags)as a freefunction (no
self, no need for a trait).Both
DbV0andDbV1become implementors by forwarding getters; theduplicated bodies disappear.
Must resolve alongside the extraction
#1033 — the duplicated
shutdown()usesshutdown_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-taskoneshot, or a count-gatedbarrier) 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:
heights_to_hashes(direct)headers[height].context.index.hashheights(direct, inverse direction from v0)hashes_to_blocks(JSON blob)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*) anddirection 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_handlerscaffolding. 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 otherselect!sites exist in thetree (
finalised_state.rs:555,jsonrpc.rs:103); they are one-offs withdistinct semantics and are out of scope here.
Acceptance criteria
DbV0andDbV1implementDbLifecycle; the bodies listed in thetable above are removed from both files.
a background task that completes, may not be registered for shutdown notification in a race condition #1033 as part of this PR or cleanly separate it).
open_or_create_dbis a single free function indb.rs, called byboth backends.
pub(super)(per the minimum-visibility rule).tokio::select!sites introduced; existing duplicated onescollapse into one.
zaino-statetests pass, including theshutdown/ready sequencing.
Out of scope / follow-ups
DbLifecycle— handled naturally when Implement ZainoDB DbV2 #860 lands.just forwards to whatever
NamedAtomicStatusbecomes.