Skip to main content

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}