nodedb_cluster/raft_loop/builder.rs
1// SPDX-License-Identifier: BUSL-1.1
2
3//! Builder-chain methods for [`RaftLoop`].
4//!
5//! Every `with_*` method is a consumed-self builder that sets one optional
6//! field and returns `Self` (or a new `RaftLoop` with a different generic when
7//! the type changes, as with `with_plan_executor`). They live here rather
8//! than in [`super::loop_core`] so that the struct definition, constructor,
9//! and runtime methods stay under the file-size limit.
10
11use std::sync::Arc;
12use std::time::Duration;
13
14use crate::applied_watcher::GroupAppliedWatchers;
15use crate::catalog::ClusterCatalog;
16use crate::forward::PlanExecutor;
17use crate::metadata_group::applier::MetadataApplier;
18
19use super::hooks::{
20 AssignRemoteSurrogate, CalvinSubmit, CalvinSubmitInbox, ReleaseReservation, ReserveRead,
21 ShuffleAggregator, ShuffleConsumer, ShuffleProducer, ShuffleReceiver, SnapshotApplier,
22 SnapshotBuilder, SnapshotQuarantineHook,
23};
24use super::loop_core::{CommitApplier, RaftLoop, VShardEnvelopeHandler};
25
26impl<A: CommitApplier, P: PlanExecutor> RaftLoop<A, P> {
27 /// Install a custom plan executor (for cluster mode — C-β path).
28 pub fn with_plan_executor<P2: PlanExecutor>(self, executor: Arc<P2>) -> RaftLoop<A, P2> {
29 RaftLoop {
30 node_id: self.node_id,
31 multi_raft: self.multi_raft,
32 transport: self.transport,
33 topology: self.topology,
34 applier: self.applier,
35 metadata_applier: self.metadata_applier,
36 plan_executor: executor,
37 tick_interval: self.tick_interval,
38 vshard_handler: self.vshard_handler,
39 catalog: self.catalog,
40 shutdown_watch: self.shutdown_watch,
41 ready_watch: self.ready_watch,
42 loop_metrics: self.loop_metrics,
43 group_watchers: self.group_watchers,
44 prev_metadata_leader: self.prev_metadata_leader,
45 snapshot_quarantine_hook: self.snapshot_quarantine_hook,
46 shuffle_receiver: self.shuffle_receiver,
47 shuffle_producer: self.shuffle_producer,
48 shuffle_consumer: self.shuffle_consumer,
49 shuffle_aggregator: self.shuffle_aggregator,
50 assign_remote_surrogate: self.assign_remote_surrogate,
51 calvin_submit: self.calvin_submit,
52 calvin_submit_inbox: self.calvin_submit_inbox,
53 reserve_read: self.reserve_read,
54 release_reservation: self.release_reservation,
55 snapshot_builder: self.snapshot_builder,
56 snapshot_applier: self.snapshot_applier,
57 partial_snapshots: self.partial_snapshots,
58 data_dir: self.data_dir,
59 snapshot_chunk_bytes: self.snapshot_chunk_bytes,
60 orphan_partial_max_age_secs: self.orphan_partial_max_age_secs,
61 replication_factor: self.replication_factor,
62 // `with_plan_executor` is a construction-time builder, called before
63 // `run()` ever ticks — `tick_count` is still 0, so reset is exact.
64 tick_count: std::sync::atomic::AtomicU64::new(0),
65 // Construction-time builder, before `run()` and any join kick — a
66 // fresh `Notify` has no pending permit, so this loses nothing.
67 reconcile_notify: tokio::sync::Notify::new(),
68 }
69 }
70
71 /// Replace the per-group apply watcher registry.
72 ///
73 /// The host crate calls this with the same `Arc` it stores on
74 /// `SharedState` so proposers and consistent-read paths share
75 /// one registry with the tick loop's bump points. Defaults to a
76 /// fresh empty registry when not set.
77 pub fn with_group_watchers(mut self, watchers: Arc<GroupAppliedWatchers>) -> Self {
78 self.group_watchers = watchers;
79 self
80 }
81
82 /// Attach the snapshot quarantine hook (builder chain variant).
83 ///
84 /// The supplied implementation is called by the `InstallSnapshotRequest`
85 /// handler to check for, record, and short-circuit quarantined chunks.
86 pub fn with_snapshot_quarantine_hook(mut self, hook: Arc<dyn SnapshotQuarantineHook>) -> Self {
87 self.snapshot_quarantine_hook = Some(hook);
88 self
89 }
90
91 /// Attach the cross-node streaming-shuffle receiver (E1, builder chain).
92 ///
93 /// The supplied implementation (backed by `nodedb`'s
94 /// `ShuffleReceiverRegistry`) is called by the `ShufflePush` transport
95 /// read-loop to create inboxes, deposit chunks, and record the per-part
96 /// build barrier.
97 pub fn with_shuffle_receiver(mut self, receiver: Arc<dyn ShuffleReceiver>) -> Self {
98 self.shuffle_receiver = Some(receiver);
99 self
100 }
101
102 /// Attach the cross-node shuffle PRODUCER (E4a, builder chain).
103 ///
104 /// The supplied implementation (backed by `nodedb`'s local streaming executor
105 /// and receiver registry) is called by the `ShuffleProduce` transport handler
106 /// to run the local scan, hash-partition its rows, and fan them out to the
107 /// part-owners.
108 pub fn with_shuffle_producer(mut self, producer: Arc<dyn ShuffleProducer>) -> Self {
109 self.shuffle_producer = Some(producer);
110 self
111 }
112
113 /// Attach the cross-node shuffle CONSUMER (E4b, builder chain).
114 ///
115 /// The supplied implementation (backed by `nodedb`'s shuffle registry +
116 /// local executor) is called by the `ShuffleConsume` transport handler to
117 /// wait for the part's staged sides to finalize and run the node-local grace
118 /// join.
119 pub fn with_shuffle_consumer(mut self, consumer: Arc<dyn ShuffleConsumer>) -> Self {
120 self.shuffle_consumer = Some(consumer);
121 self
122 }
123
124 /// Attach the cross-node distributed GROUP BY shuffle CONSUMER (E5b, builder
125 /// chain).
126 ///
127 /// SINGLE-SIDED aggregate sibling of [`with_shuffle_consumer`](Self::with_shuffle_consumer).
128 /// The supplied implementation (backed by `nodedb`'s shuffle registry + local
129 /// executor) is called by the `ShuffleAggregateConsume` transport handler to
130 /// wait for the part's single staged side to finalize and run the node-local
131 /// partial-state merge + finalize.
132 pub fn with_shuffle_aggregator(mut self, aggregator: Arc<dyn ShuffleAggregator>) -> Self {
133 self.shuffle_aggregator = Some(aggregator);
134 self
135 }
136
137 /// Attach the routed-surrogate-exchange assigner (F1b, builder chain).
138 ///
139 /// The supplied implementation (backed by `nodedb`'s `SurrogateAssigner`) is
140 /// called by the `AssignSurrogate` transport handler when this node is the
141 /// home vShard's leader: it runs a LOCAL assign for the endpoint key, which
142 /// yields the authoritative (first-wins, idempotent) surrogate the coordinator
143 /// must use.
144 pub fn with_assign_remote_surrogate(
145 mut self,
146 assigner: Arc<dyn AssignRemoteSurrogate>,
147 ) -> Self {
148 self.assign_remote_surrogate = Some(assigner);
149 self
150 }
151
152 /// Attach the routed Calvin-submit hook (Cv1, builder chain).
153 ///
154 /// The supplied implementation (backed by `nodedb`'s Calvin sequencer inbox
155 /// and `CalvinCompletionRegistry`) is called by the `SubmitCalvinTxn`
156 /// transport handler when this node is the sequencer-group leader: it submits
157 /// the carried `TxClass` to the local inbox and awaits completion, so a
158 /// cross-shard write routed here from a non-leader coordinator actually
159 /// commits.
160 pub fn with_calvin_submit(mut self, submit: Arc<dyn CalvinSubmit>) -> Self {
161 self.calvin_submit = Some(submit);
162 self
163 }
164
165 /// Attach the routed Calvin-INBOX submit hook (Cv1, builder chain).
166 ///
167 /// OLLP dependent sibling of [`with_calvin_submit`](Self::with_calvin_submit).
168 /// The supplied implementation (backed by `nodedb`'s Calvin sequencer inbox
169 /// and `CalvinCompletionRegistry`) is called by the `SubmitCalvinInbox`
170 /// transport handler when this node is the sequencer-group leader: it submits
171 /// the carried dependent `TxClass` to the local inbox and awaits only the
172 /// assignment, so a cross-shard write routed here from a non-leader
173 /// coordinator gets its assignment back immediately (the OLLP coordinator
174 /// drives it to completion itself).
175 pub fn with_calvin_submit_inbox(mut self, submit: Arc<dyn CalvinSubmitInbox>) -> Self {
176 self.calvin_submit_inbox = Some(submit);
177 self
178 }
179
180 /// Attach the routed reserve-read hook (Calvin OLLP, builder chain).
181 ///
182 /// The supplied implementation (backed by `nodedb`'s Calvin sequencer
183 /// scheduler) is called by the `ReserveRead` transport handler when this
184 /// node is the sequencer-group leader: it decodes the carried `LockKey`
185 /// and assign-only reserves the read lock against the local lock table.
186 pub fn with_reserve_read(mut self, hook: Arc<dyn ReserveRead>) -> Self {
187 self.reserve_read = Some(hook);
188 self
189 }
190
191 /// Attach the routed release-reservation hook (Calvin OLLP, builder
192 /// chain).
193 ///
194 /// Ack-only sibling of [`with_reserve_read`](Self::with_reserve_read). The
195 /// supplied implementation (backed by `nodedb`'s Calvin sequencer
196 /// scheduler) is called by the `ReleaseReservation` transport handler when
197 /// this node is the sequencer-group leader: it decodes the carried owner
198 /// and release reason and releases the reservation.
199 pub fn with_release_reservation(mut self, hook: Arc<dyn ReleaseReservation>) -> Self {
200 self.release_reservation = Some(hook);
201 self
202 }
203
204 /// Attach the per-group snapshot builder for the SEND path (builder chain).
205 ///
206 /// The supplied implementation (backed by `nodedb`'s Data Plane snapshot
207 /// dispatch) is called by the install-snapshot tick step to produce the real
208 /// serialized engine state for a lagging follower's group vshards. When not
209 /// set, the sender falls back to the stub (empty) chunk.
210 pub fn with_snapshot_builder(mut self, builder: Arc<dyn SnapshotBuilder>) -> Self {
211 self.snapshot_builder = Some(builder);
212 self
213 }
214
215 /// Attach the per-group snapshot applier for the RECEIVE path (builder chain).
216 ///
217 /// The supplied implementation (backed by `nodedb`'s Data Plane restore
218 /// dispatch) is called by the install-snapshot finalize path to apply a
219 /// received per-group snapshot to the local state machine after the atomic
220 /// `.partial`→`.snap` rename and before advancing Raft. When not set, the
221 /// follower advances Raft without restoring engine state.
222 pub fn with_snapshot_applier(mut self, applier: Arc<dyn SnapshotApplier>) -> Self {
223 self.snapshot_applier = Some(applier);
224 self
225 }
226
227 /// Set the data directory for partial-snapshot persistence and GC.
228 ///
229 /// When set, the `InstallSnapshotRequest` handler writes chunks to
230 /// `<data_dir>/recv_snapshots/<group_id>.partial` and the GC sweeper
231 /// removes stale partials on startup. When `None` (the default, used by
232 /// unit tests), disk writes are skipped — the receiver operates in-memory
233 /// only with empty chunk data.
234 pub fn with_data_dir(mut self, data_dir: std::path::PathBuf) -> Self {
235 self.data_dir = Some(data_dir);
236 self
237 }
238
239 /// Override the snapshot chunk byte size (default: 4 MiB).
240 pub fn with_snapshot_chunk_bytes(mut self, chunk_bytes: u64) -> Self {
241 self.snapshot_chunk_bytes = chunk_bytes;
242 self
243 }
244
245 /// Override the orphan-partial max age for the GC sweeper (default: 300 s).
246 pub fn with_orphan_partial_max_age_secs(mut self, secs: u64) -> Self {
247 self.orphan_partial_max_age_secs = secs;
248 self
249 }
250
251 /// Set a handler for incoming VShardEnvelope messages.
252 pub fn with_vshard_handler(mut self, handler: VShardEnvelopeHandler) -> Self {
253 self.vshard_handler = Some(handler);
254 self
255 }
256
257 /// Install the metadata applier used for group-0 commits.
258 ///
259 /// The host crate (nodedb) calls this with a production applier that
260 /// wraps an in-memory `MetadataCache` and additionally persists to
261 /// redb / broadcasts catalog change events. The default
262 /// [`NoopMetadataApplier`] is kept only for tests that don't care.
263 pub fn with_metadata_applier(mut self, applier: Arc<dyn MetadataApplier>) -> Self {
264 self.metadata_applier = applier;
265 self
266 }
267
268 pub fn with_tick_interval(mut self, interval: Duration) -> Self {
269 self.tick_interval = interval;
270 self
271 }
272
273 /// Set the cluster replication factor (target voters per group).
274 ///
275 /// Pass the value loaded from `ClusterSettings::replication_factor` at
276 /// startup. The field is immutable after the loop is wrapped in `Arc` —
277 /// call this on the builder chain before that wrap.
278 pub fn with_replication_factor(mut self, rf: u32) -> Self {
279 self.replication_factor = rf;
280 self
281 }
282
283 /// Attach a cluster catalog — used by the join flow to persist the
284 /// updated topology + routing after a conf-change commits.
285 pub fn with_catalog(mut self, catalog: Arc<ClusterCatalog>) -> Self {
286 // Seed the local cluster-epoch high-water mark from the catalog
287 // on attach, so the value emitted in the very first outbound
288 // RPC reflects whatever the previous incarnation persisted. A
289 // failure to load is treated as a fresh catalog (epoch 0); a
290 // genuine catalog read error would have surfaced from earlier
291 // catalog operations on this same handle.
292 if let Err(e) = crate::cluster_epoch::init_local_cluster_epoch_from_catalog(&catalog) {
293 tracing::warn!(error = %e, "failed to load persisted cluster_epoch; defaulting to 0");
294 }
295 self.catalog = Some(catalog);
296 self
297 }
298}