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}