Skip to main content

mz_storage_controller/
persist_handles.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 tokio tasks (and support machinery) for dealing with the persist handles
11//! that the storage controller needs to hold.
12
13use std::collections::{BTreeMap, BTreeSet};
14use std::fmt::Debug;
15use std::fmt::Write;
16use std::sync::Arc;
17
18use futures::StreamExt;
19use futures::stream::FuturesUnordered;
20use itertools::Itertools;
21use mz_ore::tracing::OpenTelemetryContext;
22use mz_persist_client::write::WriteHandle;
23use mz_persist_client::{Diagnostics, PersistClient, ShardId};
24use mz_persist_types::codec_impls::UnitSchema;
25use mz_repr::{GlobalId, Timestamp};
26use mz_storage_client::client::{TableData, Update};
27use mz_storage_client::controller::TableRegistration;
28use mz_storage_types::StorageDiff;
29use mz_storage_types::controller::{InvalidUpper, TxnsCodecRow};
30use mz_storage_types::sources::SourceData;
31use mz_txn_wal::txns::{Tidy, TxnsHandle};
32use timely::progress::Antichain;
33use tokio::sync::mpsc::UnboundedSender;
34use tokio::sync::oneshot;
35use tracing::{Instrument, Span, debug, info_span};
36
37use crate::StorageError;
38
39mod read_only_table_worker;
40
41#[derive(Debug, Clone)]
42pub struct PersistTableWriteWorker {
43    inner: Arc<PersistTableWriteWorkerInner>,
44}
45
46/// Commands for [PersistTableWriteWorker].
47#[derive(Debug)]
48enum PersistTableWriteCmd {
49    Register(
50        Timestamp,
51        Vec<TableRegistration>,
52        tokio::sync::oneshot::Sender<Result<(), StorageError>>,
53    ),
54    DropHandles {
55        forget_ts: Timestamp,
56        /// Tables that we want to drop our handle for.
57        ids: Vec<GlobalId>,
58        /// Notifies us when all resources have been cleaned up.
59        tx: oneshot::Sender<Result<(), StorageError>>,
60    },
61    Append {
62        write_ts: Timestamp,
63        advance_to: Timestamp,
64        updates: Vec<(GlobalId, Vec<TableData>)>,
65        tx: tokio::sync::oneshot::Sender<Result<(), StorageError>>,
66    },
67    Shutdown,
68}
69
70impl PersistTableWriteCmd {
71    fn name(&self) -> &'static str {
72        match self {
73            PersistTableWriteCmd::Register(_, _, _) => "PersistTableWriteCmd::Register",
74            PersistTableWriteCmd::DropHandles { .. } => "PersistTableWriteCmd::DropHandle",
75            PersistTableWriteCmd::Append { .. } => "PersistTableWriteCmd::Append",
76            PersistTableWriteCmd::Shutdown => "PersistTableWriteCmd::Shutdown",
77        }
78    }
79}
80
81/// Opens fresh table write handles concurrently.
82///
83/// Registration consumes handles even on conflict, so retries cannot reuse them.
84async fn open_table_write_handles(
85    persist_client: &PersistClient,
86    tables: Vec<TableRegistration>,
87) -> Vec<(
88    GlobalId,
89    WriteHandle<SourceData, (), Timestamp, StorageDiff>,
90)> {
91    futures::stream::iter(tables)
92        .map(|table| async move {
93            let mut write = persist_client
94                .open_writer(
95                    table.data_shard,
96                    Arc::new(table.relation_desc),
97                    Arc::new(UnitSchema),
98                    Diagnostics {
99                        shard_name: table.id.to_string(),
100                        handle_purpose: format!("table write worker data for {}", table.id),
101                    },
102                )
103                .await
104                .expect("invalid persist usage");
105            // Fetch the most recent upper: a freshly opened handle may otherwise report an upper
106            // behind the shard's since.
107            write.fetch_recent_upper().await;
108            (table.id, write)
109        })
110        .buffer_unordered(50)
111        .collect()
112        .await
113}
114
115async fn append_work(
116    write_handles: &mut BTreeMap<GlobalId, WriteHandle<SourceData, (), Timestamp, StorageDiff>>,
117    mut commands: BTreeMap<
118        GlobalId,
119        (
120            tracing::Span,
121            Vec<Update>,
122            Antichain<Timestamp>,
123            Antichain<Timestamp>,
124        ),
125    >,
126) -> Result<(), Vec<(GlobalId, Antichain<Timestamp>)>> {
127    let futs = FuturesUnordered::new();
128
129    // We cannot iterate through the updates and then set off a persist call
130    // on the write handle because we cannot mutably borrow the write handle
131    // multiple times.
132    //
133    // Instead, we first group the update by ID above and then iterate
134    // through all available write handles and see if there are any updates
135    // for it. If yes, we send them all in one go.
136    for (id, write) in write_handles.iter_mut() {
137        if let Some((span, updates, expected_upper, new_upper)) = commands.remove(id) {
138            let updates = updates.into_iter().map(|u| {
139                (
140                    (SourceData(Ok(u.row)), ()),
141                    u.timestamp,
142                    u.diff.into_inner(),
143                )
144            });
145
146            futs.push(async move {
147                write
148                    .compare_and_append(updates.clone(), expected_upper.clone(), new_upper.clone())
149                    .instrument(span.clone())
150                    .await
151                    .expect("cannot append updates")
152                    .or_else(|upper_mismatch| Err((*id, upper_mismatch.current)))?;
153
154                Ok::<_, (GlobalId, Antichain<Timestamp>)>((*id, new_upper))
155            })
156        }
157    }
158
159    // Ensure all futures run to completion, and track status of each of them individually
160    let (_new_uppers, failed_appends): (Vec<_>, Vec<_>) = futs
161        .collect::<Vec<_>>()
162        .await
163        .into_iter()
164        .partition_result();
165
166    if failed_appends.is_empty() {
167        Ok(())
168    } else {
169        Err(failed_appends)
170    }
171}
172
173impl PersistTableWriteWorker {
174    /// Create a new read-only table worker that continually bumps the upper of
175    /// it's tables. It is expected that we only register migrated builtin
176    /// tables, that cannot yet be registered in the txns system in read-only
177    /// mode.
178    ///
179    /// This takes a [WriteHandle] for the txns shard so that it can follow the
180    /// upper and continually bump the upper of registered tables to follow the
181    /// upper of the txns shard.
182    pub(crate) fn new_read_only_mode(
183        txns_handle: WriteHandle<SourceData, (), Timestamp, StorageDiff>,
184        persist_client: PersistClient,
185    ) -> Self {
186        let (tx, rx) =
187            tokio::sync::mpsc::unbounded_channel::<(tracing::Span, PersistTableWriteCmd)>();
188        mz_ore::task::spawn(
189            || "PersistTableWriteWorker",
190            read_only_table_worker::read_only_mode_table_worker(rx, txns_handle, persist_client),
191        );
192        Self {
193            inner: Arc::new(PersistTableWriteWorkerInner::new(tx)),
194        }
195    }
196
197    pub(crate) fn new_txns(
198        txns: TxnsHandle<SourceData, (), Timestamp, StorageDiff, TxnsCodecRow>,
199        persist_client: PersistClient,
200    ) -> Self {
201        let (tx, rx) =
202            tokio::sync::mpsc::unbounded_channel::<(tracing::Span, PersistTableWriteCmd)>();
203        mz_ore::task::spawn(|| "PersistTableWriteWorker", async move {
204            let mut worker = TxnsTableWorker {
205                txns,
206                persist_client,
207                write_handles: BTreeMap::new(),
208                tidy: Tidy::default(),
209            };
210            worker.run(rx).await
211        });
212        Self {
213            inner: Arc::new(PersistTableWriteWorkerInner::new(tx)),
214        }
215    }
216
217    pub(crate) fn register(
218        &self,
219        register_ts: Timestamp,
220        tables: Vec<TableRegistration>,
221    ) -> tokio::sync::oneshot::Receiver<Result<(), StorageError>> {
222        // We expect this to be awaited, so keep the span connected.
223        let span = info_span!("PersistTableWriteCmd::Register");
224        let (tx, rx) = tokio::sync::oneshot::channel();
225        let cmd = PersistTableWriteCmd::Register(register_ts, tables, tx);
226        self.inner.send_with_span(span, cmd);
227        rx
228    }
229
230    pub(crate) fn append(
231        &self,
232        write_ts: Timestamp,
233        advance_to: Timestamp,
234        updates: Vec<(GlobalId, Vec<TableData>)>,
235    ) -> tokio::sync::oneshot::Receiver<Result<(), StorageError>> {
236        let (tx, rx) = tokio::sync::oneshot::channel();
237        // Always send the append command to the txn-wal layer, even for empty
238        // updates. The txn-wal commit advances the logical upper of ALL
239        // registered data shards, which is needed for periodic group commits
240        // that have no actual data writes.
241        self.send(PersistTableWriteCmd::Append {
242            write_ts,
243            advance_to,
244            updates,
245            tx,
246        });
247        rx
248    }
249
250    fn send(&self, cmd: PersistTableWriteCmd) {
251        self.inner.send(cmd);
252    }
253}
254
255/// A [`TableWriteHandle`](mz_storage_client::controller::TableWriteHandle) backed by the table
256/// worker.
257#[derive(Debug, Clone)]
258pub(crate) struct TableWriteWorkerHandle(pub(crate) PersistTableWriteWorker);
259
260impl mz_storage_client::controller::TableWriteHandle for TableWriteWorkerHandle {
261    fn append(
262        &self,
263        write_ts: Timestamp,
264        advance_to: Timestamp,
265        commands: Vec<(GlobalId, Vec<TableData>)>,
266    ) -> oneshot::Receiver<Result<(), StorageError>> {
267        self.0.append(write_ts, advance_to, commands)
268    }
269
270    fn register(
271        &self,
272        register_ts: Timestamp,
273        tables: Vec<TableRegistration>,
274    ) -> oneshot::Receiver<Result<(), StorageError>> {
275        self.0.register(register_ts, tables)
276    }
277
278    fn forget(
279        &self,
280        forget_ts: Timestamp,
281        ids: Vec<GlobalId>,
282    ) -> oneshot::Receiver<Result<(), StorageError>> {
283        let (tx, rx) = oneshot::channel();
284        self.0
285            .send(PersistTableWriteCmd::DropHandles { forget_ts, ids, tx });
286        rx
287    }
288}
289
290struct TxnsTableWorker {
291    txns: TxnsHandle<SourceData, (), Timestamp, StorageDiff, TxnsCodecRow>,
292    persist_client: PersistClient,
293    write_handles: BTreeMap<GlobalId, ShardId>,
294    tidy: Tidy,
295}
296
297impl TxnsTableWorker {
298    async fn run(
299        &mut self,
300        mut rx: tokio::sync::mpsc::UnboundedReceiver<(Span, PersistTableWriteCmd)>,
301    ) {
302        while let Some((span, command)) = rx.recv().await {
303            match command {
304                PersistTableWriteCmd::Register(register_ts, tables, tx) => {
305                    let res = self.register(register_ts, tables).instrument(span).await;
306                    // We don't care if our waiter has gone away.
307                    let _ = tx.send(res);
308                }
309                PersistTableWriteCmd::DropHandles { forget_ts, ids, tx } => {
310                    let res = self.drop_handles(ids, forget_ts).instrument(span).await;
311                    // We don't care if our waiter has gone away.
312                    let _ = tx.send(res);
313                }
314                PersistTableWriteCmd::Append {
315                    write_ts,
316                    advance_to,
317                    updates,
318                    tx,
319                } => {
320                    self.append(write_ts, advance_to, updates, tx)
321                        .instrument(span)
322                        .await
323                }
324                PersistTableWriteCmd::Shutdown => {
325                    tracing::info!("PersistTableWriteWorker shutting down via command");
326                    return;
327                }
328            }
329        }
330
331        tracing::info!("PersistTableWriteWorker shutting down via input exhaustion");
332    }
333
334    async fn register(
335        &mut self,
336        register_ts: Timestamp,
337        tables: Vec<TableRegistration>,
338    ) -> Result<(), StorageError> {
339        let mut ids_handles = open_table_write_handles(&self.persist_client, tables).await;
340        // As tables evolve (e.g. columns are added) we treat the older versions as
341        // "views" on the later versions. While it's not required, it's easier to reason
342        // about table registration if we do it in GlobalId order.
343        ids_handles.sort_unstable_by_key(|(gid, _handle)| *gid);
344
345        for (id, write_handle) in ids_handles.iter() {
346            debug!(
347                "tables register {} {:.9}",
348                id,
349                write_handle.shard_id().to_string()
350            );
351            let previous = self.write_handles.insert(*id, write_handle.shard_id());
352            if previous.is_some() {
353                panic!("already registered a WriteHandle for collection {:?}", id);
354            }
355        }
356
357        // Registering also advances the logical upper of all shards in the txns set.
358        let new_ids = ids_handles.iter().map(|(id, _)| *id).collect_vec();
359        let handles = ids_handles.into_iter().map(|(_, handle)| handle);
360        let res = self.txns.register(register_ts, handles).await;
361        match res {
362            Ok(tidy) => {
363                self.tidy.merge(tidy);
364                Ok(())
365            }
366            Err(current) => {
367                // Registration consumed the handles. Roll back the IDs so a fresh retry can
368                // register them.
369                debug!(
370                    "register at {:?} conflicted with txns upper {:?}, rolling back {:?}",
371                    register_ts, current, new_ids
372                );
373                for id in &new_ids {
374                    self.write_handles.remove(id);
375                }
376                Err(StorageError::InvalidUppers(
377                    new_ids
378                        .into_iter()
379                        .map(|id| InvalidUpper {
380                            id,
381                            current_upper: Antichain::from_elem(current),
382                        })
383                        .collect(),
384                ))
385            }
386        }
387    }
388
389    async fn drop_handles(
390        &mut self,
391        ids: Vec<GlobalId>,
392        forget_ts: Timestamp,
393    ) -> Result<(), StorageError> {
394        tracing::info!(?ids, "drop tables");
395        let removed = ids
396            .iter()
397            // n.b. this should only remove the handle from the persist
398            // worker and not take any additional action such as closing
399            // the shard it's connected to because dataflows might still
400            // be using it.
401            .filter_map(|id| self.write_handles.remove(id).map(|shard| (*id, shard)))
402            .collect::<Vec<_>>();
403        let data_ids = removed
404            .iter()
405            .map(|(_, shard)| *shard)
406            .collect::<BTreeSet<_>>();
407        if !data_ids.is_empty() {
408            match self.txns.forget(forget_ts, data_ids.clone()).await {
409                Ok(tidy) => {
410                    self.tidy.merge(tidy);
411                    Ok(())
412                }
413                Err(current) => {
414                    // Restore local bookkeeping before the caller retries.
415                    debug!(
416                        "forget at {:?} conflicted with txns upper {:?}, restoring {:?}",
417                        forget_ts, current, ids
418                    );
419                    for (id, shard) in removed {
420                        self.write_handles.insert(id, shard);
421                    }
422                    Err(StorageError::InvalidUppers(
423                        ids.into_iter()
424                            .map(|id| InvalidUpper {
425                                id,
426                                current_upper: Antichain::from_elem(current),
427                            })
428                            .collect(),
429                    ))
430                }
431            }
432        } else {
433            Ok(())
434        }
435    }
436
437    async fn append(
438        &mut self,
439        write_ts: Timestamp,
440        advance_to: Timestamp,
441        updates: Vec<(GlobalId, Vec<TableData>)>,
442        tx: tokio::sync::oneshot::Sender<Result<(), StorageError>>,
443    ) {
444        debug!(
445            "tables append timestamp={:?} advance_to={:?} len={} ids={:?}{}",
446            write_ts,
447            advance_to,
448            updates.iter().flat_map(|(_, x)| x).count(),
449            updates
450                .iter()
451                .map(|(x, _)| x.to_string())
452                .collect::<BTreeSet<_>>(),
453            updates.iter().filter(|(_, v)| !v.is_empty()).fold(
454                String::new(),
455                |mut output, (k, v)| {
456                    let _ = write!(output, "\n  {}: {:?}", k, v.first());
457                    output
458                }
459            )
460        );
461        // TODO: txn-wal doesn't take an advance_to yet, it uses
462        // timestamp.step_forward. This is the same in all cases, so just assert that
463        // for now. Note that this uses the _persist_ StepForward, not the
464        // TimestampManipulation one (the impls are the same) because that's what
465        // txn-wal uses.
466        assert_eq!(
467            advance_to,
468            mz_persist_types::StepForward::step_forward(&write_ts)
469        );
470
471        let mut txn = self.txns.begin();
472        for (id, updates) in updates {
473            let Some(data_id) = self.write_handles.get(&id) else {
474                // HACK: When creating a table we get an append that includes it
475                // before it's been registered. When this happens there are no
476                // updates, so it's ~fine to ignore it.
477                assert!(
478                    updates.iter().all(|u| u.is_empty()),
479                    "{}: {:?}",
480                    id,
481                    updates
482                );
483                continue;
484            };
485            for update in updates {
486                match update {
487                    TableData::Rows(updates) => {
488                        for (row, diff) in updates {
489                            let () = txn
490                                .write(data_id, SourceData(Ok(row)), (), diff.into_inner())
491                                .await;
492                        }
493                    }
494                    TableData::Batches(batches) => {
495                        for batch in batches {
496                            let () = txn.write_batch(data_id, batch);
497                        }
498                    }
499                }
500            }
501        }
502        // Sneak in any txns shard tidying from previous commits.
503        txn.tidy(std::mem::take(&mut self.tidy));
504        let txn_res = txn.commit_at(&mut self.txns, write_ts).await;
505        let response = match txn_res {
506            Ok(apply) => {
507                // TODO: Do the applying in a background task. This will be a
508                // significant INSERT latency performance win.
509                debug!("applying {:?}", apply);
510                let tidy = apply.apply(&mut self.txns).await;
511                self.tidy.merge(tidy);
512
513                // We don't serve any reads out of this TxnsHandle, so go ahead
514                // and compact as aggressively as we can (i.e. to the time we
515                // just wrote).
516                let () = self.txns.compact_to(write_ts).await;
517
518                Ok(())
519            }
520            Err(current) => {
521                self.tidy.merge(txn.take_tidy());
522                debug!(
523                    "unable to commit txn at {:?} current={:?}",
524                    write_ts, current
525                );
526                Err(StorageError::InvalidUppers(
527                    self.write_handles
528                        .keys()
529                        .copied()
530                        .map(|id| InvalidUpper {
531                            id,
532                            current_upper: Antichain::from_elem(current),
533                        })
534                        .collect(),
535                ))
536            }
537        };
538        // It is not an error for the other end to hang up.
539        let _ = tx.send(response);
540    }
541}
542
543/// Contains the components necessary for sending commands to a `PersistTableWriteWorker`.
544///
545/// When `Drop`-ed sends a shutdown command, as such this should _never_ implement `Clone` because
546/// if one clone is dropped, the other clones will be unable to send commands. If you need this
547/// to be `Clone`-able, wrap it in an `Arc` or `Rc` first.
548///
549/// #[derive(Clone)] <-- do not do this.
550///
551#[derive(Debug)]
552struct PersistTableWriteWorkerInner {
553    /// Sending side of a channel that we can use to send commands.
554    tx: UnboundedSender<(tracing::Span, PersistTableWriteCmd)>,
555}
556
557impl Drop for PersistTableWriteWorkerInner {
558    fn drop(&mut self) {
559        self.send(PersistTableWriteCmd::Shutdown);
560        // TODO: Can't easily block on shutdown occurring.
561    }
562}
563
564impl PersistTableWriteWorkerInner {
565    fn new(tx: UnboundedSender<(tracing::Span, PersistTableWriteCmd)>) -> Self {
566        PersistTableWriteWorkerInner { tx }
567    }
568
569    fn send(&self, cmd: PersistTableWriteCmd) {
570        let span =
571            info_span!(parent: None, "PersistTableWriteWorkerInner::send", otel.name = cmd.name());
572        OpenTelemetryContext::obtain().attach_as_parent_to(&span);
573        self.send_with_span(span, cmd)
574    }
575
576    fn send_with_span(&self, span: Span, cmd: PersistTableWriteCmd) {
577        match self.tx.send((span, cmd)) {
578            Ok(()) => (), // All good!
579            Err(e) => {
580                tracing::trace!("could not forward command: {:?}", e);
581            }
582        }
583    }
584}