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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
//! T9: the open-time `counters_dirty` marker.
//!
//! The existing global `DbInfo.clean` flag cannot stand in for this:
//! `Db::close` is documented to tolerate writes after it returns, it writes
//! `clean = true` even when collection flushes accumulated errors, and it runs
//! the transaction checkpoint *after* that mark. All three holes survive any
//! attempt to reuse it for counter recovery.
//!
//! So T9 owns a per-collection `counters_dirty` flag ([`CollectionInfo`](crate::armour::CollectionInfo)),
//! persisted when a **counted source is opened** — in `Db::open_managed`,
//! before the collection is published — and cleared only when finalisation
//! (`Db::close` / `Db::clean_shutdown`) actually succeeded **and** this
//! session's open is what set it ([`CounterDirty::take_marked`]).
//!
//! # Why the mark is at open, not on the write path
//!
//! The mark used to run in the write-path teardown, immediately before the
//! first delta of a session applied. That placement could not deliver what it
//! promised: `Ctx::with_shard` runs the locked mutation, *releases the lock*,
//! and only then calls the projection/counter teardown — so by the time a
//! failing mark could return an error, the source row was already written.
//! Aborting there did not abort the write; it left a committed row whose delta
//! was skipped, and a retry of the same `put` produced `old == new` and hence
//! no delta at all. The counter was then permanently stale with no evidence
//! that a recount was owed — precisely the loss the flag exists to prevent.
//!
//! Marking at open is the same evidence, taken at a point where it can still
//! be enforced: the collection is not yet published, so a failed mark **fails
//! the open** and the source never takes a single write. The `Db` is also
//! provably alive there, which is why the write path no longer touches the
//! marker at all.
//!
//! The cost is deliberate: the flag stands for the whole session, so an
//! unclean shutdown recounts even when no counted write ever happened. A clean
//! close still clears it, so an ordinary restart does not recount. The
//! symmetric cost is that a write *after* `close()` — which the crate
//! tolerates — is no longer re-marked; stop writers before closing.
//!
//! Direct opens (`TypedTree::open_hooked` and its siblings) have no `Db` and
//! therefore nowhere to persist the flag, so a carrier with counters is
//! rejected there ([`check_no_counters`]) — otherwise "the engine repairs at
//! the next open" would be a promise the code cannot keep.
use Weak;
use ;
use Persist;
use crateKey;
use crateDbInfo;
use crate;
use crateTypedWriteHook;
/// One collection's `counters_dirty` marker.
///
/// Bound once, at managed open, into an `OnceLock<CounterDirty>` field on the
/// collection that owns the write path — `TypedTree`/`TypedMap` directly, or
/// `ConstTree`/`ConstMap`/`VarTree`/`VarMap` for the `ZeroTree`/`ZeroMap`/
/// `VarTypedTree`/`VarTypedMap` families they back. It lives on the collection
/// struct rather than behind the hook chain so that a user-authored wrapper
/// hook cannot silently disconnect it by forgetting a forwarding method (this
/// crate already hit that failure class once — `TtlHook` not forwarding
/// `COUNTS`, T9 Task 5).
///
/// It carries exactly two things:
///
/// - [`mark`](Self::mark) — persist `counters_dirty = true` for this
/// collection. Called once per session, from `Db::open_managed`, for a
/// counted **source**.
/// - `marked` — "this session's open is what persisted the flag", read (and
/// consumed) by `Db::close`/`Db::clean_shutdown` through
/// [`take_marked`](Self::take_marked). That distinction is load-bearing: the
/// same durable bit also carries a second, marker-less meaning set by
/// `run_migration` on a counter *target* ("my content moved under the
/// counter"), which only a source's later open consumes. A close that
/// cleared it would destroy the sole evidence a recount is owed.
pub
/// Fault injection for [`CounterDirty::mark`] — lets a test prove that a
/// failed mark **fails the open** of a counted source, so it never takes a
/// write whose delta could be lost with no durable evidence behind it.
/// Mirrors [`crate::projection::FAIL_COLLECTION_FLUSH`] and
/// `crate::counter::FAIL_COUNTER_DELTA`.
pub static FAIL_COUNTER_MARK: AtomicBool =
new;
/// Simulate a `db.info` write that fails while persisting `counters_dirty`.
/// Compiles away entirely without `test-faults` (a non-default feature).
/// Reject a counter carrier (`hook.counter_targets().is_some()`) opened
/// without a `Db` — there is nowhere to persist the dirty marker, so "the
/// engine repairs at the next open" would be a promise the code cannot keep.
///
/// Runs only on the direct `open_hooked`/`open_with_hook` entry points; a
/// managed open (`Db::open_*`) calls the private constructor directly and
/// binds the marker afterward via `ManagedOpen::bind_counter_dirty`, so it
/// never observes this check.
pub