laminar_core/cluster/control/controller/
assignment.rs1use super::*;
4
5impl ClusterController {
6 #[must_use]
9 pub fn assignable_instances(&self) -> Vec<NodeId> {
10 let mut ids = assignable_node_ids(&self.members_rx.borrow());
11 ids.retain(|id| *id != self.instance_id);
12 if self.active.load(Ordering::SeqCst)
13 && !self.is_draining()
14 && !self.instance_id.is_unassigned()
15 {
16 ids.push(self.instance_id);
17 }
18 ids.sort_unstable();
19 ids.dedup();
20 ids
21 }
22
23 pub fn set_self_locality(&self, locality: Locality) {
25 *self.self_locality.write() = locality;
26 }
27
28 #[must_use]
31 pub fn assignable_with_locality(&self) -> Vec<(NodeId, Locality)> {
32 let members = self.members_rx.borrow();
33 self.assignable_instances()
34 .into_iter()
35 .map(|id| {
36 let locality = if id == self.instance_id {
37 self.self_locality.read().clone()
38 } else {
39 members
40 .iter()
41 .find(|m| m.id == id)
42 .and_then(|m| m.metadata.failure_domain.as_deref())
43 .map(Locality::parse)
44 .unwrap_or_default()
45 };
46 (id, locality)
47 })
48 .collect()
49 }
50
51 #[must_use]
53 pub fn members_watch(&self) -> watch::Receiver<Vec<NodeInfo>> {
54 self.members_rx.clone()
55 }
56
57 pub(super) async fn drain_transition_authority_is_current(
58 &self,
59 transition: &AssignmentDrainTransition,
60 ) -> Result<bool, String> {
61 #[cfg(feature = "cluster")]
62 {
63 let authority = self
64 .checkpoint_authority()
65 .map_err(|error| error.to_string())?;
66 let Some(lease) = authority.load().await.map_err(|error| error.to_string())? else {
67 return Ok(false);
68 };
69 Ok(lease.matches_proof(&transition.leader))
70 }
71 #[cfg(not(feature = "cluster"))]
72 {
73 let _ = transition;
74 Err("assignment drain authority requires the cluster feature".into())
75 }
76 }
77
78 pub async fn announce_drain_ack(
85 &self,
86 transition: &AssignmentDrainTransition,
87 ) -> Result<(), String> {
88 if !transition.is_canonical() {
89 return Err("drain acknowledgement requires an exact assignment transition".into());
90 }
91 let participant = CheckpointParticipant {
92 node_id: self.instance_id.0,
93 boot_incarnation: self.recovery_incarnation,
94 };
95 if transition
96 .predecessor
97 .participant_incarnation(participant.node_id)
98 != Some(participant.boot_incarnation)
99 {
100 return Err("drain acknowledgement does not bind the local process".into());
101 }
102 if self.checkpoint_drain_transition.borrow().as_ref() != Some(transition) {
103 return Err("drain acknowledgement is not the locally audited transition".into());
104 }
105 if !self.process_lease_is_live()
106 || !self
107 .drain_transition_authority_is_current(transition)
108 .await?
109 {
110 return Err("drain acknowledgement authority is no longer live".into());
111 }
112 if !self.recovery_incarnation_is_current().await? {
113 return Err("drain acknowledgement came from a superseded local process".into());
114 }
115 self.write_recovery_value_exact(
116 DRAIN_ACK_KEY,
117 encode_drain_ack(&DrainAck::for_transition(participant, transition))?,
118 )
119 .await
120 }
121
122 pub async fn drain_ack_quorum_reached(
134 &self,
135 transition: &AssignmentDrainTransition,
136 ) -> Result<bool, String> {
137 if !transition.is_canonical() {
138 return Err("drain quorum requires a canonical assignment transition".into());
139 }
140 let locally_audited =
141 self.checkpoint_drain_transition.borrow().as_ref() == Some(transition);
142 if !locally_audited
143 || !self
144 .drain_transition_authority_is_current(transition)
145 .await?
146 {
147 return Ok(false);
148 }
149 if self
150 .recovery_participant_incarnations(&transition.predecessor.participant_ids())
151 .await?
152 != transition.predecessor.participants
153 {
154 return Ok(false);
155 }
156
157 let expected: std::collections::BTreeSet<NodeId> = transition
158 .required_participants()
159 .iter()
160 .map(|participant| NodeId(participant.node_id))
161 .collect();
162 let mut seen = std::collections::BTreeSet::new();
163 let mut matching = std::collections::BTreeSet::new();
164 for (publisher, raw) in self.scan_recovery_values(DRAIN_ACK_KEY).await? {
165 if !expected.contains(&publisher) {
166 continue;
167 }
168 if !seen.insert(publisher) {
169 return Err(format!(
170 "duplicate drain acknowledgement from expected participant {publisher}"
171 ));
172 }
173 let ack = parse_drain_ack(&raw, publisher)?;
174 if ack.matches_transition(transition) {
175 matching.insert(publisher);
176 }
177 }
178 if matching != expected {
179 return Ok(false);
180 }
181 let Some(snapshot_store) = self.snapshot.as_ref() else {
185 return Ok(false);
186 };
187 let Some(head) = snapshot_store
188 .load()
189 .await
190 .map_err(|error| error.to_string())?
191 else {
192 return Ok(false);
193 };
194 if !head.draining
195 || head.drain_transition.as_ref() != Some(transition)
196 || head.assignment_fence().map_err(|error| error.to_string())? != transition.target
197 {
198 return Ok(false);
199 }
200 if self
205 .recovery_participant_incarnations(&transition.predecessor.participant_ids())
206 .await?
207 != transition.predecessor.participants
208 || !self
209 .drain_transition_authority_is_current(transition)
210 .await?
211 {
212 return Ok(false);
213 }
214 let authority = self
215 .checkpoint_authority()
216 .map_err(|error| error.to_string())?;
217 if authority
218 .assignment_drain_decision(transition.target.assignment_version)
219 .await
220 .map_err(|error| error.to_string())?
221 .is_some()
222 {
223 return Ok(false);
224 }
225 Ok(
226 self.checkpoint_drain_transition.borrow().as_ref() == Some(transition)
227 && self.process_lease_is_live(),
228 )
229 }
230
231 pub async fn announce_adopted_assignment(
236 &self,
237 adoption: &CheckpointAssignmentAdoption,
238 ) -> Result<(), String> {
239 if !adoption.is_canonical()
240 || adoption.participant.node_id != self.instance_id.0
241 || adoption.participant.boot_incarnation != self.recovery_incarnation
242 || !self.recovery_incarnation_is_current().await?
243 {
244 return Err("adopted assignment report does not bind the current process".into());
245 }
246 let encoded = serde_json::to_string(adoption)
247 .map_err(|error| format!("could not encode adopted assignment: {error}"))?;
248 self.write_recovery_value_exact(ADOPTED_ASSIGNMENT_KEY, encoded)
249 .await
250 }
251
252 pub async fn read_adopted_assignments(
257 &self,
258 ) -> Result<Vec<(NodeId, CheckpointAssignmentAdoption)>, String> {
259 self.scan_recovery_values(ADOPTED_ASSIGNMENT_KEY)
260 .await?
261 .into_iter()
262 .map(|(node, raw)| {
263 let adoption: CheckpointAssignmentAdoption = serde_json::from_str(&raw)
264 .map_err(|error| format!("invalid adopted assignment from {node}: {error}"))?;
265 if !adoption.is_canonical() || adoption.participant.node_id != node.0 {
266 return Err(format!(
267 "adopted assignment from {node} has a non-canonical publisher"
268 ));
269 }
270 Ok((node, adoption))
271 })
272 .collect()
273 }
274
275 pub fn publish_checkpoint_assignment_fence(&self, fence: Option<CheckpointAssignmentFence>) {
278 let _transition = self.process_authority_transition.lock();
279 let fence = if self.process_lease_is_live() {
280 fence
281 } else {
282 None
283 };
284 if let Some(fence) = fence.as_ref().filter(|fence| fence.is_canonical()) {
285 let participants = fence.participant_ids();
286 let changed = {
287 let mut installed = self.leadership_participants.write();
288 match installed.as_ref() {
289 Some((version, _)) if *version >= fence.assignment_version => false,
290 _ => {
291 *installed = Some((fence.assignment_version, participants));
292 true
293 }
294 }
295 };
296 if changed {
297 self.notify_leader_eligibility_change();
298 }
299 }
300 self.checkpoint_assignment_fence.send_replace(fence);
301 }
302
303 pub fn publish_checkpoint_drain_transition(
305 &self,
306 transition: Option<AssignmentDrainTransition>,
307 ) {
308 let _transition = self.process_authority_transition.lock();
309 let transition = if self.process_lease_is_live() {
310 transition
311 } else {
312 None
313 };
314 self.checkpoint_drain_transition.send_replace(transition);
315 }
316
317 #[must_use]
322 pub fn clear_checkpoint_drain_transition_if_matches(
323 &self,
324 expected: &AssignmentDrainTransition,
325 ) -> bool {
326 let _transition = self.process_authority_transition.lock();
327 let matches = self.checkpoint_drain_transition.borrow().as_ref() == Some(expected);
328 if matches {
329 self.checkpoint_drain_transition.send_replace(None);
330 }
331 matches
332 }
333
334 #[must_use]
336 pub fn checkpoint_drain_transition(&self) -> Option<AssignmentDrainTransition> {
337 self.checkpoint_drain_transition.borrow().clone()
338 }
339
340 #[must_use]
343 pub fn checkpoint_assignment_watch(
344 &self,
345 ) -> watch::Receiver<Option<CheckpointAssignmentFence>> {
346 self.checkpoint_assignment_fence.subscribe()
347 }
348
349 #[must_use]
353 pub fn checkpoint_assignment_fence(
354 &self,
355 assignment_version: u64,
356 ) -> Option<CheckpointAssignmentFence> {
357 let checkpoint_capable: Vec<u64> = self
358 .checkpoint_instances()
359 .into_iter()
360 .map(|node| node.0)
361 .collect();
362 self.checkpoint_assignment_fence
363 .borrow()
364 .as_ref()
365 .filter(|fence| {
366 fence.is_canonical()
367 && fence.assignment_version == assignment_version
368 && fence
369 .participant_incarnation(self.instance_id.0)
370 .is_none_or(|incarnation| incarnation == self.recovery_incarnation)
371 && fence.participants.iter().all(|participant| {
372 checkpoint_capable
373 .binary_search(&participant.node_id)
374 .is_ok()
375 })
376 })
377 .cloned()
378 }
379
380 #[cfg(feature = "cluster")]
384 pub async fn checkpoint_assignment_fence_for_leader(
385 &self,
386 assignment_version: u64,
387 leader: &crate::checkpoint::LeaderProof,
388 ) -> Option<CheckpointAssignmentFence> {
389 if !leader.is_canonical() {
390 return None;
391 }
392 let fence = self.checkpoint_assignment_fence(assignment_version)?;
393 let authority = self.checkpoint_authority().ok()?;
394 let lease = authority.load().await.ok().flatten()?;
395 if !lease.matches_proof(leader) {
396 return None;
397 }
398 self.checkpoint_assignment_fence_after_authority_validation(fence, leader)
399 }
400
401 #[cfg(feature = "cluster")]
402 pub(super) fn checkpoint_assignment_fence_after_authority_validation(
403 &self,
404 fence: CheckpointAssignmentFence,
405 leader: &crate::checkpoint::LeaderProof,
406 ) -> Option<CheckpointAssignmentFence> {
407 let transition = self.checkpoint_drain_transition.borrow();
408 if transition
409 .as_ref()
410 .is_some_and(|transition| transition.predecessor == fence)
411 && transition.as_ref().map(|transition| &transition.leader) != Some(leader)
412 {
413 return None;
414 }
415 Some(fence)
416 }
417
418 #[cfg(feature = "cluster")]
423 pub async fn start_barrier_server(
424 &self,
425 bind_addr: std::net::SocketAddr,
426 advertise_host: Option<String>,
427 ) -> Result<std::net::SocketAddr, String> {
428 self.barrier.start_server(bind_addr, advertise_host).await
429 }
430
431 #[cfg(feature = "cluster")]
437 pub async fn start_leased_barrier_server(
438 &self,
439 bind_addr: std::net::SocketAddr,
440 advertise_host: Option<String>,
441 process_lease: &super::super::ProcessLease,
442 ) -> Result<std::net::SocketAddr, String> {
443 process_lease
444 .validate(self.instance_id)
445 .map_err(|error| error.to_string())?;
446 if process_lease.owner != self.recovery_incarnation {
447 return Err("control endpoint lease does not bind this process incarnation".into());
448 }
449 if !self.recovery_process_lease_is_live() {
450 return Err("live process lease deadline is not installed".into());
451 }
452 self.barrier.install_local_process_lease(process_lease)?;
453 self.barrier.start_server(bind_addr, advertise_host).await
454 }
455
456 #[cfg(feature = "cluster")]
463 pub async fn confirm_remote_leader_proof(
464 &self,
465 proof: &LeaderProof,
466 deadline: tokio::time::Instant,
467 ) -> Result<bool, String> {
468 self.barrier
469 .confirm_remote_leader_proof(proof, deadline)
470 .await
471 }
472
473 #[cfg(feature = "cluster")]
474 pub(super) async fn confirm_assignment_leader_proof(
475 &self,
476 leader: NodeId,
477 proof: &LeaderProof,
478 deadline: tokio::time::Instant,
479 ) -> Result<(), String> {
480 if tokio::time::Instant::now() >= deadline {
481 return Err("assignment leader authority audit deadline expired".into());
482 }
483 if proof.owner.node_id != leader.0 {
484 return Err("assignment leader proof does not match the current leader".into());
485 }
486 let confirmed = if leader == self.instance_id {
487 self.capture_leader_proof().as_ref() == Some(proof)
488 } else {
489 self.confirm_remote_leader_proof(proof, deadline).await?
490 };
491 if !confirmed {
492 return Err(format!(
493 "assignment leader process {} has no live exact grant",
494 leader.0
495 ));
496 }
497 Ok(())
498 }
499
500 #[cfg(feature = "cluster")]
511 pub async fn audit_assignment_leader_authority(
512 &self,
513 fence: &CheckpointAssignmentFence,
514 expected_proof: Option<&LeaderProof>,
515 deadline: tokio::time::Instant,
516 ) -> Result<LeaderProof, String> {
517 if tokio::time::Instant::now() >= deadline {
518 return Err("assignment leader authority audit deadline expired".into());
519 }
520 if !fence.is_canonical()
521 || self
522 .checkpoint_assignment_fence(fence.assignment_version)
523 .as_ref()
524 != Some(fence)
525 {
526 return Err(
527 "assignment leader authority audit requires the exact installed fence".into(),
528 );
529 }
530 if expected_proof.is_some_and(|proof| !proof.is_canonical()) {
531 return Err("assignment leader authority audit expected proof is not canonical".into());
532 }
533
534 let leader = self
535 .current_leader()
536 .ok_or_else(|| "assignment leader authority audit has no current leader".to_string())?;
537 let participant = fence
538 .participants
539 .iter()
540 .find(|participant| participant.node_id == leader.0)
541 .copied()
542 .ok_or_else(|| {
543 "current leader is not a participant in the exact assignment fence".to_string()
544 })?;
545 let authority = self
546 .checkpoint_authority()
547 .map_err(|error| format!("assignment leader authority is unavailable: {error}"))?;
548 let initial = tokio::time::timeout_at(deadline, authority.load())
549 .await
550 .map_err(|_| "assignment leader authority initial read timed out".to_string())?
551 .map_err(|error| format!("assignment leader authority initial read failed: {error}"))?
552 .ok_or_else(|| "assignment leader authority has no durable grant".to_string())?;
553 if initial.owner.node != leader || initial.owner.boot != participant.boot_incarnation {
554 return Err(
555 "durable leader grant does not match the elected assignment participant".into(),
556 );
557 }
558 let proof = match expected_proof {
559 Some(expected) if initial.matches_proof(expected) => expected.clone(),
560 Some(_) => {
561 return Err(
562 "durable leader grant does not match the drain-bound expected proof".into(),
563 );
564 }
565 None => initial.proof(),
566 };
567
568 self.confirm_assignment_leader_proof(leader, &proof, deadline)
569 .await?;
570 let process_authority = self
571 .process_lease_authority
572 .get()
573 .ok_or_else(|| "process lease authority is not installed".to_string())?;
574 let process_current = process_authority
575 .verify_current_participant_term(participant, proof.owner.process_term, deadline)
576 .await
577 .map_err(|error| {
578 format!("assignment leader process-term verification failed: {error}")
579 })?;
580 if !process_current {
581 return Err("assignment leader durable process term is no longer current".to_string());
582 }
583 self.confirm_assignment_leader_proof(leader, &proof, deadline)
584 .await?;
585
586 let final_grant = tokio::time::timeout_at(deadline, authority.load())
587 .await
588 .map_err(|_| "assignment leader authority final read timed out".to_string())?
589 .map_err(|error| format!("assignment leader authority final read failed: {error}"))?
590 .ok_or_else(|| "assignment leader authority grant disappeared".to_string())?;
591 if final_grant.owner != initial.owner
592 || final_grant.token != initial.token
593 || final_grant.seq < initial.seq
594 || !final_grant.matches_proof(&proof)
595 {
596 return Err("assignment leader durable grant changed during the audit".into());
597 }
598 if self.current_leader() != Some(leader)
599 || self
600 .checkpoint_assignment_fence(fence.assignment_version)
601 .as_ref()
602 != Some(fence)
603 {
604 return Err("assignment leader or exact fence changed during the audit".into());
605 }
606 if tokio::time::Instant::now() >= deadline {
607 return Err("assignment leader authority audit deadline expired".into());
608 }
609 Ok(proof)
610 }
611}