laminar_core/cluster/control/leader_lease/
topology_activation.rs1use uuid::Uuid;
4
5use super::topology_admission::{CONTROL_TIMEOUT, MAX_ADMISSION_ATTEMPTS};
6use super::{
7 read_authority_record, AssignmentSnapshotStore, AuthorityCreateOutcome, LeaderLeaseStore,
8 LeaseError, RecoveryFaultDisposition, TOPOLOGY_INSTALLATION_RECORD_VERSION,
9};
10use crate::checkpoint::LeaderProof;
11use crate::cluster::control::{
12 ProcessLeaseAuthority, TopologyActivation, TopologyAdmissionPhase, TopologyAdmissionStatus,
13 TopologyError, TopologyInstallationReceipt, TopologyRelease, TopologyRestoreInput,
14 TOPOLOGY_INSTALLATION_PROTOCOL_VERSION,
15};
16
17impl LeaderLeaseStore {
18 pub async fn authorize_topology_release(
24 &self,
25 assignments: &AssignmentSnapshotStore,
26 processes: &ProcessLeaseAuthority,
27 input: &TopologyRestoreInput,
28 runtime_id: Uuid,
29 ) -> Result<bool, TopologyError> {
30 tokio::time::timeout(CONTROL_TIMEOUT, async {
31 let head = self.load_record().await?.ok_or(TopologyError::Fenced)?;
32 let fresh = self
33 .committed_topology_restore_input(
34 assignments,
35 processes,
36 input.operation().operation_id,
37 input.process(),
38 )
39 .await?;
40 if !fresh.same_installed_generation(input)
41 || head.committed_topology_operation() != Some(fresh.operation())
42 {
43 return Ok(false);
44 }
45 Self::require_topology_release_authority(&head, &fresh)?;
46 let Some(round) = &fresh.operation().activation else {
47 return Ok(false);
48 };
49 if fresh.operation().phase != TopologyAdmissionPhase::Active
50 || round.release.is_none()
51 || round.assignment != *fresh.assignment()
52 || round.processes != fresh.processes()
53 || !round.installation_complete()
54 || !round.installations.iter().any(|receipt| {
55 receipt.process == input.process() && receipt.runtime_id == runtime_id
56 })
57 {
58 return Ok(false);
59 }
60 if round.recovery_round.is_some() {
61 return Ok(false);
64 }
65 if let Some(recovery) = head.recovery_release_head.as_ref().filter(|recovery| {
66 round
67 .release
68 .as_ref()
69 .is_some_and(|release| recovery.sequence > release.authority_sequence)
70 }) {
71 let terminal = self
72 .recovery_release_terminal_from(&head, recovery)
73 .await
74 .map_err(super::topology_admission::topology_checkpoint_error)?;
75 if terminal.round.topology_binding().is_some_and(|binding| {
76 fresh.operation().commit.as_ref() == Some(binding.commit())
77 }) {
78 return Ok(false);
79 }
80 }
81 for process in &round.processes {
82 self.require_topology_process(processes, *process).await?;
83 }
84 let after = self.load_record().await?.ok_or(TopologyError::Fenced)?;
85 Self::require_topology_release_authority(&after, &fresh)?;
86 Ok(after.lease.proof() == head.lease.proof()
87 && after.recovery_fault_revision == head.recovery_fault_revision
88 && after.committed_topology_operation() == head.committed_topology_operation()
89 && assignments
90 .load()
91 .await
92 .map_err(super::topology_admission::topology_assignment_error)?
93 .is_some_and(|assignment| {
94 !assignment.draining
95 && assignment.assignment_fence().ok().as_ref()
96 == Some(fresh.assignment())
97 }))
98 })
99 .await
100 .map_err(|_| TopologyError::ReadTimedOut)?
101 }
102
103 pub async fn certify_topology_installation(
111 &self,
112 assignments: &AssignmentSnapshotStore,
113 processes: &ProcessLeaseAuthority,
114 input: &TopologyRestoreInput,
115 runtime_id: Uuid,
116 protocol_version: u16,
117 ) -> Result<TopologyAdmissionStatus, TopologyError> {
118 self.certify_topology_installation_for_round(
119 assignments,
120 processes,
121 input,
122 runtime_id,
123 protocol_version,
124 None,
125 )
126 .await
127 }
128
129 pub async fn certify_topology_recovery_installation(
136 &self,
137 assignments: &AssignmentSnapshotStore,
138 processes: &ProcessLeaseAuthority,
139 input: &TopologyRestoreInput,
140 runtime_id: Uuid,
141 round: &crate::cluster::control::RecoveryRound,
142 epoch: u64,
143 ) -> Result<TopologyAdmissionStatus, TopologyError> {
144 self.certify_topology_installation_for_round(
145 assignments,
146 processes,
147 input,
148 runtime_id,
149 TOPOLOGY_INSTALLATION_PROTOCOL_VERSION,
150 Some((round, epoch)),
151 )
152 .await
153 }
154
155 async fn certify_topology_installation_for_round(
156 &self,
157 assignments: &AssignmentSnapshotStore,
158 processes: &ProcessLeaseAuthority,
159 input: &TopologyRestoreInput,
160 runtime_id: Uuid,
161 protocol_version: u16,
162 recovery: Option<(&crate::cluster::control::RecoveryRound, u64)>,
163 ) -> Result<TopologyAdmissionStatus, TopologyError> {
164 if protocol_version != TOPOLOGY_INSTALLATION_PROTOCOL_VERSION || runtime_id.is_nil() {
165 return Err(TopologyError::Protocol(
166 "installation requires protocol five and an exact nonzero runtime identity".into(),
167 ));
168 }
169 tokio::time::timeout(CONTROL_TIMEOUT, async {
170 for _ in 0..MAX_ADMISSION_ATTEMPTS {
171 let published = self
172 .load_published_authority_head()
173 .await?
174 .ok_or(TopologyError::Fenced)?;
175 let current = &published.record;
176 let fresh = self
177 .committed_topology_restore_input(
178 assignments,
179 processes,
180 input.operation().operation_id,
181 input.process(),
182 )
183 .await?;
184 if !fresh.same_restore_requirements(input)
185 || Some(current.lease.proof()) != fresh.current_leader()
186 {
187 return Err(TopologyError::Fenced);
188 }
189 let index = current
190 .topology_operations
191 .iter()
192 .position(|operation| operation.operation_id == input.operation().operation_id)
193 .ok_or(TopologyError::Fenced)?;
194 let operation = ¤t.topology_operations[index];
195 if operation != fresh.operation() {
196 tokio::task::yield_now().await;
197 continue;
198 }
199 if let Some((round, epoch)) = recovery {
200 self.audit_recovery_topology_from(
201 current,
202 round,
203 Some(epoch),
204 Some((assignments, processes)),
205 )
206 .await
207 .map_err(super::topology_admission::topology_checkpoint_error)?;
208 let inventory = Self::recovery_fault_inventory_from(current);
209 if round.topology_binding().is_none()
210 || inventory.revision() != round.fault_revision()
211 || inventory.faults() != round.faults
212 || inventory.has_terminal_fault()
213 || round.assignment_fence != *fresh.assignment()
214 || fresh.current_leader().as_ref() != Some(&round.leader_proof)
215 || round
216 .topology_binding()
217 .is_none_or(|binding| binding.processes() != fresh.processes())
218 || current.active_checkpoint_artifacts.is_some()
219 || current.artifact_cleanup.is_some()
220 || current.assignment_drain_reservation.is_some()
221 {
222 return Err(TopologyError::Fenced);
223 }
224 if operation.phase == TopologyAdmissionPhase::Active {
225 return Ok(operation.clone());
226 }
227 } else {
228 Self::require_topology_installation_authority(current, &fresh)?;
229 }
230 let same_round = operation.activation.as_ref().filter(|round| {
231 round.leader == current.lease.proof()
232 && round.recovery_round == recovery.map(|(round, _)| round.id)
233 });
234 if let Some(round) = same_round {
235 if round.assignment != *fresh.assignment()
236 || round.processes != fresh.processes()
237 {
238 return Err(TopologyError::Fenced);
239 }
240 if let Some(receipt) = round.installations.iter().find(|receipt| {
241 receipt.process.participant.node_id == input.process().participant.node_id
242 }) {
243 if receipt.process != input.process()
244 || receipt.runtime_id != runtime_id
245 || receipt.protocol_version != protocol_version
246 {
247 return Err(TopologyError::Fenced);
248 }
249 return Ok(operation.clone());
250 }
251 } else if operation
252 .activation
253 .as_ref()
254 .is_some_and(|round| round.release.is_some())
255 {
256 return Err(TopologyError::Fenced);
257 }
258 let mut lease = current.lease.clone();
259 lease.seq = lease
260 .seq
261 .checked_add(1)
262 .ok_or_else(|| TopologyError::Invalid("authority sequence exhausted".into()))?;
263 let sequence = lease.seq;
264 let mut next = current.preserve_with_lease(lease);
265 next.version = next.version.max(TOPOLOGY_INSTALLATION_RECORD_VERSION);
266 if recovery.is_some() {
267 next.version = next.version.max(super::TOPOLOGY_RECOVERY_RECORD_VERSION);
268 }
269 let operation = &mut next.topology_operations[index];
270 let mut round = same_round.cloned().unwrap_or_else(|| TopologyActivation {
271 leader: current.lease.proof(),
272 recovery_round: recovery.map(|(round, _)| round.id),
273 assignment: fresh.assignment().clone(),
274 processes: fresh.processes().to_vec(),
275 authority_sequence: sequence,
276 installations: Vec::new(),
277 release: None,
278 });
279 round.installations.push(TopologyInstallationReceipt {
280 process: input.process(),
281 runtime_id,
282 protocol_version,
283 authority_sequence: sequence,
284 });
285 round
286 .installations
287 .sort_by_key(|receipt| receipt.process.participant.node_id);
288 operation.activation = Some(round);
289 operation.phase = TopologyAdmissionPhase::Activating;
290 operation.status_sequence = sequence;
291 for process in fresh.processes() {
292 self.require_topology_process(processes, *process).await?;
293 }
294 match self
295 .create_authority_record(Some(&published), &next)
296 .await?
297 {
298 AuthorityCreateOutcome::Created | AuthorityCreateOutcome::ExistingIdentical => {
299 let after = self
300 .committed_topology_restore_input(
301 assignments,
302 processes,
303 input.operation().operation_id,
304 input.process(),
305 )
306 .await?;
307 if !after.same_restore_requirements(input) {
308 return Err(TopologyError::Fenced);
309 }
310 return Ok(after.operation().clone());
311 }
312 AuthorityCreateOutcome::Contended(_) => tokio::task::yield_now().await,
313 }
314 }
315 Err(TopologyError::Contended)
316 })
317 .await
318 .map_err(|_| TopologyError::Contended)?
319 }
320
321 pub async fn release_topology_target(
329 &self,
330 proof: &LeaderProof,
331 assignments: &AssignmentSnapshotStore,
332 processes: &ProcessLeaseAuthority,
333 input: &TopologyRestoreInput,
334 ) -> Result<TopologyAdmissionStatus, TopologyError> {
335 tokio::time::timeout(CONTROL_TIMEOUT, async {
336 for _ in 0..MAX_ADMISSION_ATTEMPTS {
337 let published = self
338 .load_published_authority_head()
339 .await?
340 .ok_or(TopologyError::Fenced)?;
341 let current = &published.record;
342 if !current.lease.matches_proof(proof)
343 || proof.owner.node_id != input.process().participant.node_id
344 || proof.owner.boot_id != input.process().participant.boot_incarnation
345 {
346 return Err(TopologyError::Fenced);
347 }
348 let fresh = self
349 .committed_topology_restore_input(
350 assignments,
351 processes,
352 input.operation().operation_id,
353 input.process(),
354 )
355 .await?;
356 if !fresh.same_restore_requirements(input)
357 || fresh.current_leader().as_ref() != Some(proof)
358 {
359 return Err(TopologyError::Fenced);
360 }
361 let index = current
362 .topology_operations
363 .iter()
364 .position(|operation| operation.operation_id == input.operation().operation_id)
365 .ok_or(TopologyError::Fenced)?;
366 let operation = ¤t.topology_operations[index];
367 if operation != fresh.operation() {
368 tokio::task::yield_now().await;
369 continue;
370 }
371 Self::require_topology_release_authority(current, &fresh)?;
372 let round = operation.activation.as_ref().ok_or_else(|| {
373 TopologyError::Conflict("target runtime installation has not begun".into())
374 })?;
375 if round.assignment != *fresh.assignment() || round.processes != fresh.processes() {
376 return Err(TopologyError::Fenced);
377 }
378 if !round.installation_complete() {
379 return Err(TopologyError::Conflict(
380 "Release requires every current owner and evidence runtime, including zero-vnode participants".into(),
381 ));
382 }
383 if round.release.is_some() {
384 return Ok(operation.clone());
385 }
386 Self::require_topology_installation_authority(current, &fresh)?;
387 if round.leader != *proof {
388 return Err(TopologyError::Fenced);
389 }
390 for process in &round.processes {
391 self.require_topology_process(processes, *process).await?;
392 }
393 let mut lease = current.lease.clone();
394 lease.seq = lease
395 .seq
396 .checked_add(1)
397 .ok_or_else(|| TopologyError::Invalid("authority sequence exhausted".into()))?;
398 let sequence = lease.seq;
399 let mut next = current.preserve_with_lease(lease);
400 let operation = &mut next.topology_operations[index];
401 operation
402 .activation
403 .as_mut()
404 .ok_or(TopologyError::Fenced)?
405 .release = Some(TopologyRelease {
406 authority_sequence: sequence,
407 });
408 operation.phase = TopologyAdmissionPhase::Active;
409 operation.status_sequence = sequence;
410 match self
411 .create_authority_record(Some(&published), &next)
412 .await?
413 {
414 AuthorityCreateOutcome::Created | AuthorityCreateOutcome::ExistingIdentical => {
415 let after = self
416 .committed_topology_restore_input(
417 assignments,
418 processes,
419 input.operation().operation_id,
420 input.process(),
421 )
422 .await?;
423 if !after.same_restore_requirements(input) {
424 return Err(TopologyError::Fenced);
425 }
426 return Ok(after.operation().clone());
427 }
428 AuthorityCreateOutcome::Contended(_) => tokio::task::yield_now().await,
429 }
430 }
431 Err(TopologyError::Contended)
432 })
433 .await
434 .map_err(|_| TopologyError::Contended)?
435 }
436
437 fn require_topology_installation_authority(
438 head: &super::LeaderAuthorityRecord,
439 input: &TopologyRestoreInput,
440 ) -> Result<(), TopologyError> {
441 if head.active_checkpoint_artifacts.is_some() {
442 return Err(TopologyError::Conflict(
443 "held runtime installation cannot overlap a checkpoint".into(),
444 ));
445 }
446 Self::require_topology_release_authority(head, input)
447 }
448
449 fn require_topology_release_authority(
450 head: &super::LeaderAuthorityRecord,
451 input: &TopologyRestoreInput,
452 ) -> Result<(), TopologyError> {
453 if head
456 .active_checkpoint_artifacts
457 .as_ref()
458 .is_some_and(|inventory| {
459 input.operation().phase != TopologyAdmissionPhase::Active
460 || inventory.pipeline_identity != input.descriptor().target_pipeline
461 || inventory.deployment_id != input.descriptor().deployment_id
462 || inventory.assignment_fence.as_ref() != Some(input.assignment())
463 })
464 || head.artifact_cleanup.is_some()
465 || head.assignment_drain_reservation.is_some()
466 || head.assignment_handoff_pin.is_some()
467 || head
468 .recovery_fault_slots
469 .iter()
470 .any(|slot| slot.active || slot.disposition == RecoveryFaultDisposition::Terminal)
471 || head
472 .committed_topology_operation()
473 .is_none_or(|operation| operation.operation_id != input.operation().operation_id)
474 {
475 return Err(TopologyError::Conflict("installation/Release requires the exact committed target without competing checkpoint, assignment, cleanup or recovery authority".into()));
476 }
477 Ok(())
478 }
479
480 pub(super) async fn audit_topology_activation(
481 &self,
482 operation: &TopologyAdmissionStatus,
483 ) -> Result<(), LeaseError> {
484 let Some(round) = &operation.activation else {
485 return Ok(());
486 };
487 let sequences = std::iter::once(round.authority_sequence)
488 .chain(
489 round
490 .installations
491 .iter()
492 .map(|receipt| receipt.authority_sequence),
493 )
494 .chain(
495 round
496 .release
497 .iter()
498 .map(|release| release.authority_sequence),
499 );
500 for sequence in sequences {
501 let anchor = read_authority_record(self.store.as_ref(), sequence)
502 .await?
503 .ok_or_else(|| {
504 LeaseError::Invalid(
505 "topology installation/Release authority anchor is missing".into(),
506 )
507 })?;
508 let stored = anchor
509 .topology_operations
510 .iter()
511 .find(|entry| entry.operation_id == operation.operation_id)
512 .ok_or_else(|| {
513 LeaseError::Invalid("installation operation anchor is missing".into())
514 })?;
515 let installed = stored.activation.as_ref().ok_or_else(|| {
516 LeaseError::Invalid("installation round anchor is missing".into())
517 })?;
518 let expected_installations = round
519 .installations
520 .iter()
521 .filter(|receipt| receipt.authority_sequence <= sequence)
522 .cloned()
523 .collect::<Vec<_>>();
524 let expected_release = round
525 .release
526 .as_ref()
527 .filter(|release| release.authority_sequence <= sequence);
528 if anchor.version < TOPOLOGY_INSTALLATION_RECORD_VERSION
529 || !anchor.lease.matches_proof(&round.leader)
530 || !stored.same_migration_binding(operation)
531 || stored.status_sequence != sequence
532 || stored.phase
533 != if expected_release.is_some() {
534 TopologyAdmissionPhase::Active
535 } else {
536 TopologyAdmissionPhase::Activating
537 }
538 || stored.commit != operation.commit
539 || installed.leader != round.leader
540 || installed.recovery_round != round.recovery_round
541 || installed.assignment != round.assignment
542 || installed.processes != round.processes
543 || installed.authority_sequence != round.authority_sequence
544 || installed.installations != expected_installations
545 || installed.release.as_ref() != expected_release
546 {
547 return Err(LeaseError::Invalid(
548 "installation/Release differs from its exact immutable append".into(),
549 ));
550 }
551 }
552 Ok(())
553 }
554}