Skip to main content

laminar_db/db/topology/
activation.rs

1//! Participant runtime ownership, durable Release and local intake activation.
2
3use std::sync::{atomic::Ordering, Arc};
4
5use laminar_core::cluster::control::{
6    TopologyAdmissionPhase, TopologyAdmissionStatus, TopologyError, TopologyOperationId,
7    TopologyRestoreInput,
8};
9use uuid::Uuid;
10
11use super::{DbError, DbState, LaminarDB};
12
13#[derive(Clone)]
14pub(crate) struct InstalledTopologyRuntime {
15    pub(crate) input: TopologyRestoreInput,
16    pub(crate) runtime_id: Uuid,
17    pub(crate) shutdown: tokio_util::sync::CancellationToken,
18    pub(crate) released_sequence: Option<u64>,
19    pub(crate) recovery: Option<super::restore::TopologyRecoveryRuntime>,
20}
21
22#[derive(Clone, Copy)]
23enum ActivationAction {
24    Certify,
25    Release,
26    Apply,
27}
28
29impl LaminarDB {
30    pub(crate) async fn refresh_released_topology_assignment(
31        &self,
32        controller: &laminar_core::cluster::control::ClusterController,
33        fence: &laminar_core::checkpoint::CheckpointAssignmentFence,
34        revision: u64,
35        deadline: tokio::time::Instant,
36    ) -> Result<Option<crate::db::AssignmentAuthorityActivation>, DbError> {
37        let Some(binding) = self.installed_topology_runtime.lock().clone() else {
38            return Ok(None);
39        };
40        // A settled target is an ordinary deployment; its runtime adopts ordinary reassignment.
41        if self.live_topology_commit().await?.as_ref() != binding.input.operation().commit.as_ref()
42        {
43            return Ok(None);
44        }
45        if binding.recovery.is_some() {
46            if binding.input.assignment() != fence
47                || !self.recovered_topology_runtime_is_active(&binding).await?
48            {
49                return Err(TopologyError::Fenced.into());
50            }
51            let proof = controller
52                .audit_assignment_leader_authority(fence, None, deadline)
53                .await
54                .map_err(TopologyError::Conflict)?;
55            return Ok(Some(
56                self.open_assignment_intake_after_audit(
57                    controller,
58                    fence,
59                    None,
60                    proof.owner.node_id,
61                    revision,
62                    deadline,
63                )
64                .await?,
65            ));
66        }
67        if binding.released_sequence.is_none() {
68            return Ok(None);
69        }
70        let fresh = controller
71            .committed_topology_restore_input(binding.input.operation().operation_id)
72            .await?;
73        Self::require_runtime_release(
74            &binding,
75            &fresh,
76            binding.released_sequence.ok_or(TopologyError::Fenced)?,
77        )?;
78        self.ensure_topology_runtime_live(&fresh, None).await?;
79        if fresh.assignment() != fence
80            || !controller
81                .authorize_topology_release(&fresh, binding.runtime_id)
82                .await?
83            || binding.shutdown.is_cancelled()
84            || DbState::load(&self.state) != DbState::Running
85            || self.is_closed()
86            || self.pending_recovery_fault.load(Ordering::Acquire) != 0
87            || self.cluster_authority_revoked.load(Ordering::Acquire)
88            || self.terminal_pipeline_halt.load(Ordering::Acquire)
89            || self.durable_terminal_recovery_fence.load(Ordering::Acquire)
90            || self.coordinated_recovery_in_progress()
91            || self
92                .owned_source_tasks
93                .lock()
94                .iter()
95                .any(|source| !source.is_running())
96            || self
97                .owned_sink_handles
98                .lock()
99                .iter()
100                .any(|sink| !sink.is_ready())
101        {
102            return Err(TopologyError::Fenced.into());
103        }
104        let leader = fresh.current_leader().ok_or(TopologyError::Fenced)?;
105        Ok(Some(
106            self.open_assignment_intake_after_audit(
107                controller,
108                fence,
109                None,
110                leader.owner.node_id,
111                revision,
112                deadline,
113            )
114            .await?,
115        ))
116    }
117
118    /// Certify this process's exact installed runtime while intake remains held. The DB-owned
119    /// executor continues after caller disconnect. This receipt alone never authorizes output.
120    ///
121    /// # Errors
122    /// Rejects absent/dead actors, stale process/assignment/transport, faults or uncertain writes.
123    pub async fn certify_installed_cluster_topology(
124        self: &Arc<Self>,
125        operation: TopologyOperationId,
126    ) -> Result<TopologyAdmissionStatus, DbError> {
127        self.run_owned_topology_activation(operation, ActivationAction::Certify)
128            .await
129    }
130
131    /// As the current leader, publish participant-complete Release and apply it locally. Each
132    /// follower must separately apply the same durable Release against its own runtime receipt.
133    /// A committed Release remains authoritative if local activation fails; intake stays held.
134    ///
135    /// # Errors
136    /// Rejects incomplete/currently stale rosters, leader loss, local runtime failure or the budget.
137    pub async fn release_installed_cluster_topology(
138        self: &Arc<Self>,
139        operation: TopologyOperationId,
140    ) -> Result<TopologyAdmissionStatus, DbError> {
141        self.run_owned_topology_activation(operation, ActivationAction::Release)
142            .await
143    }
144
145    /// Apply the authoritative Release to this exact runtime. Reconcile/admit sink epochs through
146    /// the existing coordinator before opening intake. Caller cancellation never owns that work.
147    ///
148    /// # Errors
149    /// Rejects missing Release, another runtime's receipt, stale authority or unresolved sink work.
150    pub async fn apply_cluster_topology_release(
151        self: &Arc<Self>,
152        operation: TopologyOperationId,
153    ) -> Result<TopologyAdmissionStatus, DbError> {
154        self.run_owned_topology_activation(operation, ActivationAction::Apply)
155            .await
156    }
157
158    async fn run_owned_topology_activation(
159        self: &Arc<Self>,
160        operation: TopologyOperationId,
161        action: ActivationAction,
162    ) -> Result<TopologyAdmissionStatus, DbError> {
163        let executor = self.control_runtime.handle()?;
164        let permit = Arc::clone(&self.topology_validation_lock)
165            .try_lock_owned()
166            .map_err(|_| TopologyError::Contended)?;
167        let owner = Arc::clone(self);
168        let (result_tx, result_rx) = tokio::sync::oneshot::channel();
169        let task = executor.spawn(async move {
170            let _permit = permit;
171            let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(45);
172            let result = tokio::time::timeout_at(
173                deadline,
174                owner.activate_installed_topology(operation, action, deadline),
175            )
176            .await
177            .map_err(|_| DbError::from(TopologyError::Contended))
178            .and_then(std::convert::identity);
179            let _ = result_tx.send(result);
180        });
181        // This bounded control-path owner retains the DB and its executor until completion. The
182        // observer owns only the response; disconnect cannot drop a sink/authority write future.
183        drop(task);
184        result_rx.await.map_err(|_| DbError::Pipeline("owned topology activation task exited without its result; reread the operation and retry".into()))?
185    }
186
187    async fn activate_installed_topology(
188        &self,
189        operation: TopologyOperationId,
190        action: ActivationAction,
191        deadline: tokio::time::Instant,
192    ) -> Result<TopologyAdmissionStatus, DbError> {
193        // Lock ordering matches startup/stop: topology -> lifecycle -> assignment. No synchronous
194        // lock crosses an await. Fault/process revocation retains its separate transition fence.
195        let _topology = self.topology_ddl_lock.write().await;
196        let _lifecycle = self.lifecycle_lock.lock().await;
197        let _assignment = self.assignment_adoption_lock.lock().await;
198        let binding = self
199            .installed_topology_runtime
200            .lock()
201            .clone()
202            .ok_or(TopologyError::Fenced)?;
203        if binding.input.operation().operation_id != operation
204            || binding.shutdown.is_cancelled()
205            || DbState::load(&self.state) != DbState::Running
206        {
207            return Err(TopologyError::Fenced.into());
208        }
209        let controller = self
210            .cluster_controller
211            .lock()
212            .clone()
213            .ok_or(TopologyError::Fenced)?;
214        let revision = self.assignment_authority_revision.load(Ordering::Acquire);
215        let fresh = controller
216            .committed_topology_restore_input(operation)
217            .await?;
218        if !fresh.same_installed_generation(&binding.input) {
219            return Err(TopologyError::Fenced.into());
220        }
221        if let Some(sequence) = binding.released_sequence {
222            Self::require_runtime_release(&binding, &fresh, sequence)?;
223            if !controller
224                .authorize_topology_release(&fresh, binding.runtime_id)
225                .await?
226            {
227                return Err(TopologyError::Fenced.into());
228            }
229            self.ensure_released_topology_runtime(&binding, &controller)?;
230            self.ensure_topology_runtime_live(&fresh, None).await?;
231            return Ok(fresh.operation().clone());
232        }
233        self.validate_topology_installation(&fresh).await?;
234        self.ensure_topology_runtime_ready(&fresh).await?;
235        let sinks = self.owned_sink_handles.lock().clone();
236        for sink in &sinks {
237            sink.sync_until(deadline).await.map_err(|error| {
238                DbError::Connector(format!(
239                    "topology readiness for sink '{}': {error}",
240                    sink.name()
241                ))
242            })?;
243        }
244        let sender = self
245            .shuffle_sender
246            .lock()
247            .clone()
248            .ok_or(TopologyError::Fenced)?;
249        sender
250            .establish_assignment_mesh(fresh.assignment())
251            .await
252            .map_err(|error| {
253                TopologyError::Conflict(format!("target receiver mesh is not ready: {error}"))
254            })?;
255        self.ensure_topology_runtime_ready(&fresh).await?;
256        self.ensure_topology_runtime_held()?;
257        if !matches!(action, ActivationAction::Apply) {
258            controller
259                .certify_topology_installation(&fresh, binding.runtime_id)
260                .await?;
261        }
262        if matches!(action, ActivationAction::Certify) {
263            self.ensure_topology_runtime_ready(&fresh).await?;
264            return Ok(controller
265                .committed_topology_restore_input(operation)
266                .await?
267                .operation()
268                .clone());
269        }
270        let fresh = controller
271            .committed_topology_restore_input(operation)
272            .await?;
273        if matches!(action, ActivationAction::Release) {
274            controller.release_topology_target(&fresh).await?;
275        }
276        let released = controller
277            .committed_topology_restore_input(operation)
278            .await?;
279        let sequence = released
280            .operation()
281            .activation
282            .as_ref()
283            .and_then(|round| round.release.as_ref())
284            .map(|release| release.authority_sequence)
285            .ok_or_else(|| {
286                TopologyError::Conflict(
287                    "the exact target runtime has no participant-complete Release".into(),
288                )
289            })?;
290        Self::require_runtime_release(&binding, &released, sequence)?;
291        self.ensure_topology_runtime_ready(&released).await?;
292        {
293            let mut coordinator = self.coordinator.lock().await;
294            let coordinator = coordinator.as_mut().ok_or(TopologyError::Fenced)?;
295            coordinator
296                .reconcile_sink_open_witness_until(deadline)
297                .await?;
298            coordinator
299                .ensure_assignment_sink_epoch_until(deadline)
300                .await?;
301        }
302        let after = controller
303            .committed_topology_restore_input(operation)
304            .await?;
305        Self::require_runtime_release(&binding, &after, sequence)?;
306        if !controller
307            .authorize_topology_release(&after, binding.runtime_id)
308            .await?
309        {
310            return Err(TopologyError::Fenced.into());
311        }
312        self.validate_topology_installation(&after).await?;
313        self.ensure_topology_runtime_ready(&after).await?;
314        {
315            let _transition = self.cluster_authority_transition.lock();
316            self.ensure_topology_runtime_held()?;
317            if binding.shutdown.is_cancelled()
318                || controller.is_recovering()
319                || controller.is_draining()
320                || controller.try_live_local_process_authority_identity().ok()
321                    != Some(after.process())
322                || self.assignment_authority_revision.load(Ordering::Acquire) != revision
323                || self
324                    .owned_sink_handles
325                    .lock()
326                    .iter()
327                    .any(|sink| !sink.is_ready())
328                || self
329                    .owned_source_tasks
330                    .lock()
331                    .iter()
332                    .any(|source| !source.is_running())
333            {
334                return Err(TopologyError::Fenced.into());
335            }
336            let mut installed = self.installed_topology_runtime.lock();
337            let installed = installed
338                .as_mut()
339                .filter(|installed| {
340                    installed.runtime_id == binding.runtime_id && !installed.shutdown.is_cancelled()
341                })
342                .ok_or(TopologyError::Fenced)?;
343            installed.released_sequence = Some(sequence);
344            self.topology_cut_hold.store(false, Ordering::Release);
345            self.source_gate.store(false, Ordering::SeqCst);
346        }
347        Ok(after.operation().clone())
348    }
349
350    fn require_runtime_release(
351        binding: &InstalledTopologyRuntime,
352        input: &TopologyRestoreInput,
353        sequence: u64,
354    ) -> Result<(), DbError> {
355        let round = input
356            .operation()
357            .activation
358            .as_ref()
359            .ok_or(TopologyError::Fenced)?;
360        if !input.same_installed_generation(&binding.input)
361            || input.operation().phase != TopologyAdmissionPhase::Active
362            || round.assignment != *input.assignment()
363            || round.processes != input.processes()
364            || !round.installation_complete()
365            || round
366                .release
367                .as_ref()
368                .is_none_or(|release| release.authority_sequence != sequence)
369            || !round.installations.iter().any(|receipt| {
370                receipt.process == input.process() && receipt.runtime_id == binding.runtime_id
371            })
372        {
373            return Err(TopologyError::Fenced.into());
374        }
375        Ok(())
376    }
377
378    fn ensure_released_topology_runtime(
379        &self,
380        binding: &InstalledTopologyRuntime,
381        controller: &laminar_core::cluster::control::ClusterController,
382    ) -> Result<(), DbError> {
383        if self.is_closed()
384            || binding.shutdown.is_cancelled()
385            || DbState::load(&self.state) != DbState::Running
386            || self.source_gate.load(Ordering::Acquire)
387            || self.topology_cut_hold.load(Ordering::Acquire)
388            || self.cluster_authority_revoked.load(Ordering::Acquire)
389            || self.terminal_pipeline_halt.load(Ordering::Acquire)
390            || self.durable_terminal_recovery_fence.load(Ordering::Acquire)
391            || self.pending_recovery_fault.load(Ordering::Acquire) != 0
392            || self.coordinated_recovery_in_progress()
393            || controller.is_recovering()
394            || controller.is_draining()
395            || controller.try_live_local_process_authority_identity().ok()
396                != Some(binding.input.process())
397        {
398            return Err(TopologyError::Fenced.into());
399        }
400        Ok(())
401    }
402}