Skip to main content

mz_persist_client/
cache.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 cache of [PersistClient]s indexed by [PersistLocation]s.
11
12use std::any::Any;
13use std::collections::BTreeMap;
14use std::collections::btree_map::Entry;
15use std::fmt::Debug;
16use std::future::Future;
17use std::sync::{Arc, RwLock, TryLockError, Weak};
18use std::time::{Duration, Instant};
19
20use differential_dataflow::difference::Monoid;
21use differential_dataflow::lattice::Lattice;
22use mz_dyncfg::{Config, ParameterScope};
23use mz_ore::instrument;
24use mz_ore::metrics::MetricsRegistry;
25use mz_ore::task::{AbortOnDropHandle, JoinHandle};
26use mz_ore::url::SensitiveUrl;
27use mz_persist::cfg::{BlobConfig, ConsensusConfig, open_hedge_sibling};
28use mz_persist::hedge::HedgedBlob;
29use mz_persist::location::{
30    BLOB_GET_LIVENESS_KEY, Blob, CONSENSUS_HEAD_LIVENESS_KEY, Consensus, ExternalError, Tasked,
31    VersionedData,
32};
33use mz_persist_types::{Codec, Codec64};
34use timely::progress::Timestamp;
35use tokio::sync::{Mutex, OnceCell};
36use tracing::debug;
37
38use crate::async_runtime::IsolatedRuntime;
39use crate::error::{CodecConcreteType, CodecMismatch};
40use crate::internal::cache::BlobMemCache;
41use crate::internal::machine::retry_external;
42use crate::internal::metrics::{LockMetrics, Metrics, MetricsBlob, MetricsConsensus, ShardMetrics};
43use crate::internal::state::TypedState;
44use crate::internal::watch::{AwaitableState, StateWatchNotifier};
45use crate::rpc::{PubSubClientConnection, PubSubSender, ShardSubscriptionToken};
46use crate::schema::SchemaCacheMaps;
47use crate::{Diagnostics, PersistClient, PersistConfig, PersistLocation, ShardId};
48
49/// A cache of [PersistClient]s indexed by [PersistLocation]s.
50///
51/// There should be at most one of these per process. All production
52/// PersistClients should be created through this cache.
53///
54/// This is because, in production, persist is heavily limited by the number of
55/// server-side Postgres/Aurora connections. This cache allows PersistClients to
56/// share, for example, these Postgres connections.
57#[derive(Debug)]
58pub struct PersistClientCache {
59    /// The tunable knobs for persist.
60    pub cfg: PersistConfig,
61    pub(crate) metrics: Arc<Metrics>,
62    blob_by_uri: Mutex<BTreeMap<SensitiveUrl, (RttLatencyTask, Arc<dyn Blob>)>>,
63    consensus_by_uri: Mutex<BTreeMap<SensitiveUrl, (RttLatencyTask, Arc<dyn Consensus>)>>,
64    isolated_runtime: Arc<IsolatedRuntime>,
65    pub(crate) state_cache: Arc<StateCache>,
66    pubsub_sender: Arc<dyn PubSubSender>,
67    _pubsub_receiver_task: JoinHandle<()>,
68}
69
70#[derive(Debug)]
71struct RttLatencyTask(#[allow(dead_code)] AbortOnDropHandle<()>);
72
73impl PersistClientCache {
74    /// Returns a new [PersistClientCache].
75    pub fn new<F>(cfg: PersistConfig, registry: &MetricsRegistry, pubsub: F) -> Self
76    where
77        F: FnOnce(&PersistConfig, Arc<Metrics>) -> PubSubClientConnection,
78    {
79        let metrics = Arc::new(Metrics::new(&cfg, registry));
80        let pubsub_client = pubsub(&cfg, Arc::clone(&metrics));
81
82        let state_cache = Arc::new(StateCache::new(
83            &cfg,
84            Arc::clone(&metrics),
85            Arc::clone(&pubsub_client.sender),
86        ));
87        let _pubsub_receiver_task = crate::rpc::subscribe_state_cache_to_pubsub(
88            Arc::clone(&state_cache),
89            pubsub_client.receiver,
90        );
91        let isolated_runtime =
92            IsolatedRuntime::new(registry, Some(cfg.isolated_runtime_worker_threads));
93
94        PersistClientCache {
95            cfg,
96            metrics,
97            blob_by_uri: Mutex::new(BTreeMap::new()),
98            consensus_by_uri: Mutex::new(BTreeMap::new()),
99            isolated_runtime: Arc::new(isolated_runtime),
100            state_cache,
101            pubsub_sender: pubsub_client.sender,
102            _pubsub_receiver_task,
103        }
104    }
105
106    /// A test helper that returns a [PersistClientCache] disconnected from
107    /// metrics.
108    pub fn new_no_metrics() -> Self {
109        Self::new(
110            PersistConfig::new_for_tests(),
111            &MetricsRegistry::new(),
112            |_, _| PubSubClientConnection::noop(),
113        )
114    }
115
116    #[cfg(feature = "turmoil")]
117    /// Create a [PersistClientCache] for use in turmoil tests.
118    ///
119    /// Turmoil wants to run all software under test in a single thread, so we disable the
120    /// (multi-threaded) isolated runtime.
121    pub fn new_for_turmoil() -> Self {
122        use crate::rpc::NoopPubSubSender;
123
124        let cfg = PersistConfig::new_for_tests();
125        let metrics = Arc::new(Metrics::new(&cfg, &MetricsRegistry::new()));
126
127        let pubsub_sender: Arc<dyn PubSubSender> = Arc::new(NoopPubSubSender);
128        let _pubsub_receiver_task = mz_ore::task::spawn(|| "noop", async {});
129
130        let state_cache = Arc::new(StateCache::new(
131            &cfg,
132            Arc::clone(&metrics),
133            Arc::clone(&pubsub_sender),
134        ));
135        let isolated_runtime = IsolatedRuntime::new_disabled();
136
137        PersistClientCache {
138            cfg,
139            metrics,
140            blob_by_uri: Mutex::new(BTreeMap::new()),
141            consensus_by_uri: Mutex::new(BTreeMap::new()),
142            isolated_runtime: Arc::new(isolated_runtime),
143            state_cache,
144            pubsub_sender,
145            _pubsub_receiver_task,
146        }
147    }
148
149    /// Returns the [PersistConfig] being used by this cache.
150    pub fn cfg(&self) -> &PersistConfig {
151        &self.cfg
152    }
153
154    /// Returns persist `Metrics`.
155    pub fn metrics(&self) -> &Arc<Metrics> {
156        &self.metrics
157    }
158
159    /// Returns `ShardMetrics` for the given shard.
160    pub fn shard_metrics(&self, shard_id: &ShardId, name: &str) -> Arc<ShardMetrics> {
161        self.metrics.shards.shard(shard_id, name)
162    }
163
164    /// Clears the state cache, allowing for tests with disconnected states.
165    ///
166    /// Only exposed for testing.
167    pub fn clear_state_cache(&mut self) {
168        self.state_cache = Arc::new(StateCache::new(
169            &self.cfg,
170            Arc::clone(&self.metrics),
171            Arc::clone(&self.pubsub_sender),
172        ))
173    }
174
175    /// Returns a new [PersistClient] for interfacing with persist shards made
176    /// durable to the given [PersistLocation].
177    ///
178    /// The same `location` may be used concurrently from multiple processes.
179    #[instrument(level = "debug")]
180    pub async fn open(&self, location: PersistLocation) -> Result<PersistClient, ExternalError> {
181        let blob = self.open_blob(location.blob_uri).await?;
182        let consensus = self.open_consensus(location.consensus_uri).await?;
183        PersistClient::new(
184            self.cfg.clone(),
185            blob,
186            consensus,
187            Arc::clone(&self.metrics),
188            Arc::clone(&self.isolated_runtime),
189            Arc::clone(&self.state_cache),
190            Arc::clone(&self.pubsub_sender),
191        )
192    }
193
194    // No sense in measuring rtt latencies more often than this.
195    const PROMETHEUS_SCRAPE_INTERVAL: Duration = Duration::from_secs(60);
196
197    async fn open_consensus(
198        &self,
199        consensus_uri: SensitiveUrl,
200    ) -> Result<Arc<dyn Consensus>, ExternalError> {
201        let mut consensus_by_uri = self.consensus_by_uri.lock().await;
202        let consensus = match consensus_by_uri.entry(consensus_uri) {
203            Entry::Occupied(x) => Arc::clone(&x.get().1),
204            Entry::Vacant(x) => {
205                // Intentionally hold the lock, so we don't double connect under
206                // concurrency.
207                let consensus = ConsensusConfig::try_from(
208                    x.key(),
209                    Box::new(self.cfg.clone()),
210                    self.metrics.postgres_consensus.clone(),
211                    Arc::clone(&self.cfg().configs),
212                )?;
213                let consensus =
214                    retry_external(&self.metrics.retries.external.consensus_open, || {
215                        consensus.clone().open()
216                    })
217                    .await;
218                let consensus =
219                    Arc::new(MetricsConsensus::new(consensus, Arc::clone(&self.metrics)));
220                let consensus = Arc::new(Tasked(consensus));
221                let task = consensus_rtt_latency_task(
222                    Arc::clone(&consensus),
223                    Arc::clone(&self.metrics),
224                    Self::PROMETHEUS_SCRAPE_INTERVAL,
225                )
226                .await;
227                Arc::clone(
228                    &x.insert((RttLatencyTask(task.abort_on_drop()), consensus))
229                        .1,
230                )
231            }
232        };
233        Ok(consensus)
234    }
235
236    async fn open_blob(&self, blob_uri: SensitiveUrl) -> Result<Arc<dyn Blob>, ExternalError> {
237        let mut blob_by_uri = self.blob_by_uri.lock().await;
238        let blob = match blob_by_uri.entry(blob_uri) {
239            Entry::Occupied(x) => Arc::clone(&x.get().1),
240            Entry::Vacant(x) => {
241                // Intentionally hold the lock, so we don't double connect under
242                // concurrency.
243                let blob = BlobConfig::try_from(
244                    x.key(),
245                    Box::new(self.cfg.clone()),
246                    self.metrics.s3_blob.clone(),
247                )
248                .await?;
249                let blob = retry_external(&self.metrics.retries.external.blob_open, || {
250                    blob.clone().open()
251                })
252                .await;
253                // Hedged gets need a second handle on an isolated connection
254                // pool. Built unconditionally (best-effort): the wrapper
255                // reads its enable flag dynamically per call.
256                //
257                // NOTE: HedgedBlob must stay below Tasked in this stack. Its
258                // race relies on dropping the losing future to cancel the
259                // request in flight, and on hedged gets running to
260                // completion once started (Tasked detaches). A task boundary
261                // between HedgedBlob and the backend would break the former,
262                // and an aborting layer above would slowly leak budget
263                // tokens via the latter.
264                let sibling = open_hedge_sibling(
265                    x.key(),
266                    Box::new(self.cfg.clone()),
267                    self.metrics.s3_blob.clone(),
268                )
269                .await;
270                let blob = Arc::new(HedgedBlob::new(
271                    blob,
272                    sibling,
273                    Arc::clone(&self.cfg.configs),
274                    self.metrics.blob_hedge.clone(),
275                ));
276                let blob = Arc::new(MetricsBlob::new(blob, Arc::clone(&self.metrics)));
277                let blob = Arc::new(Tasked(blob));
278                let task = blob_rtt_latency_task(
279                    Arc::clone(&blob),
280                    Arc::clone(&self.metrics),
281                    Self::PROMETHEUS_SCRAPE_INTERVAL,
282                )
283                .await;
284                // This is intentionally "outside" (wrapping) MetricsBlob so
285                // that we don't include cached responses in blob metrics.
286                let blob = BlobMemCache::new(&self.cfg, Arc::clone(&self.metrics), blob);
287                Arc::clone(&x.insert((RttLatencyTask(task.abort_on_drop()), blob)).1)
288            }
289        };
290        Ok(blob)
291    }
292}
293
294/// Starts a task to periodically measure the persist-observed latency to
295/// consensus.
296///
297/// This is a task, rather than something like looking at the latencies of prod
298/// traffic, so that we minimize any issues around Futures not being polled
299/// promptly (as can and does happen with the Timely-polled Futures).
300///
301/// The caller is responsible for shutdown via aborting the `JoinHandle`.
302///
303/// No matter whether we wrap MetricsConsensus before or after we start up the
304/// rtt latency task, there's the possibility for it being confusing at some
305/// point. Err on the side of more data (including the latency measurements) to
306/// start.
307#[allow(clippy::unused_async)]
308async fn blob_rtt_latency_task(
309    blob: Arc<Tasked<MetricsBlob>>,
310    metrics: Arc<Metrics>,
311    measurement_interval: Duration,
312) -> JoinHandle<()> {
313    mz_ore::task::spawn(|| "persist::blob_rtt_latency", async move {
314        // Use the tokio Instant for next_measurement because the reclock tests
315        // mess with the tokio sleep clock.
316        let mut next_measurement = tokio::time::Instant::now();
317        loop {
318            tokio::time::sleep_until(next_measurement).await;
319            let start = Instant::now();
320            match blob.get(BLOB_GET_LIVENESS_KEY).await {
321                Ok(_) => {
322                    metrics.blob.rtt_latency.set(start.elapsed().as_secs_f64());
323                }
324                Err(_) => {
325                    // Don't spam retries if this returns an error. We're
326                    // guaranteed by the method signature that we've already got
327                    // metrics coverage of these, so we'll count the errors.
328                }
329            }
330            next_measurement = tokio::time::Instant::now() + measurement_interval;
331        }
332    })
333}
334
335/// Starts a task to periodically measure the persist-observed latency to
336/// consensus.
337///
338/// This is a task, rather than something like looking at the latencies of prod
339/// traffic, so that we minimize any issues around Futures not being polled
340/// promptly (as can and does happen with the Timely-polled Futures).
341///
342/// The caller is responsible for shutdown via aborting the `JoinHandle`.
343///
344/// No matter whether we wrap MetricsConsensus before or after we start up the
345/// rtt latency task, there's the possibility for it being confusing at some
346/// point. Err on the side of more data (including the latency measurements) to
347/// start.
348#[allow(clippy::unused_async)]
349async fn consensus_rtt_latency_task(
350    consensus: Arc<Tasked<MetricsConsensus>>,
351    metrics: Arc<Metrics>,
352    measurement_interval: Duration,
353) -> JoinHandle<()> {
354    mz_ore::task::spawn(|| "persist::consensus_rtt_latency", async move {
355        // Use the tokio Instant for next_measurement because the reclock tests
356        // mess with the tokio sleep clock.
357        let mut next_measurement = tokio::time::Instant::now();
358        loop {
359            tokio::time::sleep_until(next_measurement).await;
360            let start = Instant::now();
361            match consensus.head(CONSENSUS_HEAD_LIVENESS_KEY).await {
362                Ok(_) => {
363                    metrics
364                        .consensus
365                        .rtt_latency
366                        .set(start.elapsed().as_secs_f64());
367                }
368                Err(_) => {
369                    // Don't spam retries if this returns an error. We're
370                    // guaranteed by the method signature that we've already got
371                    // metrics coverage of these, so we'll count the errors.
372                }
373            }
374            next_measurement = tokio::time::Instant::now() + measurement_interval;
375        }
376    })
377}
378
379pub(crate) trait DynState: Debug + Send + Sync {
380    fn codecs(&self) -> (String, String, String, String, Option<CodecConcreteType>);
381    fn as_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync>;
382    fn push_diff(&self, diff: VersionedData);
383}
384
385impl<K, V, T, D> DynState for LockingTypedState<K, V, T, D>
386where
387    K: Codec,
388    V: Codec,
389    T: Timestamp + Lattice + Codec64 + Sync,
390    D: Codec64,
391{
392    fn codecs(&self) -> (String, String, String, String, Option<CodecConcreteType>) {
393        (
394            K::codec_name(),
395            V::codec_name(),
396            T::codec_name(),
397            D::codec_name(),
398            Some(CodecConcreteType(std::any::type_name::<(K, V, T, D)>())),
399        )
400    }
401
402    fn as_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync> {
403        self
404    }
405
406    fn push_diff(&self, diff: VersionedData) {
407        self.write_lock(&self.metrics.locks.applier_write, |state| {
408            let seqno_before = state.seqno;
409            state.apply_encoded_diffs(&self.cfg, &self.metrics, std::iter::once(&diff));
410            let seqno_after = state.seqno;
411            assert!(seqno_after >= seqno_before);
412
413            if seqno_before != seqno_after {
414                debug!(
415                    "applied pushed diff {}. seqno {} -> {}.",
416                    state.shard_id, seqno_before, state.seqno
417                );
418                self.metrics.pubsub_client.receiver.diff_applied.inc();
419            } else {
420                debug!(
421                    "failed to apply pushed diff {}. seqno {} vs diff {}",
422                    state.shard_id, seqno_before, diff.seqno
423                );
424                if diff.seqno <= seqno_before {
425                    self.metrics
426                        .pubsub_client
427                        .receiver
428                        .diff_not_applied_stale
429                        .inc();
430                } else {
431                    self.metrics
432                        .pubsub_client
433                        .receiver
434                        .diff_not_applied_out_of_order
435                        .inc();
436                }
437            }
438        })
439    }
440}
441
442/// A cache of `TypedState`, shared between all machines for that shard.
443///
444/// This is shared between all machines that come out of the same
445/// [PersistClientCache], but in production there is one of those per process,
446/// so in practice, we have one copy of state per shard per process.
447///
448/// The mutex contention between commands is not an issue, because if two
449/// command for the same shard are executing concurrently, only one can win
450/// anyway, the other will retry. With the mutex, we even get to avoid the retry
451/// if the racing commands are on the same process.
452#[derive(Debug)]
453pub struct StateCache {
454    cfg: Arc<PersistConfig>,
455    pub(crate) metrics: Arc<Metrics>,
456    states: Arc<std::sync::Mutex<BTreeMap<ShardId, Arc<OnceCell<Weak<dyn DynState>>>>>>,
457    pubsub_sender: Arc<dyn PubSubSender>,
458}
459
460#[derive(Debug)]
461enum StateCacheInit {
462    Init(Arc<dyn DynState>),
463    NeedInit(Arc<OnceCell<Weak<dyn DynState>>>),
464}
465
466impl StateCache {
467    /// Returns a new StateCache.
468    pub fn new(
469        cfg: &PersistConfig,
470        metrics: Arc<Metrics>,
471        pubsub_sender: Arc<dyn PubSubSender>,
472    ) -> Self {
473        StateCache {
474            cfg: Arc::new(cfg.clone()),
475            metrics,
476            states: Default::default(),
477            pubsub_sender,
478        }
479    }
480
481    #[cfg(test)]
482    pub(crate) fn new_no_metrics() -> Self {
483        Self::new(
484            &PersistConfig::new_for_tests(),
485            Arc::new(Metrics::new(
486                &PersistConfig::new_for_tests(),
487                &MetricsRegistry::new(),
488            )),
489            Arc::new(crate::rpc::NoopPubSubSender),
490        )
491    }
492
493    pub(crate) async fn get<K, V, T, D, F, InitFn>(
494        &self,
495        shard_id: ShardId,
496        mut init_fn: InitFn,
497        diagnostics: &Diagnostics,
498    ) -> Result<Arc<LockingTypedState<K, V, T, D>>, Box<CodecMismatch>>
499    where
500        K: Debug + Codec,
501        V: Debug + Codec,
502        T: Timestamp + Lattice + Codec64 + Sync,
503        D: Monoid + Codec64,
504        F: Future<Output = Result<TypedState<K, V, T, D>, Box<CodecMismatch>>>,
505        InitFn: FnMut() -> F,
506    {
507        loop {
508            let init = {
509                let mut states = self.states.lock().expect("lock poisoned");
510                let state = states.entry(shard_id).or_default();
511                match state.get() {
512                    Some(once_val) => match once_val.upgrade() {
513                        Some(x) => StateCacheInit::Init(x),
514                        None => {
515                            // If the Weak has lost the ability to upgrade,
516                            // we've dropped the State and it's gone. Clear the
517                            // OnceCell and init a new one.
518                            *state = Arc::new(OnceCell::new());
519                            StateCacheInit::NeedInit(Arc::clone(state))
520                        }
521                    },
522                    None => StateCacheInit::NeedInit(Arc::clone(state)),
523                }
524            };
525
526            let state = match init {
527                StateCacheInit::Init(x) => x,
528                StateCacheInit::NeedInit(init_once) => {
529                    let mut did_init: Option<Arc<LockingTypedState<K, V, T, D>>> = None;
530                    let state = init_once
531                        .get_or_try_init::<Box<CodecMismatch>, _, _>(|| async {
532                            let init_res = init_fn().await;
533                            let state = Arc::new(LockingTypedState::new(
534                                shard_id,
535                                init_res?,
536                                Arc::clone(&self.metrics),
537                                Arc::clone(&self.cfg),
538                                Arc::clone(&self.pubsub_sender).subscribe(&shard_id),
539                                diagnostics,
540                            ));
541                            let ret = Arc::downgrade(&state);
542                            did_init = Some(state);
543                            let ret: Weak<dyn DynState> = ret;
544                            Ok(ret)
545                        })
546                        .await?;
547                    if let Some(x) = did_init {
548                        // We actually did the init work, don't bother casting back
549                        // the type erased and weak version. Additionally, inform
550                        // any listeners of this new state.
551                        return Ok(x);
552                    }
553                    let Some(state) = state.upgrade() else {
554                        // Race condition. Between when we first checked the
555                        // OnceCell and the `get_or_try_init` call, (1) the
556                        // initialization finished, (2) the other user dropped
557                        // the strong ref, and (3) the Arc noticed it was down
558                        // to only weak refs and dropped the value. Nothing we
559                        // can do except try again.
560                        continue;
561                    };
562                    state
563                }
564            };
565
566            match Arc::clone(&state)
567                .as_any()
568                .downcast::<LockingTypedState<K, V, T, D>>()
569            {
570                Ok(x) => return Ok(x),
571                Err(_) => {
572                    return Err(Box::new(CodecMismatch {
573                        requested: (
574                            K::codec_name(),
575                            V::codec_name(),
576                            T::codec_name(),
577                            D::codec_name(),
578                            Some(CodecConcreteType(std::any::type_name::<(K, V, T, D)>())),
579                        ),
580                        actual: state.codecs(),
581                    }));
582                }
583            }
584        }
585    }
586
587    pub(crate) fn get_state_weak(&self, shard_id: &ShardId) -> Option<Weak<dyn DynState>> {
588        self.states
589            .lock()
590            .expect("lock")
591            .get(shard_id)
592            .and_then(|x| x.get())
593            .map(Weak::clone)
594    }
595
596    #[cfg(test)]
597    fn get_cached(&self, shard_id: &ShardId) -> Option<Arc<dyn DynState>> {
598        self.states
599            .lock()
600            .expect("lock")
601            .get(shard_id)
602            .and_then(|x| x.get())
603            .and_then(|x| x.upgrade())
604    }
605
606    #[cfg(test)]
607    fn initialized_count(&self) -> usize {
608        self.states
609            .lock()
610            .expect("lock")
611            .values()
612            .filter(|x| x.initialized())
613            .count()
614    }
615
616    #[cfg(test)]
617    fn strong_count(&self) -> usize {
618        self.states
619            .lock()
620            .expect("lock")
621            .values()
622            .filter(|x| x.get().map_or(false, |x| x.upgrade().is_some()))
623            .count()
624    }
625}
626
627/// A locked decorator for TypedState that abstracts out the specific lock implementation used.
628/// Guards the private lock with public accessor fns to make locking scopes more explicit and
629/// simpler to reason about.
630pub(crate) struct LockingTypedState<K, V, T, D> {
631    shard_id: ShardId,
632    state: RwLock<TypedState<K, V, T, D>>,
633    notifier: StateWatchNotifier<T>,
634    cfg: Arc<PersistConfig>,
635    metrics: Arc<Metrics>,
636    // Retained only to keep this shard's per-shard series registered for as long
637    // as the state is cached; nothing reads it through this handle anymore. Don't
638    // drop it as "unused" without moving that lifetime guarantee elsewhere.
639    shard_metrics: Arc<ShardMetrics>,
640    update_semaphore: AwaitableState<Option<tokio::time::Instant>>,
641    /// A [SchemaCacheMaps<K, V>], but stored as an Any so the `: Codec` bounds
642    /// don't propagate to basically every struct in persist.
643    schema_cache: Arc<dyn Any + Send + Sync>,
644    _subscription_token: Arc<ShardSubscriptionToken>,
645}
646
647impl<K, V, T: Debug, D> Debug for LockingTypedState<K, V, T, D> {
648    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
649        let LockingTypedState {
650            shard_id,
651            state,
652            notifier,
653            cfg: _cfg,
654            metrics: _metrics,
655            shard_metrics: _shard_metrics,
656            update_semaphore: _,
657            schema_cache: _schema_cache,
658            _subscription_token,
659        } = self;
660        f.debug_struct("LockingTypedState")
661            .field("shard_id", shard_id)
662            .field("state", state)
663            .field("notifier", notifier)
664            .finish()
665    }
666}
667
668impl<K: Codec, V: Codec, T, D> LockingTypedState<K, V, T, D> {
669    fn new(
670        shard_id: ShardId,
671        initial_state: TypedState<K, V, T, D>,
672        metrics: Arc<Metrics>,
673        cfg: Arc<PersistConfig>,
674        subscription_token: Arc<ShardSubscriptionToken>,
675        diagnostics: &Diagnostics,
676    ) -> Self
677    where
678        // Bounds needed to seed the notifier with the shard's current upper.
679        T: Timestamp + Lattice + Codec64,
680        D: Codec64,
681    {
682        let notifier = StateWatchNotifier::new(Arc::clone(&metrics), initial_state.upper().clone());
683        Self {
684            shard_id,
685            notifier,
686            state: RwLock::new(initial_state),
687            cfg: Arc::clone(&cfg),
688            shard_metrics: metrics.shards.shard(&shard_id, &diagnostics.shard_name),
689            update_semaphore: AwaitableState::new(None),
690            schema_cache: Arc::new(SchemaCacheMaps::<K, V>::new(&metrics.schema)),
691            metrics,
692            _subscription_token: subscription_token,
693        }
694    }
695
696    pub(crate) fn schema_cache(&self) -> Arc<SchemaCacheMaps<K, V>> {
697        Arc::clone(&self.schema_cache)
698            .downcast::<SchemaCacheMaps<K, V>>()
699            .expect("K and V match")
700    }
701}
702
703pub(crate) const STATE_UPDATE_LEASE_TIMEOUT: Config<Duration> = Config::new(
704    "persist_state_update_lease_timeout",
705    Duration::from_secs(1),
706    "The amount of time for a command to wait for a previous command to finish before executing. \
707        (If zero, commands will not wait for others to complete.) Higher values reduce database contention \
708        at the cost of higher worst-case latencies for individual requests.",
709    ParameterScope::Environment,
710);
711
712impl<K, V, T, D> LockingTypedState<K, V, T, D> {
713    pub(crate) fn shard_id(&self) -> &ShardId {
714        &self.shard_id
715    }
716
717    pub(crate) fn read_lock<R, F: FnMut(&TypedState<K, V, T, D>) -> R>(
718        &self,
719        metrics: &LockMetrics,
720        mut f: F,
721    ) -> R {
722        metrics.acquire_count.inc();
723        let state = match self.state.try_read() {
724            Ok(x) => x,
725            Err(TryLockError::WouldBlock) => {
726                metrics.blocking_acquire_count.inc();
727                let start = Instant::now();
728                let state = self.state.read().expect("lock poisoned");
729                metrics
730                    .blocking_seconds
731                    .inc_by(start.elapsed().as_secs_f64());
732                state
733            }
734            Err(TryLockError::Poisoned(err)) => panic!("state read lock poisoned: {}", err),
735        };
736        f(&state)
737    }
738
739    pub(crate) fn write_lock<R, F>(&self, metrics: &LockMetrics, f: F) -> R
740    where
741        F: FnOnce(&mut TypedState<K, V, T, D>) -> R,
742        // Bounds needed to read the shard upper. All callers are in contexts that
743        // already satisfy these (they mutate a fully-typed shard state).
744        K: Codec,
745        V: Codec,
746        T: Timestamp + Lattice + Codec64,
747        D: Codec64,
748    {
749        metrics.acquire_count.inc();
750        let mut state = match self.state.try_write() {
751            Ok(x) => x,
752            Err(TryLockError::WouldBlock) => {
753                metrics.blocking_acquire_count.inc();
754                let start = Instant::now();
755                let state = self.state.write().expect("lock poisoned");
756                metrics
757                    .blocking_seconds
758                    .inc_by(start.elapsed().as_secs_f64());
759                state
760            }
761            Err(TryLockError::Poisoned(err)) => panic!("state read lock poisoned: {}", err),
762        };
763        let seqno_before = state.seqno;
764        let ret = f(&mut state);
765        let seqno_after = state.seqno;
766        mz_ore::soft_assert_no_log!(seqno_after >= seqno_before);
767        if seqno_after > seqno_before {
768            // The notifier only advances the upper waiters' signal on a strict
769            // upper advance. The seqno bumps for many non-data reasons (GC,
770            // rollups, since-downgrades, other writers' CaAs) and those must not
771            // re-activate upper waiters.
772            self.notifier.notify(seqno_after, state.upper());
773        }
774        // For now, make sure to notify while under lock. It's possible to move
775        // this out of the lock window, see [StateWatchNotifier::notify].
776        drop(state);
777        ret
778    }
779
780    /// We want to _mostly_ just attempt a single CaS against the same state at once, since
781    /// only one concurrent CaS can succeed. However, we also want to guard against a
782    /// single hung update blocking all progress globally. We manage this with a shared state,
783    /// tracking whether a request is in flight and when it times out. If the timeout is never hit,
784    /// this behaves like a semaphore with limit 1... but if our requests _are_ timing out, future
785    /// requests will only wait for a bounded time before retrying, and one of those retries will
786    /// be able to claim that lease and make progress.
787    pub(crate) async fn lease_for_update(&self) -> impl Drop {
788        use tokio::time::Instant;
789
790        let timeout = STATE_UPDATE_LEASE_TIMEOUT.get(&self.cfg);
791
792        struct DropLease(Option<(AwaitableState<Option<Instant>>, Instant)>);
793
794        impl Drop for DropLease {
795            fn drop(&mut self) {
796                if let Some((state, time)) = self.0.take() {
797                    // Clear the timeout if it hasn't changed since we set it.
798                    state.maybe_modify(|s| {
799                        if s.is_some_and(|t| t == time) {
800                            *s.get_mut() = None;
801                        }
802                    })
803                }
804            }
805        }
806
807        // Special case: if the timeout is set to zero, go ahead without taking a lease.
808        if timeout.is_zero() {
809            return DropLease(None);
810        }
811
812        let timeout_state = self.update_semaphore.clone();
813        loop {
814            let now = tokio::time::Instant::now();
815            let expires_at = now + timeout;
816            // Claim the lease if there isn't one, or if the current lease has expired.
817            let maybe_leased = timeout_state.maybe_modify(|state| {
818                if let Some(other_expires_at) = **state
819                    && other_expires_at > now
820                {
821                    // Still locked: sleep until the deadline and try again.
822                    Err(other_expires_at)
823                } else {
824                    *state.get_mut() = Some(expires_at);
825                    Ok(())
826                }
827            });
828
829            match maybe_leased {
830                Ok(()) => {
831                    break DropLease(Some((timeout_state, expires_at)));
832                }
833                Err(other_expires_at) => {
834                    // Wait until either the lease has dropped or timed out, whichever is first.
835                    // If there are a lot of clients trying to update the same state, this may
836                    // cause significant lock contention... but the lock is only briefly held,
837                    // and anyways that's still cheaper than contending on the remote database.
838                    let _ = tokio::time::timeout_at(
839                        other_expires_at,
840                        timeout_state.wait_while(|s| s.is_some()),
841                    )
842                    .await;
843                }
844            }
845        }
846    }
847
848    pub(crate) fn notifier(&self) -> &StateWatchNotifier<T> {
849        &self.notifier
850    }
851}
852
853#[cfg(test)]
854mod tests {
855    use std::ops::Deref;
856    use std::pin::pin;
857    use std::str::FromStr;
858    use std::sync::atomic::{AtomicBool, Ordering};
859
860    use super::*;
861    use crate::rpc::NoopPubSubSender;
862    use futures::stream::{FuturesUnordered, StreamExt};
863    use mz_build_info::DUMMY_BUILD_INFO;
864    use mz_ore::task::spawn;
865    use mz_ore::{assert_err, assert_none};
866    use tokio::sync::oneshot;
867
868    #[mz_ore::test(tokio::test)]
869    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
870    async fn client_cache() {
871        let cache = PersistClientCache::new(
872            PersistConfig::new_for_tests(),
873            &MetricsRegistry::new(),
874            |_, _| PubSubClientConnection::noop(),
875        );
876        assert_eq!(cache.blob_by_uri.lock().await.len(), 0);
877        assert_eq!(cache.consensus_by_uri.lock().await.len(), 0);
878
879        // Opening a location on an empty cache saves the results.
880        let _ = cache
881            .open(PersistLocation {
882                blob_uri: SensitiveUrl::from_str("mem://blob_zero").expect("invalid URL"),
883                consensus_uri: SensitiveUrl::from_str("mem://consensus_zero").expect("invalid URL"),
884            })
885            .await
886            .expect("failed to open location");
887        assert_eq!(cache.blob_by_uri.lock().await.len(), 1);
888        assert_eq!(cache.consensus_by_uri.lock().await.len(), 1);
889
890        // Opening a location with an already opened consensus reuses it, even
891        // if the blob is different.
892        let _ = cache
893            .open(PersistLocation {
894                blob_uri: SensitiveUrl::from_str("mem://blob_one").expect("invalid URL"),
895                consensus_uri: SensitiveUrl::from_str("mem://consensus_zero").expect("invalid URL"),
896            })
897            .await
898            .expect("failed to open location");
899        assert_eq!(cache.blob_by_uri.lock().await.len(), 2);
900        assert_eq!(cache.consensus_by_uri.lock().await.len(), 1);
901
902        // Ditto the other way.
903        let _ = cache
904            .open(PersistLocation {
905                blob_uri: SensitiveUrl::from_str("mem://blob_one").expect("invalid URL"),
906                consensus_uri: SensitiveUrl::from_str("mem://consensus_one").expect("invalid URL"),
907            })
908            .await
909            .expect("failed to open location");
910        assert_eq!(cache.blob_by_uri.lock().await.len(), 2);
911        assert_eq!(cache.consensus_by_uri.lock().await.len(), 2);
912
913        // Query params and path matter, so we get new instances.
914        let _ = cache
915            .open(PersistLocation {
916                blob_uri: SensitiveUrl::from_str("mem://blob_one?foo").expect("invalid URL"),
917                consensus_uri: SensitiveUrl::from_str("mem://consensus_one/bar")
918                    .expect("invalid URL"),
919            })
920            .await
921            .expect("failed to open location");
922        assert_eq!(cache.blob_by_uri.lock().await.len(), 3);
923        assert_eq!(cache.consensus_by_uri.lock().await.len(), 3);
924
925        // User info and port also matter, so we get new instances.
926        let _ = cache
927            .open(PersistLocation {
928                blob_uri: SensitiveUrl::from_str("mem://user@blob_one").expect("invalid URL"),
929                consensus_uri: SensitiveUrl::from_str("mem://@consensus_one:123")
930                    .expect("invalid URL"),
931            })
932            .await
933            .expect("failed to open location");
934        assert_eq!(cache.blob_by_uri.lock().await.len(), 4);
935        assert_eq!(cache.consensus_by_uri.lock().await.len(), 4);
936    }
937
938    #[mz_ore::test(tokio::test)]
939    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
940    async fn state_cache() {
941        mz_ore::test::init_logging();
942        fn new_state<K, V, T, D>(shard_id: ShardId) -> TypedState<K, V, T, D>
943        where
944            K: Codec,
945            V: Codec,
946            T: Timestamp + Lattice + Codec64,
947            D: Codec64,
948        {
949            TypedState::new(
950                DUMMY_BUILD_INFO.semver_version(),
951                shard_id,
952                "host".into(),
953                0,
954            )
955        }
956        fn assert_same<K, V, T, D>(
957            state1: &LockingTypedState<K, V, T, D>,
958            state2: &LockingTypedState<K, V, T, D>,
959        ) {
960            let pointer1 = format!("{:p}", state1.state.read().expect("lock").deref());
961            let pointer2 = format!("{:p}", state2.state.read().expect("lock").deref());
962            assert_eq!(pointer1, pointer2);
963        }
964
965        let s1 = ShardId::new();
966        let states = Arc::new(StateCache::new_no_metrics());
967
968        // The cache starts empty.
969        assert_eq!(states.states.lock().expect("lock").len(), 0);
970
971        // Panic'ing during init_fn .
972        let s = Arc::clone(&states);
973        let res = spawn(|| "test", async move {
974            s.get::<(), (), u64, i64, _, _>(
975                s1,
976                || async { panic!("forced panic") },
977                &Diagnostics::for_tests(),
978            )
979            .await
980        })
981        .into_tokio_handle()
982        .await;
983        assert_err!(res);
984        assert_eq!(states.initialized_count(), 0);
985
986        // Returning an error from init_fn doesn't initialize an entry in the cache.
987        let res = states
988            .get::<(), (), u64, i64, _, _>(
989                s1,
990                || async {
991                    Err(Box::new(CodecMismatch {
992                        requested: ("".into(), "".into(), "".into(), "".into(), None),
993                        actual: ("".into(), "".into(), "".into(), "".into(), None),
994                    }))
995                },
996                &Diagnostics::for_tests(),
997            )
998            .await;
999        assert_err!(res);
1000        assert_eq!(states.initialized_count(), 0);
1001
1002        // Initialize one shard.
1003        let did_work = Arc::new(AtomicBool::new(false));
1004        let s1_state1 = states
1005            .get::<(), (), u64, i64, _, _>(
1006                s1,
1007                || {
1008                    let did_work = Arc::clone(&did_work);
1009                    async move {
1010                        did_work.store(true, Ordering::SeqCst);
1011                        Ok(new_state(s1))
1012                    }
1013                },
1014                &Diagnostics::for_tests(),
1015            )
1016            .await
1017            .expect("should successfully initialize");
1018        assert_eq!(did_work.load(Ordering::SeqCst), true);
1019        assert_eq!(states.initialized_count(), 1);
1020        assert_eq!(states.strong_count(), 1);
1021
1022        // Trying to initialize it again does no work and returns the same state.
1023        let did_work = Arc::new(AtomicBool::new(false));
1024        let s1_state2 = states
1025            .get::<(), (), u64, i64, _, _>(
1026                s1,
1027                || {
1028                    let did_work = Arc::clone(&did_work);
1029                    async move {
1030                        did_work.store(true, Ordering::SeqCst);
1031                        did_work.store(true, Ordering::SeqCst);
1032                        Ok(new_state(s1))
1033                    }
1034                },
1035                &Diagnostics::for_tests(),
1036            )
1037            .await
1038            .expect("should successfully initialize");
1039        assert_eq!(did_work.load(Ordering::SeqCst), false);
1040        assert_eq!(states.initialized_count(), 1);
1041        assert_eq!(states.strong_count(), 1);
1042        assert_same(&s1_state1, &s1_state2);
1043
1044        // Trying to initialize with different types doesn't work.
1045        let did_work = Arc::new(AtomicBool::new(false));
1046        let res = states
1047            .get::<String, (), u64, i64, _, _>(
1048                s1,
1049                || {
1050                    let did_work = Arc::clone(&did_work);
1051                    async move {
1052                        did_work.store(true, Ordering::SeqCst);
1053                        Ok(new_state(s1))
1054                    }
1055                },
1056                &Diagnostics::for_tests(),
1057            )
1058            .await;
1059        assert_eq!(did_work.load(Ordering::SeqCst), false);
1060        assert_eq!(
1061            format!("{}", res.expect_err("types shouldn't match")),
1062            "requested codecs (\"String\", \"()\", \"u64\", \"i64\", Some(CodecConcreteType(\"(alloc::string::String, (), u64, i64)\"))) did not match ones in durable storage (\"()\", \"()\", \"u64\", \"i64\", Some(CodecConcreteType(\"((), (), u64, i64)\")))"
1063        );
1064        assert_eq!(states.initialized_count(), 1);
1065        assert_eq!(states.strong_count(), 1);
1066
1067        // We can add a shard of a different type.
1068        let s2 = ShardId::new();
1069        let s2_state1 = states
1070            .get::<String, (), u64, i64, _, _>(
1071                s2,
1072                || async { Ok(new_state(s2)) },
1073                &Diagnostics::for_tests(),
1074            )
1075            .await
1076            .expect("should successfully initialize");
1077        assert_eq!(states.initialized_count(), 2);
1078        assert_eq!(states.strong_count(), 2);
1079        let s2_state2 = states
1080            .get::<String, (), u64, i64, _, _>(
1081                s2,
1082                || async { Ok(new_state(s2)) },
1083                &Diagnostics::for_tests(),
1084            )
1085            .await
1086            .expect("should successfully initialize");
1087        assert_same(&s2_state1, &s2_state2);
1088
1089        // The cache holds weak references to State so we reclaim memory if the
1090        // shards stops being used.
1091        drop(s1_state1);
1092        assert_eq!(states.strong_count(), 2);
1093        drop(s1_state2);
1094        assert_eq!(states.strong_count(), 1);
1095        assert_eq!(states.initialized_count(), 2);
1096        assert_none!(states.get_cached(&s1));
1097
1098        // But we can re-init that shard if necessary.
1099        let s1_state1 = states
1100            .get::<(), (), u64, i64, _, _>(
1101                s1,
1102                || async { Ok(new_state(s1)) },
1103                &Diagnostics::for_tests(),
1104            )
1105            .await
1106            .expect("should successfully initialize");
1107        assert_eq!(states.initialized_count(), 2);
1108        assert_eq!(states.strong_count(), 2);
1109        drop(s1_state1);
1110        assert_eq!(states.strong_count(), 1);
1111    }
1112
1113    #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1114    #[cfg_attr(miri, ignore)] // too slow
1115    async fn state_cache_concurrency() {
1116        mz_ore::test::init_logging();
1117
1118        const COUNT: usize = 1000;
1119        let id = ShardId::new();
1120        let cache = StateCache::new_no_metrics();
1121        let diagnostics = Diagnostics::for_tests();
1122
1123        let mut futures = (0..COUNT)
1124            .map(|_| {
1125                cache.get::<(), (), u64, i64, _, _>(
1126                    id,
1127                    || async {
1128                        Ok(TypedState::new(
1129                            DUMMY_BUILD_INFO.semver_version(),
1130                            id,
1131                            "host".into(),
1132                            0,
1133                        ))
1134                    },
1135                    &diagnostics,
1136                )
1137            })
1138            .collect::<FuturesUnordered<_>>();
1139
1140        for _ in 0..COUNT {
1141            let _ = futures.next().await.unwrap();
1142        }
1143    }
1144
1145    #[mz_ore::test(tokio::test)]
1146    #[cfg_attr(miri, ignore)] // too slow
1147    async fn update_semaphore() {
1148        // Check that the update lease mechanism is not susceptible to futurelock.
1149        // If there is an issue, this test will time out.
1150        mz_ore::test::init_logging();
1151
1152        let shard_id = ShardId::new();
1153        let persist_config = Arc::new(PersistConfig::new_for_tests());
1154        let pubsub = Arc::new(NoopPubSubSender);
1155        let state: LockingTypedState<String, (), u64, i64> = LockingTypedState::new(
1156            shard_id,
1157            TypedState::new(
1158                DUMMY_BUILD_INFO.semver_version(),
1159                shard_id,
1160                "host".into(),
1161                0,
1162            ),
1163            Arc::new(Metrics::new(&*persist_config, &MetricsRegistry::new())),
1164            persist_config,
1165            pubsub.subscribe(&shard_id),
1166            &Diagnostics::for_tests(),
1167        );
1168
1169        // Initialize three futures, all of which will grab a lease and then poll a oneshot,
1170        // which allows us to externally trigger which ones will complete.
1171        let mk_future = || {
1172            let (tx, rx) = oneshot::channel();
1173            let future = async {
1174                let lease = state.lease_for_update().await;
1175                let () = rx.await.unwrap();
1176                drop(lease);
1177            };
1178            (future, tx)
1179        };
1180
1181        let (one, _one_tx) = mk_future();
1182        let (two, _two_tx) = mk_future();
1183        let (three, three_tx) = mk_future();
1184        let mut one = pin!(one);
1185        let mut two = pin!(two);
1186        let mut three = pin!(three);
1187
1188        // Poll all the futures, but fall through to the default case, since none are ready.
1189        tokio::select! { biased;
1190            _ = &mut one => { unreachable!() }
1191            _ = &mut two => { unreachable!() }
1192            _ = &mut three => { unreachable!() }
1193            _ = async {} => {}
1194        }
1195
1196        // Allow the third future to complete.
1197        three_tx.send(()).unwrap();
1198
1199        // Poll all the futures but the second future. This shouldn't hang, since the third future
1200        // is now ready to go and the others should eventually time out.
1201        tokio::select! { biased;
1202            _ = &mut one => { unreachable!() }
1203            _ = &mut three => {  }
1204        }
1205    }
1206
1207    #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1208    #[cfg_attr(miri, ignore)] // too slow
1209    async fn update_semaphore_stress() {
1210        // Check that the update lease mechanism is not susceptible to futurelock.
1211        // If there is an issue, this test will time out.
1212        mz_ore::test::init_logging();
1213
1214        const TIMEOUT: Duration = Duration::from_millis(100);
1215        const COUNT: u64 = 100;
1216
1217        let shard_id = ShardId::new();
1218        let persist_config = Arc::new(PersistConfig::new_for_tests());
1219        persist_config.set_config(&STATE_UPDATE_LEASE_TIMEOUT, TIMEOUT);
1220        let pubsub = Arc::new(NoopPubSubSender);
1221        let state: LockingTypedState<String, (), u64, i64> = LockingTypedState::new(
1222            shard_id,
1223            TypedState::new(
1224                DUMMY_BUILD_INFO.semver_version(),
1225                shard_id,
1226                "host".into(),
1227                0,
1228            ),
1229            Arc::new(Metrics::new(&*persist_config, &MetricsRegistry::new())),
1230            persist_config,
1231            pubsub.subscribe(&shard_id),
1232            &Diagnostics::for_tests(),
1233        );
1234
1235        let mut futures = (0..(COUNT * 3))
1236            .map(async |i| {
1237                state.lease_for_update().await;
1238                // Either hang forever, succeed quickly, or succeed after hitting the timeout.
1239                match i % 3 {
1240                    0 => {
1241                        let () = std::future::pending().await;
1242                    }
1243                    1 => {
1244                        tokio::time::sleep(Duration::from_millis(i)).await;
1245                    }
1246                    _ => {
1247                        tokio::time::sleep(Duration::from_millis(i) + TIMEOUT).await;
1248                    }
1249                }
1250            })
1251            .collect::<FuturesUnordered<_>>();
1252
1253        // All the futures that don't themselves hang forever should resolve.
1254        for _ in 0..(COUNT * 2) {
1255            futures.next().await.unwrap();
1256        }
1257    }
1258}