1use crate::bus::{BusManager, BusMessage, StateUpdate};
2use crate::cache::{cmp_seq, EntityCache, SnapshotBatchConfig};
3use crate::compression::maybe_compress;
4use crate::row_scope::{session_row_scopes, RowFilter};
5use crate::shared_entity::{lookup, EntityFields, SharedEntity};
6use crate::view::{ViewIndex, ViewSpec};
7use crate::websocket::admission::{WebSocketAdmissionProvider, WebSocketConnectionPermit};
8use crate::websocket::auth::{
9 AuthContext, AuthDecision, AuthDeny, ConnectionAuthRequest, WebSocketAuthPlugin,
10};
11use crate::websocket::client_manager::{ClientManager, RateLimitConfig};
12use crate::websocket::frame::{
13 apply_wire_format, Frame, Mode, SortConfig, SortOrder, SourceFields, SubscribedFrame,
14 UnsubscribedFrame, WireEntity, WireFormat,
15};
16use crate::websocket::subscription::{
17 ClientMessage, RefreshAuthRequest, RefreshAuthResponse, SocketIssueMessage, Subscription,
18 SubscriptionQuery, Unsubscription, PROTOCOL_VERSION,
19};
20use crate::websocket::usage::{UsageIdentity, WebSocketUsageEmitter, WebSocketUsageEvent};
21use crate::{WebSocketDeliveryConfig, WebSocketLimitsConfig};
22use anyhow::Result;
23use bytes::Bytes;
24use futures_util::StreamExt;
25use serde::Serialize;
26use serde_json::Value;
27use std::collections::{HashMap, HashSet, VecDeque};
28use std::future::Future;
29use std::net::SocketAddr;
30use std::sync::atomic::{AtomicUsize, Ordering};
31use std::sync::Arc;
32use std::time::{Duration, Instant};
33use tokio::net::{TcpListener, TcpStream};
34use tokio::sync::{broadcast, watch};
35use tokio_tungstenite::{
36 accept_hdr_async_with_config,
37 tungstenite::{
38 handshake::server::{ErrorResponse as HandshakeErrorResponse, Request, Response},
39 http::{header::CONTENT_TYPE, StatusCode},
40 protocol::WebSocketConfig,
41 Error as WsError,
42 },
43};
44use tokio_util::sync::CancellationToken;
45use tokio_util::task::TaskTracker;
46use tracing::{debug, error, info, info_span, warn, Instrument};
47use uuid::Uuid;
48
49#[cfg(feature = "otel")]
50use crate::metrics::Metrics;
51
52#[derive(Clone, Default)]
53struct WsMetrics {
54 #[cfg(feature = "otel")]
55 inner: Option<Arc<Metrics>>,
56 #[cfg(test)]
57 probe: Option<Arc<DeliveryProbe>>,
58}
59
60#[cfg(test)]
64#[derive(Debug, Default)]
65pub(crate) struct DeliveryProbe {
66 pub(crate) active_connections: std::sync::Mutex<std::collections::BTreeMap<String, i64>>,
68 pub(crate) active_subscriptions: std::sync::Mutex<std::collections::BTreeMap<String, i64>>,
70 pub(crate) messages_sent: std::sync::atomic::AtomicU64,
72 pub(crate) lag_events: std::sync::atomic::AtomicU64,
74 pub(crate) dropped_updates: std::sync::atomic::AtomicU64,
76 pub(crate) resnapshots: std::sync::atomic::AtomicU64,
78 pub(crate) coalesced_updates: std::sync::atomic::AtomicU64,
80 pub(crate) coalesced_flushes: std::sync::atomic::AtomicU64,
82 pub(crate) delivery_stopped: std::sync::Mutex<std::collections::BTreeMap<&'static str, u64>>,
84}
85
86#[cfg(test)]
87impl DeliveryProbe {
88 fn add(counter: &std::sync::atomic::AtomicU64, value: u64) {
89 counter.fetch_add(value, std::sync::atomic::Ordering::Relaxed);
90 }
91
92 fn add_active(
93 counts: &std::sync::Mutex<std::collections::BTreeMap<String, i64>>,
94 key: String,
95 delta: i64,
96 ) {
97 *counts
98 .lock()
99 .expect("delivery probe lock poisoned")
100 .entry(key)
101 .or_default() += delta;
102 }
103}
104
105impl WsMetrics {
106 #[cfg(feature = "otel")]
107 fn new(inner: Option<Arc<Metrics>>) -> Self {
108 Self {
109 inner,
110 #[cfg(test)]
111 probe: None,
112 }
113 }
114
115 #[cfg(test)]
116 fn probe(&self, record: impl FnOnce(&DeliveryProbe)) {
117 if let Some(probe) = &self.probe {
118 record(probe);
119 }
120 }
121
122 fn connection_opened(&self, metering_key: Option<&str>) {
123 #[cfg(test)]
124 self.probe(|probe| {
125 DeliveryProbe::add_active(
126 &probe.active_connections,
127 metering_key.unwrap_or("<none>").to_string(),
128 1,
129 )
130 });
131 #[cfg(not(feature = "otel"))]
132 let _ = metering_key;
133 #[cfg(feature = "otel")]
134 if let Some(metrics) = &self.inner {
135 if let Some(metering_key) = metering_key {
136 metrics.record_ws_connection_with_metering(metering_key);
137 } else {
138 metrics.record_ws_connection();
139 }
140 }
141 }
142
143 fn connection_closed(&self, duration_secs: f64, metering_key: Option<&str>) {
144 #[cfg(test)]
145 self.probe(|probe| {
146 DeliveryProbe::add_active(
147 &probe.active_connections,
148 metering_key.unwrap_or("<none>").to_string(),
149 -1,
150 )
151 });
152 #[cfg(not(feature = "otel"))]
153 let _ = (duration_secs, metering_key);
154 #[cfg(feature = "otel")]
155 if let Some(metrics) = &self.inner {
156 if let Some(metering_key) = metering_key {
157 metrics.record_ws_disconnection_with_metering(duration_secs, metering_key);
158 } else {
159 metrics.record_ws_disconnection(duration_secs);
160 }
161 }
162 }
163
164 fn message_received(&self, metering_key: Option<&str>) {
165 #[cfg(not(feature = "otel"))]
166 let _ = metering_key;
167 #[cfg(feature = "otel")]
168 if let Some(metrics) = &self.inner {
169 if let Some(metering_key) = metering_key {
170 metrics.record_ws_message_received_with_metering(metering_key);
171 } else {
172 metrics.record_ws_message_received();
173 }
174 }
175 }
176
177 fn message_sent(&self) {
178 #[cfg(test)]
179 self.probe(|probe| DeliveryProbe::add(&probe.messages_sent, 1));
180 #[cfg(feature = "otel")]
181 if let Some(metrics) = &self.inner {
182 metrics.record_ws_message_sent();
183 }
184 }
185
186 fn subscription_created(&self, view: &str, metering_key: Option<&str>) {
187 #[cfg(test)]
188 self.probe(|probe| {
189 DeliveryProbe::add_active(
190 &probe.active_subscriptions,
191 format!("{view}|{}", metering_key.unwrap_or("<none>")),
192 1,
193 )
194 });
195 #[cfg(not(feature = "otel"))]
196 let _ = (view, metering_key);
197 #[cfg(feature = "otel")]
198 if let Some(metrics) = &self.inner {
199 if let Some(metering_key) = metering_key {
200 metrics.record_subscription_created_with_metering(view, metering_key);
201 } else {
202 metrics.record_subscription_created(view);
203 }
204 }
205 }
206
207 fn subscription_removed(&self, view: &str, metering_key: Option<&str>) {
208 #[cfg(test)]
209 self.probe(|probe| {
210 DeliveryProbe::add_active(
211 &probe.active_subscriptions,
212 format!("{view}|{}", metering_key.unwrap_or("<none>")),
213 -1,
214 )
215 });
216 #[cfg(not(feature = "otel"))]
217 let _ = (view, metering_key);
218 #[cfg(feature = "otel")]
219 if let Some(metrics) = &self.inner {
220 if let Some(metering_key) = metering_key {
221 metrics.record_subscription_removed_with_metering(view, metering_key);
222 } else {
223 metrics.record_subscription_removed(view);
224 }
225 }
226 }
227
228 fn protocol_error(&self, code: &str) {
229 #[cfg(not(feature = "otel"))]
230 let _ = code;
231 #[cfg(feature = "otel")]
232 if let Some(metrics) = &self.inner {
233 metrics.record_ws_protocol_error(code);
234 }
235 }
236
237 fn subscription_lagged(&self, view: &str, skipped: u64) {
238 #[cfg(test)]
239 self.probe(|probe| {
240 DeliveryProbe::add(&probe.lag_events, 1);
241 DeliveryProbe::add(&probe.dropped_updates, skipped);
242 });
243 #[cfg(not(feature = "otel"))]
244 let _ = (view, skipped);
245 #[cfg(feature = "otel")]
246 if let Some(metrics) = &self.inner {
247 metrics.record_ws_subscription_lagged(view, skipped);
248 }
249 }
250
251 fn subscription_resnapshot(&self, view: &str) {
252 #[cfg(test)]
253 self.probe(|probe| DeliveryProbe::add(&probe.resnapshots, 1));
254 #[cfg(not(feature = "otel"))]
255 let _ = view;
256 #[cfg(feature = "otel")]
257 if let Some(metrics) = &self.inner {
258 metrics.record_ws_subscription_resnapshot(view);
259 }
260 }
261
262 fn collection_coalesced(&self, view: &str, updates: u64) {
263 #[cfg(test)]
264 self.probe(|probe| {
265 DeliveryProbe::add(&probe.coalesced_updates, updates);
266 DeliveryProbe::add(&probe.coalesced_flushes, 1);
267 });
268 #[cfg(not(feature = "otel"))]
269 let _ = (view, updates);
270 #[cfg(feature = "otel")]
271 if let Some(metrics) = &self.inner {
272 metrics.record_ws_collection_coalesced(view, updates);
273 }
274 }
275
276 fn delivery_stopped(&self, view: &str, reason: &'static str) {
277 #[cfg(test)]
278 self.probe(|probe| {
279 *probe
280 .delivery_stopped
281 .lock()
282 .expect("delivery probe lock poisoned")
283 .entry(reason)
284 .or_default() += 1;
285 });
286 #[cfg(not(feature = "otel"))]
287 let _ = (view, reason);
288 #[cfg(feature = "otel")]
289 if let Some(metrics) = &self.inner {
290 metrics.record_ws_delivery_stopped(view, reason);
291 }
292 }
293}
294
295async fn handle_refresh_auth(
296 client_id: Uuid,
297 remote_addr: SocketAddr,
298 refresh_req: &RefreshAuthRequest,
299 client_manager: &ClientManager,
300 auth_plugin: &Arc<dyn WebSocketAuthPlugin>,
301) {
302 let refresh_result: Result<AuthContext, String> = if let Some(signed_plugin) = auth_plugin
303 .as_any()
304 .downcast_ref::<crate::websocket::auth::SignedSessionAuthPlugin>()
305 {
306 signed_plugin
307 .verify_refresh_token_from(&refresh_req.token, remote_addr)
308 .await
309 .map_err(|error| error.reason)
310 } else {
311 Err("In-band auth refresh not supported with current auth plugin".to_string())
312 };
313
314 let response = match refresh_result {
315 Ok(new_context) => {
316 let expires_at = new_context.expires_at;
317 match client_manager
318 .try_update_client_auth_async(client_id, new_context)
319 .await
320 {
321 Ok(true) => RefreshAuthResponse {
322 success: true,
323 error: None,
324 expires_at: Some(expires_at),
325 },
326 Ok(false) => RefreshAuthResponse {
327 success: false,
328 error: Some("client-not-found".to_string()),
329 expires_at: None,
330 },
331 Err(deny) => RefreshAuthResponse {
332 success: false,
333 error: Some(deny.code.as_str().to_string()),
334 expires_at: None,
335 },
336 }
337 }
338 Err(error) => {
339 let code = if error.contains("expired") {
340 "token-expired"
341 } else if error.contains("signature") {
342 "token-invalid-signature"
343 } else if error.contains("issuer") {
344 "token-invalid-issuer"
345 } else if error.contains("audience") {
346 "token-invalid-audience"
347 } else {
348 "token-invalid"
349 };
350 RefreshAuthResponse {
351 success: false,
352 error: Some(code.to_string()),
353 expires_at: None,
354 }
355 }
356 };
357
358 if let Ok(json) = serde_json::to_string(&response) {
359 let _ = client_manager.send_text_to_client(client_id, json).await;
360 }
361}
362
363async fn send_socket_issue(
364 client_id: Uuid,
365 client_manager: &ClientManager,
366 deny: &AuthDeny,
367 fatal: bool,
368 subscription_id: Option<String>,
369) {
370 let message = SocketIssueMessage::from_auth_deny(deny, fatal, subscription_id);
371 if let Ok(json) = serde_json::to_string(&message) {
372 let _ = client_manager.send_text_to_client(client_id, json).await;
373 }
374}
375
376async fn send_protocol_issue(
377 client_id: Uuid,
378 client_manager: &ClientManager,
379 metrics: &WsMetrics,
380 subscription_id: Option<String>,
381 code: &str,
382 message: impl Into<String>,
383) {
384 metrics.protocol_error(code);
385 let issue = SocketIssueMessage::protocol(subscription_id, code, message);
386 if let Ok(json) = serde_json::to_string(&issue) {
387 let _ = client_manager.send_text_to_client(client_id, json).await;
388 }
389}
390
391async fn send_prepared_issue(
393 client_id: Uuid,
394 client_manager: &ClientManager,
395 metrics: &WsMetrics,
396 issue: SocketIssueMessage,
397) {
398 metrics.protocol_error(&issue.code);
399 if let Ok(json) = serde_json::to_string(&issue) {
400 let _ = client_manager.send_text_to_client(client_id, json).await;
401 }
402}
403
404fn key_class_label(key_class: arete_auth::KeyClass) -> &'static str {
405 match key_class {
406 arete_auth::KeyClass::Secret => "secret",
407 arete_auth::KeyClass::Publishable => "publishable",
408 }
409}
410
411#[derive(Clone)]
412struct UsageEmitterHandle {
413 emitter: Arc<dyn WebSocketUsageEmitter>,
414 tasks: TaskTracker,
415}
416
417impl UsageEmitterHandle {
418 fn new(emitter: Arc<dyn WebSocketUsageEmitter>) -> Self {
419 Self {
420 emitter,
421 tasks: TaskTracker::new(),
422 }
423 }
424
425 fn emit(&self, event: WebSocketUsageEvent) {
426 let emitter = self.emitter.clone();
427 self.tasks.spawn(async move {
428 emitter.emit(event).await;
429 });
430 }
431
432 async fn close_and_wait(&self) {
433 self.tasks.close();
434 self.tasks.wait().await;
435 }
436}
437
438fn emit_usage_event(usage_emitter: &Option<UsageEmitterHandle>, event: WebSocketUsageEvent) {
439 if let Some(emitter) = usage_emitter {
440 emitter.emit(event);
441 }
442}
443
444fn usage_identity_from_context(
445 auth_context: Option<&AuthContext>,
446) -> (UsageIdentity, Option<String>) {
447 match auth_context {
448 Some(context) => {
449 let v2 = !context.is_legacy_policy();
450 (
451 UsageIdentity {
452 metering_key: Some(context.metering_key.clone()),
453 subject: Some(context.subject.clone()),
454 key_class: Some(key_class_label(context.key_class).to_string()),
455 actor_key: v2.then(|| context.actor_key.clone()).flatten(),
456 account_key: v2.then(|| context.account_key.clone()).flatten(),
457 consumer_key: v2.then(|| context.consumer_key.clone()).flatten(),
458 plan_code: v2.then(|| context.plan.clone()).flatten(),
459 policy_version: v2.then_some(context.policy_version).flatten(),
460 },
461 context.deployment_id.clone(),
462 )
463 }
464 None => (UsageIdentity::default(), None),
465 }
466}
467
468fn emit_update_sent_for_client(
469 usage_emitter: &Option<UsageEmitterHandle>,
470 client_manager: &ClientManager,
471 client_id: Uuid,
472 view_id: &str,
473 bytes: usize,
474) {
475 let auth_context = client_manager.get_auth_context(client_id);
476 let (identity, deployment_id) = usage_identity_from_context(auth_context.as_ref());
477 emit_usage_event(
478 usage_emitter,
479 WebSocketUsageEvent::UpdateSent {
480 client_id: client_id.to_string(),
481 deployment_id,
482 identity,
483 view_id: view_id.to_string(),
484 messages: 1,
485 bytes: bytes as u64,
486 },
487 );
488}
489
490#[derive(Clone)]
491struct SubscriptionContext {
492 client_id: Uuid,
493 client_manager: ClientManager,
494 bus_manager: BusManager,
495 entity_cache: EntityCache,
496 view_index: Arc<ViewIndex>,
497 usage_emitter: Option<UsageEmitterHandle>,
498 journal: Option<Arc<crate::journal::EventJournal>>,
499 metrics: WsMetrics,
500 delivery: WebSocketDeliveryConfig,
501 shutdown: CancellationToken,
504 row_scope_lock: Arc<tokio::sync::RwLock<u64>>,
509 row_filter: Option<RowFilter>,
512 resnapshot: bool,
516}
517
518pub struct WebSocketServer {
519 bind_addr: SocketAddr,
520 client_manager: ClientManager,
521 bus_manager: BusManager,
522 entity_cache: EntityCache,
523 view_index: Arc<ViewIndex>,
524 max_clients: usize,
525 auth_plugin: Arc<dyn WebSocketAuthPlugin>,
526 usage_emitter: Option<UsageEmitterHandle>,
527 rate_limit_config: Option<RateLimitConfig>,
528 admission_provider: Option<Arc<dyn WebSocketAdmissionProvider>>,
529 journal: Option<Arc<crate::journal::EventJournal>>,
530 delivery: WebSocketDeliveryConfig,
531 limits: WebSocketLimitsConfig,
532 #[cfg(feature = "otel")]
533 metrics: Option<Arc<Metrics>>,
534}
535
536impl WebSocketServer {
537 #[cfg(feature = "otel")]
538 pub fn new(
539 bind_addr: SocketAddr,
540 bus_manager: BusManager,
541 entity_cache: EntityCache,
542 view_index: Arc<ViewIndex>,
543 metrics: Option<Arc<Metrics>>,
544 ) -> Self {
545 Self {
546 bind_addr,
547 client_manager: ClientManager::new(),
548 bus_manager,
549 entity_cache,
550 view_index,
551 max_clients: 10_000,
552 auth_plugin: Arc::new(crate::websocket::auth::AllowAllAuthPlugin),
553 usage_emitter: None,
554 rate_limit_config: None,
555 admission_provider: None,
556 journal: None,
557 delivery: WebSocketDeliveryConfig::default(),
558 limits: WebSocketLimitsConfig::default(),
559 metrics,
560 }
561 }
562
563 #[cfg(not(feature = "otel"))]
564 pub fn new(
565 bind_addr: SocketAddr,
566 bus_manager: BusManager,
567 entity_cache: EntityCache,
568 view_index: Arc<ViewIndex>,
569 ) -> Self {
570 Self {
571 bind_addr,
572 client_manager: ClientManager::new(),
573 bus_manager,
574 entity_cache,
575 view_index,
576 max_clients: 10_000,
577 auth_plugin: Arc::new(crate::websocket::auth::AllowAllAuthPlugin),
578 usage_emitter: None,
579 rate_limit_config: None,
580 admission_provider: None,
581 journal: None,
582 delivery: WebSocketDeliveryConfig::default(),
583 limits: WebSocketLimitsConfig::default(),
584 }
585 }
586
587 pub fn with_max_clients(mut self, max_clients: usize) -> Self {
588 self.max_clients = max_clients;
589 self
590 }
591
592 pub fn with_auth_plugin(mut self, auth_plugin: Arc<dyn WebSocketAuthPlugin>) -> Self {
593 self.auth_plugin = auth_plugin;
594 self
595 }
596
597 pub fn with_usage_emitter(mut self, usage_emitter: Arc<dyn WebSocketUsageEmitter>) -> Self {
598 self.usage_emitter = Some(UsageEmitterHandle::new(usage_emitter));
599 self
600 }
601
602 pub fn with_journal(mut self, journal: Arc<crate::journal::EventJournal>) -> Self {
604 self.journal = Some(journal);
605 self
606 }
607
608 pub fn with_rate_limit_config(mut self, config: RateLimitConfig) -> Self {
609 self.rate_limit_config = Some(config);
610 self
611 }
612
613 pub fn with_admission_provider(
615 mut self,
616 provider: Arc<dyn WebSocketAdmissionProvider>,
617 ) -> Self {
618 self.admission_provider = Some(provider);
619 self
620 }
621
622 pub fn with_limits(mut self, limits: WebSocketLimitsConfig) -> Self {
624 self.limits = limits;
625 self
626 }
627
628 pub fn with_delivery_config(mut self, config: WebSocketDeliveryConfig) -> Self {
629 self.delivery = config;
630 self
631 }
632
633 pub async fn start(self) -> Result<()> {
637 info!(
638 "Starting WebSocket server on {} (max_clients: {})",
639 self.bind_addr, self.max_clients
640 );
641 let listener = TcpListener::bind(&self.bind_addr).await?;
642 let (acceptor, _cleanup) = self.into_acceptor();
643 acceptor.serve_listener(listener).await
644 }
645
646 pub(crate) fn into_acceptor(self) -> (ConnectionAcceptor, tokio::task::JoinHandle<()>) {
652 let mut client_manager = self
653 .rate_limit_config
654 .map(ClientManager::with_config)
655 .unwrap_or(self.client_manager);
656 if let Some(provider) = self.admission_provider {
657 client_manager = client_manager.with_admission_provider(provider);
658 }
659 let cleanup = client_manager.start_cleanup_task();
660
661 #[cfg(feature = "otel")]
662 let metrics = WsMetrics::new(self.metrics.clone());
663 #[cfg(not(feature = "otel"))]
664 let metrics = WsMetrics::default();
665
666 let acceptor = ConnectionAcceptor {
667 client_manager,
668 bus_manager: self.bus_manager,
669 entity_cache: self.entity_cache,
670 view_index: self.view_index,
671 max_clients: self.max_clients,
672 auth_plugin: self.auth_plugin,
673 usage_emitter: self.usage_emitter,
674 journal: self.journal,
675 delivery: self.delivery,
676 limits: self.limits,
677 pending_handshakes: Arc::new(AtomicUsize::new(0)),
678 metrics,
679 shutdown: CancellationToken::new(),
680 sessions: TaskTracker::new(),
681 };
682 (acceptor, cleanup)
683 }
684}
685
686#[derive(Clone)]
694pub(crate) struct ConnectionAcceptor {
695 client_manager: ClientManager,
696 bus_manager: BusManager,
697 entity_cache: EntityCache,
698 view_index: Arc<ViewIndex>,
699 max_clients: usize,
700 auth_plugin: Arc<dyn WebSocketAuthPlugin>,
701 usage_emitter: Option<UsageEmitterHandle>,
702 journal: Option<Arc<crate::journal::EventJournal>>,
703 delivery: WebSocketDeliveryConfig,
704 limits: WebSocketLimitsConfig,
705 pending_handshakes: Arc<AtomicUsize>,
708 metrics: WsMetrics,
709 shutdown: CancellationToken,
710 sessions: TaskTracker,
714}
715
716impl ConnectionAcceptor {
717 #[cfg(test)]
720 pub(crate) fn with_delivery_probe(mut self, probe: Arc<DeliveryProbe>) -> Self {
721 self.metrics.probe = Some(probe);
722 self
723 }
724
725 pub(crate) fn client_count(&self) -> usize {
727 self.client_manager.client_count()
728 }
729
730 pub(crate) fn shutdown(&self) {
736 self.shutdown.cancel();
737 self.sessions.close();
738 }
739
740 pub(crate) async fn wait_for_sessions(&self) {
743 self.sessions.wait().await;
744 }
745
746 pub(crate) async fn wait_for_usage_events(&self) {
749 if let Some(emitter) = &self.usage_emitter {
750 emitter.close_and_wait().await;
751 }
752 }
753
754 pub(crate) async fn serve(&self, stream: TcpStream, remote_addr: SocketAddr) -> Result<()> {
760 let Some(pending) =
761 PendingHandshake::reserve(&self.pending_handshakes, self.limits.max_pending_handshakes)
762 else {
763 warn!(
764 "Rejecting connection from {}: too many pending handshakes",
765 remote_addr
766 );
767 return Ok(());
768 };
769 if self.client_manager.client_count() + pending.count() > self.max_clients {
770 warn!(
771 "Rejecting connection from {}: max clients reached",
772 remote_addr
773 );
774 return Ok(());
775 }
776
777 let context = SubscriptionContext {
778 client_id: Uuid::nil(),
779 client_manager: self.client_manager.clone(),
780 bus_manager: self.bus_manager.clone(),
781 entity_cache: self.entity_cache.clone(),
782 view_index: self.view_index.clone(),
783 usage_emitter: self.usage_emitter.clone(),
784 journal: self.journal.clone(),
785 delivery: self.delivery.clone(),
786 metrics: self.metrics.clone(),
787 shutdown: self.shutdown.clone(),
788 row_scope_lock: Arc::new(tokio::sync::RwLock::new(0)),
789 row_filter: None,
790 resnapshot: false,
791 };
792 handle_connection(
793 stream,
794 context,
795 remote_addr,
796 self.auth_plugin.clone(),
797 &self.limits,
798 pending,
799 )
800 .await
801 }
802
803 pub(crate) async fn serve_listener(self, listener: TcpListener) -> Result<()> {
806 loop {
807 let accepted = tokio::select! {
808 _ = self.shutdown.cancelled() => return Ok(()),
809 accepted = listener.accept() => accepted,
810 };
811 match accepted {
812 Ok((stream, addr)) => {
813 let acceptor = self.clone();
814 self.sessions.spawn(
815 async move {
816 if let Err(error) = acceptor.serve(stream, addr).await {
817 error!("WebSocket connection error: {}", error);
818 }
819 }
820 .instrument(info_span!("ws.connection", %addr)),
821 );
822 }
823 Err(error) => error!("Failed to accept connection: {}", error),
824 }
825 }
826 }
827}
828
829pub(crate) struct PendingHandshake {
831 counter: Arc<AtomicUsize>,
832 count: usize,
833}
834
835impl PendingHandshake {
836 fn reserve(counter: &Arc<AtomicUsize>, max: usize) -> Option<Self> {
837 let previous = counter
838 .fetch_update(Ordering::AcqRel, Ordering::Acquire, |pending| {
839 (pending < max).then_some(pending + 1)
840 })
841 .ok()?;
842 Some(Self {
843 counter: counter.clone(),
844 count: previous + 1,
845 })
846 }
847
848 fn count(&self) -> usize {
850 self.count
851 }
852}
853
854impl Drop for PendingHandshake {
855 fn drop(&mut self) {
856 self.counter.fetch_sub(1, Ordering::AcqRel);
857 }
858}
859
860#[derive(Debug, Clone)]
861struct HandshakeReject {
862 status: StatusCode,
863 body: crate::websocket::auth::ErrorResponse,
864 error_code: String,
865 retry_after_secs: Option<u64>,
866}
867
868impl HandshakeReject {
869 fn from_deny(deny: &AuthDeny) -> Self {
870 let retry_after_secs = match deny.retry_policy {
871 crate::websocket::auth::RetryPolicy::RetryAfter(duration) => Some(duration.as_secs()),
872 _ => None,
873 };
874 Self {
875 status: StatusCode::from_u16(deny.http_status).unwrap_or(StatusCode::UNAUTHORIZED),
876 body: deny.to_error_response(),
877 error_code: deny.code.to_string(),
878 retry_after_secs,
879 }
880 }
881}
882
883fn build_handshake_error_response(
884 response: &Response,
885 reject: &HandshakeReject,
886) -> HandshakeErrorResponse {
887 let mut builder = Response::builder()
888 .status(reject.status)
889 .version(response.version())
890 .header(CONTENT_TYPE, "application/json; charset=utf-8")
891 .header("X-Error-Code", &reject.error_code)
892 .header("Cache-Control", "no-store");
893 if let Some(retry_after_secs) = reject.retry_after_secs {
894 builder = builder.header("Retry-After", retry_after_secs.to_string());
895 }
896 let body = serde_json::to_string(&reject.body).unwrap_or_else(|_| {
897 format!(
898 r#"{{"error":"{}","message":"{}","code":"{}","retryable":false}}"#,
899 reject.body.error, reject.body.message, reject.body.code
900 )
901 });
902 builder
903 .body(Some(body))
904 .expect("handshake rejection response should build")
905}
906
907#[allow(clippy::result_large_err)]
908async fn accept_authorized_connection(
909 stream: TcpStream,
910 remote_addr: SocketAddr,
911 auth_plugin: Arc<dyn WebSocketAuthPlugin>,
912 client_manager: ClientManager,
913 limits: &WebSocketLimitsConfig,
914 deadline: tokio::time::Instant,
915) -> Result<
916 Option<(
917 tokio_tungstenite::WebSocketStream<TcpStream>,
918 AuthContext,
919 Option<Arc<dyn WebSocketConnectionPermit>>,
920 )>,
921> {
922 use std::sync::Mutex;
923
924 type Admission =
925 Result<(AuthContext, Option<Arc<dyn WebSocketConnectionPermit>>), HandshakeReject>;
926 let capture: Arc<Mutex<Option<Admission>>> = Arc::new(Mutex::new(None));
927 let capture_ref = capture.clone();
928 let auth_plugin_ref = auth_plugin.clone();
929 let manager_ref = client_manager.clone();
930
931 let config = WebSocketConfig::default()
933 .max_message_size(Some(limits.max_message_bytes))
934 .max_frame_size(Some(limits.max_frame_bytes));
935 let callback = move |request: &Request, response| {
936 let request = ConnectionAuthRequest::from_http_request(remote_addr, request);
937 let result = tokio::task::block_in_place(|| {
938 tokio::runtime::Handle::current().block_on(async {
942 let admission = async {
943 match auth_plugin_ref.authorize(&request).await {
944 AuthDecision::Allow(context) => manager_ref
945 .check_connection_allowed(remote_addr, &Some(context.clone()))
946 .await
947 .and_then(|()| manager_ref.reserve_connection_admission(&context))
948 .map(|permit| (context, permit))
949 .map_err(|deny| HandshakeReject::from_deny(&deny)),
950 AuthDecision::Deny(deny) => Err(HandshakeReject::from_deny(&deny)),
951 }
952 };
953 tokio::time::timeout_at(deadline, admission)
954 .await
955 .unwrap_or_else(|_| {
956 Err(HandshakeReject::from_deny(&AuthDeny::new(
957 crate::websocket::auth::AuthErrorCode::InternalError,
958 "Authentication did not complete in time",
959 )))
960 })
961 })
962 });
963 *capture_ref.lock().expect("capture lock poisoned") = Some(result.clone());
964 match result {
965 Ok(_) => Ok(response),
966 Err(reject) => Err(build_handshake_error_response(&response, &reject)),
967 }
968 };
969 let handshake_result = accept_hdr_async_with_config(stream, callback, Some(config)).await;
970
971 let auth_result = capture.lock().expect("capture lock poisoned").take();
972 match handshake_result {
973 Ok(stream) => match auth_result {
974 Some(Ok((context, permit))) => Ok(Some((stream, context, permit))),
975 Some(Err(reject)) => Err(anyhow::anyhow!(
976 "handshake unexpectedly succeeded after rejection: {}",
977 reject.body.message
978 )),
979 None => Err(anyhow::anyhow!("no auth result captured during handshake")),
980 },
981 Err(WsError::Http(_)) => Ok(None),
982 Err(error) => Err(error.into()),
983 }
984}
985
986async fn handle_connection(
987 stream: TcpStream,
988 mut context: SubscriptionContext,
989 remote_addr: SocketAddr,
990 auth_plugin: Arc<dyn WebSocketAuthPlugin>,
991 limits: &WebSocketLimitsConfig,
992 pending: PendingHandshake,
993) -> Result<()> {
994 let deadline = tokio::time::Instant::now() + limits.handshake_timeout;
997 let accepted = tokio::select! {
998 _ = context.shutdown.cancelled() => return Ok(()),
999 accepted = tokio::time::timeout_at(
1000 deadline,
1001 accept_authorized_connection(
1002 stream,
1003 remote_addr,
1004 auth_plugin.clone(),
1005 context.client_manager.clone(),
1006 limits,
1007 deadline,
1008 ),
1009 ) => match accepted {
1010 Ok(accepted) => accepted?,
1011 Err(_) => {
1012 debug!("WebSocket handshake from {} timed out", remote_addr);
1013 return Ok(());
1014 }
1015 },
1016 };
1017 let Some((ws_stream, auth_context, admission_permit)) = accepted else {
1018 return Ok(());
1019 };
1020
1021 let client_id = Uuid::new_v4();
1022 context.client_id = client_id;
1023 let connection_start = Instant::now();
1024 let (mut usage_identity, mut deployment_id) = usage_identity_from_context(Some(&auth_context));
1025 let mut metering_key = usage_identity.metering_key.clone();
1026 let connection_metrics_metering_key = metering_key.clone();
1032 context
1033 .metrics
1034 .connection_opened(connection_metrics_metering_key.as_deref());
1035
1036 let (ws_sender, mut ws_receiver) = ws_stream.split();
1037 context.client_manager.add_client_with_admission(
1038 client_id,
1039 ws_sender,
1040 Some(auth_context),
1041 remote_addr,
1042 admission_permit,
1043 );
1044 let connection_token = context.client_manager.connection_token(client_id);
1048 drop(pending);
1052 emit_usage_event(
1053 &context.usage_emitter,
1054 WebSocketUsageEvent::ConnectionEstablished {
1055 client_id: client_id.to_string(),
1056 remote_addr: remote_addr.to_string(),
1057 deployment_id: deployment_id.clone(),
1058 identity: usage_identity.clone(),
1059 },
1060 );
1061
1062 let mut active_subscriptions: HashMap<String, ActiveSubscription> = HashMap::new();
1063 loop {
1064 let message = tokio::select! {
1065 _ = context.shutdown.cancelled() => break,
1066 _ = connection_token.cancelled() => break,
1067 next = ws_receiver.next() => match next {
1068 Some(message) => message,
1069 None => break,
1070 },
1071 };
1072 let message = match message {
1073 Ok(message) => message,
1074 Err(error) => {
1075 warn!("WebSocket error for client {}: {}", client_id, error);
1076 break;
1077 }
1078 };
1079 if message.is_close() {
1080 break;
1081 }
1082 context.client_manager.update_client_last_seen(client_id);
1083 if !message.is_text() {
1084 continue;
1085 }
1086 if let Err(deny) = context
1087 .client_manager
1088 .check_inbound_message_allowed(client_id)
1089 {
1090 send_socket_issue(client_id, &context.client_manager, &deny, true, None).await;
1091 break;
1092 }
1093 context.metrics.message_received(metering_key.as_deref());
1094
1095 let text = match message.to_text() {
1096 Ok(text) => text,
1097 Err(_) => continue,
1098 };
1099 let client_message = match serde_json::from_str::<ClientMessage>(text) {
1100 Ok(message) => message,
1101 Err(parse_error) => {
1102 let subscription_id = extract_subscription_id(text);
1103 send_protocol_issue(
1104 client_id,
1105 &context.client_manager,
1106 &context.metrics,
1107 subscription_id,
1108 "malformed-message",
1109 format!("invalid protocol v2 message: {parse_error}"),
1110 )
1111 .await;
1112 continue;
1113 }
1114 };
1115
1116 match client_message {
1117 ClientMessage::Subscribe(subscription) => {
1118 let subscription_id = subscription.subscription_id.clone();
1119 if let Err(message) = subscription.validate() {
1120 send_protocol_issue(
1121 client_id,
1122 &context.client_manager,
1123 &context.metrics,
1124 Some(subscription_id),
1125 "invalid-subscription",
1126 message,
1127 )
1128 .await;
1129 continue;
1130 }
1131 if let Err(deny) = context
1132 .client_manager
1133 .check_subscription_allowed(client_id)
1134 .await
1135 {
1136 send_socket_issue(
1137 client_id,
1138 &context.client_manager,
1139 &deny,
1140 false,
1141 Some(subscription_id),
1142 )
1143 .await;
1144 continue;
1145 }
1146
1147 let cancel_token = connection_token.child_token();
1151 let added = match context
1152 .client_manager
1153 .try_add_client_subscription(
1154 client_id,
1155 subscription_id.clone(),
1156 cancel_token.clone(),
1157 )
1158 .await
1159 {
1160 Ok(added) => added,
1161 Err(deny) => {
1162 send_socket_issue(
1163 client_id,
1164 &context.client_manager,
1165 &deny,
1166 false,
1167 Some(subscription_id),
1168 )
1169 .await;
1170 continue;
1171 }
1172 };
1173 if !added {
1174 send_protocol_issue(
1175 client_id,
1176 &context.client_manager,
1177 &context.metrics,
1178 Some(subscription_id),
1179 "duplicate-subscription-id",
1180 "subscriptionId is already active on this connection",
1181 )
1182 .await;
1183 continue;
1184 }
1185
1186 let view = subscription.query.view.clone();
1187 let original = subscription.clone();
1188 if let Err(error) = attach_client_to_bus(&context, subscription, cancel_token).await
1189 {
1190 context
1191 .client_manager
1192 .remove_client_subscription(client_id, &subscription_id)
1193 .await;
1194 match error.downcast::<RejectedSubscription>() {
1198 Ok(rejected) => {
1199 send_prepared_issue(
1200 client_id,
1201 &context.client_manager,
1202 &context.metrics,
1203 rejected.0,
1204 )
1205 .await;
1206 }
1207 Err(error) => {
1208 send_protocol_issue(
1209 client_id,
1210 &context.client_manager,
1211 &context.metrics,
1212 Some(subscription_id),
1213 "subscription-rejected",
1214 error.to_string(),
1215 )
1216 .await;
1217 }
1218 }
1219 continue;
1220 }
1221
1222 active_subscriptions.insert(
1223 subscription_id,
1224 ActiveSubscription {
1225 view: view.clone(),
1226 metrics_metering_key: metering_key.clone(),
1227 subscription: original,
1228 },
1229 );
1230 context
1231 .metrics
1232 .subscription_created(&view, metering_key.as_deref());
1233 emit_usage_event(
1234 &context.usage_emitter,
1235 WebSocketUsageEvent::SubscriptionCreated {
1236 client_id: client_id.to_string(),
1237 deployment_id: deployment_id.clone(),
1238 identity: usage_identity.clone(),
1239 view_id: view,
1240 },
1241 );
1242 }
1243 ClientMessage::Unsubscribe(unsubscription) => {
1244 handle_unsubscribe(
1245 &context,
1246 unsubscription,
1247 &mut active_subscriptions,
1248 &deployment_id,
1249 &usage_identity,
1250 )
1251 .await;
1252 }
1253 ClientMessage::Ping => debug!("Received ping from client {}", client_id),
1254 ClientMessage::RefreshAuth(request) => {
1255 let row_scopes_before =
1256 session_row_scopes(context.client_manager.get_auth_context(client_id).as_ref());
1257 handle_refresh_auth(
1258 client_id,
1259 remote_addr,
1260 &request,
1261 &context.client_manager,
1262 &auth_plugin,
1263 )
1264 .await;
1265 if let Some(current_auth) = context.client_manager.get_auth_context(client_id) {
1270 (usage_identity, deployment_id) =
1271 usage_identity_from_context(Some(¤t_auth));
1272 metering_key = usage_identity.metering_key.clone();
1273 if session_row_scopes(Some(¤t_auth)) != row_scopes_before {
1274 rescope_subscriptions(&context, &active_subscriptions).await;
1275 }
1276 }
1277 }
1278 }
1279 }
1280
1281 context
1282 .client_manager
1283 .cancel_all_client_subscriptions(client_id)
1284 .await;
1285 context.client_manager.remove_client(client_id);
1286 if let Some(rate_limiter) = context.client_manager.rate_limiter().cloned() {
1287 rate_limiter.remove_client_buckets(client_id).await;
1288 }
1289 for subscription in active_subscriptions.values() {
1290 context.metrics.subscription_removed(
1291 &subscription.view,
1292 subscription.metrics_metering_key.as_deref(),
1293 );
1294 emit_usage_event(
1295 &context.usage_emitter,
1296 WebSocketUsageEvent::SubscriptionRemoved {
1297 client_id: client_id.to_string(),
1298 deployment_id: deployment_id.clone(),
1299 identity: usage_identity.clone(),
1300 view_id: subscription.view.clone(),
1301 },
1302 );
1303 }
1304 let duration = connection_start.elapsed().as_secs_f64();
1305 context
1306 .metrics
1307 .connection_closed(duration, connection_metrics_metering_key.as_deref());
1308 emit_usage_event(
1309 &context.usage_emitter,
1310 WebSocketUsageEvent::ConnectionClosed {
1311 client_id: client_id.to_string(),
1312 deployment_id,
1313 identity: usage_identity,
1314 duration_secs: Some(duration),
1315 subscription_count: u32::try_from(active_subscriptions.len()).unwrap_or(u32::MAX),
1316 },
1317 );
1318 Ok(())
1319}
1320
1321struct ActiveSubscription {
1322 view: String,
1323 metrics_metering_key: Option<String>,
1326 subscription: Subscription,
1329}
1330
1331async fn rescope_subscriptions(
1343 context: &SubscriptionContext,
1344 active_subscriptions: &HashMap<String, ActiveSubscription>,
1345) {
1346 *context.row_scope_lock.write().await += 1;
1347 for (subscription_id, active) in active_subscriptions {
1348 if context
1349 .view_index
1350 .scope_field_for_view(&active.view)
1351 .is_none()
1352 {
1353 continue;
1354 }
1355 let cancel_token = context
1356 .client_manager
1357 .connection_token(context.client_id)
1358 .child_token();
1359 if !context
1360 .client_manager
1361 .replace_client_subscription_token(
1362 context.client_id,
1363 subscription_id,
1364 cancel_token.clone(),
1365 )
1366 .await
1367 {
1368 continue;
1369 }
1370 let replayable = context.journal.as_ref().is_some_and(|journal| {
1371 journal.is_enabled()
1372 && context
1373 .view_index
1374 .get_view(&active.view)
1375 .is_some_and(|spec| spec.mode == Mode::Append)
1376 });
1377 if replayable {
1378 let _ = send_control_frame(
1379 context,
1380 &SocketIssueMessage::subscription_rescoped(subscription_id.clone()),
1381 &active.view,
1382 );
1383 continue;
1384 }
1385 let mut subscription = active.subscription.clone();
1386 subscription.snapshot.enabled = true;
1387 let mut resnapshot = context.clone();
1388 resnapshot.resnapshot = true;
1389 match attach_client_to_bus(&resnapshot, subscription, cancel_token).await {
1390 Ok(()) => context.metrics.subscription_resnapshot(&active.view),
1391 Err(error) => {
1392 warn!(
1393 "Subscription {} could not be resnapshotted for new row scopes: {error:#}",
1394 subscription_id
1395 );
1396 let _ = send_control_frame(
1397 context,
1398 &SocketIssueMessage::subscription_rescoped(subscription_id.clone()),
1399 &active.view,
1400 );
1401 }
1402 }
1403 }
1404}
1405
1406async fn handle_unsubscribe(
1407 context: &SubscriptionContext,
1408 unsubscription: Unsubscription,
1409 active_subscriptions: &mut HashMap<String, ActiveSubscription>,
1410 deployment_id: &Option<String>,
1411 usage_identity: &UsageIdentity,
1412) {
1413 let subscription_id = unsubscription.subscription_id.clone();
1414 if let Err(message) = unsubscription.validate() {
1415 send_protocol_issue(
1416 context.client_id,
1417 &context.client_manager,
1418 &context.metrics,
1419 Some(subscription_id),
1420 "invalid-unsubscription",
1421 message,
1422 )
1423 .await;
1424 return;
1425 }
1426
1427 if !context
1428 .client_manager
1429 .remove_client_subscription(context.client_id, &subscription_id)
1430 .await
1431 {
1432 send_protocol_issue(
1433 context.client_id,
1434 &context.client_manager,
1435 &context.metrics,
1436 Some(subscription_id),
1437 "unknown-subscription-id",
1438 "subscriptionId is not active on this connection",
1439 )
1440 .await;
1441 return;
1442 }
1443
1444 let Some(subscription) = active_subscriptions.remove(&subscription_id) else {
1445 return;
1446 };
1447 let _ = send_control_frame(
1448 context,
1449 &UnsubscribedFrame::new(subscription_id),
1450 &subscription.view,
1451 );
1452 context.metrics.subscription_removed(
1453 &subscription.view,
1454 subscription.metrics_metering_key.as_deref(),
1455 );
1456 emit_usage_event(
1457 &context.usage_emitter,
1458 WebSocketUsageEvent::SubscriptionRemoved {
1459 client_id: context.client_id.to_string(),
1460 deployment_id: deployment_id.clone(),
1461 identity: usage_identity.clone(),
1462 view_id: subscription.view,
1463 },
1464 );
1465}
1466
1467fn extract_subscription_id(text: &str) -> Option<String> {
1468 serde_json::from_str::<Value>(text)
1469 .ok()?
1470 .get("subscriptionId")?
1471 .as_str()
1472 .map(str::to_string)
1473}
1474
1475struct SnapshotMetadata<'a> {
1476 subscription_id: &'a str,
1477 snapshot_id: &'a str,
1478 authoritative: bool,
1479 mode: Mode,
1480 view_id: &'a str,
1481 key: Option<&'a str>,
1482}
1483
1484#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1485enum SnapshotPurpose {
1486 Initial,
1487 Recovery,
1488}
1489
1490impl SnapshotPurpose {
1491 fn first(context: &SubscriptionContext) -> Self {
1494 if context.resnapshot {
1495 Self::Recovery
1496 } else {
1497 Self::Initial
1498 }
1499 }
1500
1501 fn authoritative(self, subscription: &Subscription) -> bool {
1502 match self {
1503 Self::Initial => subscription.query.after.is_none(),
1504 Self::Recovery => true,
1508 }
1509 }
1510}
1511
1512#[derive(Serialize)]
1519#[serde(rename_all = "camelCase")]
1520struct SnapshotBatch<'a> {
1521 protocol_version: u8,
1522 subscription_id: &'a str,
1523 snapshot_id: &'a str,
1524 authoritative: bool,
1525 mode: Mode,
1526 #[serde(rename = "entity")]
1527 export: &'a str,
1528 op: &'static str,
1529 #[serde(skip_serializing_if = "Option::is_none")]
1530 key: Option<&'a str>,
1531 data: SnapshotRows<'a>,
1532 complete: bool,
1533}
1534
1535struct SnapshotRows<'a> {
1536 rows: &'a [(String, SharedEntity)],
1537 wire_format: &'a WireFormat,
1538}
1539
1540impl Serialize for SnapshotRows<'_> {
1541 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1542 use serde::ser::SerializeSeq as _;
1543 let mut rows = serializer.serialize_seq(Some(self.rows.len()))?;
1544 for (key, entity) in self.rows {
1545 rows.serialize_element(&SnapshotRow {
1546 key,
1547 data: WireEntity {
1548 entity,
1549 wire_format: self.wire_format,
1550 },
1551 })?;
1552 }
1553 rows.end()
1554 }
1555}
1556
1557#[derive(Serialize)]
1558struct SnapshotRow<'a> {
1559 key: &'a str,
1560 data: WireEntity<'a>,
1561}
1562
1563fn create_snapshot_batches<'a>(
1566 rows: &'a [(String, SharedEntity)],
1567 wire_format: &'a WireFormat,
1568 metadata: SnapshotMetadata<'a>,
1569 batch_config: &SnapshotBatchConfig,
1570) -> Vec<SnapshotBatch<'a>> {
1571 let batch = |data: &'a [(String, SharedEntity)], complete: bool| SnapshotBatch {
1572 protocol_version: PROTOCOL_VERSION,
1573 subscription_id: metadata.subscription_id,
1574 snapshot_id: metadata.snapshot_id,
1575 authoritative: metadata.authoritative,
1576 mode: metadata.mode,
1577 export: metadata.view_id,
1578 op: "snapshot",
1579 key: metadata.key,
1580 data: SnapshotRows {
1581 rows: data,
1582 wire_format,
1583 },
1584 complete,
1585 };
1586 if rows.is_empty() {
1587 return vec![batch(&[], true)];
1588 }
1589
1590 let mut batches = Vec::new();
1591 let mut offset = 0;
1592 while offset < rows.len() {
1593 let configured_size = if offset == 0 {
1594 batch_config.initial_batch_size
1595 } else {
1596 batch_config.subsequent_batch_size
1597 };
1598 let end = (offset + configured_size.max(1)).min(rows.len());
1599 batches.push(batch(&rows[offset..end], end == rows.len()));
1600 offset = end;
1601 }
1602 batches
1603}
1604
1605async fn send_snapshot_batches(
1608 context: &SubscriptionContext,
1609 subscription: &Subscription,
1610 rows: &[(String, SharedEntity)],
1611 view_spec: &ViewSpec,
1612 purpose: SnapshotPurpose,
1613 batch_config: &SnapshotBatchConfig,
1614) -> Result<()> {
1615 let snapshot_id = Uuid::new_v4().to_string();
1616 let authoritative = purpose.authoritative(subscription);
1617 let mode = view_spec.mode;
1618 let frames = create_snapshot_batches(
1619 rows,
1620 &view_spec.wire_format,
1621 SnapshotMetadata {
1622 subscription_id: &subscription.subscription_id,
1623 snapshot_id: &snapshot_id,
1624 authoritative,
1625 mode,
1626 view_id: &subscription.query.view,
1627 key: subscription.query.key.as_deref(),
1628 },
1629 batch_config,
1630 );
1631
1632 for frame in frames {
1633 let rows = frame.data.rows.len() as u32;
1634 let json = serde_json::to_vec(&frame)?;
1635 let payload = maybe_compress(&json);
1636 let bytes = payload.as_bytes().len() as u64;
1637 let _held = hold_row_scopes_async(context).await?;
1638 context
1639 .client_manager
1640 .send_compressed_async(context.client_id, payload)
1641 .await
1642 .map_err(|error| anyhow::anyhow!("failed to send snapshot: {error}"))?;
1643 context.metrics.message_sent();
1644
1645 let auth_context = context.client_manager.get_auth_context(context.client_id);
1646 let (identity, deployment_id) = usage_identity_from_context(auth_context.as_ref());
1647 emit_usage_event(
1648 &context.usage_emitter,
1649 WebSocketUsageEvent::SnapshotSent {
1650 client_id: context.client_id.to_string(),
1651 deployment_id,
1652 identity,
1653 view_id: subscription.query.view.clone(),
1654 rows,
1655 messages: 1,
1656 bytes,
1657 },
1658 );
1659 }
1660 Ok(())
1661}
1662
1663fn extract_sort_config(view_spec: &ViewSpec) -> Option<SortConfig> {
1664 if let Some(sort) = view_spec
1665 .pipeline
1666 .as_ref()
1667 .and_then(|pipeline| pipeline.sort.as_ref())
1668 {
1669 return Some(SortConfig {
1670 field: sort.field_path.clone(),
1671 order: match sort.order {
1672 crate::materialized_view::SortOrder::Asc => SortOrder::Asc,
1673 crate::materialized_view::SortOrder::Desc => SortOrder::Desc,
1674 },
1675 });
1676 }
1677 (view_spec.mode == Mode::List).then(|| SortConfig {
1678 field: vec!["_seq".to_string()],
1679 order: SortOrder::Desc,
1680 })
1681}
1682
1683fn hold_row_scopes(
1687 context: &SubscriptionContext,
1688) -> Result<Option<tokio::sync::RwLockReadGuard<'_, u64>>> {
1689 match context.row_filter.as_ref() {
1690 None => Ok(None),
1691 Some(filter) => filter
1692 .try_hold()
1693 .map(Some)
1694 .ok_or_else(|| anyhow::anyhow!("the session's row scopes changed")),
1695 }
1696}
1697
1698async fn hold_row_scopes_async(
1700 context: &SubscriptionContext,
1701) -> Result<Option<tokio::sync::RwLockReadGuard<'_, u64>>> {
1702 match context.row_filter.as_ref() {
1703 None => Ok(None),
1704 Some(filter) => filter
1705 .hold()
1706 .await
1707 .map(Some)
1708 .ok_or_else(|| anyhow::anyhow!("the session's row scopes changed")),
1709 }
1710}
1711
1712fn send_control_frame<T: Serialize>(
1713 context: &SubscriptionContext,
1714 frame: &T,
1715 view_id: &str,
1716) -> Result<()> {
1717 let json = serde_json::to_vec(frame)?;
1718 let bytes = json.len();
1719 let _held = hold_row_scopes(context)?;
1720 context
1721 .client_manager
1722 .send_to_client(context.client_id, Arc::new(Bytes::from(json)))
1723 .map_err(|error| anyhow::anyhow!("failed to send control frame: {error}"))?;
1724 context.metrics.message_sent();
1725 emit_update_sent_for_client(
1726 &context.usage_emitter,
1727 &context.client_manager,
1728 context.client_id,
1729 view_id,
1730 bytes,
1731 );
1732 Ok(())
1733}
1734
1735fn send_subscribed_frame(
1736 context: &SubscriptionContext,
1737 subscription: &Subscription,
1738 view_spec: &ViewSpec,
1739) -> Result<()> {
1740 let frame = SubscribedFrame::new(
1741 subscription.subscription_id.clone(),
1742 subscription.query.clone(),
1743 view_spec.mode,
1744 extract_sort_config(view_spec),
1745 );
1746 send_control_frame(context, &frame, &subscription.query.view)
1747}
1748
1749fn enforce_snapshot_limit(context: &SubscriptionContext, rows: usize) -> Result<()> {
1750 context
1751 .client_manager
1752 .check_snapshot_allowed(context.client_id, u32::try_from(rows).unwrap_or(u32::MAX))
1753 .map_err(|deny| anyhow::anyhow!(deny.reason))
1754}
1755
1756async fn subscribe_state_then_snapshot<F, Fut, T>(
1757 bus_manager: &BusManager,
1758 view_id: &str,
1759 key: &str,
1760 snapshot: F,
1761) -> (watch::Receiver<StateUpdate>, u64, T)
1762where
1763 F: FnOnce() -> Fut,
1764 Fut: Future<Output = T>,
1765{
1766 let mut receiver = bus_manager.get_or_create_state_bus(view_id, key).await;
1767 let published = receiver.borrow_and_update().published;
1768 let snapshot = snapshot().await;
1769 (receiver, published, snapshot)
1770}
1771
1772async fn subscribe_list_then_snapshot<F, Fut, T>(
1773 bus_manager: &BusManager,
1774 view_id: &str,
1775 snapshot: F,
1776) -> (broadcast::Receiver<Arc<BusMessage>>, T)
1777where
1778 F: FnOnce() -> Fut,
1779 Fut: Future<Output = T>,
1780{
1781 let receiver = bus_manager.get_or_create_list_bus(view_id).await;
1782 let snapshot = snapshot().await;
1783 (receiver, snapshot)
1784}
1785
1786async fn attach_client_to_bus(
1787 context: &SubscriptionContext,
1788 mut subscription: Subscription,
1789 cancel_token: CancellationToken,
1790) -> Result<()> {
1791 let view_spec = context
1792 .view_index
1793 .get_view(&subscription.query.view)
1794 .cloned()
1795 .ok_or_else(|| anyhow::anyhow!("unknown view: {}", subscription.query.view))?;
1796
1797 let mut scoped_context;
1801 let context = match context.view_index.scope_field_for_view(&view_spec.id) {
1802 Some(field) => {
1803 let generation = *context.row_scope_lock.read().await;
1804 scoped_context = context.clone();
1805 scoped_context.row_filter = Some(RowFilter::new(
1806 field.clone(),
1807 session_row_scopes(
1808 context
1809 .client_manager
1810 .get_auth_context(context.client_id)
1811 .as_ref(),
1812 ),
1813 context.row_scope_lock.clone(),
1814 generation,
1815 ));
1816 &scoped_context
1817 }
1818 None => context,
1819 };
1820
1821 if view_spec.mode == Mode::State && !view_spec.is_derived() && subscription.query.key.is_none()
1822 {
1823 return Err(anyhow::anyhow!("state subscriptions require query.key"));
1824 }
1825 if view_spec.is_derived() && subscription.query.take.is_none() {
1826 subscription.query.take = view_spec
1827 .pipeline
1828 .as_ref()
1829 .and_then(|pipeline| pipeline.limit);
1830 }
1831
1832 let journal = context
1836 .journal
1837 .clone()
1838 .filter(|journal| journal.is_enabled() && view_spec.mode == Mode::Append);
1839 if let Some(journal) = journal {
1840 return attach_journal_subscription(
1841 context,
1842 subscription,
1843 view_spec,
1844 journal,
1845 cancel_token,
1846 )
1847 .await;
1848 }
1849
1850 if view_spec.mode == Mode::State && !view_spec.is_derived() {
1851 attach_state_subscription(context, subscription, view_spec, cancel_token).await
1852 } else {
1853 attach_collection_subscription(context, subscription, view_spec, cancel_token).await
1854 }
1855}
1856
1857async fn attach_state_subscription(
1858 context: &SubscriptionContext,
1859 subscription: Subscription,
1860 view_spec: ViewSpec,
1861 cancel_token: CancellationToken,
1862) -> Result<()> {
1863 let view_id = subscription.query.view.clone();
1864 let key = subscription.query.key.clone().unwrap_or_default();
1865 let query = subscription.query.clone();
1866 let cache = context.entity_cache.clone();
1867 let view_spec_for_snapshot = view_spec.clone();
1868 let row_filter = context.row_filter.clone();
1869 let (mut receiver, mut seen, initial) =
1870 subscribe_state_then_snapshot(&context.bus_manager, &view_id, &key, move || async move {
1871 load_query_entities(
1872 &cache,
1873 None,
1874 &view_spec_for_snapshot,
1875 &query,
1876 row_filter.as_ref(),
1877 false,
1878 )
1879 .await
1880 })
1881 .await;
1882
1883 let mut snapshot_entities = initial;
1884 if let Some(limit) = subscription.query.snapshot_limit {
1885 snapshot_entities.truncate(limit);
1886 }
1887 enforce_snapshot_limit(context, snapshot_entities.len())?;
1888 if !context.resnapshot {
1889 send_subscribed_frame(context, &subscription, &view_spec)?;
1890 }
1891 let delivered = subscription.snapshot.enabled && !snapshot_entities.is_empty();
1896 let synced = if delivered {
1899 snapshot_entities
1900 .first()
1901 .map(|(_, entity)| Synced::Entity(entity.clone()))
1902 } else {
1903 None
1904 };
1905 if subscription.snapshot.enabled {
1906 send_snapshot_batches(
1907 context,
1908 &subscription,
1909 &snapshot_entities,
1910 &view_spec,
1911 SnapshotPurpose::first(context),
1912 &context.entity_cache.snapshot_config(),
1913 )
1914 .await?;
1915 }
1916
1917 let task_context = context.clone();
1918 let subscription_id = subscription.subscription_id.clone();
1919 let query = subscription.query.clone();
1920 let view_spec_task = view_spec.clone();
1921 let span_view = view_id.clone();
1922 let span_key = key.clone();
1923 tokio::spawn(
1924 async move {
1925 let mut member = delivered;
1927 let mut synced = synced;
1933 let mut touched = Value::Null;
1934 let mut behind = false;
1939 let mut replaced = false;
1944 loop {
1945 tokio::select! {
1946 _ = cancel_token.cancelled() => break,
1947 changed = receiver.changed() => {
1948 if changed.is_err() {
1949 break;
1950 }
1951 let (payload, published, replaced_at, row_scope) = {
1952 let update = receiver.borrow_and_update();
1953 (
1954 update.payload.clone(),
1955 update.published,
1956 update.replaced,
1957 update.row_scope.clone(),
1958 )
1959 };
1960 let in_scope = frame_in_row_scopes(
1965 task_context.row_filter.as_ref(),
1966 row_scope.as_ref(),
1967 &payload,
1968 );
1969 behind |= published > seen + 1;
1974 replaced |= behind && replaced_at > seen;
1975 seen = published;
1976 let metadata = source_frame_metadata(&payload);
1977 if metadata.op == "delete" {
1978 if !task_context.entity_cache
1979 .deletion_is_current(&query.view, &key)
1980 .await
1981 {
1982 behind = true;
1983 replaced = true;
1984 continue;
1985 }
1986 if member && send_membership_frame(
1987 &task_context,
1988 &subscription_id,
1989 &view_spec_task,
1990 "delete",
1991 &key,
1992 Value::Null,
1993 metadata.seq,
1994 ).is_err() {
1995 break;
1996 }
1997 member = false;
1998 behind = false;
1999 replaced = false;
2000 synced = None;
2001 touched = Value::Null;
2002 continue;
2003 }
2004
2005 let Some(cached) = task_context
2016 .entity_cache
2017 .get_shared(&view_spec_task.id, &key)
2018 .await
2019 else {
2020 if member
2023 && task_context.row_filter.as_ref().is_some_and(|filter| {
2024 source_frame_data(&payload)
2025 .is_some_and(|data| filter.denies_frame_data(&data))
2026 })
2027 {
2028 member = false;
2029 synced = None;
2030 touched = Value::Null;
2031 if send_membership_frame(
2032 &task_context,
2033 &subscription_id,
2034 &view_spec_task,
2035 "remove",
2036 &key,
2037 Value::Null,
2038 metadata.seq,
2039 ).is_err() {
2040 break;
2041 }
2042 continue;
2043 }
2044 if member && !in_scope {
2049 behind = true;
2050 continue;
2051 }
2052 if member && !replaced {
2055 record_forwarded(&payload, &metadata.op, &mut synced, &mut touched);
2056 if send_scoped_source_payload(
2057 &task_context,
2058 &subscription_id,
2059 &query.view,
2060 payload,
2061 ).is_err() {
2062 break;
2063 }
2064 }
2065 continue;
2066 };
2067 let selected = select_query_entities(
2068 vec![(key.clone(), cached)],
2069 &query,
2070 task_context.row_filter.as_ref(),
2071 true,
2072 false,
2073 );
2074 let is_member = !selected.is_empty();
2075 let caught_up = !std::mem::take(&mut behind) && in_scope;
2078 let replace = std::mem::take(&mut replaced);
2079 let result = match (member, selected.into_iter().next()) {
2080 (true, Some(_)) if caught_up => {
2081 record_forwarded(&payload, &metadata.op, &mut synced, &mut touched);
2082 send_scoped_source_payload(
2083 &task_context,
2084 &subscription_id,
2085 &query.view,
2086 payload,
2087 )
2088 }
2089 (true, Some((entity_key, data))) => {
2103 let wire_format = &view_spec_task.wire_format;
2104 let base = synced
2105 .as_ref()
2106 .filter(|_| !replace)
2107 .and_then(|synced| synced.wire(wire_format));
2108 let result = match base {
2109 Some(base) => match changed_fields(
2110 &base,
2111 &wire_value(&data, wire_format),
2112 &touched,
2113 ) {
2114 Some(body) => send_membership_frame(
2115 &task_context,
2116 &subscription_id,
2117 &view_spec_task,
2118 "patch",
2119 &entity_key,
2120 body,
2121 None,
2122 ),
2123 None => Ok(()),
2124 },
2125 None => send_entity_frame(
2128 &task_context,
2129 &subscription_id,
2130 &view_spec_task,
2131 if replace { "upsert" } else { "patch" },
2132 &entity_key,
2133 &data,
2134 None,
2135 ),
2136 };
2137 synced = Some(Synced::Entity(data));
2138 touched = Value::Null;
2139 result
2140 }
2141 (false, Some((entity_key, data))) => {
2142 let result = send_entity_frame(
2143 &task_context,
2144 &subscription_id,
2145 &view_spec_task,
2146 "upsert",
2147 &entity_key,
2148 &data,
2149 metadata.seq.as_deref(),
2150 );
2151 synced = Some(Synced::Entity(data));
2152 touched = Value::Null;
2153 result
2154 }
2155 (true, None) => {
2156 synced = None;
2157 touched = Value::Null;
2158 send_membership_frame(
2159 &task_context,
2160 &subscription_id,
2161 &view_spec_task,
2162 "remove",
2163 &key,
2164 Value::Null,
2165 metadata.seq,
2166 )
2167 }
2168 (false, None) => Ok(()),
2169 };
2170 if result.is_err() {
2171 break;
2172 }
2173 member = is_member;
2174 }
2175 }
2176 }
2177 }
2178 .instrument(info_span!("ws.subscribe.state", client_id = %context.client_id, view = %span_view, key = %span_key)),
2179 );
2180 Ok(())
2181}
2182
2183async fn attach_collection_subscription(
2184 context: &SubscriptionContext,
2185 subscription: Subscription,
2186 view_spec: ViewSpec,
2187 cancel_token: CancellationToken,
2188) -> Result<()> {
2189 let view_id = subscription.query.view.clone();
2190 let source_view_id = view_spec
2191 .source_view
2192 .clone()
2193 .unwrap_or_else(|| view_id.clone());
2194 let (mut receiver, initial_membership) = subscribe_collection_then_snapshot(
2195 context,
2196 &source_view_id,
2197 &view_spec,
2198 &subscription.query,
2199 )
2200 .await;
2201
2202 let mut snapshot_entities = initial_membership.clone();
2203 if let Some(limit) = subscription.query.snapshot_limit {
2204 snapshot_entities.truncate(limit);
2205 }
2206 enforce_snapshot_limit(context, snapshot_entities.len())?;
2207 if !context.resnapshot {
2208 send_subscribed_frame(context, &subscription, &view_spec)?;
2209 }
2210 let delivered_rows = if subscription.snapshot.enabled {
2215 snapshot_entities.len()
2216 } else {
2217 0
2218 };
2219 let mut undelivered: HashSet<String> = initial_membership
2220 .iter()
2221 .skip(delivered_rows)
2222 .map(|(key, _)| key.clone())
2223 .collect();
2224 let mut sent_whole = SentWhole::default();
2225 if subscription.snapshot.enabled {
2226 send_snapshot_batches(
2227 context,
2228 &subscription,
2229 &snapshot_entities,
2230 &view_spec,
2231 SnapshotPurpose::first(context),
2232 &context.entity_cache.snapshot_config(),
2233 )
2234 .await?;
2235 }
2236
2237 let task_context = context.clone();
2238 let task_subscription = subscription.clone();
2239 let subscription_id = subscription.subscription_id.clone();
2240 let query = subscription.query.clone();
2241 let view_spec_task = view_spec.clone();
2242 let span_view = view_id.clone();
2243 tokio::spawn(
2244 async move {
2245 let mut current = initial_membership;
2246 let coalesce_ms = (view_spec_task.mode == Mode::List)
2250 .then(|| {
2251 view_spec_task
2252 .delivery
2253 .coalesce_ms
2254 .or(task_context.delivery.collection_coalesce_ms)
2255 .filter(|milliseconds| *milliseconds > 0)
2256 })
2257 .flatten();
2258 let mut flush_interval = coalesce_ms.map(|milliseconds| {
2259 let period = Duration::from_millis(milliseconds);
2260 let mut interval = tokio::time::interval_at(
2261 tokio::time::Instant::now() + period,
2262 period,
2263 );
2264 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
2265 interval
2266 });
2267 let mut pending = HashMap::<String, Arc<BusMessage>>::new();
2268 let mut pending_updates = 0_u64;
2269
2270 loop {
2271 tokio::select! {
2272 _ = cancel_token.cancelled() => break,
2273 _ = async {
2274 flush_interval
2275 .as_mut()
2276 .expect("coalescing interval is guarded")
2277 .tick()
2278 .await;
2279 }, if flush_interval.is_some() => {
2280 if pending.is_empty() {
2281 continue;
2282 }
2283 let sorted_caches = view_spec_task
2284 .is_derived()
2285 .then(|| task_context.view_index.sorted_caches());
2286 let next = load_query_entities(
2287 &task_context.entity_cache,
2288 sorted_caches,
2289 &view_spec_task,
2290 &query,
2291 task_context.row_filter.as_ref(),
2292 false,
2293 ).await;
2294 if emit_coalesced_collection_delta(
2295 &task_context,
2296 &subscription_id,
2297 &view_spec_task,
2298 ¤t,
2299 &next,
2300 &pending,
2301 &mut undelivered,
2302 ).is_err() {
2303 task_context.metrics.delivery_stopped(&view_id, "send-failed");
2304 break;
2305 }
2306 task_context
2307 .metrics
2308 .collection_coalesced(&view_id, pending_updates);
2309 current = next;
2310 pending.clear();
2311 pending_updates = 0;
2312 }
2313 received = receiver.recv() => {
2314 let envelope = match received {
2315 Ok(envelope) => envelope,
2316 Err(broadcast::error::RecvError::Lagged(skipped)) => {
2317 task_context.metrics.subscription_lagged(&view_id, skipped);
2318 warn!(
2319 "Subscription {} lagged by {} updates",
2320 subscription_id, skipped
2321 );
2322 if view_spec_task.mode == Mode::Append {
2323 let _ = send_control_frame(
2324 &task_context,
2325 &SocketIssueMessage::append_subscription_lagged(
2326 subscription_id.clone(),
2327 skipped,
2328 ),
2329 &view_id,
2330 );
2331 task_context.metrics.delivery_stopped(&view_id, "append-lagged-without-replay");
2332 break;
2333 }
2334 if !task_subscription.snapshot.enabled {
2335 let _ = send_control_frame(
2336 &task_context,
2337 &SocketIssueMessage::subscription_lagged(
2338 subscription_id.clone(),
2339 skipped,
2340 ),
2341 &view_id,
2342 );
2343 task_context.metrics.delivery_stopped(&view_id, "lagged-without-snapshot");
2344 break;
2345 }
2346 info!(
2347 "Subscription {} is recovering from an authoritative snapshot",
2348 subscription_id
2349 );
2350 match recover_collection_subscription(
2351 &task_context,
2352 &task_subscription,
2353 &view_spec_task,
2354 &source_view_id,
2355 ).await {
2356 Ok((next_receiver, recovered)) => {
2357 receiver = next_receiver;
2358 current = recovered;
2359 undelivered.clear();
2362 sent_whole.clear();
2363 pending.clear();
2364 pending_updates = 0;
2365 task_context.metrics.subscription_resnapshot(&view_id);
2366 continue;
2367 }
2368 Err(error) => {
2369 warn!(
2370 "Subscription {} failed to recover from lag: {error:#}",
2371 subscription_id
2372 );
2373 task_context.metrics.delivery_stopped(&view_id, "resnapshot-failed");
2374 break;
2375 }
2376 }
2377 }
2378 Err(broadcast::error::RecvError::Closed) => break,
2379 };
2380
2381 apply_collection_source_event(
2382 &task_context,
2383 &source_view_id,
2384 &view_spec_task,
2385 &query,
2386 &envelope,
2387 ).await;
2388
2389 if flush_interval.is_some() {
2390 pending_updates = pending_updates.saturating_add(1);
2391 pending.insert(envelope.key.clone(), envelope);
2392 continue;
2393 }
2394
2395 let metadata = source_frame_metadata(&envelope.payload);
2396 let sorted_caches = view_spec_task
2397 .is_derived()
2398 .then(|| task_context.view_index.sorted_caches());
2399 let next = load_query_entities(
2400 &task_context.entity_cache,
2401 sorted_caches,
2402 &view_spec_task,
2403 &query,
2404 task_context.row_filter.as_ref(),
2405 false,
2406 ).await;
2407 if emit_collection_delta(
2408 &task_context,
2409 &subscription_id,
2410 &view_spec_task,
2411 ¤t,
2412 &next,
2413 &envelope,
2414 &metadata,
2415 &mut undelivered,
2416 &mut sent_whole,
2417 ).is_err() {
2418 task_context.metrics.delivery_stopped(&view_id, "send-failed");
2419 break;
2420 }
2421 current = next;
2422 }
2423 }
2424 }
2425 }
2426 .instrument(info_span!("ws.subscribe.collection", client_id = %context.client_id, view = %span_view)),
2427 );
2428 Ok(())
2429}
2430
2431async fn subscribe_collection_then_snapshot(
2432 context: &SubscriptionContext,
2433 source_view_id: &str,
2434 view_spec: &ViewSpec,
2435 query: &SubscriptionQuery,
2436) -> (
2437 broadcast::Receiver<Arc<BusMessage>>,
2438 Vec<(String, SharedEntity)>,
2439) {
2440 let cache = context.entity_cache.clone();
2441 let sorted_caches = view_spec
2442 .is_derived()
2443 .then(|| context.view_index.sorted_caches());
2444 let view_spec = view_spec.clone();
2445 let query = query.clone();
2446 let row_filter = context.row_filter.clone();
2447 subscribe_list_then_snapshot(&context.bus_manager, source_view_id, move || async move {
2448 load_query_entities(
2449 &cache,
2450 sorted_caches,
2451 &view_spec,
2452 &query,
2453 row_filter.as_ref(),
2454 false,
2455 )
2456 .await
2457 })
2458 .await
2459}
2460
2461async fn recover_collection_subscription(
2462 context: &SubscriptionContext,
2463 subscription: &Subscription,
2464 view_spec: &ViewSpec,
2465 source_view_id: &str,
2466) -> Result<(
2467 broadcast::Receiver<Arc<BusMessage>>,
2468 Vec<(String, SharedEntity)>,
2469)> {
2470 let (receiver, membership) =
2471 subscribe_collection_then_snapshot(context, source_view_id, view_spec, &subscription.query)
2472 .await;
2473 enforce_snapshot_limit(context, membership.len())?;
2478 send_snapshot_batches(
2479 context,
2480 subscription,
2481 &membership,
2482 view_spec,
2483 SnapshotPurpose::Recovery,
2484 &context.entity_cache.snapshot_config(),
2485 )
2486 .await?;
2487 Ok((receiver, membership))
2488}
2489
2490async fn apply_collection_source_event(
2491 context: &SubscriptionContext,
2492 source_view_id: &str,
2493 view_spec: &ViewSpec,
2494 query: &SubscriptionQuery,
2495 envelope: &BusMessage,
2496) {
2497 let metadata = source_frame_metadata(&envelope.payload);
2498 if metadata.op != "delete" {
2499 return;
2500 }
2501 if !context
2505 .entity_cache
2506 .deletion_is_current(source_view_id, &envelope.key)
2507 .await
2508 {
2509 return;
2510 }
2511 if view_spec.is_derived() {
2512 let caches = context.view_index.sorted_caches();
2513 let mut guard = caches.write().await;
2514 if let Some(cache) = guard.get_mut(&query.view) {
2515 cache.remove_key(&envelope.key);
2516 }
2517 }
2518}
2519
2520#[derive(Debug)]
2530pub(crate) struct RejectedSubscription(pub SocketIssueMessage);
2531
2532impl RejectedSubscription {
2533 fn into_error(issue: SocketIssueMessage) -> anyhow::Error {
2534 anyhow::Error::new(Self(issue))
2535 }
2536}
2537
2538impl std::fmt::Display for RejectedSubscription {
2539 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2540 formatter.write_str(&self.0.message)
2541 }
2542}
2543
2544impl std::error::Error for RejectedSubscription {}
2545
2546async fn attach_journal_subscription(
2564 context: &SubscriptionContext,
2565 subscription: Subscription,
2566 view_spec: ViewSpec,
2567 journal: Arc<crate::journal::EventJournal>,
2568 cancel_token: CancellationToken,
2569) -> Result<()> {
2570 let view_id = view_spec.id.clone();
2571 let subscription_id = subscription.subscription_id.clone();
2572
2573 if subscription.query.take.is_some()
2576 || subscription.query.skip.is_some()
2577 || subscription.query.snapshot_limit.is_some()
2578 {
2579 return Err(RejectedSubscription::into_error(SocketIssueMessage::protocol(
2580 Some(subscription_id),
2581 "invalid-subscription",
2582 format!(
2583 "take, skip and snapshotLimit are window options and do not apply to the replayable view {view_id}"
2584 ),
2585 )));
2586 }
2587
2588 let cursor = match subscription.query.after.as_deref() {
2591 Some(raw) => match crate::journal::Cursor::parse(raw) {
2592 Some(cursor) => Some(cursor),
2593 None => {
2594 return Err(RejectedSubscription::into_error(
2595 SocketIssueMessage::protocol(
2596 Some(subscription_id),
2597 "invalid-cursor",
2598 format!(
2599 "`after` must be an {{epoch}}:{{offset}} replay cursor for view {view_id}, got {raw:?}"
2600 ),
2601 ),
2602 ));
2603 }
2604 },
2605 None => None,
2606 };
2607
2608 let mut receiver = context.bus_manager.get_or_create_list_bus(&view_id).await;
2611
2612 let replayed = match journal.replay_after(&view_id, cursor.as_ref()).await {
2613 Ok(records) => records,
2614 Err(error) => {
2615 return Err(RejectedSubscription::into_error(
2616 SocketIssueMessage::replay_refused(Some(subscription_id), &error),
2617 ));
2618 }
2619 };
2620
2621 let frame = SubscribedFrame::new(
2622 subscription.subscription_id.clone(),
2623 subscription.query.clone(),
2624 view_spec.mode,
2625 extract_sort_config(&view_spec),
2626 )
2627 .with_replay_window(journal.window(&view_id).await);
2628 send_control_frame(context, &frame, &view_id)?;
2629
2630 let task_context = context.clone();
2635 let task_subscription_id = subscription.subscription_id.clone();
2636 let task_query = subscription.query.clone();
2637 let task_epoch = journal.epoch().await;
2638 let span_view = view_id.clone();
2639 tokio::spawn(
2640 async move {
2641 let mut last_sent = cursor.map(|cursor| cursor.offset);
2642 let mut pending: VecDeque<Arc<BusMessage>> = VecDeque::new();
2646 let mut lagged: Option<u64> = None;
2647
2648 for record in replayed {
2649 if cancel_token.is_cancelled() {
2650 return;
2651 }
2652 drain_available(&mut receiver, &mut pending, &mut lagged);
2653 if journal_record_matches(&task_query, &record)
2654 && frame_in_row_scopes(
2655 task_context.row_filter.as_ref(),
2656 record.row_scope.as_ref(),
2657 &record.payload,
2658 )
2659 && send_scoped_source_payload_async(
2660 &task_context,
2661 &task_subscription_id,
2662 &span_view,
2663 record.payload,
2664 )
2665 .await
2666 .is_err()
2667 {
2668 return;
2669 }
2670 last_sent = Some(record.offset);
2672 }
2673
2674 while let Some(envelope) = pending.pop_front() {
2677 if !forward_live_frame(
2678 &task_context,
2679 &task_subscription_id,
2680 &span_view,
2681 &task_query,
2682 &envelope,
2683 &mut last_sent,
2684 )
2685 .await
2686 {
2687 return;
2688 }
2689 }
2690
2691 if let Some(skipped) = lagged {
2692 report_replay_gap(
2693 &task_context,
2694 &task_subscription_id,
2695 &span_view,
2696 &task_epoch,
2697 skipped,
2698 last_sent,
2699 );
2700 return;
2701 }
2702
2703 loop {
2704 tokio::select! {
2705 _ = cancel_token.cancelled() => break,
2706 received = receiver.recv() => {
2707 let envelope = match received {
2708 Ok(envelope) => envelope,
2709 Err(broadcast::error::RecvError::Lagged(skipped)) => {
2727 report_replay_gap(
2728 &task_context,
2729 &task_subscription_id,
2730 &span_view,
2731 &task_epoch,
2732 skipped,
2733 last_sent,
2734 );
2735 break;
2736 }
2737 Err(broadcast::error::RecvError::Closed) => break,
2738 };
2739
2740 if !forward_live_frame(
2741 &task_context,
2742 &task_subscription_id,
2743 &span_view,
2744 &task_query,
2745 &envelope,
2746 &mut last_sent,
2747 )
2748 .await
2749 {
2750 break;
2751 }
2752 }
2753 }
2754 }
2755 }
2756 .instrument(info_span!(
2757 "ws.subscribe.replay",
2758 client_id = %context.client_id,
2759 view = %view_id
2760 )),
2761 );
2762 Ok(())
2763}
2764
2765fn drain_available(
2768 receiver: &mut broadcast::Receiver<Arc<BusMessage>>,
2769 pending: &mut VecDeque<Arc<BusMessage>>,
2770 lagged: &mut Option<u64>,
2771) {
2772 if lagged.is_some() {
2777 return;
2778 }
2779 const MAX_PENDING: usize = 8_192;
2785 loop {
2786 if pending.len() >= MAX_PENDING {
2787 return;
2788 }
2789 match receiver.try_recv() {
2790 Ok(envelope) => pending.push_back(envelope),
2791 Err(broadcast::error::TryRecvError::Empty)
2792 | Err(broadcast::error::TryRecvError::Closed) => return,
2793 Err(broadcast::error::TryRecvError::Lagged(skipped)) => {
2794 *lagged = Some(skipped);
2795 return;
2796 }
2797 }
2798 }
2799}
2800
2801fn already_delivered(offset: Option<u64>, last_sent: &mut Option<u64>) -> bool {
2807 let Some(offset) = offset else {
2808 return false;
2811 };
2812 if last_sent.is_some_and(|last| offset <= last) {
2813 return true;
2814 }
2815 *last_sent = Some(offset);
2816 false
2817}
2818
2819async fn forward_live_frame(
2823 context: &SubscriptionContext,
2824 subscription_id: &str,
2825 view_id: &str,
2826 query: &SubscriptionQuery,
2827 envelope: &Arc<BusMessage>,
2828 last_sent: &mut Option<u64>,
2829) -> bool {
2830 let metadata = source_frame_metadata(&envelope.payload);
2831 if already_delivered(metadata.offset, last_sent) {
2832 return true;
2833 }
2834 if !live_frame_matches(query, &envelope.key, &envelope.payload) {
2835 return true;
2836 }
2837 if !frame_in_row_scopes(
2838 context.row_filter.as_ref(),
2839 envelope.row_scope.as_ref(),
2840 &envelope.payload,
2841 ) {
2842 return true;
2843 }
2844 send_scoped_source_payload(context, subscription_id, view_id, envelope.payload.clone()).is_ok()
2845}
2846
2847fn frame_in_row_scopes(
2850 filter: Option<&RowFilter>,
2851 scope: Option<&crate::row_scope::RowScope>,
2852 payload: &[u8],
2853) -> bool {
2854 let Some(filter) = filter else {
2855 return true;
2856 };
2857 if let Some(scope) = scope {
2858 return filter.allows_scope(scope);
2859 }
2860 source_frame_data(payload)
2861 .as_ref()
2862 .and_then(|data| filter.field().scope_in(data))
2863 .is_some_and(|scope| filter.allows_scope(&scope))
2864}
2865
2866fn report_replay_gap(
2867 context: &SubscriptionContext,
2868 subscription_id: &str,
2869 view_id: &str,
2870 epoch: &crate::journal::JournalEpoch,
2871 skipped: u64,
2872 last_sent: Option<u64>,
2873) {
2874 warn!(
2875 "Replay subscription {} lagged past {} records; stopping with a recovery cursor",
2876 subscription_id, skipped
2877 );
2878 let recover_from = last_sent.map(|offset| crate::journal::Cursor {
2879 epoch: epoch.clone(),
2880 offset,
2881 });
2882 let _ = send_control_frame(
2883 context,
2884 &SocketIssueMessage::replay_lagged(
2885 Some(subscription_id.to_string()),
2886 skipped,
2887 recover_from,
2888 ),
2889 view_id,
2890 );
2891}
2892
2893fn journal_record_matches(
2896 query: &SubscriptionQuery,
2897 record: &crate::journal::JournalRecord,
2898) -> bool {
2899 live_frame_matches(query, &record.key, &record.payload)
2900}
2901
2902fn live_frame_matches(query: &SubscriptionQuery, key: &str, payload: &[u8]) -> bool {
2903 if !query.matches_key(key) {
2904 return false;
2905 }
2906 if query.partition.is_none() && query.filters.is_empty() {
2907 return true;
2908 }
2909 let Some(data) = source_frame_data(payload) else {
2910 return false;
2911 };
2912 let data = &data;
2913 if let Some(partition) = &query.partition {
2914 if value_at_dot_path(data, "_partition").as_deref()
2915 != Some(&Value::String(partition.clone()))
2916 {
2917 return false;
2918 }
2919 }
2920 query
2921 .filters
2922 .iter()
2923 .all(|(path, expected)| value_at_dot_path(data, path).as_deref() == Some(expected))
2924}
2925
2926async fn send_scoped_source_payload_async(
2929 context: &SubscriptionContext,
2930 subscription_id: &str,
2931 view_id: &str,
2932 payload: Arc<Bytes>,
2933) -> Result<()> {
2934 let json = SourceFields::scoped(&payload, subscription_id)?;
2935 let compressed = maybe_compress(&json);
2936 let bytes = compressed.as_bytes().len();
2937 let _held = hold_row_scopes_async(context).await?;
2938 context
2939 .client_manager
2940 .send_compressed_async(context.client_id, compressed)
2941 .await
2942 .map_err(|error| anyhow::anyhow!("failed to send replayed frame: {error}"))?;
2943 context.metrics.message_sent();
2944 emit_update_sent_for_client(
2945 &context.usage_emitter,
2946 &context.client_manager,
2947 context.client_id,
2948 view_id,
2949 bytes,
2950 );
2951 Ok(())
2952}
2953
2954#[derive(Default)]
2955struct SourceFrameMetadata {
2956 op: String,
2957 seq: Option<String>,
2958 offset: Option<u64>,
2959}
2960
2961fn source_frame_metadata(payload: &[u8]) -> SourceFrameMetadata {
2964 SourceFields::parse(payload)
2965 .map(|fields| SourceFrameMetadata {
2966 op: fields
2967 .value("op")
2968 .as_ref()
2969 .and_then(Value::as_str)
2970 .unwrap_or_default()
2971 .to_string(),
2972 seq: fields
2973 .value("seq")
2974 .as_ref()
2975 .and_then(Value::as_str)
2976 .map(str::to_string),
2977 offset: fields.value("offset").as_ref().and_then(Value::as_u64),
2978 })
2979 .unwrap_or_default()
2980}
2981
2982fn source_frame_data(payload: &[u8]) -> Option<Value> {
2984 SourceFields::parse(payload)?.value("data")
2985}
2986
2987enum Synced {
2993 Entity(SharedEntity),
2995 Frame(Arc<Bytes>),
2997}
2998
2999impl Synced {
3000 fn wire(&self, wire_format: &WireFormat) -> Option<Value> {
3002 match self {
3003 Synced::Entity(entity) => Some(wire_value(entity, wire_format)),
3004 Synced::Frame(frame) => source_frame_data(frame),
3005 }
3006 }
3007}
3008
3009fn record_forwarded(
3013 payload: &Arc<Bytes>,
3014 op: &str,
3015 synced: &mut Option<Synced>,
3016 touched: &mut Value,
3017) {
3018 if op == "upsert" {
3019 *synced = Some(Synced::Frame(payload.clone()));
3020 *touched = Value::Null;
3021 } else if let Some(data) = source_frame_data(payload) {
3022 mark_fields(touched, &data);
3023 }
3024}
3025
3026fn mark_fields(touched: &mut Value, patch: &Value) {
3034 let Value::Object(fields) = patch else {
3035 *touched = Value::Bool(true);
3036 return;
3037 };
3038 if touched.is_null() {
3039 *touched = Value::Object(serde_json::Map::new());
3040 }
3041 let Value::Object(marked) = touched else {
3042 return;
3044 };
3045 for (field, value) in fields {
3046 mark_fields(marked.entry(field.clone()).or_insert(Value::Null), value);
3047 }
3048}
3049
3050fn changed_fields(base: &Value, current: &Value, touched: &Value) -> Option<Value> {
3056 match (base, current, touched) {
3057 (_, _, Value::Bool(true)) => Some(current.clone()),
3058 (Value::Object(base), Value::Object(current), _) => {
3059 let marked = touched.as_object();
3060 let mut patch = serde_json::Map::new();
3061 for (field, value) in current {
3062 let touched = marked
3063 .and_then(|marked| marked.get(field))
3064 .unwrap_or(&Value::Null);
3065 match base.get(field) {
3066 Some(old) => {
3067 if let Some(changed) = changed_fields(old, value, touched) {
3068 patch.insert(field.clone(), changed);
3069 }
3070 }
3071 None => {
3072 patch.insert(field.clone(), value.clone());
3073 }
3074 }
3075 }
3076 for field in base
3077 .keys()
3078 .chain(marked.into_iter().flat_map(|marked| marked.keys()))
3079 {
3080 if !current.contains_key(field) {
3081 patch.insert(field.clone(), Value::Null);
3082 }
3083 }
3084 (!patch.is_empty()).then_some(Value::Object(patch))
3085 }
3086 _ => (base != current || !touched.is_null()).then(|| current.clone()),
3087 }
3088}
3089
3090fn send_scoped_source_payload(
3091 context: &SubscriptionContext,
3092 subscription_id: &str,
3093 view_id: &str,
3094 payload: Arc<Bytes>,
3095) -> Result<()> {
3096 let encoded = Arc::new(Bytes::from(SourceFields::scoped(
3097 &payload,
3098 subscription_id,
3099 )?));
3100 let bytes = encoded.len();
3101 let _held = hold_row_scopes(context)?;
3102 context
3103 .client_manager
3104 .send_to_client(context.client_id, encoded)
3105 .map_err(|error| anyhow::anyhow!("failed to send live frame: {error}"))?;
3106 context.metrics.message_sent();
3107 emit_update_sent_for_client(
3108 &context.usage_emitter,
3109 &context.client_manager,
3110 context.client_id,
3111 view_id,
3112 bytes,
3113 );
3114 Ok(())
3115}
3116
3117fn send_membership_frame(
3118 context: &SubscriptionContext,
3119 subscription_id: &str,
3120 view_spec: &ViewSpec,
3121 op: &str,
3122 key: &str,
3123 mut data: Value,
3124 seq: Option<String>,
3125) -> Result<()> {
3126 apply_wire_format(&mut data, &view_spec.wire_format);
3127 let frame = Frame::scoped(
3128 subscription_id,
3129 view_spec.mode,
3130 &view_spec.id,
3131 op,
3132 key,
3133 data,
3134 seq,
3135 );
3136 send_membership_json(context, view_spec, serde_json::to_vec(&frame)?)
3137}
3138
3139#[derive(Serialize)]
3142#[serde(rename_all = "camelCase")]
3143struct ScopedEntityFrame<'a> {
3144 protocol_version: u8,
3145 subscription_id: &'a str,
3146 mode: Mode,
3147 #[serde(rename = "entity")]
3148 export: &'a str,
3149 op: &'a str,
3150 key: &'a str,
3151 data: WireEntity<'a>,
3152 #[serde(skip_serializing_if = "Option::is_none")]
3153 seq: Option<&'a str>,
3154}
3155
3156fn send_entity_frame(
3158 context: &SubscriptionContext,
3159 subscription_id: &str,
3160 view_spec: &ViewSpec,
3161 op: &str,
3162 key: &str,
3163 entity: &SharedEntity,
3164 seq: Option<&str>,
3165) -> Result<()> {
3166 let frame = ScopedEntityFrame {
3167 protocol_version: PROTOCOL_VERSION,
3168 subscription_id,
3169 mode: view_spec.mode,
3170 export: &view_spec.id,
3171 op,
3172 key,
3173 data: WireEntity {
3174 entity,
3175 wire_format: &view_spec.wire_format,
3176 },
3177 seq,
3178 };
3179 send_membership_json(context, view_spec, serde_json::to_vec(&frame)?)
3180}
3181
3182fn send_membership_json(
3183 context: &SubscriptionContext,
3184 view_spec: &ViewSpec,
3185 json: Vec<u8>,
3186) -> Result<()> {
3187 let encoded = Arc::new(Bytes::from(json));
3188 let bytes = encoded.len();
3189 let _held = hold_row_scopes(context)?;
3190 context
3191 .client_manager
3192 .send_to_client(context.client_id, encoded)
3193 .map_err(|error| anyhow::anyhow!("failed to send membership frame: {error}"))?;
3194 context.metrics.message_sent();
3195 emit_update_sent_for_client(
3196 &context.usage_emitter,
3197 &context.client_manager,
3198 context.client_id,
3199 &view_spec.id,
3200 bytes,
3201 );
3202 Ok(())
3203}
3204
3205#[allow(clippy::too_many_arguments)]
3208fn emit_collection_delta(
3209 context: &SubscriptionContext,
3210 subscription_id: &str,
3211 view_spec: &ViewSpec,
3212 current: &[(String, SharedEntity)],
3213 next: &[(String, SharedEntity)],
3214 envelope: &BusMessage,
3215 metadata: &SourceFrameMetadata,
3216 undelivered: &mut HashSet<String>,
3217 sent_whole: &mut SentWhole,
3218) -> Result<()> {
3219 let current_keys: HashSet<&str> = current.iter().map(|(key, _)| key.as_str()).collect();
3220 let next_set: HashSet<&str> = next.iter().map(|(key, _)| key.as_str()).collect();
3221
3222 for key in current
3224 .iter()
3225 .map(|(key, _)| key.as_str())
3226 .filter(|key| !next_set.contains(key))
3227 {
3228 sent_whole.forget(key);
3229 if undelivered.remove(key) {
3231 continue;
3232 }
3233 let op = if metadata.op == "delete" && key == envelope.key {
3234 "delete"
3235 } else {
3236 "remove"
3237 };
3238 send_membership_frame(
3239 context,
3240 subscription_id,
3241 view_spec,
3242 op,
3243 key,
3244 Value::Null,
3245 metadata.seq.clone(),
3246 )?;
3247 }
3248
3249 for (key, data) in next.iter() {
3250 let in_window = current_keys.contains(key.as_str());
3251 let action = match member_action(
3252 in_window,
3253 in_window && !undelivered.contains(key),
3254 key == &envelope.key,
3255 view_spec.is_derived(),
3256 &metadata.op,
3257 ) {
3258 MemberAction::ForwardPatch
3262 if !frame_in_row_scopes(
3263 context.row_filter.as_ref(),
3264 envelope.row_scope.as_ref(),
3265 &envelope.payload,
3266 ) =>
3267 {
3268 MemberAction::Upsert
3269 }
3270 action => action,
3271 };
3272 match action {
3273 MemberAction::Skip => {}
3274 MemberAction::ForwardPatch => {
3275 if sent_whole.covers(key, || {
3279 source_frame_data(&envelope.payload)
3280 .and_then(|data| data.get("_version").cloned())
3281 }) {
3282 continue;
3283 }
3284 send_scoped_source_payload(
3285 context,
3286 subscription_id,
3287 &view_spec.id,
3288 envelope.payload.clone(),
3289 )?
3290 }
3291 MemberAction::Upsert => {
3292 let held = in_window && !undelivered.contains(key);
3293 if held && sent_whole.holds(key, data.version()) {
3296 continue;
3297 }
3298 sent_whole.sent(key, data.version());
3299 let seq = metadata
3300 .seq
3301 .as_deref()
3302 .or_else(|| data.field("_seq").and_then(Value::as_str));
3303 send_entity_frame(
3304 context,
3305 subscription_id,
3306 view_spec,
3307 "upsert",
3308 key,
3309 data,
3310 seq,
3311 )?;
3312 undelivered.remove(key);
3313 }
3314 }
3315 }
3316 Ok(())
3317}
3318
3319#[derive(Default)]
3334struct SentWhole(HashMap<String, Value>);
3335
3336impl SentWhole {
3337 const MAX_ENTRIES: usize = 4096;
3339
3340 fn sent(&mut self, key: &str, version: Option<&Value>) {
3341 match version {
3342 Some(version) => {
3343 if self.0.len() >= Self::MAX_ENTRIES && !self.0.contains_key(key) {
3344 self.0.clear();
3345 }
3346 self.0.insert(key.to_string(), version.clone());
3347 }
3348 None => {
3349 self.0.remove(key);
3350 }
3351 }
3352 }
3353
3354 fn forget(&mut self, key: &str) {
3355 self.0.remove(key);
3356 }
3357
3358 fn clear(&mut self) {
3359 self.0.clear();
3360 }
3361
3362 fn holds(&self, key: &str, version: Option<&Value>) -> bool {
3364 version.is_some() && self.0.get(key) == version
3365 }
3366
3367 fn covers(&mut self, key: &str, patch_version: impl FnOnce() -> Option<Value>) -> bool {
3372 let Some(sent) = self.0.get(key) else {
3373 return false;
3374 };
3375 let covered = match (
3376 version_parts(sent),
3377 patch_version().as_ref().and_then(version_parts),
3378 ) {
3379 (Some((sent_epoch, sent_counter)), Some((epoch, counter))) => {
3380 epoch == sent_epoch && counter <= sent_counter
3381 }
3382 _ => false,
3383 };
3384 if !covered {
3385 self.0.remove(key);
3386 }
3387 covered
3388 }
3389}
3390
3391fn version_parts(version: &Value) -> Option<(String, u64)> {
3393 let (epoch, counter) = version.as_str()?.rsplit_once(':')?;
3394 Some((epoch.to_string(), counter.parse().ok()?))
3395}
3396
3397fn emit_coalesced_collection_delta(
3402 context: &SubscriptionContext,
3403 subscription_id: &str,
3404 view_spec: &ViewSpec,
3405 current: &[(String, SharedEntity)],
3406 next: &[(String, SharedEntity)],
3407 pending: &HashMap<String, Arc<BusMessage>>,
3408 undelivered: &mut HashSet<String>,
3409) -> Result<()> {
3410 let changes = plan_coalesced_collection_delta(current, next, pending, undelivered);
3411 let next_keys: HashSet<&str> = next.iter().map(|(key, _)| key.as_str()).collect();
3414 undelivered.retain(|key| next_keys.contains(key.as_str()));
3415 for change in changes {
3416 if change.op == "upsert" {
3417 undelivered.remove(&change.key);
3418 }
3419 match &change.data {
3420 Some(entity) => send_entity_frame(
3421 context,
3422 subscription_id,
3423 view_spec,
3424 change.op,
3425 &change.key,
3426 entity,
3427 change.seq.as_deref(),
3428 )?,
3429 None => send_membership_frame(
3430 context,
3431 subscription_id,
3432 view_spec,
3433 change.op,
3434 &change.key,
3435 Value::Null,
3436 change.seq,
3437 )?,
3438 }
3439 }
3440 Ok(())
3441}
3442
3443#[derive(Debug, PartialEq)]
3444struct CollectionChange {
3445 op: &'static str,
3446 key: String,
3447 data: Option<SharedEntity>,
3449 seq: Option<String>,
3450}
3451
3452fn plan_coalesced_collection_delta(
3456 current: &[(String, SharedEntity)],
3457 next: &[(String, SharedEntity)],
3458 pending: &HashMap<String, Arc<BusMessage>>,
3459 undelivered: &HashSet<String>,
3460) -> Vec<CollectionChange> {
3461 let current_by_key: HashMap<&str, &SharedEntity> = current
3462 .iter()
3463 .map(|(key, data)| (key.as_str(), data))
3464 .collect();
3465 let next_keys: HashSet<&str> = next.iter().map(|(key, _)| key.as_str()).collect();
3466 let latest_seq = pending
3467 .values()
3468 .filter_map(|envelope| source_frame_metadata(&envelope.payload).seq)
3469 .max_by(|left, right| cmp_seq(left, right));
3470 let mut changes = Vec::new();
3471
3472 for (key, _) in current
3473 .iter()
3474 .filter(|(key, _)| !next_keys.contains(key.as_str()) && !undelivered.contains(key.as_str()))
3475 {
3476 let metadata = pending
3477 .get(key)
3478 .map(|envelope| source_frame_metadata(&envelope.payload));
3479 let op = if metadata
3480 .as_ref()
3481 .is_some_and(|metadata| metadata.op == "delete")
3482 {
3483 "delete"
3484 } else {
3485 "remove"
3486 };
3487 let seq = metadata
3488 .and_then(|metadata| metadata.seq)
3489 .or_else(|| latest_seq.clone());
3490 changes.push(CollectionChange {
3491 op,
3492 key: key.clone(),
3493 data: None,
3494 seq,
3495 });
3496 }
3497
3498 for (key, data) in next {
3499 let changed = current_by_key
3500 .get(key.as_str())
3501 .is_none_or(|previous| *previous != data);
3502 if !changed && !pending.contains_key(key) {
3503 continue;
3504 }
3505 let seq = data
3506 .field("_seq")
3507 .and_then(Value::as_str)
3508 .map(str::to_string)
3509 .or_else(|| {
3510 pending
3511 .get(key)
3512 .and_then(|envelope| source_frame_metadata(&envelope.payload).seq)
3513 })
3514 .or_else(|| latest_seq.clone());
3515 changes.push(CollectionChange {
3516 op: "upsert",
3517 key: key.clone(),
3518 data: Some(data.clone()),
3519 seq,
3520 });
3521 }
3522 changes
3523}
3524
3525#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3527enum MemberAction {
3528 Skip,
3531 ForwardPatch,
3533 Upsert,
3535}
3536
3537fn member_action(
3555 in_window: bool,
3556 held: bool,
3557 is_mutated_key: bool,
3558 is_derived: bool,
3559 op: &str,
3560) -> MemberAction {
3561 if !is_mutated_key {
3562 return if in_window {
3568 MemberAction::Skip
3569 } else {
3570 MemberAction::Upsert
3571 };
3572 }
3573 if held && !is_derived && op != "delete" {
3579 MemberAction::ForwardPatch
3580 } else {
3581 MemberAction::Upsert
3582 }
3583}
3584
3585fn wire_value(entity: &SharedEntity, wire_format: &WireFormat) -> Value {
3588 let mut value = entity.to_value();
3589 apply_wire_format(&mut value, wire_format);
3590 value
3591}
3592
3593async fn load_query_entities(
3594 entity_cache: &EntityCache,
3595 sorted_caches: Option<
3596 Arc<tokio::sync::RwLock<HashMap<String, crate::sorted_cache::SortedViewCache>>>,
3597 >,
3598 view_spec: &ViewSpec,
3599 query: &SubscriptionQuery,
3600 row_filter: Option<&RowFilter>,
3601 apply_snapshot_limit: bool,
3602) -> Vec<(String, SharedEntity)> {
3603 let mut query = query.clone();
3605 if let Some(limit) = view_spec
3606 .pipeline
3607 .as_ref()
3608 .and_then(|pipeline| pipeline.limit)
3609 {
3610 query.take = Some(query.take.unwrap_or(limit).min(limit));
3611 }
3612 if let Some(sorted_caches) = sorted_caches {
3613 let caches = sorted_caches.read().await;
3616 if let Some(cache) = caches.get(&view_spec.id) {
3617 let no_scopes = std::collections::BTreeSet::new();
3621 let rows: Box<dyn Iterator<Item = (&str, &SharedEntity)> + '_> = match row_filter {
3622 Some(filter) => Box::new(cache.iter_ordered_in_scopes(filter.scopes())),
3623 None if cache.is_scope_partitioned() => {
3624 Box::new(cache.iter_ordered_in_scopes(&no_scopes))
3625 }
3626 None => Box::new(cache.iter_ordered()),
3627 };
3628 return select_window(
3629 rows.filter(|(key, data)| query_matches_entity(&query, key, *data, row_filter))
3630 .map(|(key, data)| (key.to_string(), data.clone())),
3631 &query,
3632 apply_snapshot_limit,
3633 );
3634 }
3635 }
3636 let (entities, preordered) = if view_spec.is_derived() {
3637 let mut entities = entity_cache
3640 .get_all_shared(view_spec.source_view.as_deref().unwrap_or(&view_spec.id))
3641 .await;
3642 if let Some(filter) = view_spec
3643 .pipeline
3644 .as_ref()
3645 .and_then(|pipeline| pipeline.filter.as_ref())
3646 {
3647 entities.retain(|(_, data)| filter.matches(data));
3648 }
3649 (entities, false)
3650 } else if view_spec.mode == Mode::State {
3651 let entity = match query.key.as_deref() {
3652 Some(key) => entity_cache
3653 .get_shared(&view_spec.id, key)
3654 .await
3655 .map(|data| vec![(key.to_string(), data)])
3656 .unwrap_or_default(),
3657 None => vec![],
3658 };
3659 (entity, true)
3660 } else {
3661 (entity_cache.get_all_shared(&view_spec.id).await, false)
3662 };
3663 select_query_entities(
3664 entities,
3665 &query,
3666 row_filter,
3667 preordered,
3668 apply_snapshot_limit,
3669 )
3670}
3671
3672fn select_query_entities<E: EntityFields>(
3673 mut entities: Vec<(String, E)>,
3674 query: &SubscriptionQuery,
3675 row_filter: Option<&RowFilter>,
3676 preordered: bool,
3677 apply_snapshot_limit: bool,
3678) -> Vec<(String, E)> {
3679 entities.retain(|(key, data)| query_matches_entity(query, key, data, row_filter));
3680 if !preordered {
3681 entities.sort_by(|left, right| {
3682 let left_seq = left.1.field("_seq").and_then(Value::as_str).unwrap_or("");
3683 let right_seq = right.1.field("_seq").and_then(Value::as_str).unwrap_or("");
3684 let order = if query.after.is_some() {
3685 cmp_seq(left_seq, right_seq)
3686 } else {
3687 cmp_seq(right_seq, left_seq)
3688 };
3689 order.then_with(|| left.0.cmp(&right.0))
3690 });
3691 }
3692
3693 select_window(entities.into_iter(), query, apply_snapshot_limit)
3694}
3695
3696fn select_window<E>(
3699 rows: impl Iterator<Item = (String, E)>,
3700 query: &SubscriptionQuery,
3701 apply_snapshot_limit: bool,
3702) -> Vec<(String, E)> {
3703 let skip = query.skip.unwrap_or(0);
3704 let mut take = query.take.unwrap_or(usize::MAX);
3705 if apply_snapshot_limit {
3706 if let Some(limit) = query.snapshot_limit {
3707 take = take.min(limit);
3708 }
3709 }
3710 rows.skip(skip).take(take).collect()
3711}
3712
3713fn query_matches_entity<E: EntityFields + ?Sized>(
3718 query: &SubscriptionQuery,
3719 key: &str,
3720 data: &E,
3721 row_filter: Option<&RowFilter>,
3722) -> bool {
3723 if row_filter.is_some_and(|filter| !filter.allows(data)) {
3724 return false;
3725 }
3726 if !query.matches_key(key) {
3727 return false;
3728 }
3729 if let Some(partition) = &query.partition {
3730 if value_at_dot_path(data, "_partition").as_deref()
3731 != Some(&Value::String(partition.clone()))
3732 {
3733 return false;
3734 }
3735 }
3736 if let Some(after) = &query.after {
3737 let Some(seq) = data.field("_seq").and_then(Value::as_str) else {
3738 return false;
3739 };
3740 if cmp_seq(seq, after) != std::cmp::Ordering::Greater {
3741 return false;
3742 }
3743 }
3744 query
3745 .filters
3746 .iter()
3747 .all(|(path, expected)| value_at_dot_path(data, path).as_deref() == Some(expected))
3748}
3749
3750fn value_at_dot_path<'a, E: EntityFields + ?Sized>(
3751 entity: &'a E,
3752 path: &str,
3753) -> Option<std::borrow::Cow<'a, Value>> {
3754 lookup(entity, path.split('.'))
3755}
3756
3757#[cfg(test)]
3758mod tests {
3759 use super::*;
3760 use crate::cache::{EntityCacheConfig, PatchOrigin};
3761 use crate::view::{Delivery, Filters, Projection};
3762 use serde_json::json;
3763 use tokio::sync::oneshot;
3764
3765 #[derive(Default)]
3766 struct DelayedUsageEmitter {
3767 started: tokio::sync::Notify,
3768 release: tokio::sync::Notify,
3769 events: tokio::sync::Mutex<Vec<WebSocketUsageEvent>>,
3770 }
3771
3772 #[async_trait::async_trait]
3773 impl WebSocketUsageEmitter for DelayedUsageEmitter {
3774 async fn emit(&self, event: WebSocketUsageEvent) {
3775 self.started.notify_one();
3776 self.release.notified().await;
3777 self.events.lock().await.push(event);
3778 }
3779 }
3780
3781 #[tokio::test]
3782 async fn usage_emitter_handle_waits_for_spawned_emissions() {
3783 let emitter = Arc::new(DelayedUsageEmitter::default());
3784 let handle = UsageEmitterHandle::new(emitter.clone());
3785 handle.emit(WebSocketUsageEvent::ConnectionEstablished {
3786 client_id: "client-1".to_string(),
3787 remote_addr: "127.0.0.1:1234".to_string(),
3788 deployment_id: Some("1".to_string()),
3789 identity: UsageIdentity::default(),
3790 });
3791 emitter.started.notified().await;
3792
3793 let closing_handle = handle.clone();
3794 let closing = tokio::spawn(async move {
3795 closing_handle.close_and_wait().await;
3796 });
3797 tokio::task::yield_now().await;
3798 assert!(!closing.is_finished());
3799
3800 emitter.release.notify_one();
3801 closing.await.expect("usage task wait should finish");
3802 assert_eq!(emitter.events.lock().await.len(), 1);
3803 }
3804
3805 #[test]
3806 fn a_catch_up_carries_only_the_fields_that_changed() {
3807 let base = json!({
3808 "id": 7,
3809 "state": {"deployed": [1, 2, 3], "total": 6},
3810 "results": {"square": null},
3811 "gone": true,
3812 });
3813 let current = json!({
3814 "id": 7,
3815 "state": {"deployed": [1, 2, 4], "total": 6},
3816 "results": {"square": 3},
3817 "fresh": "x",
3818 });
3819 assert_eq!(
3820 changed_fields(&base, ¤t, &Value::Null),
3821 Some(json!({
3822 "state": {"deployed": [1, 2, 4]},
3824 "results": {"square": 3},
3825 "fresh": "x",
3826 "gone": null,
3828 }))
3829 );
3830 assert_eq!(changed_fields(¤t, ¤t, &Value::Null), None);
3831 assert_eq!(
3833 changed_fields(&json!(null), ¤t, &Value::Null),
3834 Some(current.clone())
3835 );
3836 }
3837
3838 #[test]
3839 fn a_catch_up_resends_the_fields_forwarded_patches_set() {
3840 let base = json!({"a": 1, "state": {"x": 1, "y": 1}, "list": [1]});
3841 let mut touched = Value::Null;
3842 mark_fields(&mut touched, &json!({"a": 2, "state": {"x": 2}}));
3843 mark_fields(&mut touched, &json!({"list": [2], "extra": true}));
3844 assert_eq!(
3845 touched,
3846 json!({"a": true, "state": {"x": true}, "list": true, "extra": true})
3847 );
3848 let current = json!({"a": 1, "state": {"x": 1, "y": 1}, "list": [1, 2]});
3852 assert_eq!(
3853 changed_fields(&base, ¤t, &Value::Null),
3854 Some(json!({"list": [1, 2]}))
3855 );
3856 assert_eq!(
3857 changed_fields(&base, ¤t, &touched),
3858 Some(json!({"a": 1, "state": {"x": 1}, "list": [1, 2], "extra": null}))
3859 );
3860 mark_fields(&mut touched, &json!(null));
3862 assert_eq!(
3863 changed_fields(&base, ¤t, &touched),
3864 Some(current.clone())
3865 );
3866 }
3867
3868 fn list_spec() -> ViewSpec {
3869 ViewSpec {
3870 id: "Thing/list".to_string(),
3871 export: "Thing".to_string(),
3872 mode: Mode::List,
3873 wire_format: Default::default(),
3874 projection: Projection::all(),
3875 filters: Filters::all(),
3876 delivery: Delivery::default(),
3877 pipeline: None,
3878 source_view: None,
3879 }
3880 }
3881
3882 fn plan_window(
3885 current_keys: &[&str],
3886 next_keys: &[&str],
3887 envelope_key: &str,
3888 is_derived: bool,
3889 op: &str,
3890 ) -> Vec<(String, MemberAction)> {
3891 plan_window_with_undelivered(current_keys, &[], next_keys, envelope_key, is_derived, op)
3892 }
3893
3894 fn plan_window_with_undelivered(
3896 current_keys: &[&str],
3897 undelivered: &[&str],
3898 next_keys: &[&str],
3899 envelope_key: &str,
3900 is_derived: bool,
3901 op: &str,
3902 ) -> Vec<(String, MemberAction)> {
3903 next_keys
3904 .iter()
3905 .map(|key| {
3906 let in_window = current_keys.contains(key);
3907 let held = in_window && !undelivered.contains(key);
3908 (
3909 (*key).to_string(),
3910 member_action(in_window, held, *key == envelope_key, is_derived, op),
3911 )
3912 })
3913 .collect()
3914 }
3915
3916 #[test]
3917 fn a_window_member_the_client_never_received_is_sent_whole() {
3918 let plan = plan_window_with_undelivered(
3922 &["4", "3", "2", "1"],
3923 &["2", "1"],
3924 &["1", "4", "3", "2"],
3925 "1",
3926 false,
3927 "patch",
3928 );
3929 assert_eq!(
3930 plan,
3931 vec![
3932 ("1".to_string(), MemberAction::Upsert),
3933 ("4".to_string(), MemberAction::Skip),
3934 ("3".to_string(), MemberAction::Skip),
3935 ("2".to_string(), MemberAction::Skip),
3938 ]
3939 );
3940 }
3941
3942 #[test]
3943 fn a_held_member_still_rides_its_patch_after_a_truncated_snapshot() {
3944 let plan = plan_window_with_undelivered(
3945 &["4", "3", "2", "1"],
3946 &["2", "1"],
3947 &["4", "3", "2", "1"],
3948 "4",
3949 false,
3950 "patch",
3951 );
3952 assert_eq!(plan[0], ("4".to_string(), MemberAction::ForwardPatch));
3953 }
3954
3955 #[test]
3956 fn coalescing_sends_no_remove_for_a_key_the_client_never_received() {
3957 let current = vec![
3958 ("held".to_string(), json!({"count": 1, "_seq": "10:000001"})),
3959 (
3960 "unsent".to_string(),
3961 json!({"count": 1, "_seq": "10:000001"}),
3962 ),
3963 ("gone".to_string(), json!({"count": 1, "_seq": "10:000001"})),
3964 ];
3965 let next = vec![
3966 ("held".to_string(), json!({"count": 1, "_seq": "10:000001"})),
3967 (
3968 "unsent".to_string(),
3969 json!({"count": 2, "_seq": "10:000003"}),
3970 ),
3971 ];
3972 let pending = HashMap::from([
3973 (
3974 "unsent".to_string(),
3975 list_message("unsent", "patch", "10:000003"),
3976 ),
3977 (
3978 "gone".to_string(),
3979 list_message("gone", "delete", "10:000002"),
3980 ),
3981 ]);
3982 let undelivered = HashSet::from(["unsent".to_string(), "gone".to_string()]);
3983
3984 assert_eq!(
3985 plan_coalesced_collection_delta(
3986 &shared(¤t),
3987 &shared(&next),
3988 &pending,
3989 &undelivered
3990 ),
3991 vec![CollectionChange {
3992 op: "upsert",
3993 key: "unsent".to_string(),
3994 data: Some(SharedEntity::new(json!({"count": 2, "_seq": "10:000003"}))),
3995 seq: Some("10:000003".to_string()),
3996 }]
3997 );
3998 }
3999
4000 #[test]
4001 fn a_reordered_window_sends_only_the_mutated_entity() {
4002 let current = ["4", "3", "2", "1"];
4006 let next = ["1", "4", "3", "2"];
4007 let plan = plan_window(¤t, &next, "1", false, "patch");
4008
4009 assert_eq!(
4010 plan,
4011 vec![
4012 ("1".to_string(), MemberAction::ForwardPatch),
4013 ("4".to_string(), MemberAction::Skip),
4014 ("3".to_string(), MemberAction::Skip),
4015 ("2".to_string(), MemberAction::Skip),
4016 ]
4017 );
4018 }
4019
4020 #[test]
4021 fn a_patch_the_whole_entity_already_carried_is_not_sent_again() {
4022 let mut sent = SentWhole::default();
4023 sent.sent("k", Some(&json!("e1:5")));
4024 assert!(sent.covers("k", || Some(json!("e1:4"))));
4026 assert!(sent.covers("k", || Some(json!("e1:5"))));
4027 assert!(sent.holds("k", Some(&json!("e1:5"))));
4028 assert!(!sent.covers("k", || Some(json!("e1:6"))));
4030 assert!(!sent.covers("k", || Some(json!("e1:3"))));
4031 sent.sent("k", Some(&json!("e1:5")));
4033 assert!(!sent.covers("k", || Some(json!("e2:1"))));
4034 sent.sent("k", Some(&json!("e1:5")));
4035 assert!(!sent.covers("k", || None));
4036 assert!(!sent.covers("other", || panic!("read only for keys sent whole")));
4038 sent.sent("k", Some(&json!("e1:5")));
4040 sent.sent("k", None);
4041 assert!(!sent.holds("k", None));
4042 assert!(!sent.covers("k", || Some(json!("e1:1"))));
4043 }
4044
4045 #[test]
4046 fn a_key_entering_the_window_gets_the_whole_entity() {
4047 let plan = plan_window(&["4", "3"], &["5", "4", "3"], "5", false, "patch");
4049 assert_eq!(
4050 plan,
4051 vec![
4052 ("5".to_string(), MemberAction::Upsert),
4053 ("4".to_string(), MemberAction::Skip),
4054 ("3".to_string(), MemberAction::Skip),
4055 ]
4056 );
4057 }
4058
4059 #[test]
4060 fn derived_views_still_send_whole_entities() {
4061 let plan = plan_window(&["1", "2"], &["1", "2"], "1", true, "patch");
4064 assert_eq!(
4065 plan,
4066 vec![
4067 ("1".to_string(), MemberAction::Upsert),
4068 ("2".to_string(), MemberAction::Skip),
4069 ]
4070 );
4071 }
4072
4073 #[test]
4074 fn a_delete_envelope_never_forwards_a_patch() {
4075 let plan = plan_window(&["1", "2"], &["1", "2"], "1", false, "delete");
4077 assert_eq!(
4078 plan,
4079 vec![
4080 ("1".to_string(), MemberAction::Upsert),
4081 ("2".to_string(), MemberAction::Skip),
4082 ]
4083 );
4084 }
4085
4086 #[test]
4087 fn an_unchanged_window_sends_one_frame_not_a_broadcast() {
4088 let keys: Vec<String> = (0..500).map(|index| index.to_string()).collect();
4091 let refs: Vec<&str> = keys.iter().map(String::as_str).collect();
4092 let plan = plan_window(&refs, &refs, "250", false, "patch");
4093
4094 let sent = plan
4095 .iter()
4096 .filter(|(_, action)| *action != MemberAction::Skip)
4097 .count();
4098 assert_eq!(sent, 1);
4099 assert_eq!(plan[250].1, MemberAction::ForwardPatch);
4100 }
4101
4102 fn shared(rows: &[(String, Value)]) -> Vec<(String, SharedEntity)> {
4103 rows.iter()
4104 .map(|(key, data)| (key.clone(), SharedEntity::new(data.clone())))
4105 .collect()
4106 }
4107
4108 fn list_message(key: &str, op: &str, seq: &str) -> Arc<BusMessage> {
4109 Arc::new(BusMessage {
4110 row_scope: None,
4111 key: key.to_string(),
4112 entity: "Thing/list".to_string(),
4113 payload: Arc::new(Bytes::from(
4114 serde_json::to_vec(&json!({
4115 "entity": "Thing/list",
4116 "op": op,
4117 "key": key,
4118 "seq": seq,
4119 "data": {},
4120 }))
4121 .unwrap(),
4122 )),
4123 })
4124 }
4125
4126 #[test]
4127 fn coalescing_emits_only_the_final_full_state_per_changed_key() {
4128 let current = vec![
4129 ("a".to_string(), json!({"count": 1, "_seq": "10:000001"})),
4130 ("b".to_string(), json!({"count": 1, "_seq": "10:000001"})),
4131 (
4132 "stable".to_string(),
4133 json!({"count": 1, "_seq": "10:000001"}),
4134 ),
4135 ];
4136 let next = vec![
4137 ("a".to_string(), json!({"count": 3, "_seq": "10:000004"})),
4138 ("c".to_string(), json!({"count": 1, "_seq": "10:000003"})),
4139 (
4140 "stable".to_string(),
4141 json!({"count": 1, "_seq": "10:000001"}),
4142 ),
4143 ];
4144 let pending = HashMap::from([
4145 ("a".to_string(), list_message("a", "patch", "10:000004")),
4146 ("b".to_string(), list_message("b", "delete", "10:000002")),
4147 ("c".to_string(), list_message("c", "patch", "10:000003")),
4148 ]);
4149
4150 assert_eq!(
4151 plan_coalesced_collection_delta(
4152 &shared(¤t),
4153 &shared(&next),
4154 &pending,
4155 &HashSet::new()
4156 ),
4157 vec![
4158 CollectionChange {
4159 op: "delete",
4160 key: "b".to_string(),
4161 data: None,
4162 seq: Some("10:000002".to_string()),
4163 },
4164 CollectionChange {
4165 op: "upsert",
4166 key: "a".to_string(),
4167 data: Some(SharedEntity::new(json!({"count": 3, "_seq": "10:000004"}))),
4168 seq: Some("10:000004".to_string()),
4169 },
4170 CollectionChange {
4171 op: "upsert",
4172 key: "c".to_string(),
4173 data: Some(SharedEntity::new(json!({"count": 1, "_seq": "10:000003"}))),
4174 seq: Some("10:000003".to_string()),
4175 },
4176 ]
4177 );
4178 }
4179
4180 #[test]
4181 fn a_lagged_snapshotless_subscription_gets_a_fatal_retryable_error() {
4182 let issue = SocketIssueMessage::subscription_lagged("balances".to_string(), 42);
4183 assert_eq!(issue.code, "subscription-lagged");
4184 assert!(issue.retryable);
4185 assert!(issue.fatal);
4186 assert!(issue.message.contains("42"));
4187 assert!(issue
4188 .suggested_action
4189 .unwrap()
4190 .contains("snapshots enabled"));
4191
4192 let append = SocketIssueMessage::append_subscription_lagged("trades".to_string(), 9);
4193 assert!(append.fatal);
4194 assert!(append.suggested_action.unwrap().contains("retained replay"));
4195 }
4196
4197 #[test]
4198 fn snapshot_batches_share_identity_and_completion() {
4199 let entities = ["one", "two", "three"]
4200 .map(|key| (key.to_string(), SharedEntity::new(json!({"key": key}))));
4201 let wire_format = WireFormat::default();
4202 let batches = create_snapshot_batches(
4203 &entities,
4204 &wire_format,
4205 SnapshotMetadata {
4206 subscription_id: "sub-1",
4207 snapshot_id: "snapshot-1",
4208 authoritative: true,
4209 mode: Mode::List,
4210 view_id: "Thing/list",
4211 key: None,
4212 },
4213 &SnapshotBatchConfig {
4214 initial_batch_size: 2,
4215 subsequent_batch_size: 1,
4216 },
4217 );
4218 assert_eq!(batches.len(), 2);
4219 assert!(batches.iter().all(|batch| batch.subscription_id == "sub-1"));
4220 assert!(batches
4221 .iter()
4222 .all(|batch| batch.snapshot_id == "snapshot-1"));
4223 assert!(!batches[0].complete);
4224 assert!(batches[1].complete);
4225 assert!(batches.iter().all(|batch| batch.authoritative));
4226 }
4227
4228 #[test]
4229 fn empty_incremental_snapshot_is_explicitly_non_authoritative() {
4230 let wire_format = WireFormat::default();
4231 let batches = create_snapshot_batches(
4232 &[],
4233 &wire_format,
4234 SnapshotMetadata {
4235 subscription_id: "sub-1",
4236 snapshot_id: "snapshot-1",
4237 authoritative: false,
4238 mode: Mode::State,
4239 view_id: "Thing/state",
4240 key: Some("missing"),
4241 },
4242 &SnapshotBatchConfig {
4243 initial_batch_size: 1,
4244 subsequent_batch_size: 1,
4245 },
4246 );
4247 assert_eq!(batches.len(), 1);
4248 assert!(!batches[0].authoritative);
4249 assert!(batches[0].complete);
4250 assert_eq!(batches[0].key, Some("missing"));
4251 }
4252
4253 fn wide_ints() -> WireFormat {
4254 WireFormat {
4255 wide_int_paths: vec![
4256 vec!["amount".to_string()],
4257 vec!["trades".to_string(), "price".to_string()],
4258 ],
4259 }
4260 }
4261
4262 fn sample_rows() -> Vec<(String, SharedEntity)> {
4263 (0..7)
4264 .map(|index| {
4265 (
4266 index.to_string(),
4267 SharedEntity::new(json!({
4268 "_seq": format!("10:{index:012}"),
4269 "_version": format!("e:{index}"),
4270 "amount": u64::MAX - index,
4271 "label": "Ï€ \"quoted\" \n",
4272 "trades": [{"price": index, "side": "buy"}, {"price": -1}],
4273 })),
4274 )
4275 })
4276 .collect()
4277 }
4278
4279 #[test]
4282 fn snapshot_batches_serialize_as_frames_of_formatted_copies() {
4283 use crate::websocket::frame::{SnapshotEntity, SnapshotFrame};
4284 let rows = sample_rows();
4285 for (rows, key, wire_format, (initial, subsequent)) in [
4286 (&rows[..], None, wide_ints(), (2, 3)),
4287 (&rows[..], Some("3"), WireFormat::default(), (50, 100)),
4288 (&rows[..1], Some("0"), wide_ints(), (1, 1)),
4289 (&rows[..0], None, wide_ints(), (2, 3)),
4290 ] {
4291 let metadata = SnapshotMetadata {
4292 subscription_id: "sub-1",
4293 snapshot_id: "snapshot-1",
4294 authoritative: true,
4295 mode: Mode::List,
4296 view_id: "Thing/list",
4297 key,
4298 };
4299 let config = SnapshotBatchConfig {
4300 initial_batch_size: initial,
4301 subsequent_batch_size: subsequent,
4302 };
4303 let batches = create_snapshot_batches(rows, &wire_format, metadata, &config);
4304 let mut offset = 0;
4305 for batch in &batches {
4306 let copies = rows[offset..offset + batch.data.rows.len()]
4307 .iter()
4308 .map(|(key, entity)| SnapshotEntity {
4309 key: key.clone(),
4310 data: wire_value(entity, &wire_format),
4311 })
4312 .collect();
4313 offset += batch.data.rows.len();
4314 let frame = SnapshotFrame {
4315 protocol_version: PROTOCOL_VERSION,
4316 subscription_id: "sub-1".to_string(),
4317 snapshot_id: "snapshot-1".to_string(),
4318 authoritative: true,
4319 mode: Mode::List,
4320 export: "Thing/list".to_string(),
4321 op: "snapshot",
4322 key: key.map(str::to_string),
4323 data: copies,
4324 complete: batch.complete,
4325 };
4326 assert_eq!(
4327 serde_json::to_string(batch).unwrap(),
4328 serde_json::to_string(&frame).unwrap()
4329 );
4330 }
4331 assert_eq!(offset, rows.len());
4332 }
4333 }
4334
4335 #[test]
4338 fn entity_frames_serialize_as_frames_of_formatted_copies() {
4339 for (_, entity) in sample_rows() {
4340 for (wire_format, seq) in [
4341 (wide_ints(), Some("10:000000000001")),
4342 (WireFormat::default(), None),
4343 ] {
4344 let frame = ScopedEntityFrame {
4345 protocol_version: PROTOCOL_VERSION,
4346 subscription_id: "sub-1",
4347 mode: Mode::List,
4348 export: "Thing/latest",
4349 op: "upsert",
4350 key: "7",
4351 data: WireEntity {
4352 entity: &entity,
4353 wire_format: &wire_format,
4354 },
4355 seq,
4356 };
4357 let copy = Frame::scoped(
4358 "sub-1",
4359 Mode::List,
4360 "Thing/latest",
4361 "upsert",
4362 "7",
4363 wire_value(&entity, &wire_format),
4364 seq.map(str::to_string),
4365 );
4366 assert_eq!(
4367 serde_json::to_string(&frame).unwrap(),
4368 serde_json::to_string(©).unwrap()
4369 );
4370 }
4371 }
4372 }
4373
4374 #[test]
4375 fn lag_recovery_is_authoritative_for_an_after_query() {
4376 let subscription = Subscription {
4377 protocol_version: PROTOCOL_VERSION,
4378 subscription_id: "sub-1".to_string(),
4379 query: SubscriptionQuery {
4380 view: "Thing/list".to_string(),
4381 after: Some("40:000000000010".to_string()),
4382 ..Default::default()
4383 },
4384 snapshot: Default::default(),
4385 };
4386
4387 assert!(!SnapshotPurpose::Initial.authoritative(&subscription));
4388 assert!(SnapshotPurpose::Recovery.authoritative(&subscription));
4389 }
4390
4391 #[test]
4392 fn dot_path_filters_are_exact_and_type_sensitive() {
4393 let mut query = SubscriptionQuery {
4394 view: "Thing/list".to_string(),
4395 ..Default::default()
4396 };
4397 query
4398 .filters
4399 .insert("state.status".to_string(), json!("open"));
4400 query.filters.insert("metrics.count".to_string(), json!(2));
4401 assert!(query_matches_entity(
4402 &query,
4403 "one",
4404 &json!({"state": {"status": "open"}, "metrics": {"count": 2}}),
4405 None,
4406 ));
4407 assert!(!query_matches_entity(
4408 &query,
4409 "one",
4410 &json!({"state": {"status": "open"}, "metrics": {"count": "2"}}),
4411 None,
4412 ));
4413 }
4414
4415 #[test]
4416 fn take_and_skip_define_independent_deterministic_windows() {
4417 let entities: Vec<_> = (1..=6)
4418 .map(|id| {
4419 (
4420 id.to_string(),
4421 json!({"id": id, "_seq": format!("10:{id:012}")}),
4422 )
4423 })
4424 .collect();
4425 let first = SubscriptionQuery {
4426 view: "Thing/list".to_string(),
4427 take: Some(2),
4428 skip: Some(0),
4429 ..Default::default()
4430 };
4431 let second = SubscriptionQuery {
4432 skip: Some(2),
4433 ..first.clone()
4434 };
4435 let first_keys: Vec<_> =
4436 select_query_entities(entities.clone(), &first, None, false, false)
4437 .into_iter()
4438 .map(|(key, _)| key)
4439 .collect();
4440 let second_keys: Vec<_> = select_query_entities(entities, &second, None, false, false)
4441 .into_iter()
4442 .map(|(key, _)| key)
4443 .collect();
4444 assert_eq!(first_keys, ["6", "5"]);
4445 assert_eq!(second_keys, ["4", "3"]);
4446 }
4447
4448 #[tokio::test]
4449 async fn state_receiver_is_installed_before_snapshot_awaits() {
4450 let bus = BusManager::new();
4451 let (snapshot_started_tx, snapshot_started_rx) = oneshot::channel();
4452 let (release_snapshot_tx, release_snapshot_rx) = oneshot::channel();
4453 let bus_for_task = bus.clone();
4454 let task = tokio::spawn(async move {
4455 subscribe_state_then_snapshot(&bus_for_task, "Thing/state", "one", || async move {
4456 snapshot_started_tx.send(()).unwrap();
4457 release_snapshot_rx.await.unwrap();
4458 })
4459 .await
4460 .0
4461 });
4462 snapshot_started_rx.await.unwrap();
4463 bus.publish_state(
4464 "Thing/state",
4465 "one",
4466 Arc::new(Bytes::from_static(br#"{"op":"patch"}"#)),
4467 None,
4468 )
4469 .await;
4470 release_snapshot_tx.send(()).unwrap();
4471 let mut receiver = task.await.unwrap();
4472 receiver.changed().await.unwrap();
4473 assert!(!receiver.borrow().payload.is_empty());
4474 }
4475
4476 async fn assert_list_receiver_precedes_snapshot(view: &'static str) {
4477 let bus = BusManager::new();
4478 let (snapshot_started_tx, snapshot_started_rx) = oneshot::channel();
4479 let (release_snapshot_tx, release_snapshot_rx) = oneshot::channel();
4480 let bus_for_task = bus.clone();
4481 let task = tokio::spawn(async move {
4482 subscribe_list_then_snapshot(&bus_for_task, view, || async move {
4483 snapshot_started_tx.send(()).unwrap();
4484 release_snapshot_rx.await.unwrap();
4485 })
4486 .await
4487 .0
4488 });
4489 snapshot_started_rx.await.unwrap();
4490 bus.publish_list(
4491 view,
4492 Arc::new(BusMessage {
4493 row_scope: None,
4494 key: "one".to_string(),
4495 entity: view.to_string(),
4496 payload: Arc::new(Bytes::from_static(br#"{"op":"patch"}"#)),
4497 }),
4498 )
4499 .await;
4500 release_snapshot_tx.send(()).unwrap();
4501 let mut receiver = task.await.unwrap();
4502 assert_eq!(receiver.recv().await.unwrap().key, "one");
4503 }
4504
4505 #[tokio::test]
4506 async fn list_receiver_is_installed_before_snapshot() {
4507 assert_list_receiver_precedes_snapshot("Thing/list").await;
4508 }
4509
4510 #[tokio::test]
4511 async fn append_receiver_is_installed_before_snapshot() {
4512 assert_list_receiver_precedes_snapshot("Thing/append").await;
4513 }
4514
4515 #[tokio::test]
4516 async fn derived_source_receiver_is_installed_before_snapshot() {
4517 assert_list_receiver_precedes_snapshot("Thing/list-source").await;
4518 }
4519
4520 #[tokio::test]
4523 async fn sorted_derived_reads_take_the_window_of_the_whole_order() {
4524 use crate::materialized_view::{SortConfig, SortOrder, ViewPipeline};
4525 use crate::sorted_cache::{SortOrder as CacheOrder, SortedViewCache};
4526 let mut sorted =
4527 SortedViewCache::new("Thing/top".into(), vec!["score".into()], CacheOrder::Desc);
4528 for id in 0..40u64 {
4529 sorted.upsert(
4530 id.to_string(),
4531 json!({
4532 "_seq": format!("{}:0", 100 + id * 7 % 40),
4533 "score": (id * 13 % 17).to_string(),
4534 "owner": if id % 3 == 0 { "alice" } else { "bob" },
4535 "_partition": if id % 2 == 0 { "even" } else { "odd" },
4536 }),
4537 );
4538 }
4539 let whole = sorted.ordered_entities();
4540 let mut spec = list_spec();
4541 spec.id = "Thing/top".into();
4542 spec.source_view = Some("Thing/list".into());
4543 let caches = Arc::new(tokio::sync::RwLock::new(HashMap::from([(
4544 spec.id.clone(),
4545 sorted,
4546 )])));
4547 let cache = EntityCache::new();
4548 for limit in [None, Some(5)] {
4549 spec.pipeline = Some(ViewPipeline {
4550 filter: None,
4551 sort: Some(SortConfig {
4552 field_path: vec!["score".into()],
4553 order: SortOrder::Desc,
4554 }),
4555 limit,
4556 });
4557 for skip in [None, Some(0), Some(3), Some(50)] {
4558 for take in [None, Some(1), Some(4), Some(100)] {
4559 for (owner, after, partition) in [
4560 (None, None, None),
4561 (Some("alice"), None, None),
4562 (None, Some("120:0"), Some("even")),
4563 ] {
4564 let mut query = SubscriptionQuery {
4565 view: spec.id.clone(),
4566 skip,
4567 take,
4568 after: after.map(str::to_string),
4569 partition: partition.map(str::to_string),
4570 snapshot_limit: Some(2),
4571 ..Default::default()
4572 };
4573 if let Some(owner) = owner {
4574 query.filters.insert("owner".into(), json!(owner));
4575 }
4576 for apply_snapshot_limit in [false, true] {
4577 let rows = load_query_entities(
4578 &cache,
4579 Some(caches.clone()),
4580 &spec,
4581 &query,
4582 None,
4583 apply_snapshot_limit,
4584 )
4585 .await;
4586 let mut expected_query = query.clone();
4587 if let Some(limit) = limit {
4588 expected_query.take = Some(take.unwrap_or(limit).min(limit));
4589 }
4590 let expected = select_query_entities(
4591 whole.clone(),
4592 &expected_query,
4593 None,
4594 true,
4595 apply_snapshot_limit,
4596 );
4597 assert_eq!(rows, expected, "{query:?} {limit:?}");
4598 }
4599 }
4600 }
4601 }
4602 }
4603 }
4604
4605 #[tokio::test]
4606 async fn unsorted_derived_reads_preserve_filters_order_and_snapshot_window() {
4607 use crate::materialized_view::{CompareOp, FilterConfig, ViewPipeline};
4608 let cache = EntityCache::new();
4609 for id in 1..=6 {
4610 cache
4611 .upsert(
4612 "Thing/list",
4613 &id.to_string(),
4614 json!({
4615 "_seq": format!("{}:0", id * 10), "active": id != 6,
4616 "owner": "alice", "id": id
4617 }),
4618 )
4619 .await;
4620 }
4621 let mut spec = list_spec();
4622 spec.id = "Thing/custom".into();
4623 spec.source_view = Some("Thing/list".into());
4624 spec.pipeline = Some(ViewPipeline::default());
4625 let caches = Arc::new(tokio::sync::RwLock::new(HashMap::new()));
4626 let mut query = SubscriptionQuery {
4627 view: spec.id.clone(),
4628 ..Default::default()
4629 };
4630 let rows =
4631 load_query_entities(&cache, Some(caches.clone()), &spec, &query, None, false).await;
4632 assert_eq!(
4633 rows.iter().map(|(key, _)| key.as_str()).collect::<Vec<_>>(),
4634 ["6", "5", "4", "3", "2", "1"]
4635 );
4636 spec.pipeline.as_mut().unwrap().filter = Some(FilterConfig {
4637 field_path: vec!["active".into()],
4638 op: CompareOp::Eq,
4639 value: json!(true),
4640 });
4641 query.filters.insert("owner".into(), json!("alice"));
4642 query.skip = Some(1);
4643 query.take = Some(3);
4644 query.snapshot_limit = Some(1);
4645 let rows =
4646 load_query_entities(&cache, Some(caches.clone()), &spec, &query, None, false).await;
4647 assert_eq!(
4648 rows.iter().map(|(key, _)| key.as_str()).collect::<Vec<_>>(),
4649 ["4", "3", "2"]
4650 );
4651 assert_eq!(
4652 load_query_entities(&cache, Some(caches.clone()), &spec, &query, None, true)
4653 .await
4654 .len(),
4655 1
4656 );
4657 query.skip = None;
4658 query.after = Some("20:0".into());
4659 let rows =
4660 load_query_entities(&cache, Some(caches.clone()), &spec, &query, None, false).await;
4661 assert_eq!(
4662 rows.iter().map(|(key, _)| key.as_str()).collect::<Vec<_>>(),
4663 ["3", "4", "5"]
4664 );
4665 query.filters.insert("owner".into(), json!("bob"));
4666 assert!(
4667 load_query_entities(&cache, Some(caches), &spec, &query, None, false)
4668 .await
4669 .is_empty()
4670 );
4671 }
4672
4673 #[tokio::test]
4674 async fn snapshot_limit_does_not_change_live_take_skip_membership() {
4675 let cache = EntityCache::with_config(EntityCacheConfig {
4676 max_entities_per_view: 10,
4677 ..Default::default()
4678 });
4679 for id in 1..=4 {
4680 cache
4681 .upsert(
4682 "Thing/list",
4683 &id.to_string(),
4684 json!({"_seq": format!("10:{id:012}")}),
4685 )
4686 .await;
4687 }
4688 let query = SubscriptionQuery {
4689 view: "Thing/list".to_string(),
4690 take: Some(3),
4691 skip: Some(1),
4692 snapshot_limit: Some(1),
4693 ..Default::default()
4694 };
4695 let live = load_query_entities(&cache, None, &list_spec(), &query, None, false).await;
4696 let snapshot = load_query_entities(&cache, None, &list_spec(), &query, None, true).await;
4697 assert_eq!(live.len(), 3);
4698 assert_eq!(snapshot.len(), 1);
4699 }
4700
4701 #[test]
4702 fn fixture_manifest_covers_required_conformance_cases() {
4703 let manifest: Value = serde_json::from_str(include_str!(
4704 "../../../../tests/fixtures/websocket-v2/manifest.json"
4705 ))
4706 .unwrap();
4707 let names: HashSet<_> = manifest["fixtures"]
4708 .as_array()
4709 .unwrap()
4710 .iter()
4711 .filter_map(Value::as_str)
4712 .collect();
4713 for required in [
4714 "keyed-state.json",
4715 "list-windows.json",
4716 "filters.json",
4717 "multi-batch-authoritative.json",
4718 "empty-snapshot.json",
4719 "remove.json",
4720 "delete.json",
4721 "incremental-snapshot.json",
4722 "reconnect-replacement.json",
4723 "errors.json",
4724 "whole-entities.json",
4725 ] {
4726 assert!(names.contains(required), "missing fixture {required}");
4727 }
4728
4729 for document in [
4730 include_str!("../../../../tests/fixtures/websocket-v2/keyed-state.json"),
4731 include_str!("../../../../tests/fixtures/websocket-v2/list-windows.json"),
4732 include_str!("../../../../tests/fixtures/websocket-v2/filters.json"),
4733 include_str!("../../../../tests/fixtures/websocket-v2/multi-batch-authoritative.json"),
4734 include_str!("../../../../tests/fixtures/websocket-v2/empty-snapshot.json"),
4735 include_str!("../../../../tests/fixtures/websocket-v2/remove.json"),
4736 include_str!("../../../../tests/fixtures/websocket-v2/delete.json"),
4737 include_str!("../../../../tests/fixtures/websocket-v2/incremental-snapshot.json"),
4738 include_str!("../../../../tests/fixtures/websocket-v2/reconnect-replacement.json"),
4739 include_str!("../../../../tests/fixtures/websocket-v2/errors.json"),
4740 include_str!("../../../../tests/fixtures/websocket-v2/whole-entities.json"),
4741 ] {
4742 let fixture: Value = serde_json::from_str(document).unwrap();
4743 assert!(fixture["name"].is_string());
4744 }
4745 }
4746
4747 fn append_frame(key: &str, data: Value) -> Arc<Bytes> {
4748 let frame = json!({
4749 "entity": "Trade/append",
4750 "op": "patch",
4751 "key": key,
4752 "offset": 7,
4753 "data": data,
4754 });
4755 Arc::new(Bytes::from(serde_json::to_vec(&frame).unwrap()))
4756 }
4757
4758 #[test]
4762 fn replay_delivery_applies_key_partition_and_filters() {
4763 let matching = append_frame("pool1", json!({"_partition": "us", "side": "buy"}));
4764 let other_partition = append_frame("pool1", json!({"_partition": "eu", "side": "buy"}));
4765 let other_side = append_frame("pool1", json!({"_partition": "us", "side": "sell"}));
4766
4767 let unfiltered = SubscriptionQuery {
4768 view: "Trade/append".to_string(),
4769 ..Default::default()
4770 };
4771 assert!(live_frame_matches(&unfiltered, "pool1", &matching));
4772 assert!(live_frame_matches(&unfiltered, "pool9", &matching));
4773
4774 let keyed = SubscriptionQuery {
4775 view: "Trade/append".to_string(),
4776 key: Some("pool1".to_string()),
4777 ..Default::default()
4778 };
4779 assert!(live_frame_matches(&keyed, "pool1", &matching));
4780 assert!(!live_frame_matches(&keyed, "pool2", &matching));
4781
4782 let partitioned = SubscriptionQuery {
4783 view: "Trade/append".to_string(),
4784 partition: Some("us".to_string()),
4785 ..Default::default()
4786 };
4787 assert!(live_frame_matches(&partitioned, "pool1", &matching));
4788 assert!(!live_frame_matches(&partitioned, "pool1", &other_partition));
4789
4790 let filtered = SubscriptionQuery {
4791 view: "Trade/append".to_string(),
4792 filters: [("side".to_string(), json!("buy"))].into_iter().collect(),
4793 ..Default::default()
4794 };
4795 assert!(live_frame_matches(&filtered, "pool1", &matching));
4796 assert!(!live_frame_matches(&filtered, "pool1", &other_side));
4797 }
4798
4799 #[test]
4800 fn a_frame_without_decodable_data_does_not_satisfy_a_filter() {
4801 let filtered = SubscriptionQuery {
4802 view: "Trade/append".to_string(),
4803 filters: [("side".to_string(), json!("buy"))].into_iter().collect(),
4804 ..Default::default()
4805 };
4806 let garbage = Arc::new(Bytes::from_static(b"not json"));
4807 assert!(!live_frame_matches(&filtered, "pool1", &garbage));
4808 }
4809
4810 #[test]
4814 fn replay_lagged_recovers_from_before_the_gap() {
4815 let epoch = crate::journal::JournalEpoch::new();
4816 let issue = SocketIssueMessage::replay_lagged(
4817 Some("trades".to_string()),
4818 37,
4819 Some(crate::journal::Cursor {
4820 epoch: epoch.clone(),
4821 offset: 4180,
4822 }),
4823 );
4824 assert_eq!(issue.code, "replay-lagged");
4825 assert_eq!(issue.recover_from, Some(format!("{epoch}:4180")));
4826 assert!(
4827 issue.suggested_action.unwrap().contains("4180"),
4828 "the consumer is told exactly which cursor recovers the gap"
4829 );
4830
4831 let from_scratch = SocketIssueMessage::replay_lagged(Some("trades".to_string()), 9, None);
4834 assert_eq!(from_scratch.recover_from, None);
4835 assert!(from_scratch
4836 .suggested_action
4837 .unwrap()
4838 .contains("without `after`"));
4839 }
4840
4841 fn bus_message(key: &str) -> Arc<BusMessage> {
4842 Arc::new(BusMessage {
4843 row_scope: None,
4844 key: key.to_string(),
4845 entity: "Trade/append".to_string(),
4846 payload: Arc::new(Bytes::from_static(b"{}")),
4847 })
4848 }
4849
4850 #[tokio::test]
4855 async fn draining_during_a_replay_keeps_a_busy_view_from_lapping_it() {
4856 let (sender, mut receiver) = broadcast::channel::<Arc<BusMessage>>(16);
4857 let mut pending = VecDeque::new();
4858 let mut lagged = None;
4859
4860 for index in 0..48 {
4863 sender.send(bus_message(&format!("k{index}"))).unwrap();
4864 drain_available(&mut receiver, &mut pending, &mut lagged);
4865 }
4866
4867 assert_eq!(lagged, None, "draining as we go means nothing is dropped");
4868 assert_eq!(pending.len(), 48, "every published frame is buffered");
4869 }
4870
4871 #[tokio::test]
4872 async fn a_replay_that_never_drains_is_reported_as_a_gap() {
4873 let (sender, mut receiver) = broadcast::channel::<Arc<BusMessage>>(8);
4874 for index in 0..32 {
4875 sender.send(bus_message(&format!("k{index}"))).unwrap();
4876 }
4877
4878 let mut pending = VecDeque::new();
4879 let mut lagged = None;
4880 drain_available(&mut receiver, &mut pending, &mut lagged);
4881
4882 assert!(
4883 lagged.is_some(),
4884 "overflowing the bus is a gap, not silent truncation"
4885 );
4886 }
4887
4888 #[tokio::test]
4892 async fn nothing_after_a_gap_is_buffered_ahead_of_the_report() {
4893 let (sender, mut receiver) = broadcast::channel::<Arc<BusMessage>>(8);
4894 let mut pending = VecDeque::new();
4895 let mut lagged = None;
4896
4897 sender.send(bus_message("before")).unwrap();
4899 drain_available(&mut receiver, &mut pending, &mut lagged);
4900 assert_eq!(pending.len(), 1);
4901
4902 for index in 0..32 {
4904 sender.send(bus_message(&format!("lost{index}"))).unwrap();
4905 }
4906 drain_available(&mut receiver, &mut pending, &mut lagged);
4907 assert!(lagged.is_some());
4908
4909 let buffered_at_gap = pending.len();
4912 drain_available(&mut receiver, &mut pending, &mut lagged);
4913 assert_eq!(
4914 pending.len(),
4915 buffered_at_gap,
4916 "post-gap frames must not join the pre-gap flush"
4917 );
4918 assert_eq!(pending.front().unwrap().key, "before");
4919 }
4920 #[test]
4923 fn the_seam_between_replay_and_live_neither_repeats_nor_skips() {
4924 let mut last_sent = Some(4211);
4925
4926 assert!(
4927 already_delivered(Some(4211), &mut last_sent),
4928 "the record the replay ended on must not be sent twice"
4929 );
4930 assert!(already_delivered(Some(4100), &mut last_sent));
4931 assert_eq!(last_sent, Some(4211), "a duplicate never moves the mark");
4932
4933 assert!(
4934 !already_delivered(Some(4212), &mut last_sent),
4935 "the next record is new"
4936 );
4937 assert_eq!(last_sent, Some(4212));
4938
4939 assert!(!already_delivered(None, &mut last_sent));
4942 assert_eq!(last_sent, Some(4212));
4943 }
4944
4945 #[test]
4948 fn a_fresh_subscription_delivers_its_first_live_frame() {
4949 let mut last_sent = None;
4950 assert!(!already_delivered(Some(0), &mut last_sent));
4951 assert_eq!(last_sent, Some(0));
4952 }
4953
4954 mod over_a_socket {
4959 use super::*;
4960 use crate::journal::{EventJournal, JournalConfig};
4961 use crate::projector::Projector;
4962 use crate::{MutationBatch, SlotContext};
4963 use arete_interpreter::Mutation;
4964 use futures_util::{SinkExt, StreamExt};
4965 use std::time::Duration;
4966 use tokio::net::{TcpListener, TcpStream};
4967 use tokio::sync::mpsc;
4968 use tokio_tungstenite::tungstenite::Message;
4969 use tokio_tungstenite::{client_async, WebSocketStream};
4970
4971 const RETAINED: u64 = 600;
4972
4973 fn append_index() -> ViewIndex {
4974 let mut index = ViewIndex::new();
4975 index.add_spec(ViewSpec {
4976 id: "Trade/append".to_string(),
4977 export: "Trade".to_string(),
4978 mode: Mode::Append,
4979 wire_format: Default::default(),
4980 projection: Projection::all(),
4981 filters: Filters::all(),
4982 delivery: Delivery::default(),
4983 pipeline: None,
4984 source_view: None,
4985 });
4986 index
4987 }
4988
4989 fn trade(index: u64) -> MutationBatch {
4990 MutationBatch::with_slot_context(
4991 vec![Mutation {
4992 export: "Trade".to_string(),
4993 key: json!(format!("pool{}", index % 4)),
4994 patch: json!({"trade": index}),
4995 append: vec![],
4996 }]
4997 .into_iter()
4998 .collect(),
4999 SlotContext::new(100 + index / 3, index % 3),
5000 )
5001 }
5002
5003 struct Harness {
5004 addr: SocketAddr,
5005 journal: Arc<EventJournal>,
5006 tx: mpsc::Sender<MutationBatch>,
5007 }
5008
5009 impl Harness {
5010 async fn start() -> Self {
5011 let view_index = Arc::new(append_index());
5012 let entity_cache = EntityCache::new();
5013 let bus_manager = BusManager::new();
5014 let journal = Arc::new(EventJournal::new(JournalConfig {
5015 enabled: true,
5016 max_bytes_per_view: u64::MAX,
5017 max_records_per_view: 10_000,
5018 max_age: Duration::from_secs(3_600),
5019 }));
5020
5021 let (tx, rx) = mpsc::channel::<MutationBatch>(256);
5022 tokio::spawn(
5023 Projector::new(
5024 view_index.clone(),
5025 bus_manager.clone(),
5026 entity_cache.clone(),
5027 rx,
5028 #[cfg(feature = "otel")]
5029 None,
5030 )
5031 .with_journal(journal.clone())
5032 .run(),
5033 );
5034
5035 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5036 let addr = listener.local_addr().unwrap();
5037 let server = WebSocketServer::new(
5038 addr,
5039 bus_manager,
5040 entity_cache,
5041 view_index,
5042 #[cfg(feature = "otel")]
5043 None,
5044 )
5045 .with_journal(journal.clone());
5046 let (acceptor, _cleanup) = server.into_acceptor();
5047 tokio::spawn(async move { acceptor.serve_listener(listener).await });
5048
5049 Self { addr, journal, tx }
5050 }
5051
5052 async fn publish(&self, range: std::ops::Range<u64>) {
5053 for index in range {
5054 self.tx.send(trade(index)).await.unwrap();
5055 }
5056 let (ack, wait) = oneshot::channel();
5057 self.tx
5058 .send(MutationBatch::flush_marker(ack))
5059 .await
5060 .unwrap();
5061 wait.await.unwrap();
5062 }
5063
5064 async fn connect(&self) -> WebSocketStream<TcpStream> {
5065 let stream = TcpStream::connect(self.addr).await.unwrap();
5066 client_async(format!("ws://{}/", self.addr), stream)
5067 .await
5068 .unwrap()
5069 .0
5070 }
5071 }
5072
5073 async fn next_frame(socket: &mut WebSocketStream<TcpStream>) -> Value {
5074 loop {
5075 let message = tokio::time::timeout(Duration::from_secs(10), socket.next())
5076 .await
5077 .expect("the server answers within the timeout")
5078 .expect("the stream stays open")
5079 .expect("a readable frame");
5080 let bytes = match &message {
5083 Message::Text(text) => text.as_bytes(),
5084 Message::Binary(bytes) => bytes.as_ref(),
5085 _ => continue,
5086 };
5087 return serde_json::from_slice(bytes).expect("frames are JSON");
5088 }
5089 }
5090
5091 async fn collect_trades(socket: &mut WebSocketStream<TcpStream>, count: usize) -> Vec<u64> {
5093 let mut offsets = Vec::with_capacity(count);
5094 while offsets.len() < count {
5095 let frame = next_frame(socket).await;
5096 assert_ne!(
5097 frame["type"], "error",
5098 "no error frame should interrupt delivery: {frame}"
5099 );
5100 if let Some(offset) = frame["offset"].as_u64() {
5101 offsets.push(offset);
5102 }
5103 }
5104 offsets
5105 }
5106
5107 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
5111 async fn a_reconnect_replays_from_a_cursor_and_continues_live() {
5112 let harness = Harness::start().await;
5113 harness.publish(0..RETAINED).await;
5114
5115 let cursor = harness.journal.window("Trade/append").await;
5116 let stored = format!("{}:{}", cursor.epoch, 99);
5117
5118 let mut socket = harness.connect().await;
5119 socket
5120 .send(Message::Text(
5121 json!({
5122 "type": "subscribe",
5123 "protocolVersion": 2,
5124 "subscriptionId": "trades",
5125 "query": {"view": "Trade/append", "after": stored},
5126 })
5127 .to_string()
5128 .into(),
5129 ))
5130 .await
5131 .unwrap();
5132
5133 let ack = next_frame(&mut socket).await;
5134 assert_eq!(ack["op"], "subscribed", "unexpected ack: {ack}");
5135 assert_eq!(ack["replayWindow"]["next"], json!(RETAINED));
5136
5137 let replayed = collect_trades(&mut socket, (RETAINED - 100) as usize).await;
5139 assert_eq!(
5140 replayed,
5141 (100..RETAINED).collect::<Vec<_>>(),
5142 "every event after the cursor, in order, exactly once"
5143 );
5144
5145 harness.publish(RETAINED..RETAINED + 40).await;
5147 let live = collect_trades(&mut socket, 40).await;
5148 assert_eq!(
5149 live,
5150 (RETAINED..RETAINED + 40).collect::<Vec<_>>(),
5151 "the live stream resumes exactly where the replay stopped"
5152 );
5153
5154 socket.close(None).await.ok();
5155 }
5156
5157 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
5162 async fn events_published_during_a_replay_are_not_lost() {
5163 let harness = Harness::start().await;
5164 harness.publish(0..RETAINED).await;
5165
5166 let epoch = harness.journal.window("Trade/append").await.epoch;
5167 let mut socket = harness.connect().await;
5168 socket
5169 .send(Message::Text(
5170 json!({
5171 "type": "subscribe",
5172 "protocolVersion": 2,
5173 "subscriptionId": "trades",
5174 "query": {"view": "Trade/append", "after": format!("{epoch}:0")},
5175 })
5176 .to_string()
5177 .into(),
5178 ))
5179 .await
5180 .unwrap();
5181 let ack = next_frame(&mut socket).await;
5182 assert_eq!(ack["op"], "subscribed", "unexpected ack: {ack}");
5183
5184 harness.publish(RETAINED..RETAINED + 200).await;
5186
5187 let total = (RETAINED + 200 - 1) as usize;
5188 let delivered = collect_trades(&mut socket, total).await;
5189 assert_eq!(
5190 delivered,
5191 (1..RETAINED + 200).collect::<Vec<_>>(),
5192 "replay and live output join without a gap or a repeat"
5193 );
5194
5195 socket.close(None).await.ok();
5196 }
5197
5198 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
5201 async fn a_stale_epoch_is_refused_and_frees_the_subscription_id() {
5202 let harness = Harness::start().await;
5203 harness.publish(0..50).await;
5204
5205 let mut socket = harness.connect().await;
5206 socket
5207 .send(Message::Text(
5208 json!({
5209 "type": "subscribe",
5210 "protocolVersion": 2,
5211 "subscriptionId": "trades",
5212 "query": {
5213 "view": "Trade/append",
5214 "after": format!("{}:10", crate::journal::JournalEpoch::new()),
5215 },
5216 })
5217 .to_string()
5218 .into(),
5219 ))
5220 .await
5221 .unwrap();
5222
5223 let error = next_frame(&mut socket).await;
5224 assert_eq!(error["type"], "error", "unexpected frame: {error}");
5225 assert_eq!(error["code"], "cursor-epoch-changed");
5226
5227 socket
5230 .send(Message::Text(
5231 json!({
5232 "type": "subscribe",
5233 "protocolVersion": 2,
5234 "subscriptionId": "trades",
5235 "query": {"view": "Trade/append"},
5236 })
5237 .to_string()
5238 .into(),
5239 ))
5240 .await
5241 .unwrap();
5242 let ack = next_frame(&mut socket).await;
5243 assert_eq!(
5244 ack["op"], "subscribed",
5245 "the refused id must be reusable: {ack}"
5246 );
5247
5248 assert_eq!(collect_trades(&mut socket, 50).await.len(), 50);
5249 socket.close(None).await.ok();
5250 }
5251
5252 const ROUND: &str = "Round/state";
5253
5254 #[derive(Clone)]
5257 struct StateServer {
5258 addr: SocketAddr,
5259 bus_manager: BusManager,
5260 entity_cache: EntityCache,
5261 }
5262
5263 impl StateServer {
5264 async fn start() -> Self {
5265 let mut index = ViewIndex::new();
5266 index.add_spec(ViewSpec {
5267 id: ROUND.to_string(),
5268 export: "Round".to_string(),
5269 mode: Mode::State,
5270 wire_format: Default::default(),
5271 projection: Projection::all(),
5272 filters: Filters::all(),
5273 delivery: Delivery::default(),
5274 pipeline: None,
5275 source_view: None,
5276 });
5277 let bus_manager = BusManager::new();
5278 let entity_cache = EntityCache::new();
5279 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5280 let addr = listener.local_addr().unwrap();
5281 let server = WebSocketServer::new(
5282 addr,
5283 bus_manager.clone(),
5284 entity_cache.clone(),
5285 Arc::new(index),
5286 #[cfg(feature = "otel")]
5287 None,
5288 );
5289 let (acceptor, _cleanup) = server.into_acceptor();
5290 tokio::spawn(async move { acceptor.serve_listener(listener).await });
5291 Self {
5292 addr,
5293 bus_manager,
5294 entity_cache,
5295 }
5296 }
5297
5298 async fn publish(&self, key: &str, mut patch: Value, seq: &str) {
5301 patch["_seq"] = Value::String(seq.to_string());
5302 self.entity_cache
5303 .upsert_with_append(ROUND, key, patch.clone(), &[], PatchOrigin::Unknown)
5304 .await;
5305 self.publish_frame(key, "patch", patch, seq).await;
5306 }
5307
5308 async fn create(&self, key: &str, mut entity: Value, seq: &str) {
5310 entity["_seq"] = Value::String(seq.to_string());
5311 self.entity_cache
5312 .upsert_with_append(ROUND, key, entity.clone(), &[], PatchOrigin::Creation)
5313 .await;
5314 self.publish_frame(key, "upsert", entity, seq).await;
5315 }
5316
5317 async fn publish_frame(&self, key: &str, op: &str, data: Value, seq: &str) {
5319 let frame = json!({
5320 "mode": "state",
5321 "entity": ROUND,
5322 "op": op,
5323 "key": key,
5324 "data": data,
5325 "seq": seq,
5326 });
5327 self.bus_manager
5328 .publish_state(ROUND, key, Arc::new(Bytes::from(frame.to_string())), None)
5329 .await;
5330 }
5331
5332 async fn subscribe(&self, key: &str) -> WebSocketStream<TcpStream> {
5334 let stream = TcpStream::connect(self.addr).await.unwrap();
5335 let mut socket = client_async(format!("ws://{}/", self.addr), stream)
5336 .await
5337 .unwrap()
5338 .0;
5339 socket
5340 .send(Message::Text(
5341 json!({
5342 "type": "subscribe",
5343 "protocolVersion": 2,
5344 "subscriptionId": "round",
5345 "query": {"view": ROUND, "key": key},
5346 })
5347 .to_string()
5348 .into(),
5349 ))
5350 .await
5351 .unwrap();
5352 let ack = next_frame(&mut socket).await;
5353 assert_eq!(ack["op"], "subscribed", "unexpected ack: {ack}");
5354 loop {
5355 let frame = next_frame(&mut socket).await;
5356 assert_eq!(frame["op"], "snapshot", "unexpected frame: {frame}");
5357 if frame["complete"] == true {
5358 return socket;
5359 }
5360 }
5361 }
5362 }
5363
5364 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
5370 async fn a_state_subscriber_keeps_the_fields_of_an_overwritten_patch() {
5371 let server = StateServer::start().await;
5372 server
5373 .publish("7", json!({"id": 7}), "100:000000000001")
5374 .await;
5375 let mut socket = server.subscribe("7").await;
5376
5377 server
5380 .publish(
5381 "7",
5382 json!({"results": {"slot_hash": "abc"}}),
5383 "140:003836292257",
5384 )
5385 .await;
5386 let frame = next_frame(&mut socket).await;
5387 assert_eq!(frame["op"], "patch", "unexpected frame: {frame}");
5388 assert_eq!(frame["seq"], "140:003836292257");
5389
5390 let writer = server.clone();
5395 tokio::spawn(async move {
5396 writer
5397 .publish("7", json!({"entropy": {"seed": "def"}}), "140:000000001200")
5398 .await;
5399 writer
5400 .publish("7", json!({"total": 1}), "140:000000001200")
5401 .await;
5402 })
5403 .await
5404 .unwrap();
5405 let frame = next_frame(&mut socket).await;
5406 assert_eq!(frame["op"], "patch", "unexpected frame: {frame}");
5407 assert_eq!(
5410 frame["data"],
5411 json!({
5412 "results": {"slot_hash": "abc"},
5413 "entropy": {"seed": "def"},
5414 "total": 1,
5415 "_seq": "140:000000001200",
5416 })
5417 );
5418 assert!(frame.get("seq").is_none(), "unexpected seq: {frame}");
5421
5422 server
5424 .publish("7", json!({"total": 2}), "141:000000000002")
5425 .await;
5426 let frame = next_frame(&mut socket).await;
5427 assert_eq!(frame["op"], "patch", "unexpected frame: {frame}");
5428 assert_eq!(
5429 frame["data"],
5430 json!({"total": 2, "_seq": "141:000000000002"})
5431 );
5432
5433 let writer = server.clone();
5436 tokio::spawn(async move {
5437 writer
5438 .publish("7", json!({"total": 3}), "142:000000000001")
5439 .await;
5440 writer
5441 .publish("7", json!({"flag": true}), "142:000000000001")
5442 .await;
5443 })
5444 .await
5445 .unwrap();
5446 let frame = next_frame(&mut socket).await;
5447 assert_eq!(frame["op"], "patch", "unexpected frame: {frame}");
5448 assert!(frame.get("seq").is_none(), "unexpected seq: {frame}");
5449 assert_eq!(
5450 frame["data"],
5451 json!({"total": 3, "flag": true, "_seq": "142:000000000001"})
5452 );
5453
5454 socket.close(None).await.ok();
5455 }
5456
5457 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
5460 async fn a_forwarded_whole_entity_is_what_a_catch_up_is_measured_against() {
5461 let server = StateServer::start().await;
5462 server
5463 .publish("7", json!({"id": 7, "a": 1}), "100:000000000001")
5464 .await;
5465 let mut socket = server.subscribe("7").await;
5466
5467 let whole = json!({"id": 7, "a": 1, "b": 2, "_seq": "101:000000000001"});
5468 server
5469 .entity_cache
5470 .store_whole(ROUND, "7", whole.clone())
5471 .await;
5472 server
5473 .publish_frame("7", "upsert", whole.clone(), "101:000000000001")
5474 .await;
5475 let frame = next_frame(&mut socket).await;
5476 assert_eq!(frame["op"], "upsert", "unexpected frame: {frame}");
5477 assert_eq!(frame["data"], whole);
5478
5479 let writer = server.clone();
5480 tokio::spawn(async move {
5481 writer
5482 .publish("7", json!({"c": 3}), "102:000000000001")
5483 .await;
5484 writer
5485 .publish("7", json!({"d": 4}), "102:000000000002")
5486 .await;
5487 })
5488 .await
5489 .unwrap();
5490 let frame = next_frame(&mut socket).await;
5491 assert_eq!(frame["op"], "patch", "unexpected frame: {frame}");
5492 assert_eq!(
5493 frame["data"],
5494 json!({"c": 3, "d": 4, "_seq": "102:000000000002"}),
5495 "`b` came with the whole entity, so it is not sent again"
5496 );
5497 socket.close(None).await.ok();
5498 }
5499
5500 async fn holders(server: &StateServer, key: &str) -> usize {
5503 let entity = server.entity_cache.get_shared(ROUND, key).await.unwrap();
5504 Arc::strong_count(entity.fields()) - 2
5505 }
5506
5507 async fn settle(mut held: impl AsyncFnMut() -> usize, expected: usize) {
5510 for _ in 0..200 {
5511 if held().await == expected {
5512 return;
5513 }
5514 tokio::time::sleep(Duration::from_millis(5)).await;
5515 }
5516 assert_eq!(held().await, expected);
5517 }
5518
5519 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
5523 async fn state_subscribers_share_what_they_were_sent() {
5524 let server = StateServer::start().await;
5525 server
5526 .create(
5527 "7",
5528 json!({"id": 7, "detail": {"a": 1}}),
5529 "100:000000000001",
5530 )
5531 .await;
5532 let mut sockets = Vec::new();
5533 for _ in 0..3 {
5534 sockets.push(server.subscribe("7").await);
5535 }
5536 settle(|| holders(&server, "7"), 3).await;
5537
5538 let sent = server.entity_cache.get_shared(ROUND, "7").await.unwrap();
5541 let whole = json!({"id": 7, "detail": {"b": 2}, "_seq": "101:000000000001"});
5542 server
5543 .entity_cache
5544 .store_whole(ROUND, "7", whole.clone())
5545 .await;
5546 let frame = Arc::new(Bytes::from(
5547 json!({
5548 "mode": "state",
5549 "entity": ROUND,
5550 "op": "upsert",
5551 "key": "7",
5552 "data": whole,
5553 "seq": "101:000000000001",
5554 })
5555 .to_string(),
5556 ));
5557 server
5558 .bus_manager
5559 .publish_state(ROUND, "7", frame.clone(), None)
5560 .await;
5561 for socket in &mut sockets {
5562 assert_eq!(next_frame(socket).await["op"], "upsert");
5563 }
5564 settle(async || Arc::strong_count(&frame), 2 + sockets.len()).await;
5566 settle(async || Arc::strong_count(sent.fields()), 1).await;
5568 for mut socket in sockets {
5569 socket.close(None).await.ok();
5570 }
5571 }
5572
5573 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
5577 async fn a_catch_up_restores_a_field_a_forwarded_patch_set() {
5578 let server = StateServer::start().await;
5579 server
5580 .publish("7", json!({"id": 7, "a": 1}), "100:000000000001")
5581 .await;
5582 let mut socket = server.subscribe("7").await;
5583
5584 server
5585 .publish("7", json!({"a": 2}), "101:000000000001")
5586 .await;
5587 let frame = next_frame(&mut socket).await;
5588 assert_eq!(frame["op"], "patch", "unexpected frame: {frame}");
5589 assert_eq!(frame["data"], json!({"a": 2, "_seq": "101:000000000001"}));
5590
5591 let writer = server.clone();
5592 tokio::spawn(async move {
5593 writer
5594 .publish("7", json!({"a": 1}), "102:000000000001")
5595 .await;
5596 writer
5597 .publish("7", json!({"b": 1}), "102:000000000002")
5598 .await;
5599 })
5600 .await
5601 .unwrap();
5602 let frame = next_frame(&mut socket).await;
5603 assert_eq!(frame["op"], "patch", "unexpected frame: {frame}");
5604 assert_eq!(
5605 frame["data"],
5606 json!({"a": 1, "b": 1, "_seq": "102:000000000002"}),
5607 "`a` matches the snapshot but the client holds 2"
5608 );
5609 socket.close(None).await.ok();
5610 }
5611
5612 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
5617 async fn a_key_deleted_and_recreated_between_reads_replaces_the_clients_copy() {
5618 let server = StateServer::start().await;
5619 server
5620 .publish("7", json!({"id": 7, "old": true}), "100:000000000001")
5621 .await;
5622 let mut socket = server.subscribe("7").await;
5623
5624 let writer = server.clone();
5625 tokio::spawn(async move {
5626 writer.entity_cache.remove(ROUND, "7").await;
5627 writer
5628 .publish_frame("7", "delete", Value::Null, "101:000000000001")
5629 .await;
5630 writer
5631 .publish("7", json!({"id": 7, "fresh": true}), "101:000000000002")
5632 .await;
5633 })
5634 .await
5635 .unwrap();
5636 let frame = next_frame(&mut socket).await;
5637 assert_eq!(frame["op"], "upsert", "unexpected frame: {frame}");
5638 assert_eq!(
5639 frame["data"],
5640 json!({"id": 7, "fresh": true, "_seq": "101:000000000002"})
5641 );
5642 assert!(frame.get("seq").is_none(), "unexpected seq: {frame}");
5643
5644 let writer = server.clone();
5646 tokio::spawn(async move {
5647 writer
5648 .publish("7", json!({"n": 1}), "102:000000000001")
5649 .await;
5650 writer.entity_cache.remove(ROUND, "7").await;
5651 writer
5652 .publish_frame("7", "delete", Value::Null, "102:000000000002")
5653 .await;
5654 })
5655 .await
5656 .unwrap();
5657 let frame = next_frame(&mut socket).await;
5658 assert_eq!(frame["op"], "delete", "unexpected frame: {frame}");
5659
5660 server
5662 .create("7", json!({"id": 7, "third": true}), "103:000000000001")
5663 .await;
5664 let frame = next_frame(&mut socket).await;
5665 assert_eq!(frame["op"], "upsert", "unexpected frame: {frame}");
5666 assert_eq!(frame["data"]["third"], true);
5667 assert!(frame["data"].get("fresh").is_none(), "{frame}");
5668 socket.close(None).await.ok();
5669 }
5670
5671 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
5675 async fn a_copy_of_a_deleted_entity_takes_no_patch_until_it_is_replaced() {
5676 let server = StateServer::start().await;
5677 server
5678 .publish("7", json!({"id": 7, "old": true}), "100:000000000001")
5679 .await;
5680 let mut socket = server.subscribe("7").await;
5681
5682 let writer = server.clone();
5683 tokio::spawn(async move {
5684 writer.entity_cache.remove(ROUND, "7").await;
5685 writer
5686 .publish_frame("7", "delete", Value::Null, "101:000000000001")
5687 .await;
5688 writer
5689 .publish_frame("7", "patch", json!({"n": 1}), "101:000000000002")
5690 .await;
5691 })
5692 .await
5693 .unwrap();
5694 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
5698
5699 let whole = json!({"id": 7, "n": 1, "_seq": "101:000000000002"});
5700 server
5701 .entity_cache
5702 .store_whole(ROUND, "7", whole.clone())
5703 .await;
5704 server
5705 .publish_frame("7", "upsert", whole.clone(), "101:000000000002")
5706 .await;
5707 let frame = next_frame(&mut socket).await;
5708 assert_eq!(frame["op"], "upsert", "unexpected frame: {frame}");
5709 assert_eq!(frame["data"], whole);
5710 socket.close(None).await.ok();
5711 }
5712
5713 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
5717 async fn a_whole_entity_among_overwritten_frames_replaces_the_clients_copy() {
5718 let server = StateServer::start().await;
5719 server
5720 .publish("7", json!({"id": 7, "dropped": 1}), "100:000000000001")
5721 .await;
5722 let mut socket = server.subscribe("7").await;
5723
5724 let writer = server.clone();
5725 tokio::spawn(async move {
5726 let whole = json!({"id": 7, "_seq": "101:000000000001"});
5727 writer
5728 .entity_cache
5729 .store_whole(ROUND, "7", whole.clone())
5730 .await;
5731 writer
5732 .publish_frame("7", "upsert", whole, "101:000000000001")
5733 .await;
5734 writer
5735 .publish("7", json!({"n": 1}), "101:000000000002")
5736 .await;
5737 })
5738 .await
5739 .unwrap();
5740 let frame = next_frame(&mut socket).await;
5741 assert_eq!(frame["op"], "upsert", "unexpected frame: {frame}");
5742 assert_eq!(
5743 frame["data"],
5744 json!({"id": 7, "n": 1, "_seq": "101:000000000002"})
5745 );
5746 assert!(frame.get("seq").is_none(), "unexpected seq: {frame}");
5747 socket.close(None).await.ok();
5748 }
5749
5750 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
5759 async fn a_holder_behind_on_an_evicted_key_catches_up_when_it_returns() {
5760 let server = StateServer::start().await;
5761 server
5762 .publish("7", json!({"id": 7}), "100:000000000001")
5763 .await;
5764 let mut socket = server.subscribe("7").await;
5765 server.entity_cache.remove(ROUND, "7").await;
5766
5767 let writer = server.clone();
5768 tokio::spawn(async move {
5769 writer
5770 .publish_frame("7", "patch", json!({"a": 1}), "101:000000000001")
5771 .await;
5772 writer
5773 .publish_frame("7", "patch", json!({"b": 2}), "101:000000000002")
5774 .await;
5775 })
5776 .await
5777 .unwrap();
5778 let frame = next_frame(&mut socket).await;
5779 assert_eq!(frame["op"], "patch", "unexpected frame: {frame}");
5780 assert_eq!(frame["data"], json!({"b": 2}));
5781 assert_eq!(frame["seq"], "101:000000000002");
5782
5783 let whole = json!({"id": 7, "a": 1, "b": 2, "_seq": "101:000000000002"});
5785 server
5786 .entity_cache
5787 .store_whole(ROUND, "7", whole.clone())
5788 .await;
5789 server
5790 .publish_frame("7", "upsert", whole.clone(), "101:000000000002")
5791 .await;
5792 let frame = next_frame(&mut socket).await;
5793 assert_eq!(frame["op"], "upsert", "unexpected frame: {frame}");
5794 assert_eq!(frame["data"], whole);
5795 assert!(frame.get("seq").is_none(), "unexpected seq: {frame}");
5796
5797 server
5799 .publish("7", json!({"c": 3}), "102:000000000001")
5800 .await;
5801 let frame = next_frame(&mut socket).await;
5802 assert_eq!(frame["data"], json!({"c": 3, "_seq": "102:000000000001"}));
5803 assert_eq!(frame["seq"], "102:000000000001");
5804 socket.close(None).await.ok();
5805 }
5806 }
5807
5808 mod partial_entities_over_a_socket {
5812 use super::*;
5813 use crate::projector::Projector;
5814 use crate::{MutationBatch, SlotContext};
5815 use arete_interpreter::{AccountPosition, Mutation};
5816 use futures_util::{SinkExt, StreamExt};
5817 use std::time::Duration;
5818 use tokio::net::{TcpListener, TcpStream};
5819 use tokio::sync::mpsc;
5820 use tokio_tungstenite::tungstenite::Message;
5821 use tokio_tungstenite::{client_async, WebSocketStream};
5822
5823 fn thing_index() -> ViewIndex {
5824 let mut index = ViewIndex::new();
5825 index.add_spec(list_spec());
5826 index.add_spec(ViewSpec {
5827 id: "Thing/state".to_string(),
5828 mode: Mode::State,
5829 ..list_spec()
5830 });
5831 index
5832 }
5833
5834 struct Harness {
5835 addr: SocketAddr,
5836 tx: mpsc::Sender<MutationBatch>,
5837 slot: std::sync::atomic::AtomicU64,
5838 }
5839
5840 impl Harness {
5841 async fn start(delivery: WebSocketDeliveryConfig) -> Self {
5842 Self::start_with_cache(delivery, EntityCache::new()).await
5843 }
5844
5845 async fn start_with_cache(
5846 delivery: WebSocketDeliveryConfig,
5847 entity_cache: EntityCache,
5848 ) -> Self {
5849 Self::start_with_index(delivery, entity_cache, thing_index()).await
5850 }
5851
5852 async fn start_with_index(
5853 delivery: WebSocketDeliveryConfig,
5854 entity_cache: EntityCache,
5855 index: ViewIndex,
5856 ) -> Self {
5857 let view_index = Arc::new(index);
5858 let bus_manager = BusManager::new();
5859 let (tx, rx) = mpsc::channel::<MutationBatch>(64);
5860 tokio::spawn(tokio::task::unconstrained(
5866 Projector::new(
5867 view_index.clone(),
5868 bus_manager.clone(),
5869 entity_cache.clone(),
5870 rx,
5871 #[cfg(feature = "otel")]
5872 None,
5873 )
5874 .run(),
5875 ));
5876 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5877 let addr = listener.local_addr().unwrap();
5878 let server = WebSocketServer::new(
5879 addr,
5880 bus_manager,
5881 entity_cache,
5882 view_index,
5883 #[cfg(feature = "otel")]
5884 None,
5885 )
5886 .with_delivery_config(delivery);
5887 let (acceptor, _cleanup) = server.into_acceptor();
5888 tokio::spawn(async move { acceptor.serve_listener(listener).await });
5889 Self {
5890 addr,
5891 tx,
5892 slot: std::sync::atomic::AtomicU64::new(100),
5893 }
5894 }
5895
5896 async fn patch(&self, key: &str, patch: Value) {
5898 self.mutate(Mutation {
5899 export: "Thing".to_string(),
5900 key: json!(key),
5901 patch,
5902 append: vec![],
5903 })
5904 .await;
5905 }
5906
5907 async fn mutate(&self, mutation: Mutation) {
5908 let slot = self.slot.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
5909 self.mutate_with_context(mutation, SlotContext::new(slot, 0))
5910 .await;
5911 }
5912
5913 async fn mutate_with_context(&self, mutation: Mutation, context: SlotContext) {
5914 self.tx
5915 .send(MutationBatch::with_slot_context(
5916 vec![mutation].into_iter().collect(),
5917 context,
5918 ))
5919 .await
5920 .unwrap();
5921 let (ack, wait) = oneshot::channel();
5922 self.tx
5923 .send(MutationBatch::flush_marker(ack))
5924 .await
5925 .unwrap();
5926 wait.await.unwrap();
5927 }
5928
5929 async fn subscribe(&self, query: Value, snapshot: bool) -> WebSocketStream<TcpStream> {
5930 let stream = TcpStream::connect(self.addr).await.unwrap();
5931 let mut socket = client_async(format!("ws://{}/", self.addr), stream)
5932 .await
5933 .unwrap()
5934 .0;
5935 socket
5936 .send(Message::Text(
5937 json!({
5938 "type": "subscribe",
5939 "protocolVersion": 2,
5940 "subscriptionId": "things",
5941 "query": query,
5942 "snapshot": {"enabled": snapshot},
5943 })
5944 .to_string()
5945 .into(),
5946 ))
5947 .await
5948 .unwrap();
5949 let ack = next_frame(&mut socket).await;
5950 assert_eq!(ack["op"], "subscribed", "unexpected ack: {ack}");
5951 assert_eq!(
5952 ack["wholeEntities"],
5953 json!(true),
5954 "the ack advertises the whole-entity guarantee: {ack}"
5955 );
5956 socket
5957 }
5958 }
5959
5960 async fn next_frame(socket: &mut WebSocketStream<TcpStream>) -> Value {
5961 loop {
5962 let message = tokio::time::timeout(Duration::from_secs(10), socket.next())
5963 .await
5964 .expect("the server answers within the timeout")
5965 .expect("the stream stays open")
5966 .expect("a readable frame");
5967 let bytes = match &message {
5969 Message::Text(text) => text.as_bytes(),
5970 Message::Binary(bytes) => bytes.as_ref(),
5971 _ => continue,
5972 };
5973 return serde_json::from_slice(bytes).expect("frames are JSON");
5974 }
5975 }
5976
5977 async fn snapshot_keys(socket: &mut WebSocketStream<TcpStream>) -> Vec<String> {
5979 let mut keys = Vec::new();
5980 loop {
5981 let frame = next_frame(socket).await;
5982 assert_eq!(frame["op"], "snapshot", "expected a snapshot: {frame}");
5983 keys.extend(
5984 frame["data"]
5985 .as_array()
5986 .unwrap()
5987 .iter()
5988 .map(|row| row["key"].as_str().unwrap().to_string()),
5989 );
5990 if frame["complete"] == json!(true) {
5991 return keys;
5992 }
5993 }
5994 }
5995
5996 async fn seed(harness: &Harness) {
5997 for key in ["a", "b", "c"] {
5998 harness
5999 .patch(key, json!({"name": format!("thing-{key}"), "count": 1}))
6000 .await;
6001 }
6002 }
6003
6004 fn derived_index() -> ViewIndex {
6005 use crate::materialized_view::{
6006 CompareOp, FilterConfig, SortConfig, SortOrder, ViewPipeline,
6007 };
6008 let mut index = thing_index();
6009 for name in ["empty", "filtered", "sorted"] {
6010 let mut pipeline = ViewPipeline::default();
6011 if name != "empty" {
6012 pipeline.filter = Some(FilterConfig {
6013 field_path: vec!["owner".into()],
6014 op: CompareOp::Eq,
6015 value: json!("alice"),
6016 });
6017 pipeline.limit = Some(2);
6018 }
6019 if name == "sorted" {
6020 pipeline.sort = Some(SortConfig {
6021 field_path: vec!["amount".into()],
6022 order: SortOrder::Desc,
6023 });
6024 }
6025 index.add_spec(ViewSpec {
6026 id: format!("Thing/{name}"),
6027 source_view: Some("Thing/list".into()),
6028 pipeline: Some(pipeline),
6029 ..list_spec()
6030 });
6031 }
6032 index
6033 }
6034
6035 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6036 async fn unsorted_derived_idle_bootstrap_and_reconnect_are_authoritative() {
6037 let harness = Harness::start_with_index(
6038 WebSocketDeliveryConfig::default(),
6039 EntityCache::new(),
6040 derived_index(),
6041 )
6042 .await;
6043 harness
6044 .patch("a", json!({"owner":"alice", "amount":1}))
6045 .await;
6046 harness.patch("b", json!({"owner":"bob", "amount":2})).await;
6047 harness
6048 .patch("c", json!({"owner":"alice", "amount":3}))
6049 .await;
6050 harness
6051 .patch("d", json!({"owner":"alice", "amount":4}))
6052 .await;
6053 for _ in 0..2 {
6054 for (view, expected) in [
6055 ("empty", vec!["d", "c", "b", "a"]),
6056 ("filtered", vec!["d", "c"]),
6057 ("sorted", vec!["d", "c"]),
6058 ] {
6059 let mut socket = harness
6060 .subscribe(json!({"view": format!("Thing/{view}"), "take":100}), true)
6061 .await;
6062 assert_eq!(snapshot_keys(&mut socket).await, expected);
6063 socket.close(None).await.unwrap();
6064 }
6065 }
6066 }
6067
6068 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6069 async fn source_delete_and_recreation_clear_all_views_across_reconnect() {
6070 let harness = Harness::start_with_index(
6071 WebSocketDeliveryConfig::default(),
6072 EntityCache::new(),
6073 derived_index(),
6074 )
6075 .await;
6076 harness
6077 .patch("a", json!({"owner":"alice", "amount":1, "oldField":true}))
6078 .await;
6079 let mut list = harness
6080 .subscribe(json!({"view":"Thing/filtered"}), true)
6081 .await;
6082 assert_eq!(snapshot_keys(&mut list).await, ["a"]);
6083 let mut state = harness
6084 .subscribe(json!({"view":"Thing/state", "key":"a"}), true)
6085 .await;
6086 assert_eq!(snapshot_keys(&mut state).await, ["a"]);
6087 harness.patch("a", json!({"owner":"bob"})).await;
6089 assert_eq!(next_frame(&mut list).await["op"], "remove");
6090 harness.patch("a", json!({"owner":"alice"})).await;
6091 assert_eq!(next_frame(&mut list).await["op"], "upsert");
6092 let mut deletion = Mutation::delete("Thing", json!("a"));
6093 deletion.mark_account_position(AccountPosition::new(100, 1001));
6094 harness.mutate(deletion).await;
6095 assert_eq!(next_frame(&mut list).await["op"], "delete");
6096 loop {
6098 if next_frame(&mut state).await["op"] == "delete" {
6099 break;
6100 }
6101 }
6102 for view in [
6103 "Thing/list",
6104 "Thing/empty",
6105 "Thing/filtered",
6106 "Thing/sorted",
6107 "Thing/state",
6108 ] {
6109 let mut socket = harness
6110 .subscribe(json!({"view":view, "key":"a"}), true)
6111 .await;
6112 assert!(snapshot_keys(&mut socket).await.is_empty(), "{view}");
6113 socket.close(None).await.unwrap();
6114 }
6115 harness
6117 .patch("a", json!({"owner":"alice", "oldField":true}))
6118 .await;
6119 let mut stale = Mutation {
6120 export: "Thing".into(),
6121 key: json!("a"),
6122 patch: json!({"owner":"alice", "oldField":true}),
6123 append: vec![],
6124 };
6125 stale.mark_whole_entity();
6126 harness.mutate(stale).await;
6127 let mut socket = harness
6128 .subscribe(json!({"view":"Thing/filtered"}), true)
6129 .await;
6130 assert!(snapshot_keys(&mut socket).await.is_empty());
6131 socket.close(None).await.unwrap();
6132 let mut recreated = Mutation {
6133 export: "Thing".into(),
6134 key: json!("a"),
6135 patch: json!({"owner":"alice", "amount":9}),
6136 append: vec![],
6137 };
6138 recreated.mark_created();
6139 recreated.mark_account_position(AccountPosition::new(100, 1002));
6140 harness.mutate(recreated).await;
6141 for view in [
6142 "Thing/list",
6143 "Thing/empty",
6144 "Thing/filtered",
6145 "Thing/sorted",
6146 "Thing/state",
6147 ] {
6148 let mut socket = harness
6149 .subscribe(json!({"view":view, "key":"a"}), true)
6150 .await;
6151 let frame = next_frame(&mut socket).await;
6152 assert_eq!(frame["data"][0]["key"], "a");
6153 assert_eq!(frame["data"][0]["data"]["amount"], 9);
6154 assert!(
6155 frame["data"][0]["data"].get("oldField").is_none(),
6156 "{frame}"
6157 );
6158 socket.close(None).await.unwrap();
6159 }
6160 }
6161
6162 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6163 async fn resolver_offsets_do_not_suppress_later_instruction_updates() {
6164 let harness = Harness::start(WebSocketDeliveryConfig::default()).await;
6165 let mut created = Mutation {
6166 export: "Thing".into(),
6167 key: json!("a"),
6168 patch: json!({"value":"created"}),
6169 append: vec![],
6170 };
6171 created.mark_created();
6172 created.mark_account_position(AccountPosition::new(100, 9));
6173 harness
6174 .mutate_with_context(created, SlotContext::account(100, 9))
6175 .await;
6176
6177 harness
6178 .mutate_with_context(
6179 Mutation {
6180 export: "Thing".into(),
6181 key: json!("a"),
6182 patch: json!({"value":"resolver", "resolved":true}),
6183 append: vec![],
6184 },
6185 SlotContext::resolver(100, 1_u64 << 63),
6186 )
6187 .await;
6188 harness
6189 .mutate_with_context(
6190 Mutation {
6191 export: "Thing".into(),
6192 key: json!("a"),
6193 patch: json!({"value":"instruction", "instruction":true}),
6194 append: vec![],
6195 },
6196 SlotContext::instruction(100, 900),
6197 )
6198 .await;
6199
6200 let mut socket = harness
6201 .subscribe(json!({"view":"Thing/state", "key":"a"}), true)
6202 .await;
6203 let frame = next_frame(&mut socket).await;
6204 assert_eq!(frame["op"], "snapshot", "{frame}");
6205 let row = &frame["data"][0]["data"];
6206 assert_eq!(row["value"], "instruction", "{frame}");
6207 assert_eq!(row["resolved"], true, "{frame}");
6208 assert_eq!(row["instruction"], true, "{frame}");
6209
6210 harness
6213 .mutate_with_context(
6214 Mutation {
6215 export: "Thing".into(),
6216 key: json!("a"),
6217 patch: json!({"value":"stale", "stale":true}),
6218 append: vec![],
6219 },
6220 SlotContext::instruction(100, 899),
6221 )
6222 .await;
6223 let mut reconnected = harness
6224 .subscribe(json!({"view":"Thing/state", "key":"a"}), true)
6225 .await;
6226 let frame = next_frame(&mut reconnected).await;
6227 let row = &frame["data"][0]["data"];
6228 assert_eq!(row["value"], "instruction", "{frame}");
6229 assert!(row.get("stale").is_none(), "{frame}");
6230 }
6231
6232 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
6233 async fn projected_delete_and_recreation_between_reads_replace_state() {
6234 let harness = Harness::start(WebSocketDeliveryConfig::default()).await;
6235 harness.patch("a", json!({"old":true})).await;
6236 let mut socket = harness
6237 .subscribe(json!({"view":"Thing/state", "key":"a"}), true)
6238 .await;
6239 assert_eq!(snapshot_keys(&mut socket).await, ["a"]);
6240 let mut creation = Mutation {
6241 export: "Thing".into(),
6242 key: json!("a"),
6243 patch: json!({"fresh":true}),
6244 append: vec![],
6245 };
6246 creation.mark_created();
6247 for mutation in [Mutation::delete("Thing", json!("a")), creation] {
6249 let slot = harness
6250 .slot
6251 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
6252 harness
6253 .tx
6254 .try_send(MutationBatch::with_slot_context(
6255 vec![mutation].into_iter().collect(),
6256 SlotContext::new(slot, 0),
6257 ))
6258 .unwrap();
6259 }
6260 let (ack, wait) = oneshot::channel();
6261 harness
6262 .tx
6263 .try_send(MutationBatch::flush_marker(ack))
6264 .unwrap();
6265 wait.await.unwrap();
6266 let mut frame = next_frame(&mut socket).await;
6271 if frame["op"] == "delete" {
6272 assert_eq!(frame["key"], "a", "{frame}");
6273 frame = next_frame(&mut socket).await;
6274 }
6275 assert_eq!(frame["op"], "upsert", "{frame}");
6276 assert_eq!(frame["data"]["fresh"], true);
6277 assert!(frame["data"].get("old").is_none(), "{frame}");
6278 }
6279
6280 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6281 async fn a_change_to_a_key_cut_by_snapshot_limit_is_an_upsert() {
6282 let harness = Harness::start(WebSocketDeliveryConfig::default()).await;
6283 seed(&harness).await;
6284
6285 let mut socket = harness
6286 .subscribe(json!({"view": "Thing/list", "snapshotLimit": 1}), true)
6287 .await;
6288 assert_eq!(snapshot_keys(&mut socket).await, ["c"]);
6290
6291 harness.patch("a", json!({"count": 2})).await;
6292 let frame = next_frame(&mut socket).await;
6293 assert_eq!(frame["key"], "a", "unexpected frame: {frame}");
6294 assert_eq!(frame["op"], "upsert", "a key never sent arrives whole");
6295 assert_eq!(frame["data"]["name"], "thing-a");
6296 assert_eq!(frame["data"]["count"], 2);
6297
6298 harness.patch("a", json!({"count": 3})).await;
6300 let frame = next_frame(&mut socket).await;
6301 assert_eq!(frame["key"], "a");
6302 assert_eq!(frame["op"], "patch", "unexpected frame: {frame}");
6303 assert!(frame["data"].get("name").is_none());
6304
6305 harness.patch("c", json!({"count": 2})).await;
6307 let frame = next_frame(&mut socket).await;
6308 assert_eq!(frame["key"], "c");
6309 assert_eq!(frame["op"], "patch", "unexpected frame: {frame}");
6310 socket.close(None).await.ok();
6311 }
6312
6313 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6314 async fn a_list_without_a_snapshot_upserts_each_key_on_its_first_change() {
6315 let harness = Harness::start(WebSocketDeliveryConfig::default()).await;
6316 seed(&harness).await;
6317
6318 let mut socket = harness
6319 .subscribe(json!({"view": "Thing/list"}), false)
6320 .await;
6321 harness.patch("b", json!({"count": 2})).await;
6322 let frame = next_frame(&mut socket).await;
6323 assert_eq!(frame["key"], "b");
6324 assert_eq!(frame["op"], "upsert", "unexpected frame: {frame}");
6325 assert_eq!(frame["data"]["name"], "thing-b");
6326
6327 harness.patch("b", json!({"count": 3})).await;
6328 let frame = next_frame(&mut socket).await;
6329 assert_eq!(frame["key"], "b");
6330 assert_eq!(frame["op"], "patch", "unexpected frame: {frame}");
6331 socket.close(None).await.ok();
6332 }
6333
6334 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
6342 async fn a_key_entering_a_list_is_not_sent_the_patch_its_upsert_carries() {
6343 let harness = Harness::start(WebSocketDeliveryConfig::default()).await;
6344 seed(&harness).await;
6345 let mut socket = harness
6346 .subscribe(json!({"view": "Thing/list"}), false)
6347 .await;
6348
6349 let slot = harness
6350 .slot
6351 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
6352 let mut creation = Mutation {
6353 export: "Thing".to_string(),
6354 key: json!("n"),
6355 patch: json!({"name": "thing-n"}),
6356 append: vec![],
6357 };
6358 creation.mark_created();
6359 let joined = Mutation {
6360 export: "Thing".to_string(),
6361 key: json!("n"),
6362 patch: json!({"flag": true}),
6363 append: vec![],
6364 };
6365 harness
6366 .tx
6367 .send(MutationBatch::with_slot_context(
6368 vec![creation, joined].into_iter().collect(),
6369 SlotContext::new(slot, 0),
6370 ))
6371 .await
6372 .unwrap();
6373
6374 let upsert = next_frame(&mut socket).await;
6375 assert_eq!(upsert["key"], "n");
6376 assert_eq!(upsert["op"], "upsert", "unexpected frame: {upsert}");
6377 assert_eq!(upsert["data"]["name"], "thing-n");
6378 assert_eq!(upsert["data"]["flag"], true);
6379 let (_, sent) = version_of(&upsert["data"]);
6380
6381 harness.patch("n", json!({"count": 9})).await;
6383 let patch = next_frame(&mut socket).await;
6384 assert_eq!(patch["key"], "n");
6385 assert_eq!(patch["op"], "patch", "unexpected frame: {patch}");
6386 assert_eq!(patch["data"]["count"], 9, "unexpected frame: {patch}");
6387 let (_, patched) = version_of(&patch["data"]);
6388 assert!(patched > sent, "{patched} after {sent}");
6389 socket.close(None).await.ok();
6390 }
6391
6392 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6396 async fn a_state_holder_keeps_its_patches_when_the_cache_evicts_the_key() {
6397 let harness = Harness::start_with_cache(
6398 WebSocketDeliveryConfig::default(),
6399 EntityCache::with_config(crate::cache::EntityCacheConfig {
6400 max_entities_per_view: 2,
6401 ..Default::default()
6402 }),
6403 )
6404 .await;
6405 harness
6406 .patch("a", json!({"name": "thing-a", "count": 1}))
6407 .await;
6408 let mut socket = harness
6409 .subscribe(json!({"view": "Thing/state", "key": "a"}), true)
6410 .await;
6411 assert_eq!(snapshot_keys(&mut socket).await, ["a"]);
6412
6413 for key in ["b", "c"] {
6415 harness
6416 .patch(key, json!({"name": format!("thing-{key}"), "count": 1}))
6417 .await;
6418 }
6419 harness.patch("a", json!({"count": 2})).await;
6420 let frame = next_frame(&mut socket).await;
6421 assert_eq!(frame["key"], "a", "unexpected frame: {frame}");
6422 assert_eq!(frame["op"], "patch", "the holder merges it: {frame}");
6423 assert_eq!(frame["data"]["count"], 2);
6424
6425 let mut whole = Mutation {
6427 export: "Thing".to_string(),
6428 key: json!("a"),
6429 patch: json!({"name": "thing-a", "count": 2}),
6430 append: vec![],
6431 };
6432 whole.mark_whole_entity();
6433 harness.mutate(whole).await;
6434 let frame = next_frame(&mut socket).await;
6435 assert_eq!(frame["op"], "upsert", "unexpected frame: {frame}");
6436 assert_eq!(frame["data"]["name"], "thing-a");
6437 assert_eq!(frame["data"]["count"], 2);
6438 assert!(frame["data"]
6439 .get(arete_interpreter::WHOLE_ENTITY_MARKER)
6440 .is_none());
6441
6442 harness.patch("a", json!({"count": 3})).await;
6443 let frame = next_frame(&mut socket).await;
6444 assert_eq!(frame["op"], "patch", "unexpected frame: {frame}");
6445 socket.close(None).await.ok();
6446 }
6447
6448 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6449 async fn a_state_view_without_a_snapshot_upserts_on_the_first_change() {
6450 let harness = Harness::start(WebSocketDeliveryConfig::default()).await;
6451 seed(&harness).await;
6452
6453 let mut socket = harness
6454 .subscribe(json!({"view": "Thing/state", "key": "a"}), false)
6455 .await;
6456 harness.patch("a", json!({"count": 2})).await;
6457 let frame = next_frame(&mut socket).await;
6458 assert_eq!(frame["key"], "a");
6459 assert_eq!(frame["op"], "upsert", "unexpected frame: {frame}");
6460 assert_eq!(frame["data"]["name"], "thing-a");
6461 assert_eq!(frame["data"]["count"], 2);
6462
6463 harness.patch("a", json!({"count": 3})).await;
6464 let frame = next_frame(&mut socket).await;
6465 assert_eq!(frame["op"], "patch", "unexpected frame: {frame}");
6466 socket.close(None).await.ok();
6467 }
6468
6469 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6470 async fn a_coalesced_list_sends_no_remove_for_a_key_it_never_sent() {
6471 let harness = Harness::start(WebSocketDeliveryConfig {
6472 collection_coalesce_ms: Some(10),
6473 ..Default::default()
6474 })
6475 .await;
6476 seed(&harness).await;
6477
6478 let mut socket = harness
6481 .subscribe(json!({"view": "Thing/list", "take": 1}), false)
6482 .await;
6483 harness.patch("a", json!({"count": 2})).await;
6486 let frame = next_frame(&mut socket).await;
6487 assert_eq!(frame["key"], "a", "unexpected frame: {frame}");
6488 assert_eq!(frame["op"], "upsert");
6489 assert_eq!(frame["data"]["name"], "thing-a");
6490
6491 harness.patch("c", json!({"count": 2})).await;
6493 let mut frames = [next_frame(&mut socket).await, next_frame(&mut socket).await];
6494 frames.sort_by_key(|frame| frame["key"].as_str().unwrap().to_string());
6495 assert_eq!(frames[0]["key"], "a");
6496 assert_eq!(frames[0]["op"], "remove");
6497 assert_eq!(frames[1]["key"], "c");
6498 assert_eq!(frames[1]["op"], "upsert");
6499 assert_eq!(frames[1]["data"]["name"], "thing-c");
6500 socket.close(None).await.ok();
6501 }
6502
6503 fn version_of(data: &Value) -> (String, u64) {
6505 let version = data["_version"]
6506 .as_str()
6507 .unwrap_or_else(|| panic!("no _version in {data}"));
6508 let (epoch, counter) = version.split_once(':').expect("epoch:counter");
6509 (epoch.to_string(), counter.parse().expect("decimal counter"))
6510 }
6511
6512 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
6521 async fn snapshot_rows_patches_and_catch_ups_carry_the_version() {
6522 let harness = Harness::start(WebSocketDeliveryConfig::default()).await;
6523 harness.patch("7", json!({"name": "thing-7"})).await;
6524
6525 let mut list = harness.subscribe(json!({"view": "Thing/list"}), true).await;
6526 let snapshot = next_frame(&mut list).await;
6527 assert_eq!(snapshot["op"], "snapshot", "unexpected frame: {snapshot}");
6528 let (epoch, _) = version_of(&snapshot["data"][0]["data"]);
6529
6530 let mut state = harness
6531 .subscribe(json!({"view": "Thing/state", "key": "7"}), true)
6532 .await;
6533 let snapshot = next_frame(&mut state).await;
6534 assert_eq!(snapshot["op"], "snapshot", "unexpected frame: {snapshot}");
6535 let (state_epoch, seeded) = version_of(&snapshot["data"][0]["data"]);
6536 assert_eq!(state_epoch, epoch, "one projector, one epoch");
6537
6538 harness.patch("7", json!({"count": 1})).await;
6539 let patch = next_frame(&mut list).await;
6540 assert_eq!(patch["op"], "patch", "unexpected frame: {patch}");
6541 assert_eq!(version_of(&patch["data"]).0, epoch);
6542 let patch = next_frame(&mut state).await;
6543 assert_eq!(patch["op"], "patch", "unexpected frame: {patch}");
6544 let (_, patched) = version_of(&patch["data"]);
6545 assert!(patched > seeded, "{patched} after {seeded}");
6546
6547 let slot = harness
6549 .slot
6550 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
6551 let mutation = |patch: Value| Mutation {
6552 export: "Thing".to_string(),
6553 key: json!("7"),
6554 patch,
6555 append: vec![],
6556 };
6557 harness
6558 .tx
6559 .send(MutationBatch::with_slot_context(
6560 vec![
6561 mutation(json!({"count": 2})),
6562 mutation(json!({"flag": true})),
6563 ]
6564 .into_iter()
6565 .collect(),
6566 SlotContext::new(slot, 0),
6567 ))
6568 .await
6569 .unwrap();
6570
6571 let catch_up = next_frame(&mut state).await;
6572 assert_eq!(catch_up["op"], "patch", "unexpected frame: {catch_up}");
6573 assert!(catch_up.get("seq").is_none(), "a catch-up: {catch_up}");
6574 assert_eq!(catch_up["data"]["count"], 2);
6575 assert_eq!(catch_up["data"]["flag"], true);
6576 let (_, caught_up) = version_of(&catch_up["data"]);
6577 assert!(caught_up > patched, "{caught_up} after {patched}");
6578
6579 list.close(None).await.ok();
6580 state.close(None).await.ok();
6581 }
6582 }
6583
6584 mod session_expiry {
6586 use super::*;
6587 use crate::websocket::auth::SignedSessionAuthPlugin;
6588 use arete_auth::{KeyClass, SessionClaims, SigningKey, TokenSigner, TokenVerifier};
6589 use futures_util::{SinkExt, StreamExt};
6590 use std::time::Duration;
6591 use tokio::net::{TcpListener, TcpStream};
6592 use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
6593 use tokio_tungstenite::tungstenite::protocol::CloseFrame;
6594 use tokio_tungstenite::tungstenite::Message;
6595 use tokio_tungstenite::{client_async, WebSocketStream};
6596
6597 #[derive(Default)]
6598 struct RecordingUsageEmitter {
6599 events: tokio::sync::Mutex<Vec<WebSocketUsageEvent>>,
6600 }
6601
6602 #[async_trait::async_trait]
6603 impl WebSocketUsageEmitter for RecordingUsageEmitter {
6604 async fn emit(&self, event: WebSocketUsageEvent) {
6605 self.events.lock().await.push(event);
6606 }
6607 }
6608
6609 struct Server {
6610 addr: SocketAddr,
6611 signer: TokenSigner,
6612 usage: Arc<RecordingUsageEmitter>,
6613 metrics: Arc<DeliveryProbe>,
6614 }
6615
6616 impl Server {
6617 async fn start() -> Self {
6618 let signing_key = SigningKey::generate();
6619 let verifier =
6620 TokenVerifier::new(signing_key.verifying_key(), "test-issuer", "test-audience");
6621 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6622 let addr = listener.local_addr().unwrap();
6623 let usage = Arc::new(RecordingUsageEmitter::default());
6624 let server = WebSocketServer::new(
6625 addr,
6626 BusManager::new(),
6627 EntityCache::new(),
6628 Arc::new(ViewIndex::new()),
6629 #[cfg(feature = "otel")]
6630 None,
6631 )
6632 .with_auth_plugin(Arc::new(SignedSessionAuthPlugin::new(verifier)))
6633 .with_usage_emitter(usage.clone());
6634 let metrics = Arc::new(DeliveryProbe::default());
6635 let (acceptor, _cleanup) = server.into_acceptor();
6636 let acceptor = acceptor.with_delivery_probe(metrics.clone());
6637 tokio::spawn(async move { acceptor.serve_listener(listener).await });
6638 Self {
6639 addr,
6640 signer: TokenSigner::new(signing_key, "test-issuer"),
6641 usage,
6642 metrics,
6643 }
6644 }
6645
6646 fn token(&self, ttl_seconds: u64) -> String {
6647 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
6648 .with_scope("read")
6649 .with_key_class(KeyClass::Secret)
6650 .with_ttl(ttl_seconds)
6651 .build();
6652 self.signer.sign(claims).unwrap()
6653 }
6654
6655 fn account_token(&self, ttl_seconds: u64, account: &str) -> String {
6656 let claims = SessionClaims::builder("test-issuer", account, "test-audience")
6657 .with_scope("read")
6658 .with_key_class(KeyClass::Secret)
6659 .with_metering_key(account)
6660 .with_plan("agent_trial")
6661 .with_actor_key(account)
6662 .with_account_key(account)
6663 .with_consumer_key(format!("consumer:{account}"))
6664 .with_policy_version(3)
6665 .with_account_limits(Default::default())
6666 .with_ttl(ttl_seconds)
6667 .build();
6668 self.signer.sign(claims).unwrap()
6669 }
6670
6671 async fn connect(&self, token: &str) -> WebSocketStream<TcpStream> {
6672 let stream = TcpStream::connect(self.addr).await.unwrap();
6673 client_async(format!("ws://{}/?hs_token={token}", self.addr), stream)
6674 .await
6675 .unwrap()
6676 .0
6677 }
6678 }
6679
6680 async fn send_json(socket: &mut WebSocketStream<TcpStream>, message: Value) {
6681 socket
6682 .send(Message::Text(message.to_string().into()))
6683 .await
6684 .unwrap();
6685 }
6686
6687 async fn close_within(
6689 socket: &mut WebSocketStream<TcpStream>,
6690 wait: Duration,
6691 ) -> Option<Option<CloseFrame>> {
6692 tokio::time::timeout(wait, async {
6693 while let Some(Ok(message)) = socket.next().await {
6694 if let Message::Close(frame) = message {
6695 return Some(frame);
6696 }
6697 }
6698 Some(None)
6699 })
6700 .await
6701 .ok()
6702 .flatten()
6703 }
6704
6705 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6706 async fn an_expired_session_is_closed_with_the_reason() {
6707 let server = Server::start().await;
6708 let mut socket = server.connect(&server.token(2)).await;
6709
6710 tokio::time::sleep(Duration::from_secs(3)).await;
6711 send_json(&mut socket, json!({"type": "ping"})).await;
6712
6713 let frame = close_within(&mut socket, Duration::from_secs(5))
6714 .await
6715 .expect("the server closes the expired session")
6716 .expect("the close frame carries a reason");
6717 assert_eq!(frame.code, CloseCode::Policy);
6718 assert_eq!(
6719 frame.reason.as_str(),
6720 "token-expired: Authentication token expired"
6721 );
6722 }
6723
6724 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6725 async fn a_session_refreshed_in_band_outlives_its_first_token() {
6726 let server = Server::start().await;
6727 let mut socket = server.connect(&server.token(2)).await;
6728
6729 send_json(
6730 &mut socket,
6731 json!({"type": "refresh_auth", "token": server.token(3_600)}),
6732 )
6733 .await;
6734 let reply = tokio::time::timeout(Duration::from_secs(5), async {
6735 while let Some(Ok(message)) = socket.next().await {
6736 if let Message::Text(text) = message {
6737 return serde_json::from_str::<Value>(text.as_str()).ok();
6738 }
6739 }
6740 None
6741 })
6742 .await
6743 .expect("the server answers the refresh")
6744 .expect("the answer is JSON");
6745 assert_eq!(reply["success"], true, "refresh accepted: {reply}");
6746
6747 tokio::time::sleep(Duration::from_secs(3)).await;
6748 send_json(&mut socket, json!({"type": "ping"})).await;
6749 assert!(
6750 close_within(&mut socket, Duration::from_millis(1_500))
6751 .await
6752 .is_none(),
6753 "the socket stays open on the refreshed token"
6754 );
6755 }
6756
6757 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6758 async fn usage_after_auth_refresh_uses_the_refreshed_account() {
6759 let server = Server::start().await;
6760 let mut socket = server
6761 .connect(&server.account_token(3_600, "account:1"))
6762 .await;
6763
6764 send_json(
6765 &mut socket,
6766 json!({
6767 "type": "refresh_auth",
6768 "token": server.account_token(3_600, "account:2")
6769 }),
6770 )
6771 .await;
6772 let reply = tokio::time::timeout(Duration::from_secs(5), async {
6773 while let Some(Ok(message)) = socket.next().await {
6774 if let Message::Text(text) = message {
6775 return serde_json::from_str::<Value>(text.as_str()).ok();
6776 }
6777 }
6778 None
6779 })
6780 .await
6781 .expect("the server answers the refresh")
6782 .expect("the answer is JSON");
6783 assert_eq!(reply["success"], true, "refresh accepted: {reply}");
6784 socket.close(None).await.unwrap();
6785
6786 let account = tokio::time::timeout(Duration::from_secs(5), async {
6787 loop {
6788 let events = server.usage.events.lock().await;
6789 if let Some(account) = events.iter().find_map(|event| match event {
6790 WebSocketUsageEvent::ConnectionClosed { identity, .. } => {
6791 identity.account_key.clone()
6792 }
6793 _ => None,
6794 }) {
6795 return account;
6796 }
6797 drop(events);
6798 tokio::task::yield_now().await;
6799 }
6800 })
6801 .await
6802 .expect("connection close usage event");
6803 assert_eq!(account, "account:2");
6804 let active = server
6805 .metrics
6806 .active_connections
6807 .lock()
6808 .expect("delivery probe lock poisoned");
6809 assert_eq!(active.get("account:1"), Some(&0));
6810 assert_eq!(active.get("account:2"), None);
6811 }
6812 }
6813}
6814
6815#[cfg(test)]
6816mod retained_scope_regressions {
6817 use super::*;
6818 use crate::row_scope::{RowScope, ScopeField};
6819
6820 #[test]
6821 fn sparse_private_frames_never_inherit_a_later_public_scope() {
6822 let public = RowFilter::new(
6823 ScopeField::new("meta.scope"),
6824 Default::default(),
6825 Arc::new(tokio::sync::RwLock::new(0)),
6826 0,
6827 );
6828 let entitled = RowFilter::new(
6829 ScopeField::new("meta.scope"),
6830 ["B".to_string()].into_iter().collect(),
6831 Arc::new(tokio::sync::RwLock::new(0)),
6832 0,
6833 );
6834 let patch = br#"{"data":{"secret":"private-at-the-time"}}"#;
6835 let original_scope = RowScope::Scoped("B".into());
6836 assert!(!frame_in_row_scopes(
6837 Some(&public),
6838 Some(&original_scope),
6839 patch
6840 ));
6841 assert!(frame_in_row_scopes(
6842 Some(&entitled),
6843 Some(&original_scope),
6844 patch
6845 ));
6846 assert!(frame_in_row_scopes(
6848 Some(&public),
6849 Some(&RowScope::Public),
6850 br#"{"data":{"meta":{"scope":null}}}"#
6851 ));
6852 assert!(!frame_in_row_scopes(Some(&public), None, patch));
6854 assert!(!frame_in_row_scopes(Some(&entitled), None, patch));
6855 assert!(frame_in_row_scopes(None, None, patch));
6856 }
6857}
6858
6859#[cfg(test)]
6860mod limits_tests {
6861 use super::*;
6862 use futures_util::SinkExt;
6863 use tokio::io::AsyncReadExt;
6864 use tokio_tungstenite::client_async;
6865 use tokio_tungstenite::tungstenite::Message;
6866
6867 fn limits() -> WebSocketLimitsConfig {
6868 WebSocketLimitsConfig {
6869 max_message_bytes: 16 * 1024,
6870 max_frame_bytes: 16 * 1024,
6871 handshake_timeout: Duration::from_millis(300),
6872 max_pending_handshakes: 1,
6873 }
6874 }
6875
6876 async fn start(limits: WebSocketLimitsConfig) -> SocketAddr {
6877 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6878 let addr = listener.local_addr().unwrap();
6879 let server = WebSocketServer::new(
6880 addr,
6881 BusManager::new(),
6882 EntityCache::new(),
6883 Arc::new(ViewIndex::new()),
6884 #[cfg(feature = "otel")]
6885 None,
6886 )
6887 .with_limits(limits);
6888 let (acceptor, _cleanup) = server.into_acceptor();
6889 tokio::spawn(async move { acceptor.serve_listener(listener).await });
6890 addr
6891 }
6892
6893 async fn closed_within(stream: &mut TcpStream, within: Duration) {
6895 let mut buffer = [0; 1024];
6896 tokio::time::timeout(within, async {
6897 loop {
6898 match stream.read(&mut buffer).await {
6899 Ok(0) | Err(_) => return,
6900 Ok(_) => continue,
6901 }
6902 }
6903 })
6904 .await
6905 .expect("the server closes the connection");
6906 }
6907
6908 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6909 async fn a_connection_that_never_upgrades_is_dropped_at_the_deadline() {
6910 let addr = start(limits()).await;
6911 let mut stream = TcpStream::connect(addr).await.unwrap();
6912 let started = Instant::now();
6913 closed_within(&mut stream, Duration::from_secs(5)).await;
6914 assert!(started.elapsed() >= Duration::from_millis(250));
6915 }
6916
6917 struct SlowAuthPlugin {
6919 delay: Duration,
6920 }
6921
6922 #[async_trait::async_trait]
6923 impl WebSocketAuthPlugin for SlowAuthPlugin {
6924 async fn authorize(&self, request: &ConnectionAuthRequest) -> AuthDecision {
6925 tokio::time::sleep(self.delay).await;
6926 crate::websocket::auth::AllowAllAuthPlugin
6927 .authorize(request)
6928 .await
6929 }
6930
6931 fn as_any(&self) -> &dyn std::any::Any {
6932 self
6933 }
6934 }
6935
6936 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6937 async fn authentication_slower_than_the_deadline_is_cut_off() {
6938 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6939 let addr = listener.local_addr().unwrap();
6940 let server = WebSocketServer::new(
6941 addr,
6942 BusManager::new(),
6943 EntityCache::new(),
6944 Arc::new(ViewIndex::new()),
6945 #[cfg(feature = "otel")]
6946 None,
6947 )
6948 .with_auth_plugin(Arc::new(SlowAuthPlugin {
6949 delay: Duration::from_secs(30),
6950 }))
6951 .with_limits(limits());
6952 let (acceptor, _cleanup) = server.into_acceptor();
6953 tokio::spawn(async move { acceptor.serve_listener(listener).await });
6954
6955 let started = Instant::now();
6956 let stream = TcpStream::connect(addr).await.unwrap();
6957 let handshake = tokio::time::timeout(
6958 Duration::from_secs(5),
6959 client_async(format!("ws://{addr}/"), stream),
6960 )
6961 .await
6962 .expect("the server ends the handshake before the test deadline");
6963 assert!(handshake.is_err(), "the upgrade is refused");
6964 assert!(started.elapsed() < Duration::from_secs(5));
6965 }
6966
6967 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6968 async fn pending_handshakes_past_the_cap_are_refused() {
6969 let addr = start(WebSocketLimitsConfig {
6970 handshake_timeout: Duration::from_secs(30),
6971 ..limits()
6972 })
6973 .await;
6974 let mut stalled = TcpStream::connect(addr).await.unwrap();
6975 tokio::time::sleep(Duration::from_millis(100)).await;
6977 let mut refused = TcpStream::connect(addr).await.unwrap();
6978 closed_within(&mut refused, Duration::from_secs(5)).await;
6979
6980 let mut buffer = [0; 16];
6982 assert!(
6983 tokio::time::timeout(Duration::from_millis(200), stalled.read(&mut buffer))
6984 .await
6985 .is_err()
6986 );
6987 }
6988
6989 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6990 async fn a_message_over_the_limit_closes_the_socket() {
6991 let addr = start(limits()).await;
6992 let stream = TcpStream::connect(addr).await.unwrap();
6993 let (mut socket, _) = client_async(format!("ws://{addr}/"), stream).await.unwrap();
6994 socket
6995 .send(Message::Text("x".repeat(64 * 1024).into()))
6996 .await
6997 .unwrap();
6998 let ended = tokio::time::timeout(Duration::from_secs(5), async {
6999 loop {
7000 match socket.next().await {
7001 None | Some(Err(_)) | Some(Ok(Message::Close(_))) => return,
7002 Some(Ok(_)) => continue,
7003 }
7004 }
7005 })
7006 .await;
7007 assert!(ended.is_ok(), "the server ends the session");
7008 }
7009
7010 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
7015 async fn an_evicted_session_ends_even_when_the_peer_ignores_the_close() {
7016 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
7017 let addr = listener.local_addr().unwrap();
7018 let server = WebSocketServer::new(
7019 addr,
7020 BusManager::new(),
7021 EntityCache::new(),
7022 Arc::new(ViewIndex::new()),
7023 #[cfg(feature = "otel")]
7024 None,
7025 )
7026 .with_rate_limit_config(RateLimitConfig::default().with_timeout(Duration::from_millis(300)))
7029 .with_limits(limits());
7030 let (acceptor, _cleanup) = server.into_acceptor();
7031 let probe = acceptor.clone();
7032 tokio::spawn(async move { acceptor.serve_listener(listener).await });
7033
7034 let stream = TcpStream::connect(addr).await.unwrap();
7035 let (_socket, _) = client_async(format!("ws://{addr}/"), stream).await.unwrap();
7037 tokio::time::timeout(Duration::from_secs(5), async {
7038 while probe.client_manager.client_count() == 0 {
7039 tokio::time::sleep(Duration::from_millis(10)).await;
7040 }
7041 })
7042 .await
7043 .expect("the client registers");
7044
7045 tokio::time::sleep(Duration::from_millis(400)).await;
7048 probe.client_manager.cleanup_stale_clients();
7049 assert_eq!(probe.client_manager.client_count(), 0);
7050 tokio::time::timeout(Duration::from_secs(5), async {
7051 while !probe.sessions.is_empty() {
7052 tokio::time::sleep(Duration::from_millis(10)).await;
7053 }
7054 })
7055 .await
7056 .expect("the evicted session ends");
7057 }
7058}