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