mz_adapter/coord/timeline.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//! A mechanism to ensure that a sequence of writes and reads proceed correctly through timestamps.
11
12use std::collections::{BTreeMap, BTreeSet};
13use std::fmt;
14use std::sync::Arc;
15
16use chrono::{DateTime, Utc};
17use futures::Future;
18use mz_adapter_types::connection::ConnectionId;
19use mz_compute_types::ComputeInstanceId;
20use mz_ore::now::{EpochMillis, NowFn, to_datetime};
21use mz_ore::{instrument, soft_assert_or_log};
22use mz_repr::{GlobalId, Timestamp};
23use mz_sql::names::{ResolvedDatabaseSpecifier, SchemaSpecifier};
24use mz_storage_types::sources::Timeline;
25use mz_timestamp_oracle::batching_oracle::BatchingTimestampOracle;
26use mz_timestamp_oracle::{self, TimestampOracle, TimestampOracleConfig, WriteTimestamp};
27use timely::progress::Timestamp as _;
28use tracing::{Instrument, debug, error, info};
29
30use crate::AdapterError;
31use crate::catalog::Catalog;
32use crate::coord::Coordinator;
33use crate::coord::id_bundle::CollectionIdBundle;
34use crate::coord::read_policy::ReadHolds;
35use crate::coord::timestamp_selection::TimestampProvider;
36use crate::optimize::dataflows::DataflowBuilder;
37
38/// An enum describing whether or not a query belongs to a timeline and whether the query can be
39/// affected by the timestamp at which it executes.
40#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
41pub enum TimelineContext {
42 /// Can only ever belong to a single specific timeline. The answer will depend on a timestamp
43 /// chosen from that specific timeline.
44 TimelineDependent(Timeline),
45 /// Can belong to any timeline. The answer will depend on a timestamp chosen from some
46 /// timeline.
47 TimestampDependent,
48 /// The answer does not depend on a chosen timestamp.
49 TimestampIndependent,
50}
51
52impl TimelineContext {
53 /// Whether or not the context contains a timeline.
54 pub fn contains_timeline(&self) -> bool {
55 self.timeline().is_some()
56 }
57
58 /// The timeline belonging to this context, if one exists.
59 pub fn timeline(&self) -> Option<&Timeline> {
60 match self {
61 Self::TimelineDependent(timeline) => Some(timeline),
62 Self::TimestampIndependent | Self::TimestampDependent => None,
63 }
64 }
65}
66
67/// Global state for a single timeline.
68///
69/// For each timeline we maintain a timestamp oracle, which is responsible for
70/// providing read (and sometimes write) timestamps, and a set of read holds which
71/// guarantee that those read timestamps are valid.
72pub(crate) struct TimelineState {
73 pub(crate) oracle: Arc<dyn TimestampOracle<Timestamp> + Send + Sync>,
74 pub(crate) read_holds: ReadHolds,
75}
76
77impl fmt::Debug for TimelineState {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 f.debug_struct("TimelineState")
80 .field("read_holds", &self.read_holds)
81 .finish()
82 }
83}
84
85impl Coordinator {
86 pub(crate) fn now(&self) -> EpochMillis {
87 (self.catalog().config().now)()
88 }
89
90 pub(crate) fn now_datetime(&self) -> DateTime<Utc> {
91 to_datetime(self.now())
92 }
93
94 pub(crate) fn get_timestamp_oracle(
95 &self,
96 timeline: &Timeline,
97 ) -> Arc<dyn TimestampOracle<Timestamp> + Send + Sync> {
98 let oracle = &self
99 .global_timelines
100 .get(timeline)
101 .expect("all timelines have a timestamp oracle")
102 .oracle;
103
104 Arc::clone(oracle)
105 }
106
107 /// Returns a [`TimestampOracle`] used for reads and writes from/to a local input.
108 pub(crate) fn get_local_timestamp_oracle(
109 &self,
110 ) -> Arc<dyn TimestampOracle<Timestamp> + Send + Sync> {
111 self.get_timestamp_oracle(&Timeline::EpochMilliseconds)
112 }
113
114 /// Assign a timestamp for a read from a local input. Reads following writes
115 /// must be at a time >= the write's timestamp; we choose "equal to" for
116 /// simplicity's sake and to open as few new timestamps as possible.
117 pub(crate) async fn get_local_read_ts(&self) -> Timestamp {
118 self.get_local_timestamp_oracle().read_ts().await
119 }
120
121 /// Assign a timestamp for a write to a local input and increase the local ts.
122 /// Writes following reads must ensure that they are assigned a strictly larger
123 /// timestamp to ensure they are not visible to any real-time earlier reads.
124 #[instrument(name = "coord::get_local_write_ts")]
125 pub(crate) async fn get_local_write_ts(&mut self) -> WriteTimestamp {
126 self.global_timelines
127 .get_mut(&Timeline::EpochMilliseconds)
128 .expect("no realtime timeline")
129 .oracle
130 .write_ts()
131 .await
132 }
133
134 /// Peek the current timestamp used for operations on local inputs. Used to determine how much
135 /// to block group commits by.
136 pub(crate) async fn peek_local_write_ts(&self) -> Timestamp {
137 self.get_local_timestamp_oracle().peek_write_ts().await
138 }
139
140 /// Marks a write at `timestamp` as completed, using a [`TimestampOracle`].
141 pub(crate) fn apply_local_write(
142 &self,
143 timestamp: Timestamp,
144 ) -> impl Future<Output = ()> + Send + 'static {
145 let now = self.now().into();
146 check_runaway_write_ts(&now, timestamp);
147
148 let oracle = self.get_local_timestamp_oracle();
149
150 async move {
151 oracle
152 .apply_write(timestamp)
153 .instrument(tracing::debug_span!("apply_local_write_static", ?timestamp))
154 .await
155 }
156 }
157
158 /// Assign a timestamp for a write to the catalog. This timestamp should have the following
159 /// properties:
160 ///
161 /// - Monotonically increasing.
162 /// - Greater than or equal to the current catalog upper.
163 /// - Greater than the largest write timestamp used in the
164 /// [epoch millisecond timeline](Timeline::EpochMilliseconds).
165 ///
166 /// In general this is fully satisfied by the getting the current write timestamp in the
167 /// [epoch millisecond timeline](Timeline::EpochMilliseconds) from the timestamp oracle,
168 /// however, in read-only mode we cannot modify the timestamp oracle.
169 pub(crate) async fn get_catalog_write_ts(&mut self) -> Timestamp {
170 if self.read_only_controllers {
171 let (write_ts, upper) =
172 futures::future::join(self.peek_local_write_ts(), self.catalog().current_upper())
173 .await;
174 std::cmp::max(write_ts, upper)
175 } else {
176 self.get_local_write_ts().await.timestamp
177 }
178 }
179
180 /// Ensures that a global timeline state exists for `timeline`.
181 pub(crate) async fn ensure_timeline_state<'a>(
182 &'a mut self,
183 timeline: &'a Timeline,
184 ) -> &'a mut TimelineState {
185 Self::ensure_timeline_state_with_initial_time(
186 timeline,
187 Timestamp::minimum(),
188 self.catalog().config().now.clone(),
189 self.timestamp_oracle_config.clone(),
190 &mut self.global_timelines,
191 self.read_only_controllers,
192 )
193 .await
194 }
195
196 /// Ensures that a global timeline state exists for `timeline`, with an initial time
197 /// of `initially`.
198 #[instrument]
199 pub(crate) async fn ensure_timeline_state_with_initial_time<'a>(
200 timeline: &'a Timeline,
201 initially: Timestamp,
202 now: NowFn,
203 oracle_config: Option<TimestampOracleConfig>,
204 global_timelines: &'a mut BTreeMap<Timeline, TimelineState>,
205 read_only: bool,
206 ) -> &'a mut TimelineState {
207 if !global_timelines.contains_key(timeline) {
208 info!("opening a new TimestampOracle for timeline {:?}", timeline,);
209
210 let now_fn = if timeline == &Timeline::EpochMilliseconds {
211 now
212 } else {
213 // Timelines that are not `EpochMilliseconds` don't have an
214 // "external" clock that wants to drive forward timestamps in
215 // addition to the rule that write timestamps must be strictly
216 // monotonically increasing.
217 //
218 // Passing in a clock that always yields the minimum takes the
219 // clock out of the equation and makes timestamps advance only
220 // by the rule about strict monotonicity mentioned above.
221 NowFn::from(|| Timestamp::minimum().into())
222 };
223
224 let oracle_config = oracle_config.expect(
225 "missing --timestamp-oracle-url even though the timestamp oracle was configured",
226 );
227
228 let oracle = oracle_config
229 .open(timeline.to_string(), initially, now_fn, read_only)
230 .await;
231
232 let batching_oracle = BatchingTimestampOracle::new(oracle_config.metrics(), oracle);
233
234 let oracle: Arc<dyn TimestampOracle<mz_repr::Timestamp> + Send + Sync> =
235 Arc::new(batching_oracle);
236
237 global_timelines.insert(
238 timeline.clone(),
239 TimelineState {
240 oracle,
241 read_holds: ReadHolds::new(),
242 },
243 );
244 }
245 global_timelines.get_mut(timeline).expect("inserted above")
246 }
247
248 /// Given a [`Timeline`] and a [`CollectionIdBundle`], removes all of the "storage ids"
249 /// and "compute ids" in the bundle, from the timeline.
250 pub(crate) fn remove_resources_associated_with_timeline(
251 &mut self,
252 timeline: Timeline,
253 ids: CollectionIdBundle,
254 ) -> bool {
255 let TimelineState { read_holds, .. } = self
256 .global_timelines
257 .get_mut(&timeline)
258 .expect("all timeslines have a timestamp oracle");
259
260 // Remove all of the underlying resources.
261 for id in ids.storage_ids {
262 read_holds.remove_storage_collection(id);
263 }
264 for (compute_id, ids) in ids.compute_ids {
265 for id in ids {
266 read_holds.remove_compute_collection(compute_id, id);
267 }
268 }
269 let became_empty = read_holds.is_empty();
270
271 became_empty
272 }
273
274 /// Downgrades the [`EpochMilliseconds`](Timeline::EpochMilliseconds) timeline's read holds
275 /// to `read_ts`.
276 ///
277 /// `read_ts` must not exceed the local oracle's read frontier, i.e. it must come from the
278 /// oracle's `read_ts()` or from a write that was already applied with `apply_write`.
279 /// Downgrading past the oracle's read frontier could let compaction pass timestamps the
280 /// oracle can still serve reads at.
281 pub(crate) fn downgrade_local_read_holds(&mut self, read_ts: Timestamp) {
282 let TimelineState { read_holds, .. } = self
283 .global_timelines
284 .get_mut(&Timeline::EpochMilliseconds)
285 .expect("no realtime timeline");
286 read_holds.downgrade(read_ts);
287 }
288
289 /// Advances all timelines other than [`EpochMilliseconds`](Timeline::EpochMilliseconds) to
290 /// their objects' uppers and downgrades their read holds accordingly.
291 ///
292 /// The `EpochMilliseconds` oracle is advanced by group commits, so it is not touched here.
293 /// Its read holds are downgraded separately via [`Self::downgrade_local_read_holds`].
294 #[instrument(level = "debug")]
295 pub(crate) async fn advance_custom_timelines(&mut self) {
296 // Common case: only the EpochMilliseconds timeline exists, nothing to do.
297 if !self
298 .global_timelines
299 .keys()
300 .any(|timeline| *timeline != Timeline::EpochMilliseconds)
301 {
302 return;
303 }
304
305 // Take the map so we can call `&self` methods while mutating the timeline states.
306 let global_timelines = std::mem::take(&mut self.global_timelines);
307 for (
308 timeline,
309 TimelineState {
310 oracle,
311 mut read_holds,
312 },
313 ) in global_timelines
314 {
315 if timeline == Timeline::EpochMilliseconds {
316 self.global_timelines
317 .insert(timeline, TimelineState { oracle, read_holds });
318 continue;
319 }
320 if !self.read_only_controllers {
321 // For non realtime sources, we define now as the largest timestamp, not in
322 // advance of any object's upper. This is the largest timestamp that is closed
323 // to writes.
324 let id_bundle = self.catalog().ids_in_timeline(&timeline);
325
326 // Advance the timeline if-and-only-if there are objects in it.
327 // Otherwise we'd advance to the empty frontier, meaning we
328 // close it off for ever.
329 if !id_bundle.is_empty() {
330 let least_valid_write = self.least_valid_write(&id_bundle);
331 let now = Self::largest_not_in_advance_of_upper(&least_valid_write);
332 oracle.apply_write(now).await;
333 debug!(
334 least_valid_write = ?least_valid_write,
335 oracle_read_ts = ?oracle.read_ts().await,
336 "advanced {:?} to {}",
337 timeline,
338 now,
339 );
340 }
341 }
342 let read_ts = oracle.read_ts().await;
343 read_holds.downgrade(read_ts);
344 self.global_timelines
345 .insert(timeline, TimelineState { oracle, read_holds });
346 }
347 }
348}
349
350/// The highest timestamp the `EpochMilliseconds` write timeline may be advanced to
351/// while the wall clock reads `now`.
352///
353/// A write above this is a runaway: the oracle is monotone and durable, so every later
354/// write and strict-serializable read on the timeline blocks until the wall clock catches
355/// up, across restarts. Group commit stays under it by allocating from the oracle, which
356/// clamps to the clock. A caller that chooses its own write timestamp has to be checked
357/// against it, see `GroupCommitter::commit_timestamped`.
358pub(crate) fn write_ts_upper_bound(now: &mz_repr::Timestamp) -> mz_repr::Timestamp {
359 const TIMESTAMP_INTERVAL_MS: u64 = 5000;
360 const TIMESTAMP_INTERVAL_UPPER_BOUND: u64 = 2;
361
362 now.saturating_add(TIMESTAMP_INTERVAL_MS * TIMESTAMP_INTERVAL_UPPER_BOUND)
363}
364
365/// Reports a write timestamp that is further ahead of `now` than
366/// [`write_ts_upper_bound`] allows, the signal that the `EpochMilliseconds` timeline has
367/// run away (e.g. after a wall-clock regression, or from a durably poisoned oracle).
368///
369/// This is a detector, not a guard: the timestamp has already been chosen, and every
370/// caller that can still refuse one checks the bound itself. It logs rather than fails,
371/// because every way left to reach it is a condition this process inherited. A durable
372/// runaway is re-applied to the oracle on every boot, and a backwards clock step is what
373/// the group committer's throttle waits out, so failing here would turn a stalled
374/// timeline into a crash loop.
375pub(crate) fn check_runaway_write_ts(now: &mz_repr::Timestamp, timestamp: mz_repr::Timestamp) {
376 let upper_bound = write_ts_upper_bound(now);
377 if timestamp > upper_bound {
378 error!(
379 %now,
380 "setting local write timestamp to {timestamp}, which is more than \
381 the desired upper bound {upper_bound}"
382 );
383 }
384}
385
386/// Return the set of ids in a timedomain and verify timeline correctness.
387///
388/// When a user starts a transaction, we need to prevent compaction of anything
389/// they might read from. We use a heuristic of "anything in the same database
390/// schemas with the same timeline as whatever the first query is".
391///
392/// This is a free-standing function that can be called from both the old peek sequencing
393/// and the new frontend peek sequencing.
394///
395/// This function assumes that uses_ids only includes such ids that are the latest versions of each
396/// object. This should be easy to satisfy when calling this function with the ids directly
397/// referenced by a new query, because a new query should not be able to refer to old versions of
398/// objects.
399pub(crate) fn timedomain_for<'a, I>(
400 catalog: &Catalog,
401 dataflow_builder: &DataflowBuilder,
402 uses_ids: I,
403 timeline_context: &TimelineContext,
404 conn_id: &ConnectionId,
405 compute_instance: ComputeInstanceId,
406) -> Result<CollectionIdBundle, AdapterError>
407where
408 I: IntoIterator<Item = &'a GlobalId>,
409{
410 // This is just for the assert below.
411 let mut orig_uses_ids = Vec::new();
412
413 // Gather all the used schemas.
414 let mut schemas = BTreeSet::new();
415 for id in uses_ids {
416 orig_uses_ids.push(id.clone());
417
418 let entry = catalog.get_entry_by_global_id(id);
419 let name = entry.name();
420 schemas.insert((name.qualifiers.database_spec, name.qualifiers.schema_spec));
421 }
422
423 let pg_catalog_schema = (
424 ResolvedDatabaseSpecifier::Ambient,
425 SchemaSpecifier::Id(catalog.get_pg_catalog_schema_id()),
426 );
427 let system_schemas: Vec<_> = catalog
428 .system_schema_ids()
429 .map(|id| (ResolvedDatabaseSpecifier::Ambient, SchemaSpecifier::Id(id)))
430 .collect();
431
432 if system_schemas.iter().any(|s| schemas.contains(s)) {
433 // If any of the system schemas is specified, add the rest of the
434 // system schemas.
435 schemas.extend(system_schemas);
436 } else if !schemas.is_empty() {
437 // Always include the pg_catalog schema, if schemas is non-empty. The pg_catalog schemas is
438 // sometimes used by applications in followup queries.
439 schemas.insert(pg_catalog_schema);
440 }
441
442 // Gather the IDs of all items in all used schemas.
443 let mut collection_ids: BTreeSet<GlobalId> = BTreeSet::new();
444 for (db, schema) in schemas {
445 let schema = catalog.get_schema(&db, &schema, conn_id);
446 // Note: We include just the latest `GlobalId` instead of all `GlobalId`s associated
447 // with an object, because older versions will already get included, if there are
448 // objects the depend on them.
449 let global_ids = schema
450 .items
451 .values()
452 .map(|item_id| catalog.get_entry(item_id).latest_global_id());
453 collection_ids.extend(global_ids);
454 }
455
456 {
457 // Assert that we got back a superset of the original ids.
458 // This should be true, because the query is able to directly reference only the latest
459 // version of each object.
460 for id in orig_uses_ids.iter() {
461 soft_assert_or_log!(
462 collection_ids.contains(id),
463 "timedomain_for is about to miss {}",
464 id
465 );
466 }
467 }
468
469 // Gather the dependencies of those items.
470 let mut id_bundle: CollectionIdBundle = dataflow_builder.sufficient_collections(collection_ids);
471
472 // Filter out ids from different timelines.
473 for ids in [
474 &mut id_bundle.storage_ids,
475 &mut id_bundle.compute_ids.entry(compute_instance).or_default(),
476 ] {
477 ids.retain(|gid| {
478 let id_timeline_context = catalog
479 .validate_timeline_context(vec![*gid])
480 .expect("single id should never fail");
481 match (&id_timeline_context, &timeline_context) {
482 // If this id doesn't have a timeline, we can keep it.
483 (
484 TimelineContext::TimestampIndependent | TimelineContext::TimestampDependent,
485 _,
486 ) => true,
487 // If there's no source timeline, we have the option to opt into a timeline,
488 // so optimistically choose epoch ms. This is useful when the first query in a
489 // transaction is on a static view.
490 (
491 TimelineContext::TimelineDependent(id_timeline),
492 TimelineContext::TimestampIndependent | TimelineContext::TimestampDependent,
493 ) => *id_timeline == Timeline::EpochMilliseconds,
494 // Otherwise check if timelines are the same.
495 (
496 TimelineContext::TimelineDependent(id_timeline),
497 TimelineContext::TimelineDependent(source_timeline),
498 ) => id_timeline == source_timeline,
499 }
500 });
501 }
502
503 Ok(id_bundle)
504}