Skip to main content

mz_adapter/coord/
timestamp_selection.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 selecting timestamps for various operations on collections.
11
12use std::fmt;
13use std::sync::Arc;
14
15use async_trait::async_trait;
16use chrono::{DateTime, Utc};
17use constraints::Constraints;
18use differential_dataflow::lattice::Lattice;
19use itertools::Itertools;
20use mz_compute_types::ComputeInstanceId;
21use mz_ore::cast::CastLossy;
22use mz_repr::{GlobalId, Timestamp, TimestampManipulation};
23use mz_sql::plan::QueryWhen;
24use mz_sql::session::vars::IsolationLevel;
25use mz_storage_types::sources::Timeline;
26use mz_timestamp_oracle::TimestampOracle;
27use serde::{Deserialize, Serialize};
28use timely::progress::{Antichain, Timestamp as _};
29
30use crate::AdapterError;
31use crate::catalog::CatalogState;
32use crate::coord::Coordinator;
33use crate::coord::id_bundle::CollectionIdBundle;
34use crate::coord::read_policy::ReadHolds;
35use crate::coord::timeline::TimelineContext;
36use crate::session::Session;
37
38/// The timeline and timestamp context of a read.
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40pub enum TimestampContext {
41    /// Read is executed in a specific timeline with a specific timestamp.
42    TimelineTimestamp {
43        timeline: Timeline,
44        /// The timestamp that was chosen for a read. This can differ from the
45        /// `oracle_ts` when collections are not readable at the (linearized)
46        /// timestamp for the oracle. In those cases (when the chosen timestamp
47        /// is further ahead than the oracle timestamp) we have to delay
48        /// returning peek results until the timestamp oracle is also
49        /// sufficiently advanced.
50        chosen_ts: Timestamp,
51        /// The timestamp that would have been chosen for the read by the
52        /// (linearized) timestamp oracle). In most cases this will be picked as
53        /// the `chosen_ts`.
54        oracle_ts: Option<Timestamp>,
55    },
56    /// Read is executed without a timeline or timestamp.
57    NoTimestamp,
58}
59
60impl TimestampContext {
61    /// Creates a `TimestampContext` from a timestamp and `TimelineContext`.
62    pub fn from_timeline_context(
63        chosen_ts: Timestamp,
64        oracle_ts: Option<Timestamp>,
65        transaction_timeline: Option<Timeline>,
66        timeline_context: &TimelineContext,
67    ) -> TimestampContext {
68        match timeline_context {
69            TimelineContext::TimelineDependent(timeline) => {
70                if let Some(transaction_timeline) = transaction_timeline {
71                    assert_eq!(timeline, &transaction_timeline);
72                }
73                Self::TimelineTimestamp {
74                    timeline: timeline.clone(),
75                    chosen_ts,
76                    oracle_ts,
77                }
78            }
79            TimelineContext::TimestampDependent => {
80                // We default to the `Timeline::EpochMilliseconds` timeline if one doesn't exist.
81                Self::TimelineTimestamp {
82                    timeline: transaction_timeline.unwrap_or(Timeline::EpochMilliseconds),
83                    chosen_ts,
84                    oracle_ts,
85                }
86            }
87            TimelineContext::TimestampIndependent => Self::NoTimestamp,
88        }
89    }
90
91    /// The timeline belonging to this context, if one exists.
92    pub fn timeline(&self) -> Option<&Timeline> {
93        self.timeline_timestamp().map(|tt| tt.0)
94    }
95
96    /// The timestamp belonging to this context, if one exists.
97    pub fn timestamp(&self) -> Option<&Timestamp> {
98        self.timeline_timestamp().map(|tt| tt.1)
99    }
100
101    /// The timeline and timestamp belonging to this context, if one exists.
102    pub fn timeline_timestamp(&self) -> Option<(&Timeline, &Timestamp)> {
103        match self {
104            Self::TimelineTimestamp {
105                timeline,
106                chosen_ts,
107                ..
108            } => Some((timeline, chosen_ts)),
109            Self::NoTimestamp => None,
110        }
111    }
112
113    /// The timestamp belonging to this context, or a sensible default if one does not exists.
114    pub fn timestamp_or_default(&self) -> Timestamp {
115        match self {
116            Self::TimelineTimestamp { chosen_ts, .. } => chosen_ts.clone(),
117            // Anything without a timestamp is given the maximum possible timestamp to indicate
118            // that they have been closed up until the end of time. This allows us to SUBSCRIBE to
119            // static views.
120            Self::NoTimestamp => Timestamp::maximum(),
121        }
122    }
123
124    /// Whether or not the context contains a timestamp.
125    pub fn contains_timestamp(&self) -> bool {
126        self.timestamp().is_some()
127    }
128
129    /// Converts this `TimestampContext` to an `Antichain`.
130    pub fn antichain(&self) -> Antichain<Timestamp> {
131        Antichain::from_elem(self.timestamp_or_default())
132    }
133}
134
135#[async_trait(?Send)]
136impl TimestampProvider for Coordinator {
137    /// Reports a collection's current read frontier.
138    fn compute_read_frontier(
139        &self,
140        instance: ComputeInstanceId,
141        id: GlobalId,
142    ) -> Antichain<Timestamp> {
143        self.controller
144            .compute
145            .collection_frontiers(id, Some(instance))
146            .expect("id does not exist")
147            .read_frontier
148    }
149
150    /// Reports a collection's current write frontier.
151    fn compute_write_frontier(
152        &self,
153        instance: ComputeInstanceId,
154        id: GlobalId,
155    ) -> Antichain<Timestamp> {
156        self.controller
157            .compute
158            .collection_frontiers(id, Some(instance))
159            .expect("id does not exist")
160            .write_frontier
161    }
162
163    fn storage_frontiers(
164        &self,
165        ids: Vec<GlobalId>,
166    ) -> Vec<(GlobalId, Antichain<Timestamp>, Antichain<Timestamp>)> {
167        self.controller
168            .storage
169            .collections_frontiers(ids)
170            .expect("missing collections")
171    }
172
173    fn acquire_read_holds(&self, id_bundle: &CollectionIdBundle) -> ReadHolds {
174        self.acquire_read_holds(id_bundle)
175    }
176
177    fn catalog_state(&self) -> &CatalogState {
178        self.catalog().state()
179    }
180}
181
182/// A timestamp determination, which includes the timestamp, constraints, and session oracle read
183/// timestamp.
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct RawTimestampDetermination {
186    pub timestamp: Timestamp,
187    pub constraints: Constraints,
188    pub session_oracle_read_ts: Option<Timestamp>,
189}
190
191#[async_trait(?Send)]
192pub trait TimestampProvider {
193    fn compute_read_frontier(
194        &self,
195        instance: ComputeInstanceId,
196        id: GlobalId,
197    ) -> Antichain<Timestamp>;
198    fn compute_write_frontier(
199        &self,
200        instance: ComputeInstanceId,
201        id: GlobalId,
202    ) -> Antichain<Timestamp>;
203
204    /// Returns the implied capability (since) and write frontier (upper) for
205    /// the specified storage collections.
206    fn storage_frontiers(
207        &self,
208        ids: Vec<GlobalId>,
209    ) -> Vec<(GlobalId, Antichain<Timestamp>, Antichain<Timestamp>)>;
210
211    fn catalog_state(&self) -> &CatalogState;
212
213    fn get_timeline(timeline_context: &TimelineContext) -> Option<Timeline> {
214        let timeline = match timeline_context {
215            TimelineContext::TimelineDependent(timeline) => Some(timeline.clone()),
216            // We default to the `Timeline::EpochMilliseconds` timeline if one doesn't exist.
217            TimelineContext::TimestampDependent => Some(Timeline::EpochMilliseconds),
218            TimelineContext::TimestampIndependent => None,
219        };
220
221        timeline
222    }
223
224    /// Returns true if-and-only-if the given configuration needs a linearized
225    /// read timestamp from a timestamp oracle.
226    ///
227    /// This assumes that the query happens in the context of a timeline. If
228    /// there is no timeline, we cannot and don't have to get a linearized read
229    /// timestamp.
230    fn needs_linearized_read_ts(isolation_level: &IsolationLevel, when: &QueryWhen) -> bool {
231        // When we're in the context of a timeline (assumption) and one of these
232        // scenarios hold, we need to use a linearized read timestamp:
233        // - The isolation level anchors against the oracle (Strict Serializable,
234        //   Strong Session Serializable, Bounded Staleness) and the `when` allows
235        //   us to use the timestamp oracle (ex: queries with no AS OF).
236        // - The `when` requires us to use the timestamp oracle (ex: read-then-write
237        //   queries).
238        when.must_advance_to_timeline_ts()
239            || (when.can_advance_to_timeline_ts()
240                && matches!(
241                    isolation_level,
242                    IsolationLevel::StrictSerializable
243                        | IsolationLevel::StrongSessionSerializable
244                        | IsolationLevel::BoundedStaleness(_)
245                ))
246    }
247
248    /// Uses constraints and preferences to determine a timestamp for a query.
249    /// Returns the determined timestamp, the constraints that were applied, and
250    /// session_oracle_read_ts.
251    fn determine_timestamp_via_constraints(
252        session: &Session,
253        read_holds: &ReadHolds,
254        id_bundle: &CollectionIdBundle,
255        when: &QueryWhen,
256        oracle_read_ts: Option<Timestamp>,
257        real_time_recency_ts: Option<Timestamp>,
258        isolation_level: &IsolationLevel,
259        timeline: &Option<Timeline>,
260        largest_not_in_advance_of_upper: Timestamp,
261    ) -> Result<RawTimestampDetermination, AdapterError> {
262        use constraints::{Constraints, Preference, Reason};
263
264        let mut session_oracle_read_ts = None;
265        // We start by establishing the hard constraints that must be applied to timestamp determination.
266        // These constraints are derived from the input arguments, and properties of the collections involved.
267        // TODO: Many of the constraints are expressed obliquely, and could be made more direct.
268        let constraints = {
269            // Constraints we will populate through a sequence of opinions.
270            let mut constraints = Constraints::default();
271
272            // First, we have validity constraints from the `id_bundle` argument which indicates
273            // which collections we are reading from.
274            // TODO: Refine the detail about which identifiers are binding and which are not.
275            // TODO(dov): It's not entirely clear to me that there ever would be a non
276            // binding constraint introduced by the `id_bundle`. We should revisit this.
277            let since = read_holds.least_valid_read();
278            let storage = id_bundle
279                .storage_ids
280                .iter()
281                .cloned()
282                .collect::<Vec<GlobalId>>();
283            if !storage.is_empty() {
284                constraints
285                    .lower
286                    .push((since.clone(), Reason::StorageInput(storage)));
287            }
288            let compute = id_bundle
289                .compute_ids
290                .iter()
291                .flat_map(|(key, ids)| ids.iter().map(|id| (*key, *id)))
292                .collect::<Vec<(ComputeInstanceId, GlobalId)>>();
293            if !compute.is_empty() {
294                constraints
295                    .lower
296                    .push((since.clone(), Reason::ComputeInput(compute)));
297            }
298
299            // The query's `when` may indicates a specific timestamp we must advance to, or a specific value we must use.
300            if let Some(ts) = when.advance_to_timestamp() {
301                constraints
302                    .lower
303                    .push((Antichain::from_elem(ts), Reason::QueryAsOf));
304                // If the query is at a specific timestamp, we must introduce an upper bound as well.
305                if when.constrains_upper() {
306                    constraints
307                        .upper
308                        .push((Antichain::from_elem(ts), Reason::QueryAsOf));
309                }
310            }
311
312            // The specification of an `oracle_read_ts` may indicates that we must advance to it,
313            // except in some isolation modes, or if `when` does not indicate that we should.
314            // At the moment, only `QueryWhen::FreshestTableWrite` indicates that we should.
315            // TODO: Should this just depend on the isolation level?
316            if let Some(timestamp) = &oracle_read_ts {
317                // Whether this isolation level treats `oracle_read_ts` as a hard
318                // lower bound. Strong session serializable (session-local oracle)
319                // and bounded staleness (`oracle - D` anchor) instead consult it
320                // below; pushing it here would shadow those semantics.
321                let hard_lower_bound = match isolation_level {
322                    IsolationLevel::StrongSessionSerializable
323                    | IsolationLevel::BoundedStaleness(_) => false,
324                    IsolationLevel::ReadUncommitted
325                    | IsolationLevel::ReadCommitted
326                    | IsolationLevel::RepeatableRead
327                    | IsolationLevel::Serializable
328                    | IsolationLevel::StrictSerializable => true,
329                };
330                // `must_advance_to_timeline_ts()` (only `FreshestTableWrite`)
331                // forces the bound regardless; bounded staleness rejects writes
332                // upstream, so that path is unreachable for it in practice.
333                if hard_lower_bound || when.must_advance_to_timeline_ts() {
334                    constraints.lower.push((
335                        Antichain::from_elem(*timestamp),
336                        Reason::IsolationLevel(*isolation_level),
337                    ));
338                }
339            }
340
341            // If a real time recency timestamp is supplied, we must advance to it.
342            if let Some(real_time_recency_ts) = real_time_recency_ts {
343                assert!(
344                    session.vars().real_time_recency()
345                        && isolation_level == &IsolationLevel::StrictSerializable,
346                    "real time recency timestamp should only be supplied when real time recency \
347                                is enabled and the isolation level is strict serializable"
348                );
349                constraints.lower.push((
350                    Antichain::from_elem(real_time_recency_ts),
351                    Reason::RealTimeRecency,
352                ));
353            }
354
355            // Bounded staleness anchors the freshness floor at `oracle.read_ts - D`
356            // and clamps the upper at `largest_not_in_advance_of_upper`. The upper
357            // clamp is what makes an infeasible bound fail fast in the coordinator
358            // instead of blocking on compute for the upper to advance — which would
359            // make the failure mode cluster-shape-dependent. `oracle_read_ts` is
360            // `None` under `AS OF`, where the user-chosen `T` is the only constraint.
361            if let IsolationLevel::BoundedStaleness(d) = isolation_level {
362                if let Some(anchor) = oracle_read_ts {
363                    let bound_ms = u64::try_from(d.as_millis()).unwrap_or(u64::MAX);
364                    let lower = anchor.saturating_sub(bound_ms);
365                    constraints.lower.push((
366                        Antichain::from_elem(lower),
367                        Reason::IsolationLevel(*isolation_level),
368                    ));
369                    constraints.upper.push((
370                        Antichain::from_elem(largest_not_in_advance_of_upper),
371                        Reason::IsolationLevel(*isolation_level),
372                    ));
373                }
374            }
375
376            // If we are operating in Strong Session Serializable, we use an alternate timestamp lower bound.
377            if isolation_level == &IsolationLevel::StrongSessionSerializable {
378                if let Some(timeline) = &timeline {
379                    if let Some(oracle) = session.get_timestamp_oracle(timeline) {
380                        let session_ts = oracle.read_ts();
381                        constraints.lower.push((
382                            Antichain::from_elem(session_ts),
383                            Reason::IsolationLevel(*isolation_level),
384                        ));
385                        session_oracle_read_ts = Some(session_ts);
386                    }
387
388                    // When advancing the read timestamp under Strong Session Serializable, there is a
389                    // trade-off to make between freshness and latency. We can choose a timestamp close the
390                    // `upper`, but then later queries might block if the `upper` is too far into the
391                    // future. We can chose a timestamp close to the current time, but then we may not be
392                    // getting results that are as fresh as possible. As a heuristic, we choose the minimum
393                    // of now and the upper, where we use the global timestamp oracle read timestamp as a
394                    // proxy for now. If upper > now, then we choose now and prevent blocking future
395                    // queries. If upper < now, then we choose the upper and prevent blocking the current
396                    // query.
397                    if when.can_advance_to_upper() && when.can_advance_to_timeline_ts() {
398                        let mut advance_to = largest_not_in_advance_of_upper;
399                        if let Some(oracle_read_ts) = oracle_read_ts {
400                            advance_to = std::cmp::min(advance_to, oracle_read_ts);
401                        }
402                        constraints.lower.push((
403                            Antichain::from_elem(advance_to),
404                            Reason::IsolationLevel(*isolation_level),
405                        ));
406                    }
407                }
408            }
409
410            constraints.minimize();
411            constraints
412        };
413
414        // Next we establish the preferences that we would like to apply to timestamp determination.
415        // Generally, we want to choose the freshest timestamp possible, although there are exceptions
416        // when we either want a maximally *stale* timestamp, or we want to protect other queries from
417        // a recklessly advanced timestamp.
418        let preferences = {
419            // Counter-intuitively, the only `when` that allows `can_advance_to_upper` is `Immediately`,
420            // and not `FreshestTableWrite`. This is because `FreshestTableWrite` instead imposes a lower
421            // bound through the `oracle_read_ts`, and then requires the stalest valid timestamp.
422
423            if when.can_advance_to_upper()
424                && (isolation_level == &IsolationLevel::Serializable
425                    || matches!(isolation_level, IsolationLevel::BoundedStaleness(_))
426                    || timeline.is_none())
427            {
428                Preference::FreshestAvailable
429            } else {
430                Preference::StalestValid
431            }
432
433            // TODO: `StrongSessionSerializable` has a different set of preferences that starts to tease
434            // out the trade-off between freshness and responsiveness. I think we don't yet know enough
435            // to properly frame these preferences, though they are clearly aimed at the right concerns.
436        };
437
438        // Determine a candidate based on constraints and preferences.
439        let constraint_candidate = {
440            let mut candidate = Timestamp::minimum();
441            // Note: These `advance_by` calls are no-ops if the given frontier is `[]`.
442            candidate.advance_by(constraints.lower_bound().borrow());
443            // If we have a preference to be the freshest available, advance to the minimum
444            // of the upper bound constraints and the `largest_not_in_advance_of_upper`.
445            if let Preference::FreshestAvailable = preferences {
446                let mut upper_bound = constraints.upper_bound();
447                upper_bound.insert(largest_not_in_advance_of_upper);
448                candidate.advance_by(upper_bound.borrow());
449            }
450            // If the candidate is strictly outside the constraints, we didn't have a viable
451            // timestamp. This can happen e.g. when the query has AS OF, or when the lower bound is
452            // `[]`.
453            if !constraints.lower_bound().less_equal(&candidate)
454                || constraints.upper_bound().less_than(&candidate)
455            {
456                // Bounded staleness wants a specific error describing the staleness
457                // gap. Derive it directly from `anchor - D` and the inputs' upper,
458                // not from `constraints.{lower,upper}_bound()` — those join *all*
459                // reasons (`since`, `AS OF`, …), so a non-bs constraint could
460                // dominate and the reported gap would not describe the bs failure.
461                // A zero gap means the bs floor was satisfiable and something else
462                // caused the infeasibility; fall through to the generic error.
463                // `oracle_read_ts` is unset under `AS OF`, where bs added no
464                // constraint, so we cannot reach here for a bs-specific failure.
465                if let IsolationLevel::BoundedStaleness(d) = isolation_level {
466                    if let Some(anchor) = oracle_read_ts {
467                        let bound_ms = u64::try_from(d.as_millis()).unwrap_or(u64::MAX);
468                        let bs_lower: u64 = anchor.saturating_sub(bound_ms).into();
469                        let upper: u64 = largest_not_in_advance_of_upper.into();
470                        let gap = bs_lower.saturating_sub(upper);
471                        if gap > 0 {
472                            return Err(AdapterError::BoundedStalenessExceeded {
473                                bound: *d,
474                                gap_ms: gap,
475                                slowest_input: None,
476                            });
477                        }
478                    }
479                }
480                return Err(AdapterError::ImpossibleTimestampConstraints {
481                    constraints: constraints.display(timeline.as_ref()).to_string(),
482                });
483            } else {
484                candidate
485            }
486        };
487
488        Ok(RawTimestampDetermination {
489            timestamp: constraint_candidate,
490            constraints,
491            session_oracle_read_ts,
492        })
493    }
494
495    /// Determines the timestamp for a query.
496    ///
497    /// Timestamp determination may fail due to the restricted validity of
498    /// traces. Each has a `since` and `upper` frontier, and are only valid
499    /// after `since` and sure to be available not after `upper`.
500    ///
501    /// The timeline that `id_bundle` belongs to is also returned, if one exists.
502    fn determine_timestamp_for(
503        &self,
504        session: &Session,
505        id_bundle: &CollectionIdBundle,
506        when: &QueryWhen,
507        timeline_context: &TimelineContext,
508        oracle_read_ts: Option<Timestamp>,
509        real_time_recency_ts: Option<Timestamp>,
510        isolation_level: &IsolationLevel,
511    ) -> Result<(TimestampDetermination, ReadHolds), AdapterError> {
512        // First, we acquire read holds that will ensure the queried collections
513        // stay queryable at the chosen timestamp.
514        let read_holds = self.acquire_read_holds(id_bundle);
515
516        let upper = self.least_valid_write(id_bundle);
517
518        Self::determine_timestamp_for_inner(
519            session,
520            id_bundle,
521            when,
522            timeline_context,
523            oracle_read_ts,
524            real_time_recency_ts,
525            isolation_level,
526            read_holds,
527            upper,
528        )
529    }
530
531    /// Same as determine_timestamp_for, but read_holds and least_valid_write are already passed in.
532    fn determine_timestamp_for_inner(
533        session: &Session,
534        id_bundle: &CollectionIdBundle,
535        when: &QueryWhen,
536        timeline_context: &TimelineContext,
537        oracle_read_ts: Option<Timestamp>,
538        real_time_recency_ts: Option<Timestamp>,
539        isolation_level: &IsolationLevel,
540        read_holds: ReadHolds,
541        upper: Antichain<Timestamp>,
542    ) -> Result<(TimestampDetermination, ReadHolds), AdapterError> {
543        let timeline = Self::get_timeline(timeline_context);
544        let largest_not_in_advance_of_upper = Coordinator::largest_not_in_advance_of_upper(&upper);
545        let since = read_holds.least_valid_read();
546
547        // If the `since` is empty, then timestamp determination would fail. Let's return a more
548        // specific error in this case: Empty `since` frontiers happen here when collections were
549        // dropped concurrently with sequencing the query.
550        if since.is_empty() {
551            // Figure out what made the since frontier empty.
552            let mut unreadable_collections = Vec::new();
553            for (coll_id, hold) in read_holds.storage_holds {
554                if hold.since().is_empty() {
555                    unreadable_collections.push(coll_id);
556                }
557            }
558            for ((_instance_id, coll_id), hold) in read_holds.compute_holds {
559                if hold.since().is_empty() {
560                    unreadable_collections.push(coll_id);
561                }
562            }
563            return Err(AdapterError::CollectionUnreadable {
564                id: unreadable_collections.into_iter().join(", "),
565            });
566        }
567
568        // Bounded staleness freshness math assumes the EpochMilliseconds timeline,
569        // where timestamps are wall-clock milliseconds. Timeline-less queries
570        // (`TimestampIndependent`, e.g. constant queries) are fine — the freshness
571        // contract is vacuous for them.
572        if isolation_level.is_bounded_staleness()
573            && matches!(&timeline, Some(t) if *t != Timeline::EpochMilliseconds)
574        {
575            return Err(AdapterError::BoundedStalenessTimelineUnsupported);
576        }
577
578        let raw_determination = Self::determine_timestamp_via_constraints(
579            session,
580            &read_holds,
581            id_bundle,
582            when,
583            oracle_read_ts,
584            real_time_recency_ts,
585            isolation_level,
586            &timeline,
587            largest_not_in_advance_of_upper,
588        )?;
589
590        let timestamp_context = TimestampContext::from_timeline_context(
591            raw_determination.timestamp,
592            oracle_read_ts,
593            timeline,
594            timeline_context,
595        );
596
597        let determination = TimestampDetermination {
598            timestamp_context,
599            since,
600            upper,
601            largest_not_in_advance_of_upper,
602            oracle_read_ts,
603            session_oracle_read_ts: raw_determination.session_oracle_read_ts,
604            real_time_recency_ts,
605            constraints: raw_determination.constraints,
606        };
607
608        Ok((determination, read_holds))
609    }
610
611    /// Acquires [ReadHolds], for the given `id_bundle` at the earliest possible
612    /// times.
613    fn acquire_read_holds(&self, id_bundle: &CollectionIdBundle) -> ReadHolds;
614
615    /// The smallest common valid write frontier among the specified collections.
616    ///
617    /// Times that are not greater or equal to this frontier are complete for all collections
618    /// identified as arguments.
619    fn least_valid_write(&self, id_bundle: &CollectionIdBundle) -> Antichain<mz_repr::Timestamp> {
620        let mut upper = Antichain::new();
621        {
622            for (_id, _since, collection_upper) in
623                self.storage_frontiers(id_bundle.storage_ids.iter().cloned().collect_vec())
624            {
625                upper.extend(collection_upper);
626            }
627        }
628        {
629            for (instance, compute_ids) in &id_bundle.compute_ids {
630                for id in compute_ids.iter() {
631                    upper.extend(self.compute_write_frontier(*instance, *id));
632                }
633            }
634        }
635        upper
636    }
637
638    /// Returns `least_valid_write` - 1, i.e., each time in `least_valid_write` stepped back in a
639    /// saturating way.
640    fn greatest_available_read(&self, id_bundle: &CollectionIdBundle) -> Antichain<Timestamp> {
641        let mut frontier = Antichain::new();
642        for t in self.least_valid_write(id_bundle) {
643            frontier.insert(t.step_back().unwrap_or(t));
644        }
645        frontier
646    }
647}
648
649impl Coordinator {
650    /// Returns the timestamp oracle to obtain a linearized read timestamp from,
651    /// if the given isolation level and `when` require one, and `None`
652    /// otherwise.
653    ///
654    /// The caller must perform the `read_ts()` round-trip off the coordinator
655    /// loop, in a spawned task. The oracle backing store can be slow, so doing
656    /// the read inline would wedge every other session until it returns. See
657    /// `peek_linearize_timestamp` for the canonical use of this helper.
658    pub(crate) fn linearized_read_ts_oracle(
659        &self,
660        session: &Session,
661        timeline_ctx: &TimelineContext,
662        when: &QueryWhen,
663    ) -> Option<Arc<dyn TimestampOracle<Timestamp> + Send + Sync>> {
664        let isolation_level = session.vars().transaction_isolation();
665        let timeline = Coordinator::get_timeline(timeline_ctx);
666        let needs_linearized_read_ts = Coordinator::needs_linearized_read_ts(isolation_level, when);
667
668        match timeline {
669            Some(timeline) if needs_linearized_read_ts => {
670                Some(self.get_timestamp_oracle(&timeline))
671            }
672            Some(_) | None => None,
673        }
674    }
675
676    /// Determines the timestamp for a query, acquires read holds that ensure the
677    /// query remains executable at that time, and returns those.
678    /// The caller is responsible for eventually dropping those read holds.
679    #[mz_ore::instrument(level = "debug")]
680    pub(crate) fn determine_timestamp(
681        &self,
682        session: &Session,
683        id_bundle: &CollectionIdBundle,
684        when: &QueryWhen,
685        compute_instance: ComputeInstanceId,
686        timeline_context: &TimelineContext,
687        oracle_read_ts: Option<Timestamp>,
688        real_time_recency_ts: Option<mz_repr::Timestamp>,
689    ) -> Result<(TimestampDetermination, ReadHolds), AdapterError> {
690        let isolation_level = session.vars().transaction_isolation();
691        let (det, read_holds) = self.determine_timestamp_for(
692            session,
693            id_bundle,
694            when,
695            timeline_context,
696            oracle_read_ts,
697            real_time_recency_ts,
698            isolation_level,
699        )?;
700        self.metrics
701            .determine_timestamp
702            .with_label_values(&[
703                match det.respond_immediately() {
704                    true => "true",
705                    false => "false",
706                },
707                isolation_level.as_variant_str(),
708                &compute_instance.to_string(),
709            ])
710            .inc();
711        if !det.respond_immediately()
712            && isolation_level == &IsolationLevel::StrictSerializable
713            && real_time_recency_ts.is_none()
714        {
715            // Note down the difference between StrictSerializable and Serializable into a metric.
716            if let Some(strict) = det.timestamp_context.timestamp() {
717                let (serializable_det, _tmp_read_holds) = self.determine_timestamp_for(
718                    session,
719                    id_bundle,
720                    when,
721                    timeline_context,
722                    oracle_read_ts,
723                    real_time_recency_ts,
724                    &IsolationLevel::Serializable,
725                )?;
726
727                if let Some(serializable) = serializable_det.timestamp_context.timestamp() {
728                    self.metrics
729                        .timestamp_difference_for_strict_serializable_ms
730                        .with_label_values(&[compute_instance.to_string().as_str()])
731                        .observe(f64::cast_lossy(u64::from(
732                            strict.saturating_sub(*serializable),
733                        )));
734                }
735            }
736        }
737        if !det.respond_immediately()
738            && isolation_level.is_bounded_staleness()
739            && real_time_recency_ts.is_none()
740        {
741            // Note down the difference between BoundedStaleness and Serializable into a metric.
742            if let Some(bs_ts) = det.timestamp_context.timestamp() {
743                let (serializable_det, _tmp_read_holds) = self.determine_timestamp_for(
744                    session,
745                    id_bundle,
746                    when,
747                    timeline_context,
748                    oracle_read_ts,
749                    real_time_recency_ts,
750                    &IsolationLevel::Serializable,
751                )?;
752                if let Some(serializable) = serializable_det.timestamp_context.timestamp() {
753                    self.metrics
754                        .timestamp_difference_for_bounded_staleness_ms
755                        .with_label_values(&[compute_instance.to_string().as_str()])
756                        .observe(f64::cast_lossy(u64::from(
757                            serializable.saturating_sub(*bs_ts),
758                        )));
759                }
760            }
761        }
762        Ok((det, read_holds))
763    }
764
765    /// The largest timestamp not greater or equal to an element of `upper`.
766    ///
767    /// If no such timestamp exists, for example because `upper` contains only the
768    /// minimal timestamp, the return value is `Timestamp::minimum()`.
769    pub(crate) fn largest_not_in_advance_of_upper(
770        upper: &Antichain<mz_repr::Timestamp>,
771    ) -> mz_repr::Timestamp {
772        // We peek at the largest element not in advance of `upper`, which
773        // involves a subtraction. If `upper` contains a zero timestamp there
774        // is no "prior" answer, and we do not want to peek at it as it risks
775        // hanging awaiting the response to data that may never arrive.
776        if let Some(upper) = upper.as_option() {
777            upper.step_back().unwrap_or_else(Timestamp::minimum)
778        } else {
779            // A complete trace can be read in its final form with this time.
780            //
781            // This should only happen for literals that have no sources or sources that
782            // are known to have completed (non-tailed files for example).
783            Timestamp::MAX
784        }
785    }
786}
787
788/// Information used when determining the timestamp for a query.
789#[derive(Serialize, Deserialize, Debug, Clone)]
790pub struct TimestampDetermination {
791    /// The chosen timestamp context from `determine_timestamp`.
792    pub timestamp_context: TimestampContext,
793    /// The read frontier of all involved sources.
794    pub since: Antichain<Timestamp>,
795    /// The write frontier of all involved sources.
796    pub upper: Antichain<Timestamp>,
797    /// The largest timestamp not in advance of upper.
798    pub largest_not_in_advance_of_upper: Timestamp,
799    /// The value of the timeline's oracle timestamp, if used.
800    pub oracle_read_ts: Option<Timestamp>,
801    /// The value of the session local timestamp's oracle timestamp, if used.
802    pub session_oracle_read_ts: Option<Timestamp>,
803    /// The value of the real time recency timestamp, if used.
804    pub real_time_recency_ts: Option<Timestamp>,
805    /// The constraints used by the constraint based solver.
806    /// See the [`constraints`] module for more information.
807    pub constraints: Constraints,
808}
809
810impl TimestampDetermination {
811    pub fn respond_immediately(&self) -> bool {
812        match &self.timestamp_context {
813            TimestampContext::TimelineTimestamp { chosen_ts, .. } => {
814                !self.upper.less_equal(chosen_ts)
815            }
816            TimestampContext::NoTimestamp => true,
817        }
818    }
819}
820
821/// Information used when determining the timestamp for a query.
822#[derive(Clone, Debug, Serialize, Deserialize)]
823pub struct TimestampExplanation {
824    /// The chosen timestamp from `determine_timestamp`.
825    pub determination: TimestampDetermination,
826    /// Details about each source.
827    pub sources: Vec<TimestampSource>,
828    /// Wall time of first statement executed in this transaction
829    pub session_wall_time: DateTime<Utc>,
830    /// Cached value of determination.respond_immediately()
831    pub respond_immediately: bool,
832}
833
834#[derive(Clone, Debug, Serialize, Deserialize)]
835pub struct TimestampSource {
836    pub name: String,
837    pub read_frontier: Vec<Timestamp>,
838    pub write_frontier: Vec<Timestamp>,
839}
840
841pub trait DisplayableInTimeline {
842    fn fmt(&self, timeline: Option<&Timeline>, f: &mut fmt::Formatter) -> fmt::Result;
843    fn display<'a>(&'a self, timeline: Option<&'a Timeline>) -> DisplayInTimeline<'a, Self> {
844        DisplayInTimeline { t: self, timeline }
845    }
846}
847
848impl DisplayableInTimeline for mz_repr::Timestamp {
849    fn fmt(&self, timeline: Option<&Timeline>, f: &mut fmt::Formatter) -> fmt::Result {
850        if let Some(Timeline::EpochMilliseconds) = timeline {
851            let ts_ms: u64 = self.into();
852            if let Ok(ts_ms) = i64::try_from(ts_ms) {
853                if let Some(ndt) = DateTime::from_timestamp_millis(ts_ms) {
854                    return write!(f, "{:13} ({})", self, ndt.format("%Y-%m-%d %H:%M:%S%.3f"));
855                }
856            }
857        }
858        write!(f, "{:13}", self)
859    }
860}
861
862pub struct DisplayInTimeline<'a, T: ?Sized> {
863    t: &'a T,
864    timeline: Option<&'a Timeline>,
865}
866impl<'a, T> fmt::Display for DisplayInTimeline<'a, T>
867where
868    T: DisplayableInTimeline,
869{
870    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
871        self.t.fmt(self.timeline, f)
872    }
873}
874
875impl<'a, T> fmt::Debug for DisplayInTimeline<'a, T>
876where
877    T: DisplayableInTimeline,
878{
879    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
880        fmt::Display::fmt(&self, f)
881    }
882}
883
884impl fmt::Display for TimestampExplanation {
885    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
886        let timeline = self.determination.timestamp_context.timeline();
887        writeln!(
888            f,
889            "                query timestamp: {}",
890            self.determination
891                .timestamp_context
892                .timestamp_or_default()
893                .display(timeline)
894        )?;
895        if let Some(oracle_read_ts) = &self.determination.oracle_read_ts {
896            writeln!(
897                f,
898                "          oracle read timestamp: {}",
899                oracle_read_ts.display(timeline)
900            )?;
901        }
902        if let Some(session_oracle_read_ts) = &self.determination.session_oracle_read_ts {
903            writeln!(
904                f,
905                "  session oracle read timestamp: {}",
906                session_oracle_read_ts.display(timeline)
907            )?;
908        }
909        if let Some(real_time_recency_ts) = &self.determination.real_time_recency_ts {
910            writeln!(
911                f,
912                "    real time recency timestamp: {}",
913                real_time_recency_ts.display(timeline)
914            )?;
915        }
916        writeln!(
917            f,
918            "largest not in advance of upper: {}",
919            self.determination
920                .largest_not_in_advance_of_upper
921                .display(timeline),
922        )?;
923        writeln!(
924            f,
925            "                          upper:{:?}",
926            self.determination
927                .upper
928                .iter()
929                .map(|t| t.display(timeline))
930                .collect::<Vec<_>>()
931        )?;
932        writeln!(
933            f,
934            "                          since:{:?}",
935            self.determination
936                .since
937                .iter()
938                .map(|t| t.display(timeline))
939                .collect::<Vec<_>>()
940        )?;
941        writeln!(
942            f,
943            "        can respond immediately: {}",
944            self.respond_immediately
945        )?;
946        writeln!(f, "                       timeline: {:?}", &timeline)?;
947        writeln!(
948            f,
949            "              session wall time: {:13} ({})",
950            self.session_wall_time.timestamp_millis(),
951            self.session_wall_time.format("%Y-%m-%d %H:%M:%S%.3f"),
952        )?;
953
954        for source in &self.sources {
955            writeln!(f, "")?;
956            writeln!(f, "source {}:", source.name)?;
957            writeln!(
958                f,
959                "                  read frontier:{:?}",
960                source
961                    .read_frontier
962                    .iter()
963                    .map(|t| t.display(timeline))
964                    .collect::<Vec<_>>()
965            )?;
966            writeln!(
967                f,
968                "                 write frontier:{:?}",
969                source
970                    .write_frontier
971                    .iter()
972                    .map(|t| t.display(timeline))
973                    .collect::<Vec<_>>()
974            )?;
975        }
976
977        writeln!(f, "")?;
978        writeln!(f, "binding constraints:")?;
979        write!(f, "{}", self.determination.constraints.display(timeline))?;
980
981        Ok(())
982    }
983}
984
985/// Types and logic in support of a constraint-based approach to timestamp determination.
986mod constraints {
987
988    use core::fmt;
989    use std::fmt::Debug;
990
991    use differential_dataflow::lattice::Lattice;
992    use mz_storage_types::sources::Timeline;
993    use serde::{Deserialize, Serialize};
994    use timely::progress::{Antichain, Timestamp};
995
996    use mz_compute_types::ComputeInstanceId;
997    use mz_repr::GlobalId;
998    use mz_sql::session::vars::IsolationLevel;
999
1000    use super::DisplayableInTimeline;
1001
1002    /// Constraints expressed on the timestamp of a query.
1003    ///
1004    /// The constraints are expressed on the minimum and maximum values,
1005    /// resulting in a (possibly empty) interval of valid timestamps.
1006    ///
1007    /// The constraints may be redundant, in the interest of providing
1008    /// more complete explanations, but they may also be minimized at
1009    /// any point without altering their behavior by removing redundant
1010    /// constraints.
1011    ///
1012    /// When combined with a `Preference` one can determine an
1013    /// ideal timestamp to use.
1014    #[derive(Default, Serialize, Deserialize, Clone)]
1015    pub struct Constraints {
1016        /// Timestamps and reasons that impose an inclusive lower bound.
1017        pub lower: Vec<(Antichain<mz_repr::Timestamp>, Reason)>,
1018        /// Timestamps and reasons that impose an inclusive upper bound.
1019        pub upper: Vec<(Antichain<mz_repr::Timestamp>, Reason)>,
1020    }
1021
1022    impl DisplayableInTimeline for Constraints {
1023        fn fmt(&self, timeline: Option<&Timeline>, f: &mut fmt::Formatter) -> fmt::Result {
1024            if !self.lower.is_empty() {
1025                writeln!(f, "lower:")?;
1026                for (ts, reason) in &self.lower {
1027                    let ts: Vec<_> = ts
1028                        .iter()
1029                        .map(|t| format!("{}", t.display(timeline)))
1030                        .collect();
1031                    writeln!(f, "  ({}): [{}]", reason, ts.join(", "))?;
1032                }
1033            }
1034            if !self.upper.is_empty() {
1035                writeln!(f, "upper:")?;
1036                for (ts, reason) in &self.upper {
1037                    let ts: Vec<_> = ts
1038                        .iter()
1039                        .map(|t| format!("{}", t.display(timeline)))
1040                        .collect();
1041                    writeln!(f, "  ({}): [{}]", reason, ts.join(", "))?;
1042                }
1043            }
1044            Ok(())
1045        }
1046    }
1047
1048    impl Debug for Constraints {
1049        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1050            self.display(None).fmt(f)?;
1051            Ok(())
1052        }
1053    }
1054
1055    impl Constraints {
1056        /// Remove constraints that are dominated by other constraints.
1057        ///
1058        /// This removes redundant constraints, without removing constraints
1059        /// that are "tight" in the sense that the interval would be
1060        /// meaningfully different without them.
1061        /// For example, two constraints at the same
1062        /// time will both be retained, in the interest of full information.
1063        /// But a lower bound constraint at time `t` will be removed if there is a
1064        /// constraint at time `t + 1` (or any larger time).
1065        pub fn minimize(&mut self) {
1066            // Establish the upper bound of lower constraints.
1067            let lower_frontier = self.lower_bound();
1068            // Retain constraints that intersect `lower_frontier`.
1069            self.lower.retain(|(anti, _)| {
1070                anti.iter()
1071                    .any(|time| lower_frontier.elements().contains(time))
1072            });
1073
1074            // Establish the lower bound of upper constraints.
1075            let upper_frontier = self.upper_bound();
1076            // Retain constraints that intersect `upper_frontier`.
1077            self.upper.retain(|(anti, _)| {
1078                anti.iter()
1079                    .any(|time| upper_frontier.elements().contains(time))
1080            });
1081        }
1082
1083        /// An antichain equal to the least upper bound of lower bounds.
1084        pub fn lower_bound(&self) -> Antichain<mz_repr::Timestamp> {
1085            let mut lower = Antichain::from_elem(mz_repr::Timestamp::minimum());
1086            for (anti, _) in self.lower.iter() {
1087                lower = lower.join(anti);
1088            }
1089            lower
1090        }
1091        /// An antichain equal to the greatest lower bound of upper bounds.
1092        pub fn upper_bound(&self) -> Antichain<mz_repr::Timestamp> {
1093            self.upper
1094                .iter()
1095                .flat_map(|(anti, _)| anti.iter())
1096                .cloned()
1097                .collect()
1098        }
1099    }
1100
1101    /// An explanation of reasons for a timestamp constraint.
1102    #[derive(Serialize, Deserialize, Clone)]
1103    pub enum Reason {
1104        /// A compute input at a compute instance.
1105        /// This is something like an index or view
1106        /// that is maintained by compute.
1107        ComputeInput(Vec<(ComputeInstanceId, GlobalId)>),
1108        /// A storage input.
1109        StorageInput(Vec<GlobalId>),
1110        /// A specified isolation level and the timestamp it requires.
1111        IsolationLevel(IsolationLevel),
1112        /// Real-time recency may constrain the timestamp from below.
1113        RealTimeRecency,
1114        /// The query expressed its own constraint on the timestamp.
1115        QueryAsOf,
1116    }
1117
1118    impl fmt::Display for Reason {
1119        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1120            match self {
1121                Reason::ComputeInput(ids) => {
1122                    let formatted: Vec<_> =
1123                        ids.iter().map(|(c, g)| format!("({}, {})", c, g)).collect();
1124                    write!(f, "Indexed inputs: [{}]", formatted.join(", "))
1125                }
1126                Reason::StorageInput(ids) => {
1127                    let formatted: Vec<_> = ids.iter().map(|g| format!("{}", g)).collect();
1128                    write!(f, "Storage inputs: [{}]", formatted.join(", "))
1129                }
1130                Reason::IsolationLevel(level) => {
1131                    write!(f, "Isolation level: {:?}", level)
1132                }
1133                Reason::RealTimeRecency => {
1134                    write!(f, "Real-time recency")
1135                }
1136                Reason::QueryAsOf => {
1137                    write!(f, "Query's AS OF")
1138                }
1139            }
1140        }
1141    }
1142
1143    /// Given an interval [read, write) of timestamp options,
1144    /// this expresses a preference for either end of the spectrum.
1145    pub enum Preference {
1146        /// Prefer the greatest timestamp immediately available.
1147        ///
1148        /// This considers the immediate inputs to a query and
1149        /// selects the greatest timestamp not greater or equal
1150        /// to any of their write frontiers.
1151        ///
1152        /// The preference only relates to immediate query inputs,
1153        /// but it could be extended to transitive inputs as well.
1154        /// For example, one could imagine preferring the freshest
1155        /// data known to be ingested into Materialize, under the
1156        /// premise that those answers should soon become available,
1157        /// and may be more fresh than the immediate inputs.
1158        FreshestAvailable,
1159        /// Prefer the least valid timestamp.
1160        ///
1161        /// This is useful when one has no expressed freshness
1162        /// constraints, and wants to minimally impact others.
1163        /// For example, `AS OF AT LEAST <time>`.
1164        StalestValid,
1165    }
1166}