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) async fn bootstrap_introspection_subscribes(&mut self) {
105 let mut cluster_replicas = Vec::new();
106 for cluster in self.catalog.clusters() {
107 for replica in cluster.replicas() {
108 cluster_replicas.push((cluster.id, replica.replica_id));
109 }
110 }
111
112 for (cluster_id, replica_id) in cluster_replicas {
113 self.install_introspection_subscribes(cluster_id, replica_id)
114 .await;
115 }
116 }
117
118 pub(super) async fn install_introspection_subscribes(
120 &mut self,
121 cluster_id: ClusterId,
122 replica_id: ReplicaId,
123 ) {
124 let dyncfgs = self.catalog().system_config().dyncfgs();
125 if !ENABLE_INTROSPECTION_SUBSCRIBES.get(dyncfgs) {
126 return;
127 }
128
129 for spec in SUBSCRIBES {
130 self.install_introspection_subscribe(cluster_id, replica_id, spec)
131 .await;
132 }
133 }
134
135 async fn install_introspection_subscribe(
136 &mut self,
137 cluster_id: ClusterId,
138 replica_id: ReplicaId,
139 spec: &'static SubscribeSpec,
140 ) {
141 let (_, id) = self.allocate_transient_id();
142 info!(
143 %id,
144 %replica_id,
145 type_ = ?spec.introspection_type,
146 "installing introspection subscribe",
147 );
148
149 let subscribe = IntrospectionSubscribe {
153 cluster_id,
154 replica_id,
155 spec,
156 deferred_write: None,
157 first_data_at: None,
158 };
159 self.introspection_subscribes.insert(id, subscribe);
160
161 self.sequence_introspection_subscribe(id, spec, cluster_id, replica_id)
162 .await;
163 }
164
165 async fn sequence_introspection_subscribe(
166 &mut self,
167 subscribe_id: GlobalId,
168 spec: &'static SubscribeSpec,
169 cluster_id: ClusterId,
170 replica_id: ReplicaId,
171 ) {
172 let catalog = self.catalog().for_system_session();
173 let plan = spec.to_plan(&catalog).expect("valid spec");
174
175 let role_metadata = RoleMetadata::new(MZ_SYSTEM_ROLE_ID);
176 let dependencies = plan
177 .from
178 .depends_on()
179 .iter()
180 .map(|id| self.catalog().resolve_item_id(id))
181 .collect();
182 let validity = PlanValidity::new(
183 &self.catalog,
184 dependencies,
185 Some(cluster_id),
186 Some(replica_id),
187 role_metadata,
188 );
189
190 let stage = IntrospectionSubscribeStage::OptimizeMir(IntrospectionSubscribeOptimizeMir {
191 validity,
192 plan,
193 subscribe_id,
194 cluster_id,
195 replica_id,
196 });
197 self.sequence_staged((), Span::current(), stage).await;
198 }
199
200 fn sequence_introspection_subscribe_optimize_mir(
201 &self,
202 stage: IntrospectionSubscribeOptimizeMir,
203 ) -> Result<StageResult<Box<IntrospectionSubscribeStage>>, AdapterError> {
204 let IntrospectionSubscribeOptimizeMir {
205 mut validity,
206 plan,
207 subscribe_id,
208 cluster_id,
209 replica_id,
210 } = stage;
211
212 let compute_instance = self.instance_snapshot(cluster_id).expect("must exist");
213 let (_, view_id) = self.allocate_transient_id();
214
215 let vars = self.catalog().system_config();
216 let overrides = self.catalog.get_cluster(cluster_id).config.features();
217 let optimizer_config = optimize::OptimizerConfig::from(vars)
218 .override_from(&overrides)
219 .override_from(&self.cluster_scoped_optimizer_overrides(cluster_id));
220
221 let mut optimizer = optimize::subscribe::Optimizer::new(
222 self.owned_catalog(),
223 compute_instance,
224 view_id,
225 subscribe_id,
226 plan.with_snapshot,
227 None,
228 format!("introspection-subscribe-{subscribe_id}"),
229 optimizer_config,
230 self.optimizer_metrics(),
231 );
232 let catalog = self.owned_catalog();
233
234 let span = Span::current();
235 Ok(StageResult::Handle(mz_ore::task::spawn_blocking(
236 || "optimize introspection subscribe (mir)",
237 move || {
238 span.in_scope(|| {
239 let global_mir_plan = optimizer.catch_unwind_optimize(plan)?;
241 let id_bundle = global_mir_plan.id_bundle(cluster_id);
243 let item_ids = id_bundle.iter().map(|id| catalog.resolve_item_id(&id));
244 validity.extend_dependencies(&catalog, item_ids);
245
246 let stage = IntrospectionSubscribeStage::TimestampOptimizeLir(
247 IntrospectionSubscribeTimestampOptimizeLir {
248 validity,
249 optimizer,
250 global_mir_plan,
251 cluster_id,
252 replica_id,
253 },
254 );
255 Ok(Box::new(stage))
256 })
257 },
258 )))
259 }
260
261 fn sequence_introspection_subscribe_timestamp_optimize_lir(
262 &self,
263 stage: IntrospectionSubscribeTimestampOptimizeLir,
264 ) -> Result<StageResult<Box<IntrospectionSubscribeStage>>, AdapterError> {
265 let IntrospectionSubscribeTimestampOptimizeLir {
266 validity,
267 mut optimizer,
268 global_mir_plan,
269 cluster_id,
270 replica_id,
271 } = stage;
272
273 let id_bundle = global_mir_plan.id_bundle(cluster_id);
275 let read_holds = self.acquire_read_holds(&id_bundle);
276 let as_of = read_holds.least_valid_read();
277
278 let global_mir_plan = global_mir_plan.resolve(as_of);
279
280 let span = Span::current();
281 Ok(StageResult::Handle(mz_ore::task::spawn_blocking(
282 || "optimize introspection subscribe (lir)",
283 move || {
284 span.in_scope(|| {
285 let global_lir_plan =
287 optimizer.catch_unwind_optimize(global_mir_plan.clone())?;
288
289 let stage = IntrospectionSubscribeStage::Finish(IntrospectionSubscribeFinish {
290 validity,
291 global_lir_plan,
292 read_holds,
293 cluster_id,
294 replica_id,
295 });
296 Ok(Box::new(stage))
297 })
298 },
299 )))
300 }
301
302 async fn sequence_introspection_subscribe_finish(
303 &mut self,
304 stage: IntrospectionSubscribeFinish,
305 ) -> Result<StageResult<Box<IntrospectionSubscribeStage>>, AdapterError> {
306 let IntrospectionSubscribeFinish {
307 validity: _,
308 global_lir_plan,
309 read_holds,
310 cluster_id,
311 replica_id,
312 } = stage;
313
314 let subscribe_id = global_lir_plan.sink_id();
315
316 let response = if self.introspection_subscribes.contains_key(&subscribe_id) {
319 let (df_desc, _df_meta) = global_lir_plan.unapply();
320 self.ship_dataflow(df_desc, cluster_id, Some(replica_id))
321 .await;
322
323 Ok(StageResult::Response(
324 ExecuteResponse::CreatedIntrospectionSubscribe,
325 ))
326 } else {
327 Err(AdapterError::internal(
328 "introspection",
329 "introspection subscribe has already been dropped",
330 ))
331 };
332
333 drop(read_holds);
334 response
335 }
336
337 pub(super) fn drop_introspection_subscribes(&mut self, replica_id: ReplicaId) {
345 let to_drop: Vec<_> = self
346 .introspection_subscribes
347 .iter()
348 .filter(|(_, s)| s.replica_id == replica_id)
349 .map(|(id, _)| *id)
350 .collect();
351
352 for id in to_drop {
353 self.drop_introspection_subscribe(id);
354 }
355 }
356
357 fn drop_introspection_subscribe(&mut self, id: GlobalId) {
358 let Some(subscribe) = self.introspection_subscribes.remove(&id) else {
359 soft_panic_or_log!("attempt to remove unknown introspection subscribe (id={id})");
360 return;
361 };
362
363 info!(
364 %id,
365 replica_id = %subscribe.replica_id,
366 type_ = ?subscribe.spec.introspection_type,
367 "dropping introspection subscribe",
368 );
369
370 let _ = self
374 .controller
375 .compute
376 .drop_collections(subscribe.cluster_id, vec![id]);
377
378 self.controller.storage.update_introspection_collection(
379 subscribe.spec.introspection_type,
380 subscribe.delete_write_op(),
381 );
382 }
383
384 async fn reinstall_introspection_subscribe(&mut self, id: GlobalId) {
385 let Some(mut subscribe) = self.introspection_subscribes.remove(&id) else {
386 soft_panic_or_log!("attempt to reinstall unknown introspection subscribe (id={id})");
387 return;
388 };
389
390 let IntrospectionSubscribe {
396 cluster_id,
397 replica_id,
398 spec,
399 ..
400 } = subscribe;
401 let old_id = id;
402 let (_, new_id) = self.allocate_transient_id();
403
404 info!(
405 %old_id, %new_id, %replica_id,
406 type_ = ?subscribe.spec.introspection_type,
407 "reinstalling introspection subscribe",
408 );
409
410 if let Err(error) = self
411 .controller
412 .compute
413 .drop_collections(cluster_id, vec![old_id])
414 {
415 soft_panic_or_log!(
416 "error dropping compute collection for introspection subscribe: {error} \
417 (id={old_id}, cluster_id={cluster_id})"
418 );
419 }
420
421 subscribe.deferred_write = Some(subscribe.delete_write_op());
424 subscribe.first_data_at = None;
427
428 self.introspection_subscribes.insert(new_id, subscribe);
429 self.sequence_introspection_subscribe(new_id, spec, cluster_id, replica_id)
430 .await;
431 }
432
433 pub(super) async fn handle_introspection_subscribe_batch(
438 &mut self,
439 id: GlobalId,
440 batch: SubscribeBatch,
441 ) {
442 let Some(subscribe) = self.introspection_subscribes.get_mut(&id) else {
443 soft_panic_or_log!("updates for unknown introspection subscribe (id={id})");
444 return;
445 };
446
447 let updates = match batch.updates {
448 Ok(updates) if updates.is_empty() => return,
449 Ok(updates) => updates,
450 Err(error) if error == ERROR_TARGET_REPLICA_FAILED => {
451 self.reinstall_introspection_subscribe(id).await;
453 return;
454 }
455 Err(error) => {
456 soft_panic_or_log!(
457 "introspection subscribe produced an error: {error} \
458 (id={id}, subscribe={subscribe:?})",
459 );
460 return;
461 }
462 };
463
464 let replica_id = subscribe.replica_id.to_string();
466 let mut new_updates = Vec::with_capacity(updates.len());
467 let mut new_row = Row::default();
468 for collection in updates {
469 for (row, _time, diff) in collection.iter() {
470 let mut packer = new_row.packer();
471 packer.push(Datum::String(&replica_id));
472 packer.extend_by_row_ref(row);
473 new_updates.push((new_row.clone(), diff));
474 }
475 }
476
477 if let Some(op) = subscribe.deferred_write.take() {
480 self.controller
481 .storage
482 .update_introspection_collection(subscribe.spec.introspection_type, op);
483 }
484
485 subscribe.first_data_at.get_or_insert_with(Instant::now);
486
487 self.controller.storage.update_introspection_collection(
488 subscribe.spec.introspection_type,
489 StorageWriteOp::Append {
490 updates: new_updates,
491 },
492 );
493 }
494
495 pub(super) fn invalidate_introspection_freshness(&mut self, replica_id: ReplicaId) {
503 for subscribe in self.introspection_subscribes.values_mut() {
504 if subscribe.replica_id == replica_id {
505 subscribe.first_data_at = None;
506 }
507 }
508 }
509
510 pub(super) fn fresh_introspection_replicas(
520 &self,
521 introspection_type: IntrospectionType,
522 margin: Duration,
523 ) -> BTreeSet<String> {
524 self.introspection_subscribes
525 .values()
526 .filter(|s| s.spec.introspection_type == introspection_type)
527 .filter(|s| s.first_data_at.is_some_and(|at| at.elapsed() >= margin))
528 .map(|s| s.replica_id.to_string())
529 .collect()
530 }
531}
532
533impl Staged for IntrospectionSubscribeStage {
534 type Ctx = ();
535
536 fn validity(&mut self) -> &mut PlanValidity {
537 match self {
538 Self::OptimizeMir(stage) => &mut stage.validity,
539 Self::TimestampOptimizeLir(stage) => &mut stage.validity,
540 Self::Finish(stage) => &mut stage.validity,
541 }
542 }
543
544 async fn stage(
545 self,
546 coord: &mut Coordinator,
547 _ctx: &mut (),
548 ) -> Result<StageResult<Box<Self>>, AdapterError> {
549 match self {
550 Self::OptimizeMir(stage) => coord.sequence_introspection_subscribe_optimize_mir(stage),
551 Self::TimestampOptimizeLir(stage) => {
552 coord.sequence_introspection_subscribe_timestamp_optimize_lir(stage)
553 }
554 Self::Finish(stage) => coord.sequence_introspection_subscribe_finish(stage).await,
555 }
556 }
557
558 fn message(self, _ctx: (), span: Span) -> super::Message {
559 Message::IntrospectionSubscribeStageReady { span, stage: self }
560 }
561
562 fn cancel_enabled(&self) -> bool {
563 false
564 }
565}
566
567#[derive(Debug)]
569pub(super) struct SubscribeSpec {
570 introspection_type: IntrospectionType,
573 sql: &'static str,
575}
576
577impl SubscribeSpec {
578 fn to_plan(&self, catalog: &dyn SessionCatalog) -> Result<SubscribePlan, anyhow::Error> {
579 let parsed = mz_sql::parse::parse(self.sql)?.into_element();
580 let (stmt, resolved_ids) = mz_sql::names::resolve(catalog, parsed.ast)?;
581 let (plan, _sql_impl_ids) =
582 mz_sql::plan::plan(None, catalog, stmt, &Params::empty(), &resolved_ids)?;
583 match plan {
584 Plan::Subscribe(plan) => Ok(plan),
585 _ => bail!("unexpected plan type: {plan:?}"),
586 }
587 }
588}
589
590const SUBSCRIBES: &[SubscribeSpec] = &[
591 SubscribeSpec {
592 introspection_type: IntrospectionType::ComputeErrorCounts,
593 sql: "SUBSCRIBE (
594 SELECT export_id, sum(count)
595 FROM mz_introspection.mz_compute_error_counts_raw
596 GROUP BY export_id
597 )",
598 },
599 SubscribeSpec {
600 introspection_type: IntrospectionType::ComputeHydrationTimes,
601 sql: "SUBSCRIBE (
602 SELECT
603 export_id,
604 CASE count(*) = count(time_ns)
605 WHEN true THEN max(time_ns)
606 ELSE NULL
607 END AS time_ns
608 FROM mz_introspection.mz_compute_hydration_times_per_worker
609 WHERE export_id NOT LIKE 't%'
610 GROUP BY export_id
611 OPTIONS (AGGREGATE INPUT GROUP SIZE = 1)
612 )",
613 },
614 SubscribeSpec {
615 introspection_type: IntrospectionType::ComputeOperatorHydrationStatus,
616 sql: "SUBSCRIBE (
617 SELECT
618 export_id,
619 lir_id,
620 bool_and(hydrated) AS hydrated
621 FROM mz_introspection.mz_compute_operator_hydration_statuses_per_worker
622 GROUP BY export_id, lir_id
623 )",
624 },
625 SubscribeSpec {
649 introspection_type: IntrospectionType::ComputeObjectArrangementSizes,
650 sql: "SUBSCRIBE (
651 SELECT
652 ce.export_id AS object_id,
653 ((COUNT(*) + 5242880) / 10485760 * 10485760)::int8 AS size
654 FROM mz_introspection.mz_compute_exports AS ce
655 JOIN (
656 SELECT addrs.address[1] AS dataflow_id, addrs.id AS operator_id
657 FROM mz_introspection.mz_dataflow_addresses addrs
658 ) AS od ON od.dataflow_id = ce.dataflow_id
659 JOIN (
660 SELECT operator_id FROM mz_introspection.mz_arrangement_heap_size_raw
661 UNION ALL
662 SELECT operator_id FROM mz_introspection.mz_arrangement_batcher_size_raw
663 ) AS rs ON rs.operator_id = od.operator_id
664 WHERE ce.export_id NOT LIKE 't%'
665 GROUP BY ce.export_id
666 )",
667 },
668];