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