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.shard_metrics.pubsub_push_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.shard_metrics.pubsub_push_diff_not_applied_stale.inc();
426                } else {
427                    self.shard_metrics
428                        .pubsub_push_diff_not_applied_out_of_order
429                        .inc();
430                }
431            }
432        })
433    }
434}
435
436/// A cache of `TypedState`, shared between all machines for that shard.
437///
438/// This is shared between all machines that come out of the same
439/// [PersistClientCache], but in production there is one of those per process,
440/// so in practice, we have one copy of state per shard per process.
441///
442/// The mutex contention between commands is not an issue, because if two
443/// command for the same shard are executing concurrently, only one can win
444/// anyway, the other will retry. With the mutex, we even get to avoid the retry
445/// if the racing commands are on the same process.
446#[derive(Debug)]
447pub struct StateCache {
448    cfg: Arc<PersistConfig>,
449    pub(crate) metrics: Arc<Metrics>,
450    states: Arc<std::sync::Mutex<BTreeMap<ShardId, Arc<OnceCell<Weak<dyn DynState>>>>>>,
451    pubsub_sender: Arc<dyn PubSubSender>,
452}
453
454#[derive(Debug)]
455enum StateCacheInit {
456    Init(Arc<dyn DynState>),
457    NeedInit(Arc<OnceCell<Weak<dyn DynState>>>),
458}
459
460impl StateCache {
461    /// Returns a new StateCache.
462    pub fn new(
463        cfg: &PersistConfig,
464        metrics: Arc<Metrics>,
465        pubsub_sender: Arc<dyn PubSubSender>,
466    ) -> Self {
467        StateCache {
468            cfg: Arc::new(cfg.clone()),
469            metrics,
470            states: Default::default(),
471            pubsub_sender,
472        }
473    }
474
475    #[cfg(test)]
476    pub(crate) fn new_no_metrics() -> Self {
477        Self::new(
478            &PersistConfig::new_for_tests(),
479            Arc::new(Metrics::new(
480                &PersistConfig::new_for_tests(),
481                &MetricsRegistry::new(),
482            )),
483            Arc::new(crate::rpc::NoopPubSubSender),
484        )
485    }
486
487    pub(crate) async fn get<K, V, T, D, F, InitFn>(
488        &self,
489        shard_id: ShardId,
490        mut init_fn: InitFn,
491        diagnostics: &Diagnostics,
492    ) -> Result<Arc<LockingTypedState<K, V, T, D>>, Box<CodecMismatch>>
493    where
494        K: Debug + Codec,
495        V: Debug + Codec,
496        T: Timestamp + Lattice + Codec64 + Sync,
497        D: Monoid + Codec64,
498        F: Future<Output = Result<TypedState<K, V, T, D>, Box<CodecMismatch>>>,
499        InitFn: FnMut() -> F,
500    {
501        loop {
502            let init = {
503                let mut states = self.states.lock().expect("lock poisoned");
504                let state = states.entry(shard_id).or_default();
505                match state.get() {
506                    Some(once_val) => match once_val.upgrade() {
507                        Some(x) => StateCacheInit::Init(x),
508                        None => {
509                            // If the Weak has lost the ability to upgrade,
510                            // we've dropped the State and it's gone. Clear the
511                            // OnceCell and init a new one.
512                            *state = Arc::new(OnceCell::new());
513                            StateCacheInit::NeedInit(Arc::clone(state))
514                        }
515                    },
516                    None => StateCacheInit::NeedInit(Arc::clone(state)),
517                }
518            };
519
520            let state = match init {
521                StateCacheInit::Init(x) => x,
522                StateCacheInit::NeedInit(init_once) => {
523                    let mut did_init: Option<Arc<LockingTypedState<K, V, T, D>>> = None;
524                    let state = init_once
525                        .get_or_try_init::<Box<CodecMismatch>, _, _>(|| async {
526                            let init_res = init_fn().await;
527                            let state = Arc::new(LockingTypedState::new(
528                                shard_id,
529                                init_res?,
530                                Arc::clone(&self.metrics),
531                                Arc::clone(&self.cfg),
532                                Arc::clone(&self.pubsub_sender).subscribe(&shard_id),
533                                diagnostics,
534                            ));
535                            let ret = Arc::downgrade(&state);
536                            did_init = Some(state);
537                            let ret: Weak<dyn DynState> = ret;
538                            Ok(ret)
539                        })
540                        .await?;
541                    if let Some(x) = did_init {
542                        // We actually did the init work, don't bother casting back
543                        // the type erased and weak version. Additionally, inform
544                        // any listeners of this new state.
545                        return Ok(x);
546                    }
547                    let Some(state) = state.upgrade() else {
548                        // Race condition. Between when we first checked the
549                        // OnceCell and the `get_or_try_init` call, (1) the
550                        // initialization finished, (2) the other user dropped
551                        // the strong ref, and (3) the Arc noticed it was down
552                        // to only weak refs and dropped the value. Nothing we
553                        // can do except try again.
554                        continue;
555                    };
556                    state
557                }
558            };
559
560            match Arc::clone(&state)
561                .as_any()
562                .downcast::<LockingTypedState<K, V, T, D>>()
563            {
564                Ok(x) => return Ok(x),
565                Err(_) => {
566                    return Err(Box::new(CodecMismatch {
567                        requested: (
568                            K::codec_name(),
569                            V::codec_name(),
570                            T::codec_name(),
571                            D::codec_name(),
572                            Some(CodecConcreteType(std::any::type_name::<(K, V, T, D)>())),
573                        ),
574                        actual: state.codecs(),
575                    }));
576                }
577            }
578        }
579    }
580
581    pub(crate) fn get_state_weak(&self, shard_id: &ShardId) -> Option<Weak<dyn DynState>> {
582        self.states
583            .lock()
584            .expect("lock")
585            .get(shard_id)
586            .and_then(|x| x.get())
587            .map(Weak::clone)
588    }
589
590    #[cfg(test)]
591    fn get_cached(&self, shard_id: &ShardId) -> Option<Arc<dyn DynState>> {
592        self.states
593            .lock()
594            .expect("lock")
595            .get(shard_id)
596            .and_then(|x| x.get())
597            .and_then(|x| x.upgrade())
598    }
599
600    #[cfg(test)]
601    fn initialized_count(&self) -> usize {
602        self.states
603            .lock()
604            .expect("lock")
605            .values()
606            .filter(|x| x.initialized())
607            .count()
608    }
609
610    #[cfg(test)]
611    fn strong_count(&self) -> usize {
612        self.states
613            .lock()
614            .expect("lock")
615            .values()
616            .filter(|x| x.get().map_or(false, |x| x.upgrade().is_some()))
617            .count()
618    }
619}
620
621/// A locked decorator for TypedState that abstracts out the specific lock implementation used.
622/// Guards the private lock with public accessor fns to make locking scopes more explicit and
623/// simpler to reason about.
624pub(crate) struct LockingTypedState<K, V, T, D> {
625    shard_id: ShardId,
626    state: RwLock<TypedState<K, V, T, D>>,
627    notifier: StateWatchNotifier<T>,
628    cfg: Arc<PersistConfig>,
629    metrics: Arc<Metrics>,
630    shard_metrics: Arc<ShardMetrics>,
631    update_semaphore: AwaitableState<Option<tokio::time::Instant>>,
632    /// A [SchemaCacheMaps<K, V>], but stored as an Any so the `: Codec` bounds
633    /// don't propagate to basically every struct in persist.
634    schema_cache: Arc<dyn Any + Send + Sync>,
635    _subscription_token: Arc<ShardSubscriptionToken>,
636}
637
638impl<K, V, T: Debug, D> Debug for LockingTypedState<K, V, T, D> {
639    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
640        let LockingTypedState {
641            shard_id,
642            state,
643            notifier,
644            cfg: _cfg,
645            metrics: _metrics,
646            shard_metrics: _shard_metrics,
647            update_semaphore: _,
648            schema_cache: _schema_cache,
649            _subscription_token,
650        } = self;
651        f.debug_struct("LockingTypedState")
652            .field("shard_id", shard_id)
653            .field("state", state)
654            .field("notifier", notifier)
655            .finish()
656    }
657}
658
659impl<K: Codec, V: Codec, T, D> LockingTypedState<K, V, T, D> {
660    fn new(
661        shard_id: ShardId,
662        initial_state: TypedState<K, V, T, D>,
663        metrics: Arc<Metrics>,
664        cfg: Arc<PersistConfig>,
665        subscription_token: Arc<ShardSubscriptionToken>,
666        diagnostics: &Diagnostics,
667    ) -> Self
668    where
669        // Bounds needed to seed the notifier with the shard's current upper.
670        T: Timestamp + Lattice + Codec64,
671        D: Codec64,
672    {
673        let notifier = StateWatchNotifier::new(Arc::clone(&metrics), initial_state.upper().clone());
674        Self {
675            shard_id,
676            notifier,
677            state: RwLock::new(initial_state),
678            cfg: Arc::clone(&cfg),
679            shard_metrics: metrics.shards.shard(&shard_id, &diagnostics.shard_name),
680            update_semaphore: AwaitableState::new(None),
681            schema_cache: Arc::new(SchemaCacheMaps::<K, V>::new(&metrics.schema)),
682            metrics,
683            _subscription_token: subscription_token,
684        }
685    }
686
687    pub(crate) fn schema_cache(&self) -> Arc<SchemaCacheMaps<K, V>> {
688        Arc::clone(&self.schema_cache)
689            .downcast::<SchemaCacheMaps<K, V>>()
690            .expect("K and V match")
691    }
692}
693
694pub(crate) const STATE_UPDATE_LEASE_TIMEOUT: Config<Duration> = Config::new(
695    "persist_state_update_lease_timeout",
696    Duration::from_secs(1),
697    "The amount of time for a command to wait for a previous command to finish before executing. \
698        (If zero, commands will not wait for others to complete.) Higher values reduce database contention \
699        at the cost of higher worst-case latencies for individual requests.",
700    ParameterScope::Environment,
701);
702
703impl<K, V, T, D> LockingTypedState<K, V, T, D> {
704    pub(crate) fn shard_id(&self) -> &ShardId {
705        &self.shard_id
706    }
707
708    pub(crate) fn read_lock<R, F: FnMut(&TypedState<K, V, T, D>) -> R>(
709        &self,
710        metrics: &LockMetrics,
711        mut f: F,
712    ) -> R {
713        metrics.acquire_count.inc();
714        let state = match self.state.try_read() {
715            Ok(x) => x,
716            Err(TryLockError::WouldBlock) => {
717                metrics.blocking_acquire_count.inc();
718                let start = Instant::now();
719                let state = self.state.read().expect("lock poisoned");
720                metrics
721                    .blocking_seconds
722                    .inc_by(start.elapsed().as_secs_f64());
723                state
724            }
725            Err(TryLockError::Poisoned(err)) => panic!("state read lock poisoned: {}", err),
726        };
727        f(&state)
728    }
729
730    pub(crate) fn write_lock<R, F>(&self, metrics: &LockMetrics, f: F) -> R
731    where
732        F: FnOnce(&mut TypedState<K, V, T, D>) -> R,
733        // Bounds needed to read the shard upper. All callers are in contexts that
734        // already satisfy these (they mutate a fully-typed shard state).
735        K: Codec,
736        V: Codec,
737        T: Timestamp + Lattice + Codec64,
738        D: Codec64,
739    {
740        metrics.acquire_count.inc();
741        let mut state = match self.state.try_write() {
742            Ok(x) => x,
743            Err(TryLockError::WouldBlock) => {
744                metrics.blocking_acquire_count.inc();
745                let start = Instant::now();
746                let state = self.state.write().expect("lock poisoned");
747                metrics
748                    .blocking_seconds
749                    .inc_by(start.elapsed().as_secs_f64());
750                state
751            }
752            Err(TryLockError::Poisoned(err)) => panic!("state read lock poisoned: {}", err),
753        };
754        let seqno_before = state.seqno;
755        let ret = f(&mut state);
756        let seqno_after = state.seqno;
757        mz_ore::soft_assert_no_log!(seqno_after >= seqno_before);
758        if seqno_after > seqno_before {
759            // The notifier only advances the upper waiters' signal on a strict
760            // upper advance. The seqno bumps for many non-data reasons (GC,
761            // rollups, since-downgrades, other writers' CaAs) and those must not
762            // re-activate upper waiters.
763            self.notifier.notify(seqno_after, state.upper());
764        }
765        // For now, make sure to notify while under lock. It's possible to move
766        // this out of the lock window, see [StateWatchNotifier::notify].
767        drop(state);
768        ret
769    }
770
771    /// We want to _mostly_ just attempt a single CaS against the same state at once, since
772    /// only one concurrent CaS can succeed. However, we also want to guard against a
773    /// single hung update blocking all progress globally. We manage this with a shared state,
774    /// tracking whether a request is in flight and when it times out. If the timeout is never hit,
775    /// this behaves like a semaphore with limit 1... but if our requests _are_ timing out, future
776    /// requests will only wait for a bounded time before retrying, and one of those retries will
777    /// be able to claim that lease and make progress.
778    pub(crate) async fn lease_for_update(&self) -> impl Drop {
779        use tokio::time::Instant;
780
781        let timeout = STATE_UPDATE_LEASE_TIMEOUT.get(&self.cfg);
782
783        struct DropLease(Option<(AwaitableState<Option<Instant>>, Instant)>);
784
785        impl Drop for DropLease {
786            fn drop(&mut self) {
787                if let Some((state, time)) = self.0.take() {
788                    // Clear the timeout if it hasn't changed since we set it.
789                    state.maybe_modify(|s| {
790                        if s.is_some_and(|t| t == time) {
791                            *s.get_mut() = None;
792                        }
793                    })
794                }
795            }
796        }
797
798        // Special case: if the timeout is set to zero, go ahead without taking a lease.
799        if timeout.is_zero() {
800            return DropLease(None);
801        }
802
803        let timeout_state = self.update_semaphore.clone();
804        loop {
805            let now = tokio::time::Instant::now();
806            let expires_at = now + timeout;
807            // Claim the lease if there isn't one, or if the current lease has expired.
808            let maybe_leased = timeout_state.maybe_modify(|state| {
809                if let Some(other_expires_at) = **state
810                    && other_expires_at > now
811                {
812                    // Still locked: sleep until the deadline and try again.
813                    Err(other_expires_at)
814                } else {
815                    *state.get_mut() = Some(expires_at);
816                    Ok(())
817                }
818            });
819
820            match maybe_leased {
821                Ok(()) => {
822                    break DropLease(Some((timeout_state, expires_at)));
823                }
824                Err(other_expires_at) => {
825                    // Wait until either the lease has dropped or timed out, whichever is first.
826                    // If there are a lot of clients trying to update the same state, this may
827                    // cause significant lock contention... but the lock is only briefly held,
828                    // and anyways that's still cheaper than contending on the remote database.
829                    let _ = tokio::time::timeout_at(
830                        other_expires_at,
831                        timeout_state.wait_while(|s| s.is_some()),
832                    )
833                    .await;
834                }
835            }
836        }
837    }
838
839    pub(crate) fn notifier(&self) -> &StateWatchNotifier<T> {
840        &self.notifier
841    }
842}
843
844#[cfg(test)]
845mod tests {
846    use std::ops::Deref;
847    use std::pin::pin;
848    use std::str::FromStr;
849    use std::sync::atomic::{AtomicBool, Ordering};
850
851    use super::*;
852    use crate::rpc::NoopPubSubSender;
853    use futures::stream::{FuturesUnordered, StreamExt};
854    use mz_build_info::DUMMY_BUILD_INFO;
855    use mz_ore::task::spawn;
856    use mz_ore::{assert_err, assert_none};
857    use tokio::sync::oneshot;
858
859    #[mz_ore::test(tokio::test)]
860    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
861    async fn client_cache() {
862        let cache = PersistClientCache::new(
863            PersistConfig::new_for_tests(),
864            &MetricsRegistry::new(),
865            |_, _| PubSubClientConnection::noop(),
866        );
867        assert_eq!(cache.blob_by_uri.lock().await.len(), 0);
868        assert_eq!(cache.consensus_by_uri.lock().await.len(), 0);
869
870        // Opening a location on an empty cache saves the results.
871        let _ = cache
872            .open(PersistLocation {
873                blob_uri: SensitiveUrl::from_str("mem://blob_zero").expect("invalid URL"),
874                consensus_uri: SensitiveUrl::from_str("mem://consensus_zero").expect("invalid URL"),
875            })
876            .await
877            .expect("failed to open location");
878        assert_eq!(cache.blob_by_uri.lock().await.len(), 1);
879        assert_eq!(cache.consensus_by_uri.lock().await.len(), 1);
880
881        // Opening a location with an already opened consensus reuses it, even
882        // if the blob is different.
883        let _ = cache
884            .open(PersistLocation {
885                blob_uri: SensitiveUrl::from_str("mem://blob_one").expect("invalid URL"),
886                consensus_uri: SensitiveUrl::from_str("mem://consensus_zero").expect("invalid URL"),
887            })
888            .await
889            .expect("failed to open location");
890        assert_eq!(cache.blob_by_uri.lock().await.len(), 2);
891        assert_eq!(cache.consensus_by_uri.lock().await.len(), 1);
892
893        // Ditto the other way.
894        let _ = cache
895            .open(PersistLocation {
896                blob_uri: SensitiveUrl::from_str("mem://blob_one").expect("invalid URL"),
897                consensus_uri: SensitiveUrl::from_str("mem://consensus_one").expect("invalid URL"),
898            })
899            .await
900            .expect("failed to open location");
901        assert_eq!(cache.blob_by_uri.lock().await.len(), 2);
902        assert_eq!(cache.consensus_by_uri.lock().await.len(), 2);
903
904        // Query params and path matter, so we get new instances.
905        let _ = cache
906            .open(PersistLocation {
907                blob_uri: SensitiveUrl::from_str("mem://blob_one?foo").expect("invalid URL"),
908                consensus_uri: SensitiveUrl::from_str("mem://consensus_one/bar")
909                    .expect("invalid URL"),
910            })
911            .await
912            .expect("failed to open location");
913        assert_eq!(cache.blob_by_uri.lock().await.len(), 3);
914        assert_eq!(cache.consensus_by_uri.lock().await.len(), 3);
915
916        // User info and port also matter, so we get new instances.
917        let _ = cache
918            .open(PersistLocation {
919                blob_uri: SensitiveUrl::from_str("mem://user@blob_one").expect("invalid URL"),
920                consensus_uri: SensitiveUrl::from_str("mem://@consensus_one:123")
921                    .expect("invalid URL"),
922            })
923            .await
924            .expect("failed to open location");
925        assert_eq!(cache.blob_by_uri.lock().await.len(), 4);
926        assert_eq!(cache.consensus_by_uri.lock().await.len(), 4);
927    }
928
929    #[mz_ore::test(tokio::test)]
930    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
931    async fn state_cache() {
932        mz_ore::test::init_logging();
933        fn new_state<K, V, T, D>(shard_id: ShardId) -> TypedState<K, V, T, D>
934        where
935            K: Codec,
936            V: Codec,
937            T: Timestamp + Lattice + Codec64,
938            D: Codec64,
939        {
940            TypedState::new(
941                DUMMY_BUILD_INFO.semver_version(),
942                shard_id,
943                "host".into(),
944                0,
945            )
946        }
947        fn assert_same<K, V, T, D>(
948            state1: &LockingTypedState<K, V, T, D>,
949            state2: &LockingTypedState<K, V, T, D>,
950        ) {
951            let pointer1 = format!("{:p}", state1.state.read().expect("lock").deref());
952            let pointer2 = format!("{:p}", state2.state.read().expect("lock").deref());
953            assert_eq!(pointer1, pointer2);
954        }
955
956        let s1 = ShardId::new();
957        let states = Arc::new(StateCache::new_no_metrics());
958
959        // The cache starts empty.
960        assert_eq!(states.states.lock().expect("lock").len(), 0);
961
962        // Panic'ing during init_fn .
963        let s = Arc::clone(&states);
964        let res = spawn(|| "test", async move {
965            s.get::<(), (), u64, i64, _, _>(
966                s1,
967                || async { panic!("forced panic") },
968                &Diagnostics::for_tests(),
969            )
970            .await
971        })
972        .into_tokio_handle()
973        .await;
974        assert_err!(res);
975        assert_eq!(states.initialized_count(), 0);
976
977        // Returning an error from init_fn doesn't initialize an entry in the cache.
978        let res = states
979            .get::<(), (), u64, i64, _, _>(
980                s1,
981                || async {
982                    Err(Box::new(CodecMismatch {
983                        requested: ("".into(), "".into(), "".into(), "".into(), None),
984                        actual: ("".into(), "".into(), "".into(), "".into(), None),
985                    }))
986                },
987                &Diagnostics::for_tests(),
988            )
989            .await;
990        assert_err!(res);
991        assert_eq!(states.initialized_count(), 0);
992
993        // Initialize one shard.
994        let did_work = Arc::new(AtomicBool::new(false));
995        let s1_state1 = states
996            .get::<(), (), u64, i64, _, _>(
997                s1,
998                || {
999                    let did_work = Arc::clone(&did_work);
1000                    async move {
1001                        did_work.store(true, Ordering::SeqCst);
1002                        Ok(new_state(s1))
1003                    }
1004                },
1005                &Diagnostics::for_tests(),
1006            )
1007            .await
1008            .expect("should successfully initialize");
1009        assert_eq!(did_work.load(Ordering::SeqCst), true);
1010        assert_eq!(states.initialized_count(), 1);
1011        assert_eq!(states.strong_count(), 1);
1012
1013        // Trying to initialize it again does no work and returns the same state.
1014        let did_work = Arc::new(AtomicBool::new(false));
1015        let s1_state2 = states
1016            .get::<(), (), u64, i64, _, _>(
1017                s1,
1018                || {
1019                    let did_work = Arc::clone(&did_work);
1020                    async move {
1021                        did_work.store(true, Ordering::SeqCst);
1022                        did_work.store(true, Ordering::SeqCst);
1023                        Ok(new_state(s1))
1024                    }
1025                },
1026                &Diagnostics::for_tests(),
1027            )
1028            .await
1029            .expect("should successfully initialize");
1030        assert_eq!(did_work.load(Ordering::SeqCst), false);
1031        assert_eq!(states.initialized_count(), 1);
1032        assert_eq!(states.strong_count(), 1);
1033        assert_same(&s1_state1, &s1_state2);
1034
1035        // Trying to initialize with different types doesn't work.
1036        let did_work = Arc::new(AtomicBool::new(false));
1037        let res = states
1038            .get::<String, (), u64, i64, _, _>(
1039                s1,
1040                || {
1041                    let did_work = Arc::clone(&did_work);
1042                    async move {
1043                        did_work.store(true, Ordering::SeqCst);
1044                        Ok(new_state(s1))
1045                    }
1046                },
1047                &Diagnostics::for_tests(),
1048            )
1049            .await;
1050        assert_eq!(did_work.load(Ordering::SeqCst), false);
1051        assert_eq!(
1052            format!("{}", res.expect_err("types shouldn't match")),
1053            "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)\")))"
1054        );
1055        assert_eq!(states.initialized_count(), 1);
1056        assert_eq!(states.strong_count(), 1);
1057
1058        // We can add a shard of a different type.
1059        let s2 = ShardId::new();
1060        let s2_state1 = states
1061            .get::<String, (), u64, i64, _, _>(
1062                s2,
1063                || async { Ok(new_state(s2)) },
1064                &Diagnostics::for_tests(),
1065            )
1066            .await
1067            .expect("should successfully initialize");
1068        assert_eq!(states.initialized_count(), 2);
1069        assert_eq!(states.strong_count(), 2);
1070        let s2_state2 = states
1071            .get::<String, (), u64, i64, _, _>(
1072                s2,
1073                || async { Ok(new_state(s2)) },
1074                &Diagnostics::for_tests(),
1075            )
1076            .await
1077            .expect("should successfully initialize");
1078        assert_same(&s2_state1, &s2_state2);
1079
1080        // The cache holds weak references to State so we reclaim memory if the
1081        // shards stops being used.
1082        drop(s1_state1);
1083        assert_eq!(states.strong_count(), 2);
1084        drop(s1_state2);
1085        assert_eq!(states.strong_count(), 1);
1086        assert_eq!(states.initialized_count(), 2);
1087        assert_none!(states.get_cached(&s1));
1088
1089        // But we can re-init that shard if necessary.
1090        let s1_state1 = states
1091            .get::<(), (), u64, i64, _, _>(
1092                s1,
1093                || async { Ok(new_state(s1)) },
1094                &Diagnostics::for_tests(),
1095            )
1096            .await
1097            .expect("should successfully initialize");
1098        assert_eq!(states.initialized_count(), 2);
1099        assert_eq!(states.strong_count(), 2);
1100        drop(s1_state1);
1101        assert_eq!(states.strong_count(), 1);
1102    }
1103
1104    #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1105    #[cfg_attr(miri, ignore)] // too slow
1106    async fn state_cache_concurrency() {
1107        mz_ore::test::init_logging();
1108
1109        const COUNT: usize = 1000;
1110        let id = ShardId::new();
1111        let cache = StateCache::new_no_metrics();
1112        let diagnostics = Diagnostics::for_tests();
1113
1114        let mut futures = (0..COUNT)
1115            .map(|_| {
1116                cache.get::<(), (), u64, i64, _, _>(
1117                    id,
1118                    || async {
1119                        Ok(TypedState::new(
1120                            DUMMY_BUILD_INFO.semver_version(),
1121                            id,
1122                            "host".into(),
1123                            0,
1124                        ))
1125                    },
1126                    &diagnostics,
1127                )
1128            })
1129            .collect::<FuturesUnordered<_>>();
1130
1131        for _ in 0..COUNT {
1132            let _ = futures.next().await.unwrap();
1133        }
1134    }
1135
1136    #[mz_ore::test(tokio::test)]
1137    #[cfg_attr(miri, ignore)] // too slow
1138    async fn update_semaphore() {
1139        // Check that the update lease mechanism is not susceptible to futurelock.
1140        // If there is an issue, this test will time out.
1141        mz_ore::test::init_logging();
1142
1143        let shard_id = ShardId::new();
1144        let persist_config = Arc::new(PersistConfig::new_for_tests());
1145        let pubsub = Arc::new(NoopPubSubSender);
1146        let state: LockingTypedState<String, (), u64, i64> = LockingTypedState::new(
1147            shard_id,
1148            TypedState::new(
1149                DUMMY_BUILD_INFO.semver_version(),
1150                shard_id,
1151                "host".into(),
1152                0,
1153            ),
1154            Arc::new(Metrics::new(&*persist_config, &MetricsRegistry::new())),
1155            persist_config,
1156            pubsub.subscribe(&shard_id),
1157            &Diagnostics::for_tests(),
1158        );
1159
1160        // Initialize three futures, all of which will grab a lease and then poll a oneshot,
1161        // which allows us to externally trigger which ones will complete.
1162        let mk_future = || {
1163            let (tx, rx) = oneshot::channel();
1164            let future = async {
1165                let lease = state.lease_for_update().await;
1166                let () = rx.await.unwrap();
1167                drop(lease);
1168            };
1169            (future, tx)
1170        };
1171
1172        let (one, _one_tx) = mk_future();
1173        let (two, _two_tx) = mk_future();
1174        let (three, three_tx) = mk_future();
1175        let mut one = pin!(one);
1176        let mut two = pin!(two);
1177        let mut three = pin!(three);
1178
1179        // Poll all the futures, but fall through to the default case, since none are ready.
1180        tokio::select! { biased;
1181            _ = &mut one => { unreachable!() }
1182            _ = &mut two => { unreachable!() }
1183            _ = &mut three => { unreachable!() }
1184            _ = async {} => {}
1185        }
1186
1187        // Allow the third future to complete.
1188        three_tx.send(()).unwrap();
1189
1190        // Poll all the futures but the second future. This shouldn't hang, since the third future
1191        // is now ready to go and the others should eventually time out.
1192        tokio::select! { biased;
1193            _ = &mut one => { unreachable!() }
1194            _ = &mut three => {  }
1195        }
1196    }
1197
1198    #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1199    #[cfg_attr(miri, ignore)] // too slow
1200    async fn update_semaphore_stress() {
1201        // Check that the update lease mechanism is not susceptible to futurelock.
1202        // If there is an issue, this test will time out.
1203        mz_ore::test::init_logging();
1204
1205        const TIMEOUT: Duration = Duration::from_millis(100);
1206        const COUNT: u64 = 100;
1207
1208        let shard_id = ShardId::new();
1209        let persist_config = Arc::new(PersistConfig::new_for_tests());
1210        persist_config.set_config(&STATE_UPDATE_LEASE_TIMEOUT, TIMEOUT);
1211        let pubsub = Arc::new(NoopPubSubSender);
1212        let state: LockingTypedState<String, (), u64, i64> = LockingTypedState::new(
1213            shard_id,
1214            TypedState::new(
1215                DUMMY_BUILD_INFO.semver_version(),
1216                shard_id,
1217                "host".into(),
1218                0,
1219            ),
1220            Arc::new(Metrics::new(&*persist_config, &MetricsRegistry::new())),
1221            persist_config,
1222            pubsub.subscribe(&shard_id),
1223            &Diagnostics::for_tests(),
1224        );
1225
1226        let mut futures = (0..(COUNT * 3))
1227            .map(async |i| {
1228                state.lease_for_update().await;
1229                // Either hang forever, succeed quickly, or succeed after hitting the timeout.
1230                match i % 3 {
1231                    0 => {
1232                        let () = std::future::pending().await;
1233                    }
1234                    1 => {
1235                        tokio::time::sleep(Duration::from_millis(i)).await;
1236                    }
1237                    _ => {
1238                        tokio::time::sleep(Duration::from_millis(i) + TIMEOUT).await;
1239                    }
1240                }
1241            })
1242            .collect::<FuturesUnordered<_>>();
1243
1244        // All the futures that don't themselves hang forever should resolve.
1245        for _ in 0..(COUNT * 2) {
1246            futures.next().await.unwrap();
1247        }
1248    }
1249}