Skip to main content

mz_storage/source/reclock/
compat.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//! Reclocking compatibility code until the whole ingestion pipeline is transformed to native
11//! timestamps
12
13use std::cell::RefCell;
14use std::rc::Rc;
15use std::sync::Arc;
16use std::time::{Duration, Instant};
17
18use anyhow::Context;
19use differential_dataflow::lattice::Lattice;
20use fail::fail_point;
21use futures::StreamExt;
22use futures::stream::LocalBoxStream;
23use mz_ore::soft_panic_or_log;
24use mz_persist_client::Diagnostics;
25use mz_persist_client::cache::PersistClientCache;
26use mz_persist_client::error::UpperMismatch;
27use mz_persist_client::read::ListenEvent;
28use mz_persist_client::write::WriteHandle;
29use mz_persist_types::Codec64;
30use mz_persist_types::codec_impls::UnitSchema;
31use mz_repr::{Diff, GlobalId, RelationDesc};
32use mz_storage_client::util::remap_handle::{RemapHandle, RemapHandleReader};
33use mz_storage_types::StorageDiff;
34use mz_storage_types::controller::CollectionMetadata;
35use mz_storage_types::sources::{SourceData, SourceTimestamp};
36use timely::order::{PartialOrder, TotalOrder};
37use timely::progress::Timestamp;
38use timely::progress::frontier::Antichain;
39use tokio::sync::watch;
40
41/// A handle to a persist shard that stores remap bindings
42pub struct PersistHandle<FromTime: SourceTimestamp, IntoTime: Timestamp + Lattice + Codec64> {
43    events:
44        LocalBoxStream<'static, ListenEvent<IntoTime, ((SourceData, ()), IntoTime, StorageDiff)>>,
45    write_handle: WriteHandle<SourceData, (), IntoTime, StorageDiff>,
46    /// Whether or not this handle is in read-only mode.
47    read_only_rx: watch::Receiver<bool>,
48    pending_batch: Vec<(FromTime, IntoTime, Diff)>,
49    // Reports `self`'s write frontier.
50    shared_write_frontier: Rc<RefCell<Antichain<IntoTime>>>,
51}
52
53impl<FromTime: Timestamp, IntoTime: Timestamp + Sync> PersistHandle<FromTime, IntoTime>
54where
55    FromTime: SourceTimestamp,
56    IntoTime: Timestamp + TotalOrder + Lattice + Codec64,
57{
58    pub async fn new(
59        persist_clients: Arc<PersistClientCache>,
60        read_only_rx: watch::Receiver<bool>,
61        remap_metadata: CollectionMetadata,
62        as_of: Antichain<IntoTime>,
63        shared_write_frontier: Rc<RefCell<Antichain<IntoTime>>>,
64        // additional information to improve logging
65        id: GlobalId,
66        operator: &str,
67        worker_id: usize,
68        worker_count: usize,
69        // Must match the `FromTime`. Ideally we would be able to discover this
70        // from `SourceTimestamp`, but each source would need a specific `SourceTimestamp`
71        // implementation, as they do not share remap `RelationDesc`'s (columns names
72        // are different).
73        //
74        // TODO(guswynn): use the type-system to prevent misuse here.
75        remap_relation_desc: RelationDesc,
76        remap_collection_id: GlobalId,
77    ) -> anyhow::Result<Self> {
78        let persist_client = persist_clients
79            .open(remap_metadata.persist_location.clone())
80            .await
81            .context("error creating persist client")?;
82
83        let (mut write_handle, mut read_handle) = persist_client
84            .open(
85                remap_metadata.data_shard,
86                Arc::new(remap_relation_desc),
87                Arc::new(UnitSchema),
88                Diagnostics {
89                    shard_name: remap_collection_id.to_string(),
90                    handle_purpose: format!("reclock for {}", id),
91                },
92                false,
93            )
94            .await
95            .expect("invalid usage");
96
97        let upper = write_handle.fetch_recent_upper().await;
98        // We want a leased reader because elsewhere in the code the `as_of`
99        // time may also be determined by another `ReadHandle`, and the pair of
100        // them offer the invariant that we need (that the `as_of` if <= this
101        // `since`). Using a `SinceHandle` here does not offer the same
102        // invariant when paired with a `ReadHandle`.
103        let since = read_handle.since();
104
105        // Allow manually simulating the scenario where the since of the remap
106        // shard has advanced too far.
107        fail_point!("invalid_remap_as_of");
108
109        if since.is_empty() {
110            // This can happen when, say, a source is being dropped but we on
111            // the cluster are busy and notice that only later. In those cases
112            // it can happen that we still try to render an ingestion that is
113            // not valid anymore and where the shards it uses are not valid to
114            // use anymore.
115            //
116            // This is a rare race condition and something that is expected to
117            // happen every now and then. It's not a bug in the current way of
118            // how things work.
119            tracing::info!(
120                source_id = %id,
121                %worker_id,
122                "since of remap shard is the empty antichain, suspending...");
123
124            // We wait 5 hours to give the commands a chance to arrive at this
125            // replica and for it to drop our dataflow.
126            tokio::time::sleep(Duration::from_secs(5 * 60 * 60)).await;
127
128            // If we're still here after 5 hours, something has gone wrong and
129            // we complain.
130            soft_panic_or_log!(
131                "since of remap shard is the empty antichain, source_id = {id}, worker_id = {worker_id}"
132            );
133        }
134
135        if !PartialOrder::less_equal(since, &as_of) {
136            anyhow::bail!(
137                "invalid as_of: as_of({as_of:?}) < since({since:?}), \
138                source {id}, \
139                remap_shard: {:?}",
140                remap_metadata.data_shard
141            );
142        }
143
144        assert!(
145            as_of.elements() == [IntoTime::minimum()] || PartialOrder::less_than(&as_of, upper),
146            "invalid as_of: upper({upper:?}) <= as_of({as_of:?})",
147        );
148
149        tracing::info!(
150            ?since,
151            ?as_of,
152            ?upper,
153            "{operator}({id}) {worker_id}/{worker_count} initializing PersistHandle"
154        );
155
156        // Own the operator name: the snapshot stream below outlives this call.
157        let operator = operator.to_string();
158
159        use futures::stream;
160        let events = stream::once(async move {
161            // Bracket the snapshot read. It is the first thing polled and the
162            // point that wedges if the blob store can't serve a batch part, so a
163            // "reading" with no matching "read" pins the stall to the read here.
164            tracing::info!(
165                ?as_of,
166                "{operator}({id}) {worker_id}/{worker_count} reading remap snapshot"
167            );
168            let snapshot_start = Instant::now();
169            let updates = read_handle
170                .snapshot_and_fetch(as_of.clone())
171                .await
172                .expect("since <= as_of asserted");
173            tracing::info!(
174                ?as_of,
175                update_count = updates.len(),
176                elapsed = ?snapshot_start.elapsed(),
177                "{operator}({id}) {worker_id}/{worker_count} read remap snapshot"
178            );
179            let snapshot = stream::once(std::future::ready(ListenEvent::Updates(updates)));
180
181            let listener = read_handle
182                .listen(as_of.clone())
183                .await
184                .expect("since <= as_of asserted");
185
186            let listen_stream = stream::unfold(listener, |mut listener| async move {
187                let events = stream::iter(listener.fetch_next().await);
188                Some((events, listener))
189            })
190            .flatten();
191
192            snapshot.chain(listen_stream)
193        })
194        .flatten()
195        .boxed_local();
196
197        Ok(Self {
198            events,
199            write_handle,
200            read_only_rx,
201            pending_batch: vec![],
202            shared_write_frontier,
203        })
204    }
205}
206
207#[async_trait::async_trait(?Send)]
208impl<FromTime, IntoTime> RemapHandleReader for PersistHandle<FromTime, IntoTime>
209where
210    FromTime: SourceTimestamp,
211    IntoTime: Timestamp + Lattice + Codec64,
212{
213    type FromTime = FromTime;
214    type IntoTime = IntoTime;
215
216    async fn next(
217        &mut self,
218    ) -> Option<(
219        Vec<(Self::FromTime, Self::IntoTime, Diff)>,
220        Antichain<Self::IntoTime>,
221    )> {
222        while let Some(event) = self.events.next().await {
223            match event {
224                ListenEvent::Progress(new_upper) => {
225                    // Peel off a batch of pending data
226                    let batch = self
227                        .pending_batch
228                        .extract_if(.., |(_, ts, _)| !new_upper.less_equal(ts))
229                        .collect();
230                    return Some((batch, new_upper));
231                }
232                ListenEvent::Updates(msgs) => {
233                    for ((update, _), into_ts, diff) in msgs {
234                        let from_ts = FromTime::decode_row(&update.0.expect("invalid row"));
235                        self.pending_batch.push((from_ts, into_ts, diff.into()));
236                    }
237                }
238            }
239        }
240        None
241    }
242}
243
244#[async_trait::async_trait(?Send)]
245impl<FromTime, IntoTime> RemapHandle for PersistHandle<FromTime, IntoTime>
246where
247    FromTime: SourceTimestamp,
248    IntoTime: Timestamp + TotalOrder + Lattice + Codec64 + Sync,
249{
250    async fn compare_and_append(
251        &mut self,
252        updates: Vec<(Self::FromTime, Self::IntoTime, Diff)>,
253        upper: Antichain<Self::IntoTime>,
254        new_upper: Antichain<Self::IntoTime>,
255    ) -> Result<(), UpperMismatch<Self::IntoTime>> {
256        if *self.read_only_rx.borrow() {
257            // We have to wait for either us coming out of read-only mode or
258            // someone else advancing the upper. If we just returned an
259            // `UpperMismatch` while in read-only mode, we would go into a busy
260            // loop because we'd be called over and over again. One presumes.
261
262            loop {
263                tracing::trace!(
264                    ?upper,
265                    ?new_upper,
266                    persist_upper = ?self.write_handle.upper(),
267                    "persist remap handle is in read-only mode, waiting until we come out of it or the shard upper advances");
268
269                // We don't try to be too smart here, and for example use
270                // `wait_for_upper_past()`. We'd have to use a select!, which
271                // would require cancel safety of `wait_for_upper_past()`, which
272                // it doesn't advertise.
273                let _ =
274                    tokio::time::timeout(Duration::from_secs(1), self.read_only_rx.changed()).await;
275
276                if !*self.read_only_rx.borrow() {
277                    tracing::trace!(
278                        ?upper,
279                        ?new_upper,
280                        persist_upper = ?self.write_handle.upper(),
281                        "persist remap handle has come out of read-only mode"
282                    );
283
284                    // It's okay to write now.
285                    break;
286                }
287
288                let current_upper = self.write_handle.fetch_recent_upper().await;
289
290                if PartialOrder::less_than(&upper, current_upper) {
291                    tracing::trace!(
292                        ?upper,
293                        ?new_upper,
294                        persist_upper = ?current_upper,
295                        "someone else advanced the upper, aborting write"
296                    );
297
298                    return Err(UpperMismatch {
299                        current: current_upper.clone(),
300                        expected: upper,
301                    });
302                }
303            }
304        }
305
306        let row_updates = updates.into_iter().map(|(from_ts, into_ts, diff)| {
307            (
308                (SourceData(Ok(from_ts.encode_row())), ()),
309                into_ts,
310                diff.into_inner(),
311            )
312        });
313
314        match self
315            .write_handle
316            .compare_and_append(row_updates, upper, new_upper.clone())
317            .await
318        {
319            Ok(result) => {
320                *self.shared_write_frontier.borrow_mut() = new_upper;
321                return result;
322            }
323            Err(invalid_use) => panic!("compare_and_append failed: {invalid_use}"),
324        }
325    }
326
327    fn upper(&self) -> &Antichain<Self::IntoTime> {
328        self.write_handle.upper()
329    }
330}