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#[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 #[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 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 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 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 #[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 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 #[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#[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}