chainlink_data_streams_sdk/
stream.rs1mod establish_connection;
2mod monitor_connection;
3
4use establish_connection::connect;
5use monitor_connection::run_stream;
6
7use crate::auth::generate_auth_headers;
8use crate::config::{Config, WebSocketHighAvailability};
9use crate::endpoints::get_cll_avail_origins_header;
10
11use chainlink_data_streams_report::feed_id::ID;
12use chainlink_data_streams_report::report::Report;
13
14use reqwest::Client as HttpClient;
15use serde::{Deserialize, Serialize};
16use std::{
17 collections::HashMap,
18 sync::{
19 atomic::{AtomicUsize, Ordering},
20 Arc,
21 },
22 time::{SystemTime, UNIX_EPOCH},
23};
24use tokio::{
25 net::TcpStream,
26 sync::{broadcast, mpsc, Mutex},
27 time::{sleep, Duration},
28};
29use tokio_tungstenite::{MaybeTlsStream, WebSocketStream as TungsteniteWebSocketStream};
30use tracing::{debug, error, info, warn};
31
32pub const DEFAULT_WS_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
33pub const MIN_WS_RECONNECT_INTERVAL: Duration = Duration::from_millis(1000);
34pub const MAX_WS_RECONNECT_INTERVAL: Duration = Duration::from_millis(10000);
35
36#[derive(Debug, thiserror::Error)]
37pub enum StreamError {
38 #[error("WebSocket error: {0}")]
39 WebSocketError(#[from] tokio_tungstenite::tungstenite::Error),
40
41 #[error("Connection error: {0}")]
42 ConnectionError(String),
43
44 #[error("Authentication error: {0}")]
45 AuthError(#[from] crate::auth::HmacError),
46
47 #[error("Serialization error: {0}")]
48 SerializationError(#[from] serde_json::Error),
49
50 #[error("Stream closed")]
51 StreamClosed,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55pub struct WebSocketReport {
56 pub report: Report,
57}
58
59struct Stats {
60 accepted: AtomicUsize,
62 deduplicated: AtomicUsize,
64 partial_reconnects: AtomicUsize,
66 full_reconnects: AtomicUsize,
68 configured_connections: AtomicUsize,
70 active_connections: AtomicUsize,
72}
73
74#[derive(Debug)]
75pub enum WebSocketConnection {
76 Single(TungsteniteWebSocketStream<MaybeTlsStream<TcpStream>>),
77 Multiple(Vec<(TungsteniteWebSocketStream<MaybeTlsStream<TcpStream>>, String)>),
78}
79
80pub struct Stream {
85 config: Config,
86 feed_ids: Vec<ID>,
87 conn: Option<WebSocketConnection>,
88 report_sender: mpsc::Sender<WebSocketReport>,
89 report_receiver: mpsc::Receiver<WebSocketReport>,
90 shutdown_sender: broadcast::Sender<()>,
91 stats: Arc<Stats>,
92 water_mark: Arc<Mutex<HashMap<String, usize>>>,
93}
94
95impl Stream {
96 pub async fn new(config: &Config, feed_ids: Vec<ID>) -> Result<Stream, StreamError> {
136 let (report_sender, report_receiver) = mpsc::channel(100);
137 let (shutdown_sender, _) = broadcast::channel(1);
138
139 let stats = Arc::new(Stats {
140 accepted: AtomicUsize::new(0),
141 deduplicated: AtomicUsize::new(0),
142 partial_reconnects: AtomicUsize::new(0),
143 full_reconnects: AtomicUsize::new(0),
144 configured_connections: AtomicUsize::new(0),
145 active_connections: AtomicUsize::new(0),
146 });
147
148 let origins: Vec<String> = if config.ws_ha == WebSocketHighAvailability::Enabled {
149 match fetch_ha_origins(config).await {
150 Ok(o) if !o.is_empty() => {
151 info!("HA mode: discovered {} origins", o.len());
152 o
153 }
154 Ok(_) => {
155 warn!("HA mode: no origins returned from HEAD request, degrading to single connection");
156 vec![]
157 }
158 Err(e) => {
159 warn!("HA mode: origin discovery failed ({}), degrading to single connection", e);
160 vec![]
161 }
162 }
163 } else {
164 vec![]
165 };
166
167 let conn = connect(config, &origins, &feed_ids, stats.clone()).await?;
168
169 let water_mark = Arc::new(Mutex::new(HashMap::new()));
170
171 Ok(Stream {
172 config: config.clone(),
173 feed_ids,
174 conn: Some(conn),
175 report_sender,
176 report_receiver,
177 shutdown_sender,
178 stats,
179 water_mark,
180 })
181 }
182
183 pub async fn listen(&mut self) -> Result<(), StreamError> {
186 let conn = self
187 .conn
188 .take()
189 .ok_or_else(|| StreamError::ConnectionError("No connection".into()))?;
190
191 match conn {
192 WebSocketConnection::Single(stream) => {
193 let report_sender = self.report_sender.clone();
194 let shutdown_receiver = self.shutdown_sender.subscribe();
195 let stats = self.stats.clone();
196 let water_mark = self.water_mark.clone();
197 let config = self.config.clone();
198 let feed_ids = self.feed_ids.clone();
199
200 tokio::spawn(run_stream(
201 stream,
202 String::new(), report_sender,
204 shutdown_receiver,
205 stats,
206 water_mark,
207 config,
208 feed_ids,
209 ));
210 }
211 WebSocketConnection::Multiple(streams) => {
212 for (stream, origin) in streams {
213 let report_sender = self.report_sender.clone();
214 let shutdown_receiver = self.shutdown_sender.subscribe();
215 let stats = self.stats.clone();
216 let water_mark = self.water_mark.clone();
217 let config = self.config.clone();
218 let feed_ids = self.feed_ids.clone();
219
220 tokio::spawn(run_stream(
221 stream,
222 origin,
223 report_sender,
224 shutdown_receiver,
225 stats,
226 water_mark,
227 config,
228 feed_ids,
229 ));
230 }
231 }
232 }
233
234 Ok(())
235 }
236
237 pub async fn read(&mut self) -> Result<WebSocketReport, StreamError> {
244 self.report_receiver
245 .recv()
246 .await
247 .ok_or(StreamError::StreamClosed)
248 }
249
250 pub async fn close(&mut self) -> Result<(), StreamError> {
253 info!("Closing stream...");
254
255 if let Err(e) = self.shutdown_sender.send(()) {
257 debug!("Shutdown signal not sent (no active receivers). Stream may already be closed. Error received: {:?}", e);
258 }
259
260 sleep(Duration::from_millis(100)).await;
262
263 Ok(())
264 }
265
266 pub fn get_stats(&self) -> StatsSnapshot {
279 let accepted = self.stats.accepted.load(Ordering::SeqCst);
280 let deduplicated = self.stats.deduplicated.load(Ordering::SeqCst);
281
282 StatsSnapshot {
283 accepted,
284 deduplicated,
285 total_received: accepted + deduplicated,
286 partial_reconnects: self.stats.partial_reconnects.load(Ordering::SeqCst),
287 full_reconnects: self.stats.full_reconnects.load(Ordering::SeqCst),
288 configured_connections: self.stats.configured_connections.load(Ordering::SeqCst),
289 active_connections: self.stats.active_connections.load(Ordering::SeqCst),
290 }
291 }
292}
293
294#[derive(Debug, Clone)]
296pub struct StatsSnapshot {
297 pub accepted: usize,
299 pub deduplicated: usize,
301 pub total_received: usize,
303 pub partial_reconnects: usize,
305 pub full_reconnects: usize,
307 pub configured_connections: usize,
309 pub active_connections: usize,
311}
312
313fn parse_origins_from_header(header_value: &str) -> Vec<String> {
314 let inner = header_value
315 .strip_prefix('{')
316 .and_then(|s| s.strip_suffix('}'))
317 .unwrap_or(header_value);
318 if inner.is_empty() {
319 return vec![];
320 }
321 inner
322 .split(',')
323 .map(|s| s.trim().to_string())
324 .filter(|s| !s.is_empty())
325 .collect()
326}
327
328fn convert_ws_to_http_scheme(ws_url: &str) -> String {
329 if let Some(rest) = ws_url.strip_prefix("wss://") {
330 format!("https://{}", rest)
331 } else if let Some(rest) = ws_url.strip_prefix("ws://") {
332 format!("http://{}", rest)
333 } else {
334 ws_url.to_string()
335 }
336}
337
338async fn fetch_ha_origins(config: &Config) -> Result<Vec<String>, StreamError> {
339 let http = HttpClient::builder()
340 .danger_accept_invalid_certs(config.insecure_skip_verify.to_bool())
341 .build()
342 .map_err(|e| StreamError::ConnectionError(e.to_string()))?;
343
344 let http_url = {
347 let mut u = reqwest::Url::parse(&convert_ws_to_http_scheme(&config.ws_url))
348 .map_err(|e| StreamError::ConnectionError(format!("Invalid ws_url: {}", e)))?;
349 u.set_path("/");
350 u.set_query(None);
351 u.to_string()
352 };
353
354 let request_timestamp = SystemTime::now()
355 .duration_since(UNIX_EPOCH)
356 .expect("System time error")
357 .as_millis();
358
359 let auth_headers = generate_auth_headers(
360 "HEAD",
361 "/",
362 b"",
363 &config.api_key,
364 &config.api_secret,
365 request_timestamp,
366 )?;
367
368 let response = http
369 .head(&http_url)
370 .headers(auth_headers)
371 .send()
372 .await
373 .map_err(|e| StreamError::ConnectionError(format!("HA origin discovery request failed: {}", e)))?;
374
375 if !response.status().is_success() {
376 return Err(StreamError::ConnectionError(format!(
377 "HA origin discovery HEAD request returned status {}",
378 response.status()
379 )));
380 }
381
382 let header_value = response
383 .headers()
384 .get(get_cll_avail_origins_header())
385 .and_then(|v| v.to_str().ok())
386 .unwrap_or("")
387 .to_string();
388
389 Ok(parse_origins_from_header(&header_value))
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395
396 #[test]
397 fn test_parse_origins_from_header_empty() {
398 assert_eq!(parse_origins_from_header(""), Vec::<String>::new());
399 }
400
401 #[test]
402 fn test_parse_origins_from_header_with_braces() {
403 let result = parse_origins_from_header("{001,002}");
404 assert_eq!(result, vec!["001".to_string(), "002".to_string()]);
405 }
406
407 #[test]
408 fn test_parse_origins_from_header_without_braces() {
409 let result = parse_origins_from_header("001,002");
410 assert_eq!(result, vec!["001".to_string(), "002".to_string()]);
411 }
412
413 #[test]
414 fn test_parse_origins_from_header_single_origin() {
415 let result = parse_origins_from_header("{001}");
416 assert_eq!(result, vec!["001".to_string()]);
417 }
418
419 #[test]
420 fn test_parse_origins_from_header_empty_braces() {
421 assert_eq!(parse_origins_from_header("{}"), Vec::<String>::new());
422 }
423
424 #[test]
425 fn test_convert_ws_scheme_wss() {
426 assert_eq!(
427 convert_ws_to_http_scheme("wss://ws.dataengine.chain.link"),
428 "https://ws.dataengine.chain.link"
429 );
430 }
431
432 #[test]
433 fn test_convert_ws_scheme_ws() {
434 assert_eq!(
435 convert_ws_to_http_scheme("ws://127.0.0.1:8080"),
436 "http://127.0.0.1:8080"
437 );
438 }
439
440 #[test]
441 fn test_convert_ws_scheme_passthrough() {
442 assert_eq!(
443 convert_ws_to_http_scheme("https://already.https.com"),
444 "https://already.https.com"
445 );
446 }
447}