Skip to main content

client/
client.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::sync::Arc;
16use std::sync::atomic::{AtomicBool, Ordering};
17use std::time::Duration;
18
19use api::v1::HealthCheckRequest;
20use api::v1::flow::flow_client::FlowClient as PbFlowClient;
21use api::v1::health_check_client::HealthCheckClient;
22use api::v1::prometheus_gateway_client::PrometheusGatewayClient;
23use arrow_flight::flight_service_client::FlightServiceClient;
24use common_grpc::channel_manager::{
25    ChannelConfig, ChannelManager, ClientTlsOption, load_client_tls_config,
26};
27use parking_lot::RwLock;
28use snafu::{OptionExt, ResultExt};
29use tonic::codec::CompressionEncoding;
30use tonic::transport::Channel;
31
32use crate::load_balance::{LoadBalance, Loadbalancer};
33use crate::{Result, error};
34
35const DEFAULT_HEALTH_CHECK_INTERVAL: Duration = Duration::from_secs(30);
36const DEFAULT_HEALTH_CHECK_TIMEOUT: Duration = Duration::from_secs(1);
37
38/// Options for a gRPC client.
39#[derive(Clone, Debug)]
40pub struct ClientOptions {
41    /// Interval for refreshing peer health. `Duration::ZERO` disables background health checks.
42    pub health_check_interval: Duration,
43    /// Timeout for checking the health of a peer.
44    pub health_check_timeout: Duration,
45}
46
47impl Default for ClientOptions {
48    fn default() -> Self {
49        Self {
50            health_check_interval: DEFAULT_HEALTH_CHECK_INTERVAL,
51            health_check_timeout: DEFAULT_HEALTH_CHECK_TIMEOUT,
52        }
53    }
54}
55
56pub struct FlightClient {
57    addr: String,
58    client: FlightServiceClient<Channel>,
59}
60
61impl FlightClient {
62    pub fn addr(&self) -> &str {
63        &self.addr
64    }
65
66    pub fn mut_inner(&mut self) -> &mut FlightServiceClient<Channel> {
67        &mut self.client
68    }
69}
70
71#[derive(Clone, Debug, Default)]
72pub struct Client {
73    inner: Arc<Inner>,
74}
75
76#[derive(Debug)]
77struct Inner {
78    query_channel_manager: ChannelManager,
79    control_channel_manager: ChannelManager,
80    peers: RwLock<Peers>,
81    load_balance: Loadbalancer,
82    health_check_interval: Duration,
83    health_check_timeout: Duration,
84    health_check_started: AtomicBool,
85}
86
87impl Default for Inner {
88    fn default() -> Self {
89        #[allow(deprecated)]
90        Self::with_manager_and_peers(ChannelManager::new(), Vec::new(), ClientOptions::default())
91    }
92}
93
94#[derive(Debug, Default, PartialEq, Eq)]
95struct PeerStates {
96    active: Vec<usize>,
97    inactive: Vec<usize>,
98}
99
100#[derive(Debug, Default)]
101struct Peers {
102    addresses: Vec<String>,
103    states: PeerStates,
104    generation: u64,
105}
106
107impl Inner {
108    #[deprecated(note = "legacy single-manager path shares its pool between lanes")]
109    fn with_manager_and_peers(
110        channel_manager: ChannelManager,
111        peers: Vec<String>,
112        options: ClientOptions,
113    ) -> Self {
114        Self::with_managers_and_peers(channel_manager.clone(), channel_manager, peers, options)
115    }
116
117    // The explicit dual-manager path keeps query and control pools independent.
118    fn with_managers_and_peers(
119        query_channel_manager: ChannelManager,
120        control_channel_manager: ChannelManager,
121        peers: Vec<String>,
122        options: ClientOptions,
123    ) -> Self {
124        let peer_count = peers.len();
125        Self {
126            query_channel_manager,
127            control_channel_manager,
128            peers: RwLock::new(Peers {
129                addresses: peers,
130                states: PeerStates {
131                    active: (0..peer_count).collect(),
132                    inactive: Vec::new(),
133                },
134                generation: 0,
135            }),
136            load_balance: Loadbalancer::default(),
137            health_check_interval: options.health_check_interval,
138            health_check_timeout: options.health_check_timeout,
139            health_check_started: AtomicBool::new(false),
140        }
141    }
142
143    fn set_peers(&self, addresses: Vec<String>) {
144        let peer_count = addresses.len();
145        let mut peers = self.peers.write();
146        peers.addresses = addresses;
147        peers.states = PeerStates {
148            active: (0..peer_count).collect(),
149            inactive: Vec::new(),
150        };
151        peers.generation = peers.generation.wrapping_add(1);
152    }
153
154    fn peer_count(&self) -> usize {
155        self.peers.read().addresses.len()
156    }
157
158    fn get_peer(&self) -> Option<String> {
159        let peers = self.peers.read();
160        let index = self
161            .load_balance
162            .get_index(&peers.states.active)
163            .or_else(|| self.load_balance.get_index(&peers.states.inactive))?;
164        Some(peers.addresses[*index].clone())
165    }
166
167    async fn refresh_peer_states(&self) {
168        let (generation, peers) = {
169            let peers = self.peers.read();
170            let addresses = peers
171                .states
172                .active
173                .iter()
174                .chain(&peers.states.inactive)
175                .map(|&index| (index, peers.addresses[index].clone()))
176                .collect::<Vec<_>>();
177            (peers.generation, addresses)
178        };
179        let health_checks = peers.into_iter().map(|(index, addr)| async move {
180            let is_active = self.check_peer_health(&addr).await;
181            (index, is_active)
182        });
183        let results = futures::future::join_all(health_checks).await;
184
185        let (active, inactive) = results.into_iter().fold(
186            (Vec::new(), Vec::new()),
187            |(mut active, mut inactive), (index, is_active)| {
188                if is_active {
189                    active.push(index);
190                } else {
191                    inactive.push(index);
192                }
193                (active, inactive)
194            },
195        );
196
197        let mut peers = self.peers.write();
198        if peers.generation == generation {
199            peers.states = PeerStates { active, inactive };
200        }
201    }
202
203    async fn check_peer_health(&self, addr: &str) -> bool {
204        let Ok(channel) = self.control_channel_manager.get(addr) else {
205            return false;
206        };
207        let mut client = HealthCheckClient::new(channel);
208        tokio::time::timeout(
209            self.health_check_timeout,
210            client.health_check(HealthCheckRequest {}),
211        )
212        .await
213        .is_ok_and(|result| result.is_ok())
214    }
215}
216
217fn random_initial_delay(max_delay: Duration) -> Duration {
218    let max_nanos = max_delay.as_nanos().min(u64::MAX as u128) as u64;
219    if max_nanos == 0 {
220        return Duration::ZERO;
221    }
222
223    Duration::from_nanos(rand::random_range(0..max_nanos))
224}
225
226impl Client {
227    /// Creates a client whose query and control lanes intentionally share the default manager.
228    #[deprecated(
229        note = "shares one manager between query and control lanes; use `with_query_and_control_managers` with independently constructed managers instead"
230    )]
231    pub fn new() -> Self {
232        Default::default()
233    }
234
235    /// Creates a client whose query and control lanes intentionally share one manager.
236    #[deprecated(
237        note = "shares one manager between query and control lanes; use `with_query_and_control_managers` with independently constructed managers instead"
238    )]
239    pub fn with_urls<U, A>(urls: A) -> Self
240    where
241        U: AsRef<str>,
242        A: AsRef<[U]>,
243    {
244        #[allow(deprecated)]
245        Self::with_urls_and_options(urls, ClientOptions::default())
246    }
247
248    /// Creates a client with URLs and custom options.
249    ///
250    /// The query and control lanes intentionally share one manager.
251    #[deprecated(
252        note = "shares one manager between query and control lanes; use `with_query_and_control_managers` instead"
253    )]
254    pub fn with_urls_and_options<U, A>(urls: A, options: ClientOptions) -> Self
255    where
256        U: AsRef<str>,
257        A: AsRef<[U]>,
258    {
259        #[allow(deprecated)]
260        Self::with_manager_and_urls_and_options(ChannelManager::new(), urls, options)
261    }
262
263    /// Creates a TLS client whose query and control lanes intentionally share one manager.
264    #[deprecated(
265        note = "shares one manager between query and control lanes; use `with_query_and_control_managers` instead"
266    )]
267    pub fn with_tls_and_urls<U, A>(urls: A, client_tls: ClientTlsOption) -> Result<Self>
268    where
269        U: AsRef<str>,
270        A: AsRef<[U]>,
271    {
272        #[allow(deprecated)]
273        Self::with_tls_and_urls_and_options(urls, client_tls, ClientOptions::default())
274    }
275
276    /// Creates a client with TLS URLs and custom options.
277    ///
278    /// The query and control lanes intentionally share one manager.
279    #[deprecated(
280        note = "shares one manager between query and control lanes; use `with_query_and_control_managers` instead"
281    )]
282    pub fn with_tls_and_urls_and_options<U, A>(
283        urls: A,
284        client_tls: ClientTlsOption,
285        options: ClientOptions,
286    ) -> Result<Self>
287    where
288        U: AsRef<str>,
289        A: AsRef<[U]>,
290    {
291        let channel_config = ChannelConfig::default().client_tls_config(client_tls.clone());
292        let tls_config =
293            load_client_tls_config(Some(client_tls)).context(error::CreateTlsChannelSnafu)?;
294        let channel_manager = ChannelManager::with_config(channel_config, tls_config);
295        #[allow(deprecated)]
296        Ok(Self::with_manager_and_urls_and_options(
297            channel_manager,
298            urls,
299            options,
300        ))
301    }
302
303    /// Creates a client with one manager shared intentionally by query and control lanes.
304    #[deprecated(
305        note = "shares one manager between query and control lanes; use `with_query_and_control_managers` with independently constructed managers instead"
306    )]
307    pub fn with_manager_and_urls<U, A>(channel_manager: ChannelManager, urls: A) -> Self
308    where
309        U: AsRef<str>,
310        A: AsRef<[U]>,
311    {
312        #[allow(deprecated)]
313        Self::with_manager_and_urls_and_options(channel_manager, urls, ClientOptions::default())
314    }
315
316    /// Creates a client with query and control lanes backed by the supplied managers.
317    ///
318    /// The lanes are isolated only when the supplied managers are independently constructed;
319    /// this constructor does not enforce that they are distinct.
320    pub fn with_query_and_control_managers<U, A>(
321        query_channel_manager: ChannelManager,
322        control_channel_manager: ChannelManager,
323        urls: A,
324    ) -> Self
325    where
326        U: AsRef<str>,
327        A: AsRef<[U]>,
328    {
329        Self::with_query_and_control_managers_and_options(
330            query_channel_manager,
331            control_channel_manager,
332            urls,
333            ClientOptions::default(),
334        )
335    }
336
337    fn with_query_and_control_managers_and_options<U, A>(
338        query_channel_manager: ChannelManager,
339        control_channel_manager: ChannelManager,
340        urls: A,
341        options: ClientOptions,
342    ) -> Self
343    where
344        U: AsRef<str>,
345        A: AsRef<[U]>,
346    {
347        let urls: Vec<String> = urls
348            .as_ref()
349            .iter()
350            .map(|peer| peer.as_ref().to_string())
351            .collect();
352        Self {
353            inner: Arc::new(Inner::with_managers_and_peers(
354                query_channel_manager,
355                control_channel_manager,
356                urls,
357                options,
358            )),
359        }
360    }
361
362    /// Creates a client with a channel manager, URLs, and custom options.
363    ///
364    /// The query and control lanes intentionally share this manager and its pool.
365    #[deprecated(
366        note = "shares this manager and its pool between query and control lanes; use `with_query_and_control_managers` instead"
367    )]
368    pub fn with_manager_and_urls_and_options<U, A>(
369        channel_manager: ChannelManager,
370        urls: A,
371        options: ClientOptions,
372    ) -> Self
373    where
374        U: AsRef<str>,
375        A: AsRef<[U]>,
376    {
377        let channel_manager_for_query = channel_manager.clone();
378        // Legacy constructors intentionally share the manager and therefore its pool.
379        Self::with_query_and_control_managers_and_options(
380            channel_manager_for_query,
381            channel_manager,
382            urls,
383            options,
384        )
385    }
386
387    pub fn start<U, A>(&self, urls: A)
388    where
389        U: AsRef<str>,
390        A: AsRef<[U]>,
391    {
392        let urls = urls
393            .as_ref()
394            .iter()
395            .map(|peer| peer.as_ref().to_string())
396            .collect();
397        self.inner.set_peers(urls);
398    }
399
400    fn trigger_health_check(&self) {
401        if self.inner.health_check_interval.is_zero() || self.inner.peer_count() <= 1 {
402            return;
403        }
404
405        if self
406            .inner
407            .health_check_started
408            .swap(true, Ordering::Relaxed)
409        {
410            return;
411        }
412
413        let inner = Arc::downgrade(&self.inner);
414        let health_check_interval = self.inner.health_check_interval;
415        common_runtime::spawn_global(async move {
416            tokio::time::sleep(random_initial_delay(health_check_interval)).await;
417            let mut interval = tokio::time::interval(health_check_interval);
418            interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
419
420            loop {
421                interval.tick().await;
422                let Some(inner) = inner.upgrade() else {
423                    return;
424                };
425                if inner.peer_count() > 1 {
426                    inner.refresh_peer_states().await;
427                }
428            }
429        });
430    }
431
432    pub fn find_channel(&self) -> Result<(String, Channel)> {
433        self.trigger_health_check();
434
435        let addr = self
436            .inner
437            .get_peer()
438            .context(error::IllegalGrpcClientStateSnafu {
439                err_msg: "No available peer found",
440            })?;
441
442        let channel = self
443            .inner
444            .control_channel_manager
445            .get(&addr)
446            .context(error::CreateChannelSnafu { addr: &addr })?;
447        Ok((addr, channel))
448    }
449
450    pub fn max_grpc_recv_message_size(&self) -> usize {
451        self.inner
452            .control_channel_manager
453            .config()
454            .max_recv_message_size
455            .as_bytes() as usize
456    }
457
458    pub fn max_grpc_send_message_size(&self) -> usize {
459        self.inner
460            .control_channel_manager
461            .config()
462            .max_send_message_size
463            .as_bytes() as usize
464    }
465
466    /// Creates a Flight client on the query lane for DoGet/distributed reads.
467    ///
468    /// This public name is retained for compatibility.
469    pub fn make_flight_client(
470        &self,
471        send_compression: bool,
472        accept_compression: bool,
473    ) -> Result<FlightClient> {
474        self.make_flight_client_with_manager(
475            &self.inner.query_channel_manager,
476            send_compression,
477            accept_compression,
478        )
479    }
480
481    pub(crate) fn make_control_flight_client(
482        &self,
483        send_compression: bool,
484        accept_compression: bool,
485    ) -> Result<FlightClient> {
486        self.make_flight_client_with_manager(
487            &self.inner.control_channel_manager,
488            send_compression,
489            accept_compression,
490        )
491    }
492
493    fn make_flight_client_with_manager(
494        &self,
495        channel_manager: &ChannelManager,
496        send_compression: bool,
497        accept_compression: bool,
498    ) -> Result<FlightClient> {
499        self.trigger_health_check();
500        let addr = self
501            .inner
502            .get_peer()
503            .context(error::IllegalGrpcClientStateSnafu {
504                err_msg: "No available peer found",
505            })?;
506        let channel = channel_manager
507            .get(&addr)
508            .context(error::CreateChannelSnafu { addr: &addr })?;
509
510        let mut client = FlightServiceClient::new(channel)
511            .max_decoding_message_size(
512                channel_manager.config().max_recv_message_size.as_bytes() as usize
513            )
514            .max_encoding_message_size(
515                channel_manager.config().max_send_message_size.as_bytes() as usize
516            );
517        // todo(hl): support compression methods.
518        if send_compression {
519            client = client.send_compressed(CompressionEncoding::Zstd);
520        }
521        if accept_compression {
522            client = client.accept_compressed(CompressionEncoding::Zstd);
523        }
524
525        Ok(FlightClient { addr, client })
526    }
527
528    /// Creates the region gRPC transport with the configured message limits.
529    pub(crate) fn raw_region_client(&self) -> Result<(String, tonic::client::Grpc<Channel>)> {
530        let (addr, channel) = self.find_channel()?;
531        let client = tonic::client::Grpc::new(channel)
532            .max_decoding_message_size(self.max_grpc_recv_message_size())
533            .max_encoding_message_size(self.max_grpc_send_message_size());
534        Ok((addr, client))
535    }
536
537    pub(crate) fn raw_flow_client(&self) -> Result<(String, PbFlowClient<Channel>)> {
538        let (addr, channel) = self.find_channel()?;
539        let client = PbFlowClient::new(channel)
540            .max_decoding_message_size(
541                self.inner
542                    .control_channel_manager
543                    .config()
544                    .max_recv_message_size
545                    .as_bytes() as usize,
546            )
547            .max_encoding_message_size(
548                self.inner
549                    .control_channel_manager
550                    .config()
551                    .max_send_message_size
552                    .as_bytes() as usize,
553            )
554            .accept_compressed(CompressionEncoding::Zstd)
555            .send_compressed(CompressionEncoding::Zstd);
556        Ok((addr, client))
557    }
558
559    pub fn make_prometheus_gateway_client(&self) -> Result<PrometheusGatewayClient<Channel>> {
560        let (_, channel) = self.find_channel()?;
561        let client = PrometheusGatewayClient::new(channel)
562            .accept_compressed(CompressionEncoding::Gzip)
563            .accept_compressed(CompressionEncoding::Zstd)
564            .send_compressed(CompressionEncoding::Gzip)
565            .send_compressed(CompressionEncoding::Zstd);
566        Ok(client)
567    }
568
569    pub async fn health_check(&self) -> Result<()> {
570        let (_, channel) = self.find_channel()?;
571        let mut client = HealthCheckClient::new(channel);
572        let _ = client.health_check(HealthCheckRequest {}).await?;
573        Ok(())
574    }
575
576    /// Returns the number of cached channels in the query and control pools for tests.
577    #[cfg(feature = "testing")]
578    pub fn channel_pool_sizes(&self) -> (usize, usize) {
579        let pool_size = |channel_manager: &ChannelManager| {
580            let mut size = 0;
581            channel_manager.retain_channel(|_, _| {
582                size += 1;
583                true
584            });
585            size
586        };
587        (
588            pool_size(&self.inner.query_channel_manager),
589            pool_size(&self.inner.control_channel_manager),
590        )
591    }
592
593    /// Returns peer addresses grouped by active and inactive state for tests.
594    #[cfg(feature = "testing")]
595    pub fn peer_addresses_by_state(&self) -> (Vec<String>, Vec<String>) {
596        let peers = self.inner.peers.read();
597        let addresses = |indices: &[usize]| {
598            indices
599                .iter()
600                .map(|&index| peers.addresses[index].clone())
601                .collect()
602        };
603        (
604            addresses(&peers.states.active),
605            addresses(&peers.states.inactive),
606        )
607    }
608}
609
610#[cfg(test)]
611#[allow(deprecated)]
612mod tests {
613    use std::collections::HashSet;
614    use std::sync::Arc;
615    use std::sync::atomic::Ordering;
616    use std::time::Duration;
617
618    use api::v1::health_check_server::{HealthCheck, HealthCheckServer};
619    use api::v1::{HealthCheckRequest, HealthCheckResponse};
620    use common_grpc::channel_manager::ChannelManager;
621    use tokio::net::TcpListener;
622    use tokio::sync::Notify;
623    use tokio::task::JoinHandle;
624    use tokio::time::{interval, timeout};
625    use tokio_stream::wrappers::TcpListenerStream;
626    use tonic::{Request, Response, Status};
627
628    use super::{Client, ClientOptions, Inner, PeerStates};
629    use crate::load_balance::Loadbalancer;
630
631    const HEALTH_REFRESH_INTERVAL: Duration = Duration::from_millis(10);
632    const STATE_REFRESH_TIMEOUT: Duration = Duration::from_secs(1);
633
634    struct HealthyHealthCheck;
635
636    #[tonic::async_trait]
637    impl HealthCheck for HealthyHealthCheck {
638        async fn health_check(
639            &self,
640            _request: Request<HealthCheckRequest>,
641        ) -> Result<Response<HealthCheckResponse>, Status> {
642            Ok(Response::new(HealthCheckResponse {}))
643        }
644    }
645
646    struct UnhealthyHealthCheck;
647
648    #[tonic::async_trait]
649    impl HealthCheck for UnhealthyHealthCheck {
650        async fn health_check(
651            &self,
652            _request: Request<HealthCheckRequest>,
653        ) -> Result<Response<HealthCheckResponse>, Status> {
654            Err(Status::unavailable("peer is unavailable"))
655        }
656    }
657
658    struct PendingHealthCheck {
659        started: Option<Arc<Notify>>,
660    }
661
662    #[tonic::async_trait]
663    impl HealthCheck for PendingHealthCheck {
664        async fn health_check(
665            &self,
666            _request: Request<HealthCheckRequest>,
667        ) -> Result<Response<HealthCheckResponse>, Status> {
668            if let Some(started) = &self.started {
669                started.notify_one();
670            }
671            std::future::pending().await
672        }
673    }
674
675    async fn start_health_check_server<T>(handler: T) -> (String, JoinHandle<()>)
676    where
677        T: HealthCheck + Send + Sync + 'static,
678    {
679        let listener = TcpListener::bind("127.0.0.1:0")
680            .await
681            .expect("bind health check server");
682        let addr = listener
683            .local_addr()
684            .expect("read health check server address")
685            .to_string();
686        let server = tokio::spawn(async move {
687            tonic::transport::Server::builder()
688                .add_service(HealthCheckServer::new(handler))
689                .serve_with_incoming(TcpListenerStream::new(listener))
690                .await
691                .expect("serve health check server");
692        });
693
694        (addr, server)
695    }
696
697    async fn wait_for_peer_states(client: &Client, expected: PeerStates) {
698        let mut poll = interval(HEALTH_REFRESH_INTERVAL);
699        timeout(STATE_REFRESH_TIMEOUT, async {
700            loop {
701                poll.tick().await;
702                if client.inner.peers.read().states == expected {
703                    return;
704                }
705            }
706        })
707        .await
708        .expect("health refresh did not reach expected peer states");
709    }
710
711    fn mock_peers() -> Vec<String> {
712        vec![
713            "127.0.0.1:3001".to_string(),
714            "127.0.0.1:3002".to_string(),
715            "127.0.0.1:3003".to_string(),
716        ]
717    }
718
719    #[tokio::test]
720    async fn test_explicit_dual_manager_constructor_uses_isolated_reused_channel_pools() {
721        let query_channel_manager = ChannelManager::new();
722        let control_channel_manager = ChannelManager::new();
723        let client = Client::with_query_and_control_managers(
724            query_channel_manager.clone(),
725            control_channel_manager.clone(),
726            ["127.0.0.1:3001"],
727        );
728
729        client.make_flight_client(false, false).unwrap();
730        client.make_flight_client(false, false).unwrap();
731        assert_eq!(1, channel_pool_size(&query_channel_manager));
732        assert_eq!(0, channel_pool_size(&control_channel_manager));
733
734        client.make_control_flight_client(false, false).unwrap();
735        client.make_control_flight_client(false, false).unwrap();
736        assert_eq!(1, channel_pool_size(&query_channel_manager));
737        assert_eq!(1, channel_pool_size(&control_channel_manager));
738    }
739
740    fn channel_pool_size(manager: &ChannelManager) -> usize {
741        let mut size = 0;
742        manager.retain_channel(|_, _| {
743            size += 1;
744            true
745        });
746        size
747    }
748
749    #[test]
750    fn test_inner() {
751        let inner = Inner::default();
752
753        assert!(matches!(
754            inner.load_balance,
755            Loadbalancer::Random(crate::load_balance::Random)
756        ));
757        assert!(inner.get_peer().is_none());
758
759        let peers = mock_peers();
760        let all: HashSet<String> = peers.iter().cloned().collect();
761        let inner =
762            Inner::with_manager_and_peers(ChannelManager::new(), peers, ClientOptions::default());
763
764        for _ in 0..20 {
765            assert!(all.contains(&inner.get_peer().unwrap()));
766        }
767    }
768
769    #[test]
770    fn test_inner_prefers_active_peer() {
771        let peers = mock_peers();
772        let inner = Inner::with_manager_and_peers(
773            ChannelManager::new(),
774            peers.clone(),
775            ClientOptions::default(),
776        );
777        inner.peers.write().states = PeerStates {
778            active: vec![0],
779            inactive: vec![1, 2],
780        };
781
782        assert_eq!(Some(peers[0].clone()), inner.get_peer());
783    }
784
785    #[test]
786    fn test_zero_health_check_interval_disables_background_task() {
787        let client = Client::with_urls_and_options(
788            mock_peers(),
789            ClientOptions {
790                health_check_interval: Duration::ZERO,
791                ..Default::default()
792            },
793        );
794
795        assert!(!client.inner.health_check_started.load(Ordering::Relaxed));
796        let peers = client.inner.peers.read();
797        assert_eq!(mock_peers(), peers.addresses);
798        assert_eq!(vec![0, 1, 2], peers.states.active);
799        assert!(peers.states.inactive.is_empty());
800    }
801
802    #[test]
803    fn test_multi_peer_constructor_defers_background_task() {
804        let client = Client::with_urls(mock_peers());
805
806        assert!(!client.inner.health_check_started.load(Ordering::Relaxed));
807    }
808
809    #[tokio::test]
810    async fn test_single_peer_does_not_start_background_task() {
811        let client = Client::with_urls(["127.0.0.1:3001"]);
812
813        client.find_channel().unwrap();
814
815        assert!(!client.inner.health_check_started.load(Ordering::Relaxed));
816    }
817
818    #[test]
819    fn test_start_initializes_new_client_without_starting_background_task() {
820        let client = Client::new();
821        let peers = mock_peers();
822
823        client.start(peers.clone());
824
825        assert!(peers.contains(&client.inner.get_peer().unwrap()));
826        assert!(!client.inner.health_check_started.load(Ordering::Relaxed));
827    }
828
829    #[tokio::test]
830    async fn test_health_refresh_marks_unhealthy_peer_inactive_and_selects_healthy_peer() {
831        // Arrange: one peer responds to health checks and the other rejects them.
832        let (healthy_addr, healthy_server) = start_health_check_server(HealthyHealthCheck).await;
833        let (unhealthy_addr, unhealthy_server) =
834            start_health_check_server(UnhealthyHealthCheck).await;
835        let client = Client::with_urls_and_options(
836            [healthy_addr.clone(), unhealthy_addr],
837            ClientOptions {
838                health_check_interval: HEALTH_REFRESH_INTERVAL,
839                ..Default::default()
840            },
841        );
842        assert!(!client.inner.health_check_started.load(Ordering::Relaxed));
843
844        // Act: trigger lazy health checks, then poll until the background refresh completes.
845        client.find_channel().unwrap();
846        assert!(client.inner.health_check_started.load(Ordering::Relaxed));
847        wait_for_peer_states(
848            &client,
849            PeerStates {
850                active: vec![0],
851                inactive: vec![1],
852            },
853        )
854        .await;
855
856        // Assert: an inactive peer does not prevent selection of its active peer.
857        assert_eq!(Some(healthy_addr), client.inner.get_peer());
858
859        healthy_server.abort();
860        unhealthy_server.abort();
861    }
862
863    #[tokio::test]
864    async fn test_health_refresh_times_out_pending_peer() {
865        let (healthy_addr, healthy_server) = start_health_check_server(HealthyHealthCheck).await;
866        let (pending_addr, pending_server) =
867            start_health_check_server(PendingHealthCheck { started: None }).await;
868        let client = Client::with_urls_and_options(
869            [healthy_addr, pending_addr],
870            ClientOptions {
871                health_check_interval: HEALTH_REFRESH_INTERVAL,
872                health_check_timeout: Duration::from_millis(20),
873            },
874        );
875
876        client.find_channel().unwrap();
877        wait_for_peer_states(
878            &client,
879            PeerStates {
880                active: vec![0],
881                inactive: vec![1],
882            },
883        )
884        .await;
885
886        healthy_server.abort();
887        pending_server.abort();
888    }
889
890    #[tokio::test]
891    async fn test_peer_update_ignores_in_flight_health_result() {
892        let started = Arc::new(Notify::new());
893        let (pending_addr, pending_server) = start_health_check_server(PendingHealthCheck {
894            started: Some(started.clone()),
895        })
896        .await;
897        let inner = Arc::new(Inner::with_manager_and_peers(
898            ChannelManager::new(),
899            vec![pending_addr],
900            ClientOptions {
901                health_check_timeout: Duration::from_millis(20),
902                ..Default::default()
903            },
904        ));
905        let refresh_inner = inner.clone();
906        let refresh = tokio::spawn(async move {
907            refresh_inner.refresh_peer_states().await;
908        });
909        timeout(STATE_REFRESH_TIMEOUT, started.notified())
910            .await
911            .expect("pending health check did not start");
912
913        inner.set_peers(vec!["127.0.0.1:3001".to_string()]);
914        refresh.await.unwrap();
915
916        let peers = inner.peers.read();
917        assert_eq!(vec!["127.0.0.1:3001"], peers.addresses);
918        assert_eq!(vec![0], peers.states.active);
919        assert!(peers.states.inactive.is_empty());
920        pending_server.abort();
921    }
922}