laminar_connectors/websocket/
mod.rs1mod backpressure;
7mod connection;
8mod fanout;
9mod metrics;
10mod parser;
11mod protocol;
12mod serializer;
13mod sink;
14mod sink_client;
15mod sink_config;
16mod sink_metrics;
17mod source;
18mod source_config;
19
20use std::sync::{Arc, OnceLock, Weak};
21
22use metrics::WebSocketSourceMetrics;
23use sink::WebSocketSinkServer;
24use sink_client::WebSocketSinkClient;
25use sink_config::WebSocketSinkConfig;
26use sink_metrics::WebSocketSinkMetrics;
27use source::WebSocketSource;
28use source_config::WebSocketSourceConfig;
29
30use crate::config::{ConfigKeySpec, ConnectorInfo};
31use crate::registry::ConnectorRegistry;
32
33struct RegisteredMetricFamily<T> {
34 registry: Weak<prometheus::Registry>,
35 metrics: T,
36}
37
38struct MetricFamilyCache<T> {
39 family: OnceLock<RegisteredMetricFamily<T>>,
40 initialization: parking_lot::Mutex<()>,
41}
42
43impl<T> Default for MetricFamilyCache<T> {
44 fn default() -> Self {
45 Self {
46 family: OnceLock::new(),
47 initialization: parking_lot::Mutex::new(()),
48 }
49 }
50}
51
52impl<T: Clone> MetricFamilyCache<T> {
53 fn get_or_try_init(
54 &self,
55 registry: &Arc<prometheus::Registry>,
56 initialize: impl FnOnce() -> Result<T, crate::error::ConnectorError>,
57 ) -> Result<T, crate::error::ConnectorError> {
58 if let Some(family) = self.family.get() {
59 return family.for_registry(registry);
60 }
61 let _guard = self.initialization.lock();
62 if let Some(family) = self.family.get() {
63 return family.for_registry(registry);
64 }
65 let family = RegisteredMetricFamily {
66 registry: Arc::downgrade(registry),
67 metrics: initialize()?,
68 };
69 self.family
70 .set(family)
71 .unwrap_or_else(|_| unreachable!("metric initialization is serialized"));
72 Ok(self
73 .family
74 .get()
75 .expect("metric family was initialized")
76 .metrics
77 .clone())
78 }
79}
80
81impl<T: Clone> RegisteredMetricFamily<T> {
82 fn for_registry(
83 &self,
84 registry: &Arc<prometheus::Registry>,
85 ) -> Result<T, crate::error::ConnectorError> {
86 if !Weak::ptr_eq(&self.registry, &Arc::downgrade(registry)) {
87 return Err(crate::error::ConnectorError::ConfigurationError(
88 "WebSocket connector registry is already bound to a different Prometheus registry"
89 .into(),
90 ));
91 }
92 Ok(self.metrics.clone())
93 }
94}
95
96pub fn register_websocket_source(
106 registry: &ConnectorRegistry,
107) -> Result<(), crate::error::ConnectorError> {
108 let info = ConnectorInfo {
109 schema_capabilities: crate::schema::resolution::SchemaCapabilities::declared(false),
110 name: "websocket".to_string(),
111 display_name: "WebSocket Source".to_string(),
112 version: env!("CARGO_PKG_VERSION").to_string(),
113 is_source: true,
114 is_sink: false,
115 config_keys: websocket_source_config_keys(),
116 };
117
118 let metric_cache = Arc::new(MetricFamilyCache::<WebSocketSourceMetrics>::default());
119 registry.register_source(
120 "websocket",
121 info,
122 Arc::new(move |registry: Option<&Arc<prometheus::Registry>>| {
123 let metrics = if let Some(registry) = registry {
124 metric_cache
125 .get_or_try_init(registry, || WebSocketSourceMetrics::register(registry))?
126 } else {
127 WebSocketSourceMetrics::local()
128 };
129 Ok(Box::new(WebSocketSource::new(
130 Arc::new(arrow_schema::Schema::empty()),
131 WebSocketSourceConfig::default(),
132 metrics,
133 )))
134 }),
135 )
136}
137
138pub fn register_websocket_sink(
148 registry: &ConnectorRegistry,
149) -> Result<(), crate::error::ConnectorError> {
150 let info = ConnectorInfo {
151 schema_capabilities: crate::schema::resolution::SchemaCapabilities::declared(true),
152 name: "websocket".to_string(),
153 display_name: "WebSocket Sink".to_string(),
154 version: env!("CARGO_PKG_VERSION").to_string(),
155 is_source: false,
156 is_sink: true,
157 config_keys: websocket_sink_config_keys(),
158 };
159
160 let metric_cache = Arc::new(MetricFamilyCache::<WebSocketSinkMetrics>::default());
161 registry.register_sink(
162 "websocket",
163 info,
164 Arc::new(
165 move |config, registry: Option<&Arc<prometheus::Registry>>| {
166 let sink_config = WebSocketSinkConfig::from_config(config)?;
167 let decoded_schema = config.arrow_schema();
168 if config.get("_arrow_schema").is_some() && decoded_schema.is_none() {
169 return Err(crate::error::ConnectorError::ConfigurationError(
170 "invalid WebSocket sink _arrow_schema encoding".into(),
171 ));
172 }
173 let schema = decoded_schema.ok_or_else(|| {
174 crate::error::ConnectorError::ConfigurationError(
175 "WebSocket sink requires a declared Arrow schema".into(),
176 )
177 })?;
178 let metrics = if let Some(registry) = registry {
179 metric_cache
180 .get_or_try_init(registry, || WebSocketSinkMetrics::register(registry))?
181 } else {
182 WebSocketSinkMetrics::local()
183 };
184 let is_server = matches!(&sink_config, WebSocketSinkConfig::Server { .. });
185 let sink: Box<dyn crate::connector::SinkConnector> = if is_server {
186 Box::new(WebSocketSinkServer::new(schema, sink_config, metrics))
187 } else {
188 Box::new(WebSocketSinkClient::new(schema, sink_config, metrics))
189 };
190 Ok(sink)
191 },
192 ),
193 )
194}
195
196fn websocket_source_config_keys() -> Vec<ConfigKeySpec> {
197 vec![
198 ConfigKeySpec::required("url", "WebSocket URL to connect to (ws:// or wss://)"),
199 ConfigKeySpec::optional("format", "Message format (json/csv/binary)", "json"),
200 ConfigKeySpec::optional(
201 "subscribe.message",
202 "Text subscription message to send after handshake",
203 "",
204 ),
205 ConfigKeySpec::optional("reconnect.enabled", "Enable automatic reconnection", "true"),
206 ConfigKeySpec::optional(
207 "reconnect.initial.delay.ms",
208 "Initial reconnect delay in ms",
209 "100",
210 ),
211 ConfigKeySpec::optional(
212 "reconnect.max.delay.ms",
213 "Maximum reconnect delay in ms",
214 "30000",
215 ),
216 ConfigKeySpec::optional(
217 "reconnect.max.retries",
218 "Maximum reconnect attempts; empty means unlimited",
219 "",
220 ),
221 ConfigKeySpec::optional(
222 "on.backpressure",
223 "Backpressure strategy (block/drop_newest)",
224 "block",
225 ),
226 ConfigKeySpec::optional(
227 "max.message.size",
228 "Max WebSocket message size in bytes",
229 "67108864",
230 ),
231 ]
232}
233
234fn websocket_sink_config_keys() -> Vec<ConfigKeySpec> {
235 vec![
236 ConfigKeySpec::optional(
237 "bind.address",
238 "Socket address required in server mode (e.g., 0.0.0.0:8080)",
239 "",
240 ),
241 ConfigKeySpec::optional("mode", "Operating mode (server/client)", "server"),
242 ConfigKeySpec::optional(
243 "max.connections",
244 "Max concurrent client connections",
245 "10000",
246 ),
247 ConfigKeySpec::optional("ping.interval.ms", "Ping interval in ms", "30000"),
248 ConfigKeySpec::optional("ping.timeout.ms", "Pong timeout in ms", "10000"),
249 ConfigKeySpec::optional("url", "WebSocket URL required in client mode", ""),
250 ]
251}
252
253#[cfg(test)]
254mod tests;