Skip to main content

pulsar/
service_discovery.rs

1use std::sync::Arc;
2
3use futures::{future::try_join_all, FutureExt};
4use url::Url;
5
6use crate::{
7    connection_manager::{BrokerAddress, ConnectionManager},
8    error::{ConnectionError, ServiceDiscoveryError},
9    executor::Executor,
10    message::proto::{
11        command_lookup_topic_response, command_partitioned_topic_metadata_response,
12        CommandLookupTopicResponse,
13    },
14};
15
16/// Look up broker addresses for topics and partitioned topics
17///
18/// The ServiceDiscovery object provides a single interface to start
19/// interacting with a cluster. It will automatically follow redirects
20/// or use a proxy, and aggregate broker connections
21#[derive(Clone)]
22pub struct ServiceDiscovery<Exe: Executor> {
23    manager: Arc<ConnectionManager<Exe>>,
24}
25
26impl<Exe: Executor> ServiceDiscovery<Exe> {
27    #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
28    pub fn with_manager(manager: Arc<ConnectionManager<Exe>>) -> Self {
29        ServiceDiscovery { manager }
30    }
31
32    /// get the broker address for a topic
33    #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
34    pub async fn lookup_topic<S: Into<String>>(
35        &self,
36        topic: S,
37    ) -> Result<BrokerAddress, ServiceDiscoveryError> {
38        let topic = topic.into();
39        let mut proxied_query = false;
40        let mut conn = self.manager.get_base_connection().await?;
41        let base_url = self.manager.url.clone();
42        let mut is_authoritative = false;
43        let mut broker_address = self.manager.get_base_address();
44
45        let mut current_retries = 0u32;
46        let start = std::time::Instant::now();
47        let operation_retry_options = self.manager.operation_retry_options.clone();
48
49        loop {
50            let response = match conn
51                .sender()
52                .lookup_topic(topic.to_string(), is_authoritative)
53                .await
54            {
55                Ok(res) => res,
56                Err(ConnectionError::Disconnected) => {
57                    error!("tried to lookup a topic but connection was closed, reconnecting...");
58                    conn = self.manager.get_connection(&broker_address).await?;
59                    conn.sender()
60                        .lookup_topic(topic.to_string(), is_authoritative)
61                        .await?
62                }
63                Err(e) => {
64                    error!("tried to lookup a topic but error occrured: {:?}", e);
65                    return Err(e.into());
66                }
67            };
68
69            if response.response.is_none()
70                || response.response
71                    == Some(command_lookup_topic_response::LookupType::Failed as i32)
72            {
73                let error = response.error.and_then(crate::error::server_error);
74                if matches!(
75                    error,
76                    Some(
77                        crate::message::proto::ServerError::ServiceNotReady
78                            | crate::message::proto::ServerError::MetadataError,
79                    )
80                ) {
81                    if operation_retry_options.max_retries.is_none()
82                        || operation_retry_options.max_retries.unwrap() > current_retries
83                    {
84                        error!("lookup({}) failed with {:?}, retrying request after {}ms (max_retries = {:?})", topic, error, operation_retry_options.retry_delay.as_millis(), operation_retry_options.max_retries);
85                        current_retries += 1;
86                        self.manager
87                            .executor
88                            .delay(operation_retry_options.retry_delay)
89                            .await;
90                        continue;
91                    } else {
92                        error!("lookup({}) reached max retries", topic);
93                    }
94                }
95
96                error!(
97                    "tried to lookup a topic but error occured[{:?}]: {:?}",
98                    line!(),
99                    error
100                );
101                return Err(ServiceDiscoveryError::Query(
102                    error,
103                    response.message.clone(),
104                ));
105            }
106
107            if current_retries > 0 {
108                let dur = (std::time::Instant::now() - start).as_secs();
109                log::info!(
110                    "lookup({}) success after {} retries over {} seconds",
111                    topic,
112                    current_retries + 1,
113                    dur
114                );
115            }
116            let LookupResponse {
117                broker_url,
118                broker_url_tls,
119                proxy,
120                redirect,
121                authoritative,
122            } = convert_lookup_response(&response)?;
123            is_authoritative = authoritative;
124
125            // Use broker url with the same schema of url in setting
126            let (broker_url_maybe_none, broker_port) = match base_url.scheme() {
127                "pulsar+ssl" => (&broker_url_tls, 6651),
128                "pulsar" => (&broker_url, 6650),
129                other => {
130                    error!("invalid scheme: {}", other);
131                    return Err(ServiceDiscoveryError::NotFound);
132                }
133            };
134
135            let (connection_url, broker_url) = if let Some(u) = broker_url_maybe_none {
136                (
137                    u.clone(),
138                    format!(
139                        "{}:{}",
140                        u.host_str().unwrap(),
141                        u.port().unwrap_or(broker_port)
142                    ),
143                )
144            } else {
145                return Err(ServiceDiscoveryError::NotFound);
146            };
147
148            // if going through a proxy, we use the base URL
149            let url = if proxied_query || proxy {
150                base_url.clone()
151            } else {
152                connection_url.clone()
153            };
154
155            broker_address = BrokerAddress {
156                url,
157                broker_url,
158                proxy: proxied_query || proxy,
159            };
160
161            // if the response indicated a redirect, do another query
162            // to the target broker
163            if redirect {
164                conn = self.manager.get_connection(&broker_address).await?;
165                proxied_query = broker_address.proxy;
166                continue;
167            } else {
168                let res = self
169                    .manager
170                    .get_connection(&broker_address)
171                    .await
172                    .map(|_| broker_address)
173                    .map_err(ServiceDiscoveryError::Connection);
174                break res;
175            }
176        }
177    }
178
179    /// get the number of partitions for a partitioned topic
180    #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
181    pub async fn lookup_partitioned_topic_number<S: Into<String>>(
182        &self,
183        topic: S,
184    ) -> Result<u32, ServiceDiscoveryError> {
185        let topic = topic.into();
186
187        // This is a fast path to reduce the number of lookup requests for topic partition metadata
188        // and as a side effect reduce amplification on zookeeper.
189        // For example, for a topic with 12 partitions, before this patch we did 13 lookup requests
190        // and now, we only did once.
191        // There are some cases where we ask if a partition of a topic is partitioned which could
192        // not be the case. We are able to detect those requests as the topic name is ending
193        // with '...-partition-<number>'. So, to be effective and avoid a regex here, we use
194        // the `contains` method to detect the pattern '-partition-'. if it matches the
195        // pattern as there is no partition in a partitioned topic, we could safely return that the
196        // partition number is 0, that implicitly say that there is only 1 topic and the index is 0.
197        if topic.contains("-partition-") {
198            return Ok(0);
199        }
200
201        let mut connection = self.manager.get_base_connection().await?;
202        let mut current_retries = 0u32;
203        let start = std::time::Instant::now();
204        let operation_retry_options = self.manager.operation_retry_options.clone();
205
206        let response = loop {
207            let response = match connection.sender().lookup_partitioned_topic(&topic).await {
208                Ok(res) => res,
209                Err(ConnectionError::Disconnected) => {
210                    error!("tried to lookup a topic but connection was closed, reconnecting...");
211                    connection = self.manager.get_base_connection().await?;
212                    connection.sender().lookup_partitioned_topic(&topic).await?
213                }
214                Err(e) => return Err(e.into()),
215            };
216
217            if response.response.is_none()
218                || response.response
219                    == Some(command_partitioned_topic_metadata_response::LookupType::Failed as i32)
220            {
221                let error = response.error.and_then(crate::error::server_error);
222                if error == Some(crate::message::proto::ServerError::ServiceNotReady) {
223                    if operation_retry_options.max_retries.is_none()
224                        || operation_retry_options.max_retries.unwrap() > current_retries
225                    {
226                        error!("lookup_partitioned_topic_number({}) answered ServiceNotReady, retrying request after {}ms (max_retries = {:?})",
227                    topic, operation_retry_options.retry_delay.as_millis(),
228                    operation_retry_options.max_retries);
229
230                        current_retries += 1;
231                        self.manager
232                            .executor
233                            .delay(operation_retry_options.retry_delay)
234                            .await;
235                        continue;
236                    } else {
237                        error!(
238                            "lookup_partitioned_topic_number({}) reached max retries",
239                            topic
240                        );
241                    }
242                }
243                return Err(ServiceDiscoveryError::Query(
244                    error,
245                    response.message.clone(),
246                ));
247            }
248
249            break response;
250        };
251
252        if current_retries > 0 {
253            let dur = (std::time::Instant::now() - start).as_secs();
254            log::info!(
255                "lookup_partitioned_topic_number({}) success after {} retries over {} seconds",
256                topic,
257                current_retries + 1,
258                dur
259            );
260        }
261
262        match response.partitions {
263            Some(partitions) => Ok(partitions),
264            None => Err(ServiceDiscoveryError::Query(
265                response.error.and_then(crate::error::server_error),
266                response.message,
267            )),
268        }
269    }
270
271    /// Lookup a topic, returning a list of the partitions (if partitioned) and addresses
272    /// associated with that topic.
273    #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
274    pub async fn lookup_partitioned_topic<S: Into<String>>(
275        &self,
276        topic: S,
277    ) -> Result<Vec<(String, BrokerAddress)>, ServiceDiscoveryError> {
278        let topic = topic.into();
279        let partitions = self.lookup_partitioned_topic_number(&topic).await?;
280
281        trace!("Partitions for topic {}: {}", &topic, &partitions);
282        let topics = match partitions {
283            0 => vec![topic],
284            _ => (0..partitions)
285                .map(|n| format!("{}-partition-{}", &topic, n))
286                .collect(),
287        };
288        try_join_all(topics.into_iter().map(|topic| {
289            self.lookup_topic(topic.clone())
290                .map(move |address_res| match address_res {
291                    Err(e) => Err(e),
292                    Ok(address) => Ok((topic, address)),
293                })
294        }))
295        .await
296    }
297}
298
299struct LookupResponse {
300    pub broker_url: Option<Url>,
301    pub broker_url_tls: Option<Url>,
302    pub proxy: bool,
303    pub redirect: bool,
304    pub authoritative: bool,
305}
306
307/// extracts information from a lookup response
308#[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
309fn convert_lookup_response(
310    response: &CommandLookupTopicResponse,
311) -> Result<LookupResponse, ServiceDiscoveryError> {
312    let proxy = response.proxy_through_service_url.unwrap_or(false);
313    let authoritative = response.authoritative.unwrap_or(false);
314    let redirect =
315        response.response == Some(command_lookup_topic_response::LookupType::Redirect as i32);
316
317    let broker_url = match response.broker_service_url.as_ref() {
318        Some(_u) => Some(
319            Url::parse(&response.broker_service_url.clone().unwrap()).map_err(|e| {
320                error!("error parsing URL: {:?}", e);
321                ServiceDiscoveryError::NotFound
322            })?,
323        ),
324        None => None,
325    };
326
327    let broker_url_tls = match response.broker_service_url_tls.as_ref() {
328        Some(u) => Some(Url::parse(u).map_err(|e| {
329            error!("error parsing URL: {:?}", e);
330            ServiceDiscoveryError::NotFound
331        })?),
332        None => None,
333    };
334
335    Ok(LookupResponse {
336        broker_url,
337        broker_url_tls,
338        proxy,
339        redirect,
340        authoritative,
341    })
342}