1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
/// Common trait for armdb collections managed by [`Db`](super::tree_db::Db).
///
/// Implemented for `TypedTree`, `TypedMap`, `ZeroTree`, `ZeroMap`
/// where the value type implements [`CollectionMeta`](crate::CollectionMeta).
pub trait Collection: Send + Sync {
/// Collection name (from `CollectionMeta::NAME`).
fn name(&self) -> &str;
/// Number of entries in the collection.
fn len(&self) -> usize;
fn is_empty(&self) -> bool {
self.len() == 0
}
/// Run a compaction pass across all shards.
fn compact(&self) -> crate::DbResult<usize>;
/// Flush in-memory write buffers and pending hint data so an immediate
/// process exit will not lose committed writes. No default — every impl
/// must provide this to avoid silent durability gaps.
fn flush(&self) -> crate::DbResult<()>;
/// The tick's barrier, per backend policy. Backend-specific:
/// - Bitcask: flush write buffers and fsync dirty shards only (same as
/// [`durable_flush`](Self::durable_flush)).
/// - FixedStore: fdatasync shards with unsynced data only.
/// - `LsmTree`: `persist` with the collection's configured `PersistPolicy`.
///
/// The background flush worker calls this. Write-triggered `Config::sync_interval`
/// checks run only as writes return, so this tick is the durability deadline
/// while a collection is silent. No default — every impl must provide it.
fn periodic_flush(&self) -> crate::DbResult<()>;
/// The strongest barrier on this collection: Bitcask drains dirty shard
/// buffers under each shard lock, then fsyncs their captured active files
/// outside it without rebuilding hints; FixedStore — `fdatasync` unsynced
/// shards; LSM — `PersistMode::SyncAll`. A clean Bitcask shard is skipped,
/// and a failed drain or fsync is returned to the caller.
///
/// On Bitcask and FixedStore this coincides with
/// [`periodic_flush`](Self::periodic_flush). On LSM it does not: the tick
/// honours `PersistPolicy`, this method always uses `SyncAll`. The
/// transaction checkpoint uses it before retiring a log record. Each
/// completed Bitcask fsync stage emits
/// `armdb.flush.barriers{result="ok"|"failed"}`, and a shard that needs no
/// barrier records `result="skipped"`; a pre-fsync drain failure stays in
/// flush-pass accounting because no fsync stage completed.
fn durable_flush(&self) -> crate::DbResult<()>;
/// Full clean shutdown for this collection. Heavier than [`flush`](Self::flush):
/// - Bitcask/Var: flush write buffers, write hint files, fsync data files.
/// - FixedStore: fdatasync data, write the `fixed.versions` sidecar and set
/// the clean-shutdown flag so the NEXT open takes Clean recovery.
///
/// # Contract
///
/// The caller must stop all application writers first. Any write after this
/// call — a local mutation *or* a FixedStore replication-apply from a leader —
/// downgrades the affected shard back to Dirty recovery (safe, slow); it never
/// corrupts. Background [`periodic_flush`](Self::periodic_flush) and
/// [`compact`](Self::compact) are not writes and are harmless afterwards.
/// [`Db::close`](super::db::Db::close) / [`Db::clean_shutdown`](super::db::Db::clean_shutdown)
/// also gate TTL sweep (write source) before calling this; joining the sweeper
/// thread remains [`Db::shutdown`](super::db::Db::shutdown).
/// Idempotent; [`Db`](super::db::Db) drop after it keeps the clean state intact.
///
/// # Comparison with other shutdown paths
///
/// | Method | Hints | Fixed sidecar/flag | Final fsync | Txlog checkpoint | Stops workers | Error |
/// |---|---|---|---|---|---|---|
/// | periodic_flush | no | no | dirty Bitcask/Fixed; LSM policy | no | no | logged |
/// | close | writes | no | dirty shards only | yes | no | returned |
/// | clean_shutdown | writes | yes | yes | yes | no | returned |
/// | shutdown(&mut self) | via clean_shutdown | yes | yes | yes | yes | returned |
/// | Drop | via close | no | dirty shards only | yes | yes | logged only |
///
/// Hinted Bitcask collections re-barrier once per shard during
/// [`Db`](super::db::Db) drop
/// after `clean_shutdown`; clean no-hints shards issue zero fsyncs.
fn clean_shutdown(&self) -> crate::DbResult<()>;
/// Sweep expired rows up to `budget` candidates. `now_ms` is wall-clock millis.
fn sweep_expired(
&self,
_now_ms: u64,
_budget: usize,
) -> crate::DbResult<crate::ttl::SweepOutcome> {
Ok(crate::ttl::SweepOutcome::default())
}
/// Enable or disable background TTL sweep for this collection.
fn set_ttl_sweep(&self, _enabled: bool) -> crate::DbResult<()> {
Ok(())
}
/// Rebuild the in-memory TTL index from live rows.
fn rebuild_ttl_index(&self) -> crate::DbResult<()> {
Err(crate::DbError::Client("collection has no TTL policy"))
}
/// Consume the `counters_dirty` marker (T9) and report whether it was set
/// — i.e. whether **this session's open of this collection as a counted
/// source** is what persisted the flag.
///
/// `Db::close` / `Db::clean_shutdown` clear the durable flag only for a
/// collection that answers `true`: the flag also carries a second,
/// marker-less meaning set by `run_migration` ("my content moved under
/// some counter"), which only a *source's* open consumes. Clearing that
/// one at close would drop the sole evidence that a recount is owed;
/// leaving it costs at most one redundant recount per open. (Once a
/// source's open-time recount *has* consumed it, `Db` records that
/// separately and the close retires the target's flag too.)
///
/// Default `false` — a collection with no bound counter marker never
/// marked anything.
fn reset_counter_dirty_marker(&self) -> bool {
false
}
/// Open-time / on-demand counter recount (T9 Task 8) — see
/// `ManagedOpen::reconcile_counters` for the two-pass walk this reaches.
/// [`Db::recount_counters`](super::db::Db::recount_counters) uses this
/// by-name entry point since it only has a collection name, not a
/// concrete type. Default no-op (`Ok(0)`) — only a collection with a
/// bound counter carrier needs it.
fn reconcile_counters(&self) -> crate::DbResult<u64> {
Ok(0)
}
/// The collections this one declares as counter targets (T9 Task 8) —
/// empty when this collection is not itself a counted source (it may be
/// a plain counter *target*, or unrelated to the counter system).
/// [`Db::recount_counters`](super::db::Db::recount_counters) uses this to
/// refuse recounting — and, critically, clearing the `counters_dirty`
/// flag of — a collection that isn't a source: for a plain target, that
/// flag is the only evidence a source's own later open reads to know a
/// recount is owed (see `Db::open_managed`), so clearing it here would
/// silently destroy recovery state instead of recounting anything.
/// Default empty; overridden by the six typed families.
fn counter_target_names(&self) -> Vec<&'static str> {
Vec::new()
}
/// Shards holding writes no durability barrier covers yet — this
/// collection's share of the power-loss window between two flush ticks.
///
/// `None` means the backend has no shard-level dirty state to report
/// (`LsmTree`, whose durability is fjall's own `PersistPolicy`), so a
/// caller summing these values never reads an unknown as a clean zero.
/// Sampled shard by shard without a consistent cut: a concurrent writer or
/// barrier can move the answer mid-count.
fn dirty_shards(&self) -> Option<usize> {
None
}
/// Terminally close active realtime receivers after all database writers stop.
fn close_watches(&self) {}
}