1use std::collections::BTreeMap;
31use std::sync::Arc;
32use std::time::{Duration, Instant};
33
34use itertools::Itertools;
35use mz_adapter_types::dyncfgs::{
36 HYDRATION_HISTORY_COLLECTION_INTERVAL, HYDRATION_HISTORY_RETENTION_PERIOD,
37 REPLICA_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::session::Session;
59use crate::{AdapterError, PeekClient};
60
61const SCHEDULE_RECHECK_CAP: Duration = Duration::from_secs(5);
67
68const DISABLED_RECHECK_INTERVAL: Duration = Duration::from_secs(60);
73
74const MUTATION_TIMEOUT: Duration = Duration::from_secs(300);
80
81const RETENTION_BATCH_SIZE: usize = 1000;
89
90fn next_fire_delay(now: EpochMillis, interval_ms: EpochMillis, offset: EpochMillis) -> Duration {
96 debug_assert!(interval_ms > 0);
97 let this_period = (now - (now % interval_ms)).saturating_add(offset);
98 let next = if this_period > now {
99 this_period
100 } else {
101 this_period.saturating_add(interval_ms)
102 };
103 Duration::from_millis(next.saturating_sub(now))
104}
105
106fn environment_schedule_offset(environment_id: &str, interval_ms: EpochMillis) -> EpochMillis {
108 debug_assert!(interval_ms > 0);
109 let digest = Sha256::digest(environment_id);
110 let hash = u64::from_le_bytes(digest[..8].try_into().expect("SHA-256 digest has 32 bytes"));
111 hash % interval_ms
112}
113
114impl Coordinator {
115 pub(super) fn schedule_hydration_history_collection(&self) {
128 let interval =
129 HYDRATION_HISTORY_COLLECTION_INTERVAL.get(self.catalog().system_config().dyncfgs());
130
131 let (delay, fire) = if interval.is_zero() {
134 (DISABLED_RECHECK_INTERVAL, false)
135 } else {
136 let interval_ms = EpochMillis::try_from(interval.as_millis())
139 .unwrap_or(EpochMillis::MAX)
140 .max(1);
141 let remaining = next_fire_delay(
142 self.now(),
143 interval_ms,
144 self.hydration_history_schedule_offset(interval_ms),
145 );
146 if remaining <= SCHEDULE_RECHECK_CAP {
147 (remaining, true)
148 } else {
149 (SCHEDULE_RECHECK_CAP, false)
150 }
151 };
152
153 let internal_cmd_tx = self.internal_cmd_tx.clone();
154 task::spawn(|| "hydration_history_schedule", async move {
155 tokio::time::sleep(delay).await;
156 let message = if fire {
157 Message::HydrationHistoryRun
158 } else {
159 Message::HydrationHistorySchedule
160 };
161 let _ = internal_cmd_tx.send(message);
163 });
164 }
165
166 fn hydration_history_schedule_offset(&self, interval_ms: EpochMillis) -> EpochMillis {
174 let environment_id = self.catalog().state().config().environment_id.to_string();
175 environment_schedule_offset(&environment_id, interval_ms)
176 }
177
178 pub(super) fn run_hydration_history_collection(&mut self) {
180 let collection_interval = {
181 let dyncfgs = self.catalog().system_config().dyncfgs();
182 HYDRATION_HISTORY_COLLECTION_INTERVAL.get(dyncfgs)
183 };
184 if collection_interval.is_zero() || self.controller.read_only() {
189 self.schedule_hydration_history_collection();
190 return;
191 }
192
193 let replicas = self
194 .catalog()
195 .clusters()
196 .flat_map(|cluster| cluster.replicas())
197 .filter(|replica| replica.config.compute.logging.enabled())
198 .filter(|replica| match &replica.config.location {
199 ReplicaLocation::Managed(_) => {
200 self.cluster_replica_statuses
201 .get_cluster_replica_status(replica.cluster_id, replica.replica_id)
202 == ClusterStatus::Online
203 }
204 ReplicaLocation::Unmanaged(_) => true,
207 })
208 .map(|replica| ReplicaTarget {
209 cluster_id: replica.cluster_id,
210 replica_id: replica.replica_id,
211 process_count: replica.config.location.num_processes(),
212 })
213 .sorted_by_key(|replica| replica.replica_id)
214 .collect_vec();
215
216 let catalog = self.owned_catalog();
217 let catalog_server = catalog.resolve_builtin_cluster(&MZ_CATALOG_SERVER_CLUSTER);
221 let catalog_server_target = catalog_server
222 .replicas()
223 .next()
224 .map(|replica| (catalog_server.id, replica.replica_id));
225
226 let replica = next_replica(&replicas, self.hydration_history_replica_cursor);
227 if let Some(replica) = replica {
228 self.hydration_history_replica_cursor = Some(replica.replica_id);
229 }
230 let mut sweep = self.new_sweep(catalog);
231 let internal_cmd_tx = self.internal_cmd_tx.clone();
232
233 let handle = task::spawn(|| "hydration_history_sweep", async move {
234 let started = Instant::now();
235 if let Some(replica) = replica {
236 sweep.collect(replica).await;
237 }
238
239 if let Some((cluster_id, replica_id)) = catalog_server_target {
243 sweep.retain(cluster_id, replica_id).await;
244 }
245
246 sweep
247 .metrics
248 .hydration_history_sweep_duration_seconds
249 .observe(started.elapsed().as_secs_f64());
250 let _ = internal_cmd_tx.send(Message::HydrationHistorySchedule);
251 });
252
253 self.hydration_history_sweep = Some(handle.abort_on_drop());
256 }
257
258 fn new_sweep(&self, catalog: Arc<Catalog>) -> Sweep {
260 let now = self.now();
261 let cutoff = |retention: Duration| {
262 let retention_ms = u64::try_from(retention.as_millis()).unwrap_or(u64::MAX);
263 mz_ore::now::to_datetime(now.saturating_sub(retention_ms)).to_rfc3339()
264 };
265 let dyncfgs = catalog.system_config().dyncfgs();
266 let object_cutoff = cutoff(HYDRATION_HISTORY_RETENTION_PERIOD.get(dyncfgs));
267 let replica_cutoff = cutoff(REPLICA_HYDRATION_HISTORY_RETENTION_PERIOD.get(dyncfgs));
268 let client = self.background_peek_client(&catalog);
269 Sweep {
270 client,
271 object_history_id: catalog.resolve_builtin_table(&MZ_OBJECT_HYDRATION_HISTORY),
272 replica_history_id: catalog.resolve_builtin_table(&MZ_REPLICA_HYDRATION_HISTORY),
273 catalog,
274 metrics: self.metrics.clone(),
275 wall_time: self.now_datetime(),
276 object_cutoff,
277 replica_cutoff,
278 }
279 }
280}
281
282#[derive(Clone, Copy, Debug, Eq, PartialEq)]
284struct ReplicaTarget {
285 cluster_id: ClusterId,
286 replica_id: ReplicaId,
287 process_count: usize,
288}
289
290fn next_replica(replicas: &[ReplicaTarget], cursor: Option<ReplicaId>) -> Option<ReplicaTarget> {
296 replicas
297 .iter()
298 .find(|replica| cursor.is_none_or(|cursor| replica.replica_id > cursor))
299 .or_else(|| replicas.first())
300 .copied()
301}
302
303fn object_collection_sql(cluster_id: ClusterId, replica_id: ReplicaId, cutoff: &str) -> String {
345 format!(
360 "SELECT
361 e.object_id,
362 '{cluster_id}'::text AS cluster_id,
363 '{replica_id}'::text AS replica_id,
364 e.installed_at,
365 e.started_at,
366 e.hydrated_at,
367 'hydrated'::text AS status
368 FROM (
369 SELECT
370 t.export_id AS object_id,
371 min(t.installed_at) AS installed_at,
372 min(t.started_at) AS started_at,
373 max(t.hydrated_at) AS hydrated_at
374 FROM mz_introspection.mz_compute_hydration_times_per_worker AS t
375 WHERE t.export_id NOT LIKE 'si%'
376 AND t.export_id NOT LIKE 't%'
377 GROUP BY t.export_id
378 HAVING count(*) = count(t.hydrated_at)
379 ) AS e
380 WHERE e.hydrated_at >= TIMESTAMPTZ '{cutoff}'
381 AND NOT EXISTS (
382 SELECT 1
383 FROM mz_internal.mz_object_hydration_history AS h
384 WHERE h.object_id = e.object_id
385 AND h.replica_id = '{replica_id}'::text
386 AND h.installed_at = e.installed_at
387 )"
388 )
389}
390
391fn replica_collection_sql(target: ReplicaTarget, cutoff: &str) -> String {
409 let ReplicaTarget {
410 cluster_id,
411 replica_id,
412 process_count,
413 } = target;
414 format!(
417 "WITH
418 -- One hydration interval per compute export: earliest install and
419 -- latest finish across its workers. Hydrated only once every worker
420 -- visible at this timestamp has finished.
421 objects AS (
422 SELECT
423 t.export_id AS object_id,
424 min(t.installed_at) AS installed_at,
425 max(t.hydrated_at) AS hydrated_at,
426 count(*) = count(t.hydrated_at) AS hydrated
427 FROM mz_introspection.mz_compute_hydration_times_per_worker AS t
428 WHERE t.export_id NOT LIKE 't%'
429 GROUP BY t.export_id
430 ),
431 -- Completed intervals in install order, each with the coverage
432 -- horizon: the latest finish among this and all earlier intervals.
433 covered AS (
434 SELECT
435 object_id,
436 installed_at,
437 hydrated_at,
438 max(hydrated_at) OVER (
439 ORDER BY installed_at, object_id
440 ROWS UNBOUNDED PRECEDING
441 ) AS covered_through
442 FROM objects
443 WHERE hydrated
444 ),
445 -- An interval starts a new episode when the horizon just before it
446 -- does not reach its install: for a moment, nothing was hydrating.
447 flagged AS (
448 SELECT
449 object_id,
450 installed_at,
451 hydrated_at,
452 lag(covered_through) OVER (
453 ORDER BY installed_at, object_id
454 ) IS NULL
455 OR lag(covered_through) OVER (
456 ORDER BY installed_at, object_id
457 ) < installed_at AS starts_episode
458 FROM covered
459 ),
460 -- Each interval belongs to the latest episode start at or before it.
461 labeled AS (
462 SELECT
463 object_id,
464 installed_at,
465 hydrated_at,
466 max(CASE WHEN starts_episode THEN installed_at END) OVER (
467 ORDER BY installed_at, object_id
468 ROWS UNBOUNDED PRECEDING
469 ) AS episode_started_at
470 FROM flagged
471 ),
472 -- One row per completed episode.
473 episodes AS (
474 SELECT
475 episode_started_at AS started_at,
476 max(hydrated_at) AS finished_at,
477 count(*) FILTER (WHERE object_id NOT LIKE 'si%')::uint8 AS object_count
478 FROM labeled
479 GROUP BY episode_started_at
480 ),
481 -- The earliest install of an export that has not hydrated yet.
482 open_min AS (
483 SELECT min(installed_at) AS v FROM objects WHERE NOT hydrated
484 ),
485 -- The episode to record: the latest one that finished before any
486 -- unhydrated export was installed. An episode finishing at or after
487 -- open_min contains that open interval and is still in progress.
488 -- Comparing against this one scalar, instead of joining episodes
489 -- with open intervals, avoids a cross product that is quadratic when
490 -- many episodes coexist with many still-hydrating exports.
491 episode AS (
492 SELECT e.started_at, e.finished_at, e.object_count
493 FROM episodes AS e, open_min AS o
494 WHERE o.v IS NULL OR e.finished_at < o.v
495 ORDER BY e.started_at DESC
496 LIMIT 1
497 ),
498 -- Process-lifetime resource high-water marks for each process.
499 resources AS (
500 SELECT
501 process_id,
502 max(value) FILTER (
503 WHERE source = 'cgroup' AND metric = 'memory_peak'
504 ) AS peak_memory_bytes,
505 coalesce(
506 max(value) FILTER (
507 WHERE source = 'statvfs' AND metric = 'fs_used_peak'
508 ),
509 max(value) FILTER (
510 WHERE source = 'cgroup' AND metric = 'swap_peak'
511 )
512 ) AS peak_disk_bytes
513 FROM mz_introspection.mz_cluster_replica_resource_usage
514 GROUP BY process_id
515 ),
516 -- The history rows to write, held back until every configured process
517 -- has reported resource usage and dropped once the episode has aged
518 -- past the retention cutoff.
519 candidate AS (
520 SELECT
521 '{replica_id}'::text AS replica_id,
522 '{cluster_id}'::text AS cluster_id,
523 e.started_at,
524 e.finished_at,
525 e.object_count,
526 r.peak_memory_bytes,
527 r.peak_disk_bytes,
528 'hydrated'::text AS status,
529 r.process_id
530 FROM episode AS e
531 CROSS JOIN resources AS r
532 WHERE (SELECT count(*) FROM resources) = {process_count}::uint8
533 AND e.finished_at >= TIMESTAMPTZ '{cutoff}'
534 )
535 -- Skip episodes the history already covers: a recorded row finishing
536 -- at or after this start is this episode, or overlaps it under
537 -- cross-process clock skew.
538 SELECT c.*
539 FROM candidate AS c
540 WHERE NOT EXISTS (
541 SELECT 1
542 FROM mz_internal.mz_replica_hydration_history AS h
543 WHERE h.replica_id = c.replica_id
544 AND h.finished_at >= c.started_at
545 )"
546 )
547}
548
549fn object_retention_sql(cutoff: &str) -> String {
555 format!(
559 "SELECT * FROM (
560 SELECT
561 object_id, cluster_id, replica_id, installed_at, started_at,
562 hydrated_at, status
563 FROM mz_internal.mz_object_hydration_history
564 WHERE hydrated_at < TIMESTAMPTZ '{cutoff}'
565 ORDER BY hydrated_at
566 LIMIT {RETENTION_BATCH_SIZE}
567 )"
568 )
569}
570
571fn replica_retention_sql(cutoff: &str) -> String {
573 format!(
574 "SELECT * FROM (
575 SELECT
576 replica_id, cluster_id, started_at, finished_at, object_count,
577 peak_memory_bytes, peak_disk_bytes, status, process_id
578 FROM mz_internal.mz_replica_hydration_history
579 WHERE finished_at < TIMESTAMPTZ '{cutoff}'
580 ORDER BY finished_at
581 LIMIT {RETENTION_BATCH_SIZE}
582 )"
583 )
584}
585
586struct Sweep {
588 client: PeekClient,
589 catalog: Arc<Catalog>,
590 object_history_id: CatalogItemId,
591 replica_history_id: CatalogItemId,
592 metrics: Metrics,
593 wall_time: chrono::DateTime<chrono::Utc>,
594 object_cutoff: String,
599 replica_cutoff: String,
600}
601
602impl Sweep {
603 async fn collect(&mut self, target: ReplicaTarget) {
605 let ReplicaTarget {
606 cluster_id,
607 replica_id,
608 ..
609 } = target;
610 let sql = object_collection_sql(cluster_id, replica_id, &self.object_cutoff);
611 let _ = self
612 .run(
613 "collection",
614 self.object_history_id,
615 cluster_id,
616 replica_id,
617 MutationKind::Insert,
618 &sql,
619 )
620 .await;
621
622 let sql = replica_collection_sql(target, &self.replica_cutoff);
623 let _ = self
624 .run(
625 "replica_collection",
626 self.replica_history_id,
627 cluster_id,
628 replica_id,
629 MutationKind::Insert,
630 &sql,
631 )
632 .await;
633 }
634
635 async fn retain(&mut self, cluster_id: ClusterId, replica_id: ReplicaId) {
637 let sql = object_retention_sql(&self.object_cutoff);
638 if let Some(deleted) = self
639 .run(
640 "retention",
641 self.object_history_id,
642 cluster_id,
643 replica_id,
644 MutationKind::Delete,
645 &sql,
646 )
647 .await
648 && deleted == RETENTION_BATCH_SIZE
649 {
650 self.metrics.hydration_history_retention_batch_full.inc();
651 }
652
653 let sql = replica_retention_sql(&self.replica_cutoff);
654 if let Some(deleted) = self
655 .run(
656 "replica_retention",
657 self.replica_history_id,
658 cluster_id,
659 replica_id,
660 MutationKind::Delete,
661 &sql,
662 )
663 .await
664 && deleted == RETENTION_BATCH_SIZE
665 {
666 self.metrics.hydration_history_retention_batch_full.inc();
667 }
668 }
669
670 async fn run(
681 &mut self,
682 step: &'static str,
683 history_id: CatalogItemId,
684 cluster_id: ClusterId,
685 replica_id: ReplicaId,
686 kind: MutationKind,
687 sql: &str,
688 ) -> Option<usize> {
689 let mutation = async {
690 let plan = plan_mutation(&self.catalog, history_id, kind, sql)?;
691 let mut session = Session::dummy();
692 session.start_transaction_single_stmt(self.wall_time);
693 let response = self
694 .client
695 .background_read_then_write(
696 &mut session,
697 plan,
698 cluster_id,
699 replica_id,
700 &self.catalog,
701 )
702 .await?;
703 Ok::<_, AdapterError>(response)
704 };
705 match tokio::time::timeout(MUTATION_TIMEOUT, mutation).await {
706 Ok(Ok(response)) => {
707 let (rows, action) = match (kind, response) {
708 (MutationKind::Insert, ExecuteResponse::Inserted(rows)) => (rows, "appended"),
709 (MutationKind::Update, ExecuteResponse::Updated(rows)) => (rows, "updated"),
710 (MutationKind::Delete, ExecuteResponse::Deleted(rows)) => (rows, "deleted"),
711 (_, response) => {
712 self.observe_mutation(step, "error");
713 mz_ore::soft_panic_or_log!(
714 "hydration history {step} returned an unexpected response: {response:?}"
715 );
716 return None;
717 }
718 };
719 self.metrics
720 .hydration_history_rows_affected
721 .with_label_values(&[action])
722 .inc_by(u64::cast_from(rows));
723 let outcome = if rows == 0 { "noop" } else { "success" };
724 self.observe_mutation(step, outcome);
725 Some(rows)
726 }
727 Ok(Err(error)) => {
728 self.observe_mutation(step, "error");
729 if step.ends_with("collection")
730 && matches!(&error, AdapterError::ReadThenWriteContention)
731 {
732 warn!(
733 %step, %cluster_id, %replica_id, %error,
734 "hydration history step failed, the replica's introspection frontier \
735 may be trailing the write frontier"
736 );
737 } else {
738 warn!(%step, %cluster_id, %replica_id, %error, "hydration history step failed");
739 }
740 None
741 }
742 Err(_) if step.ends_with("collection") => {
746 self.observe_mutation(step, "timeout");
747 warn!(
748 %step, %cluster_id, %replica_id,
749 "hydration history step timed out, \
750 the replica's introspection frontier may be trailing the write frontier"
751 );
752 None
753 }
754 Err(_) => {
755 self.observe_mutation(step, "timeout");
756 warn!(%step, %cluster_id, %replica_id, "hydration history step timed out");
757 None
758 }
759 }
760 }
761
762 fn observe_mutation(&self, operation: &str, outcome: &str) {
763 self.metrics
764 .hydration_history_mutations
765 .with_label_values(&[operation, outcome])
766 .inc();
767 }
768}
769
770fn plan_mutation(
780 catalog: &Arc<Catalog>,
781 target_id: CatalogItemId,
782 kind: MutationKind,
783 sql: &str,
784) -> Result<ReadThenWritePlan, AdapterError> {
785 let session_catalog = catalog.for_system_session();
786 let parsed = mz_sql::parse::parse(sql)
787 .map_err(AdapterError::from)?
788 .into_element();
789 let (stmt, resolved_ids) = mz_sql::names::resolve(&session_catalog, parsed.ast)?;
790 let (plan, _) = mz_sql::plan::plan(
791 None,
792 &session_catalog,
793 stmt,
794 &Params::empty(),
795 &resolved_ids,
796 )?;
797 let Plan::Select(select) = plan else {
798 return Err(AdapterError::Internal(
799 "hydration history query did not plan as SELECT".into(),
800 ));
801 };
802
803 let target_desc = catalog
804 .get_entry(&target_id)
805 .relation_desc_latest()
806 .expect("hydration history target is a table");
807 let selection_types = select.source.typ(&[], &BTreeMap::new()).column_types;
808 let target_types = &target_desc.typ().column_types;
809 let matches = selection_types.len() == target_types.len()
810 && selection_types
811 .iter()
812 .zip_eq(target_types)
813 .all(|(selected, target)| selected.scalar_type == target.scalar_type);
816 if !matches {
817 return Err(AdapterError::Internal(format!(
818 "hydration history query does not match the target table: \
819 selection {selection_types:?}, table {target_types:?}"
820 )));
821 }
822
823 Ok(ReadThenWritePlan {
824 id: target_id,
825 selection: select.source,
826 finishing: select.finishing,
827 assignments: BTreeMap::new(),
828 kind,
829 returning: Vec::new(),
830 })
831}
832
833#[cfg(test)]
834mod tests {
835 use super::*;
836
837 #[mz_ore::test]
838 fn replica_sweep_advances_and_wraps() {
839 let cluster = ClusterId::user(1).expect("valid cluster ID");
840 let replicas = [
841 ReplicaTarget {
842 cluster_id: cluster,
843 replica_id: ReplicaId::User(1),
844 process_count: 1,
845 },
846 ReplicaTarget {
847 cluster_id: cluster,
848 replica_id: ReplicaId::User(3),
849 process_count: 1,
850 },
851 ];
852
853 assert_eq!(next_replica(&replicas, None), Some(replicas[0]));
854 assert_eq!(
855 next_replica(&replicas, Some(ReplicaId::User(1))),
856 Some(replicas[1])
857 );
858 assert_eq!(
859 next_replica(&replicas, Some(ReplicaId::User(3))),
860 Some(replicas[0])
861 );
862 assert_eq!(next_replica(&[], None), None);
863 }
864
865 #[mz_ore::test]
868 fn fire_delay_is_offset_within_the_interval() {
869 let interval = 60_000;
870
871 assert_eq!(
873 next_fire_delay(1_000, interval, 5_000),
874 Duration::from_millis(4_000)
875 );
876 assert_eq!(
878 next_fire_delay(5_000, interval, 5_000),
879 Duration::from_millis(interval)
880 );
881 assert_eq!(
883 next_fire_delay(6_000, interval, 5_000),
884 Duration::from_millis(59_000)
885 );
886 assert_eq!(
888 next_fire_delay(59_999, interval, 0),
889 Duration::from_millis(1)
890 );
891 assert_eq!(
892 next_fire_delay(60_000, interval, 0),
893 Duration::from_millis(interval)
894 );
895
896 let one = environment_schedule_offset(
898 "aws-us-east-1-00000000-0000-0000-0000-000000000000-0",
899 interval,
900 );
901 let two = environment_schedule_offset(
902 "aws-us-west-1-00000000-0000-0000-0000-000000000000-1",
903 interval,
904 );
905 assert_eq!(one, 30_189);
906 assert_eq!(two, 38_252);
907 assert_ne!(one, two);
908 }
909
910 #[mz_ore::test]
915 fn collect_requires_every_worker() {
916 let cutoff = "1970-01-01T00:00:00+00:00";
917 let sql = object_collection_sql(
918 ClusterId::user(1).expect("valid cluster ID"),
919 ReplicaId::User(2),
920 cutoff,
921 );
922 assert!(
923 sql.contains("HAVING count(*) = count(t.hydrated_at)"),
924 "{sql}"
925 );
926 assert!(sql.contains("max(t.hydrated_at)"), "{sql}");
927 assert!(!sql.contains("worker_id"), "{sql}");
928
929 let aggregate_end = sql.find(") AS e").expect("aggregate subquery");
932 assert!(sql.find(cutoff).expect("cutoff") > aggregate_end, "{sql}");
933 assert!(
934 sql.find("NOT EXISTS").expect("anti-join") > aggregate_end,
935 "{sql}"
936 );
937 }
938
939 #[mz_ore::test]
946 fn replica_collection_uses_latest_completed_interval_island() {
947 let sql = replica_collection_sql(
948 ReplicaTarget {
949 cluster_id: ClusterId::user(1).expect("valid cluster ID"),
950 replica_id: ReplicaId::User(2),
951 process_count: 3,
952 },
953 "1970-01-01T00:00:00+00:00",
954 );
955 let normalized_sql = sql.split_whitespace().collect::<Vec<_>>().join(" ");
956
957 assert!(sql.contains("ROWS UNBOUNDED PRECEDING"), "{sql}");
961 assert!(sql.contains("lag(covered_through)"), "{sql}");
962 assert!(
963 sql.contains("CASE WHEN starts_episode THEN installed_at END"),
964 "{sql}"
965 );
966 assert!(sql.contains("GROUP BY episode_started_at"), "{sql}");
967 assert!(!sql.contains("bool_and(hydrated)"), "{sql}");
972 assert!(
976 normalized_sql.contains("WHERE o.v IS NULL OR e.finished_at < o.v"),
977 "{sql}"
978 );
979 assert!(!sql.contains("o.installed_at <= e.finished_at"), "{sql}");
980 assert!(
981 normalized_sql.contains("ORDER BY e.started_at DESC LIMIT 1"),
982 "{sql}"
983 );
984 assert!(
985 sql.contains("(SELECT count(*) FROM resources) = 3::uint8"),
986 "{sql}"
987 );
988 assert!(sql.contains("WHERE t.export_id NOT LIKE 't%'"), "{sql}");
989 assert!(!sql.contains("WHERE t.export_id LIKE 'u%'"), "{sql}");
990 assert!(!sql.contains("mz_object_global_ids"), "{sql}");
991 assert!(!sql.contains("mz_catalog.mz_objects"), "{sql}");
992 assert!(
993 normalized_sql
994 .contains("max(value) FILTER ( WHERE source = 'cgroup' AND metric = 'memory_peak'"),
995 "{sql}"
996 );
997 assert!(
998 normalized_sql.contains(
999 "max(value) FILTER ( WHERE source = 'statvfs' AND metric = 'fs_used_peak'"
1000 ),
1001 "{sql}"
1002 );
1003 assert!(
1004 normalized_sql
1005 .contains("max(value) FILTER ( WHERE source = 'cgroup' AND metric = 'swap_peak'"),
1006 "{sql}"
1007 );
1008 assert!(!normalized_sql.contains("sum(value)"), "{sql}");
1009 assert!(
1010 sql.contains("FROM mz_internal.mz_replica_hydration_history"),
1011 "{sql}"
1012 );
1013 assert!(sql.contains("h.finished_at >= c.started_at"), "{sql}");
1014 }
1015}