1use std::sync::Arc;
4use std::time::Duration;
5
6use laminar_core::cluster::control::{
7 AssignmentSnapshotStore, CatalogManifest, CatalogManifestRef, ClusterController, LeaderProof,
8 LegacyTopologyBaseline, LocalProcessAuthorityIdentity, TopologyAdmissionPlan,
9 TopologyAdoptionOutcome, TopologyError, TOPOLOGY_SUBMISSION_PROTOCOL_VERSION,
10};
11use laminar_sql::parser::StreamingStatement;
12
13use super::{DbError, LaminarDB, TopologyAdmissionStatus, TopologyOperationId, TopologyVersion};
14
15pub(super) const SUBMISSION_TIMEOUT: Duration = Duration::from_secs(45);
16
17#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
19#[serde(deny_unknown_fields)]
20pub struct ClusterTopologyRequest {
21 pub operation_id: TopologyOperationId,
23 pub expected_parent_version: TopologyVersion,
25 pub statements: Vec<String>,
27}
28
29#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
31#[serde(deny_unknown_fields)]
32pub struct ClusterTopologyAdoptionRequest {
33 pub operation_id: TopologyOperationId,
35 pub expected_manifest: CatalogManifestRef,
37 pub expected_deployment_id: String,
39 pub coordinated_upgrade_complete: bool,
41}
42
43impl LaminarDB {
44 pub async fn submit_cluster_topology_change(
56 &self,
57 request: &ClusterTopologyRequest,
58 ) -> Result<TopologyAdmissionStatus, DbError> {
59 self.submit_cluster_topology_until(request, SUBMISSION_TIMEOUT, true)
60 .await
61 }
62
63 #[doc(hidden)]
68 pub async fn submit_cluster_topology_forwarded(
69 &self,
70 request: &ClusterTopologyRequest,
71 remaining: Duration,
72 ) -> Result<TopologyAdmissionStatus, DbError> {
73 self.submit_cluster_topology_until(request, remaining.min(SUBMISSION_TIMEOUT), false)
74 .await
75 }
76
77 async fn submit_cluster_topology_until(
78 &self,
79 request: &ClusterTopologyRequest,
80 timeout: Duration,
81 allow_forwarding: bool,
82 ) -> Result<TopologyAdmissionStatus, DbError> {
83 if timeout.is_zero() {
84 return Err(TopologyError::Contended.into());
85 }
86 let deadline = tokio::time::Instant::now()
87 .checked_add(timeout)
88 .ok_or(TopologyError::Contended)?;
89 tokio::time::timeout_at(
90 deadline,
91 Box::pin(async {
92 super::planning::validate_request_bounds(&request.statements)?;
93 let controller = self.topology_submission_controller()?;
94 let authority = controller
95 .checkpoint_authority()
96 .map_err(|error| TopologyError::Protocol(error.to_string()))?;
97 if !allow_forwarding {
98 self.topology_submission_route(&controller, false).await?;
99 }
100 if authority
103 .topology_operation_status(request.operation_id)
104 .await?
105 .is_some()
106 {
107 let (operation, plan, _target, descriptor) = authority
108 .topology_preparation_input(request.operation_id)
109 .await?;
110 if plan.protocol_version != TOPOLOGY_SUBMISSION_PROTOCOL_VERSION
111 || plan.expected_parent != request.expected_parent_version
112 || descriptor.statements != request.statements
113 {
114 return Err(TopologyError::Conflict(
115 "operation identity was already bound to another payload".into(),
116 )
117 .into());
118 }
119 return Ok(operation);
120 }
121 self.ensure_topology_submission_available()?;
122 if let Some(address) = self
123 .topology_submission_route(&controller, allow_forwarding)
124 .await?
125 {
126 let operation: TopologyAdmissionStatus = self
127 .forward_topology_request(
128 &address,
129 "/api/v1/cluster/topology/operations",
130 request,
131 deadline,
132 )
133 .await?;
134 if operation.operation_id != request.operation_id {
135 return Err(TopologyError::Invalid(
136 "leader receipt belongs to another operation".into(),
137 )
138 .into());
139 }
140 return Ok(operation);
141 }
142 let before = controller
143 .try_live_local_process_authority_identity()
144 .map_err(|_| TopologyError::Fenced)?;
145 let proof = controller
146 .capture_leader_proof()
147 .ok_or(TopologyError::Fenced)?;
148 let (descriptor, target) = Box::pin(self.plan_cluster_topology_change(
149 request.expected_parent_version,
150 &request.statements,
151 ))
152 .await?;
153 let assignments = controller.snapshot_store().ok_or_else(|| {
154 TopologyError::Protocol(
155 "submission requires the configured assignment authority".into(),
156 )
157 })?;
158 let assignment = assignments
159 .load()
160 .await
161 .map_err(|error| TopologyError::Protocol(error.to_string()))?
162 .ok_or_else(|| {
163 TopologyError::Conflict("assignment inventory is uninitialized".into())
164 })?
165 .assignment_fence()
166 .map_err(|error| TopologyError::Invalid(error.to_string()))?;
167 if assignment.participant_incarnation(before.participant.node_id)
168 != Some(before.participant.boot_incarnation)
169 || controller
170 .checkpoint_assignment_fence(assignment.assignment_version)
171 .as_ref()
172 != Some(&assignment)
173 || controller.try_live_local_process_authority_identity().ok() != Some(before)
174 || !controller.proof_is_live(&proof)
175 {
176 return Err(TopologyError::Fenced.into());
177 }
178 let compatibility = authority.stage_topology_compatibility(&descriptor).await?;
179 let plan = TopologyAdmissionPlan {
180 protocol_version: TOPOLOGY_SUBMISSION_PROTOCOL_VERSION,
181 operation_id: request.operation_id,
182 expected_parent: request.expected_parent_version,
183 parent_manifest: descriptor.parent_manifest,
184 target_manifest: descriptor.target_manifest,
185 assignment,
186 compatibility: Some(compatibility),
187 };
188 let admitted = self
189 .admit_submitted_plan(&controller, &proof, before, assignments, &plan, &target)
190 .await?;
191 tracing::info!(operation = %request.operation_id.get(), phase = ?admitted.phase,
192 "admitted durable topology request; coordinator owns further progress");
193 Ok(admitted)
194 }),
195 )
196 .await
197 .map_err(|_| TopologyError::Contended)?
198 }
199
200 async fn admit_submitted_plan(
202 &self,
203 controller: &ClusterController,
204 proof: &LeaderProof,
205 before: LocalProcessAuthorityIdentity,
206 assignments: &AssignmentSnapshotStore,
207 plan: &TopologyAdmissionPlan,
208 target: &CatalogManifest,
209 ) -> Result<TopologyAdmissionStatus, DbError> {
210 let authority = controller
211 .checkpoint_authority()
212 .map_err(|error| TopologyError::Protocol(error.to_string()))?;
213 let _checkpoints = controller.hold_periodic_checkpoints();
214 loop {
215 self.ensure_topology_submission_available()?;
216 if !controller.proof_is_live(proof)
217 || controller.try_live_local_process_authority_identity().ok() != Some(before)
218 {
219 return Err(TopologyError::Fenced.into());
220 }
221 match authority
222 .admit_topology_plan(proof, assignments, plan, target)
223 .await
224 {
225 Ok(admitted) => return Ok(admitted),
226 Err(TopologyError::Contended) => {
227 tokio::time::sleep(Duration::from_millis(25)).await;
231 }
232 Err(error) => return Err(error.into()),
233 }
234 }
235 }
236
237 pub async fn adopt_cluster_topology(
243 &self,
244 request: &ClusterTopologyAdoptionRequest,
245 ) -> Result<LegacyTopologyBaseline, DbError> {
246 self.adopt_cluster_topology_until(request, SUBMISSION_TIMEOUT, true)
247 .await
248 }
249
250 #[doc(hidden)]
255 pub async fn adopt_cluster_topology_forwarded(
256 &self,
257 request: &ClusterTopologyAdoptionRequest,
258 remaining: Duration,
259 ) -> Result<LegacyTopologyBaseline, DbError> {
260 self.adopt_cluster_topology_until(request, remaining.min(SUBMISSION_TIMEOUT), false)
261 .await
262 }
263
264 async fn adopt_cluster_topology_until(
265 &self,
266 request: &ClusterTopologyAdoptionRequest,
267 timeout: Duration,
268 allow_forwarding: bool,
269 ) -> Result<LegacyTopologyBaseline, DbError> {
270 if timeout.is_zero() {
271 return Err(TopologyError::Contended.into());
272 }
273 let deadline = tokio::time::Instant::now()
274 .checked_add(timeout)
275 .ok_or(TopologyError::Contended)?;
276 tokio::time::timeout_at(deadline, async {
277 if !request.coordinated_upgrade_complete {
278 return Err(TopologyError::Protocol("complete the coordinated binary upgrade on every process before explicit legacy adoption".into()).into());
279 }
280 let deployment = uuid::Uuid::parse_str(&request.expected_deployment_id)
281 .map_err(|_| TopologyError::Invalid("deployment must be a canonical nonzero UUID".into()))?;
282 if deployment.is_nil() || deployment.to_string() != request.expected_deployment_id {
283 return Err(TopologyError::Invalid("deployment must be a canonical nonzero UUID".into()).into());
284 }
285 let controller = self.topology_submission_controller()?;
286 self.ensure_topology_preparation_available()?;
287 if let Some(address) = self.topology_submission_route(&controller, allow_forwarding).await? {
288 let baseline: LegacyTopologyBaseline = self.forward_topology_request(&address, "/api/v1/cluster/topology/adopt", request, deadline).await?;
289 if baseline.manifest != request.expected_manifest
290 || baseline.deployment_id != request.expected_deployment_id {
291 return Err(TopologyError::Invalid("leader adoption receipt differs from the exact request".into()).into());
292 }
293 return Ok(baseline);
294 }
295 let authority = controller.checkpoint_authority()
296 .map_err(|error| TopologyError::Protocol(error.to_string()))?;
297 let proof = controller.capture_leader_proof().ok_or(TopologyError::Fenced)?;
298 let catalog = self.catalog_manifest_store.lock().clone()
299 .ok_or_else(|| TopologyError::Protocol("adoption requires configured catalog authority".into()))?;
300 let (manifest, _) = catalog.load_with_topology().await?
301 .ok_or_else(|| TopologyError::Conflict("legacy catalog is not sealed".into()))?;
302 if manifest.reference().map_err(TopologyError::from)? != request.expected_manifest
303 || self.catalog_manifest_inventory()? != manifest.entries {
304 return Err(TopologyError::Conflict("adoption requires the exact sealed, replayed live catalog".into()).into());
305 }
306 let baseline = match authority.adopt_legacy_topology(&proof, request.operation_id,
307 &request.expected_manifest, &request.expected_deployment_id).await? {
308 TopologyAdoptionOutcome::Created(baseline) | TopologyAdoptionOutcome::Existing(baseline) => baseline,
309 };
310 self.ensure_topology_preparation_available()?;
311 if !controller.proof_is_live(&proof) {
312 return Err(TopologyError::Fenced.into());
313 }
314 *self.replayed_topology.lock() = Some(super::ReplayedTopology::LEGACY_BASELINE);
315 Ok(baseline)
316 }).await.map_err(|_| TopologyError::Contended)?
317 }
318
319 pub(in crate::db) async fn submit_cluster_topology_sql(
320 &self,
321 sql: &str,
322 statement: &StreamingStatement,
323 ) -> Result<crate::handle::ExecuteResult, DbError> {
324 let (name, _, _) = super::catalog_changes::statement_identity(self, sql, statement)?;
325 let parent = self
326 .cluster_topology_status()
327 .await?
328 .committed_version
329 .ok_or_else(|| {
330 TopologyError::Protocol(
331 "explicit legacy topology adoption is required before runtime DDL".into(),
332 )
333 })?;
334 let request = ClusterTopologyRequest {
335 operation_id: uuid::Uuid::new_v4().try_into()?,
336 expected_parent_version: parent,
337 statements: vec![sql.to_owned()],
338 };
339 let status = Box::pin(self.submit_cluster_topology_change(&request))
340 .await
341 .map_err(|source| {
342 if matches!(
343 &source,
344 DbError::Topology(
345 TopologyError::Contended
346 | TopologyError::Authority(_)
347 | TopologyError::Fenced
348 )
349 ) {
350 DbError::TopologySubmission {
351 operation_id: request.operation_id,
352 source: Box::new(source),
353 }
354 } else {
355 source
356 }
357 })?;
358 Ok(crate::handle::ExecuteResult::Ddl(crate::handle::DdlInfo {
359 statement_type: "TOPOLOGY MIGRATION".into(),
360 object_name: name,
361 applied: false,
362 topology_operation: Some(Box::new(status)),
363 }))
364 }
365
366 fn topology_submission_controller(&self) -> Result<Arc<ClusterController>, DbError> {
367 if !self.is_cluster_runtime() {
368 return Err(TopologyError::Unsupported(
369 "topology submission requires cluster mode".into(),
370 )
371 .into());
372 }
373 self.cluster_controller.lock().clone().ok_or_else(|| {
374 TopologyError::Protocol("submission requires the configured cluster controller".into())
375 .into()
376 })
377 }
378
379 fn ensure_topology_submission_available(&self) -> Result<(), DbError> {
380 self.ensure_topology_preparation_available()?;
381 if self.cluster_intake_fenced()
382 || self.force_ckpt_tx.lock().is_none()
383 || self
384 .recovery_monitor
385 .lock()
386 .as_ref()
387 .is_none_or(tokio::task::JoinHandle::is_finished)
388 {
389 return Err(TopologyError::Conflict("submission requires released parent intake and the live checkpoint/recovery coordinator".into()).into());
390 }
391 Ok(())
392 }
393
394 async fn topology_submission_route(
395 &self,
396 controller: &ClusterController,
397 allow_forwarding: bool,
398 ) -> Result<Option<String>, DbError> {
399 let authority = controller
400 .checkpoint_authority()
401 .map_err(|error| TopologyError::Protocol(error.to_string()))?;
402 let lease = authority
403 .load()
404 .await
405 .map_err(TopologyError::from)?
406 .ok_or(TopologyError::Fenced)?;
407 if controller
408 .capture_leader_proof()
409 .as_ref()
410 .is_some_and(|proof| lease.matches_proof(proof))
411 {
412 return Ok(None);
413 }
414 if !allow_forwarding || !controller.process_lease_is_live() {
415 return Err(TopologyError::Fenced.into());
416 }
417 let watch = controller.members_watch();
418 let members = watch.borrow();
419 let address = members
420 .iter()
421 .find(|member| member.id.0 == lease.owner.node.0)
422 .map(|member| member.rpc_address.trim())
423 .filter(|address| !address.is_empty())
424 .ok_or_else(|| {
425 TopologyError::Conflict(
426 "durable leader HTTP address is unresolved; retry the same request identity"
427 .into(),
428 )
429 })?;
430 Ok(Some(address.to_owned()))
431 }
432}