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