Skip to main content

laminar_db/db/topology/
transport.rs

1//! Exact committed generation and pipeline identity on the process-owned shuffle fabric; runtime
2//! activation stays held.
3
4use std::sync::atomic::Ordering;
5use std::sync::Arc;
6
7use laminar_core::cluster::control::{TopologyError, TopologyRestoreInput, TopologyVersion};
8use laminar_core::shuffle::ShuffleTopologyFence;
9
10use super::{DbError, DbState, LaminarDB, PreparedTopologyRestore};
11
12impl LaminarDB {
13    /// Activate the shuffle assignment certificate, then keep a fresh process on the committed
14    /// topology generation. Callers hold the assignment adoption and execution fences.
15    pub(crate) fn install_shuffle_assignment_fence(
16        &self,
17        fence: &laminar_core::checkpoint::CheckpointAssignmentFence,
18    ) -> Result<(), DbError> {
19        self.install_shuffle_assignment_certificate(fence)?;
20        self.bind_replayed_topology_fence().inspect_err(|_| {
21            self.invalidate_shuffle_assignment_fence();
22        })
23    }
24
25    /// Peers refuse shuffle from another topology generation. A live migration binds its fence
26    /// through the restore path; a process that starts after settlement has none, so bind the
27    /// committed generation it replayed once its assignment is active. Ordinary startup calls
28    /// this before building the graph, and assignment activation calls it again.
29    pub(crate) fn bind_replayed_topology_fence(&self) -> Result<(), DbError> {
30        let Some(target) = self
31            .replayed_topology
32            .lock()
33            .and_then(|replayed| replayed.fence)
34        else {
35            return Ok(());
36        };
37        let sender = self.shuffle_sender.lock().clone();
38        let receiver = self.shuffle_receiver.lock().clone();
39        let (Some(sender), Some(receiver)) = (sender, receiver) else {
40            return Ok(());
41        };
42        if sender.topology_fence().is_some()
43            || sender.active_assignment_digest().is_none()
44            || receiver.active_assignment_digest().is_none()
45        {
46            return Ok(());
47        }
48        sender
49            .install_topology_fence_pair(&receiver, None, target)
50            .map_err(|error| TopologyError::Conflict(error.to_string()))?;
51        Ok(())
52    }
53
54    /// Peers refuse shuffle handshakes from a process bound to another pipeline identity, so
55    /// processes built from different binaries or catalogs never exchange frames. Startup binds
56    /// the identity it computed before the graph sends.
57    pub(crate) fn bind_shuffle_pipeline_identity(
58        &self,
59        identity: &laminar_core::checkpoint::PipelineIdentity,
60    ) -> Result<(), DbError> {
61        let sender = self.shuffle_sender.lock().clone();
62        let receiver = self.shuffle_receiver.lock().clone();
63        let (Some(sender), Some(receiver)) = (sender, receiver) else {
64            return Ok(());
65        };
66        sender
67            .install_pipeline_identity_pair(&receiver, identity)
68            .map_err(|error| DbError::Pipeline(format!("shuffle pipeline identity: {error}")))?;
69        Ok(())
70    }
71
72    /// Topology generation an ordinary graph sends under: the endpoint's, or for a fresh process
73    /// whose assignment is not active yet, the committed generation it replayed.
74    pub(crate) fn shuffle_topology_fence(
75        &self,
76        sender: &laminar_core::shuffle::ShuffleSender,
77    ) -> Option<ShuffleTopologyFence> {
78        sender.topology_fence().or_else(|| {
79            self.replayed_topology
80                .lock()
81                .and_then(|replayed| replayed.fence)
82        })
83    }
84
85    /// Fence this process's transport and private graph to the exact committed target.
86    /// Reobserves retirement for a held parent; a Created DB must have no runtime/connector owners.
87    /// Current full-roster, process/adoption, assignment, source availability and Commit are audited
88    /// around publication. Sources, sinks, local catalog/coordinator and intake remain unstarted/held.
89    ///
90    /// The cooperative budget is 45 seconds. Cancellation after local publication retains the
91    /// target fence and cut hold; retry this image or reconstruct from the immutable Commit.
92    /// This is transport preparation, not installed receivers/state/sinks readiness or Release.
93    ///
94    /// # Errors
95    /// Rejects foreign/uncommitted/stale images, unresolved actors, process/assignment mismatches,
96    /// local faults, damaged cursors and expired deadlines. Runtime DDL remains guarded.
97    pub async fn prepare_cluster_topology_transport(
98        &self,
99        image: &mut PreparedTopologyRestore,
100    ) -> Result<(), DbError> {
101        if !image.belongs_to(self) || !image.input.is_committed() {
102            return Err(TopologyError::Conflict(
103                "transport preparation requires this DB's committed private image".into(),
104            )
105            .into());
106        }
107        self.prepare_topology_transport_image(image).await
108    }
109
110    pub(crate) async fn prepare_coordinated_topology_transport(
111        &self,
112        image: &mut PreparedTopologyRestore,
113    ) -> Result<(), DbError> {
114        let start = image.recovery_start.as_ref().ok_or(TopologyError::Fenced)?;
115        if !image.belongs_to_recovery(self, start) || !image.is_committed() {
116            return Err(TopologyError::Fenced.into());
117        }
118        self.prepare_topology_transport_image(image).await
119    }
120
121    fn ensure_topology_transport_available(
122        &self,
123        image: &PreparedTopologyRestore,
124    ) -> Result<(), DbError> {
125        if let Some(start) = &image.recovery_start {
126            self.ensure_topology_recovery_stopped(start)
127        } else {
128            self.ensure_topology_restore_available(true)
129        }
130    }
131
132    async fn topology_transport_input(
133        &self,
134        image: &mut PreparedTopologyRestore,
135    ) -> Result<TopologyRestoreInput, DbError> {
136        let controller = self
137            .cluster_controller
138            .lock()
139            .clone()
140            .ok_or(TopologyError::Fenced)?;
141        if let Some(start) = &image.recovery_start {
142            self.ensure_topology_recovery_stopped(start)?;
143            if controller
144                .observe_recover_control()
145                .await
146                .map_err(|error| TopologyError::Conflict(error.to_string()))?
147                .as_ref()
148                != Some(start)
149            {
150                return Err(TopologyError::Fenced.into());
151            }
152            let fresh = controller
153                .topology_recovery_input(&start.round, super::recovery_runtime::start_epoch(start)?)
154                .await?;
155            if image
156                .recovery_input()
157                .is_none_or(|selected| !fresh.same_restore_requirements(selected))
158            {
159                return Err(TopologyError::Fenced.into());
160            }
161            Ok(fresh.migration().clone())
162        } else {
163            Ok(controller
164                .committed_topology_restore_input(image.input.operation().operation_id)
165                .await?)
166        }
167    }
168
169    async fn prepare_topology_transport_image(
170        &self,
171        image: &mut PreparedTopologyRestore,
172    ) -> Result<(), DbError> {
173        tokio::time::timeout(std::time::Duration::from_secs(45), async {
174            self.ensure_topology_transport_available(image)?;
175            let controller = self
176                .cluster_controller
177                .lock()
178                .clone()
179                .ok_or(TopologyError::Fenced)?;
180            let before = self.topology_transport_input(image).await?;
181            if !before.same_restore_requirements(&image.input) {
182                return Err(TopologyError::Fenced.into());
183            }
184            if DbState::load(&self.state) == DbState::ShuttingDown {
185                self.stop_pipeline_for_topology_retirement().await?;
186            }
187            drop(
188                super::restore::prepare_source_positions_at_cut(
189                    &image.candidate,
190                    &before,
191                    image.recovery_input(),
192                )
193                .await?,
194            );
195            let _assignment = self.assignment_adoption_lock.lock().await;
196            let _execution = Arc::clone(&self.rotation_execution_fence)
197                .write_owned()
198                .await;
199            let fresh = self.topology_transport_input(image).await?;
200            if !fresh.same_restore_requirements(&before) {
201                return Err(TopologyError::Fenced.into());
202            }
203            let target = ShuffleTopologyFence::from_manifest(
204                image.target_version(),
205                &fresh.plan().target_manifest,
206            )
207            .map_err(|error| TopologyError::Invalid(error.to_string()))?;
208            let sender = self
209                .shuffle_sender
210                .lock()
211                .clone()
212                .ok_or(TopologyError::Fenced)?;
213            let receiver = self
214                .shuffle_receiver
215                .lock()
216                .clone()
217                .ok_or(TopologyError::Fenced)?;
218            let mut parent = if fresh.plan().expected_parent == TopologyVersion::LEGACY_BASELINE {
219                None
220            } else {
221                Some(
222                    ShuffleTopologyFence::from_manifest(
223                        fresh.plan().expected_parent,
224                        &fresh.plan().parent_manifest,
225                    )
226                    .map_err(|error| TopologyError::Invalid(error.to_string()))?,
227                )
228            };
229            if DbState::load(&self.state) == DbState::Created && sender.topology_fence().is_none() {
230                parent = None;
231            }
232            if sender.local_id() != fresh.process().participant.node_id
233                || sender.incarnation() != fresh.process().participant.boot_incarnation
234                || sender.active_assignment_digest() != Some(fresh.assignment().digest())
235                || receiver.active_assignment_digest() != Some(fresh.assignment().digest())
236            {
237                return Err(TopologyError::Fenced.into());
238            }
239            // Synchronous local publication shares the process/fault/lifecycle transition fence.
240            {
241                let _transition = self.cluster_authority_transition.lock();
242                self.ensure_topology_transport_available(image)?;
243                if controller.try_live_local_process_authority_identity().ok()
244                    != Some(fresh.process())
245                    || controller.is_draining()
246                    || self
247                        .startup_attempt
248                        .lock()
249                        .as_ref()
250                        .is_some_and(|attempt| !attempt.is_complete())
251                    || self
252                        .runtime_handle
253                        .try_lock()
254                        .map_err(|_| TopologyError::Fenced)?
255                        .is_some()
256                    || !self.owned_source_tasks.lock().is_empty()
257                    || !self.owned_sink_handles.lock().is_empty()
258                    || !self.owned_connector_task_fences.lock().is_empty()
259                {
260                    return Err(TopologyError::Fenced.into());
261                }
262                image.graph.bind_cluster_topology_fence(target)?;
263                self.source_gate.store(true, Ordering::Release);
264                self.topology_cut_hold.store(true, Ordering::Release);
265                if let Some(start) = &image.recovery_start {
266                    sender.install_topology_fence_pair_for_recovery(
267                        &receiver,
268                        parent,
269                        target,
270                        start.round.id.generation,
271                    )
272                } else {
273                    sender.install_topology_fence_pair(&receiver, parent, target)
274                }
275                .map_err(|error| TopologyError::Conflict(error.to_string()))?;
276            }
277            let after = self.topology_transport_input(image).await?;
278            if !after.same_restore_requirements(&fresh) {
279                return Err(TopologyError::Fenced.into());
280            }
281            self.ensure_topology_transport_available(image)?;
282            Ok(())
283        })
284        .await
285        .map_err(|_| TopologyError::Contended)?
286    }
287}