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