Skip to main content

laminar_core/cluster/control/controller/
barrier.rs

1//! Checkpoint barrier announcement, observation, and acknowledgement.
2
3use super::*;
4
5impl ClusterController {
6    /// Leader-side announce.
7    ///
8    /// # Errors
9    /// Propagates [`BarrierCoordinator::announce`] errors.
10    pub async fn announce_barrier(&self, ann: &BarrierAnnouncement) -> Result<(), String> {
11        if !self.process_lease_is_live() {
12            return Err("stable node process lease is no longer live".into());
13        }
14        self.barrier.announce(ann).await
15    }
16
17    /// Leader-side assignment-certified Prepare publication with its configured quorum window.
18    ///
19    /// # Errors
20    /// Propagates process-lease and [`BarrierCoordinator::announce_prepare`] errors.
21    #[cfg(feature = "cluster")]
22    pub async fn announce_prepare_barrier(
23        &self,
24        ann: &BarrierAnnouncement,
25        quorum_window: Duration,
26    ) -> Result<(), String> {
27        if !self.process_lease_is_live() {
28            return Err("stable node process lease is no longer live".into());
29        }
30        self.barrier.announce_prepare(ann, quorum_window).await
31    }
32
33    /// Leader-side Prepare publication whose fan-out is fenced by the exact attempt deadline.
34    /// The retry window remains independent so a long checkpoint timeout does not create long
35    /// individual RPC attempts.
36    ///
37    /// # Errors
38    /// Propagates process-lease and [`BarrierCoordinator::announce_prepare_until`] errors.
39    #[cfg(feature = "cluster")]
40    pub async fn announce_prepare_barrier_until(
41        &self,
42        ann: &BarrierAnnouncement,
43        attempt_deadline: tokio::time::Instant,
44        retry_window: Duration,
45    ) -> Result<(), String> {
46        if !self.process_lease_is_live() {
47            return Err("stable node process lease is no longer live".into());
48        }
49        self.barrier
50            .announce_prepare_until(ann, attempt_deadline, retry_window)
51            .await
52    }
53
54    /// Observe the merged barrier history, validating durable authority only when `predicate`
55    /// selects the announcement. Malformed or conflicting histories fail before filtering.
56    ///
57    /// # Errors
58    /// Propagates merge, transport, and matching reversible-authority failures.
59    pub async fn observe_barrier_matching<F>(
60        &self,
61        mut predicate: F,
62    ) -> Result<Option<BarrierAnnouncement>, String>
63    where
64        F: FnMut(&BarrierAnnouncement) -> bool,
65    {
66        let Some(leader) = self.current_leader() else {
67            return Ok(None);
68        };
69        let Some(announcement) = self.barrier.observe_hint(leader).await? else {
70            return Ok(None);
71        };
72        if !predicate(&announcement) {
73            return Ok(None);
74        }
75        #[cfg(feature = "cluster")]
76        self.barrier.validate_observed(&announcement).await?;
77        Ok(Some(announcement))
78    }
79
80    /// Observe a clustered `Prepare` with a bounded compatibility timeout.
81    ///
82    /// # Errors
83    /// Rejects missing, stale, or conflicting authority and assignment certificates.
84    #[cfg(feature = "cluster")]
85    pub async fn observe_checkpoint_prepare(
86        &self,
87    ) -> Result<Option<CheckpointPrepareObservation>, String> {
88        self.observe_checkpoint_prepare_until(CHECKPOINT_PREPARE_OBSERVATION_TIMEOUT)
89            .await
90    }
91
92    /// Observe a clustered `Prepare`, validate its leader with one durable-authority read, and
93    /// report the local assignment disposition without consulting authority again. Hint I/O is
94    /// bounded by the caller's budget. Once an exact Prepare is decoded, authority validation uses
95    /// the earlier of its same-identity direct receipt or stable first gossip observation, which
96    /// retries cannot refresh. An unrelated stale direct Prepare never shortens the observation
97    /// window for a newer gossip-only attempt.
98    ///
99    /// # Errors
100    /// Rejects a zero or overflowing timeout, observation/authority timeout, or missing, stale,
101    /// conflicting authority and assignment certificates.
102    #[cfg(feature = "cluster")]
103    pub async fn observe_checkpoint_prepare_until(
104        &self,
105        checkpoint_timeout: Duration,
106    ) -> Result<Option<CheckpointPrepareObservation>, String> {
107        if checkpoint_timeout.is_zero() {
108            return Err("checkpoint Prepare observation timeout must be greater than zero".into());
109        }
110        let Some(leader) = self.current_leader() else {
111            return Ok(None);
112        };
113        let observation_started = tokio::time::Instant::now();
114        let observation_deadline = observation_started
115            .checked_add(checkpoint_timeout)
116            .ok_or_else(|| "checkpoint Prepare observation deadline overflowed".to_string())?;
117        if tokio::time::Instant::now() >= observation_deadline {
118            return Err("checkpoint Prepare observation deadline already elapsed".into());
119        }
120        let Some(announcement) =
121            tokio::time::timeout_at(observation_deadline, self.barrier.observe_hint(leader))
122                .await
123                .map_err(|_| "checkpoint Prepare hint observation timed out".to_string())??
124        else {
125            return Ok(None);
126        };
127        if announcement.phase != super::super::Phase::Prepare {
128            return Ok(None);
129        }
130        let attempt = crate::checkpoint::CheckpointAttempt::new(
131            announcement.epoch,
132            announcement.checkpoint_id,
133        );
134        if self
135            .barrier
136            .prepare_settlement_covers(attempt.checkpoint_id)
137        {
138            return Ok(None);
139        }
140        let received_at = self
141            .barrier
142            .prepare_received_at_or_insert(&announcement, observation_started.into_std())
143            .ok_or_else(|| "checkpoint Prepare lost its exact observation clock".to_string())?;
144        let attempt_deadline = tokio::time::Instant::from_std(received_at)
145            .checked_add(checkpoint_timeout)
146            .ok_or_else(|| "checkpoint Prepare attempt deadline overflowed".to_string())?;
147        if tokio::time::Instant::now() >= attempt_deadline {
148            return self
149                .ignore_durably_settled_prepare(
150                    attempt,
151                    observation_deadline,
152                    "checkpoint Prepare attempt deadline elapsed before authority validation",
153                )
154                .await;
155        }
156        let validation = tokio::time::timeout_at(
157            attempt_deadline,
158            self.barrier.validate_checkpoint_prepare(&announcement),
159        )
160        .await;
161        let validation_error = match validation {
162            Ok(Ok(())) => None,
163            Ok(Err(error)) => Some(error),
164            Err(_) => Some("checkpoint Prepare authority validation timed out".to_string()),
165        };
166        if let Some(error) = validation_error {
167            return self
168                .ignore_durably_settled_prepare(attempt, observation_deadline, &error)
169                .await;
170        }
171        let proof = announcement
172            .leader_proof
173            .as_ref()
174            .filter(|proof| proof.is_canonical())
175            .ok_or_else(|| "leader Prepare omitted its canonical authority proof".to_string())?;
176        let assignment_error = match announcement.assignment_fence.as_ref() {
177            None => Some(
178                "[LDB-6055] leader Prepare omitted its canonical assignment certificate"
179                    .to_string(),
180            ),
181            Some(fence) if !fence.is_canonical() => Some(
182                "[LDB-6055] leader Prepare carried a non-canonical assignment certificate"
183                    .to_string(),
184            ),
185            Some(fence) => self
186                .checkpoint_assignment_fence(fence.assignment_version)
187                .and_then(|certified| {
188                    self.checkpoint_assignment_fence_after_authority_validation(certified, proof)
189                })
190                .map_or_else(
191                    || {
192                        Some(format!(
193                            "[LDB-6055] follower cannot certify leader Prepare assignment {}",
194                            fence.assignment_version
195                        ))
196                    },
197                    |certified| {
198                        (certified != *fence).then(|| {
199                            format!(
200                                "[LDB-6055] follower assignment differs from leader Prepare assignment {}",
201                                fence.assignment_version
202                            )
203                        })
204                    },
205                ),
206        };
207        Ok(Some(match assignment_error {
208            Some(error) => CheckpointPrepareObservation::AssignmentRejected {
209                announcement,
210                error,
211            },
212            None => CheckpointPrepareObservation::AssignmentReady(announcement),
213        }))
214    }
215
216    #[cfg(feature = "cluster")]
217    async fn ignore_durably_settled_prepare(
218        &self,
219        attempt: crate::checkpoint::CheckpointAttempt,
220        deadline: tokio::time::Instant,
221        validation_error: &str,
222    ) -> Result<Option<CheckpointPrepareObservation>, String> {
223        if self
224            .barrier
225            .prepare_settlement_covers(attempt.checkpoint_id)
226        {
227            return Ok(None);
228        }
229        if tokio::time::Instant::now() >= deadline {
230            return Err(validation_error.to_owned());
231        }
232        let authority = self.checkpoint_authority().map_err(|error| {
233            format!(
234                "{validation_error}; checkpoint Prepare settlement authority is unavailable: {error}"
235            )
236        })?;
237        let settlement =
238            tokio::time::timeout_at(deadline, authority.cluster_attempt_settlement(attempt))
239                .await
240                .map_err(|_| {
241                    format!("{validation_error}; checkpoint Prepare settlement audit timed out")
242                })?
243                .map_err(|error| {
244                    format!(
245                        "{validation_error}; checkpoint Prepare settlement audit failed: {error}"
246                    )
247                })?;
248        let Some(settlement) = settlement else {
249            return Err(validation_error.to_owned());
250        };
251        let settled =
252            crate::checkpoint::CheckpointAttempt::new(settlement.epoch, settlement.checkpoint_id);
253        match settled.relation_to(attempt) {
254            crate::checkpoint::CheckpointAttemptRelation::Exact
255            | crate::checkpoint::CheckpointAttemptRelation::Newer => {
256                self.barrier
257                    .record_prepare_settlement(settled.checkpoint_id);
258                Ok(None)
259            }
260            crate::checkpoint::CheckpointAttemptRelation::Older
261            | crate::checkpoint::CheckpointAttemptRelation::Conflict => Err(format!(
262                "{validation_error}; checkpoint Prepare settlement {settled:?} does not close {attempt:?}"
263            )),
264        }
265    }
266
267    /// Subscribe to direct checkpoint announcements. Consumers must retain a bounded KV poll:
268    /// the watch is a latency path, while the merged gossip history remains the fallback.
269    #[cfg(feature = "cluster")]
270    #[must_use]
271    pub fn checkpoint_announcement_watch(
272        &self,
273    ) -> Option<watch::Receiver<Option<BarrierAnnouncement>>> {
274        self.barrier.announcement_watch()
275    }
276
277    /// Stable local monotonic receipt or first-observation time for this exact Prepare.
278    #[must_use]
279    pub fn checkpoint_prepare_received_at(
280        &self,
281        prepare: &BarrierAnnouncement,
282    ) -> Option<std::time::Instant> {
283        self.barrier.prepare_received_at(prepare)
284    }
285
286    /// Follower-side ack.
287    ///
288    /// # Errors
289    /// Propagates [`BarrierCoordinator::ack`] errors.
290    pub async fn ack_barrier(&self, ack: &BarrierAck) -> Result<(), String> {
291        if !self.process_lease_is_live() {
292            return Err("stable node process lease is no longer live".into());
293        }
294        self.barrier.ack(ack).await
295    }
296}