1use std::collections::BTreeSet;
13use std::pin::pin;
14use std::sync::Arc;
15use std::time::Duration;
16
17use mz_adapter::ResultExt;
18use mz_catalog::durable::{
19 BootstrapArgs, CatalogError, DurableCatalogState, Metrics, OpenableDurableCatalogState,
20};
21use mz_controller_types::ReplicaId;
22use mz_ore::channel::trigger;
23use mz_ore::exit;
24use mz_ore::halt;
25use mz_ore::restart_in_place;
26use mz_ore::str::separated;
27use mz_persist_client::PersistClient;
28use mz_repr::{CatalogItemId, Timestamp};
29use mz_sql::catalog::EnvironmentId;
30use tokio::sync::oneshot;
31use tracing::info;
32
33use crate::BUILD_INFO;
34use crate::deployment::state::DeploymentState;
35
36pub struct CatchupConfig {
38 pub boot_ts: Timestamp,
39 pub environment_id: EnvironmentId,
40 pub persist_client: PersistClient,
41 pub deploy_generation: u64,
42 pub deployment_state: DeploymentState,
43 pub catalog_metrics: Arc<Metrics>,
44 pub caught_up_max_wait: Duration,
45 pub ddl_check_interval: Duration,
46 pub panic_after_timeout: bool,
47 pub bootstrap_args: BootstrapArgs,
48}
49
50pub async fn preflight_0dt(
55 openable_adapter_storage: &mut dyn OpenableDurableCatalogState,
56 deploy_generation: u64,
57 restarted_in_place: bool,
58) -> Result<bool, CatalogError> {
59 if !openable_adapter_storage.is_initialized().await? {
60 info!("catalog not initialized; booting with writes allowed");
61 return Ok(false);
62 }
63
64 let catalog_generation = openable_adapter_storage.get_deployment_generation().await?;
65 info!(%catalog_generation, %deploy_generation, "catalog initialized");
66 if catalog_generation < deploy_generation {
67 info!("this deployment is a new generation; booting in read only mode");
68 Ok(true)
69 } else if catalog_generation == deploy_generation {
70 if restarted_in_place {
78 halt!(
79 "restarted in place, but deployment generation {} has been promoted; halting",
80 catalog_generation
81 );
82 }
83 info!("this deployment is the current generation; booting with writes allowed");
84 Ok(false)
85 } else {
86 exit!(0, "this deployment has been fenced out");
87 }
88}
89
90pub fn spawn_catchup(
98 CatchupConfig {
99 boot_ts,
100 environment_id,
101 persist_client,
102 deploy_generation,
103 deployment_state,
104 catalog_metrics,
105 caught_up_max_wait,
106 ddl_check_interval,
107 panic_after_timeout,
108 bootstrap_args,
109 }: CatchupConfig,
110 mut caught_up_receiver: trigger::Receiver,
111 bootstrapped: oneshot::Receiver<(BTreeSet<CatalogItemId>, BTreeSet<ReplicaId>)>,
112) {
113 mz_ore::task::spawn(|| "deployment_catchup", async move {
114 let mut skip_catchup = deployment_state.set_catching_up();
115
116 let initial_ids = tokio::select! {
117 biased;
118
119 () = &mut skip_catchup => None,
120 result = bootstrapped => match result {
121 Ok(ids) => Some(ids),
122 Err(_) => return,
123 },
124 };
125
126 if let Some((initial_user_items, initial_user_replicas)) = initial_ids {
127 info!(
128 user_items = initial_user_items.len(),
129 user_replicas = initial_user_replicas.len(),
130 ?caught_up_max_wait,
131 "waiting for deployment to be caught up"
132 );
133
134 let mut caught_up_max_wait_fut = pin!(tokio::time::sleep(caught_up_max_wait));
135
136 let mut check_ddl_changes_interval = tokio::time::interval(ddl_check_interval);
137 check_ddl_changes_interval
138 .set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
139
140 let mut should_skip_catchup = false;
141 loop {
142 tokio::select! {
143 biased;
144
145 () = &mut skip_catchup => {
146 info!("skipping waiting for deployment to catch up due to administrator request");
147 should_skip_catchup = true;
148 break;
149 }
150 () = &mut caught_up_receiver => {
151 info!("deployment caught up");
152 break;
153 }
154 () = &mut caught_up_max_wait_fut => {
155 if panic_after_timeout {
156 panic!("not caught up within {:?}", caught_up_max_wait);
157 }
158 info!("not caught up within {:?}, proceeding now", caught_up_max_wait);
159 break;
160 }
161 _ = check_ddl_changes_interval.tick() => {
162 check_ddl_changes(
163 boot_ts,
164 persist_client.clone(),
165 environment_id.clone(),
166 deploy_generation,
167 Arc::clone(&catalog_metrics),
168 bootstrap_args.clone(),
169 &initial_user_items,
170 &initial_user_replicas,
171 )
172 .await;
173 }
174 }
175 }
176
177 if !should_skip_catchup {
180 check_ddl_changes(
181 boot_ts,
182 persist_client.clone(),
183 environment_id.clone(),
184 deploy_generation,
185 Arc::clone(&catalog_metrics),
186 bootstrap_args.clone(),
187 &initial_user_items,
188 &initial_user_replicas,
189 )
190 .await;
191 }
192 } else {
193 info!("skipping bootstrap and catch-up due to administrator request");
194 }
195
196 let promoted = deployment_state.set_ready_to_promote();
198 info!("announced as ready to promote; waiting for promotion");
199 promoted.await;
200
201 info!("promoted; attempting takeover");
203
204 let openable_adapter_storage = openable_catalog(
218 persist_client.clone(),
219 &environment_id,
220 deploy_generation,
221 Arc::clone(&catalog_metrics),
222 )
223 .await;
224
225 let _catalog = openable_adapter_storage
226 .open(boot_ts, &bootstrap_args)
227 .await
228 .unwrap_or_terminate("unexpected error while fencing out old deployment");
229
230 halt!("fenced out old deployment; rebooting as leader")
233 });
234}
235
236async fn openable_catalog(
237 persist_client: PersistClient,
238 environment_id: &EnvironmentId,
239 deploy_generation: u64,
240 catalog_metrics: Arc<Metrics>,
241) -> Box<dyn OpenableDurableCatalogState> {
242 mz_catalog::durable::persist_backed_catalog_state(
243 persist_client,
244 environment_id.organization_id(),
245 BUILD_INFO.semver_version(),
246 Some(deploy_generation),
247 catalog_metrics,
248 )
249 .await
250 .expect("incompatible catalog/persist version")
251}
252
253async fn check_ddl_changes(
256 boot_ts: Timestamp,
257 persist_client: PersistClient,
258 environment_id: EnvironmentId,
259 deploy_generation: u64,
260 catalog_metrics: Arc<Metrics>,
261 bootstrap_args: BootstrapArgs,
262 initial_user_items: &BTreeSet<CatalogItemId>,
263 initial_user_replicas: &BTreeSet<ReplicaId>,
264) {
265 let mut openable_adapter_storage = openable_catalog(
266 persist_client,
267 &environment_id,
268 deploy_generation,
269 catalog_metrics,
270 )
271 .await;
272
273 let catalog_generation = openable_adapter_storage
277 .get_deployment_generation()
278 .await
279 .unwrap_or_terminate("unexpected error while reading the deployment generation");
280
281 let mut catalog = openable_adapter_storage
282 .open_savepoint(boot_ts, &bootstrap_args)
283 .await
284 .unwrap_or_terminate("can open in savepoint mode");
285
286 let _ = catalog
289 .sync_to_current_updates()
290 .await
291 .unwrap_or_terminate("unexpected error while draining initial catalog updates");
292 let tx = catalog
293 .transaction()
294 .await
295 .unwrap_or_terminate("unexpected error while getting transaction");
296
297 let mut current_replicas = BTreeSet::new();
302 let mut new_replicas = Vec::new();
303 for replica in tx.get_cluster_replicas() {
304 current_replicas.insert(replica.replica_id);
305 if replica.replica_id.is_user() && !initial_user_replicas.contains(&replica.replica_id) {
306 new_replicas.push(replica);
307 }
308 }
309
310 let mut current_items = BTreeSet::new();
311 let mut new_objects = Vec::new();
312 for item in tx.get_items() {
313 current_items.insert(item.id);
314 if item.id.is_user() && !initial_user_items.contains(&item.id) {
315 new_objects.push(item);
316 }
317 }
318
319 let dropped_items = initial_user_items
320 .difference(¤t_items)
321 .collect::<Vec<_>>();
322 let dropped_replicas = initial_user_replicas
323 .difference(¤t_replicas)
324 .collect::<Vec<_>>();
325
326 if new_replicas.is_empty()
327 && new_objects.is_empty()
328 && dropped_items.is_empty()
329 && dropped_replicas.is_empty()
330 {
331 return;
332 }
333
334 let mut info_parts = Vec::new();
335
336 if !dropped_items.is_empty() {
337 info_parts.push(format!(
338 "Dropped objects: [{}]",
339 separated(", ", dropped_items)
340 ));
341 }
342 if !dropped_replicas.is_empty() {
343 info_parts.push(format!(
344 "Dropped replicas: [{}]",
345 separated(", ", dropped_replicas)
346 ));
347 }
348
349 if !new_replicas.is_empty() {
350 let replicas = new_replicas.iter().map(|r| {
351 format!(
352 "{{replica_id: {}, replica_name: {}, cluster_id: {}}}",
353 r.replica_id, r.name, r.cluster_id
354 )
355 });
356 info_parts.push(format!("New replicas: [{}]", separated(", ", replicas)));
357 }
358
359 if !new_objects.is_empty() {
360 let objects = new_objects
361 .iter()
362 .map(|o| format!("{{object_id: {}, object_name: {}}}", o.id, o.name));
363 info_parts.push(format!("New objects: [{}]", separated(", ", objects)));
364 }
365
366 let extra_info = separated(". ", info_parts);
367
368 if catalog_generation < deploy_generation {
372 restart_in_place!(
373 "there have been DDL that we need to react to; rebooting in read-only mode. {}",
374 extra_info
375 )
376 } else {
377 halt!(
378 "deployment generation {} has been promoted; halting",
379 catalog_generation
380 )
381 }
382}
383
384pub async fn get_user_ids(
387 catalog: &mut dyn DurableCatalogState,
388) -> Result<(BTreeSet<CatalogItemId>, BTreeSet<ReplicaId>), CatalogError> {
389 let snapshot = catalog.snapshot().await?;
391 let mut dry_run = catalog.transaction_from_snapshot(snapshot)?;
392 let tx = dry_run.transaction_mut();
393
394 let items = tx
395 .get_items()
396 .map(|item| item.id)
397 .filter(|id| id.is_user())
398 .collect();
399 let replicas = tx
400 .get_cluster_replicas()
401 .map(|replica| replica.replica_id)
402 .filter(|id| matches!(id, ReplicaId::User(_)))
403 .collect();
404 Ok((items, replicas))
405}
406
407#[cfg(test)]
408mod tests {
409 use super::*;
410 use mz_catalog::durable::{TestCatalogStateBuilder, test_bootstrap_args};
411 use mz_orchestratord::controller::materialize::generation::DeploymentStatus;
412 use mz_ore::metrics::MetricsRegistry;
413 use mz_ore::now::SYSTEM_TIME;
414 use mz_persist_client::PersistLocation;
415 use mz_persist_client::cache::PersistClientCache;
416 use mz_persist_client::cfg::PersistConfig;
417 use mz_persist_client::rpc::PubSubClientConnection;
418 use mz_repr::GlobalId;
419 use mz_sql::session::user::MZ_SYSTEM_ROLE_ID;
420
421 use crate::deployment::state::DeploymentStateHandle;
422
423 async fn setup() -> (
424 TestCatalogStateBuilder,
425 CatchupConfig,
426 DeploymentStateHandle,
427 ) {
428 let mut config = PersistConfig::new_for_tests();
429 config.build_version = BUILD_INFO.semver_version();
430 let cache = PersistClientCache::new(config, &MetricsRegistry::new(), |_, _| {
431 PubSubClientConnection::noop()
432 });
433 let persist_client = cache.open(PersistLocation::new_in_mem()).await.unwrap();
434 let environment_id = EnvironmentId::for_tests();
435 let metrics = Arc::new(Metrics::new(&MetricsRegistry::new()));
436 let builder = TestCatalogStateBuilder::new(persist_client.clone())
437 .with_organization_id(environment_id.organization_id())
438 .with_version(BUILD_INFO.semver_version())
439 .with_metrics(Arc::clone(&metrics))
440 .with_deploy_generation(0);
441 let boot_ts = SYSTEM_TIME().into();
442 let catalog = builder
443 .clone()
444 .unwrap_build()
445 .await
446 .open(boot_ts, &test_bootstrap_args())
447 .await
448 .unwrap();
449 catalog.expire().await;
450
451 let (deployment_state, handle) = DeploymentState::new();
452 let config = CatchupConfig {
453 boot_ts,
454 environment_id,
455 persist_client,
456 deploy_generation: 1,
457 deployment_state,
458 catalog_metrics: metrics,
459 caught_up_max_wait: Duration::from_secs(1),
460 ddl_check_interval: Duration::from_millis(10),
461 panic_after_timeout: false,
462 bootstrap_args: test_bootstrap_args(),
463 };
464 (builder, config, handle)
465 }
466
467 async fn wait_ready(handle: &DeploymentStateHandle) {
468 tokio::time::timeout(Duration::from_secs(10), async {
469 while handle.status() != DeploymentStatus::ReadyToPromote {
470 tokio::time::sleep(Duration::from_millis(10)).await;
471 }
472 })
473 .await
474 .unwrap();
475 }
476
477 #[mz_ore::test(tokio::test)]
478 async fn catchup_starts_after_bootstrap() {
479 let (builder, config, handle) = setup().await;
480 let mut openable = builder.with_deploy_generation(1).unwrap_build().await;
481 assert!(preflight_0dt(openable.as_mut(), 1, false).await.unwrap());
482 let (_trigger, receiver) = trigger::channel();
483 let (bootstrapped, bootstrapped_receiver) = oneshot::channel();
484 let metrics = Arc::clone(&config.catalog_metrics);
485 let before = metrics.transactions_started.get();
486 let caught_up_max_wait = config.caught_up_max_wait;
487 let (boot_ts, bootstrap_args) = (config.boot_ts, config.bootstrap_args.clone());
488
489 tokio::time::pause();
490 spawn_catchup(config, receiver, bootstrapped_receiver);
491 tokio::time::sleep(2 * caught_up_max_wait).await;
492 assert_eq!(metrics.transactions_started.get(), before);
493 assert_eq!(handle.status(), DeploymentStatus::Initializing);
494
495 let mut catalog = openable
496 .open_savepoint(boot_ts, &bootstrap_args)
497 .await
498 .unwrap();
499 bootstrapped
500 .send(get_user_ids(catalog.as_mut()).await.unwrap())
501 .unwrap();
502 wait_ready(&handle).await;
503 catalog.expire().await;
504 }
505
506 #[mz_ore::test(tokio::test)]
507 async fn skip_before_bootstrap() {
508 let (_, config, handle) = setup().await;
509 let (_trigger, receiver) = trigger::channel();
510 let (_bootstrapped, bootstrapped_receiver) = oneshot::channel();
511 spawn_catchup(config, receiver, bootstrapped_receiver);
512 tokio::time::timeout(Duration::from_secs(10), async {
513 while handle.try_skip_catchup().is_err() {
514 tokio::task::yield_now().await;
515 }
516 })
517 .await
518 .unwrap();
519 wait_ready(&handle).await;
520 }
521
522 #[mz_ore::test(tokio::test)]
523 async fn caught_up_before_task_starts() {
524 let (builder, mut config, handle) = setup().await;
525 let mut catalog = builder
526 .with_deploy_generation(1)
527 .unwrap_build()
528 .await
529 .open_savepoint(config.boot_ts, &config.bootstrap_args)
530 .await
531 .unwrap();
532 let (bootstrapped, bootstrapped_receiver) = oneshot::channel();
533 bootstrapped
534 .send(get_user_ids(catalog.as_mut()).await.unwrap())
535 .unwrap();
536 let (trigger, receiver) = trigger::channel();
537 drop(trigger);
538 config.caught_up_max_wait = Duration::ZERO;
539 config.panic_after_timeout = true;
540 spawn_catchup(config, receiver, bootstrapped_receiver);
541 wait_ready(&handle).await;
542 catalog.expire().await;
543 }
544
545 #[mz_ore::test(tokio::test)]
546 async fn baseline_uses_bootstrap_snapshot() {
547 let (builder, config, _) = setup().await;
548 let mut writer = builder
549 .clone()
550 .unwrap_build()
551 .await
552 .open(config.boot_ts, &config.bootstrap_args)
553 .await
554 .unwrap();
555 let mut catalog = builder
556 .with_deploy_generation(1)
557 .unwrap_build()
558 .await
559 .open_savepoint(config.boot_ts, &config.bootstrap_args)
560 .await
561 .unwrap();
562 let initial_ids = get_user_ids(catalog.as_mut()).await.unwrap();
563 assert!(!catalog.sync_to_current_updates().await.unwrap().is_empty());
564
565 writer.sync_to_current_updates().await.unwrap();
566 let mut tx = writer.transaction().await.unwrap();
567 let item_id = 1000;
568 let replica_id = 1000;
569 let schema_id = tx.get_schemas().find(|s| s.name == "public").unwrap().id;
570 tx.insert_item(
571 CatalogItemId::User(item_id),
572 20000,
573 GlobalId::User(item_id),
574 schema_id,
575 "new_item",
576 "CREATE TABLE new_item (a int)".into(),
577 MZ_SYSTEM_ROLE_ID,
578 Vec::new(),
579 Default::default(),
580 None,
581 )
582 .unwrap();
583 let replica = tx.get_cluster_replicas().next().unwrap();
584 tx.insert_cluster_replica_with_id(
585 replica.cluster_id,
586 ReplicaId::User(replica_id),
587 "new_replica",
588 replica.config,
589 replica.owner_id,
590 )
591 .unwrap();
592 let commit_ts = tx.upper();
593 let _ = tx.get_and_commit_op_updates();
594 tx.commit(commit_ts).await.unwrap();
595
596 assert_eq!(get_user_ids(catalog.as_mut()).await.unwrap(), initial_ids);
597 let mut expected = initial_ids;
598 expected.0.insert(CatalogItemId::User(item_id));
599 expected.1.insert(ReplicaId::User(replica_id));
600 assert_eq!(get_user_ids(writer.as_mut()).await.unwrap(), expected);
601 catalog.expire().await;
602 writer.expire().await;
603 }
604}