Skip to main content

mz_adapter/coord/
appends.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 and types for all appends executed by the [`Coordinator`].
11//!
12//! Runtime table appends, registrations, and forgets are serialized by the
13//! [`GroupCommitter`]. FIFO order is required so appends cannot overtake registration or
14//! forgetting.
15//!
16//! For each command, the committer:
17//!
18//! 1. allocates a write timestamp from the shared oracle,
19//! 2. advances the catalog upper to [`WriteTimestamp::advance_to`],
20//! 3. writes the txns shard, retrying `InvalidUppers` from step 1, and
21//! 4. applies the successful write to the oracle.
22//!
23//! Step 2 keeps the catalog readable at the oracle read timestamp. It also enforces fencing for
24//! a post-fence retry: the fresh oracle timestamp's advance frontier is above the stale process's
25//! cached catalog upper, so the advance reaches Persist and observes the fence before another txns
26//! write.
27//!
28//! On `environmentd` bootstrap in read/write mode, system-table snapshots cannot complete until a
29//! txns-shard write has advanced the table uppers. A stale write either linearizes before this
30//! barrier and is observed by the snapshot, or conflicts and follows the fenced retry path above.
31//! This relies on generations sharing the oracle.
32//!
33//! Work that requires coordinator state is returned via [`Message::GroupCommitApplied`].
34
35use std::collections::{BTreeMap, BTreeSet};
36use std::future::Future;
37use std::ops::ControlFlow;
38use std::pin::Pin;
39use std::sync::{Arc, LazyLock};
40use std::time::{Duration, Instant};
41
42use derivative::Derivative;
43use futures::future::{BoxFuture, FutureExt};
44use mz_adapter_types::connection::ConnectionId;
45use mz_adapter_types::dyncfgs::GROUP_COMMIT_MAX_ATTEMPTS;
46use mz_catalog::builtin::{BuiltinTable, MZ_SESSIONS};
47use mz_dyncfg::{ConfigSet, ConfigValHandle};
48use mz_expr::CollectionPlan;
49use mz_ore::assert_none;
50use mz_ore::halt;
51use mz_ore::instrument;
52use mz_ore::now::NowFn;
53use mz_ore::task;
54use mz_repr::{CatalogItemId, GlobalId, Timestamp};
55use mz_sql::names::ResolvedIds;
56use mz_sql::plan::{ExplainPlanPlan, ExplainTimestampPlan, Explainee, ExplaineeStatement, Plan};
57use mz_sql::session::metadata::SessionMetadata;
58use mz_storage_client::client::TableData;
59use mz_storage_client::controller::{TableRegistration, TableWriteHandle};
60use mz_storage_types::controller::StorageError;
61use mz_timestamp_oracle::{TimestampOracle, WriteTimestamp};
62use smallvec::SmallVec;
63use tokio::sync::{Notify, OwnedMutexGuard, OwnedSemaphorePermit, Semaphore, mpsc, oneshot};
64use tracing::{Instrument, Span, info, warn};
65
66use crate::catalog::{BuiltinTableUpdate, Catalog, CatalogUpperHandle};
67use crate::coord::{Coordinator, Message, PendingTxn, PlanValidity};
68use crate::metrics::Metrics;
69use crate::session::{GroupCommitWriteLocks, Session, WriteLocks};
70use crate::statement_logging::StatementLoggingId;
71use crate::util::{CompletedClientTransmitter, ResultExt};
72use crate::{AdapterError, ExecuteContext};
73
74/// Tables that we emit updates for when starting a new session.
75pub(crate) static REQUIRED_BUILTIN_TABLES: &[&LazyLock<BuiltinTable>] = &[&MZ_SESSIONS];
76
77/// An operation that was deferred waiting on a resource to be available.
78///
79/// For example when inserting into a table we defer on acquiring [`WriteLocks`].
80#[derive(Debug)]
81pub enum DeferredOp {
82    /// A plan, e.g. ReadThenWrite, that needs locks before sequencing.
83    Plan(DeferredPlan),
84    /// Inserts into a collection.
85    Write(DeferredWrite),
86}
87
88impl DeferredOp {
89    /// Certain operations, e.g. "blind writes"/`INSERT` statements, can be optimistically retried
90    /// because we can share a write lock between multiple operations. In this case we wait to
91    /// acquire the locks until [`stage_group_commit`], where writes are grouped by collection and
92    /// committed at a single timestamp.
93    ///
94    /// Other operations, e.g. read-then-write plans/`UPDATE` statements, must uniquely hold their
95    /// write locks and thus we should acquire the locks in [`try_deferred`] to prevent multiple
96    /// queued plans attempting to get retried at the same time, when we know only one can proceed.
97    ///
98    /// [`try_deferred`]: crate::coord::Coordinator::try_deferred
99    /// [`stage_group_commit`]: crate::coord::Coordinator::stage_group_commit
100    pub(crate) fn can_be_optimistically_retried(&self) -> bool {
101        match self {
102            DeferredOp::Plan(_) => false,
103            DeferredOp::Write(_) => true,
104        }
105    }
106
107    /// Returns an Iterator of all the required locks for current operation.
108    pub fn required_locks(&self) -> impl Iterator<Item = CatalogItemId> + '_ {
109        match self {
110            DeferredOp::Plan(plan) => {
111                let iter = plan.requires_locks.iter().copied();
112                itertools::Either::Left(iter)
113            }
114            DeferredOp::Write(write) => {
115                let iter = write.writes.keys().copied();
116                itertools::Either::Right(iter)
117            }
118        }
119    }
120
121    /// Returns the [`ConnectionId`] associated with this deferred op.
122    pub fn conn_id(&self) -> &ConnectionId {
123        match self {
124            DeferredOp::Plan(plan) => plan.ctx.session().conn_id(),
125            DeferredOp::Write(write) => write.pending_txn.ctx.session().conn_id(),
126        }
127    }
128
129    /// Consumes the [`DeferredOp`], returning the inner [`ExecuteContext`].
130    pub fn into_ctx(self) -> ExecuteContext {
131        match self {
132            DeferredOp::Plan(plan) => plan.ctx,
133            DeferredOp::Write(write) => write.pending_txn.ctx,
134        }
135    }
136}
137
138/// Describes a plan that is awaiting [`WriteLocks`].
139#[derive(Derivative)]
140#[derivative(Debug)]
141pub struct DeferredPlan {
142    #[derivative(Debug = "ignore")]
143    pub ctx: ExecuteContext,
144    pub plan: Plan,
145    pub validity: PlanValidity,
146    pub requires_locks: BTreeSet<CatalogItemId>,
147    pub resolved_ids: ResolvedIds,
148    pub sql_impl_resolved_ids: ResolvedIds,
149}
150
151#[derive(Debug)]
152pub struct DeferredWrite {
153    pub span: Span,
154    pub writes: BTreeMap<CatalogItemId, SmallVec<[TableData; 1]>>,
155    pub pending_txn: PendingTxn,
156}
157
158/// Describes what action triggered an update to a builtin table.
159#[derive(Debug)]
160pub(crate) enum BuiltinTableUpdateSource {
161    /// Internal update, notify the caller when it's complete.
162    Internal(oneshot::Sender<()>),
163    /// Update was triggered by some background process, such as periodic heartbeats from COMPUTE.
164    Background(oneshot::Sender<()>),
165}
166
167/// Where to deliver the result of a [`PendingWriteTxn::User`] write.
168#[derive(Debug)]
169pub(crate) enum UserWriteResponder {
170    /// Session-bound write. The coordinator retires the session's
171    /// `ExecuteContext` once the write commits.
172    Session(PendingTxn),
173}
174
175/// A pending write transaction that will be committing during the next group commit.
176#[derive(Debug)]
177pub(crate) enum PendingWriteTxn {
178    /// Write to a user table. The write timestamp is picked by the oracle
179    /// during group commit. The write lock is either handed off from the
180    /// submitting session (via `write_locks: Some(..)`) or acquired during
181    /// group commit (`write_locks: None`).
182    User {
183        span: Span,
184        /// List of all write operations within the transaction.
185        writes: BTreeMap<CatalogItemId, SmallVec<[TableData; 1]>>,
186        /// If they exist, should contain locks for each [`CatalogItemId`] in `writes`.
187        write_locks: Option<WriteLocks>,
188        /// Where to deliver the result once the write commits.
189        responder: UserWriteResponder,
190    },
191    /// Write to a system table.
192    System {
193        updates: Vec<BuiltinTableUpdate>,
194        source: BuiltinTableUpdateSource,
195    },
196}
197
198impl PendingWriteTxn {
199    fn is_internal_system(&self) -> bool {
200        match self {
201            PendingWriteTxn::System {
202                source: BuiltinTableUpdateSource::Internal(_),
203                ..
204            } => true,
205            _ => false,
206        }
207    }
208}
209
210pub(crate) enum TableWriteCmd {
211    GroupCommit(GroupCommitRequest),
212    Register {
213        tables: Vec<TableRegistration>,
214        result: oneshot::Sender<Timestamp>,
215    },
216    Forget {
217        ids: Vec<GlobalId>,
218        result: oneshot::Sender<Timestamp>,
219    },
220}
221
222/// A group commit staged on the coordinator loop for the [`GroupCommitter`].
223pub(crate) struct GroupCommitRequest {
224    /// Appends resolved to their latest [`GlobalId`]. Empty for a keepalive.
225    appends: Vec<(GlobalId, Vec<TableData>)>,
226    responses: Vec<CompletedClientTransmitter>,
227    statement_logging_ids: Vec<StatementLoggingId>,
228    notifies: Vec<oneshot::Sender<()>>,
229    write_locks: GroupCommitWriteLocks,
230    /// In-progress permits held until the commit is applied.
231    permits: Vec<GroupCommitPermit>,
232    contains_internal_system_write: bool,
233    span: Span,
234}
235
236impl GroupCommitRequest {
237    fn merge(&mut self, other: GroupCommitRequest) {
238        let GroupCommitRequest {
239            appends,
240            responses,
241            statement_logging_ids,
242            notifies,
243            write_locks,
244            permits,
245            contains_internal_system_write,
246            span: _,
247        } = other;
248        self.appends.extend(appends);
249        self.responses.extend(responses);
250        self.statement_logging_ids.extend(statement_logging_ids);
251        self.notifies.extend(notifies);
252        self.write_locks.extend(write_locks);
253        self.permits.extend(permits);
254        self.contains_internal_system_write |= contains_internal_system_write;
255    }
256}
257
258/// Serializes runtime txns-shard writes off the coordinator loop.
259///
260/// Dropped group-commit requests retire their clients through [`ExecuteContext`]. Dropped
261/// registration or forget replies cause their coordinator waiters to halt.
262pub(crate) struct GroupCommitter {
263    rx: mpsc::UnboundedReceiver<TableWriteCmd>,
264    oracle: Arc<dyn TimestampOracle<Timestamp> + Send + Sync>,
265    table_write_handle: Arc<dyn TableWriteHandle>,
266    catalog_upper: CatalogUpperHandle,
267    internal_cmd_tx: mpsc::UnboundedSender<Message>,
268    now: NowFn,
269    metrics: Metrics,
270    max_attempts: ConfigValHandle<usize>,
271}
272
273impl GroupCommitter {
274    async fn run(mut self) {
275        while let Some(cmd) = self.rx.recv().await {
276            // `commit` may pull a non-mergeable command off the queue while it waits in the
277            // throttle. Process it before receiving anew, to preserve queue order.
278            let mut next = Some(cmd);
279            while let Some(cmd) = next.take() {
280                match cmd {
281                    TableWriteCmd::GroupCommit(request) => {
282                        let span = request.span.clone();
283                        match self.commit(request).instrument(span).await {
284                            ControlFlow::Continue(deferred) => next = deferred,
285                            ControlFlow::Break(()) => return,
286                        }
287                    }
288                    TableWriteCmd::Register { tables, result } => {
289                        let Some(write_ts) = self
290                            .write_to_txns(None, |ts, _advance_to| {
291                                self.table_write_handle.register(ts, tables.clone())
292                            })
293                            .await
294                        else {
295                            return;
296                        };
297                        let _ = result.send(write_ts.timestamp);
298                    }
299                    TableWriteCmd::Forget { ids, result } => {
300                        let Some(write_ts) = self
301                            .write_to_txns(None, |ts, _advance_to| {
302                                self.table_write_handle.forget(ts, ids.clone())
303                            })
304                            .await
305                        else {
306                            return;
307                        };
308                        let _ = result.send(write_ts.timestamp);
309                    }
310                }
311            }
312        }
313    }
314
315    /// Writes at a fresh oracle timestamp and applies a successful write to the oracle.
316    ///
317    /// Returns `None` when the table write worker shuts down.
318    async fn write_to_txns(
319        &self,
320        op_duration_metric: Option<&prometheus::Histogram>,
321        mut op: impl FnMut(Timestamp, Timestamp) -> oneshot::Receiver<Result<(), StorageError>>,
322    ) -> Option<WriteTimestamp> {
323        // Persistent conflicts indicate an unexpected writer. Halt instead of spinning forever.
324        let mut attempt = 0;
325        let write_ts = loop {
326            let max_attempts = self.max_attempts.get().max(1);
327            if attempt >= max_attempts {
328                halt!(
329                    "txns-shard write reached attempt limit {max_attempts} after {attempt} conflicts, rebuilding"
330                );
331            }
332            attempt += 1;
333            let write_ts = self.oracle.write_ts().await;
334
335            // A post-fence retry has an advance frontier above this handle's stale upper, so this
336            // reaches Persist and observes the fence.
337            let catalog_upper_start = Instant::now();
338            self.catalog_upper
339                .advance_upper(write_ts.advance_to)
340                .await
341                .unwrap_or_terminate("unable to advance catalog upper");
342            self.metrics
343                .group_commit_catalog_upper_seconds
344                .observe(catalog_upper_start.elapsed().as_secs_f64());
345
346            let op_start = Instant::now();
347            let op_res = op(write_ts.timestamp, write_ts.advance_to).await;
348            if let Some(metric) = op_duration_metric {
349                metric.observe(op_start.elapsed().as_secs_f64());
350            }
351
352            match op_res {
353                Ok(Ok(())) => break write_ts,
354                Ok(Err(StorageError::InvalidUppers(_))) => {
355                    warn!(
356                        write_ts = %write_ts.timestamp,
357                        attempt,
358                        "txns-shard write conflicted with another writer, retrying at a fresh timestamp"
359                    );
360                    continue;
361                }
362                Ok(Err(other)) => {
363                    Err::<(), _>(other).unwrap_or_terminate("cannot fail to write to txns shard");
364                    unreachable!("unwrap_or_terminate does not return on Err");
365                }
366                Err(_recv) => {
367                    // The outcome is indeterminate. Stop before processing more writes.
368                    warn!("table write worker gone (process shutting down), winding down");
369                    return None;
370                }
371            }
372        };
373
374        let now: Timestamp = (self.now)().into();
375        crate::coord::timeline::check_runaway_write_ts(&now, write_ts.timestamp);
376
377        self.oracle.apply_write(write_ts.timestamp).await;
378
379        Some(write_ts)
380    }
381
382    /// Applies a staged group commit.
383    ///
384    /// Group commits queued during throttling are merged until a registration or forget command
385    /// preserves the queue boundary. `Break` means the table worker shut down.
386    async fn commit(
387        &mut self,
388        mut request: GroupCommitRequest,
389    ) -> ControlFlow<(), Option<TableWriteCmd>> {
390        let mut deferred_cmd = None;
391        // Once the channel is closed, `recv` resolves immediately with `None`. Disable that
392        // select branch then, so the throttle sleep still runs instead of busy-looping.
393        let mut rx_closed = false;
394
395        // Throttle: keep the global write timeline from running ahead of the wall clock. The
396        // peek and the sleep happen here in the committer task, so a slow oracle backend does
397        // not stall the coordinator loop.
398        loop {
399            while deferred_cmd.is_none() {
400                match self.rx.try_recv() {
401                    Ok(TableWriteCmd::GroupCommit(other)) => request.merge(other),
402                    Ok(other) => deferred_cmd = Some(other),
403                    Err(_) => break,
404                }
405            }
406
407            // Internal writes bypass the throttle for mocked clocks. A queued DDL registration or
408            // forget bypasses it because DDL was not previously throttled and blocks the loop.
409            if request.contains_internal_system_write || deferred_cmd.is_some() {
410                break;
411            }
412            let ts = self.oracle.peek_write_ts().await;
413            let now: Timestamp = (self.now)().into();
414            if ts <= now {
415                break;
416            }
417            // A fixed one-second cap bounds clock-regression stalls. Queue wakeups do not extend
418            // this deadline.
419            let remaining_ms = std::cmp::min(ts.saturating_sub(now), Timestamp::from(1_000u64));
420            let sleep = tokio::time::sleep(Duration::from_millis(remaining_ms.into()));
421            tokio::pin!(sleep);
422            loop {
423                tokio::select! {
424                    _ = &mut sleep => break,
425                    cmd = self.rx.recv(), if deferred_cmd.is_none() && !rx_closed => match cmd {
426                        Some(TableWriteCmd::GroupCommit(other)) => request.merge(other),
427                        Some(other) => deferred_cmd = Some(other),
428                        None => rx_closed = true,
429                    },
430                }
431                if request.contains_internal_system_write || deferred_cmd.is_some() {
432                    break;
433                }
434            }
435        }
436
437        let GroupCommitRequest {
438            appends,
439            responses,
440            statement_logging_ids,
441            notifies,
442            write_locks,
443            permits,
444            contains_internal_system_write: _,
445            span: _,
446        } = request;
447
448        let append_metric = self.metrics.append_table_duration_seconds.clone();
449        let Some(write_ts) = self
450            .write_to_txns(Some(&append_metric), |ts, advance_to| {
451                self.table_write_handle
452                    .append(ts, advance_to, appends.clone())
453            })
454            .await
455        else {
456            // Dropping the batch retires its clients through `ExecuteContext`.
457            return ControlFlow::Break(());
458        };
459        let timestamp = write_ts.timestamp;
460
461        let modified_tables: Vec<_> = appends
462            .iter()
463            .filter_map(|(id, updates)| {
464                (id.is_user() && !updates.iter().all(|u| u.is_empty())).then_some(id)
465            })
466            .collect();
467        if !modified_tables.is_empty() {
468            info!(
469                "Appending to tables, {modified_tables:?}, at {timestamp}, advancing to {}",
470                write_ts.advance_to
471            );
472        }
473
474        // Hold permits and locks until `apply_write` completes. Otherwise another write could
475        // proceed while this timestamp is not yet readable.
476        drop(permits);
477        drop(write_locks);
478
479        for notify in notifies {
480            let _ = notify.send(());
481        }
482
483        // The coordinator records timestamps before retiring responses. The applied write
484        // timestamp is also a valid frontier for local read holds.
485        if self
486            .internal_cmd_tx
487            .send(Message::GroupCommitApplied {
488                responses,
489                statement_logging_ids,
490                write_ts: timestamp,
491            })
492            .is_err()
493        {
494            warn!("coordinator shut down before a group commit could be finalized");
495        }
496
497        ControlFlow::Continue(deferred_cmd)
498    }
499}
500
501pub(crate) fn spawn_group_committer(
502    rx: mpsc::UnboundedReceiver<TableWriteCmd>,
503    oracle: Arc<dyn TimestampOracle<Timestamp> + Send + Sync>,
504    table_write_handle: Arc<dyn TableWriteHandle>,
505    catalog_upper: CatalogUpperHandle,
506    internal_cmd_tx: mpsc::UnboundedSender<Message>,
507    now: NowFn,
508    metrics: Metrics,
509    dyncfgs: &ConfigSet,
510) {
511    let committer = GroupCommitter {
512        rx,
513        oracle,
514        table_write_handle,
515        catalog_upper,
516        internal_cmd_tx,
517        now,
518        metrics,
519        max_attempts: GROUP_COMMIT_MAX_ATTEMPTS.handle(dyncfgs),
520    };
521    task::spawn(|| "group_committer", committer.run());
522}
523
524impl Coordinator {
525    /// Send a message to the Coordinate to start a group commit.
526    pub(crate) fn trigger_group_commit(&mut self) {
527        self.group_commit_tx.notify();
528        // Avoid excessive `Message::GroupCommitInitiate` by resetting the periodic table
529        // advancement. The group commit triggered by the message above will already advance all
530        // tables.
531        self.advance_timelines_interval.reset();
532    }
533
534    /// Tries to execute a previously [`DeferredOp`] that requires write locks.
535    ///
536    /// If we can't acquire all of the write locks then we'll defer the plan again and wait for
537    /// the necessary locks to become available.
538    pub(crate) async fn try_deferred(
539        &mut self,
540        conn_id: ConnectionId,
541        acquired_lock: Option<(CatalogItemId, tokio::sync::OwnedMutexGuard<()>)>,
542    ) {
543        // Try getting the deferred op, it may have already been canceled.
544        let Some(op) = self.deferred_write_ops.remove(&conn_id) else {
545            tracing::warn!(%conn_id, "no deferred op found, it must have been canceled?");
546            return;
547        };
548        tracing::info!(%conn_id, "trying deferred plan");
549
550        // If we pre-acquired a lock, try to acquire the rest.
551        let write_locks = match acquired_lock {
552            Some((acquired_gid, acquired_lock)) => {
553                let mut write_locks = WriteLocks::builder(op.required_locks());
554
555                // Insert the one lock we already acquired into the our builder.
556                write_locks.insert_lock(acquired_gid, acquired_lock);
557
558                // Acquire the rest of our locks, filtering out the one we already have.
559                for gid in op.required_locks().filter(|gid| *gid != acquired_gid) {
560                    if let Some(lock) = self.try_grant_object_write_lock(gid) {
561                        write_locks.insert_lock(gid, lock);
562                    }
563                }
564
565                // If we failed to acquire any locks, spawn a task that waits for them to become available.
566                let locks = match write_locks.all_or_nothing(op.conn_id()) {
567                    Ok(locks) => locks,
568                    Err(failed_to_acquire) => {
569                        let acquire_future = self
570                            .grant_object_write_lock(failed_to_acquire)
571                            .map(Option::Some);
572                        self.defer_op(acquire_future, op);
573                        return;
574                    }
575                };
576
577                Some(locks)
578            }
579            None => None,
580        };
581
582        match op {
583            DeferredOp::Plan(mut deferred) => {
584                if let Err(e) = deferred.validity.check(self.catalog()) {
585                    deferred.ctx.retire(Err(e))
586                } else {
587                    // If we pre-acquired our locks, grant them to the session.
588                    if let Some(locks) = write_locks {
589                        let conn_id = deferred.ctx.session().conn_id().clone();
590                        if let Err(existing) =
591                            deferred.ctx.session_mut().try_grant_write_locks(locks)
592                        {
593                            tracing::error!(
594                                %conn_id,
595                                ?existing,
596                                "session already write locks granted?",
597                            );
598                            return deferred.ctx.retire(Err(AdapterError::WrongSetOfLocks));
599                        }
600                    };
601
602                    // Note: This plan is not guaranteed to run, it may get deferred again.
603                    self.sequence_plan(
604                        deferred.ctx,
605                        deferred.plan,
606                        deferred.resolved_ids,
607                        deferred.sql_impl_resolved_ids,
608                    )
609                    .await;
610                }
611            }
612            DeferredOp::Write(DeferredWrite {
613                span,
614                writes,
615                pending_txn,
616            }) => {
617                self.submit_write(PendingWriteTxn::User {
618                    span,
619                    writes,
620                    write_locks,
621                    responder: UserWriteResponder::Session(pending_txn),
622                });
623            }
624        }
625    }
626
627    /// Stages pending writes for the group committer.
628    ///
629    /// Writes blocked on locks are deferred. Included writes share one timestamp.
630    #[instrument(name = "coord::stage_group_commit")]
631    pub(crate) fn stage_group_commit(&mut self, permit: Option<GroupCommitPermit>) {
632        let mut validated_writes = Vec::new();
633        let mut deferred_writes = Vec::new();
634        let mut group_write_locks = GroupCommitWriteLocks::default();
635
636        // TODO(parkmycar): Refactor away this allocation. Currently `drain(..)` requires holding
637        // a mutable borrow on the Coordinator and so does trying to grant a write lock.
638        let pending_writes: Vec<_> = self.pending_writes.drain(..).collect();
639
640        // Validate, merge, and possibly acquire write locks for as many pending writes as possible.
641        for pending_write in pending_writes {
642            match pending_write {
643                // We always allow system writes to proceed.
644                PendingWriteTxn::System { .. } => validated_writes.push(pending_write),
645                // We have a set of locks! Validate they're correct (expected).
646                PendingWriteTxn::User {
647                    span,
648                    write_locks: Some(write_locks),
649                    writes,
650                    responder: UserWriteResponder::Session(pending_txn),
651                } => match write_locks.validate(writes.keys().copied()) {
652                    Ok(validated_locks) => {
653                        // Merge all of our write locks together since we can allow concurrent
654                        // writes at the same timestamp.
655                        group_write_locks.merge(validated_locks);
656
657                        let validated_write = PendingWriteTxn::User {
658                            span,
659                            writes,
660                            write_locks: None,
661                            responder: UserWriteResponder::Session(pending_txn),
662                        };
663                        validated_writes.push(validated_write);
664                    }
665                    // This is very unexpected since callers of this method should be validating.
666                    //
667                    // We cannot allow these write to occur since if the correct set of locks was
668                    // not taken we could violate serializability.
669                    Err(missing) => {
670                        let writes: Vec<_> = writes.keys().collect();
671                        panic!(
672                            "got to group commit with partial set of locks!\nmissing: {:?}, writes: {:?}, txn: {:?}",
673                            missing, writes, pending_txn,
674                        );
675                    }
676                },
677                // If we don't have any locks, try to acquire them, otherwise defer the write.
678                PendingWriteTxn::User {
679                    span,
680                    writes,
681                    write_locks: None,
682                    responder: UserWriteResponder::Session(pending_txn),
683                } => {
684                    let missing = group_write_locks.missing_locks(writes.keys().copied());
685
686                    if missing.is_empty() {
687                        // We have all the locks! Queue the pending write.
688                        let validated_write = PendingWriteTxn::User {
689                            span,
690                            writes,
691                            write_locks: None,
692                            responder: UserWriteResponder::Session(pending_txn),
693                        };
694                        validated_writes.push(validated_write);
695                    } else {
696                        // Try to acquire the locks we're missing.
697                        let mut just_in_time_locks = WriteLocks::builder(missing.clone());
698                        for collection in missing {
699                            if let Some(lock) = self.try_grant_object_write_lock(collection) {
700                                just_in_time_locks.insert_lock(collection, lock);
701                            }
702                        }
703
704                        match just_in_time_locks.all_or_nothing(pending_txn.ctx.session().conn_id())
705                        {
706                            // We acquired all of the locks! Proceed with the write.
707                            Ok(locks) => {
708                                group_write_locks.merge(locks);
709                                let validated_write = PendingWriteTxn::User {
710                                    span,
711                                    writes,
712                                    write_locks: None,
713                                    responder: UserWriteResponder::Session(pending_txn),
714                                };
715                                validated_writes.push(validated_write);
716                            }
717                            // Darn. We couldn't acquire the locks, defer the write.
718                            Err(missing) => {
719                                let acquire_future =
720                                    self.grant_object_write_lock(missing).map(Option::Some);
721                                let write = DeferredWrite {
722                                    span,
723                                    writes,
724                                    pending_txn,
725                                };
726                                deferred_writes.push((acquire_future, write));
727                            }
728                        }
729                    }
730                }
731            }
732        }
733
734        // Queue all of our deferred ops.
735        for (acquire_future, write) in deferred_writes {
736            self.defer_op(acquire_future, DeferredOp::Write(write));
737        }
738
739        let contains_internal_system_write = validated_writes
740            .iter()
741            .any(|write| write.is_internal_system());
742
743        let mut appends: BTreeMap<CatalogItemId, SmallVec<[TableData; 1]>> = BTreeMap::new();
744        let mut responses = Vec::with_capacity(validated_writes.len());
745        let mut statement_logging_ids = Vec::new();
746        let mut notifies = Vec::new();
747
748        for validated_write_txn in validated_writes {
749            match validated_write_txn {
750                PendingWriteTxn::User {
751                    span: _,
752                    writes,
753                    write_locks,
754                    responder:
755                        UserWriteResponder::Session(PendingTxn {
756                            ctx,
757                            response,
758                            action,
759                        }),
760                } => {
761                    assert_none!(write_locks, "should have merged together all locks above");
762                    for (id, table_data) in writes {
763                        // If the table that some write was targeting has been deleted while the
764                        // write was waiting, then the write will be ignored and we respond to the
765                        // client that the write was successful. This is only possible if the write
766                        // and the delete were concurrent. Therefore, we are free to order the
767                        // write before the delete without violating any consistency guarantees.
768                        if self.catalog().try_get_entry(&id).is_some() {
769                            appends.entry(id).or_default().extend(table_data);
770                        }
771                    }
772                    if let Some(id) = ctx.extra().contents() {
773                        statement_logging_ids.push(id);
774                    }
775
776                    responses.push(CompletedClientTransmitter::new(ctx, response, action));
777                }
778                PendingWriteTxn::System { updates, source } => {
779                    for update in updates {
780                        appends.entry(update.id).or_default().push(update.data);
781                    }
782                    // Once the write completes we notify any waiters.
783                    match source {
784                        BuiltinTableUpdateSource::Internal(tx)
785                        | BuiltinTableUpdateSource::Background(tx) => notifies.push(tx),
786                    }
787                }
788            }
789        }
790
791        // Consolidate all Rows for a given table. We do not consolidate the
792        // staged batches, that's up to whoever staged them.
793        let mut all_appends = Vec::with_capacity(appends.len());
794        for (item_id, table_data) in appends.into_iter() {
795            let mut all_rows = Vec::new();
796            let mut all_data = Vec::new();
797            for data in table_data {
798                match data {
799                    TableData::Rows(rows) => all_rows.extend(rows),
800                    TableData::Batches(_) => all_data.push(data),
801                }
802            }
803            differential_dataflow::consolidation::consolidate(&mut all_rows);
804            all_data.push(TableData::Rows(all_rows));
805
806            // TODO(parkmycar): Use SmallVec throughout.
807            all_appends.push((item_id, all_data));
808        }
809
810        let appends: Vec<_> = all_appends
811            .into_iter()
812            .map(|(id, updates)| {
813                let gid = self.catalog().get_entry(&id).latest_global_id();
814                (gid, updates)
815            })
816            .collect();
817
818        // Always enqueue keepalives so registered tables remain readable at the oracle read ts.
819        let request = GroupCommitRequest {
820            appends,
821            responses,
822            statement_logging_ids,
823            notifies,
824            write_locks: group_write_locks,
825            permits: permit.into_iter().collect(),
826            contains_internal_system_write,
827            span: Span::current(),
828        };
829        if self
830            .group_committer_tx
831            .send(TableWriteCmd::GroupCommit(request))
832            .is_err()
833        {
834            // Dropping the request retires its clients and notifies its waiters.
835            warn!("group committer task gone, dropping staged group commit");
836        }
837    }
838
839    /// Registers `tables` in FIFO order and returns the applied timestamp.
840    pub(crate) async fn register_tables_via_committer(
841        &self,
842        tables: Vec<TableRegistration>,
843    ) -> Timestamp {
844        let (tx, rx) = oneshot::channel();
845        if self
846            .group_committer_tx
847            .send(TableWriteCmd::Register { tables, result: tx })
848            .is_err()
849        {
850            halt!("group committer terminated before a table registration could be submitted");
851        }
852        match rx.await {
853            Ok(ts) => ts,
854            Err(_) => halt!("group committer terminated with a table registration outstanding"),
855        }
856    }
857
858    /// Forgets `ids` in FIFO order and returns the applied timestamp.
859    pub(crate) async fn forget_tables_via_committer(&self, ids: Vec<GlobalId>) -> Timestamp {
860        let (tx, rx) = oneshot::channel();
861        if self
862            .group_committer_tx
863            .send(TableWriteCmd::Forget { ids, result: tx })
864            .is_err()
865        {
866            halt!("group committer terminated before a table forget could be submitted");
867        }
868        match rx.await {
869            Ok(ts) => ts,
870            Err(_) => halt!("group committer terminated with a table forget outstanding"),
871        }
872    }
873
874    /// Submit a write to be executed during the next group commit and trigger a group commit.
875    pub(crate) fn submit_write(&mut self, pending_write_txn: PendingWriteTxn) {
876        if self.controller.read_only() {
877            panic!(
878                "attempting table write in read-only mode: {:?}",
879                pending_write_txn
880            );
881        }
882        self.pending_writes.push(pending_write_txn);
883        self.trigger_group_commit();
884    }
885
886    /// Append some [`BuiltinTableUpdate`]s, with various degrees of waiting and blocking.
887    pub(crate) fn builtin_table_update<'a>(&'a mut self) -> BuiltinTableAppend<'a> {
888        BuiltinTableAppend { coord: self }
889    }
890
891    pub(crate) fn defer_op<F>(&mut self, acquire_future: F, op: DeferredOp)
892    where
893        F: Future<Output = Option<(CatalogItemId, tokio::sync::OwnedMutexGuard<()>)>>
894            + Send
895            + 'static,
896    {
897        let conn_id = op.conn_id().clone();
898
899        // Track all of our deferred ops.
900        let is_optimistic = op.can_be_optimistically_retried();
901        self.deferred_write_ops.insert(conn_id.clone(), op);
902
903        let internal_cmd_tx = self.internal_cmd_tx.clone();
904        let conn_id_ = conn_id.clone();
905        mz_ore::task::spawn(|| format!("defer op {conn_id_}"), async move {
906            tracing::info!(%conn_id, "deferring plan");
907            // Once we can acquire the first failed lock, try running the deferred plan.
908            //
909            // Note: This does not guarantee the plan will be able to run, there might be
910            // other locks that we later fail to get.
911            let acquired_lock = acquire_future.await;
912
913            // Some operations, e.g. blind INSERTs, can be optimistically retried, meaning we
914            // can run multiple at once. In those cases we don't hold the lock so we retry all
915            // blind writes for a single object.
916            let acquired_lock = match (acquired_lock, is_optimistic) {
917                (Some(_lock), true) => None,
918                (Some(lock), false) => Some(lock),
919                (None, _) => None,
920            };
921
922            // If this send fails then the Coordinator is shutting down.
923            let _ = internal_cmd_tx.send(Message::TryDeferred {
924                conn_id,
925                acquired_lock,
926            });
927        });
928    }
929
930    /// Returns a future that waits until it can get an exclusive lock on the specified collection.
931    pub(crate) fn grant_object_write_lock(
932        &mut self,
933        object_id: CatalogItemId,
934    ) -> impl Future<Output = (CatalogItemId, OwnedMutexGuard<()>)> + 'static {
935        let write_lock_handle = self
936            .write_locks
937            .entry(object_id)
938            .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(())));
939        let write_lock_handle = Arc::clone(write_lock_handle);
940
941        write_lock_handle
942            .lock_owned()
943            .map(move |guard| (object_id, guard))
944    }
945
946    /// Lazily creates the lock for the provided `object_id`, and grants it if possible, returns
947    /// `None` if the lock is already held.
948    pub(crate) fn try_grant_object_write_lock(
949        &mut self,
950        object_id: CatalogItemId,
951    ) -> Option<OwnedMutexGuard<()>> {
952        let write_lock_handle = self
953            .write_locks
954            .entry(object_id)
955            .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(())));
956        let write_lock_handle = Arc::clone(write_lock_handle);
957
958        write_lock_handle.try_lock_owned().ok()
959    }
960}
961
962/// Helper struct to run a builtin table append.
963pub struct BuiltinTableAppend<'a> {
964    coord: &'a mut Coordinator,
965}
966
967/// `Future` that notifies when a builtin table write has completed.
968///
969/// Callers that expose completion of an operation whose builtin-table write is
970/// user-observable should await this future before sending that completion. It
971/// is safe to drop the future only when the caller does not provide such an
972/// ordering guarantee, or when the future is known to resolve immediately.
973///
974/// Note: builtin table writes need to talk to persist, which can take 100s of milliseconds. This
975/// type allows you to execute a builtin table write, e.g. via [`BuiltinTableAppend::execute`], and
976/// wait for it to complete, while other long running tasks are concurrently executing.
977pub type BuiltinTableAppendNotify = Pin<Box<dyn Future<Output = ()> + Send + Sync + 'static>>;
978
979/// Completion handle for a builtin-table append response barrier.
980pub struct BuiltinTableAppendCompletion {
981    notify: BuiltinTableAppendNotify,
982}
983
984impl BuiltinTableAppendCompletion {
985    pub fn new(notify: BuiltinTableAppendNotify) -> Self {
986        Self { notify }
987    }
988
989    pub fn into_notify(self) -> BuiltinTableAppendNotify {
990        self.notify
991    }
992}
993
994impl<'a> BuiltinTableAppend<'a> {
995    /// Submit a write to a system table to be executed during the next group commit. This method
996    /// __does not__ trigger a group commit.
997    ///
998    /// This is useful for non-critical writes like metric updates because it allows us to piggy
999    /// back off the next group commit instead of triggering a potentially expensive group commit.
1000    ///
1001    /// Note: __do not__ call this for DDL which needs the system tables updated immediately.
1002    ///
1003    /// Note: When in read-only mode, this will buffer the update and return
1004    /// immediately.
1005    pub fn background(self, mut updates: Vec<BuiltinTableUpdate>) -> BuiltinTableAppendNotify {
1006        if self.coord.controller.read_only() {
1007            self.coord
1008                .buffered_builtin_table_updates
1009                .as_mut()
1010                .expect("in read-only mode")
1011                .append(&mut updates);
1012
1013            return Box::pin(futures::future::ready(()));
1014        }
1015
1016        let (tx, rx) = oneshot::channel();
1017        self.coord.pending_writes.push(PendingWriteTxn::System {
1018            updates,
1019            source: BuiltinTableUpdateSource::Background(tx),
1020        });
1021
1022        Box::pin(rx.map(|_| ()))
1023    }
1024
1025    /// Submits a write to be executed during the next group commit __and__ triggers a group commit.
1026    ///
1027    /// Returns a `Future` that resolves when the write has completed, does not block the
1028    /// Coordinator.
1029    ///
1030    /// Note: When in read-only mode, this will buffer the update and the
1031    /// returned future will resolve immediately, without the update actually
1032    /// having been written.
1033    pub fn defer(self, mut updates: Vec<BuiltinTableUpdate>) -> BuiltinTableAppendNotify {
1034        if self.coord.controller.read_only() {
1035            self.coord
1036                .buffered_builtin_table_updates
1037                .as_mut()
1038                .expect("in read-only mode")
1039                .append(&mut updates);
1040
1041            return Box::pin(futures::future::ready(()));
1042        }
1043
1044        let (tx, rx) = oneshot::channel();
1045        self.coord.pending_writes.push(PendingWriteTxn::System {
1046            updates,
1047            source: BuiltinTableUpdateSource::Internal(tx),
1048        });
1049        self.coord.trigger_group_commit();
1050
1051        Box::pin(rx.map(|_| ()))
1052    }
1053
1054    /// Submits a system-table write immediately and returns its completion future.
1055    ///
1056    /// In read-only mode, buffers the update and returns a ready future.
1057    pub fn execute(self, mut updates: Vec<BuiltinTableUpdate>) -> BuiltinTableAppendNotify {
1058        if self.coord.controller.read_only() {
1059            self.coord
1060                .buffered_builtin_table_updates
1061                .as_mut()
1062                .expect("in read-only mode")
1063                .append(&mut updates);
1064
1065            return Box::pin(futures::future::ready(()));
1066        }
1067
1068        let (tx, rx) = oneshot::channel();
1069
1070        // DDL system writes bypass the periodic wait. Extremely fast DDL can advance the global
1071        // timeline ahead of the wall clock, delaying later queries without affecting correctness.
1072        self.coord.pending_writes.push(PendingWriteTxn::System {
1073            updates,
1074            source: BuiltinTableUpdateSource::Internal(tx),
1075        });
1076        self.coord.stage_group_commit(None);
1077
1078        // The staged commit already advances every table.
1079        self.coord.advance_timelines_interval.reset();
1080
1081        Box::pin(rx.map(|_| ()))
1082    }
1083}
1084
1085/// Returns two sides of a "channel" that can be used to notify the coordinator when we want a
1086/// group commit to be run.
1087pub fn notifier() -> (GroupCommitNotifier, GroupCommitWaiter) {
1088    let notify = Arc::new(Notify::new());
1089    let in_progress = Arc::new(Semaphore::new(1));
1090
1091    let notifier = GroupCommitNotifier {
1092        notify: Arc::clone(&notify),
1093    };
1094    let waiter = GroupCommitWaiter {
1095        notify,
1096        in_progress,
1097    };
1098
1099    (notifier, waiter)
1100}
1101
1102/// A handle that allows us to notify the coordinator that a group commit should be run at some
1103/// point in the future.
1104#[derive(Debug, Clone)]
1105pub struct GroupCommitNotifier {
1106    /// Tracks if there are any outstanding group commits.
1107    notify: Arc<Notify>,
1108}
1109
1110impl GroupCommitNotifier {
1111    /// Notifies the [`GroupCommitWaiter`] that we'd like a group commit to be run.
1112    pub fn notify(&self) {
1113        self.notify.notify_one()
1114    }
1115}
1116
1117/// A handle that returns a future when a group commit needs to be run, and one is not currently
1118/// being run.
1119#[derive(Debug)]
1120pub struct GroupCommitWaiter {
1121    /// Tracks if there are any outstanding group commits.
1122    notify: Arc<Notify>,
1123    /// Distributes permits which tracks in progress group commits.
1124    in_progress: Arc<Semaphore>,
1125}
1126static_assertions::assert_not_impl_all!(GroupCommitWaiter: Clone);
1127
1128impl GroupCommitWaiter {
1129    /// Returns a permit for a group commit, once a permit is available _and_ there someone
1130    /// requested a group commit to be run.
1131    ///
1132    /// # Cancel Safety
1133    ///
1134    /// * Waiting on the returned Future is cancel safe because we acquire an in-progress permit
1135    ///   before waiting for notifications. If the Future gets dropped after acquiring a permit but
1136    ///   before a group commit is queued, we'll release the permit which can be acquired by the
1137    ///   next caller.
1138    ///
1139    pub async fn ready(&self) -> GroupCommitPermit {
1140        let permit = Semaphore::acquire_owned(Arc::clone(&self.in_progress))
1141            .await
1142            .expect("semaphore should not close");
1143
1144        // Note: We must wait for notifies _after_ waiting for a permit to be acquired for cancel
1145        // safety.
1146        self.notify.notified().await;
1147
1148        GroupCommitPermit {
1149            _permit: Some(permit),
1150        }
1151    }
1152}
1153
1154/// A permit to run a group commit, this must be kept alive for the entire duration of the commit.
1155///
1156/// Note: We sometimes want to throttle how many group commits are running at once, which this
1157/// permit allows us to do.
1158#[derive(Debug)]
1159pub struct GroupCommitPermit {
1160    /// Permit that is preventing other group commits from running.
1161    ///
1162    /// Only `None` if the permit has been moved into a tokio task for waiting.
1163    _permit: Option<OwnedSemaphorePermit>,
1164}
1165
1166/// When we start a [`Session`] we need to update some builtin tables, but we don't want to wait for
1167/// these writes to complete for two reasons:
1168///
1169/// 1. Doing a write can take a relatively long time.
1170/// 2. Decoupling the write from the session start allows us to batch multiple writes together, if
1171///    sessions are being created with a high frequency.
1172///
1173/// So, as an optimization we do not wait for these writes to complete. But if a [`Session`] tries
1174/// to query any of these builtin objects, we need to block that query on the writes completing to
1175/// maintain linearizability.
1176///
1177/// Warning: this already clears the wait flag (i.e., it calls `clear_builtin_table_updates`).
1178///
1179/// TODO(peek-seq): After we delete the old peek sequencing, we can remove the first component of
1180/// the return tuple.
1181pub(crate) fn waiting_on_startup_appends(
1182    catalog: &Catalog,
1183    session: &mut Session,
1184    plan: &Plan,
1185) -> Option<(BTreeSet<CatalogItemId>, BoxFuture<'static, ()>)> {
1186    // TODO(parkmycar): We need to check transitive uses here too if we ever move the
1187    // referenced builtin tables out of mz_internal, or we allow creating views on
1188    // mz_internal objects.
1189    let depends_on = match plan {
1190        Plan::Select(plan) => plan.source.depends_on(),
1191        Plan::ReadThenWrite(plan) => plan.selection.depends_on(),
1192        Plan::ShowColumns(plan) => plan.select_plan.source.depends_on(),
1193        Plan::Subscribe(plan) => plan.from.depends_on(),
1194        Plan::ExplainPlan(ExplainPlanPlan {
1195            explainee: Explainee::Statement(ExplaineeStatement::Select { plan, .. }),
1196            ..
1197        }) => plan.source.depends_on(),
1198        Plan::ExplainTimestamp(ExplainTimestampPlan { raw_plan, .. }) => raw_plan.depends_on(),
1199        Plan::CreateConnection(_)
1200        | Plan::CreateDatabase(_)
1201        | Plan::CreateSchema(_)
1202        | Plan::CreateRole(_)
1203        | Plan::CreateNetworkPolicy(_)
1204        | Plan::CreateCluster(_)
1205        | Plan::CreateClusterReplica(_)
1206        | Plan::CreateSource(_)
1207        | Plan::CreateSources(_)
1208        | Plan::CreateSecret(_)
1209        | Plan::CreateSink(_)
1210        | Plan::CreateTable(_)
1211        | Plan::CreateView(_)
1212        | Plan::CreateMaterializedView(_)
1213        | Plan::CreateIndex(_)
1214        | Plan::CreateType(_)
1215        | Plan::Comment(_)
1216        | Plan::DiscardTemp
1217        | Plan::DiscardAll
1218        | Plan::DropObjects(_)
1219        | Plan::DropOwned(_)
1220        | Plan::EmptyQuery
1221        | Plan::ShowAllVariables
1222        | Plan::ShowCreate(_)
1223        | Plan::ShowVariable(_)
1224        | Plan::InspectShard(_)
1225        | Plan::SetVariable(_)
1226        | Plan::ResetVariable(_)
1227        | Plan::SetTransaction(_)
1228        | Plan::StartTransaction(_)
1229        | Plan::CommitTransaction(_)
1230        | Plan::AbortTransaction(_)
1231        | Plan::CopyFrom(_)
1232        | Plan::CopyTo(_)
1233        | Plan::ExplainPlan(_)
1234        | Plan::ExplainPushdown(_)
1235        | Plan::ExplainSinkSchema(_)
1236        | Plan::Insert(_)
1237        | Plan::AlterNetworkPolicy(_)
1238        | Plan::AlterNoop(_)
1239        | Plan::AlterClusterRename(_)
1240        | Plan::AlterClusterSwap(_)
1241        | Plan::AlterClusterReplicaRename(_)
1242        | Plan::AlterCluster(_)
1243        | Plan::AlterConnection(_)
1244        | Plan::AlterSource(_)
1245        | Plan::AlterSetCluster(_)
1246        | Plan::AlterItemRename(_)
1247        | Plan::AlterRetainHistory(_)
1248        | Plan::AlterSourceTimestampInterval(_)
1249        | Plan::AlterSchemaRename(_)
1250        | Plan::AlterSchemaSwap(_)
1251        | Plan::AlterSecret(_)
1252        | Plan::AlterSink(_)
1253        | Plan::AlterSystemSet(_)
1254        | Plan::AlterSystemReset(_)
1255        | Plan::AlterSystemResetAll(_)
1256        | Plan::AlterRole(_)
1257        | Plan::AlterOwner(_)
1258        | Plan::AlterTableAddColumn(_)
1259        | Plan::AlterMaterializedViewApplyReplacement(_)
1260        | Plan::Declare(_)
1261        | Plan::Fetch(_)
1262        | Plan::Close(_)
1263        | Plan::Prepare(_)
1264        | Plan::Execute(_)
1265        | Plan::Deallocate(_)
1266        | Plan::Raise(_)
1267        | Plan::GrantRole(_)
1268        | Plan::RevokeRole(_)
1269        | Plan::GrantPrivileges(_)
1270        | Plan::RevokePrivileges(_)
1271        | Plan::AlterDefaultPrivileges(_)
1272        | Plan::ReassignOwned(_)
1273        | Plan::ValidateConnection(_)
1274        | Plan::SideEffectingFunc(_) => BTreeSet::default(),
1275    };
1276    let depends_on_required_id = REQUIRED_BUILTIN_TABLES
1277        .iter()
1278        .map(|table| catalog.resolve_builtin_table(&**table))
1279        .any(|id| {
1280            catalog
1281                .get_global_ids(&id)
1282                .any(|gid| depends_on.contains(&gid))
1283        });
1284
1285    // If our plan does not depend on any required ID, then we don't need to
1286    // wait for any builtin writes to occur.
1287    if !depends_on_required_id {
1288        return None;
1289    }
1290
1291    // Even if we depend on a builtin table, there's no need to wait if the
1292    // writes have already completed.
1293    //
1294    // TODO(parkmycar): As an optimization we should add a `Notify` type to
1295    // `mz_ore` that allows peeking. If the builtin table writes have already
1296    // completed then there is no need to defer this plan.
1297    match session.clear_builtin_table_updates() {
1298        Some(wait_future) => {
1299            let depends_on = depends_on
1300                .into_iter()
1301                .map(|gid| catalog.get_entry_by_global_id(&gid).id())
1302                .collect();
1303            Some((depends_on, wait_future.boxed()))
1304        }
1305        None => None,
1306    }
1307}
1308
1309#[cfg(test)]
1310mod tests {
1311    use std::sync::Mutex;
1312    use std::sync::atomic::{AtomicUsize, Ordering};
1313
1314    use async_trait::async_trait;
1315    use mz_ore::metrics::MetricsRegistry;
1316    use mz_ore::now::SYSTEM_TIME;
1317    use timely::progress::Antichain;
1318
1319    use super::*;
1320    use crate::catalog::Catalog;
1321
1322    #[derive(Debug, Default)]
1323    struct MemTimestampOracle {
1324        read_write_ts: Mutex<(Timestamp, Timestamp)>,
1325        apply_writes: AtomicUsize,
1326    }
1327
1328    impl MemTimestampOracle {
1329        fn starting_at(ts: Timestamp) -> Self {
1330            Self {
1331                read_write_ts: Mutex::new((ts, ts)),
1332                apply_writes: AtomicUsize::new(0),
1333            }
1334        }
1335    }
1336
1337    #[async_trait]
1338    impl TimestampOracle<Timestamp> for MemTimestampOracle {
1339        async fn write_ts(&self) -> WriteTimestamp {
1340            let (read_ts, write_ts) = &mut *self.read_write_ts.lock().expect("lock poisoned");
1341            let new_write_ts = std::cmp::max(*read_ts, *write_ts).step_forward();
1342            *write_ts = new_write_ts;
1343            WriteTimestamp {
1344                timestamp: new_write_ts,
1345                advance_to: new_write_ts.step_forward(),
1346            }
1347        }
1348
1349        async fn peek_write_ts(&self) -> Timestamp {
1350            let (_, write_ts) = &*self.read_write_ts.lock().expect("lock poisoned");
1351            *write_ts
1352        }
1353
1354        async fn read_ts(&self) -> Timestamp {
1355            let (read_ts, _) = &*self.read_write_ts.lock().expect("lock poisoned");
1356            *read_ts
1357        }
1358
1359        async fn apply_write(&self, lower_bound: Timestamp) {
1360            self.apply_writes.fetch_add(1, Ordering::SeqCst);
1361            let (read_ts, write_ts) = &mut *self.read_write_ts.lock().expect("lock poisoned");
1362            *read_ts = std::cmp::max(*read_ts, lower_bound);
1363            *write_ts = std::cmp::max(*read_ts, *write_ts);
1364        }
1365    }
1366
1367    #[derive(Debug)]
1368    struct ConflictingTableWriteHandle {
1369        conflicts: usize,
1370        calls: AtomicUsize,
1371        write_timestamps: Mutex<Vec<Timestamp>>,
1372    }
1373
1374    impl ConflictingTableWriteHandle {
1375        fn new(conflicts: usize) -> Self {
1376            Self {
1377                conflicts,
1378                calls: AtomicUsize::new(0),
1379                write_timestamps: Mutex::new(Vec::new()),
1380            }
1381        }
1382
1383        fn respond(&self, write_ts: Timestamp) -> oneshot::Receiver<Result<(), StorageError>> {
1384            let call = self.calls.fetch_add(1, Ordering::SeqCst);
1385            self.write_timestamps
1386                .lock()
1387                .expect("lock poisoned")
1388                .push(write_ts);
1389            let (tx, rx) = oneshot::channel();
1390            let result = if call < self.conflicts {
1391                Err(StorageError::InvalidUppers(vec![
1392                    mz_storage_types::controller::InvalidUpper {
1393                        id: GlobalId::User(1),
1394                        current_upper: Antichain::from_elem(write_ts.step_forward()),
1395                    },
1396                ]))
1397            } else {
1398                Ok(())
1399            };
1400            tx.send(result).expect("receiver still in scope");
1401            rx
1402        }
1403    }
1404
1405    impl TableWriteHandle for ConflictingTableWriteHandle {
1406        fn append(
1407            &self,
1408            write_ts: Timestamp,
1409            _advance_to: Timestamp,
1410            _commands: Vec<(GlobalId, Vec<TableData>)>,
1411        ) -> oneshot::Receiver<Result<(), StorageError>> {
1412            self.respond(write_ts)
1413        }
1414
1415        fn register(
1416            &self,
1417            register_ts: Timestamp,
1418            _tables: Vec<TableRegistration>,
1419        ) -> oneshot::Receiver<Result<(), StorageError>> {
1420            self.respond(register_ts)
1421        }
1422
1423        fn forget(
1424            &self,
1425            forget_ts: Timestamp,
1426            _ids: Vec<GlobalId>,
1427        ) -> oneshot::Receiver<Result<(), StorageError>> {
1428            self.respond(forget_ts)
1429        }
1430    }
1431
1432    #[mz_ore::test(tokio::test)]
1433    #[cfg_attr(miri, ignore)] // too slow
1434    async fn test_write_to_txns_conflict_retry() {
1435        Catalog::with_debug(|catalog| async move {
1436            // Start beyond the catalog upper to exercise the durable advance path.
1437            let initial_upper = catalog.current_upper().await;
1438            let oracle = Arc::new(MemTimestampOracle::starting_at(initial_upper));
1439            let handle = Arc::new(ConflictingTableWriteHandle::new(1));
1440            let oracle_dyn: Arc<dyn TimestampOracle<Timestamp> + Send + Sync> =
1441                Arc::<MemTimestampOracle>::clone(&oracle);
1442            let handle_dyn: Arc<dyn TableWriteHandle> =
1443                Arc::<ConflictingTableWriteHandle>::clone(&handle);
1444            let (_tx, rx) = mpsc::unbounded_channel();
1445            let (internal_cmd_tx, _internal_cmd_rx) = mpsc::unbounded_channel();
1446            let committer = GroupCommitter {
1447                rx,
1448                oracle: oracle_dyn,
1449                table_write_handle: handle_dyn,
1450                catalog_upper: catalog.upper_handle(),
1451                internal_cmd_tx,
1452                now: SYSTEM_TIME.clone(),
1453                metrics: Metrics::register_into(&MetricsRegistry::new()),
1454                max_attempts: ConfigValHandle::disconnected(2),
1455            };
1456
1457            let write_ts = committer
1458                .write_to_txns(None, |ts, advance_to| {
1459                    assert_eq!(advance_to, ts.step_forward());
1460                    handle.append(ts, advance_to, Vec::new())
1461                })
1462                .await
1463                .expect("worker stays alive in this test");
1464
1465            let attempts = handle
1466                .write_timestamps
1467                .lock()
1468                .expect("lock poisoned")
1469                .clone();
1470            assert_eq!(attempts.len(), 2, "one conflict, one success");
1471            assert!(
1472                attempts[0] > initial_upper,
1473                "attempts start past the initial catalog upper: {attempts:?} vs {initial_upper}"
1474            );
1475            assert!(
1476                attempts[0] < attempts[1],
1477                "retry must use a fresh, larger timestamp: {attempts:?}"
1478            );
1479            assert_eq!(write_ts.timestamp, attempts[1]);
1480
1481            assert_eq!(oracle.apply_writes.load(Ordering::SeqCst), 1);
1482            assert_eq!(oracle.read_ts().await, write_ts.timestamp);
1483
1484            let catalog_upper = catalog.current_upper().await;
1485            assert!(
1486                catalog_upper >= write_ts.advance_to,
1487                "catalog upper {catalog_upper} must cover the write's advance_to {}",
1488                write_ts.advance_to
1489            );
1490
1491            catalog.expire().await;
1492        })
1493        .await;
1494    }
1495
1496    #[mz_ore::test(tokio::test)]
1497    #[cfg_attr(miri, ignore)] // too slow
1498    async fn test_write_to_txns_zero_limit_allows_one_attempt() {
1499        Catalog::with_debug(|catalog| async move {
1500            let initial_upper = catalog.current_upper().await;
1501            let oracle = Arc::new(MemTimestampOracle::starting_at(initial_upper));
1502            let handle = Arc::new(ConflictingTableWriteHandle::new(0));
1503            let oracle_dyn: Arc<dyn TimestampOracle<Timestamp> + Send + Sync> =
1504                Arc::<MemTimestampOracle>::clone(&oracle);
1505            let handle_dyn: Arc<dyn TableWriteHandle> =
1506                Arc::<ConflictingTableWriteHandle>::clone(&handle);
1507            let (_tx, rx) = mpsc::unbounded_channel();
1508            let (internal_cmd_tx, _internal_cmd_rx) = mpsc::unbounded_channel();
1509            let committer = GroupCommitter {
1510                rx,
1511                oracle: oracle_dyn,
1512                table_write_handle: handle_dyn,
1513                catalog_upper: catalog.upper_handle(),
1514                internal_cmd_tx,
1515                now: SYSTEM_TIME.clone(),
1516                metrics: Metrics::register_into(&MetricsRegistry::new()),
1517                max_attempts: ConfigValHandle::disconnected(0),
1518            };
1519
1520            let write_ts = committer
1521                .write_to_txns(None, |ts, advance_to| {
1522                    handle.append(ts, advance_to, Vec::new())
1523                })
1524                .await
1525                .expect("zero limit permits one successful attempt");
1526
1527            let attempts = handle
1528                .write_timestamps
1529                .lock()
1530                .expect("lock poisoned")
1531                .clone();
1532            assert_eq!(attempts, vec![write_ts.timestamp]);
1533            assert_eq!(oracle.apply_writes.load(Ordering::SeqCst), 1);
1534
1535            catalog.expire().await;
1536        })
1537        .await;
1538    }
1539}