1use 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#[derive(Debug)]
58pub struct PersistClientCache {
59 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 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 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 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 pub fn cfg(&self) -> &PersistConfig {
151 &self.cfg
152 }
153
154 pub fn metrics(&self) -> &Arc<Metrics> {
156 &self.metrics
157 }
158
159 pub fn shard_metrics(&self, shard_id: &ShardId, name: &str) -> Arc<ShardMetrics> {
161 self.metrics.shards.shard(shard_id, name)
162 }
163
164 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 #[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 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 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 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 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 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#[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 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 }
329 }
330 next_measurement = tokio::time::Instant::now() + measurement_interval;
331 }
332 })
333}
334
335#[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 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 }
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#[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 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 *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 return Ok(x);
552 }
553 let Some(state) = state.upgrade() else {
554 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
627pub(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 shard_metrics: Arc<ShardMetrics>,
640 update_semaphore: AwaitableState<Option<tokio::time::Instant>>,
641 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 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 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 self.notifier.notify(seqno_after, state.upper());
773 }
774 drop(state);
777 ret
778 }
779
780 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 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 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 let maybe_leased = timeout_state.maybe_modify(|state| {
818 if let Some(other_expires_at) = **state
819 && other_expires_at > now
820 {
821 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 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)] 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 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 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 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 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 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)] 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 assert_eq!(states.states.lock().expect("lock").len(), 0);
970
971 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 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 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 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 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 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 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 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)] 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)] async fn update_semaphore() {
1148 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 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 tokio::select! { biased;
1190 _ = &mut one => { unreachable!() }
1191 _ = &mut two => { unreachable!() }
1192 _ = &mut three => { unreachable!() }
1193 _ = async {} => {}
1194 }
1195
1196 three_tx.send(()).unwrap();
1198
1199 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)] async fn update_semaphore_stress() {
1210 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 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 for _ in 0..(COUNT * 2) {
1255 futures.next().await.unwrap();
1256 }
1257 }
1258}