1use std::collections::BTreeMap;
31use std::sync::Arc;
32use std::time::{Duration, Instant};
33
34use itertools::Itertools;
35use mz_adapter_types::dyncfgs::{
36 FRONTEND_READ_THEN_WRITE, HYDRATION_HISTORY_COLLECTION_INTERVAL,
37 HYDRATION_HISTORY_RETENTION_PERIOD,
38};
39use mz_catalog::builtin::{
40 MZ_CATALOG_SERVER_CLUSTER, MZ_OBJECT_HYDRATION_HISTORY, MZ_REPLICA_HYDRATION_HISTORY,
41};
42use mz_cluster_client::ReplicaId;
43use mz_controller::clusters::{ClusterStatus, ReplicaLocation};
44use mz_controller_types::ClusterId;
45use mz_ore::cast::CastFrom;
46use mz_ore::collections::CollectionExt;
47use mz_ore::now::EpochMillis;
48use mz_ore::task;
49use mz_repr::CatalogItemId;
50use mz_sql::plan::{MutationKind, Params, Plan, ReadThenWritePlan};
51use sha2::{Digest, Sha256};
52use tracing::warn;
53
54use crate::catalog::Catalog;
55use crate::command::ExecuteResponse;
56use crate::coord::{Coordinator, Message};
57use crate::metrics::Metrics;
58use crate::peek_client::CoordinatorClient;
59use crate::session::Session;
60use crate::{AdapterError, PeekClient};
61
62const SCHEDULE_RECHECK_CAP: Duration = Duration::from_secs(5);
68
69const DISABLED_RECHECK_INTERVAL: Duration = Duration::from_secs(60);
74
75const MUTATION_TIMEOUT: Duration = Duration::from_secs(300);
81
82const RETENTION_BATCH_SIZE: usize = 1000;
90
91fn next_fire_delay(now: EpochMillis, interval_ms: EpochMillis, offset: EpochMillis) -> Duration {
97 debug_assert!(interval_ms > 0);
98 let this_period = (now - (now % interval_ms)).saturating_add(offset);
99 let next = if this_period > now {
100 this_period
101 } else {
102 this_period.saturating_add(interval_ms)
103 };
104 Duration::from_millis(next.saturating_sub(now))
105}
106
107fn environment_schedule_offset(environment_id: &str, interval_ms: EpochMillis) -> EpochMillis {
109 debug_assert!(interval_ms > 0);
110 let digest = Sha256::digest(environment_id);
111 let hash = u64::from_le_bytes(digest[..8].try_into().expect("SHA-256 digest has 32 bytes"));
112 hash % interval_ms
113}
114
115impl Coordinator {
116 pub(super) fn schedule_hydration_history_collection(&self) {
129 let interval =
130 HYDRATION_HISTORY_COLLECTION_INTERVAL.get(self.catalog().system_config().dyncfgs());
131
132 let (delay, fire) = if interval.is_zero() {
135 (DISABLED_RECHECK_INTERVAL, false)
136 } else {
137 let interval_ms = EpochMillis::try_from(interval.as_millis())
140 .unwrap_or(EpochMillis::MAX)
141 .max(1);
142 let remaining = next_fire_delay(
143 self.now(),
144 interval_ms,
145 self.hydration_history_schedule_offset(interval_ms),
146 );
147 if remaining <= SCHEDULE_RECHECK_CAP {
148 (remaining, true)
149 } else {
150 (SCHEDULE_RECHECK_CAP, false)
151 }
152 };
153
154 let internal_cmd_tx = self.internal_cmd_tx.clone();
155 task::spawn(|| "hydration_history_schedule", async move {
156 tokio::time::sleep(delay).await;
157 let message = if fire {
158 Message::HydrationHistoryRun
159 } else {
160 Message::HydrationHistorySchedule
161 };
162 let _ = internal_cmd_tx.send(message);
164 });
165 }
166
167 fn hydration_history_schedule_offset(&self, interval_ms: EpochMillis) -> EpochMillis {
175 let environment_id = self.catalog().state().config().environment_id.to_string();
176 environment_schedule_offset(&environment_id, interval_ms)
177 }
178
179 pub(super) fn run_hydration_history_collection(&mut self) {
181 let (collection_interval, retention) = {
182 let dyncfgs = self.catalog().system_config().dyncfgs();
183 (
184 HYDRATION_HISTORY_COLLECTION_INTERVAL.get(dyncfgs),
185 HYDRATION_HISTORY_RETENTION_PERIOD.get(dyncfgs),
186 )
187 };
188 if collection_interval.is_zero() || self.controller.read_only() {
195 self.schedule_hydration_history_collection();
196 return;
197 }
198
199 let replicas = self
200 .catalog()
201 .user_cluster_replicas()
202 .filter(|replica| replica.config.compute.logging.enabled())
203 .filter(|replica| match &replica.config.location {
204 ReplicaLocation::Managed(_) => {
205 self.cluster_replica_statuses
206 .get_cluster_replica_status(replica.cluster_id, replica.replica_id)
207 == ClusterStatus::Online
208 }
209 ReplicaLocation::Unmanaged(_) => true,
212 })
213 .map(|replica| ReplicaTarget {
214 cluster_id: replica.cluster_id,
215 replica_id: replica.replica_id,
216 process_count: replica.config.location.num_processes(),
217 })
218 .sorted_by_key(|replica| replica.replica_id)
219 .collect_vec();
220
221 let catalog = self.owned_catalog();
222 let catalog_server = catalog.resolve_builtin_cluster(&MZ_CATALOG_SERVER_CLUSTER);
226 let catalog_server_target = catalog_server
227 .replicas()
228 .next()
229 .map(|replica| (catalog_server.id, replica.replica_id));
230
231 let replica = next_replica(&replicas, self.hydration_history_replica_cursor);
232 if let Some(replica) = replica {
233 self.hydration_history_replica_cursor = Some(replica.replica_id);
234 }
235 let mut sweep = self.new_sweep(catalog, retention);
236 let internal_cmd_tx = self.internal_cmd_tx.clone();
237
238 let handle = task::spawn(|| "hydration_history_sweep", async move {
239 let started = Instant::now();
240 if let Some(replica) = replica {
241 sweep.collect(replica).await;
242 }
243
244 if let Some((cluster_id, replica_id)) = catalog_server_target {
248 sweep.retain(cluster_id, replica_id).await;
249 }
250
251 sweep
252 .metrics
253 .hydration_history_sweep_duration_seconds
254 .observe(started.elapsed().as_secs_f64());
255 let _ = internal_cmd_tx.send(Message::HydrationHistorySchedule);
256 });
257
258 self.hydration_history_sweep = Some(handle.abort_on_drop());
261 }
262
263 fn new_sweep(&self, catalog: Arc<Catalog>, retention: Duration) -> Sweep {
265 let retention_ms = u64::try_from(retention.as_millis()).unwrap_or(u64::MAX);
266 let build_version = catalog.state().config().build_info.human_version(None);
267 let client = PeekClient::new(
271 CoordinatorClient::Background {
272 tx: self.internal_cmd_tx.clone(),
273 metrics: self.metrics.clone(),
274 },
275 &catalog,
276 Arc::clone(&self.controller.storage_collections),
277 Arc::clone(&self.transient_id_gen),
278 self.optimizer_metrics.clone(),
279 self.persist_client.clone(),
280 self.statement_logging.create_frontend(build_version),
281 Arc::clone(&self.occ_write_semaphore),
282 FRONTEND_READ_THEN_WRITE.get(self.catalog().system_config().dyncfgs()),
283 self.group_commit_tx.clone(),
284 self.controller.read_only(),
285 );
286 Sweep {
287 client,
288 object_history_id: catalog.resolve_builtin_table(&MZ_OBJECT_HYDRATION_HISTORY),
289 replica_history_id: catalog.resolve_builtin_table(&MZ_REPLICA_HYDRATION_HISTORY),
290 catalog,
291 metrics: self.metrics.clone(),
292 wall_time: self.now_datetime(),
293 cutoff: mz_ore::now::to_datetime(self.now().saturating_sub(retention_ms)).to_rfc3339(),
294 }
295 }
296}
297
298#[derive(Clone, Copy, Debug, Eq, PartialEq)]
300struct ReplicaTarget {
301 cluster_id: ClusterId,
302 replica_id: ReplicaId,
303 process_count: usize,
304}
305
306fn next_replica(replicas: &[ReplicaTarget], cursor: Option<ReplicaId>) -> Option<ReplicaTarget> {
312 replicas
313 .iter()
314 .find(|replica| cursor.is_none_or(|cursor| replica.replica_id > cursor))
315 .or_else(|| replicas.first())
316 .copied()
317}
318
319fn object_collection_sql(cluster_id: ClusterId, replica_id: ReplicaId, cutoff: &str) -> String {
361 format!(
376 "SELECT
377 e.object_id,
378 '{cluster_id}'::text AS cluster_id,
379 '{replica_id}'::text AS replica_id,
380 e.installed_at,
381 e.started_at,
382 e.hydrated_at,
383 'hydrated'::text AS status
384 FROM (
385 SELECT
386 t.export_id AS object_id,
387 min(t.installed_at) AS installed_at,
388 min(t.started_at) AS started_at,
389 max(t.hydrated_at) AS hydrated_at
390 FROM mz_introspection.mz_compute_hydration_times_per_worker AS t
391 WHERE t.export_id NOT LIKE 'si%'
392 AND t.export_id NOT LIKE 't%'
393 GROUP BY t.export_id
394 HAVING count(*) = count(t.hydrated_at)
395 ) AS e
396 WHERE e.hydrated_at >= TIMESTAMPTZ '{cutoff}'
397 AND NOT EXISTS (
398 SELECT 1
399 FROM mz_internal.mz_object_hydration_history AS h
400 WHERE h.object_id = e.object_id
401 AND h.replica_id = '{replica_id}'::text
402 AND h.installed_at = e.installed_at
403 )"
404 )
405}
406
407fn replica_collection_sql(target: ReplicaTarget, cutoff: &str) -> String {
425 let ReplicaTarget {
426 cluster_id,
427 replica_id,
428 process_count,
429 } = target;
430 format!(
433 "WITH
434 -- One hydration interval per compute export: earliest install and
435 -- latest finish across its workers. Hydrated only once every worker
436 -- visible at this timestamp has finished.
437 objects AS (
438 SELECT
439 t.export_id AS object_id,
440 min(t.installed_at) AS installed_at,
441 max(t.hydrated_at) AS hydrated_at,
442 count(*) = count(t.hydrated_at) AS hydrated
443 FROM mz_introspection.mz_compute_hydration_times_per_worker AS t
444 WHERE t.export_id NOT LIKE 't%'
445 GROUP BY t.export_id
446 ),
447 -- Completed intervals in install order, each with the coverage
448 -- horizon: the latest finish among this and all earlier intervals.
449 covered AS (
450 SELECT
451 object_id,
452 installed_at,
453 hydrated_at,
454 max(hydrated_at) OVER (
455 ORDER BY installed_at, object_id
456 ROWS UNBOUNDED PRECEDING
457 ) AS covered_through
458 FROM objects
459 WHERE hydrated
460 ),
461 -- An interval starts a new episode when the horizon just before it
462 -- does not reach its install: for a moment, nothing was hydrating.
463 flagged AS (
464 SELECT
465 object_id,
466 installed_at,
467 hydrated_at,
468 lag(covered_through) OVER (
469 ORDER BY installed_at, object_id
470 ) IS NULL
471 OR lag(covered_through) OVER (
472 ORDER BY installed_at, object_id
473 ) < installed_at AS starts_episode
474 FROM covered
475 ),
476 -- Each interval belongs to the latest episode start at or before it.
477 labeled AS (
478 SELECT
479 object_id,
480 installed_at,
481 hydrated_at,
482 max(CASE WHEN starts_episode THEN installed_at END) OVER (
483 ORDER BY installed_at, object_id
484 ROWS UNBOUNDED PRECEDING
485 ) AS episode_started_at
486 FROM flagged
487 ),
488 -- One row per completed episode.
489 episodes AS (
490 SELECT
491 episode_started_at AS started_at,
492 max(hydrated_at) AS finished_at,
493 count(*) FILTER (WHERE object_id NOT LIKE 'si%')::uint8 AS object_count
494 FROM labeled
495 GROUP BY episode_started_at
496 ),
497 -- The earliest install of an export that has not hydrated yet.
498 open_min AS (
499 SELECT min(installed_at) AS v FROM objects WHERE NOT hydrated
500 ),
501 -- The episode to record: the latest one that finished before any
502 -- unhydrated export was installed. An episode finishing at or after
503 -- open_min contains that open interval and is still in progress.
504 -- Comparing against this one scalar, instead of joining episodes
505 -- with open intervals, avoids a cross product that is quadratic when
506 -- many episodes coexist with many still-hydrating exports.
507 episode AS (
508 SELECT e.started_at, e.finished_at, e.object_count
509 FROM episodes AS e, open_min AS o
510 WHERE o.v IS NULL OR e.finished_at < o.v
511 ORDER BY e.started_at DESC
512 LIMIT 1
513 ),
514 -- Process-lifetime resource high-water marks, and how many processes
515 -- have reported them.
516 resources AS (
517 SELECT
518 count(DISTINCT process_id) AS process_count,
519 max(value) FILTER (
520 WHERE source = 'cgroup' AND metric = 'memory_peak'
521 ) AS peak_memory_bytes,
522 coalesce(
523 max(value) FILTER (
524 WHERE source = 'statvfs' AND metric = 'fs_used_peak'
525 ),
526 max(value) FILTER (
527 WHERE source = 'cgroup' AND metric = 'swap_peak'
528 )
529 ) AS peak_disk_bytes
530 FROM mz_introspection.mz_cluster_replica_resource_usage
531 ),
532 -- The history row to write, held back until every configured process
533 -- has reported resource usage and dropped once the episode has aged
534 -- past the retention cutoff.
535 candidate AS (
536 SELECT
537 '{replica_id}'::text AS replica_id,
538 '{cluster_id}'::text AS cluster_id,
539 e.started_at,
540 e.finished_at,
541 e.object_count,
542 r.peak_memory_bytes,
543 r.peak_disk_bytes,
544 'hydrated'::text AS status
545 FROM episode AS e
546 CROSS JOIN resources AS r
547 WHERE r.process_count = {process_count}::uint8
548 AND e.finished_at >= TIMESTAMPTZ '{cutoff}'
549 )
550 -- Skip episodes the history already covers: a recorded row finishing
551 -- at or after this start is this episode, or overlaps it under
552 -- cross-process clock skew.
553 SELECT c.*
554 FROM candidate AS c
555 WHERE NOT EXISTS (
556 SELECT 1
557 FROM mz_internal.mz_replica_hydration_history AS h
558 WHERE h.replica_id = c.replica_id
559 AND h.finished_at >= c.started_at
560 )"
561 )
562}
563
564fn object_retention_sql(cutoff: &str) -> String {
570 format!(
574 "SELECT * FROM (
575 SELECT
576 object_id, cluster_id, replica_id, installed_at, started_at,
577 hydrated_at, status
578 FROM mz_internal.mz_object_hydration_history
579 WHERE hydrated_at < TIMESTAMPTZ '{cutoff}'
580 ORDER BY hydrated_at
581 LIMIT {RETENTION_BATCH_SIZE}
582 )"
583 )
584}
585
586fn replica_retention_sql(cutoff: &str) -> String {
588 format!(
589 "SELECT * FROM (
590 SELECT
591 replica_id, cluster_id, started_at, finished_at, object_count,
592 peak_memory_bytes, peak_disk_bytes, status
593 FROM mz_internal.mz_replica_hydration_history
594 WHERE finished_at < TIMESTAMPTZ '{cutoff}'
595 ORDER BY finished_at
596 LIMIT {RETENTION_BATCH_SIZE}
597 )"
598 )
599}
600
601struct Sweep {
603 client: PeekClient,
604 catalog: Arc<Catalog>,
605 object_history_id: CatalogItemId,
606 replica_history_id: CatalogItemId,
607 metrics: Metrics,
608 wall_time: chrono::DateTime<chrono::Utc>,
609 cutoff: String,
613}
614
615impl Sweep {
616 async fn collect(&mut self, target: ReplicaTarget) {
618 let ReplicaTarget {
619 cluster_id,
620 replica_id,
621 ..
622 } = target;
623 let sql = object_collection_sql(cluster_id, replica_id, &self.cutoff);
624 let _ = self
625 .run(
626 "collection",
627 self.object_history_id,
628 cluster_id,
629 replica_id,
630 MutationKind::Insert,
631 &sql,
632 )
633 .await;
634
635 let sql = replica_collection_sql(target, &self.cutoff);
636 let _ = self
637 .run(
638 "replica_collection",
639 self.replica_history_id,
640 cluster_id,
641 replica_id,
642 MutationKind::Insert,
643 &sql,
644 )
645 .await;
646 }
647
648 async fn retain(&mut self, cluster_id: ClusterId, replica_id: ReplicaId) {
650 let sql = object_retention_sql(&self.cutoff);
651 if let Some(deleted) = self
652 .run(
653 "retention",
654 self.object_history_id,
655 cluster_id,
656 replica_id,
657 MutationKind::Delete,
658 &sql,
659 )
660 .await
661 && deleted == RETENTION_BATCH_SIZE
662 {
663 self.metrics.hydration_history_retention_batch_full.inc();
664 }
665
666 let sql = replica_retention_sql(&self.cutoff);
667 if let Some(deleted) = self
668 .run(
669 "replica_retention",
670 self.replica_history_id,
671 cluster_id,
672 replica_id,
673 MutationKind::Delete,
674 &sql,
675 )
676 .await
677 && deleted == RETENTION_BATCH_SIZE
678 {
679 self.metrics.hydration_history_retention_batch_full.inc();
680 }
681 }
682
683 async fn run(
694 &mut self,
695 step: &'static str,
696 history_id: CatalogItemId,
697 cluster_id: ClusterId,
698 replica_id: ReplicaId,
699 kind: MutationKind,
700 sql: &str,
701 ) -> Option<usize> {
702 let mutation = async {
703 let plan = plan_mutation(&self.catalog, history_id, kind, sql)?;
704 let mut session = Session::dummy();
705 session.start_transaction_single_stmt(self.wall_time);
706 let response = self
707 .client
708 .background_read_then_write(
709 &mut session,
710 plan,
711 cluster_id,
712 replica_id,
713 &self.catalog,
714 )
715 .await?;
716 Ok::<_, AdapterError>(response)
717 };
718 match tokio::time::timeout(MUTATION_TIMEOUT, mutation).await {
719 Ok(Ok(response)) => {
720 let (rows, action) = match (kind, response) {
721 (MutationKind::Insert, ExecuteResponse::Inserted(rows)) => (rows, "appended"),
722 (MutationKind::Update, ExecuteResponse::Updated(rows)) => (rows, "updated"),
723 (MutationKind::Delete, ExecuteResponse::Deleted(rows)) => (rows, "deleted"),
724 (_, response) => {
725 self.observe_mutation(step, "error");
726 mz_ore::soft_panic_or_log!(
727 "hydration history {step} returned an unexpected response: {response:?}"
728 );
729 return None;
730 }
731 };
732 self.metrics
733 .hydration_history_rows_affected
734 .with_label_values(&[action])
735 .inc_by(u64::cast_from(rows));
736 let outcome = if rows == 0 { "noop" } else { "success" };
737 self.observe_mutation(step, outcome);
738 Some(rows)
739 }
740 Ok(Err(error)) => {
741 self.observe_mutation(step, "error");
742 if step.ends_with("collection")
743 && matches!(&error, AdapterError::ReadThenWriteContention)
744 {
745 warn!(
746 %step, %cluster_id, %replica_id, %error,
747 "hydration history step failed, the replica's introspection frontier \
748 may be trailing the write frontier"
749 );
750 } else {
751 warn!(%step, %cluster_id, %replica_id, %error, "hydration history step failed");
752 }
753 None
754 }
755 Err(_) if step.ends_with("collection") => {
759 self.observe_mutation(step, "timeout");
760 warn!(
761 %step, %cluster_id, %replica_id,
762 "hydration history step timed out, \
763 the replica's introspection frontier may be trailing the write frontier"
764 );
765 None
766 }
767 Err(_) => {
768 self.observe_mutation(step, "timeout");
769 warn!(%step, %cluster_id, %replica_id, "hydration history step timed out");
770 None
771 }
772 }
773 }
774
775 fn observe_mutation(&self, operation: &str, outcome: &str) {
776 self.metrics
777 .hydration_history_mutations
778 .with_label_values(&[operation, outcome])
779 .inc();
780 }
781}
782
783fn plan_mutation(
793 catalog: &Arc<Catalog>,
794 target_id: CatalogItemId,
795 kind: MutationKind,
796 sql: &str,
797) -> Result<ReadThenWritePlan, AdapterError> {
798 let session_catalog = catalog.for_system_session();
799 let parsed = mz_sql::parse::parse(sql)
800 .map_err(AdapterError::from)?
801 .into_element();
802 let (stmt, resolved_ids) = mz_sql::names::resolve(&session_catalog, parsed.ast)?;
803 let (plan, _) = mz_sql::plan::plan(
804 None,
805 &session_catalog,
806 stmt,
807 &Params::empty(),
808 &resolved_ids,
809 )?;
810 let Plan::Select(select) = plan else {
811 return Err(AdapterError::Internal(
812 "hydration history query did not plan as SELECT".into(),
813 ));
814 };
815
816 let target_desc = catalog
817 .get_entry(&target_id)
818 .relation_desc_latest()
819 .expect("hydration history target is a table");
820 let selection_types = select.source.typ(&[], &BTreeMap::new()).column_types;
821 let target_types = &target_desc.typ().column_types;
822 let matches = selection_types.len() == target_types.len()
823 && selection_types
824 .iter()
825 .zip_eq(target_types)
826 .all(|(selected, target)| selected.scalar_type == target.scalar_type);
829 if !matches {
830 return Err(AdapterError::Internal(format!(
831 "hydration history query does not match the target table: \
832 selection {selection_types:?}, table {target_types:?}"
833 )));
834 }
835
836 Ok(ReadThenWritePlan {
837 id: target_id,
838 selection: select.source,
839 finishing: select.finishing,
840 assignments: BTreeMap::new(),
841 kind,
842 returning: Vec::new(),
843 })
844}
845
846#[cfg(test)]
847mod tests {
848 use super::*;
849
850 #[mz_ore::test]
851 fn replica_sweep_advances_and_wraps() {
852 let cluster = ClusterId::user(1).expect("valid cluster ID");
853 let replicas = [
854 ReplicaTarget {
855 cluster_id: cluster,
856 replica_id: ReplicaId::User(1),
857 process_count: 1,
858 },
859 ReplicaTarget {
860 cluster_id: cluster,
861 replica_id: ReplicaId::User(3),
862 process_count: 1,
863 },
864 ];
865
866 assert_eq!(next_replica(&replicas, None), Some(replicas[0]));
867 assert_eq!(
868 next_replica(&replicas, Some(ReplicaId::User(1))),
869 Some(replicas[1])
870 );
871 assert_eq!(
872 next_replica(&replicas, Some(ReplicaId::User(3))),
873 Some(replicas[0])
874 );
875 assert_eq!(next_replica(&[], None), None);
876 }
877
878 #[mz_ore::test]
881 fn fire_delay_is_offset_within_the_interval() {
882 let interval = 60_000;
883
884 assert_eq!(
886 next_fire_delay(1_000, interval, 5_000),
887 Duration::from_millis(4_000)
888 );
889 assert_eq!(
891 next_fire_delay(5_000, interval, 5_000),
892 Duration::from_millis(interval)
893 );
894 assert_eq!(
896 next_fire_delay(6_000, interval, 5_000),
897 Duration::from_millis(59_000)
898 );
899 assert_eq!(
901 next_fire_delay(59_999, interval, 0),
902 Duration::from_millis(1)
903 );
904 assert_eq!(
905 next_fire_delay(60_000, interval, 0),
906 Duration::from_millis(interval)
907 );
908
909 let one = environment_schedule_offset(
911 "aws-us-east-1-00000000-0000-0000-0000-000000000000-0",
912 interval,
913 );
914 let two = environment_schedule_offset(
915 "aws-us-west-1-00000000-0000-0000-0000-000000000000-1",
916 interval,
917 );
918 assert_eq!(one, 30_189);
919 assert_eq!(two, 38_252);
920 assert_ne!(one, two);
921 }
922
923 #[mz_ore::test]
928 fn collect_requires_every_worker() {
929 let cutoff = "1970-01-01T00:00:00+00:00";
930 let sql = object_collection_sql(
931 ClusterId::user(1).expect("valid cluster ID"),
932 ReplicaId::User(2),
933 cutoff,
934 );
935 assert!(
936 sql.contains("HAVING count(*) = count(t.hydrated_at)"),
937 "{sql}"
938 );
939 assert!(sql.contains("max(t.hydrated_at)"), "{sql}");
940 assert!(!sql.contains("worker_id"), "{sql}");
941
942 let aggregate_end = sql.find(") AS e").expect("aggregate subquery");
945 assert!(sql.find(cutoff).expect("cutoff") > aggregate_end, "{sql}");
946 assert!(
947 sql.find("NOT EXISTS").expect("anti-join") > aggregate_end,
948 "{sql}"
949 );
950 }
951
952 #[mz_ore::test]
959 fn replica_collection_uses_latest_completed_interval_island() {
960 let sql = replica_collection_sql(
961 ReplicaTarget {
962 cluster_id: ClusterId::user(1).expect("valid cluster ID"),
963 replica_id: ReplicaId::User(2),
964 process_count: 3,
965 },
966 "1970-01-01T00:00:00+00:00",
967 );
968 let normalized_sql = sql.split_whitespace().collect::<Vec<_>>().join(" ");
969
970 assert!(sql.contains("ROWS UNBOUNDED PRECEDING"), "{sql}");
974 assert!(sql.contains("lag(covered_through)"), "{sql}");
975 assert!(
976 sql.contains("CASE WHEN starts_episode THEN installed_at END"),
977 "{sql}"
978 );
979 assert!(sql.contains("GROUP BY episode_started_at"), "{sql}");
980 assert!(!sql.contains("bool_and(hydrated)"), "{sql}");
985 assert!(
989 normalized_sql.contains("WHERE o.v IS NULL OR e.finished_at < o.v"),
990 "{sql}"
991 );
992 assert!(!sql.contains("o.installed_at <= e.finished_at"), "{sql}");
993 assert!(
994 normalized_sql.contains("ORDER BY e.started_at DESC LIMIT 1"),
995 "{sql}"
996 );
997 assert!(sql.contains("r.process_count = 3::uint8"), "{sql}");
998 assert!(sql.contains("WHERE t.export_id NOT LIKE 't%'"), "{sql}");
999 assert!(!sql.contains("WHERE t.export_id LIKE 'u%'"), "{sql}");
1000 assert!(!sql.contains("mz_object_global_ids"), "{sql}");
1001 assert!(!sql.contains("mz_catalog.mz_objects"), "{sql}");
1002 assert!(
1003 normalized_sql
1004 .contains("max(value) FILTER ( WHERE source = 'cgroup' AND metric = 'memory_peak'"),
1005 "{sql}"
1006 );
1007 assert!(
1008 normalized_sql.contains(
1009 "max(value) FILTER ( WHERE source = 'statvfs' AND metric = 'fs_used_peak'"
1010 ),
1011 "{sql}"
1012 );
1013 assert!(
1014 normalized_sql
1015 .contains("max(value) FILTER ( WHERE source = 'cgroup' AND metric = 'swap_peak'"),
1016 "{sql}"
1017 );
1018 assert!(!normalized_sql.contains("sum(value)"), "{sql}");
1019 assert!(
1020 sql.contains("FROM mz_internal.mz_replica_hydration_history"),
1021 "{sql}"
1022 );
1023 assert!(sql.contains("h.finished_at >= c.started_at"), "{sql}");
1024 }
1025}