Skip to main content

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/// Convenience function for calculating the current upper bound that we want to
351/// prevent the global timestamp from exceeding.
352fn upper_bound(now: &mz_repr::Timestamp) -> mz_repr::Timestamp {
353    const TIMESTAMP_INTERVAL_MS: u64 = 5000;
354    const TIMESTAMP_INTERVAL_UPPER_BOUND: u64 = 2;
355
356    now.saturating_add(TIMESTAMP_INTERVAL_MS * TIMESTAMP_INTERVAL_UPPER_BOUND)
357}
358
359/// Logs an error when `timestamp` is further ahead of `now` than a local write timestamp
360/// should ever be, the signal that the `EpochMilliseconds` timeline has run away (e.g. after a
361/// wall-clock regression).
362pub(crate) fn check_runaway_write_ts(now: &mz_repr::Timestamp, timestamp: mz_repr::Timestamp) {
363    let upper_bound = upper_bound(now);
364    if timestamp > upper_bound {
365        error!(
366            %now,
367            "Setting local write timestamp to {timestamp}, which is more than \
368            the desired upper bound {upper_bound}."
369        );
370    }
371}
372
373/// Return the set of ids in a timedomain and verify timeline correctness.
374///
375/// When a user starts a transaction, we need to prevent compaction of anything
376/// they might read from. We use a heuristic of "anything in the same database
377/// schemas with the same timeline as whatever the first query is".
378///
379/// This is a free-standing function that can be called from both the old peek sequencing
380/// and the new frontend peek sequencing.
381///
382/// This function assumes that uses_ids only includes such ids that are the latest versions of each
383/// object. This should be easy to satisfy when calling this function with the ids directly
384/// referenced by a new query, because a new query should not be able to refer to old versions of
385/// objects.
386pub(crate) fn timedomain_for<'a, I>(
387    catalog: &Catalog,
388    dataflow_builder: &DataflowBuilder,
389    uses_ids: I,
390    timeline_context: &TimelineContext,
391    conn_id: &ConnectionId,
392    compute_instance: ComputeInstanceId,
393) -> Result<CollectionIdBundle, AdapterError>
394where
395    I: IntoIterator<Item = &'a GlobalId>,
396{
397    // This is just for the assert below.
398    let mut orig_uses_ids = Vec::new();
399
400    // Gather all the used schemas.
401    let mut schemas = BTreeSet::new();
402    for id in uses_ids {
403        orig_uses_ids.push(id.clone());
404
405        let entry = catalog.get_entry_by_global_id(id);
406        let name = entry.name();
407        schemas.insert((name.qualifiers.database_spec, name.qualifiers.schema_spec));
408    }
409
410    let pg_catalog_schema = (
411        ResolvedDatabaseSpecifier::Ambient,
412        SchemaSpecifier::Id(catalog.get_pg_catalog_schema_id()),
413    );
414    let system_schemas: Vec<_> = catalog
415        .system_schema_ids()
416        .map(|id| (ResolvedDatabaseSpecifier::Ambient, SchemaSpecifier::Id(id)))
417        .collect();
418
419    if system_schemas.iter().any(|s| schemas.contains(s)) {
420        // If any of the system schemas is specified, add the rest of the
421        // system schemas.
422        schemas.extend(system_schemas);
423    } else if !schemas.is_empty() {
424        // Always include the pg_catalog schema, if schemas is non-empty. The pg_catalog schemas is
425        // sometimes used by applications in followup queries.
426        schemas.insert(pg_catalog_schema);
427    }
428
429    // Gather the IDs of all items in all used schemas.
430    let mut collection_ids: BTreeSet<GlobalId> = BTreeSet::new();
431    for (db, schema) in schemas {
432        let schema = catalog.get_schema(&db, &schema, conn_id);
433        // Note: We include just the latest `GlobalId` instead of all `GlobalId`s associated
434        // with an object, because older versions will already get included, if there are
435        // objects the depend on them.
436        let global_ids = schema
437            .items
438            .values()
439            .map(|item_id| catalog.get_entry(item_id).latest_global_id());
440        collection_ids.extend(global_ids);
441    }
442
443    {
444        // Assert that we got back a superset of the original ids.
445        // This should be true, because the query is able to directly reference only the latest
446        // version of each object.
447        for id in orig_uses_ids.iter() {
448            soft_assert_or_log!(
449                collection_ids.contains(id),
450                "timedomain_for is about to miss {}",
451                id
452            );
453        }
454    }
455
456    // Gather the dependencies of those items.
457    let mut id_bundle: CollectionIdBundle = dataflow_builder.sufficient_collections(collection_ids);
458
459    // Filter out ids from different timelines.
460    for ids in [
461        &mut id_bundle.storage_ids,
462        &mut id_bundle.compute_ids.entry(compute_instance).or_default(),
463    ] {
464        ids.retain(|gid| {
465            let id_timeline_context = catalog
466                .validate_timeline_context(vec![*gid])
467                .expect("single id should never fail");
468            match (&id_timeline_context, &timeline_context) {
469                // If this id doesn't have a timeline, we can keep it.
470                (
471                    TimelineContext::TimestampIndependent | TimelineContext::TimestampDependent,
472                    _,
473                ) => true,
474                // If there's no source timeline, we have the option to opt into a timeline,
475                // so optimistically choose epoch ms. This is useful when the first query in a
476                // transaction is on a static view.
477                (
478                    TimelineContext::TimelineDependent(id_timeline),
479                    TimelineContext::TimestampIndependent | TimelineContext::TimestampDependent,
480                ) => *id_timeline == Timeline::EpochMilliseconds,
481                // Otherwise check if timelines are the same.
482                (
483                    TimelineContext::TimelineDependent(id_timeline),
484                    TimelineContext::TimelineDependent(source_timeline),
485                ) => id_timeline == source_timeline,
486            }
487        });
488    }
489
490    Ok(id_bundle)
491}