Skip to main content

mz_adapter/coord/
introspection.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Support for unified compute introspection.
11//!
12//! Unified compute introspection is the process of collecting introspection data exported by
13//! individual replicas through their logging indexes and then writing that data, tagged with the
14//! respective replica ID, to "unified" storage collections. These storage collections then allow
15//! querying introspection data across all replicas and regardless of the health of individual
16//! replicas.
17//!
18//! # Lifecycle of Introspection Subscribes
19//!
20//! * After a new replica was created, the coordinator calls `install_introspection_subscribes` to
21//!   install all defined introspection subscribes on the new replica.
22//! * The coordinator calls `handle_introspection_subscribe_batch` for each response it receives
23//!   from an introspection subscribe, to write received updates to their corresponding
24//!   storage-managed collection.
25//! * Before a replica is dropped, the coordinator calls `drop_introspection_subscribes` to drop
26//!   all introspection subscribes previously installed on the replica.
27//! * When a replica disconnects without being dropped (e.g. because of a crash or network
28//!   failure), `handle_introspection_subscribe_batch` reacts on the corresponding error responses
29//!   by reinstalling the failed introspection subscribes.
30
31use 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// State tracked about an active introspection subscribe.
60#[derive(Derivative)]
61#[derivative(Debug)]
62pub(super) struct IntrospectionSubscribe {
63    /// The ID of the targeted cluster.
64    cluster_id: ClusterId,
65    /// The ID of the targeted replica.
66    replica_id: ReplicaId,
67    /// The spec from which this subscribe was created.
68    spec: &'static SubscribeSpec,
69    /// A storage write to be applied the next time the introspection subscribe produces any
70    /// output.
71    ///
72    /// This mechanism exists to delay the deletion of previous subscribe results from the target
73    /// storage collection when an introspection subscribe is reinstalled. After reinstallation it
74    /// can take a while for the new subscribe dataflow to produce its snapshot and keeping the old
75    /// introspection data around in the meantime makes for a better UX than removing it.
76    #[derivative(Debug = "ignore")]
77    deferred_write: Option<StorageWriteOp>,
78    /// When this subscribe first appended data to the target storage collection, if it has.
79    ///
80    /// Until then, the target collection may still contain rows written by a previous incarnation
81    /// of this subscribe (before an environmentd restart, or before the target replica
82    /// reconnected). Consumers that must not observe such stale rows, like the
83    /// `mz_object_arrangement_size_history` snapshots, use this to judge per-replica freshness.
84    first_data_at: Option<Instant>,
85}
86
87impl IntrospectionSubscribe {
88    /// Returns a `StorageWriteOp` that instructs the deletion of all data previously written by
89    /// this subscribe.
90    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    /// Every `(cluster, replica)` pair currently in the catalog.
102    ///
103    /// The set a per-replica feature (introspection subscribes, curated metric sinks) must install
104    /// onto the replicas that already exist when the coordinator starts. Shared so those callers
105    /// cannot drift on what "all replicas" means.
106    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    /// Installs introspection subscribes on all existing replicas.
118    ///
119    /// Meant to be invoked during coordinator bootstrapping.
120    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    /// Installs introspection subscribes on the given replica.
128    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        // Sequencing is performed asynchronously, and the target replica may be dropped before it
159        // completes. To ensure the subscribe does not leak in this case, we need to already add it
160        // to the coordinator state here, rather than at the end of sequencing.
161        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                    // MIR ⇒ MIR optimization (global)
249                    let global_mir_plan = optimizer.catch_unwind_optimize(plan)?;
250                    // Add introduced indexes as validity dependencies.
251                    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        // Timestamp selection.
283        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                    // MIR ⇒ LIR lowering and LIR ⇒ LIR optimization (global)
295                    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        // The subscribe may already have been dropped, in which case we must not install a
326        // dataflow for it.
327        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    /// Drops the introspection subscribes installed on the given replica.
347    ///
348    /// Dropping an introspection subscribe entails:
349    ///  * removing it from [`Coordinator::introspection_subscribes`]
350    ///  * dropping its compute collection
351    ///  * retracting any rows previously omitted by it from its corresponding storage-managed
352    ///    collection
353    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        // This can fail if the sequencing hasn't finished yet for the subscribe. In this case,
380        // `sequence_introspection_subscribe_finish` will skip installing the compute collection in
381        // the first place.
382        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        // Note that we don't simply call `drop_introspection_subscribe` here because that would
400        // cause an immediate deletion of all data previously reported by the subscribe from its
401        // target storage collection. We'd like to not present empty introspection data while the
402        // replica reconnects, so we want to delay the `StorageWriteOp::Delete` until then.
403
404        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        // Ensure that the contents of the target storage collection are cleaned when the new
431        // subscribe starts reporting data.
432        subscribe.deferred_write = Some(subscribe.delete_write_op());
433        // Until then, the collection serves the previous subscribe's data, which the replica may
434        // have invalidated by restarting.
435        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    /// Processes a batch returned by an introspection subscribe.
443    ///
444    /// Depending on the contents of the batch, this either appends received updates to the
445    /// corresponding storage-managed collection, or reinstalls a disconnected subscribe.
446    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                // The target replica disconnected, reinstall the subscribe.
461                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        // Prepend the `replica_id` to each row.
474        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 we have a pending deferred write, we need to apply it _before_ the append of the new
487        // rows.
488        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    /// Invalidates introspection-subscribe freshness for the given replica.
505    ///
506    /// Called when a cluster event reports one of the replica's processes offline or restarted.
507    /// The target collections may keep serving data written for the previous incarnation of the
508    /// replica, and the subscribe failures that will reinstall them can arrive after further
509    /// consumers of `fresh_introspection_replicas` have run. Freshness returns once a subscribe
510    /// delivers data again.
511    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    /// Returns the IDs of replicas whose introspection subscribe of the given type first
520    /// delivered data at least `margin` ago.
521    ///
522    /// Rows for other replicas in the corresponding storage collection are not trustworthy: they
523    /// either predate this environmentd process or were written before the replica reconnected,
524    /// and may describe a previous incarnation of the replica. The margin accounts for the
525    /// subscribe's first append becoming visible to readers only asynchronously (the collection
526    /// manager flushes writes in batches, and snapshot reads use an oracle timestamp that trails
527    /// the wall clock).
528    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/// The specification for an introspection subscribe.
577#[derive(Debug)]
578pub(super) struct SubscribeSpec {
579    /// An [`IntrospectionType`] identifying the storage-managed collection to which updates
580    /// received from subscribes instantiated from this spec are written.
581    introspection_type: IntrospectionType,
582    /// The SQL definition of the subscribe.
583    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    // Per-object arrangement sizes, one row per `(object_id, replica)`,
635    // populating `mz_object_arrangement_sizes`.
636    //
637    // `mz_arrangement_heap_size_raw` and `mz_arrangement_batcher_size_raw` are
638    // differential logs where each `+1` row represents one byte of heap delta;
639    // after consolidation, `COUNT(*)` is the current arrangement size in bytes.
640    //
641    // Sizes are quantized to the nearest 10 MiB: the heap-size collection
642    // wiggles by a few bytes per second from ordinary allocator activity, and
643    // emitting exact bytes would push a downstream update on every wiggle.
644    // Arrangements below 5 MiB quantize to a size of 0. They are kept rather
645    // than dropped so that every arranged object is present in the collection.
646    //
647    // `mz_dataflow_addresses.address[1]` is the root of each operator's address
648    // tree, which equals the owning `dataflow_id` — so we can go addresses →
649    // operator → dataflow without joining `mz_dataflow_operator_dataflows`.
650    //
651    // Joining on `ce.dataflow_id` assumes one dataflow exports a single object;
652    // if that stops holding, the same arrangement bytes would be attributed
653    // to multiple `export_id`s and we'd need to revisit the granularity.
654    //
655    // Transient export IDs (`t*`) are ephemeral dataflows (peeks, subscribes,
656    // including this one); we drop them to avoid self-feedback churn.
657    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];