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