Skip to main content

mz_adapter/coord/
message_handler.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//! Logic for processing [`Coordinator`] messages. The [`Coordinator`] receives
11//! messages from various sources (ex: controller, clients, background tasks, etc).
12
13use std::collections::{BTreeMap, BTreeSet, btree_map};
14use std::sync::Arc;
15use std::time::{Duration, Instant};
16
17use futures::FutureExt;
18use maplit::btreemap;
19use mz_audit_log::VersionedStorageUsage;
20use mz_catalog::memory::objects::ClusterReplicaProcessStatus;
21use mz_controller::ControllerResponse;
22use mz_controller::clusters::{ClusterEvent, ClusterStatus};
23use mz_ore::cast::CastFrom;
24use mz_ore::instrument;
25use mz_ore::now::EpochMillis;
26use mz_ore::option::OptionExt;
27use mz_ore::tracing::OpenTelemetryContext;
28use mz_ore::{soft_assert_or_log, task};
29use mz_persist_client::usage::ShardsUsageReferenced;
30use mz_repr::{Datum, Diff, Row};
31use mz_sql::ast::Statement;
32use mz_sql::names::ResolvedIds;
33use mz_sql::pure::PurifiedStatement;
34use mz_storage_client::controller::IntrospectionType;
35use opentelemetry::trace::TraceContextExt;
36use rand::{Rng, SeedableRng, rngs};
37use serde_json::json;
38use tracing::{Instrument, Level, event, info_span, warn};
39use tracing_opentelemetry::OpenTelemetrySpanExt;
40
41use crate::active_compute_sink::{ActiveComputeSink, ActiveComputeSinkRetireReason};
42use crate::catalog::BuiltinTableUpdate;
43use crate::command::Command;
44use crate::coord::{
45    AlterConnectionValidationReady, ClusterReplicaStatuses, Coordinator,
46    CreateConnectionValidationReady, Message, PurifiedStatementReady, WatchSetResponse,
47};
48use crate::telemetry::{EventDetails, SegmentClientExt};
49use crate::{AdapterNotice, TimestampContext};
50
51impl Coordinator {
52    /// BOXED FUTURE: As of Nov 2023 the returned Future from this function was 74KB. This would
53    /// get stored on the stack which is bad for runtime performance, and blow up our stack usage.
54    /// Because of that we purposefully move Futures of inner function calls onto the heap
55    /// (i.e. Box it).
56    #[instrument]
57    pub(crate) async fn handle_message(&mut self, msg: Message) -> () {
58        match msg {
59            Message::Command(otel_ctx, cmd) => {
60                // TODO: We need a Span that is not none for the otel_ctx to attach the parent
61                // relationship to. If we swap the otel_ctx in `Command::Message` for a Span, we
62                // can downgrade this to a debug_span.
63                let span = tracing::info_span!("message_command").or_current();
64                span.in_scope(|| otel_ctx.attach_as_parent());
65                self.message_command(cmd).instrument(span).await
66            }
67            Message::ControllerReady { controller: _ } => {
68                let Coordinator {
69                    controller,
70                    catalog,
71                    ..
72                } = self;
73                let storage_metadata = catalog.state().storage_metadata();
74                if let Some(m) = controller
75                    .process(storage_metadata)
76                    .expect("`process` never returns an error")
77                {
78                    self.message_controller(m).boxed_local().await
79                }
80            }
81            Message::PurifiedStatementReady(ready) => {
82                self.message_purified_statement_ready(ready)
83                    .boxed_local()
84                    .await
85            }
86            Message::CreateConnectionValidationReady(ready) => {
87                self.message_create_connection_validation_ready(ready)
88                    .boxed_local()
89                    .await
90            }
91            Message::AlterConnectionValidationReady(ready) => {
92                self.message_alter_connection_validation_ready(ready)
93                    .boxed_local()
94                    .await
95            }
96            Message::TryDeferred {
97                conn_id,
98                acquired_lock,
99            } => self.try_deferred(conn_id, acquired_lock).await,
100            Message::GroupCommitInitiate(span, permit) => {
101                // Add an OpenTelemetry link to our current span.
102                tracing::Span::current().add_link(span.context().span().span_context().clone());
103                self.try_group_commit(permit)
104                    .instrument(span)
105                    .boxed_local()
106                    .await
107            }
108            Message::AdvanceTimelines => {
109                self.advance_timelines().boxed_local().await;
110            }
111            Message::ClusterEvent(event) => self.message_cluster_event(event).boxed_local().await,
112            Message::CancelPendingPeeks { conn_id } => {
113                self.cancel_pending_peeks(&conn_id);
114            }
115            Message::LinearizeReads => {
116                self.message_linearize_reads().boxed_local().await;
117            }
118            Message::StagedBatches {
119                conn_id,
120                table_id,
121                batches,
122            } => {
123                self.commit_staged_batches(conn_id, table_id, batches);
124            }
125            Message::StorageUsageSchedule => {
126                self.schedule_storage_usage_collection().boxed_local().await;
127            }
128            Message::StorageUsageFetch => {
129                self.storage_usage_fetch().boxed_local().await;
130            }
131            Message::StorageUsageUpdate(sizes) => {
132                self.storage_usage_update(sizes).boxed_local().await;
133            }
134            Message::StorageUsagePrune(expired) => {
135                self.storage_usage_prune(expired).boxed_local().await;
136            }
137            Message::ArrangementSizesSchedule => {
138                self.schedule_arrangement_sizes_collection()
139                    .boxed_local()
140                    .await;
141            }
142            Message::ArrangementSizesSnapshot => {
143                self.arrangement_sizes_snapshot().boxed_local().await;
144            }
145            Message::ArrangementSizesPrune(expired) => {
146                self.arrangement_sizes_prune(expired).boxed_local().await;
147            }
148            Message::RetireExecute {
149                otel_ctx,
150                data,
151                reason,
152            } => {
153                otel_ctx.attach_as_parent();
154                self.retire_execution(reason, data);
155            }
156            Message::ExecuteSingleStatementTransaction {
157                ctx,
158                otel_ctx,
159                stmt,
160                params,
161            } => {
162                otel_ctx.attach_as_parent();
163                self.sequence_execute_single_statement_transaction(ctx, stmt, params)
164                    .boxed_local()
165                    .await;
166            }
167            Message::PeekStageReady { ctx, span, stage } => {
168                self.sequence_staged(ctx, span, stage).boxed_local().await;
169            }
170            Message::CreateIndexStageReady { ctx, span, stage } => {
171                self.sequence_staged(ctx, span, stage).boxed_local().await;
172            }
173            Message::CreateViewStageReady { ctx, span, stage } => {
174                self.sequence_staged(ctx, span, stage).boxed_local().await;
175            }
176            Message::CreateMaterializedViewStageReady { ctx, span, stage } => {
177                self.sequence_staged(ctx, span, stage).boxed_local().await;
178            }
179            Message::SubscribeStageReady { ctx, span, stage } => {
180                self.sequence_staged(ctx, span, stage).boxed_local().await;
181            }
182            Message::IntrospectionSubscribeStageReady { span, stage } => {
183                self.sequence_staged((), span, stage).boxed_local().await;
184            }
185            Message::ExplainTimestampStageReady { ctx, span, stage } => {
186                self.sequence_staged(ctx, span, stage).boxed_local().await;
187            }
188            Message::SecretStageReady { ctx, span, stage } => {
189                self.sequence_staged(ctx, span, stage).boxed_local().await;
190            }
191            Message::ClusterStageReady { ctx, span, stage } => {
192                self.sequence_staged(ctx, span, stage).boxed_local().await;
193            }
194            Message::DrainStatementLog => {
195                self.drain_statement_log();
196            }
197            Message::PrivateLinkVpcEndpointEvents(events) => {
198                if !self.controller.read_only() {
199                    self.controller.storage.append_introspection_updates(
200                        IntrospectionType::PrivatelinkConnectionStatusHistory,
201                        events
202                            .into_iter()
203                            .map(|e| (mz_repr::Row::from(e), Diff::ONE))
204                            .collect(),
205                    );
206                }
207            }
208            Message::CheckSchedulingPolicies => {
209                self.check_scheduling_policies().boxed_local().await;
210            }
211            Message::SchedulingDecisions(decisions) => {
212                self.handle_scheduling_decisions(decisions)
213                    .boxed_local()
214                    .await;
215            }
216            Message::ClusterControllerRequest(request) => {
217                self.handle_cluster_controller_request(request)
218                    .boxed_local()
219                    .await;
220            }
221            Message::DeferredStatementReady => {
222                self.handle_deferred_statement().boxed_local().await;
223            }
224        }
225    }
226
227    #[mz_ore::instrument(level = "debug")]
228    pub async fn storage_usage_fetch(&self) {
229        // In read-only mode (e.g. a standby coordinator during a zero-downtime
230        // deployment) we cannot durably write the per-batch allocator bump or
231        // append to `mz_storage_usage_by_shard`, and we also don't want to do
232        // the slow shard scan on a process that isn't going to record the
233        // results. Skip the whole cycle and reschedule so we resume
234        // automatically once the coordinator transitions out of read-only.
235        if self.controller.read_only() {
236            tracing::info!("skipping storage usage collection in read-only mode");
237            if let Err(e) = self.internal_cmd_tx.send(Message::StorageUsageSchedule) {
238                warn!("internal_cmd_rx dropped before we could send: {:?}", e);
239            }
240            return;
241        }
242
243        let internal_cmd_tx = self.internal_cmd_tx.clone();
244        let client = self.storage_usage_client.clone();
245
246        // Record the currently live shards.
247        let live_shards: BTreeSet<_> = self
248            .controller
249            .storage
250            .active_collection_metadatas()
251            .into_iter()
252            .map(|(_id, m)| m.data_shard)
253            .collect();
254
255        let collection_metric = self.metrics.storage_usage_collection_time_seconds.clone();
256
257        // Spawn an asynchronous task to compute the storage usage, which
258        // requires a slow scan of the underlying storage engine.
259        task::spawn(|| "storage_usage_fetch", async move {
260            let collection_metric_timer = collection_metric.start_timer();
261            let shard_sizes = client.shards_usage_referenced(live_shards).await;
262            collection_metric_timer.observe_duration();
263
264            // It is not an error for shard sizes to become ready after
265            // `internal_cmd_rx` is dropped.
266            if let Err(e) = internal_cmd_tx.send(Message::StorageUsageUpdate(shard_sizes)) {
267                warn!("internal_cmd_rx dropped before we could send: {:?}", e);
268            }
269        });
270    }
271
272    #[mz_ore::instrument(level = "debug")]
273    async fn storage_usage_update(&mut self, shards_usage: ShardsUsageReferenced) {
274        // Similar to audit events, use the oracle ts so this is guaranteed to
275        // increase. This is intentionally the timestamp of when collection
276        // finished, not when it started, so that we don't write data with a
277        // timestamp in the past.
278        //
279        // `storage_usage_fetch` skips this path in read-only mode, so we can
280        // unconditionally bump the oracle write ts here.
281        let write_ts = self.get_local_write_ts().await.timestamp;
282        let collection_timestamp: EpochMillis = write_ts.into();
283
284        // All rows in this collection cycle share `batch_id` so consumers can
285        // identify rows that were collected together. We use one durable
286        // allocator bump per cycle (rather than per shard) so the id is
287        // monotonic across coordinator restarts while still keeping the
288        // coord-blocking cost proportional to one round-trip, not N.
289        let batch_id = match self.catalog().allocate_storage_usage_id(write_ts).await {
290            Ok(id) => id,
291            Err(err) => {
292                tracing::warn!("failed to allocate storage usage batch id: {:?}", err);
293                return;
294            }
295        };
296
297        let updates: Vec<_> = shards_usage
298            .by_shard
299            .into_iter()
300            .map(|(shard_id, shard_usage)| {
301                let event = VersionedStorageUsage::new(
302                    batch_id,
303                    Some(shard_id.to_string()),
304                    shard_usage.size_bytes(),
305                    collection_timestamp,
306                );
307                self.catalog().pack_storage_usage_update(event, Diff::ONE)
308            })
309            .collect();
310
311        let (table_updates, _) = self.builtin_table_update().execute(updates).await;
312
313        let internal_cmd_tx = self.internal_cmd_tx.clone();
314        let task_span = info_span!(parent: None, "coord::storage_usage_update::table_updates");
315        OpenTelemetryContext::obtain().attach_as_parent_to(&task_span);
316        task::spawn(|| "storage_usage_update_table_updates", async move {
317            table_updates.instrument(task_span).await;
318            // It is not an error for this task to be running after `internal_cmd_rx` is dropped.
319            if let Err(e) = internal_cmd_tx.send(Message::StorageUsageSchedule) {
320                warn!("internal_cmd_rx dropped before we could send: {e:?}");
321            }
322        });
323    }
324
325    #[mz_ore::instrument(level = "debug")]
326    async fn storage_usage_prune(&mut self, expired: Vec<BuiltinTableUpdate>) {
327        let (fut, _) = self.builtin_table_update().execute(expired).await;
328        task::spawn(|| "storage_usage_pruning_apply", async move {
329            fut.await;
330        });
331    }
332
333    pub async fn schedule_storage_usage_collection(&self) {
334        // Instead of using an `tokio::timer::Interval`, we calculate the time until the next
335        // usage collection and wait for that amount of time. This is so we can keep the intervals
336        // consistent even across restarts. If collection takes too long, it is possible that
337        // we miss an interval.
338
339        // 1) Deterministically pick some offset within the collection interval to prevent
340        // thundering herds across environments.
341        const SEED_LEN: usize = 32;
342        let mut seed = [0; SEED_LEN];
343        for (i, byte) in self
344            .catalog()
345            .state()
346            .config()
347            .environment_id
348            .organization_id()
349            .as_bytes()
350            .into_iter()
351            .take(SEED_LEN)
352            .enumerate()
353        {
354            seed[i] = *byte;
355        }
356        let storage_usage_collection_interval_ms: EpochMillis =
357            EpochMillis::try_from(self.storage_usage_collection_interval.as_millis())
358                .expect("storage usage collection interval must fit into u64");
359        let offset =
360            rngs::SmallRng::from_seed(seed).random_range(0..storage_usage_collection_interval_ms);
361        let now_ts: EpochMillis = self.peek_local_write_ts().await.into();
362
363        // 2) Determine the amount of ms between now and the next collection time.
364        let previous_collection_ts =
365            (now_ts - (now_ts % storage_usage_collection_interval_ms)) + offset;
366        let next_collection_ts = if previous_collection_ts > now_ts {
367            previous_collection_ts
368        } else {
369            previous_collection_ts + storage_usage_collection_interval_ms
370        };
371        let next_collection_interval = Duration::from_millis(next_collection_ts - now_ts);
372
373        // 3) Sleep for that amount of time, then initiate another storage usage collection.
374        let internal_cmd_tx = self.internal_cmd_tx.clone();
375        task::spawn(|| "storage_usage_collection", async move {
376            tokio::time::sleep(next_collection_interval).await;
377            if internal_cmd_tx.send(Message::StorageUsageFetch).is_err() {
378                // If sending fails, the main thread has shutdown.
379            }
380        });
381    }
382
383    /// Schedules the next per-object arrangement sizes snapshot.
384    ///
385    /// Aligns each fire to an `organization_id`-seeded offset within the
386    /// interval so collections stay consistent across restarts and don't
387    /// synchronize across environments. Sleeps are capped at `MAX_SLEEP`,
388    /// so dyncfg changes (interval edits or the `0s` disable sentinel) take
389    /// effect within one cap rather than after the full interval.
390    pub async fn schedule_arrangement_sizes_collection(&self) {
391        const MAX_SLEEP: Duration = Duration::from_secs(60);
392
393        let interval_duration =
394            mz_adapter_types::dyncfgs::ARRANGEMENT_SIZE_HISTORY_COLLECTION_INTERVAL
395                .get(self.catalog().system_config().dyncfgs());
396
397        // `0s` disables collection. Keep polling so re-enabling takes effect
398        // within `MAX_SLEEP` rather than requiring an envd restart.
399        if interval_duration.is_zero() {
400            let internal_cmd_tx = self.internal_cmd_tx.clone();
401            task::spawn(|| "arrangement_sizes_collection_disabled", async move {
402                tokio::time::sleep(MAX_SLEEP).await;
403                let _ = internal_cmd_tx.send(Message::ArrangementSizesSchedule);
404            });
405            return;
406        }
407
408        const SEED_LEN: usize = 32;
409        let mut seed = [0; SEED_LEN];
410        for (i, byte) in self
411            .catalog()
412            .state()
413            .config()
414            .environment_id
415            .organization_id()
416            .as_bytes()
417            .into_iter()
418            .take(SEED_LEN)
419            .enumerate()
420        {
421            seed[i] = *byte;
422        }
423        let interval_ms: EpochMillis = EpochMillis::try_from(interval_duration.as_millis())
424            .expect("arrangement_size_history_collection_interval must fit into u64");
425        // `rand::random_range` panics on an empty range.
426        let interval_ms = interval_ms.max(1);
427        let offset = rngs::SmallRng::from_seed(seed).random_range(0..interval_ms);
428        let now_ts: EpochMillis = self.peek_local_write_ts().await.into();
429
430        let previous_collection_ts = (now_ts - (now_ts % interval_ms)) + offset;
431        let next_collection_ts = if previous_collection_ts > now_ts {
432            previous_collection_ts
433        } else {
434            previous_collection_ts + interval_ms
435        };
436        let sleep_for = Duration::from_millis(next_collection_ts - now_ts);
437
438        // Within one cap of the next fire we sleep the remainder and snapshot;
439        // further out we sleep the cap and re-enter so a dyncfg change is
440        // picked up before committing to a long sleep.
441        let (capped_sleep, fire_snapshot) = if sleep_for <= MAX_SLEEP {
442            (sleep_for, true)
443        } else {
444            (MAX_SLEEP, false)
445        };
446
447        let internal_cmd_tx = self.internal_cmd_tx.clone();
448        task::spawn(|| "arrangement_sizes_collection", async move {
449            tokio::time::sleep(capped_sleep).await;
450            let msg = if fire_snapshot {
451                Message::ArrangementSizesSnapshot
452            } else {
453                Message::ArrangementSizesSchedule
454            };
455            // Send is best-effort: if the coordinator is shutting down, drop.
456            let _ = internal_cmd_tx.send(msg);
457        });
458    }
459
460    /// Snapshots the current contents of `mz_object_arrangement_sizes` and
461    /// appends them to `mz_object_arrangement_size_history`, tagged with a
462    /// shared `collection_timestamp`. Reschedules on completion.
463    ///
464    /// Each `(replica_id, object_id)` pair is recorded with a
465    /// `hydration_complete` flag derived from `mz_compute_hydration_times`:
466    /// `true` once the pair's initial hydration on that replica is finished,
467    /// `false` while still building. Consumers that want only stable sizes
468    /// should filter `WHERE hydration_complete`.
469    #[mz_ore::instrument(level = "debug")]
470    async fn arrangement_sizes_snapshot(&mut self) {
471        // The catalog server is not writable in read-only mode.
472        if self.controller.read_only() {
473            self.schedule_arrangement_sizes_collection().await;
474            return;
475        }
476
477        let collection_timer = self
478            .metrics
479            .arrangement_sizes_collection_time_seconds
480            .start_timer();
481
482        let live_item_id = self.catalog().resolve_builtin_storage_collection(
483            &mz_catalog::builtin::MZ_OBJECT_ARRANGEMENT_SIZES_UNIFIED,
484        );
485        let live_global_id = self.catalog.get_entry(&live_item_id).latest_global_id();
486        let hydration_item_id = self
487            .catalog()
488            .resolve_builtin_storage_collection(&mz_catalog::builtin::MZ_COMPUTE_HYDRATION_TIMES);
489        let hydration_global_id = self
490            .catalog
491            .get_entry(&hydration_item_id)
492            .latest_global_id();
493        let history_item_id = self
494            .catalog()
495            .resolve_builtin_table(&mz_catalog::builtin::MZ_OBJECT_ARRANGEMENT_SIZE_HISTORY);
496
497        let read_ts = self.get_local_read_ts().await;
498        let snapshot = match self
499            .controller
500            .storage_collections
501            .snapshot(live_global_id, read_ts)
502            .await
503        {
504            Ok(s) => s,
505            Err(e) => {
506                tracing::warn!("arrangement sizes snapshot failed: {e:?}");
507                drop(collection_timer);
508                self.schedule_arrangement_sizes_collection().await;
509                return;
510            }
511        };
512        let mut hydration_snapshot = match self
513            .controller
514            .storage_collections
515            .snapshot(hydration_global_id, read_ts)
516            .await
517        {
518            Ok(s) => s,
519            Err(e) => {
520                tracing::warn!("arrangement sizes hydration snapshot failed: {e:?}");
521                drop(collection_timer);
522                self.schedule_arrangement_sizes_collection().await;
523                return;
524            }
525        };
526        differential_dataflow::consolidation::consolidate(&mut hydration_snapshot);
527
528        // Build the set of pairs whose initial hydration has finished
529        // (`time_ns IS NOT NULL`). The set drives the `hydration_complete`
530        // flag for each row we emit below.
531        let mut datum_vec = mz_repr::DatumVec::new();
532        let mut hydrated: BTreeSet<(String, String)> = BTreeSet::new();
533        const HYDRATION_COL_REPLICA_ID: usize = 0;
534        const HYDRATION_COL_OBJECT_ID: usize = 1;
535        const HYDRATION_COL_TIME_NS: usize = 2;
536        const HYDRATION_COL_COUNT: usize = 3;
537        for (row, diff) in &hydration_snapshot {
538            if *diff != 1 {
539                continue;
540            }
541            let datums = datum_vec.borrow_with(row);
542            if datums.len() < HYDRATION_COL_COUNT {
543                continue;
544            }
545            if datums[HYDRATION_COL_TIME_NS].is_null() {
546                continue;
547            }
548            hydrated.insert((
549                datums[HYDRATION_COL_REPLICA_ID].unwrap_str().to_string(),
550                datums[HYDRATION_COL_OBJECT_ID].unwrap_str().to_string(),
551            ));
552        }
553
554        // `collection_ts` is stamped after the snapshot so it's always >= the
555        // state the rows describe, and monotone across restarts. The snapshot
556        // read and this stamp aren't atomic, but the resulting skew is bounded
557        // by snapshot latency and negligible at this cadence.
558        let collection_ts: EpochMillis = self.get_local_write_ts().await.timestamp.into();
559        let collection_datum = Datum::TimestampTz(
560            mz_ore::now::to_datetime(collection_ts)
561                .try_into()
562                .expect("collection_timestamp must fit into TimestampTz"),
563        );
564
565        let mut consolidated = snapshot;
566        differential_dataflow::consolidation::consolidate(&mut consolidated);
567
568        // Column positions in `mz_object_arrangement_sizes`.
569        const LIVE_COL_REPLICA_ID: usize = 0;
570        const LIVE_COL_OBJECT_ID: usize = 1;
571        const LIVE_COL_SIZE: usize = 2;
572        const LIVE_COL_COUNT: usize = 3;
573
574        let mut skipped_malformed: u64 = 0;
575        let mut skipped_null_size: u64 = 0;
576        let mut updates: Vec<BuiltinTableUpdate> = Vec::with_capacity(consolidated.len());
577        for (row, diff) in consolidated.iter() {
578            if *diff != 1 {
579                continue;
580            }
581            let datums = datum_vec.borrow_with(row);
582            // Surface schema drift via a warn log below rather than silently
583            // skipping entire snapshots.
584            if datums.len() != LIVE_COL_COUNT {
585                skipped_malformed += 1;
586                continue;
587            }
588            let replica_id = datums[LIVE_COL_REPLICA_ID].unwrap_str();
589            let object_id = datums[LIVE_COL_OBJECT_ID].unwrap_str();
590            let size_datum = datums[LIVE_COL_SIZE];
591            // The history table's `size` is non-null; fabricating zero would
592            // be misleading, so drop.
593            if size_datum.is_null() {
594                skipped_null_size += 1;
595                continue;
596            }
597            let size = size_datum.unwrap_int64();
598            // Pairs whose hydration hasn't completed yet are still recorded,
599            // tagged with `hydration_complete = false`. Consumers that care
600            // only about stable sizes can filter on `hydration_complete`.
601            let hydration_complete =
602                hydrated.contains(&(replica_id.to_string(), object_id.to_string()));
603            let new_row = Row::pack_slice(&[
604                Datum::String(replica_id),
605                Datum::String(object_id),
606                Datum::Int64(size),
607                collection_datum,
608                Datum::from(hydration_complete),
609            ]);
610            updates.push(BuiltinTableUpdate::row(history_item_id, new_row, Diff::ONE));
611        }
612        if skipped_malformed > 0 {
613            warn!(
614                "mz_object_arrangement_sizes schema drift: skipped {skipped_malformed} rows \
615                 with unexpected arity"
616            );
617        }
618        if skipped_null_size > 0 {
619            tracing::debug!("skipped {skipped_null_size} live rows with null size");
620        }
621
622        let row_count = updates.len();
623        // Captures snapshot + row construction. The async table-apply below
624        // is captured separately by `mz_append_table_duration_seconds`.
625        collection_timer.observe_duration();
626
627        if !updates.is_empty() {
628            self.metrics
629                .arrangement_sizes_rows_written
630                .inc_by(u64::cast_from(row_count));
631            // TODO(arrangement-sizes): when the writeable-catalog-server plumbing
632            // in https://github.com/MaterializeInc/materialize/pull/35436 lands,
633            // append directly on `mz_catalog_server` instead of going through
634            // the environmentd builtin-table-update path.
635            let (fut, _) = self.builtin_table_update().execute(updates).await;
636            let internal_cmd_tx = self.internal_cmd_tx.clone();
637            let task_span =
638                info_span!(parent: None, "coord::arrangement_sizes_snapshot::table_updates");
639            OpenTelemetryContext::obtain().attach_as_parent_to(&task_span);
640            task::spawn(|| "arrangement_sizes_snapshot_apply", async move {
641                fut.instrument(task_span).await;
642                if let Err(e) = internal_cmd_tx.send(Message::ArrangementSizesSchedule) {
643                    warn!("internal_cmd_rx dropped before we could send: {e:?}");
644                }
645            });
646        } else {
647            self.schedule_arrangement_sizes_collection().await;
648        }
649
650        tracing::debug!(
651            "appended {row_count} rows to mz_object_arrangement_size_history at ts {collection_ts}"
652        );
653    }
654
655    #[mz_ore::instrument(level = "debug")]
656    async fn arrangement_sizes_prune(&mut self, expired: Vec<BuiltinTableUpdate>) {
657        let (fut, _) = self.builtin_table_update().execute(expired).await;
658        task::spawn(|| "arrangement_sizes_pruning_apply", async move {
659            fut.await;
660        });
661    }
662
663    #[mz_ore::instrument(level = "debug")]
664    async fn message_command(&mut self, cmd: Command) {
665        self.handle_command(cmd).await;
666    }
667
668    #[mz_ore::instrument(level = "debug")]
669    async fn message_controller(&mut self, message: ControllerResponse) {
670        event!(Level::TRACE, message = format!("{:?}", message));
671        match message {
672            ControllerResponse::PeekNotification(uuid, response, otel_ctx) => {
673                self.handle_peek_notification(uuid, response, otel_ctx);
674            }
675            ControllerResponse::SubscribeResponse(sink_id, response) => {
676                if let Some(ActiveComputeSink::Subscribe(active_subscribe)) =
677                    self.active_compute_sinks.get_mut(&sink_id)
678                {
679                    let finished = active_subscribe.process_response(response);
680                    if finished {
681                        let retire_notify = self
682                            .retire_compute_sinks(btreemap! {
683                                sink_id => ActiveComputeSinkRetireReason::Finished,
684                            })
685                            .await;
686                        // `retire_compute_sinks` waits before sending the terminal
687                        // SUBSCRIBE response. There is no separate statement response here.
688                        drop(retire_notify);
689                    }
690
691                    soft_assert_or_log!(
692                        !self.introspection_subscribes.contains_key(&sink_id),
693                        "`sink_id` {sink_id} unexpectedly found in both `active_subscribes` \
694                         and `introspection_subscribes`",
695                    );
696                } else if self.introspection_subscribes.contains_key(&sink_id) {
697                    self.handle_introspection_subscribe_batch(sink_id, response)
698                        .await;
699                } else {
700                    // Cancellation may cause us to receive responses for subscribes no longer
701                    // tracked, so we quietly ignore them.
702                }
703            }
704            ControllerResponse::CopyToResponse(sink_id, response) => {
705                match self.drop_compute_sink(sink_id).await {
706                    Some((ActiveComputeSink::CopyTo(active_copy_to), _write_notify)) => {
707                        active_copy_to.retire_with_response(response);
708                    }
709                    _ => {
710                        // Cancellation may cause us to receive responses for subscribes no longer
711                        // tracked, so we quietly ignore them.
712                    }
713                }
714            }
715            ControllerResponse::WatchSetFinished(ws_ids) => {
716                let now = self.now();
717                for ws_id in ws_ids {
718                    let Some((conn_id, rsp)) = self.installed_watch_sets.remove(&ws_id) else {
719                        continue;
720                    };
721                    self.connection_watch_sets
722                        .get_mut(&conn_id)
723                        .expect("corrupted coordinator state: unknown connection id")
724                        .remove(&ws_id);
725                    if self.connection_watch_sets[&conn_id].is_empty() {
726                        self.connection_watch_sets.remove(&conn_id);
727                    }
728
729                    match rsp {
730                        WatchSetResponse::StatementDependenciesReady(id, ev) => {
731                            self.record_statement_lifecycle_event(&id, &ev, now);
732                        }
733                        WatchSetResponse::AlterSinkReady(ctx) => {
734                            self.sequence_alter_sink_finish(ctx).await;
735                        }
736                        WatchSetResponse::AlterMaterializedViewReady(ctx) => {
737                            self.sequence_alter_materialized_view_apply_replacement_finish(ctx)
738                                .await;
739                        }
740                    }
741                }
742            }
743        }
744    }
745
746    #[mz_ore::instrument(level = "debug")]
747    async fn message_purified_statement_ready(
748        &mut self,
749        PurifiedStatementReady {
750            ctx,
751            result,
752            params,
753            mut plan_validity,
754            original_stmt,
755            otel_ctx,
756        }: PurifiedStatementReady,
757    ) {
758        otel_ctx.attach_as_parent();
759
760        // Ensure that all dependencies still exist after purification, as a
761        // `DROP CONNECTION` or other `DROP` may have sneaked in. If any have gone missing, we
762        // repurify the original statement. This will either produce a nice
763        // "unknown connector" error, or pick up a new connector that has
764        // replaced the dropped connector.
765        //
766        // n.b. an `ALTER CONNECTION` occurring during purification is OK
767        // because we always look up/populate a connection's state after
768        // committing to the catalog, so are guaranteed to see the connection's
769        // most recent version.
770        if plan_validity.check(self.catalog()).is_err() {
771            self.handle_execute_inner(original_stmt, params, ctx).await;
772            return;
773        }
774
775        let purified_statement = match result {
776            Ok(ok) => ok,
777            Err(e) => return ctx.retire(Err(e)),
778        };
779
780        let plan = match purified_statement {
781            PurifiedStatement::PurifiedCreateSource {
782                create_progress_subsource_stmt,
783                create_source_stmt,
784                subsources,
785                available_source_references,
786            } => self
787                .plan_purified_create_source(
788                    &ctx,
789                    params,
790                    create_progress_subsource_stmt,
791                    create_source_stmt,
792                    subsources,
793                    available_source_references,
794                )
795                .await
796                .map(|(plan, resolved_ids)| (plan, resolved_ids, ResolvedIds::empty())),
797            PurifiedStatement::PurifiedAlterSourceAddSubsources {
798                source_name,
799                options,
800                subsources,
801            } => self
802                .plan_purified_alter_source_add_subsource(
803                    ctx.session(),
804                    params,
805                    source_name,
806                    options,
807                    subsources,
808                )
809                .await
810                .map(|(plan, resolved_ids)| (plan, resolved_ids, ResolvedIds::empty())),
811            PurifiedStatement::PurifiedAlterSourceRefreshReferences {
812                source_name,
813                available_source_references,
814            } => self
815                .plan_purified_alter_source_refresh_references(
816                    ctx.session(),
817                    params,
818                    source_name,
819                    available_source_references,
820                )
821                .map(|(plan, resolved_ids)| (plan, resolved_ids, ResolvedIds::empty())),
822            o @ (PurifiedStatement::PurifiedAlterSource { .. }
823            | PurifiedStatement::PurifiedCreateSink(..)
824            | PurifiedStatement::PurifiedCreateTableFromSource { .. }) => {
825                // Unify these into a `Statement`.
826                let stmt = match o {
827                    PurifiedStatement::PurifiedAlterSource { alter_source_stmt } => {
828                        Statement::AlterSource(alter_source_stmt)
829                    }
830                    PurifiedStatement::PurifiedCreateTableFromSource { stmt } => {
831                        Statement::CreateTableFromSource(stmt)
832                    }
833                    PurifiedStatement::PurifiedCreateSink(stmt) => Statement::CreateSink(stmt),
834                    PurifiedStatement::PurifiedCreateSource { .. }
835                    | PurifiedStatement::PurifiedAlterSourceAddSubsources { .. }
836                    | PurifiedStatement::PurifiedAlterSourceRefreshReferences { .. } => {
837                        unreachable!("not part of exterior match stmt")
838                    }
839                };
840
841                // Determine all dependencies, not just those in the statement
842                // itself.
843                let catalog = self.catalog().for_session(ctx.session());
844                let resolved_ids = mz_sql::names::visit_dependencies(&catalog, &stmt);
845                self.plan_statement(ctx.session(), stmt, &params, &resolved_ids)
846                    .map(|(plan, sql_impl_ids)| (plan, resolved_ids, sql_impl_ids))
847            }
848        };
849
850        match plan {
851            Ok((plan, resolved_ids, sql_impl_ids)) => {
852                self.sequence_plan(ctx, plan, resolved_ids, sql_impl_ids)
853                    .await
854            }
855            Err(e) => ctx.retire(Err(e)),
856        }
857    }
858
859    #[mz_ore::instrument(level = "debug")]
860    async fn message_create_connection_validation_ready(
861        &mut self,
862        CreateConnectionValidationReady {
863            mut ctx,
864            result,
865            connection_id,
866            connection_gid,
867            mut plan_validity,
868            otel_ctx,
869            resolved_ids,
870        }: CreateConnectionValidationReady,
871    ) {
872        otel_ctx.attach_as_parent();
873
874        // Ensure that all dependencies still exist after validation, as a
875        // `DROP SECRET` may have sneaked in.
876        //
877        // WARNING: If we support `ALTER SECRET`, we'll need to also check
878        // for connectors that were altered while we were purifying.
879        if let Err(e) = plan_validity.check(self.catalog()) {
880            if self.secrets_controller.delete(connection_id).await.is_ok() {
881                self.caching_secrets_reader.invalidate(connection_id);
882            }
883            return ctx.retire(Err(e));
884        }
885
886        let plan = match result {
887            Ok(ok) => ok,
888            Err(e) => {
889                if self.secrets_controller.delete(connection_id).await.is_ok() {
890                    self.caching_secrets_reader.invalidate(connection_id);
891                }
892                return ctx.retire(Err(e));
893            }
894        };
895
896        let result = self
897            .sequence_create_connection_stage_finish(
898                &mut ctx,
899                connection_id,
900                connection_gid,
901                plan,
902                resolved_ids,
903            )
904            .await;
905        ctx.retire(result);
906    }
907
908    #[mz_ore::instrument(level = "debug")]
909    async fn message_alter_connection_validation_ready(
910        &mut self,
911        AlterConnectionValidationReady {
912            mut ctx,
913            result,
914            connection_id,
915            connection_gid: _,
916            mut plan_validity,
917            otel_ctx,
918            resolved_ids: _,
919        }: AlterConnectionValidationReady,
920    ) {
921        otel_ctx.attach_as_parent();
922
923        // Ensure that all dependencies still exist after validation, as a
924        // `DROP SECRET` may have sneaked in.
925        //
926        // WARNING: If we support `ALTER SECRET`, we'll need to also check
927        // for connectors that were altered while we were purifying.
928        if let Err(e) = plan_validity.check(self.catalog()) {
929            return ctx.retire(Err(e));
930        }
931
932        let conn = match result {
933            Ok(ok) => ok,
934            Err(e) => {
935                return ctx.retire(Err(e));
936            }
937        };
938
939        let result = self
940            .sequence_alter_connection_stage_finish(ctx.session_mut(), connection_id, conn)
941            .await;
942        ctx.retire(result);
943    }
944
945    #[mz_ore::instrument(level = "debug")]
946    async fn message_cluster_event(&mut self, event: ClusterEvent) {
947        event!(Level::TRACE, event = format!("{:?}", event));
948
949        if let Some(segment_client) = &self.segment_client {
950            let env_id = &self.catalog().config().environment_id;
951            let mut properties = json!({
952                "cluster_id": event.cluster_id.to_string(),
953                "replica_id": event.replica_id.to_string(),
954                "process_id": event.process_id,
955                "status": event.status.as_kebab_case_str(),
956            });
957            match event.status {
958                ClusterStatus::Online => (),
959                ClusterStatus::Offline(reason) => {
960                    let properties = match &mut properties {
961                        serde_json::Value::Object(map) => map,
962                        _ => unreachable!(),
963                    };
964                    properties.insert(
965                        "reason".into(),
966                        json!(reason.display_or("unknown").to_string()),
967                    );
968                }
969            };
970            segment_client.environment_track(
971                env_id,
972                "Cluster Changed Status",
973                properties,
974                EventDetails {
975                    timestamp: Some(event.time),
976                    ..Default::default()
977                },
978            );
979        }
980
981        // It is possible that we receive a status update for a replica that has
982        // already been dropped from the catalog. Just ignore these events.
983        let Some(replica_statuses) = self
984            .cluster_replica_statuses
985            .try_get_cluster_replica_statuses(event.cluster_id, event.replica_id)
986        else {
987            return;
988        };
989
990        let old_process_status = &replica_statuses[&event.process_id];
991        let status_changed = event.status != old_process_status.status;
992        let restart_count_changed = event.restart_count != old_process_status.restart_count;
993
994        // We mirror the restart count in memory even when only it changes (and the
995        // status stays the same), so the 0dt caught-up check can detect replica
996        // restarts it would otherwise miss by only sampling the status. The status
997        // history and the status-changed notice are keyed on the status itself, so
998        // we only touch those when the status actually changes.
999        //
1000        // NOTE: The 0dt stability gate detects flaps by watching a process's
1001        // status-change `time` advance between checks. That only works because we
1002        // freeze `time` on no-op events, i.e. we return early here instead of
1003        // rewriting the record when neither the status nor the restart count
1004        // changed.
1005        if !status_changed && !restart_count_changed {
1006            return;
1007        }
1008
1009        if status_changed && !self.controller.read_only() {
1010            let offline_reason = match event.status {
1011                ClusterStatus::Online => None,
1012                ClusterStatus::Offline(None) => None,
1013                ClusterStatus::Offline(Some(reason)) => Some(reason.to_string()),
1014            };
1015            let row = Row::pack_slice(&[
1016                Datum::String(&event.replica_id.to_string()),
1017                Datum::UInt64(event.process_id),
1018                Datum::String(event.status.as_kebab_case_str()),
1019                Datum::from(offline_reason.as_deref()),
1020                Datum::TimestampTz(event.time.try_into().expect("must fit")),
1021            ]);
1022            self.controller.storage.append_introspection_updates(
1023                IntrospectionType::ReplicaStatusHistory,
1024                vec![(row, Diff::ONE)],
1025            );
1026        }
1027
1028        // Capture the rolled-up replica status before the update so we can tell
1029        // whether the user-visible status changed. Only needed for the notice.
1030        let old_replica_status = status_changed
1031            .then(|| ClusterReplicaStatuses::cluster_replica_status(replica_statuses));
1032
1033        let new_process_status = ClusterReplicaProcessStatus {
1034            status: event.status,
1035            restart_count: event.restart_count,
1036            time: event.time,
1037        };
1038        self.cluster_replica_statuses.ensure_cluster_status(
1039            event.cluster_id,
1040            event.replica_id,
1041            event.process_id,
1042            new_process_status,
1043        );
1044
1045        if let Some(old_replica_status) = old_replica_status {
1046            let cluster = self.catalog().get_cluster(event.cluster_id);
1047            let replica = cluster.replica(event.replica_id).expect("Replica exists");
1048            let new_replica_status = self
1049                .cluster_replica_statuses
1050                .get_cluster_replica_status(event.cluster_id, event.replica_id);
1051
1052            if old_replica_status != new_replica_status {
1053                let notifier = self.broadcast_notice_tx();
1054                let notice = AdapterNotice::ClusterReplicaStatusChanged {
1055                    cluster: cluster.name.clone(),
1056                    replica: replica.name.clone(),
1057                    status: new_replica_status,
1058                    time: event.time,
1059                };
1060                notifier(notice);
1061            }
1062        }
1063    }
1064
1065    #[mz_ore::instrument(level = "debug")]
1066    /// Linearizes sending the results of a read transaction by,
1067    ///   1. Holding back any results that were executed at some point in the future, until the
1068    ///   containing timeline has advanced to that point in the future.
1069    ///   2. Confirming that we are still the current leader before sending results to the client.
1070    async fn message_linearize_reads(&mut self) {
1071        let mut shortest_wait = Duration::MAX;
1072        let mut ready_txns = Vec::new();
1073
1074        // Cache for `TimestampOracle::read_ts` calls. These are somewhat
1075        // expensive so we cache the value. This is correct since all we're
1076        // risking is being too conservative. We will not accidentally "release"
1077        // a result too early.
1078        let mut cached_oracle_ts = BTreeMap::new();
1079
1080        for (conn_id, mut read_txn) in std::mem::take(&mut self.pending_linearize_read_txns) {
1081            if let TimestampContext::TimelineTimestamp {
1082                timeline,
1083                chosen_ts,
1084                oracle_ts,
1085            } = read_txn.timestamp_context()
1086            {
1087                let oracle_ts = match oracle_ts {
1088                    Some(oracle_ts) => oracle_ts,
1089                    None => {
1090                        // There was no oracle timestamp, so no need to delay.
1091                        ready_txns.push(read_txn);
1092                        continue;
1093                    }
1094                };
1095
1096                if chosen_ts <= oracle_ts {
1097                    // Chosen ts was already <= the oracle ts, so we're good
1098                    // to go!
1099                    ready_txns.push(read_txn);
1100                    continue;
1101                }
1102
1103                // See what the oracle timestamp is now and delay when needed.
1104                let current_oracle_ts = cached_oracle_ts.entry(timeline.clone());
1105                let current_oracle_ts = match current_oracle_ts {
1106                    btree_map::Entry::Vacant(entry) => {
1107                        let timestamp_oracle = self.get_timestamp_oracle(timeline);
1108                        let read_ts = timestamp_oracle.read_ts().await;
1109                        entry.insert(read_ts.clone());
1110                        read_ts
1111                    }
1112                    btree_map::Entry::Occupied(entry) => entry.get().clone(),
1113                };
1114
1115                if *chosen_ts <= current_oracle_ts {
1116                    ready_txns.push(read_txn);
1117                } else {
1118                    let wait =
1119                        Duration::from_millis(chosen_ts.saturating_sub(current_oracle_ts).into());
1120                    if wait < shortest_wait {
1121                        shortest_wait = wait;
1122                    }
1123                    read_txn.num_requeues += 1;
1124                    self.pending_linearize_read_txns.insert(conn_id, read_txn);
1125                }
1126            } else {
1127                ready_txns.push(read_txn);
1128            }
1129        }
1130
1131        if !ready_txns.is_empty() {
1132            // Sniff out one ctx, this is where tracing breaks down because we
1133            // process all outstanding txns as a batch here.
1134            let otel_ctx = ready_txns.first().expect("known to exist").otel_ctx.clone();
1135            let span = tracing::debug_span!("message_linearize_reads");
1136            otel_ctx.attach_as_parent_to(&span);
1137
1138            let now = Instant::now();
1139            for ready_txn in ready_txns {
1140                let span = tracing::debug_span!("retire_read_results");
1141                ready_txn.otel_ctx.attach_as_parent_to(&span);
1142                let _entered = span.enter();
1143                self.metrics
1144                    .linearize_message_seconds
1145                    .with_label_values(&[
1146                        ready_txn.txn.label(),
1147                        if ready_txn.num_requeues == 0 {
1148                            "true"
1149                        } else {
1150                            "false"
1151                        },
1152                    ])
1153                    .observe((now - ready_txn.created).as_secs_f64());
1154                if let Some((ctx, result)) = ready_txn.txn.finish() {
1155                    ctx.retire(result);
1156                }
1157            }
1158        }
1159
1160        if !self.pending_linearize_read_txns.is_empty() {
1161            // Cap wait time to 1s, then signal a re-check. `serve` awaits this
1162            // below group commit; see its linearize branch for why.
1163            let remaining_ms = std::cmp::min(shortest_wait, Duration::from_millis(1_000));
1164            let linearize_reads_notify = Arc::clone(&self.linearize_reads_notify);
1165            task::spawn(|| "deferred_read_txns", async move {
1166                tokio::time::sleep(remaining_ms).await;
1167                linearize_reads_notify.notify_one();
1168            });
1169        }
1170    }
1171}