Skip to main content

mz_storage_controller/persist_handles/
read_only_table_worker.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, VecDeque};
14use std::ops::ControlFlow;
15
16use differential_dataflow::lattice::Lattice;
17use futures::FutureExt;
18use mz_persist_client::write::WriteHandle;
19use mz_repr::{GlobalId, Timestamp};
20use mz_storage_client::client::{TableData, Update};
21use mz_storage_types::StorageDiff;
22use mz_storage_types::controller::InvalidUpper;
23use mz_storage_types::sources::SourceData;
24use timely::PartialOrder;
25use timely::progress::Antichain;
26use tracing::Span;
27
28use crate::StorageError;
29use crate::persist_handles::{PersistTableWriteCmd, append_work};
30
31/// Handles table updates in read only mode.
32///
33/// In read only mode, we write to tables outside of the txn-wal system. This is
34/// a gross hack, but it is a quick fix to allow us to perform migrations of the
35/// built-in tables in the new generation during a deployment. We need to write
36/// to the new shards for migrated built-in tables so that dataflows that depend
37/// on those tables can catch up, but we don't want to register them into the
38/// existing txn-wal shard, as that would mutate the state of the old generation
39/// while it's still running. We could instead create a new txn shard in the new
40/// generation for *just* system catalog tables, but then we'd have to do a
41/// complicated dance to move the system catalog tables back to the original txn
42/// shard during promotion, without ever losing track of a shard or registering
43/// it in two txn shards simultaneously.
44///
45/// This code is a nearly line-for-line reintroduction of the code that managed
46/// writing to tables before the txn-wal system. This code can (again) be
47/// deleted when we switch to using native persist schema migrations to perform
48/// mgirations of built-in tables.
49pub(crate) async fn read_only_mode_table_worker(
50    mut rx: tokio::sync::mpsc::UnboundedReceiver<(Span, PersistTableWriteCmd)>,
51    txns_handle: WriteHandle<SourceData, (), Timestamp, StorageDiff>,
52    persist_client: mz_persist_client::PersistClient,
53) {
54    let mut write_handles =
55        BTreeMap::<GlobalId, WriteHandle<SourceData, (), Timestamp, StorageDiff>>::new();
56
57    let gen_upper_future = |mut handle: WriteHandle<SourceData, (), Timestamp, StorageDiff>| {
58        let fut = async move {
59            let current_upper = handle.shared_upper();
60            handle.wait_for_upper_past(&current_upper).await;
61            let new_upper = handle.shared_upper();
62            (handle, new_upper)
63        };
64
65        fut.boxed()
66    };
67
68    let mut txns_upper_future = {
69        let txns_upper_future = gen_upper_future(txns_handle);
70        txns_upper_future
71    };
72
73    let shutdown_reason = loop {
74        tokio::select! {
75            (handle, upper) = &mut txns_upper_future => {
76                tracing::debug!("new upper from txns shard: {:?}, advancing upper of migrated builtin tables", upper);
77                advance_uppers(&mut write_handles, upper).await;
78
79                let fut = gen_upper_future(handle);
80                txns_upper_future = fut;
81            }
82            cmd = rx.recv() => {
83                let Some(cmd) = cmd else {
84                    break "command rx closed".to_string();
85                };
86
87                // Peel off all available commands.
88                // We do this in case we can consolidate commands.
89                // It would be surprising to receive multiple concurrent `Append` commands,
90                // but we might receive multiple *empty* `Append` commands.
91                let mut commands = VecDeque::new();
92                commands.push_back(cmd);
93                while let Ok(cmd) = rx.try_recv() {
94                    commands.push_back(cmd);
95                }
96
97                let result = handle_commands(&mut write_handles, commands, &persist_client).await;
98
99                match result {
100                    ControlFlow::Continue(_) => {
101                        continue;
102                    }
103                    ControlFlow::Break(msg) => {
104                        break msg;
105                    }
106                }
107
108            }
109        }
110    };
111
112    tracing::info!(%shutdown_reason, "PersistTableWriteWorker shutting down");
113}
114
115/// Handles the given commands.
116async fn handle_commands(
117    write_handles: &mut BTreeMap<GlobalId, WriteHandle<SourceData, (), Timestamp, StorageDiff>>,
118    mut commands: VecDeque<(Span, PersistTableWriteCmd)>,
119    persist_client: &mz_persist_client::PersistClient,
120) -> ControlFlow<String> {
121    let mut shutdown = false;
122
123    // Accumulated updates and upper frontier.
124    let mut all_updates = BTreeMap::default();
125    let mut all_responses = Vec::default();
126
127    while let Some((span, command)) = commands.pop_front() {
128        match command {
129            PersistTableWriteCmd::Register(_register_ts, tables, tx) => {
130                let ids_handles =
131                    crate::persist_handles::open_table_write_handles(persist_client, tables).await;
132                for (id, write_handle) in ids_handles {
133                    // As of today, we can only migrate builtin (system) tables.
134                    assert!(id.is_system(), "trying to register non-system id {id}");
135
136                    let previous = write_handles.insert(id, write_handle);
137                    if previous.is_some() {
138                        panic!("already registered a WriteHandle for collection {:?}", id);
139                    }
140                }
141                // We don't care if our waiter has gone away.
142                let _ = tx.send(Ok(()));
143            }
144            PersistTableWriteCmd::DropHandles {
145                forget_ts: _,
146                ids,
147                tx,
148            } => {
149                // n.b. this should only remove the
150                // handle from the persist worker and
151                // not take any additional action such
152                // as closing the shard it's connected
153                // to because dataflows might still be
154                // using it.
155                for id in ids {
156                    write_handles.remove(&id);
157                }
158                // We don't care if our waiter has gone away.
159                let _ = tx.send(Ok(()));
160            }
161            PersistTableWriteCmd::Append {
162                write_ts,
163                advance_to,
164                updates,
165                tx,
166            } => {
167                let mut ids = BTreeSet::new();
168                for (id, updates_no_ts) in updates {
169                    ids.insert(id);
170                    let (old_span, updates, _expected_upper, old_new_upper) =
171                        all_updates.entry(id).or_insert_with(|| {
172                            (
173                                span.clone(),
174                                Vec::default(),
175                                Antichain::from_elem(write_ts),
176                                Antichain::from_elem(Timestamp::MIN),
177                            )
178                        });
179
180                    if old_span.id() != span.id() {
181                        // Link in any spans for `Append` operations that we
182                        // lump together by doing this. This is not ideal,
183                        // because we only have a true tracing history for
184                        // the "first" span that we process, but it's better
185                        // than nothing.
186                        old_span.follows_from(span.id());
187                    }
188                    let updates_with_ts = updates_no_ts.into_iter().flat_map(|x| match x {
189                        TableData::Rows(rows) => {
190                            let iter = rows.into_iter().map(|(row, diff)| Update {
191                                row,
192                                timestamp: write_ts,
193                                diff,
194                            });
195                            itertools::Either::Left(iter)
196                        }
197                        TableData::Batches(_) => {
198                            // TODO(cf1): Handle Batches of updates in ReadOnlyTableWorker.
199                            mz_ore::soft_panic_or_log!(
200                                "handle Batches of updates in the ReadOnlyTableWorker"
201                            );
202                            itertools::Either::Right(std::iter::empty())
203                        }
204                    });
205                    updates.extend(updates_with_ts);
206                    old_new_upper.join_assign(&Antichain::from_elem(advance_to));
207                }
208                all_responses.push((ids, tx));
209            }
210            PersistTableWriteCmd::Shutdown => shutdown = true,
211        }
212    }
213
214    let result = append_work(write_handles, all_updates).await;
215
216    for (ids, response) in all_responses {
217        let result = match &result {
218            Err(bad_ids) => {
219                let filtered: Vec<_> = bad_ids
220                    .iter()
221                    .filter(|(id, _)| ids.contains(id))
222                    .cloned()
223                    .map(|(id, current_upper)| InvalidUpper { id, current_upper })
224                    .collect();
225                if filtered.is_empty() {
226                    Ok(())
227                } else {
228                    Err(StorageError::InvalidUppers(filtered))
229                }
230            }
231            Ok(()) => Ok(()),
232        };
233        // It is not an error for the other end to hang up.
234        let _ = response.send(result);
235    }
236
237    if shutdown {
238        ControlFlow::Break("received a shutdown command".to_string())
239    } else {
240        ControlFlow::Continue(())
241    }
242}
243
244/// Advances the upper of all registered tables (which are only the migrated
245/// builtin tables) to the given `upper`.
246async fn advance_uppers(
247    write_handles: &mut BTreeMap<GlobalId, WriteHandle<SourceData, (), Timestamp, StorageDiff>>,
248    upper: Antichain<Timestamp>,
249) {
250    let mut all_updates = BTreeMap::default();
251
252    for (id, write_handle) in write_handles.iter_mut() {
253        // As of today, we can only migrate builtin (system) tables, and so only
254        // expect to register those in this read-only table worker.
255        assert!(id.is_system(), "trying to register non-system id {id}");
256
257        // This business of continually advancing the upper is expensive, but
258        // we're a) only doing it when in read-only mode, and b) only doing it
259        // for each migrated builtin table, of which there usually aren't many.
260        let expected_upper = write_handle.fetch_recent_upper().await.to_owned();
261
262        // Avoid advancing the upper until the coordinator has a chance to back-fill the shard.
263        if expected_upper.elements() == &[Timestamp::MIN] {
264            continue;
265        }
266
267        if PartialOrder::less_equal(&upper, &expected_upper) {
268            // Nothing to do, and append_work doesn't like being called with a
269            // new upper that is less_equal the current upper.
270            continue;
271        }
272
273        all_updates.insert(
274            *id,
275            (Span::none(), Vec::new(), expected_upper, upper.clone()),
276        );
277    }
278
279    let result = append_work(write_handles, all_updates).await;
280    tracing::debug!(?result, "advanced upper of migrated builtin tables");
281}