laminar_core/cluster/control/controller/
recovery_identity.rs1use super::*;
4
5impl ClusterController {
6 pub(super) async fn write_recovery_value(
7 &self,
8 key: &str,
9 value: String,
10 ) -> Result<(), String> {
11 if !self.process_lease_is_live() {
12 return Err("stable node process lease is no longer live".into());
13 }
14 tokio::time::timeout(
15 RECOVERY_CONTROL_IO_TIMEOUT,
16 self.recovery_kv.write_checked(key, value),
17 )
18 .await
19 .map_err(|_| format!("recovery control write for {key} timed out"))?
20 .map_err(|error| format!("recovery control write for {key} failed: {error}"))
21 }
22
23 pub(super) async fn write_recovery_value_exact(
24 &self,
25 key: &str,
26 value: String,
27 ) -> Result<(), String> {
28 self.write_recovery_value(key, value.clone()).await?;
29 let observed = self
30 .read_recovery_value(self.instance_id, key)
31 .await?
32 .ok_or_else(|| format!("recovery control value for {key} vanished after write"))?;
33 if observed != value {
34 return Err(format!(
35 "recovery control read-back mismatch for {key}; write was not durable"
36 ));
37 }
38 Ok(())
39 }
40
41 pub(super) async fn read_recovery_value(
42 &self,
43 node: NodeId,
44 key: &str,
45 ) -> Result<Option<String>, String> {
46 tokio::time::timeout(
47 RECOVERY_CONTROL_IO_TIMEOUT,
48 self.recovery_kv.read_from_checked(node, key),
49 )
50 .await
51 .map_err(|_| format!("recovery control read for {key} from {node} timed out"))?
52 .map_err(|error| format!("recovery control read for {key} from {node} failed: {error}"))
53 }
54
55 pub(super) async fn scan_recovery_values(
56 &self,
57 key: &str,
58 ) -> Result<Vec<(NodeId, String)>, String> {
59 tokio::time::timeout(
60 RECOVERY_CONTROL_IO_TIMEOUT,
61 self.recovery_kv.scan_checked(key),
62 )
63 .await
64 .map_err(|_| format!("recovery control scan for {key} timed out"))?
65 .map_err(|error| format!("recovery control scan for {key} failed: {error}"))
66 }
67
68 pub async fn max_recovery_generation(&self) -> Result<u64, String> {
73 let mut maximum = 0;
74 for (node, raw) in self.scan_recovery_values("control:recovery-gen").await? {
75 let generation = raw.parse::<u64>().map_err(|error| {
76 format!("invalid replicated recovery generation from {node}: {error}")
77 })?;
78 maximum = maximum.max(generation);
79 }
80 Ok(maximum)
81 }
82
83 pub async fn adopt_recovery_generation(&self, generation: u64) -> Result<(), String> {
88 if generation == 0 {
89 return Err("recovery generation must be nonzero".into());
90 }
91 let current = self
92 .read_recovery_value(self.instance_id, "control:recovery-gen")
93 .await?
94 .map(|raw| {
95 raw.parse::<u64>()
96 .map_err(|error| format!("invalid local recovery generation: {error}"))
97 })
98 .transpose()?;
99 if let Some(current) = current {
100 if current > generation {
101 return Err(format!(
102 "local recovery generation {current} is newer than proposed generation {generation}"
103 ));
104 }
105 if current == generation {
106 return Ok(());
107 }
108 }
109 self.write_recovery_value_exact("control:recovery-gen", generation.to_string())
110 .await
111 }
112
113 pub(super) async fn read_recovery_stopped_reports(
114 &self,
115 round: &RecoveryRound,
116 roster: &[CheckpointParticipant],
117 ) -> Result<Vec<RecoveryStoppedReport>, RecoveryControlError> {
118 round.validate().map_err(RecoveryControlError::Conflict)?;
119 if roster
120 .windows(2)
121 .any(|pair| pair[0].node_id >= pair[1].node_id)
122 || roster.iter().any(|participant| {
123 round.stopped_participant_incarnation(NodeId(participant.node_id))
124 != Some(participant.boot_incarnation)
125 })
126 {
127 return Err(RecoveryControlError::Conflict(
128 "recovery stopped-report read roster is not a canonical round subset".into(),
129 ));
130 }
131 let reads = futures::stream::iter(roster.iter().copied().map(|participant| async move {
132 let value = self
133 .read_recovery_value(NodeId(participant.node_id), RECOVERY_STOPPED_REPORT_KEY)
134 .await;
135 (participant, value)
136 }))
137 .buffer_unordered(CONTROL_ROSTER_IO_CONCURRENCY)
138 .collect::<Vec<_>>()
139 .await;
140 let mut reports = Vec::new();
141 for (participant, value) in reads {
142 let Some(raw) = value.map_err(RecoveryControlError::Uncertain)? else {
143 continue;
144 };
145 let publisher = NodeId(participant.node_id);
146 let report = parse_recovery_stopped_report_shape(&raw, publisher)
147 .map_err(RecoveryControlError::Conflict)?;
148 if report.round_id.generation < round.id.generation {
149 continue;
150 }
151 if report.round_id.generation > round.id.generation {
152 let adopted = self
156 .read_recovery_value(publisher, "control:recovery-gen")
157 .await
158 .map_err(RecoveryControlError::Uncertain)?;
159 let incarnation = self
160 .read_recovery_value(publisher, RECOVERY_INCARNATION_KEY)
161 .await
162 .map_err(RecoveryControlError::Uncertain)?;
163 let Some((adopted, incarnation)) = adopted.zip(incarnation) else {
164 continue;
165 };
166 let adopted = adopted.parse::<u64>().map_err(|error| {
167 RecoveryControlError::Conflict(format!(
168 "invalid replicated recovery generation from {publisher}: {error}"
169 ))
170 })?;
171 let incarnation = Uuid::parse_str(&incarnation).map_err(|error| {
172 RecoveryControlError::Conflict(format!(
173 "invalid recovery incarnation published by {publisher}: {error}"
174 ))
175 })?;
176 if adopted >= report.round_id.generation
177 && incarnation == report.publisher.boot_incarnation
178 {
179 reports.push(report);
180 }
181 continue;
182 }
183 report.validate_semantics(round).map_err(|error| {
184 RecoveryControlError::Conflict(format!(
185 "same-generation recovery stopped report from {publisher} conflicts with the exact frozen round: {error}"
186 ))
187 })?;
188 reports.push(report);
189 }
190 reports.sort_unstable_by_key(|report| report.publisher.node_id);
191 Ok(reports)
192 }
193
194 pub(super) async fn read_recovery_announcement_map(
195 &self,
196 key: &str,
197 ) -> Result<Vec<(NodeId, RecoveryAnnouncement)>, String> {
198 let mut announcements = Vec::new();
199 for (node, raw) in self.scan_recovery_values(key).await? {
200 let announcement = parse_recovery_announcement_ack(&raw, node)?;
201 announcements.push((node, announcement));
202 }
203 Ok(announcements)
204 }
205
206 #[must_use]
208 pub fn recovery_incarnation(&self) -> Uuid {
209 self.recovery_incarnation
210 }
211
212 pub fn try_live_local_process_authority_identity(
222 &self,
223 ) -> Result<LocalProcessAuthorityIdentity, String> {
224 let _transition = self
225 .process_authority_transition
226 .try_lock()
227 .ok_or_else(|| "local process authority transition is in progress".to_string())?;
228 self.capture_live_local_process_authority_locked()
229 }
230
231 pub async fn read_local_process_authority_evidence(
250 &self,
251 ) -> Result<LocalProcessAuthorityEvidence, LocalProcessAuthorityEvidenceError> {
252 let before = self
253 .capture_live_local_process_authority()
254 .map_err(LocalProcessAuthorityEvidenceError::Unavailable)?;
255 let node = NodeId(before.participant.node_id);
256 let adopted_raw = self
257 .read_recovery_value(node, ADOPTED_ASSIGNMENT_KEY)
258 .await
259 .map_err(LocalProcessAuthorityEvidenceError::Unavailable)?;
260
261 let after_read = self
262 .capture_live_local_process_authority()
263 .map_err(LocalProcessAuthorityEvidenceError::Unavailable)?;
264 if after_read != before {
265 return Err(LocalProcessAuthorityEvidenceError::Unavailable(
266 "local process authority changed while reading retained evidence".into(),
267 ));
268 }
269
270 let adopted_raw = adopted_raw.ok_or_else(|| {
271 LocalProcessAuthorityEvidenceError::Unavailable(
272 "local process has no durable assignment adoption".into(),
273 )
274 })?;
275 let adoption = parse_local_adopted_assignment(&adopted_raw, before.participant)
276 .map_err(LocalProcessAuthorityEvidenceError::Invalid)?
277 .ok_or_else(|| {
278 LocalProcessAuthorityEvidenceError::Unavailable(
279 "durable assignment adoption belongs to a prior local boot".into(),
280 )
281 })?;
282 let expected_fence = match self.checkpoint_assignment_fence(adoption.assignment_version) {
283 Some(fence) if adoption.matches_fence(&fence) => fence,
284 Some(_) => {
285 return Err(LocalProcessAuthorityEvidenceError::Invalid(
286 "durable local adoption contradicts the same-version audited assignment fence"
287 .into(),
288 ));
289 }
290 None => {
291 return Err(LocalProcessAuthorityEvidenceError::Unavailable(
292 "matching locally audited assignment fence is unavailable".into(),
293 ));
294 }
295 };
296
297 let after = self
298 .capture_live_local_process_authority()
299 .map_err(LocalProcessAuthorityEvidenceError::Unavailable)?;
300 if after != before {
301 return Err(LocalProcessAuthorityEvidenceError::Unavailable(
302 "local process authority changed while reading retained evidence".into(),
303 ));
304 }
305 match self.checkpoint_assignment_fence(adoption.assignment_version) {
306 Some(fence) if fence == expected_fence && adoption.matches_fence(&fence) => {}
307 Some(_) => {
308 return Err(LocalProcessAuthorityEvidenceError::Invalid(
309 "locally audited assignment fence changed or contradicted its durable adoption at the same version"
310 .into(),
311 ));
312 }
313 None => {
314 return Err(LocalProcessAuthorityEvidenceError::Unavailable(
315 "matching locally audited assignment fence changed during evidence capture"
316 .into(),
317 ));
318 }
319 }
320
321 Ok(LocalProcessAuthorityEvidence {
322 participant: after.participant,
323 process_term: after.process_term,
324 adopted_assignment: adoption,
325 })
326 }
327
328 pub async fn publish_recovery_incarnation(&self) -> Result<(), String> {
336 self.write_recovery_value(
337 RECOVERY_INCARNATION_KEY,
338 self.recovery_incarnation.to_string(),
339 )
340 .await?;
341 let observed = self
342 .read_recovery_value(self.instance_id, RECOVERY_INCARNATION_KEY)
343 .await?
344 .ok_or_else(|| "recovery incarnation was not readable after publication".to_string())?;
345 let observed = Uuid::parse_str(&observed)
346 .map_err(|error| format!("invalid recovery incarnation after publication: {error}"))?;
347 if observed != self.recovery_incarnation {
348 return Err("recovery incarnation read-back mismatch".into());
349 }
350 Ok(())
351 }
352
353 pub async fn publish_leased_recovery_incarnation(
358 &self,
359 lease: &super::super::ProcessLease,
360 ) -> Result<(), String> {
361 lease
362 .validate(self.instance_id)
363 .map_err(|error| error.to_string())?;
364 if lease.node != self.instance_id || lease.owner != self.recovery_incarnation {
365 return Err("process lease does not bind this recovery incarnation".into());
366 }
367 if !self.recovery_process_lease_is_live() {
368 return Err("live process lease deadline is not installed".into());
369 }
370 let publisher = RecoveryFaultPublisher {
371 participant: CheckpointParticipant {
372 node_id: lease.node.0,
373 boot_incarnation: lease.owner,
374 },
375 process_term: lease.term,
376 };
377 if !self
378 .recovery_fault_publisher_is_current(publisher)
379 .await
380 .map_err(|error| error.to_string())?
381 {
382 return Err("process lease is not the current durable stable-node term".into());
383 }
384 if !self.recovery_process_lease_is_live() {
385 return Err("process lease changed before publishing recovery identity".into());
386 }
387 self.barrier.install_local_process_lease(lease)?;
388 if let Err(error) = self.publish_recovery_incarnation().await {
389 self.fence_process_lease();
390 return Err(error);
391 }
392 if !self.recovery_process_lease_is_live() {
393 self.fence_process_lease();
394 return Err("process lease changed while publishing recovery identity".into());
395 }
396 match self.recovery_fault_publisher_is_current(publisher).await {
397 Ok(true) if self.recovery_process_lease_is_live() => {}
398 Ok(_) => {
399 self.fence_process_lease();
400 return Err("process lease changed while publishing recovery identity".into());
401 }
402 Err(error) => {
403 self.fence_process_lease();
404 return Err(format!(
405 "process lease authority became uncertain after recovery publication: {error}"
406 ));
407 }
408 }
409 self.recovery_process_term
410 .store(lease.term, Ordering::Release);
411 Ok(())
412 }
413}