Skip to main content

mz_persist_client/
rpc.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//! gRPC-based implementations of Persist PubSub client and server.
11
12use std::collections::BTreeMap;
13use std::collections::btree_map::Entry;
14use std::fmt::{Debug, Formatter};
15use std::net::SocketAddr;
16use std::pin::Pin;
17use std::str::FromStr;
18use std::sync::atomic::{AtomicUsize, Ordering};
19use std::sync::{Arc, Mutex, RwLock, Weak};
20use std::time::{Duration, Instant, SystemTime};
21
22use anyhow::{Error, anyhow};
23use async_trait::async_trait;
24use bytes::Bytes;
25use futures::Stream;
26use futures_util::StreamExt;
27use mz_dyncfg::{Config, ParameterScope};
28use mz_ore::cast::CastFrom;
29use mz_ore::collections::{HashMap, HashSet};
30use mz_ore::metrics::MetricsRegistry;
31use mz_ore::retry::RetryResult;
32use mz_ore::task::JoinHandle;
33use mz_persist::location::VersionedData;
34use mz_proto::{ProtoType, RustType};
35use prost::Message;
36use tokio::sync::mpsc::Sender;
37use tokio::sync::mpsc::error::TrySendError;
38use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
39use tokio_stream::wrappers::{BroadcastStream, ReceiverStream};
40use tonic::metadata::{AsciiMetadataKey, AsciiMetadataValue, MetadataMap};
41use tonic::transport::Endpoint;
42use tonic::{Extensions, Request, Response, Status, Streaming};
43use tracing::{Instrument, debug, error, info, info_span, warn};
44
45use crate::ShardId;
46use crate::cache::{DynState, StateCache};
47use crate::cfg::PersistConfig;
48use crate::internal::metrics::{PubSubClientCallMetrics, PubSubServerMetrics};
49use crate::internal::service::proto_persist_pub_sub_client::ProtoPersistPubSubClient;
50use crate::internal::service::proto_persist_pub_sub_server::ProtoPersistPubSubServer;
51use crate::internal::service::{
52    ProtoPubSubMessage, ProtoPushDiff, ProtoSubscribe, ProtoUnsubscribe,
53    proto_persist_pub_sub_server, proto_pub_sub_message,
54};
55use crate::metrics::Metrics;
56
57/// Determines whether PubSub clients should connect to the PubSub server.
58pub(crate) const PUBSUB_CLIENT_ENABLED: Config<bool> = Config::new(
59    "persist_pubsub_client_enabled",
60    true,
61    "Whether to connect to the Persist PubSub service.",
62    ParameterScope::Environment,
63);
64
65/// For connected clients, determines whether to push state diffs to the PubSub
66/// server. For the server, determines whether to broadcast state diffs to
67/// subscribed clients.
68pub(crate) const PUBSUB_PUSH_DIFF_ENABLED: Config<bool> = Config::new(
69    "persist_pubsub_push_diff_enabled",
70    true,
71    "Whether to push state diffs to Persist PubSub.",
72    ParameterScope::Environment,
73);
74
75/// For connected clients, determines whether to push state diffs to the PubSub
76/// server. For the server, determines whether to broadcast state diffs to
77/// subscribed clients.
78pub(crate) const PUBSUB_SAME_PROCESS_DELEGATE_ENABLED: Config<bool> = Config::new(
79    "persist_pubsub_same_process_delegate_enabled",
80    true,
81    "Whether to push state diffs to Persist PubSub on the same process.",
82    ParameterScope::Environment,
83);
84
85/// Timeout per connection attempt to Persist PubSub service.
86pub(crate) const PUBSUB_CONNECT_ATTEMPT_TIMEOUT: Config<Duration> = Config::new(
87    "persist_pubsub_connect_attempt_timeout",
88    Duration::from_secs(5),
89    "Timeout per connection attempt to Persist PubSub service.",
90    ParameterScope::Environment,
91);
92
93/// Timeout per request attempt to Persist PubSub service.
94pub(crate) const PUBSUB_REQUEST_TIMEOUT: Config<Duration> = Config::new(
95    "persist_pubsub_request_timeout",
96    Duration::from_secs(5),
97    "Timeout per request attempt to Persist PubSub service.",
98    ParameterScope::Environment,
99);
100
101/// Maximum backoff when retrying connection establishment to Persist PubSub service.
102pub(crate) const PUBSUB_CONNECT_MAX_BACKOFF: Config<Duration> = Config::new(
103    "persist_pubsub_connect_max_backoff",
104    Duration::from_secs(60),
105    "Maximum backoff when retrying connection establishment to Persist PubSub service.",
106    ParameterScope::Environment,
107);
108
109/// Size of channel used to buffer send messages to PubSub service.
110pub(crate) const PUBSUB_CLIENT_SENDER_CHANNEL_SIZE: Config<usize> = Config::new(
111    "persist_pubsub_client_sender_channel_size",
112    25,
113    "Size of channel used to buffer send messages to PubSub service.",
114    ParameterScope::Environment,
115);
116
117/// Size of channel used to buffer received messages from PubSub service.
118pub(crate) const PUBSUB_CLIENT_RECEIVER_CHANNEL_SIZE: Config<usize> = Config::new(
119    "persist_pubsub_client_receiver_channel_size",
120    25,
121    "Size of channel used to buffer received messages from PubSub service.",
122    ParameterScope::Environment,
123);
124
125/// Size of channel used per connection to buffer broadcasted messages from PubSub server.
126pub(crate) const PUBSUB_SERVER_CONNECTION_CHANNEL_SIZE: Config<usize> = Config::new(
127    "persist_pubsub_server_connection_channel_size",
128    25,
129    "Size of channel used per connection to buffer broadcasted messages from PubSub server.",
130    ParameterScope::Environment,
131);
132
133/// Size of channel used by the state cache to broadcast shard state references.
134pub(crate) const PUBSUB_STATE_CACHE_SHARD_REF_CHANNEL_SIZE: Config<usize> = Config::new(
135    "persist_pubsub_state_cache_shard_ref_channel_size",
136    25,
137    "Size of channel used by the state cache to broadcast shard state references.",
138    ParameterScope::Environment,
139);
140
141/// Backoff after an established connection to Persist PubSub service fails.
142pub(crate) const PUBSUB_RECONNECT_BACKOFF: Config<Duration> = Config::new(
143    "persist_pubsub_reconnect_backoff",
144    Duration::from_secs(5),
145    "Backoff after an established connection to Persist PubSub service fails.",
146    ParameterScope::Environment,
147);
148
149/// Max message size, used to configure gRPC servers and clients.
150///
151/// While `max_encoding_message_size` defaults to `usize::MAX`, `max_decoding_message_size` only
152/// defaults to 4MB, so we bump it to avoid protocol errors.
153const MAX_GRPC_MESSAGE_SIZE: usize = usize::MAX;
154
155/// Top-level Trait to create a PubSubClient.
156///
157/// Returns a [PubSubClientConnection] with a [PubSubSender] for issuing RPCs to the PubSub
158/// server, and a [PubSubReceiver] that receives messages, such as state diffs.
159pub trait PersistPubSubClient {
160    /// Receive handles with which to push and subscribe to diffs.
161    fn connect(
162        pubsub_config: PersistPubSubClientConfig,
163        metrics: Arc<Metrics>,
164    ) -> PubSubClientConnection;
165}
166
167/// Wrapper type for a matching [PubSubSender] and [PubSubReceiver] client pair.
168#[derive(Debug)]
169pub struct PubSubClientConnection {
170    /// The sender client to Persist PubSub.
171    pub sender: Arc<dyn PubSubSender>,
172    /// The receiver client to Persist PubSub.
173    pub receiver: Box<dyn PubSubReceiver>,
174}
175
176impl PubSubClientConnection {
177    /// Creates a new [PubSubClientConnection] from a matching [PubSubSender] and [PubSubReceiver].
178    pub fn new(sender: Arc<dyn PubSubSender>, receiver: Box<dyn PubSubReceiver>) -> Self {
179        Self { sender, receiver }
180    }
181
182    /// Creates a no-op [PubSubClientConnection] that neither sends nor receives messages.
183    pub fn noop() -> Self {
184        Self {
185            sender: Arc::new(NoopPubSubSender),
186            receiver: Box::new(futures::stream::empty()),
187        }
188    }
189}
190
191/// The public send-side client to Persist PubSub.
192pub trait PubSubSender: std::fmt::Debug + Send + Sync {
193    /// Push a diff to subscribers.
194    fn push_diff(&self, shard_id: &ShardId, diff: &VersionedData);
195
196    /// Subscribe the corresponding [PubSubReceiver] to diffs for the given shard.
197    ///
198    /// Returns a token that, when dropped, will unsubscribe the client from the
199    /// shard.
200    ///
201    /// If the client is already subscribed to the shard, repeated calls will make
202    /// no further calls to the server and instead return clones of the `Arc<ShardSubscriptionToken>`.
203    fn subscribe(self: Arc<Self>, shard_id: &ShardId) -> Arc<ShardSubscriptionToken>;
204}
205
206/// The internal send-side client trait to Persist PubSub, responsible for issuing RPCs
207/// to the PubSub service. This trait is separated out from [PubSubSender] to keep the
208/// client implementations straightforward, while offering a more ergonomic public API
209/// in [PubSubSender].
210trait PubSubSenderInternal: Debug + Send + Sync {
211    /// Push a diff to subscribers.
212    fn push_diff(&self, shard_id: &ShardId, diff: &VersionedData);
213
214    /// Subscribe the corresponding [PubSubReceiver] to diffs for the given shard.
215    ///
216    /// This call is idempotent and is a no-op for an already subscribed shard.
217    fn subscribe(&self, shard_id: &ShardId);
218
219    /// Unsubscribe the corresponding [PubSubReceiver] from diffs for the given shard.
220    ///
221    /// This call is idempotent and is a no-op for already unsubscribed shards.
222    fn unsubscribe(&self, shard_id: &ShardId);
223}
224
225/// The receive-side client to Persist PubSub.
226///
227/// Returns diffs (and maybe in the future, blobs) for any shards subscribed to
228/// by the corresponding `PubSubSender`.
229pub trait PubSubReceiver:
230    Stream<Item = ProtoPubSubMessage> + Send + Unpin + std::fmt::Debug
231{
232}
233
234impl<T> PubSubReceiver for T where
235    T: Stream<Item = ProtoPubSubMessage> + Send + Unpin + std::fmt::Debug
236{
237}
238
239/// A token corresponding to a subscription to diffs for a particular shard.
240///
241/// When dropped, the client that originated the token will be unsubscribed
242/// from further diffs to the shard.
243pub struct ShardSubscriptionToken {
244    pub(crate) shard_id: ShardId,
245    sender: Arc<dyn PubSubSenderInternal>,
246}
247
248impl Debug for ShardSubscriptionToken {
249    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
250        let ShardSubscriptionToken {
251            shard_id,
252            sender: _sender,
253        } = self;
254        write!(f, "ShardSubscriptionToken({})", shard_id)
255    }
256}
257
258impl Drop for ShardSubscriptionToken {
259    fn drop(&mut self) {
260        self.sender.unsubscribe(&self.shard_id);
261    }
262}
263
264/// A gRPC metadata key to indicate the caller id of a client.
265pub const PERSIST_PUBSUB_CALLER_KEY: &str = "persist-pubsub-caller-id";
266
267/// Client configuration for connecting to a remote PubSub server.
268#[derive(Debug)]
269pub struct PersistPubSubClientConfig {
270    /// Connection address for the pubsub server, e.g. `http://localhost:6879`
271    pub url: String,
272    /// A caller ID for the client. Used for debugging.
273    pub caller_id: String,
274    /// A copy of [PersistConfig]
275    pub persist_cfg: PersistConfig,
276}
277
278/// A [PersistPubSubClient] implementation backed by gRPC.
279///
280/// Returns a [PubSubClientConnection] backed by channels that submit and receive
281/// messages to and from a long-lived bidirectional gRPC stream. The gRPC stream
282/// will be transparently reestablished if the connection is lost.
283#[derive(Debug)]
284pub struct GrpcPubSubClient;
285
286impl GrpcPubSubClient {
287    async fn reconnect_to_server_forever(
288        send_requests: tokio::sync::broadcast::Sender<ProtoPubSubMessage>,
289        receiver_input: &tokio::sync::mpsc::Sender<ProtoPubSubMessage>,
290        sender: Arc<SubscriptionTrackingSender>,
291        metadata: MetadataMap,
292        config: PersistPubSubClientConfig,
293        metrics: Arc<Metrics>,
294    ) {
295        // Once enabled, the PubSub server cannot be disabled or otherwise
296        // reconfigured. So we wait for at least one configuration sync to
297        // complete. This gives `environmentd` at least one chance to update
298        // PubSub configuration parameters. See database-issues#7168 for details.
299        config.persist_cfg.configs_synced_once().await;
300
301        let mut is_first_connection_attempt = true;
302        loop {
303            let sender = Arc::clone(&sender);
304            metrics.pubsub_client.grpc_connection.connected.set(0);
305
306            if !PUBSUB_CLIENT_ENABLED.get(&config.persist_cfg) {
307                tokio::time::sleep(Duration::from_secs(5)).await;
308                continue;
309            }
310
311            // add a bit of backoff when reconnecting after some network/server failure
312            if is_first_connection_attempt {
313                is_first_connection_attempt = false;
314            } else {
315                tokio::time::sleep(PUBSUB_RECONNECT_BACKOFF.get(&config.persist_cfg)).await;
316            }
317
318            info!("Connecting to Persist PubSub: {}", config.url);
319            let client = mz_ore::retry::Retry::default()
320                .clamp_backoff(PUBSUB_CONNECT_MAX_BACKOFF.get(&config.persist_cfg))
321                .retry_async(|_| async {
322                    metrics
323                        .pubsub_client
324                        .grpc_connection
325                        .connect_call_attempt_count
326                        .inc();
327                    let endpoint = match Endpoint::from_str(&config.url) {
328                        Ok(endpoint) => endpoint,
329                        Err(err) => return RetryResult::FatalErr(err),
330                    };
331                    ProtoPersistPubSubClient::connect(
332                        endpoint
333                            .connect_timeout(
334                                PUBSUB_CONNECT_ATTEMPT_TIMEOUT.get(&config.persist_cfg),
335                            )
336                            .timeout(PUBSUB_REQUEST_TIMEOUT.get(&config.persist_cfg)),
337                    )
338                    .await
339                    .into()
340                })
341                .await;
342
343            let mut client = match client {
344                Ok(client) => client.max_decoding_message_size(MAX_GRPC_MESSAGE_SIZE),
345                Err(err) => {
346                    error!("fatal error connecting to persist pubsub: {:?}", err);
347                    return;
348                }
349            };
350
351            metrics
352                .pubsub_client
353                .grpc_connection
354                .connection_established_count
355                .inc();
356            metrics.pubsub_client.grpc_connection.connected.set(1);
357
358            info!("Connected to Persist PubSub: {}", config.url);
359
360            let mut broadcast = BroadcastStream::new(send_requests.subscribe());
361            let broadcast_errors = metrics
362                .pubsub_client
363                .grpc_connection
364                .broadcast_recv_lagged_count
365                .clone();
366
367            // `client.pub_sub(...)` starts a hyper background task reading from the
368            // `broadcast_messages` stream, to serve the HTTP2 connection. The broadcast stream
369            // doesn't normally terminate, which means the HTTP2 connection doesn't terminate
370            // either. We set up a cancelation token to force termination and avoid a connection
371            // leak.
372            let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel::<()>();
373
374            // shard subscriptions are tracked by connection on the server, so if our
375            // gRPC stream is ever swapped out, we must inform the server which shards
376            // our client intended to be subscribed to.
377            let broadcast_messages = async_stream::stream! {
378                let mut cancel_rx = std::pin::pin!(cancel_rx);
379                'reconnect: loop {
380                    // If we have active subscriptions, resend them.
381                    for id in sender.subscriptions() {
382                        debug!("re-subscribing to shard: {id}");
383                        let msg = proto_pub_sub_message::Message::Subscribe(
384                            ProtoSubscribe {
385                                shard_id: id.into_proto(),
386                            },
387                        );
388                        yield create_request(msg);
389                    }
390
391                    // Forward on messages from the broadcast channel, reconnecting if necessary.
392                    loop {
393                        tokio::select! {
394                            message = broadcast.next() => {
395                                debug!("sending pubsub message: {:?}", message);
396                                match message {
397                                    Some(Ok(message)) => yield message,
398                                    Some(Err(BroadcastStreamRecvError::Lagged(i))) => {
399                                        broadcast_errors.inc_by(i);
400                                        continue 'reconnect;
401                                    }
402                                    None => {
403                                        debug!("exhausted pubsub broadcast stream; shutting down");
404                                        return;
405                                    }
406                                }
407                            }
408                            _ = &mut cancel_rx => {
409                                debug!("pubsub broadcast stream cancelled; shutting down");
410                                return;
411                            }
412                        }
413                    }
414                }
415            };
416            let pubsub_request =
417                Request::from_parts(metadata.clone(), Extensions::default(), broadcast_messages);
418
419            let responses = match client.pub_sub(pubsub_request).await {
420                Ok(response) => response.into_inner(),
421                Err(err) => {
422                    warn!("pub_sub rpc error: {:?}", err);
423                    continue;
424                }
425            };
426
427            let stream_completed = GrpcPubSubClient::consume_grpc_stream(
428                responses,
429                receiver_input,
430                &config,
431                metrics.as_ref(),
432            )
433            .await;
434
435            drop(cancel_tx);
436
437            match stream_completed {
438                // common case: reconnect due to some transient error
439                Ok(_) => continue,
440                // uncommon case: we should stop connecting to the PubSub server entirely.
441                // in practice, we should only see this during shut down.
442                Err(err) => {
443                    warn!("shutting down connection loop to Persist PubSub: {}", err);
444                    return;
445                }
446            }
447        }
448    }
449
450    async fn consume_grpc_stream(
451        mut responses: Streaming<ProtoPubSubMessage>,
452        receiver_input: &Sender<ProtoPubSubMessage>,
453        config: &PersistPubSubClientConfig,
454        metrics: &Metrics,
455    ) -> Result<(), Error> {
456        loop {
457            if !PUBSUB_CLIENT_ENABLED.get(&config.persist_cfg) {
458                return Ok(());
459            }
460
461            debug!("awaiting next pubsub response");
462            match responses.next().await {
463                Some(Ok(message)) => {
464                    debug!("received pubsub message: {:?}", message);
465                    match receiver_input.send(message).await {
466                        Ok(_) => {}
467                        // if the receiver has dropped, we can drop our
468                        // no-longer-needed grpc connection entirely.
469                        Err(err) => {
470                            return Err(anyhow!("closing pubsub grpc client connection: {}", err));
471                        }
472                    }
473                }
474                Some(Err(err)) => {
475                    metrics.pubsub_client.grpc_connection.grpc_error_count.inc();
476                    warn!("pubsub client error: {:?}", err);
477                    return Ok(());
478                }
479                None => return Ok(()),
480            }
481        }
482    }
483}
484
485impl PersistPubSubClient for GrpcPubSubClient {
486    fn connect(config: PersistPubSubClientConfig, metrics: Arc<Metrics>) -> PubSubClientConnection {
487        // Create a stable channel for our client to transmit message into our gRPC stream. We use a
488        // broadcast to allow us to create new Receivers on demand, in case the underlying gRPC stream
489        // is swapped out (e.g. due to connection failure). It is expected that only 1 Receiver is
490        // ever active at a given time.
491        let (send_requests, _) = tokio::sync::broadcast::channel(
492            PUBSUB_CLIENT_SENDER_CHANNEL_SIZE.get(&config.persist_cfg),
493        );
494        // Create a stable channel to receive messages from our gRPC stream. The input end lives inside
495        // a task that continuously reads from the active gRPC stream, decoupling the `PubSubReceiver`
496        // from the lifetime of a specific gRPC connection.
497        let (receiver_input, receiver_output) = tokio::sync::mpsc::channel(
498            PUBSUB_CLIENT_RECEIVER_CHANNEL_SIZE.get(&config.persist_cfg),
499        );
500
501        let sender = Arc::new(SubscriptionTrackingSender::new(Arc::new(
502            GrpcPubSubSender {
503                metrics: Arc::clone(&metrics),
504                requests: send_requests.clone(),
505            },
506        )));
507        let pubsub_sender = Arc::clone(&sender);
508        mz_ore::task::spawn(
509            || "persist::rpc::client::connection".to_string(),
510            async move {
511                let mut metadata = MetadataMap::new();
512                metadata.insert(
513                    AsciiMetadataKey::from_static(PERSIST_PUBSUB_CALLER_KEY),
514                    AsciiMetadataValue::try_from(&config.caller_id)
515                        .unwrap_or_else(|_| AsciiMetadataValue::from_static("unknown")),
516                );
517
518                GrpcPubSubClient::reconnect_to_server_forever(
519                    send_requests,
520                    &receiver_input,
521                    pubsub_sender,
522                    metadata,
523                    config,
524                    metrics,
525                )
526                .await;
527            },
528        );
529
530        PubSubClientConnection {
531            sender,
532            receiver: Box::new(ReceiverStream::new(receiver_output)),
533        }
534    }
535}
536
537/// An internal, gRPC-backed implementation of [PubSubSender].
538struct GrpcPubSubSender {
539    metrics: Arc<Metrics>,
540    requests: tokio::sync::broadcast::Sender<ProtoPubSubMessage>,
541}
542
543impl Debug for GrpcPubSubSender {
544    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
545        let GrpcPubSubSender {
546            metrics: _metrics,
547            requests: _requests,
548        } = self;
549
550        write!(f, "GrpcPubSubSender")
551    }
552}
553
554fn create_request(message: proto_pub_sub_message::Message) -> ProtoPubSubMessage {
555    let now = SystemTime::now()
556        .duration_since(SystemTime::UNIX_EPOCH)
557        .expect("failed to get millis since epoch");
558
559    ProtoPubSubMessage {
560        timestamp: Some(now.into_proto()),
561        message: Some(message),
562    }
563}
564
565impl GrpcPubSubSender {
566    fn send(&self, message: proto_pub_sub_message::Message, metrics: &PubSubClientCallMetrics) {
567        let size = message.encoded_len();
568
569        match self.requests.send(create_request(message)) {
570            Ok(_) => {
571                metrics.succeeded.inc();
572                metrics.bytes_sent.inc_by(u64::cast_from(size));
573            }
574            Err(err) => {
575                metrics.failed.inc();
576                debug!("error sending client message: {}", err);
577            }
578        }
579    }
580}
581
582impl PubSubSenderInternal for GrpcPubSubSender {
583    fn push_diff(&self, shard_id: &ShardId, diff: &VersionedData) {
584        self.send(
585            proto_pub_sub_message::Message::PushDiff(ProtoPushDiff {
586                shard_id: shard_id.into_proto(),
587                seqno: diff.seqno.into_proto(),
588                diff: diff.data.clone(),
589            }),
590            &self.metrics.pubsub_client.sender.push,
591        )
592    }
593
594    fn subscribe(&self, shard_id: &ShardId) {
595        self.send(
596            proto_pub_sub_message::Message::Subscribe(ProtoSubscribe {
597                shard_id: shard_id.into_proto(),
598            }),
599            &self.metrics.pubsub_client.sender.subscribe,
600        )
601    }
602
603    fn unsubscribe(&self, shard_id: &ShardId) {
604        self.send(
605            proto_pub_sub_message::Message::Unsubscribe(ProtoUnsubscribe {
606                shard_id: shard_id.into_proto(),
607            }),
608            &self.metrics.pubsub_client.sender.unsubscribe,
609        )
610    }
611}
612
613/// An wrapper for a [PubSubSenderInternal] that implements [PubSubSender]
614/// by maintaining a map of active shard subscriptions to their tokens.
615#[derive(Debug)]
616struct SubscriptionTrackingSender {
617    delegate: Arc<dyn PubSubSenderInternal>,
618    subscribes: Arc<Mutex<BTreeMap<ShardId, Weak<ShardSubscriptionToken>>>>,
619}
620
621impl SubscriptionTrackingSender {
622    fn new(sender: Arc<dyn PubSubSenderInternal>) -> Self {
623        Self {
624            delegate: sender,
625            subscribes: Default::default(),
626        }
627    }
628
629    fn subscriptions(&self) -> Vec<ShardId> {
630        let mut subscribes = self.subscribes.lock().expect("lock");
631        let mut out = Vec::with_capacity(subscribes.len());
632        subscribes.retain(|shard_id, token| {
633            if token.upgrade().is_none() {
634                false
635            } else {
636                debug!("reconnecting to: {}", shard_id);
637                out.push(*shard_id);
638                true
639            }
640        });
641        out
642    }
643}
644
645impl PubSubSender for SubscriptionTrackingSender {
646    fn push_diff(&self, shard_id: &ShardId, diff: &VersionedData) {
647        self.delegate.push_diff(shard_id, diff)
648    }
649
650    fn subscribe(self: Arc<Self>, shard_id: &ShardId) -> Arc<ShardSubscriptionToken> {
651        let mut subscribes = self.subscribes.lock().expect("lock");
652        if let Some(token) = subscribes.get(shard_id) {
653            match token.upgrade() {
654                None => assert!(subscribes.remove(shard_id).is_some()),
655                Some(token) => {
656                    return Arc::clone(&token);
657                }
658            }
659        }
660
661        let pubsub_sender = Arc::clone(&self.delegate);
662        let token = Arc::new(ShardSubscriptionToken {
663            shard_id: *shard_id,
664            sender: pubsub_sender,
665        });
666
667        assert!(
668            subscribes
669                .insert(*shard_id, Arc::downgrade(&token))
670                .is_none()
671        );
672
673        self.delegate.subscribe(shard_id);
674
675        token
676    }
677}
678
679/// A wrapper intended to provide client-side metrics for a connection
680/// that communicates directly with the server state, such as one created
681/// by [PersistGrpcPubSubServer::new_same_process_connection].
682#[derive(Debug)]
683pub struct MetricsSameProcessPubSubSender {
684    delegate_subscribe: bool,
685    metrics: Arc<Metrics>,
686    delegate: Arc<dyn PubSubSender>,
687}
688
689impl MetricsSameProcessPubSubSender {
690    /// Returns a new [MetricsSameProcessPubSubSender], wrapping the given
691    /// `Arc<dyn PubSubSender>`'s calls to provide client-side metrics.
692    pub fn new(
693        cfg: &PersistConfig,
694        pubsub_sender: Arc<dyn PubSubSender>,
695        metrics: Arc<Metrics>,
696    ) -> Self {
697        Self {
698            delegate_subscribe: PUBSUB_SAME_PROCESS_DELEGATE_ENABLED.get(cfg),
699            delegate: pubsub_sender,
700            metrics,
701        }
702    }
703}
704
705impl PubSubSender for MetricsSameProcessPubSubSender {
706    fn push_diff(&self, shard_id: &ShardId, diff: &VersionedData) {
707        self.delegate.push_diff(shard_id, diff);
708        self.metrics.pubsub_client.sender.push.succeeded.inc();
709    }
710
711    fn subscribe(self: Arc<Self>, shard_id: &ShardId) -> Arc<ShardSubscriptionToken> {
712        if self.delegate_subscribe {
713            let delegate = Arc::clone(&self.delegate);
714            delegate.subscribe(shard_id)
715        } else {
716            // Create a no-op token that does not subscribe nor unsubscribe.
717            // This is ideal for single-process persist setups, since the sender and
718            // receiver should already share a state cache... but if the diffs are
719            // generated remotely but applied on the server, this may cause us to fall
720            // back to polling consensus.
721            Arc::new(ShardSubscriptionToken {
722                shard_id: *shard_id,
723                sender: Arc::new(NoopPubSubSender),
724            })
725        }
726    }
727}
728
729#[derive(Debug)]
730pub(crate) struct NoopPubSubSender;
731
732impl PubSubSenderInternal for NoopPubSubSender {
733    fn push_diff(&self, _shard_id: &ShardId, _diff: &VersionedData) {}
734    fn subscribe(&self, _shard_id: &ShardId) {}
735    fn unsubscribe(&self, _shard_id: &ShardId) {}
736}
737
738impl PubSubSender for NoopPubSubSender {
739    fn push_diff(&self, _shard_id: &ShardId, _diff: &VersionedData) {}
740
741    fn subscribe(self: Arc<Self>, shard_id: &ShardId) -> Arc<ShardSubscriptionToken> {
742        Arc::new(ShardSubscriptionToken {
743            shard_id: *shard_id,
744            sender: self,
745        })
746    }
747}
748
749/// Spawns a Tokio task that consumes a [PubSubReceiver], applying its diffs to a [StateCache].
750pub(crate) fn subscribe_state_cache_to_pubsub(
751    cache: Arc<StateCache>,
752    mut pubsub_receiver: Box<dyn PubSubReceiver>,
753) -> JoinHandle<()> {
754    let mut state_refs: HashMap<ShardId, Weak<dyn DynState>> = HashMap::new();
755    let receiver_metrics = cache.metrics.pubsub_client.receiver.clone();
756
757    mz_ore::task::spawn(
758        || "persist::rpc::client::state_cache_diff_apply",
759        async move {
760            while let Some(msg) = pubsub_receiver.next().await {
761                match msg.message {
762                    Some(proto_pub_sub_message::Message::PushDiff(diff)) => {
763                        receiver_metrics.push_received.inc();
764                        let shard_id = diff.shard_id.into_rust().expect("valid shard id");
765                        let diff = VersionedData {
766                            seqno: diff.seqno.into_rust().expect("valid SeqNo"),
767                            data: diff.diff,
768                        };
769                        debug!(
770                            "applying pubsub diff {} {} {}",
771                            shard_id,
772                            diff.seqno,
773                            diff.data.len()
774                        );
775
776                        let mut pushed_diff = false;
777                        if let Some(state_ref) = state_refs.get(&shard_id) {
778                            // common case: we have a reference to the shard state already
779                            // and can apply our diff directly.
780                            if let Some(state) = state_ref.upgrade() {
781                                state.push_diff(diff.clone());
782                                pushed_diff = true;
783                                receiver_metrics.state_pushed_diff_fast_path.inc();
784                            }
785                        }
786
787                        if !pushed_diff {
788                            // uncommon case: we either don't have a reference yet, or ours
789                            // is out-of-date (e.g. the shard was dropped and then re-added
790                            // to StateCache). here we'll fetch the latest, try to apply the
791                            // diff again, and update our local reference.
792                            let state_ref = cache.get_state_weak(&shard_id);
793                            match state_ref {
794                                None => {
795                                    state_refs.remove(&shard_id);
796                                }
797                                Some(state_ref) => {
798                                    if let Some(state) = state_ref.upgrade() {
799                                        state.push_diff(diff);
800                                        pushed_diff = true;
801                                        state_refs.insert(shard_id, state_ref);
802                                    } else {
803                                        state_refs.remove(&shard_id);
804                                    }
805                                }
806                            }
807
808                            if pushed_diff {
809                                receiver_metrics.state_pushed_diff_slow_path_succeeded.inc();
810                            } else {
811                                receiver_metrics.state_pushed_diff_slow_path_failed.inc();
812                            }
813                        }
814
815                        if let Some(send_timestamp) = msg.timestamp {
816                            let send_timestamp =
817                                send_timestamp.into_rust().expect("valid timestamp");
818                            let now = SystemTime::now()
819                                .duration_since(SystemTime::UNIX_EPOCH)
820                                .expect("failed to get millis since epoch");
821                            receiver_metrics
822                                .approx_diff_latency_seconds
823                                .observe((now.saturating_sub(send_timestamp)).as_secs_f64());
824                        }
825                    }
826                    ref msg @ None | ref msg @ Some(_) => {
827                        warn!("pubsub client received unexpected message: {:?}", msg);
828                        receiver_metrics.unknown_message_received.inc();
829                    }
830                }
831            }
832        },
833    )
834}
835
836/// Internal state of a PubSub server implementation.
837#[derive(Debug)]
838pub(crate) struct PubSubState {
839    /// Assigns a unique ID to each incoming connection.
840    connection_id_counter: AtomicUsize,
841    /// Maintains a mapping of `ShardId --> [ConnectionId -> Tx]`.
842    shard_subscribers:
843        Arc<RwLock<BTreeMap<ShardId, BTreeMap<usize, Sender<Result<ProtoPubSubMessage, Status>>>>>>,
844    /// Active connections.
845    connections: Arc<RwLock<HashSet<usize>>>,
846    /// Server-side metrics.
847    metrics: Arc<PubSubServerMetrics>,
848}
849
850impl PubSubState {
851    fn new_connection(
852        self: Arc<Self>,
853        notifier: Sender<Result<ProtoPubSubMessage, Status>>,
854    ) -> PubSubConnection {
855        let connection_id = self.connection_id_counter.fetch_add(1, Ordering::SeqCst);
856        {
857            debug!("inserting connid: {}", connection_id);
858            let mut connections = self.connections.write().expect("lock");
859            assert!(connections.insert(connection_id));
860        }
861
862        self.metrics.active_connections.inc();
863        PubSubConnection {
864            connection_id,
865            notifier,
866            state: self,
867        }
868    }
869
870    fn remove_connection(&self, connection_id: usize) {
871        let now = Instant::now();
872
873        {
874            debug!("removing connid: {}", connection_id);
875            let mut connections = self.connections.write().expect("lock");
876            assert!(
877                connections.remove(&connection_id),
878                "unknown connection id: {}",
879                connection_id
880            );
881        }
882
883        {
884            let mut subscribers = self.shard_subscribers.write().expect("lock poisoned");
885            subscribers.retain(|_shard, connections_for_shard| {
886                connections_for_shard.remove(&connection_id);
887                !connections_for_shard.is_empty()
888            });
889        }
890
891        self.metrics
892            .connection_cleanup_seconds
893            .inc_by(now.elapsed().as_secs_f64());
894        self.metrics.active_connections.dec();
895    }
896
897    fn push_diff(&self, connection_id: usize, shard_id: &ShardId, data: &VersionedData) {
898        let now = Instant::now();
899        self.metrics.push_call_count.inc();
900
901        assert!(
902            self.connections
903                .read()
904                .expect("lock")
905                .contains(&connection_id),
906            "unknown connection id: {}",
907            connection_id
908        );
909
910        let subscribers = self.shard_subscribers.read().expect("lock poisoned");
911        if let Some(subscribed_connections) = subscribers.get(shard_id) {
912            let mut num_sent = 0;
913            let mut data_size = 0;
914
915            for (subscribed_conn_id, tx) in subscribed_connections {
916                // skip sending the diff back to the original sender
917                if *subscribed_conn_id == connection_id {
918                    continue;
919                }
920                debug!(
921                    "server forwarding req to conn {}: {} {} {}",
922                    subscribed_conn_id,
923                    &shard_id,
924                    data.seqno,
925                    data.data.len()
926                );
927                let req = create_request(proto_pub_sub_message::Message::PushDiff(ProtoPushDiff {
928                    seqno: data.seqno.into_proto(),
929                    shard_id: shard_id.to_string(),
930                    diff: Bytes::clone(&data.data),
931                }));
932                data_size = req.encoded_len();
933                match tx.try_send(Ok(req)) {
934                    Ok(_) => {
935                        num_sent += 1;
936                    }
937                    Err(TrySendError::Full(_)) => {
938                        self.metrics.broadcasted_diff_dropped_channel_full.inc();
939                    }
940                    Err(TrySendError::Closed(_)) => {}
941                };
942            }
943
944            self.metrics.broadcasted_diff_count.inc_by(num_sent);
945            self.metrics
946                .broadcasted_diff_bytes
947                .inc_by(num_sent * u64::cast_from(data_size));
948        }
949
950        self.metrics
951            .push_seconds
952            .inc_by(now.elapsed().as_secs_f64());
953    }
954
955    fn subscribe(
956        &self,
957        connection_id: usize,
958        notifier: Sender<Result<ProtoPubSubMessage, Status>>,
959        shard_id: &ShardId,
960    ) {
961        let now = Instant::now();
962        self.metrics.subscribe_call_count.inc();
963
964        assert!(
965            self.connections
966                .read()
967                .expect("lock")
968                .contains(&connection_id),
969            "unknown connection id: {}",
970            connection_id
971        );
972
973        {
974            let mut subscribed_shards = self.shard_subscribers.write().expect("lock poisoned");
975            subscribed_shards
976                .entry(*shard_id)
977                .or_default()
978                .insert(connection_id, notifier);
979        }
980
981        self.metrics
982            .subscribe_seconds
983            .inc_by(now.elapsed().as_secs_f64());
984    }
985
986    fn unsubscribe(&self, connection_id: usize, shard_id: &ShardId) {
987        let now = Instant::now();
988        self.metrics.unsubscribe_call_count.inc();
989
990        assert!(
991            self.connections
992                .read()
993                .expect("lock")
994                .contains(&connection_id),
995            "unknown connection id: {}",
996            connection_id
997        );
998
999        {
1000            let mut subscribed_shards = self.shard_subscribers.write().expect("lock poisoned");
1001            if let Entry::Occupied(mut entry) = subscribed_shards.entry(*shard_id) {
1002                let subscribed_connections = entry.get_mut();
1003                subscribed_connections.remove(&connection_id);
1004
1005                if subscribed_connections.is_empty() {
1006                    entry.remove_entry();
1007                }
1008            }
1009        }
1010
1011        self.metrics
1012            .unsubscribe_seconds
1013            .inc_by(now.elapsed().as_secs_f64());
1014    }
1015
1016    #[cfg(test)]
1017    fn new_for_test() -> Self {
1018        Self {
1019            connection_id_counter: AtomicUsize::new(0),
1020            shard_subscribers: Default::default(),
1021            connections: Default::default(),
1022            metrics: Arc::new(PubSubServerMetrics::new(&MetricsRegistry::new())),
1023        }
1024    }
1025
1026    #[cfg(test)]
1027    fn active_connections(&self) -> HashSet<usize> {
1028        self.connections.read().expect("lock").clone()
1029    }
1030
1031    #[cfg(test)]
1032    fn subscriptions(&self, connection_id: usize) -> HashSet<ShardId> {
1033        let mut shards = HashSet::new();
1034
1035        let subscribers = self.shard_subscribers.read().expect("lock");
1036        for (shard, subscribed_connections) in subscribers.iter() {
1037            if subscribed_connections.contains_key(&connection_id) {
1038                shards.insert(*shard);
1039            }
1040        }
1041
1042        shards
1043    }
1044
1045    #[cfg(test)]
1046    fn shard_subscription_counts(&self) -> mz_ore::collections::HashMap<ShardId, usize> {
1047        let mut shards = mz_ore::collections::HashMap::new();
1048
1049        let subscribers = self.shard_subscribers.read().expect("lock");
1050        for (shard, subscribed_connections) in subscribers.iter() {
1051            shards.insert(*shard, subscribed_connections.len());
1052        }
1053
1054        shards
1055    }
1056}
1057
1058/// A gRPC-based implementation of a Persist PubSub server.
1059#[derive(Debug)]
1060pub struct PersistGrpcPubSubServer {
1061    cfg: PersistConfig,
1062    state: Arc<PubSubState>,
1063}
1064
1065impl PersistGrpcPubSubServer {
1066    /// Creates a new [PersistGrpcPubSubServer].
1067    pub fn new(cfg: &PersistConfig, metrics_registry: &MetricsRegistry) -> Self {
1068        let metrics = PubSubServerMetrics::new(metrics_registry);
1069        let state = Arc::new(PubSubState {
1070            connection_id_counter: AtomicUsize::new(0),
1071            shard_subscribers: Default::default(),
1072            connections: Default::default(),
1073            metrics: Arc::new(metrics),
1074        });
1075
1076        PersistGrpcPubSubServer {
1077            cfg: cfg.clone(),
1078            state,
1079        }
1080    }
1081
1082    /// Creates a connection to [PersistGrpcPubSubServer] that is directly connected
1083    /// to the server state. Calls into this connection do not go over the network
1084    /// nor require message serde.
1085    pub fn new_same_process_connection(&self) -> PubSubClientConnection {
1086        let (tx, rx) =
1087            tokio::sync::mpsc::channel(PUBSUB_CLIENT_RECEIVER_CHANNEL_SIZE.get(&self.cfg));
1088        let sender: Arc<dyn PubSubSender> = Arc::new(SubscriptionTrackingSender::new(Arc::new(
1089            Arc::clone(&self.state).new_connection(tx),
1090        )));
1091
1092        PubSubClientConnection {
1093            sender,
1094            receiver: Box::new(
1095                ReceiverStream::new(rx).map(|x| x.expect("cannot receive grpc errors locally")),
1096            ),
1097        }
1098    }
1099
1100    /// Starts the gRPC server. Consumes `self` and runs until the task is cancelled.
1101    pub async fn serve(self, listen_addr: SocketAddr) -> Result<(), anyhow::Error> {
1102        // Increase the default message decoding limit to avoid unnecessary panics
1103        tonic::transport::Server::builder()
1104            .add_service(
1105                ProtoPersistPubSubServer::new(self)
1106                    .max_decoding_message_size(MAX_GRPC_MESSAGE_SIZE),
1107            )
1108            .serve(listen_addr)
1109            .await?;
1110        Ok(())
1111    }
1112
1113    /// Starts the gRPC server with the given listener stream.
1114    /// Consumes `self` and runs until the task is cancelled.
1115    pub async fn serve_with_stream(
1116        self,
1117        listener: tokio_stream::wrappers::TcpListenerStream,
1118    ) -> Result<(), anyhow::Error> {
1119        tonic::transport::Server::builder()
1120            .add_service(
1121                ProtoPersistPubSubServer::new(self)
1122                    .max_decoding_message_size(MAX_GRPC_MESSAGE_SIZE),
1123            )
1124            .serve_with_incoming(listener)
1125            .await?;
1126        Ok(())
1127    }
1128}
1129
1130#[async_trait]
1131impl proto_persist_pub_sub_server::ProtoPersistPubSub for PersistGrpcPubSubServer {
1132    type PubSubStream = Pin<Box<dyn Stream<Item = Result<ProtoPubSubMessage, Status>> + Send>>;
1133
1134    #[mz_ore::instrument(name = "persist::rpc::server", level = "info")]
1135    async fn pub_sub(
1136        &self,
1137        request: Request<Streaming<ProtoPubSubMessage>>,
1138    ) -> Result<Response<Self::PubSubStream>, Status> {
1139        let caller_id = request
1140            .metadata()
1141            .get(AsciiMetadataKey::from_static(PERSIST_PUBSUB_CALLER_KEY))
1142            .map(|key| key.to_str().ok())
1143            .flatten()
1144            .map(|key| key.to_string())
1145            .unwrap_or_else(|| "unknown".to_string());
1146        info!("Received Persist PubSub connection from: {:?}", caller_id);
1147
1148        let mut in_stream = request.into_inner();
1149        let (tx, rx) =
1150            tokio::sync::mpsc::channel(PUBSUB_SERVER_CONNECTION_CHANNEL_SIZE.get(&self.cfg));
1151
1152        let caller = caller_id.clone();
1153        let cfg = Arc::clone(&self.cfg.configs);
1154        let server_state = Arc::clone(&self.state);
1155        // this spawn here to cleanup after connection error / disconnect, otherwise the stream
1156        // would not be polled after the connection drops. in our case, we want to clear the
1157        // connection and its subscriptions from our shared state when it drops.
1158        let connection_span = info_span!("connection", caller_id);
1159        mz_ore::task::spawn(
1160            || format!("persist_pubsub_connection({})", caller),
1161            async move {
1162                let connection = server_state.new_connection(tx);
1163                while let Some(result) = in_stream.next().await {
1164                    let req = match result {
1165                        Ok(req) => req,
1166                        Err(err) => {
1167                            warn!("pubsub connection err: {}", err);
1168                            break;
1169                        }
1170                    };
1171
1172                    match req.message {
1173                        None => {
1174                            warn!("received empty message from: {}", caller_id);
1175                        }
1176                        Some(proto_pub_sub_message::Message::PushDiff(req)) => {
1177                            let shard_id = req.shard_id.parse().expect("valid shard id");
1178                            let diff = VersionedData {
1179                                seqno: req.seqno.into_rust().expect("valid seqno"),
1180                                data: req.diff.clone(),
1181                            };
1182                            if PUBSUB_PUSH_DIFF_ENABLED.get(&cfg) {
1183                                connection.push_diff(&shard_id, &diff);
1184                            }
1185                        }
1186                        Some(proto_pub_sub_message::Message::Subscribe(diff)) => {
1187                            let shard_id = diff.shard_id.parse().expect("valid shard id");
1188                            connection.subscribe(&shard_id);
1189                        }
1190                        Some(proto_pub_sub_message::Message::Unsubscribe(diff)) => {
1191                            let shard_id = diff.shard_id.parse().expect("valid shard id");
1192                            connection.unsubscribe(&shard_id);
1193                        }
1194                    }
1195                }
1196
1197                info!("Persist PubSub connection ended: {:?}", caller_id);
1198            }
1199            .instrument(connection_span),
1200        );
1201
1202        let out_stream: Self::PubSubStream = Box::pin(ReceiverStream::new(rx));
1203        Ok(Response::new(out_stream))
1204    }
1205}
1206
1207/// An active connection managed by [PubSubState].
1208///
1209/// When dropped, removes itself from [PubSubState], clearing all of its subscriptions.
1210#[derive(Debug)]
1211pub(crate) struct PubSubConnection {
1212    connection_id: usize,
1213    notifier: Sender<Result<ProtoPubSubMessage, Status>>,
1214    state: Arc<PubSubState>,
1215}
1216
1217impl PubSubSenderInternal for PubSubConnection {
1218    fn push_diff(&self, shard_id: &ShardId, diff: &VersionedData) {
1219        self.state.push_diff(self.connection_id, shard_id, diff)
1220    }
1221
1222    fn subscribe(&self, shard_id: &ShardId) {
1223        self.state
1224            .subscribe(self.connection_id, self.notifier.clone(), shard_id)
1225    }
1226
1227    fn unsubscribe(&self, shard_id: &ShardId) {
1228        self.state.unsubscribe(self.connection_id, shard_id)
1229    }
1230}
1231
1232impl Drop for PubSubConnection {
1233    fn drop(&mut self) {
1234        self.state.remove_connection(self.connection_id)
1235    }
1236}
1237
1238#[cfg(test)]
1239mod pubsub_state {
1240    use std::str::FromStr;
1241    use std::sync::Arc;
1242    use std::sync::LazyLock;
1243
1244    use bytes::Bytes;
1245    use mz_ore::collections::HashSet;
1246    use mz_persist::location::{SeqNo, VersionedData};
1247    use mz_proto::RustType;
1248    use tokio::sync::mpsc::Receiver;
1249    use tokio::sync::mpsc::error::TryRecvError;
1250    use tonic::Status;
1251
1252    use crate::ShardId;
1253    use crate::internal::service::ProtoPubSubMessage;
1254    use crate::internal::service::proto_pub_sub_message::Message;
1255    use crate::rpc::{PubSubSenderInternal, PubSubState};
1256
1257    static SHARD_ID_0: LazyLock<ShardId> =
1258        LazyLock::new(|| ShardId::from_str("s00000000-0000-0000-0000-000000000000").unwrap());
1259    static SHARD_ID_1: LazyLock<ShardId> =
1260        LazyLock::new(|| ShardId::from_str("s11111111-1111-1111-1111-111111111111").unwrap());
1261
1262    const VERSIONED_DATA_0: VersionedData = VersionedData {
1263        seqno: SeqNo(0),
1264        data: Bytes::from_static(&[0, 1, 2, 3]),
1265    };
1266
1267    const VERSIONED_DATA_1: VersionedData = VersionedData {
1268        seqno: SeqNo(1),
1269        data: Bytes::from_static(&[4, 5, 6, 7]),
1270    };
1271
1272    #[mz_ore::test]
1273    #[should_panic(expected = "unknown connection id: 100")]
1274    fn test_zero_connections_push_diff() {
1275        let state = Arc::new(PubSubState::new_for_test());
1276        state.push_diff(100, &SHARD_ID_0, &VERSIONED_DATA_0);
1277    }
1278
1279    #[mz_ore::test]
1280    #[should_panic(expected = "unknown connection id: 100")]
1281    fn test_zero_connections_subscribe() {
1282        let state = Arc::new(PubSubState::new_for_test());
1283        let (tx, _) = tokio::sync::mpsc::channel(100);
1284        state.subscribe(100, tx, &SHARD_ID_0);
1285    }
1286
1287    #[mz_ore::test]
1288    #[should_panic(expected = "unknown connection id: 100")]
1289    fn test_zero_connections_unsubscribe() {
1290        let state = Arc::new(PubSubState::new_for_test());
1291        state.unsubscribe(100, &SHARD_ID_0);
1292    }
1293
1294    #[mz_ore::test]
1295    #[should_panic(expected = "unknown connection id: 100")]
1296    fn test_zero_connections_remove() {
1297        let state = Arc::new(PubSubState::new_for_test());
1298        state.remove_connection(100)
1299    }
1300
1301    #[mz_ore::test]
1302    fn test_single_connection() {
1303        let state = Arc::new(PubSubState::new_for_test());
1304
1305        let (tx, mut rx) = tokio::sync::mpsc::channel(100);
1306        let connection = Arc::clone(&state).new_connection(tx);
1307
1308        assert_eq!(
1309            state.active_connections(),
1310            HashSet::from([connection.connection_id])
1311        );
1312
1313        // no messages should have been broadcasted yet
1314        assert!(matches!(rx.try_recv(), Err(TryRecvError::Empty)));
1315
1316        connection.push_diff(
1317            &SHARD_ID_0,
1318            &VersionedData {
1319                seqno: SeqNo::minimum(),
1320                data: Bytes::new(),
1321            },
1322        );
1323
1324        // server should not broadcast a message back to originating client
1325        assert!(matches!(rx.try_recv(), Err(TryRecvError::Empty)));
1326
1327        // a connection can subscribe to a shard
1328        connection.subscribe(&SHARD_ID_0);
1329        assert_eq!(
1330            state.subscriptions(connection.connection_id),
1331            HashSet::from([SHARD_ID_0.clone()])
1332        );
1333
1334        // a connection can unsubscribe
1335        connection.unsubscribe(&SHARD_ID_0);
1336        assert!(state.subscriptions(connection.connection_id).is_empty());
1337
1338        // a connection can subscribe to many shards
1339        connection.subscribe(&SHARD_ID_0);
1340        connection.subscribe(&SHARD_ID_1);
1341        assert_eq!(
1342            state.subscriptions(connection.connection_id),
1343            HashSet::from([*SHARD_ID_0, *SHARD_ID_1])
1344        );
1345
1346        // and to a single shard many times idempotently
1347        connection.subscribe(&SHARD_ID_0);
1348        connection.subscribe(&SHARD_ID_0);
1349        assert_eq!(
1350            state.subscriptions(connection.connection_id),
1351            HashSet::from([*SHARD_ID_0, *SHARD_ID_1])
1352        );
1353
1354        // dropping the connection should unsubscribe all shards and unregister the connection
1355        let connection_id = connection.connection_id;
1356        drop(connection);
1357        assert!(state.subscriptions(connection_id).is_empty());
1358        assert!(state.active_connections().is_empty());
1359    }
1360
1361    #[mz_ore::test]
1362    fn test_many_connection() {
1363        let state = Arc::new(PubSubState::new_for_test());
1364
1365        let (tx1, mut rx1) = tokio::sync::mpsc::channel(100);
1366        let conn1 = Arc::clone(&state).new_connection(tx1);
1367
1368        let (tx2, mut rx2) = tokio::sync::mpsc::channel(100);
1369        let conn2 = Arc::clone(&state).new_connection(tx2);
1370
1371        let (tx3, mut rx3) = tokio::sync::mpsc::channel(100);
1372        let conn3 = Arc::clone(&state).new_connection(tx3);
1373
1374        conn1.subscribe(&SHARD_ID_0);
1375        conn2.subscribe(&SHARD_ID_0);
1376        conn2.subscribe(&SHARD_ID_1);
1377
1378        assert_eq!(
1379            state.active_connections(),
1380            HashSet::from([
1381                conn1.connection_id,
1382                conn2.connection_id,
1383                conn3.connection_id
1384            ])
1385        );
1386
1387        // broadcast a diff to a shard subscribed to by several connections
1388        conn3.push_diff(&SHARD_ID_0, &VERSIONED_DATA_0);
1389        assert_push(&mut rx1, &SHARD_ID_0, &VERSIONED_DATA_0);
1390        assert_push(&mut rx2, &SHARD_ID_0, &VERSIONED_DATA_0);
1391        assert!(matches!(rx3.try_recv(), Err(TryRecvError::Empty)));
1392
1393        // broadcast a diff shared by publisher. it should not receive the diff back.
1394        conn1.push_diff(&SHARD_ID_0, &VERSIONED_DATA_0);
1395        assert!(matches!(rx1.try_recv(), Err(TryRecvError::Empty)));
1396        assert_push(&mut rx2, &SHARD_ID_0, &VERSIONED_DATA_0);
1397        assert!(matches!(rx3.try_recv(), Err(TryRecvError::Empty)));
1398
1399        // broadcast a diff to a shard subscribed to by one connection
1400        conn3.push_diff(&SHARD_ID_1, &VERSIONED_DATA_1);
1401        assert!(matches!(rx1.try_recv(), Err(TryRecvError::Empty)));
1402        assert_push(&mut rx2, &SHARD_ID_1, &VERSIONED_DATA_1);
1403        assert!(matches!(rx3.try_recv(), Err(TryRecvError::Empty)));
1404
1405        // broadcast a diff to a shard subscribed to by no connections
1406        conn2.unsubscribe(&SHARD_ID_1);
1407        conn3.push_diff(&SHARD_ID_1, &VERSIONED_DATA_1);
1408        assert!(matches!(rx1.try_recv(), Err(TryRecvError::Empty)));
1409        assert!(matches!(rx2.try_recv(), Err(TryRecvError::Empty)));
1410        assert!(matches!(rx3.try_recv(), Err(TryRecvError::Empty)));
1411
1412        // dropping connections unsubscribes them
1413        let conn1_id = conn1.connection_id;
1414        drop(conn1);
1415        conn3.push_diff(&SHARD_ID_0, &VERSIONED_DATA_0);
1416        assert!(matches!(rx1.try_recv(), Err(TryRecvError::Disconnected)));
1417        assert_push(&mut rx2, &SHARD_ID_0, &VERSIONED_DATA_0);
1418        assert!(matches!(rx3.try_recv(), Err(TryRecvError::Empty)));
1419
1420        assert!(state.subscriptions(conn1_id).is_empty());
1421        assert_eq!(
1422            state.subscriptions(conn2.connection_id),
1423            HashSet::from([*SHARD_ID_0])
1424        );
1425        assert_eq!(state.subscriptions(conn3.connection_id), HashSet::new());
1426        assert_eq!(
1427            state.active_connections(),
1428            HashSet::from([conn2.connection_id, conn3.connection_id])
1429        );
1430    }
1431
1432    fn assert_push(
1433        rx: &mut Receiver<Result<ProtoPubSubMessage, Status>>,
1434        shard: &ShardId,
1435        data: &VersionedData,
1436    ) {
1437        let message = rx
1438            .try_recv()
1439            .expect("message in channel")
1440            .expect("pubsub")
1441            .message
1442            .expect("proto contains message");
1443        match message {
1444            Message::PushDiff(x) => {
1445                assert_eq!(x.shard_id, shard.into_proto());
1446                assert_eq!(x.seqno, data.seqno.into_proto());
1447                assert_eq!(x.diff, data.data);
1448            }
1449            Message::Subscribe(_) | Message::Unsubscribe(_) => panic!("unexpected message type"),
1450        };
1451    }
1452}
1453
1454#[cfg(test)]
1455mod grpc {
1456    use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
1457    use std::str::FromStr;
1458    use std::sync::Arc;
1459    use std::time::{Duration, Instant};
1460
1461    use bytes::Bytes;
1462    use futures_util::FutureExt;
1463    use mz_dyncfg::ConfigUpdates;
1464    use mz_ore::assert_none;
1465    use mz_ore::collections::HashMap;
1466    use mz_ore::metrics::MetricsRegistry;
1467    use mz_persist::location::{SeqNo, VersionedData};
1468    use mz_proto::RustType;
1469    use std::sync::LazyLock;
1470    use tokio::net::TcpListener;
1471    use tokio_stream::StreamExt;
1472    use tokio_stream::wrappers::TcpListenerStream;
1473
1474    use crate::ShardId;
1475    use crate::cfg::PersistConfig;
1476    use crate::internal::service::ProtoPubSubMessage;
1477    use crate::internal::service::proto_pub_sub_message::Message;
1478    use crate::metrics::Metrics;
1479    use crate::rpc::{
1480        GrpcPubSubClient, PUBSUB_CLIENT_ENABLED, PUBSUB_RECONNECT_BACKOFF, PersistGrpcPubSubServer,
1481        PersistPubSubClient, PersistPubSubClientConfig, PubSubState,
1482    };
1483
1484    static SHARD_ID_0: LazyLock<ShardId> =
1485        LazyLock::new(|| ShardId::from_str("s00000000-0000-0000-0000-000000000000").unwrap());
1486    static SHARD_ID_1: LazyLock<ShardId> =
1487        LazyLock::new(|| ShardId::from_str("s11111111-1111-1111-1111-111111111111").unwrap());
1488    const VERSIONED_DATA_0: VersionedData = VersionedData {
1489        seqno: SeqNo(0),
1490        data: Bytes::from_static(&[0, 1, 2, 3]),
1491    };
1492    const VERSIONED_DATA_1: VersionedData = VersionedData {
1493        seqno: SeqNo(1),
1494        data: Bytes::from_static(&[4, 5, 6, 7]),
1495    };
1496
1497    const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
1498    const SUBSCRIPTIONS_TIMEOUT: Duration = Duration::from_secs(3);
1499    const SERVER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(2);
1500
1501    // NB: we use separate runtimes for client and server throughout these tests to cleanly drop
1502    // ALL tasks (including spawned child tasks) associated with one end of a connection, to most
1503    // closely model an actual disconnect.
1504
1505    #[mz_ore::test]
1506    #[cfg_attr(miri, ignore)] // error: unsupported operation: can't call foreign function `socket` on OS `linux`
1507    fn grpc_server() {
1508        let metrics = Arc::new(Metrics::new(
1509            &test_persist_config(),
1510            &MetricsRegistry::new(),
1511        ));
1512        let server_runtime = tokio::runtime::Runtime::new().expect("server runtime");
1513        let client_runtime = tokio::runtime::Runtime::new().expect("client runtime");
1514
1515        // start the server
1516        let (addr, tcp_listener_stream) = server_runtime.block_on(new_tcp_listener());
1517        let server_state = server_runtime.block_on(spawn_server(tcp_listener_stream));
1518
1519        // start a client.
1520        {
1521            let _guard = client_runtime.enter();
1522            mz_ore::task::spawn(|| "client".to_string(), async move {
1523                let client = GrpcPubSubClient::connect(
1524                    PersistPubSubClientConfig {
1525                        url: format!("http://{}", addr),
1526                        caller_id: "client".to_string(),
1527                        persist_cfg: test_persist_config(),
1528                    },
1529                    metrics,
1530                );
1531                let _token = client.sender.subscribe(&SHARD_ID_0);
1532                tokio::time::sleep(Duration::MAX).await;
1533            });
1534        }
1535
1536        // wait until the client is connected and subscribed
1537        server_runtime.block_on(async {
1538            poll_until_true(CONNECT_TIMEOUT, || {
1539                server_state.active_connections().len() == 1
1540            })
1541            .await;
1542            poll_until_true(SUBSCRIPTIONS_TIMEOUT, || {
1543                server_state.shard_subscription_counts() == HashMap::from([(*SHARD_ID_0, 1)])
1544            })
1545            .await
1546        });
1547
1548        // drop the client
1549        client_runtime.shutdown_timeout(SERVER_SHUTDOWN_TIMEOUT);
1550
1551        // server should notice the client dropping and clean up its state
1552        server_runtime.block_on(async {
1553            poll_until_true(CONNECT_TIMEOUT, || {
1554                server_state.active_connections().is_empty()
1555            })
1556            .await;
1557            poll_until_true(SUBSCRIPTIONS_TIMEOUT, || {
1558                server_state.shard_subscription_counts() == HashMap::new()
1559            })
1560            .await
1561        });
1562    }
1563
1564    #[mz_ore::test]
1565    #[cfg_attr(miri, ignore)] // error: unsupported operation: can't call foreign function `socket` on OS `linux`
1566    fn grpc_client_sender_reconnects() {
1567        let metrics = Arc::new(Metrics::new(
1568            &test_persist_config(),
1569            &MetricsRegistry::new(),
1570        ));
1571        let server_runtime = tokio::runtime::Runtime::new().expect("server runtime");
1572        let client_runtime = tokio::runtime::Runtime::new().expect("client runtime");
1573        let (addr, tcp_listener_stream) = server_runtime.block_on(new_tcp_listener());
1574
1575        // start a client
1576        let client = client_runtime.block_on(async {
1577            GrpcPubSubClient::connect(
1578                PersistPubSubClientConfig {
1579                    url: format!("http://{}", addr),
1580                    caller_id: "client".to_string(),
1581                    persist_cfg: test_persist_config(),
1582                },
1583                metrics,
1584            )
1585        });
1586
1587        // we can subscribe before connecting to the pubsub server
1588        let _token = Arc::clone(&client.sender).subscribe(&SHARD_ID_0);
1589        // we can subscribe and unsubscribe before connecting to the pubsub server
1590        let _token_2 = Arc::clone(&client.sender).subscribe(&SHARD_ID_1);
1591        drop(_token_2);
1592
1593        // create the server after the client is up
1594        let server_state = server_runtime.block_on(spawn_server(tcp_listener_stream));
1595
1596        server_runtime.block_on(async {
1597            // client connects automatically once the server is up
1598            poll_until_true(CONNECT_TIMEOUT, || {
1599                server_state.active_connections().len() == 1
1600            })
1601            .await;
1602
1603            // client rehydrated its subscriptions. notably, only includes the shard that
1604            // still has an active token
1605            poll_until_true(SUBSCRIPTIONS_TIMEOUT, || {
1606                server_state.shard_subscription_counts() == HashMap::from([(*SHARD_ID_0, 1)])
1607            })
1608            .await;
1609        });
1610
1611        // kill the server
1612        server_runtime.shutdown_timeout(SERVER_SHUTDOWN_TIMEOUT);
1613
1614        // client can still send requests without error
1615        let _token_2 = Arc::clone(&client.sender).subscribe(&SHARD_ID_1);
1616
1617        // create a new server
1618        let server_runtime = tokio::runtime::Runtime::new().expect("server runtime");
1619        let tcp_listener_stream = server_runtime.block_on(async {
1620            TcpListenerStream::new(
1621                TcpListener::bind(addr)
1622                    .await
1623                    .expect("can bind to previous addr"),
1624            )
1625        });
1626        let server_state = server_runtime.block_on(spawn_server(tcp_listener_stream));
1627
1628        server_runtime.block_on(async {
1629            // client automatically reconnects to new server
1630            poll_until_true(CONNECT_TIMEOUT, || {
1631                server_state.active_connections().len() == 1
1632            })
1633            .await;
1634
1635            // and rehydrates its subscriptions, including the new one that was sent
1636            // while the server was unavailable.
1637            poll_until_true(SUBSCRIPTIONS_TIMEOUT, || {
1638                server_state.shard_subscription_counts()
1639                    == HashMap::from([(*SHARD_ID_0, 1), (*SHARD_ID_1, 1)])
1640            })
1641            .await;
1642        });
1643    }
1644
1645    #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1646    #[cfg_attr(miri, ignore)] // error: unsupported operation: can't call foreign function `socket` on OS `linux`
1647    async fn grpc_client_sender_subscription_tokens() {
1648        let metrics = Arc::new(Metrics::new(
1649            &test_persist_config(),
1650            &MetricsRegistry::new(),
1651        ));
1652
1653        let (addr, tcp_listener_stream) = new_tcp_listener().await;
1654        let server_state = spawn_server(tcp_listener_stream).await;
1655
1656        let client = GrpcPubSubClient::connect(
1657            PersistPubSubClientConfig {
1658                url: format!("http://{}", addr),
1659                caller_id: "client".to_string(),
1660                persist_cfg: test_persist_config(),
1661            },
1662            metrics,
1663        );
1664
1665        // our client connects
1666        poll_until_true(CONNECT_TIMEOUT, || {
1667            server_state.active_connections().len() == 1
1668        })
1669        .await;
1670
1671        // we can subscribe to a shard, receiving back a token
1672        let token = Arc::clone(&client.sender).subscribe(&SHARD_ID_0);
1673        poll_until_true(SUBSCRIPTIONS_TIMEOUT, || {
1674            server_state.shard_subscription_counts() == HashMap::from([(*SHARD_ID_0, 1)])
1675        })
1676        .await;
1677
1678        // dropping the token will unsubscribe our client
1679        drop(token);
1680        poll_until_true(SUBSCRIPTIONS_TIMEOUT, || {
1681            server_state.shard_subscription_counts() == HashMap::new()
1682        })
1683        .await;
1684
1685        // we can resubscribe to a shard
1686        let token = Arc::clone(&client.sender).subscribe(&SHARD_ID_0);
1687        poll_until_true(SUBSCRIPTIONS_TIMEOUT, || {
1688            server_state.shard_subscription_counts() == HashMap::from([(*SHARD_ID_0, 1)])
1689        })
1690        .await;
1691
1692        // we can subscribe many times idempotently, receiving back Arcs to the same token
1693        let token2 = Arc::clone(&client.sender).subscribe(&SHARD_ID_0);
1694        let token3 = Arc::clone(&client.sender).subscribe(&SHARD_ID_0);
1695        assert_eq!(Arc::strong_count(&token), 3);
1696        poll_until_true(SUBSCRIPTIONS_TIMEOUT, || {
1697            server_state.shard_subscription_counts() == HashMap::from([(*SHARD_ID_0, 1)])
1698        })
1699        .await;
1700
1701        // dropping all of the tokens will unsubscribe the shard
1702        drop(token);
1703        drop(token2);
1704        drop(token3);
1705        poll_until_true(SUBSCRIPTIONS_TIMEOUT, || {
1706            server_state.shard_subscription_counts() == HashMap::new()
1707        })
1708        .await;
1709
1710        // we can subscribe to many shards
1711        let _token0 = Arc::clone(&client.sender).subscribe(&SHARD_ID_0);
1712        let _token1 = Arc::clone(&client.sender).subscribe(&SHARD_ID_1);
1713        poll_until_true(SUBSCRIPTIONS_TIMEOUT, || {
1714            server_state.shard_subscription_counts()
1715                == HashMap::from([(*SHARD_ID_0, 1), (*SHARD_ID_1, 1)])
1716        })
1717        .await;
1718    }
1719
1720    #[mz_ore::test]
1721    #[cfg_attr(miri, ignore)] // error: unsupported operation: can't call foreign function `socket` on OS `linux`
1722    fn grpc_client_receiver() {
1723        let metrics = Arc::new(Metrics::new(
1724            &PersistConfig::new_for_tests(),
1725            &MetricsRegistry::new(),
1726        ));
1727        let server_runtime = tokio::runtime::Runtime::new().expect("server runtime");
1728        let client_runtime = tokio::runtime::Runtime::new().expect("client runtime");
1729        let (addr, tcp_listener_stream) = server_runtime.block_on(new_tcp_listener());
1730
1731        // create two clients, so we can test that broadcast messages are received by the other
1732        let mut client_1 = client_runtime.block_on(async {
1733            GrpcPubSubClient::connect(
1734                PersistPubSubClientConfig {
1735                    url: format!("http://{}", addr),
1736                    caller_id: "client_1".to_string(),
1737                    persist_cfg: test_persist_config(),
1738                },
1739                Arc::clone(&metrics),
1740            )
1741        });
1742        let mut client_2 = client_runtime.block_on(async {
1743            GrpcPubSubClient::connect(
1744                PersistPubSubClientConfig {
1745                    url: format!("http://{}", addr),
1746                    caller_id: "client_2".to_string(),
1747                    persist_cfg: test_persist_config(),
1748                },
1749                metrics,
1750            )
1751        });
1752
1753        // we can check our receiver output before connecting to the server.
1754        // these calls are race-y, since there's no guarantee on the time it
1755        // would take for a message to be received were one to have been sent,
1756        // but, better than nothing?
1757        assert_none!(client_1.receiver.next().now_or_never());
1758        assert_none!(client_2.receiver.next().now_or_never());
1759
1760        // start the server
1761        let server_state = server_runtime.block_on(spawn_server(tcp_listener_stream));
1762
1763        // wait until both clients are connected
1764        server_runtime.block_on(poll_until_true(CONNECT_TIMEOUT, || {
1765            server_state.active_connections().len() == 2
1766        }));
1767
1768        // no messages have been broadcast yet
1769        assert_none!(client_1.receiver.next().now_or_never());
1770        assert_none!(client_2.receiver.next().now_or_never());
1771
1772        // subscribe and send a diff
1773        let _token_client_1 = Arc::clone(&client_1.sender).subscribe(&SHARD_ID_0);
1774        let _token_client_2 = Arc::clone(&client_2.sender).subscribe(&SHARD_ID_0);
1775        server_runtime.block_on(poll_until_true(SUBSCRIPTIONS_TIMEOUT, || {
1776            server_state.shard_subscription_counts() == HashMap::from([(*SHARD_ID_0, 2)])
1777        }));
1778
1779        // the subscriber non-sender client receives the diff
1780        client_1.sender.push_diff(&SHARD_ID_0, &VERSIONED_DATA_1);
1781        assert_none!(client_1.receiver.next().now_or_never());
1782        client_runtime.block_on(async {
1783            assert_push(
1784                client_2.receiver.next().await.expect("has diff"),
1785                &SHARD_ID_0,
1786                &VERSIONED_DATA_1,
1787            )
1788        });
1789
1790        // kill the server
1791        server_runtime.shutdown_timeout(SERVER_SHUTDOWN_TIMEOUT);
1792
1793        // receivers can still be polled without error
1794        assert_none!(client_1.receiver.next().now_or_never());
1795        assert_none!(client_2.receiver.next().now_or_never());
1796
1797        // create a new server
1798        let server_runtime = tokio::runtime::Runtime::new().expect("server runtime");
1799        let tcp_listener_stream = server_runtime.block_on(async {
1800            TcpListenerStream::new(
1801                TcpListener::bind(addr)
1802                    .await
1803                    .expect("can bind to previous addr"),
1804            )
1805        });
1806        let server_state = server_runtime.block_on(spawn_server(tcp_listener_stream));
1807
1808        // client automatically reconnects to new server and rehydrates subscriptions
1809        server_runtime.block_on(async {
1810            poll_until_true(CONNECT_TIMEOUT, || {
1811                server_state.active_connections().len() == 2
1812            })
1813            .await;
1814            poll_until_true(SUBSCRIPTIONS_TIMEOUT, || {
1815                server_state.shard_subscription_counts() == HashMap::from([(*SHARD_ID_0, 2)])
1816            })
1817            .await;
1818        });
1819
1820        // pushing and receiving diffs works as expected.
1821        // this time we'll push from the other client.
1822        client_2.sender.push_diff(&SHARD_ID_0, &VERSIONED_DATA_0);
1823        client_runtime.block_on(async {
1824            assert_push(
1825                client_1.receiver.next().await.expect("has diff"),
1826                &SHARD_ID_0,
1827                &VERSIONED_DATA_0,
1828            )
1829        });
1830        assert_none!(client_2.receiver.next().now_or_never());
1831    }
1832
1833    async fn new_tcp_listener() -> (SocketAddr, TcpListenerStream) {
1834        let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0));
1835        let tcp_listener = TcpListener::bind(addr).await.expect("tcp listener");
1836
1837        (
1838            tcp_listener.local_addr().expect("bound to local address"),
1839            TcpListenerStream::new(tcp_listener),
1840        )
1841    }
1842
1843    #[allow(clippy::unused_async)]
1844    async fn spawn_server(tcp_listener_stream: TcpListenerStream) -> Arc<PubSubState> {
1845        let server = PersistGrpcPubSubServer::new(&test_persist_config(), &MetricsRegistry::new());
1846        let server_state = Arc::clone(&server.state);
1847
1848        let _server_task = mz_ore::task::spawn(|| "server".to_string(), async move {
1849            server.serve_with_stream(tcp_listener_stream).await
1850        });
1851        server_state
1852    }
1853
1854    async fn poll_until_true<F>(timeout: Duration, f: F)
1855    where
1856        F: Fn() -> bool,
1857    {
1858        let now = Instant::now();
1859        loop {
1860            if f() {
1861                return;
1862            }
1863
1864            if now.elapsed() > timeout {
1865                panic!("timed out");
1866            }
1867
1868            tokio::time::sleep(Duration::from_millis(1)).await;
1869        }
1870    }
1871
1872    fn assert_push(message: ProtoPubSubMessage, shard: &ShardId, data: &VersionedData) {
1873        let message = message.message.expect("proto contains message");
1874        match message {
1875            Message::PushDiff(x) => {
1876                assert_eq!(x.shard_id, shard.into_proto());
1877                assert_eq!(x.seqno, data.seqno.into_proto());
1878                assert_eq!(x.diff, data.data);
1879            }
1880            Message::Subscribe(_) | Message::Unsubscribe(_) => panic!("unexpected message type"),
1881        };
1882    }
1883
1884    fn test_persist_config() -> PersistConfig {
1885        let cfg = PersistConfig::new_for_tests();
1886
1887        let mut updates = ConfigUpdates::default();
1888        updates.add(&PUBSUB_CLIENT_ENABLED, true);
1889        updates.add(&PUBSUB_RECONNECT_BACKOFF, Duration::ZERO);
1890        cfg.apply_from(&updates);
1891
1892        cfg
1893    }
1894}