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.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#[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 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 *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 return Ok(x);
546 }
547 let Some(state) = state.upgrade() else {
548 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
621pub(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 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 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 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 self.notifier.notify(seqno_after, state.upper());
764 }
765 drop(state);
768 ret
769 }
770
771 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 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 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 let maybe_leased = timeout_state.maybe_modify(|state| {
809 if let Some(other_expires_at) = **state
810 && other_expires_at > now
811 {
812 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 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)] 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 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 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 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 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 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)] 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 assert_eq!(states.states.lock().expect("lock").len(), 0);
961
962 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 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 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 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 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 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 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 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)] 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)] async fn update_semaphore() {
1139 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 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 tokio::select! { biased;
1181 _ = &mut one => { unreachable!() }
1182 _ = &mut two => { unreachable!() }
1183 _ = &mut three => { unreachable!() }
1184 _ = async {} => {}
1185 }
1186
1187 three_tx.send(()).unwrap();
1189
1190 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)] async fn update_semaphore_stress() {
1201 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 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 for _ in 0..(COUNT * 2) {
1246 futures.next().await.unwrap();
1247 }
1248 }
1249}