1use std::collections::BTreeSet;
32use std::time::{Duration, Instant};
33
34use anyhow::bail;
35use derivative::Derivative;
36use mz_adapter_types::dyncfgs::ENABLE_INTROSPECTION_SUBSCRIBES;
37use mz_cluster_client::ReplicaId;
38use mz_compute_client::controller::error::ERROR_TARGET_REPLICA_FAILED;
39use mz_compute_client::protocol::response::SubscribeBatch;
40use mz_controller_types::ClusterId;
41use mz_ore::collections::CollectionExt;
42use mz_ore::soft_panic_or_log;
43use mz_repr::optimize::OverrideFrom;
44use mz_repr::{Datum, GlobalId, Row};
45use mz_sql::catalog::SessionCatalog;
46use mz_sql::plan::{Params, Plan, SubscribePlan};
47use mz_sql::session::user::{MZ_SYSTEM_ROLE_ID, RoleMetadata};
48use mz_storage_client::controller::{IntrospectionType, StorageWriteOp};
49use tracing::{Span, info};
50
51use crate::coord::{
52 Coordinator, IntrospectionSubscribeFinish, IntrospectionSubscribeOptimizeMir,
53 IntrospectionSubscribeStage, IntrospectionSubscribeTimestampOptimizeLir, Message, PlanValidity,
54 StageResult, Staged,
55};
56use crate::optimize::Optimize;
57use crate::{AdapterError, ExecuteResponse, optimize};
58
59#[derive(Derivative)]
61#[derivative(Debug)]
62pub(super) struct IntrospectionSubscribe {
63 cluster_id: ClusterId,
65 replica_id: ReplicaId,
67 spec: &'static SubscribeSpec,
69 #[derivative(Debug = "ignore")]
77 deferred_write: Option<StorageWriteOp>,
78 first_data_at: Option<Instant>,
85}
86
87impl IntrospectionSubscribe {
88 fn delete_write_op(&self) -> StorageWriteOp {
91 let target_replica = self.replica_id.to_string();
92 let filter = Box::new(move |row: &Row| {
93 let replica_id = row.unpack_first();
94 replica_id == Datum::String(&target_replica)
95 });
96 StorageWriteOp::Delete { filter }
97 }
98}
99
100impl Coordinator {
101 pub(super) fn all_cluster_replicas(&self) -> Vec<(ClusterId, ReplicaId)> {
107 self.catalog
108 .clusters()
109 .flat_map(|cluster| {
110 cluster
111 .replicas()
112 .map(move |replica| (cluster.id, replica.replica_id))
113 })
114 .collect()
115 }
116
117 pub(super) async fn bootstrap_introspection_subscribes(&mut self) {
121 for (cluster_id, replica_id) in self.all_cluster_replicas() {
122 self.install_introspection_subscribes(cluster_id, replica_id)
123 .await;
124 }
125 }
126
127 pub(super) async fn install_introspection_subscribes(
129 &mut self,
130 cluster_id: ClusterId,
131 replica_id: ReplicaId,
132 ) {
133 let dyncfgs = self.catalog().system_config().dyncfgs();
134 if !ENABLE_INTROSPECTION_SUBSCRIBES.get(dyncfgs) {
135 return;
136 }
137
138 for spec in SUBSCRIBES {
139 self.install_introspection_subscribe(cluster_id, replica_id, spec)
140 .await;
141 }
142 }
143
144 async fn install_introspection_subscribe(
145 &mut self,
146 cluster_id: ClusterId,
147 replica_id: ReplicaId,
148 spec: &'static SubscribeSpec,
149 ) {
150 let (_, id) = self.allocate_transient_id();
151 info!(
152 %id,
153 %replica_id,
154 type_ = ?spec.introspection_type,
155 "installing introspection subscribe",
156 );
157
158 let subscribe = IntrospectionSubscribe {
162 cluster_id,
163 replica_id,
164 spec,
165 deferred_write: None,
166 first_data_at: None,
167 };
168 self.introspection_subscribes.insert(id, subscribe);
169
170 self.sequence_introspection_subscribe(id, spec, cluster_id, replica_id)
171 .await;
172 }
173
174 async fn sequence_introspection_subscribe(
175 &mut self,
176 subscribe_id: GlobalId,
177 spec: &'static SubscribeSpec,
178 cluster_id: ClusterId,
179 replica_id: ReplicaId,
180 ) {
181 let catalog = self.catalog().for_system_session();
182 let plan = spec.to_plan(&catalog).expect("valid spec");
183
184 let role_metadata = RoleMetadata::new(MZ_SYSTEM_ROLE_ID);
185 let dependencies = plan
186 .from
187 .depends_on()
188 .iter()
189 .map(|id| self.catalog().resolve_item_id(id))
190 .collect();
191 let validity = PlanValidity::new(
192 &self.catalog,
193 dependencies,
194 Some(cluster_id),
195 Some(replica_id),
196 role_metadata,
197 );
198
199 let stage = IntrospectionSubscribeStage::OptimizeMir(IntrospectionSubscribeOptimizeMir {
200 validity,
201 plan,
202 subscribe_id,
203 cluster_id,
204 replica_id,
205 });
206 self.sequence_staged((), Span::current(), stage).await;
207 }
208
209 fn sequence_introspection_subscribe_optimize_mir(
210 &self,
211 stage: IntrospectionSubscribeOptimizeMir,
212 ) -> Result<StageResult<Box<IntrospectionSubscribeStage>>, AdapterError> {
213 let IntrospectionSubscribeOptimizeMir {
214 mut validity,
215 plan,
216 subscribe_id,
217 cluster_id,
218 replica_id,
219 } = stage;
220
221 let compute_instance = self.instance_snapshot(cluster_id).expect("must exist");
222 let (_, view_id) = self.allocate_transient_id();
223
224 let vars = self.catalog().system_config();
225 let overrides = self.catalog.get_cluster(cluster_id).config.features();
226 let optimizer_config = optimize::OptimizerConfig::from(vars)
227 .override_from(&overrides)
228 .override_from(&self.cluster_scoped_optimizer_overrides(cluster_id));
229
230 let mut optimizer = optimize::subscribe::Optimizer::new(
231 self.owned_catalog(),
232 compute_instance,
233 view_id,
234 subscribe_id,
235 plan.with_snapshot,
236 None,
237 format!("introspection-subscribe-{subscribe_id}"),
238 optimizer_config,
239 self.optimizer_metrics(),
240 );
241 let catalog = self.owned_catalog();
242
243 let span = Span::current();
244 Ok(StageResult::Handle(mz_ore::task::spawn_blocking(
245 || "optimize introspection subscribe (mir)",
246 move || {
247 span.in_scope(|| {
248 let global_mir_plan = optimizer.catch_unwind_optimize(plan)?;
250 let id_bundle = global_mir_plan.id_bundle(cluster_id);
252 let item_ids = id_bundle.iter().map(|id| catalog.resolve_item_id(&id));
253 validity.extend_dependencies(&catalog, item_ids);
254
255 let stage = IntrospectionSubscribeStage::TimestampOptimizeLir(
256 IntrospectionSubscribeTimestampOptimizeLir {
257 validity,
258 optimizer,
259 global_mir_plan,
260 cluster_id,
261 replica_id,
262 },
263 );
264 Ok(Box::new(stage))
265 })
266 },
267 )))
268 }
269
270 fn sequence_introspection_subscribe_timestamp_optimize_lir(
271 &self,
272 stage: IntrospectionSubscribeTimestampOptimizeLir,
273 ) -> Result<StageResult<Box<IntrospectionSubscribeStage>>, AdapterError> {
274 let IntrospectionSubscribeTimestampOptimizeLir {
275 validity,
276 mut optimizer,
277 global_mir_plan,
278 cluster_id,
279 replica_id,
280 } = stage;
281
282 let id_bundle = global_mir_plan.id_bundle(cluster_id);
284 let read_holds = self.acquire_read_holds(&id_bundle);
285 let as_of = read_holds.least_valid_read();
286
287 let global_mir_plan = global_mir_plan.resolve(as_of);
288
289 let span = Span::current();
290 Ok(StageResult::Handle(mz_ore::task::spawn_blocking(
291 || "optimize introspection subscribe (lir)",
292 move || {
293 span.in_scope(|| {
294 let global_lir_plan =
296 optimizer.catch_unwind_optimize(global_mir_plan.clone())?;
297
298 let stage = IntrospectionSubscribeStage::Finish(IntrospectionSubscribeFinish {
299 validity,
300 global_lir_plan,
301 read_holds,
302 cluster_id,
303 replica_id,
304 });
305 Ok(Box::new(stage))
306 })
307 },
308 )))
309 }
310
311 async fn sequence_introspection_subscribe_finish(
312 &mut self,
313 stage: IntrospectionSubscribeFinish,
314 ) -> Result<StageResult<Box<IntrospectionSubscribeStage>>, AdapterError> {
315 let IntrospectionSubscribeFinish {
316 validity: _,
317 global_lir_plan,
318 read_holds,
319 cluster_id,
320 replica_id,
321 } = stage;
322
323 let subscribe_id = global_lir_plan.sink_id();
324
325 let response = if self.introspection_subscribes.contains_key(&subscribe_id) {
328 let (df_desc, _df_meta) = global_lir_plan.unapply();
329 self.ship_dataflow(df_desc, cluster_id, Some(replica_id))
330 .await;
331
332 Ok(StageResult::Response(
333 ExecuteResponse::CreatedIntrospectionSubscribe,
334 ))
335 } else {
336 Err(AdapterError::internal(
337 "introspection",
338 "introspection subscribe has already been dropped",
339 ))
340 };
341
342 drop(read_holds);
343 response
344 }
345
346 pub(super) fn drop_introspection_subscribes(&mut self, replica_id: ReplicaId) {
354 let to_drop: Vec<_> = self
355 .introspection_subscribes
356 .iter()
357 .filter(|(_, s)| s.replica_id == replica_id)
358 .map(|(id, _)| *id)
359 .collect();
360
361 for id in to_drop {
362 self.drop_introspection_subscribe(id);
363 }
364 }
365
366 fn drop_introspection_subscribe(&mut self, id: GlobalId) {
367 let Some(subscribe) = self.introspection_subscribes.remove(&id) else {
368 soft_panic_or_log!("attempt to remove unknown introspection subscribe (id={id})");
369 return;
370 };
371
372 info!(
373 %id,
374 replica_id = %subscribe.replica_id,
375 type_ = ?subscribe.spec.introspection_type,
376 "dropping introspection subscribe",
377 );
378
379 let _ = self
383 .controller
384 .compute
385 .drop_collections(subscribe.cluster_id, vec![id]);
386
387 self.controller.storage.update_introspection_collection(
388 subscribe.spec.introspection_type,
389 subscribe.delete_write_op(),
390 );
391 }
392
393 async fn reinstall_introspection_subscribe(&mut self, id: GlobalId) {
394 let Some(mut subscribe) = self.introspection_subscribes.remove(&id) else {
395 soft_panic_or_log!("attempt to reinstall unknown introspection subscribe (id={id})");
396 return;
397 };
398
399 let IntrospectionSubscribe {
405 cluster_id,
406 replica_id,
407 spec,
408 ..
409 } = subscribe;
410 let old_id = id;
411 let (_, new_id) = self.allocate_transient_id();
412
413 info!(
414 %old_id, %new_id, %replica_id,
415 type_ = ?subscribe.spec.introspection_type,
416 "reinstalling introspection subscribe",
417 );
418
419 if let Err(error) = self
420 .controller
421 .compute
422 .drop_collections(cluster_id, vec![old_id])
423 {
424 soft_panic_or_log!(
425 "error dropping compute collection for introspection subscribe: {error} \
426 (id={old_id}, cluster_id={cluster_id})"
427 );
428 }
429
430 subscribe.deferred_write = Some(subscribe.delete_write_op());
433 subscribe.first_data_at = None;
436
437 self.introspection_subscribes.insert(new_id, subscribe);
438 self.sequence_introspection_subscribe(new_id, spec, cluster_id, replica_id)
439 .await;
440 }
441
442 pub(super) async fn handle_introspection_subscribe_batch(
447 &mut self,
448 id: GlobalId,
449 batch: SubscribeBatch,
450 ) {
451 let Some(subscribe) = self.introspection_subscribes.get_mut(&id) else {
452 soft_panic_or_log!("updates for unknown introspection subscribe (id={id})");
453 return;
454 };
455
456 let updates = match batch.updates {
457 Ok(updates) if updates.is_empty() => return,
458 Ok(updates) => updates,
459 Err(error) if error == ERROR_TARGET_REPLICA_FAILED => {
460 self.reinstall_introspection_subscribe(id).await;
462 return;
463 }
464 Err(error) => {
465 soft_panic_or_log!(
466 "introspection subscribe produced an error: {error} \
467 (id={id}, subscribe={subscribe:?})",
468 );
469 return;
470 }
471 };
472
473 let replica_id = subscribe.replica_id.to_string();
475 let mut new_updates = Vec::with_capacity(updates.len());
476 let mut new_row = Row::default();
477 for collection in updates {
478 for (row, _time, diff) in collection.iter() {
479 let mut packer = new_row.packer();
480 packer.push(Datum::String(&replica_id));
481 packer.extend_by_row_ref(row);
482 new_updates.push((new_row.clone(), diff));
483 }
484 }
485
486 if let Some(op) = subscribe.deferred_write.take() {
489 self.controller
490 .storage
491 .update_introspection_collection(subscribe.spec.introspection_type, op);
492 }
493
494 subscribe.first_data_at.get_or_insert_with(Instant::now);
495
496 self.controller.storage.update_introspection_collection(
497 subscribe.spec.introspection_type,
498 StorageWriteOp::Append {
499 updates: new_updates,
500 },
501 );
502 }
503
504 pub(super) fn invalidate_introspection_freshness(&mut self, replica_id: ReplicaId) {
512 for subscribe in self.introspection_subscribes.values_mut() {
513 if subscribe.replica_id == replica_id {
514 subscribe.first_data_at = None;
515 }
516 }
517 }
518
519 pub(super) fn fresh_introspection_replicas(
529 &self,
530 introspection_type: IntrospectionType,
531 margin: Duration,
532 ) -> BTreeSet<String> {
533 self.introspection_subscribes
534 .values()
535 .filter(|s| s.spec.introspection_type == introspection_type)
536 .filter(|s| s.first_data_at.is_some_and(|at| at.elapsed() >= margin))
537 .map(|s| s.replica_id.to_string())
538 .collect()
539 }
540}
541
542impl Staged for IntrospectionSubscribeStage {
543 type Ctx = ();
544
545 fn validity(&mut self) -> &mut PlanValidity {
546 match self {
547 Self::OptimizeMir(stage) => &mut stage.validity,
548 Self::TimestampOptimizeLir(stage) => &mut stage.validity,
549 Self::Finish(stage) => &mut stage.validity,
550 }
551 }
552
553 async fn stage(
554 self,
555 coord: &mut Coordinator,
556 _ctx: &mut (),
557 ) -> Result<StageResult<Box<Self>>, AdapterError> {
558 match self {
559 Self::OptimizeMir(stage) => coord.sequence_introspection_subscribe_optimize_mir(stage),
560 Self::TimestampOptimizeLir(stage) => {
561 coord.sequence_introspection_subscribe_timestamp_optimize_lir(stage)
562 }
563 Self::Finish(stage) => coord.sequence_introspection_subscribe_finish(stage).await,
564 }
565 }
566
567 fn message(self, _ctx: (), span: Span) -> super::Message {
568 Message::IntrospectionSubscribeStageReady { span, stage: self }
569 }
570
571 fn cancel_enabled(&self) -> bool {
572 false
573 }
574}
575
576#[derive(Debug)]
578pub(super) struct SubscribeSpec {
579 introspection_type: IntrospectionType,
582 sql: &'static str,
584}
585
586impl SubscribeSpec {
587 fn to_plan(&self, catalog: &dyn SessionCatalog) -> Result<SubscribePlan, anyhow::Error> {
588 let parsed = mz_sql::parse::parse(self.sql)?.into_element();
589 let (stmt, resolved_ids) = mz_sql::names::resolve(catalog, parsed.ast)?;
590 let (plan, _sql_impl_ids) =
591 mz_sql::plan::plan(None, catalog, stmt, &Params::empty(), &resolved_ids)?;
592 match plan {
593 Plan::Subscribe(plan) => Ok(plan),
594 _ => bail!("unexpected plan type: {plan:?}"),
595 }
596 }
597}
598
599const SUBSCRIBES: &[SubscribeSpec] = &[
600 SubscribeSpec {
601 introspection_type: IntrospectionType::ComputeErrorCounts,
602 sql: "SUBSCRIBE (
603 SELECT export_id, sum(count)
604 FROM mz_introspection.mz_compute_error_counts_raw
605 GROUP BY export_id
606 )",
607 },
608 SubscribeSpec {
609 introspection_type: IntrospectionType::ComputeHydrationTimes,
610 sql: "SUBSCRIBE (
611 SELECT
612 export_id,
613 CASE count(*) = count(time_ns)
614 WHEN true THEN max(time_ns)
615 ELSE NULL
616 END AS time_ns
617 FROM mz_introspection.mz_compute_hydration_times_per_worker
618 WHERE export_id NOT LIKE 't%'
619 GROUP BY export_id
620 OPTIONS (AGGREGATE INPUT GROUP SIZE = 1)
621 )",
622 },
623 SubscribeSpec {
624 introspection_type: IntrospectionType::ComputeOperatorHydrationStatus,
625 sql: "SUBSCRIBE (
626 SELECT
627 export_id,
628 lir_id,
629 bool_and(hydrated) AS hydrated
630 FROM mz_introspection.mz_compute_operator_hydration_statuses_per_worker
631 GROUP BY export_id, lir_id
632 )",
633 },
634 SubscribeSpec {
658 introspection_type: IntrospectionType::ComputeObjectArrangementSizes,
659 sql: "SUBSCRIBE (
660 SELECT
661 ce.export_id AS object_id,
662 ((COUNT(*) + 5242880) / 10485760 * 10485760)::int8 AS size
663 FROM mz_introspection.mz_compute_exports AS ce
664 JOIN (
665 SELECT addrs.address[1] AS dataflow_id, addrs.id AS operator_id
666 FROM mz_introspection.mz_dataflow_addresses addrs
667 ) AS od ON od.dataflow_id = ce.dataflow_id
668 JOIN (
669 SELECT operator_id FROM mz_introspection.mz_arrangement_heap_size_raw
670 UNION ALL
671 SELECT operator_id FROM mz_introspection.mz_arrangement_batcher_size_raw
672 ) AS rs ON rs.operator_id = od.operator_id
673 WHERE ce.export_id NOT LIKE 't%'
674 GROUP BY ce.export_id
675 )",
676 },
677];