1use std::{collections::HashMap, sync::Arc};
5
6use reifydb_client::{HydrationConfig as ClientHydrationConfig, SubscriptionConfig as ClientSubscriptionConfig};
7use reifydb_core::{
8 common::CommitVersion,
9 interface::catalog::{id::SubscriptionId, subscription::HydrationConfig},
10 metric::ExecutionMetrics,
11 value::column::columns::Columns,
12};
13use reifydb_engine::subscription::{HydrateError, SubscriptionServiceRef};
14use reifydb_remote_proxy::{RemoteSubscription, connect_remote, proxy_remote_to_sink};
15use reifydb_subscription::{batch::BatchId, delivery::DeliveryResult};
16use reifydb_transaction::multi::lease::VersionLeaseGuard;
17use reifydb_value::{
18 reifydb_assertions,
19 value::{duration::Duration, frame::frame::Frame, identity::IdentityId},
20};
21use tokio::{spawn, sync::watch::Receiver as WatchReceiver, task::JoinHandle};
22use tracing::{debug, warn};
23
24use crate::{
25 interceptor::RequestMetadata,
26 state::AppState,
27 subscription::{
28 cleanup::cleanup_subscription,
29 create::{CreateSubscriptionResult, create_subscription},
30 errors::CreateSubscriptionError,
31 hydrate::run_hydrate,
32 registry::{ConnectionId, PromoteResult, SubscriptionRegistry},
33 wire_sink::{BatchSubscribedMember, WireSink},
34 },
35};
36
37#[derive(Debug, Clone)]
38pub struct BatchMemberInfo {
39 pub index: usize,
40 pub subscription_id: SubscriptionId,
41}
42
43pub struct BatchAck {
44 pub batch_id: BatchId,
45 pub members: Vec<BatchMemberInfo>,
46 pub remote_handles: Vec<JoinHandle<()>>,
47}
48
49pub struct SubscribeAck {
50 pub subscription_id: SubscriptionId,
51 pub remote_handle: Option<JoinHandle<()>>,
52}
53
54#[derive(Debug)]
55pub enum SubscribeError {
56 Create(CreateSubscriptionError),
57 RemoteConnect(String),
58 InvalidRemoteId,
59 LeaseFailed {
60 code: &'static str,
61 message: String,
62 },
63 HydrationBackpressure,
64 HydrationFailed {
65 error: HydrateError,
66 rql: String,
67 max_rows: u64,
68 },
69 HydrationServiceUnavailable(String),
70}
71
72#[derive(Debug)]
73pub enum BatchSubscribeError {
74 Empty,
75 Create(CreateSubscriptionError),
76 RemoteConnect(String),
77 InvalidRemoteId,
78 LeaseFailed {
79 code: &'static str,
80 message: String,
81 },
82 HydrationBackpressure,
83 HydrationFailed {
84 error: HydrateError,
85 rql: String,
86 max_rows: u64,
87 },
88 HydrationServiceUnavailable(String),
89}
90
91enum ResolvedBatchMember {
92 Local {
93 index: usize,
94 subscription_id: SubscriptionId,
95 query: String,
96 },
97 Remote {
98 index: usize,
99 subscription_id: SubscriptionId,
100 remote_sub: Box<RemoteSubscription>,
101 },
102}
103
104impl ResolvedBatchMember {
105 fn index(&self) -> usize {
106 match self {
107 Self::Local {
108 index,
109 ..
110 }
111 | Self::Remote {
112 index,
113 ..
114 } => *index,
115 }
116 }
117
118 fn subscription_id(&self) -> SubscriptionId {
119 match self {
120 Self::Local {
121 subscription_id,
122 ..
123 }
124 | Self::Remote {
125 subscription_id,
126 ..
127 } => *subscription_id,
128 }
129 }
130}
131
132#[allow(clippy::too_many_arguments)]
133pub async fn handle_subscribe<S: WireSink>(
134 state: &AppState,
135 connection_id: ConnectionId,
136 identity: IdentityId,
137 rql: String,
138 sink: S,
139 registry: &Arc<SubscriptionRegistry<S>>,
140 format: S::Format,
141 shutdown: WatchReceiver<bool>,
142 metadata: RequestMetadata,
143) -> Result<SubscribeAck, SubscribeError> {
144 match create_subscription(state, identity, &rql, metadata).await {
145 Ok(CreateSubscriptionResult::Local {
146 id: subscription_id,
147 hydration,
148 throttle,
149 linger: _,
150 }) => {
151 handle_subscribe_local(
152 state,
153 connection_id,
154 identity,
155 rql,
156 sink,
157 registry,
158 format,
159 subscription_id,
160 hydration,
161 throttle,
162 )
163 .await
164 }
165 Ok(CreateSubscriptionResult::Remote {
166 address,
167 body,
168 token: ns_token,
169 hydration,
170 throttle,
171 linger,
172 }) => {
173 handle_subscribe_remote(
174 connection_id,
175 sink,
176 format,
177 shutdown,
178 address,
179 body,
180 ns_token,
181 hydration,
182 throttle,
183 linger,
184 )
185 .await
186 }
187 Err(e) => Err(SubscribeError::Create(e)),
188 }
189}
190
191#[inline]
192#[allow(clippy::too_many_arguments)]
193async fn handle_subscribe_local<S: WireSink>(
194 state: &AppState,
195 connection_id: ConnectionId,
196 identity: IdentityId,
197 rql: String,
198 sink: S,
199 registry: &Arc<SubscriptionRegistry<S>>,
200 format: S::Format,
201 subscription_id: SubscriptionId,
202 hydration: HydrationConfig,
203 throttle: Option<Duration>,
204) -> Result<SubscribeAck, SubscribeError> {
205 let server_cap = state.subscribe_max_hydration_rows();
206 let throttle = state.clamp_throttle(throttle);
207 let max_rows = match hydration.max_rows {
208 Some(n) if n > server_cap => {
209 warn!("clamping hydration.max_rows from {} to server cap {}", n, server_cap);
210 server_cap
211 }
212 Some(n) => n,
213 None => server_cap,
214 };
215 let warming_cap = if hydration.enabled {
216 Some(max_rows as usize)
217 } else {
218 None
219 };
220
221 reifydb_assertions! {
222 let warming = warming_cap.is_some();
223 assert!(
224 warming == hydration.enabled,
225 "warming cap must be Some iff hydration is enabled (warming={warming}, enabled={}); \
226 a mismatch leaves a warming subscription that the hydrate-vs-promote branch never \
227 promotes to live, stranding the client",
228 hydration.enabled
229 );
230 }
231
232 registry.subscribe(subscription_id, connection_id, rql.clone(), sink.clone(), format, warming_cap, throttle);
233
234 if !matches!(sink.send_subscribed(subscription_id), DeliveryResult::Delivered) {
235 abort_warming(state, registry, subscription_id).await;
236 return Err(SubscribeError::LeaseFailed {
237 code: "STREAM_CLOSED",
238 message: "Client stream closed before Subscribed could be delivered".to_string(),
239 });
240 }
241
242 if hydration.enabled {
243 let lease = match state.engine().acquire_current_snapshot_lease() {
244 Ok((_, lease)) => lease,
245 Err(e) => {
246 let code = if e.0.code == "TXN_012" {
247 "HYDRATION_VERSION_EVICTED"
248 } else {
249 "PIN_VERSION_FAILED"
250 };
251 abort_warming(state, registry, subscription_id).await;
252 return Err(SubscribeError::LeaseFailed {
253 code,
254 message: e.to_string(),
255 });
256 }
257 };
258
259 if let Err(err) = run_member_hydrate(
260 state,
261 registry,
262 &sink,
263 subscription_id,
264 &rql,
265 identity,
266 lease,
267 max_rows,
268 format,
269 )
270 .await
271 {
272 return Err(err.into());
273 }
274 } else {
275 let _ = registry.promote_to_live(subscription_id);
276 }
277
278 debug!(
279 "Connection {} subscribed: subscription_id={} hydration_enabled={}",
280 connection_id, subscription_id, hydration.enabled
281 );
282
283 Ok(SubscribeAck {
284 subscription_id,
285 remote_handle: None,
286 })
287}
288
289#[inline]
290#[allow(clippy::too_many_arguments)]
291async fn handle_subscribe_remote<S: WireSink>(
292 connection_id: ConnectionId,
293 sink: S,
294 format: S::Format,
295 shutdown: WatchReceiver<bool>,
296 address: String,
297 body: String,
298 ns_token: Option<String>,
299 hydration: HydrationConfig,
300 throttle: Option<Duration>,
301 linger: Option<Duration>,
302) -> Result<SubscribeAck, SubscribeError> {
303 let client_format = S::client_wire_format(format);
304 let config = ClientSubscriptionConfig {
305 hydration: ClientHydrationConfig {
306 enabled: hydration.enabled,
307 max_rows: hydration.max_rows,
308 },
309 throttle,
310 linger,
311 };
312 let remote_sub = connect_remote(&address, &body, config, ns_token.as_deref(), client_format)
313 .await
314 .map_err(|e| SubscribeError::RemoteConnect(e.to_string()))?;
315 let remote_id = remote_sub.subscription_id().to_string();
316 let subscription_id = SubscriptionId(remote_id.parse::<u64>().map_err(|_| SubscribeError::InvalidRemoteId)?);
317
318 if !matches!(sink.send_subscribed(subscription_id), DeliveryResult::Delivered) {
319 return Err(SubscribeError::LeaseFailed {
320 code: "STREAM_CLOSED",
321 message: "Client stream closed before Subscribed could be delivered".to_string(),
322 });
323 }
324
325 let sink_for_proxy = sink.clone();
326 let sink_for_close = sink.clone();
327 let handle = spawn(async move {
328 proxy_remote_to_sink(remote_sub, shutdown, move |payload| {
329 matches!(
330 sink_for_proxy.send_remote_change(subscription_id, payload, format),
331 DeliveryResult::Delivered
332 )
333 })
334 .await;
335 let _ = sink_for_close.send_closed(subscription_id);
336 });
337
338 debug!("Connection {} subscribed to remote: subscription_id={}", connection_id, subscription_id);
339
340 Ok(SubscribeAck {
341 subscription_id,
342 remote_handle: Some(handle),
343 })
344}
345
346#[allow(clippy::too_many_arguments)]
347pub async fn handle_batch_subscribe<S: WireSink>(
348 state: &AppState,
349 connection_id: ConnectionId,
350 identity: IdentityId,
351 queries: &[String],
352 sink: S,
353 registry: &Arc<SubscriptionRegistry<S>>,
354 format: S::Format,
355 shutdown: WatchReceiver<bool>,
356 metadata: RequestMetadata,
357) -> Result<BatchAck, BatchSubscribeError> {
358 if queries.is_empty() {
359 return Err(BatchSubscribeError::Empty);
360 }
361
362 let (resolved, local_hydrations, member_lingers) =
363 resolve_batch_members::<S>(state, identity, queries, format, &metadata).await?;
364
365 let server_cap = state.subscribe_max_hydration_rows();
366 let effective_max_rows = compute_effective_max_rows(&local_hydrations, server_cap);
367 register_local_members(
368 state,
369 connection_id,
370 &sink,
371 registry,
372 format,
373 &resolved,
374 &local_hydrations,
375 &effective_max_rows,
376 server_cap,
377 );
378
379 let (batch_id, members_for_ack, remote_members_taken) =
380 register_batch_and_ack(state, connection_id, &sink, registry, format, resolved, &member_lingers)
381 .await?;
382
383 hydrate_batch_locals(
384 state,
385 identity,
386 &sink,
387 registry,
388 format,
389 batch_id,
390 &local_hydrations,
391 &effective_max_rows,
392 server_cap,
393 )
394 .await?;
395
396 let remote_handles = spawn_batch_remote_proxies(registry, batch_id, remote_members_taken, &shutdown);
397
398 debug!("Connection {} created batch {} with {} members", connection_id, batch_id, members_for_ack.len());
399
400 Ok(BatchAck {
401 batch_id,
402 members: members_for_ack,
403 remote_handles,
404 })
405}
406
407type LocalHydration = (SubscriptionId, String, HydrationConfig, Option<Duration>);
408type ResolvedBatch = (Vec<ResolvedBatchMember>, Vec<LocalHydration>, HashMap<SubscriptionId, Duration>);
409type BatchAckParts = (BatchId, Vec<BatchMemberInfo>, Vec<(SubscriptionId, RemoteSubscription)>);
410
411#[inline]
412async fn resolve_batch_members<S: WireSink>(
413 state: &AppState,
414 identity: IdentityId,
415 queries: &[String],
416 format: S::Format,
417 metadata: &RequestMetadata,
418) -> Result<ResolvedBatch, BatchSubscribeError> {
419 let mut resolved: Vec<ResolvedBatchMember> = Vec::with_capacity(queries.len());
420 let mut local_hydrations: Vec<LocalHydration> = Vec::new();
421 let mut member_lingers: HashMap<SubscriptionId, Duration> = HashMap::new();
422
423 for (index, user_rql) in queries.iter().enumerate() {
424 match create_subscription(state, identity, user_rql, metadata.clone()).await {
425 Ok(CreateSubscriptionResult::Local {
426 id: subscription_id,
427 hydration,
428 throttle,
429 linger,
430 }) => {
431 member_lingers.insert(subscription_id, state.clamp_linger(linger));
432 local_hydrations.push((subscription_id, user_rql.clone(), hydration, throttle));
433 resolved.push(ResolvedBatchMember::Local {
434 index,
435 subscription_id,
436 query: user_rql.clone(),
437 });
438 }
439 Ok(CreateSubscriptionResult::Remote {
440 address,
441 body,
442 token: ns_token,
443 hydration,
444 throttle,
445 linger,
446 }) => {
447 let client_format = S::client_wire_format(format);
448 let config = ClientSubscriptionConfig {
449 hydration: ClientHydrationConfig {
450 enabled: hydration.enabled,
451 max_rows: hydration.max_rows,
452 },
453 throttle,
454 linger,
455 };
456 let remote_sub = match connect_remote(
457 &address,
458 &body,
459 config,
460 ns_token.as_deref(),
461 client_format,
462 )
463 .await
464 {
465 Ok(s) => s,
466 Err(e) => {
467 rollback_batch_members(state, &resolved).await;
468 return Err(BatchSubscribeError::RemoteConnect(e.to_string()));
469 }
470 };
471 let remote_id = remote_sub.subscription_id().to_string();
472 let subscription_id = match remote_id.parse::<u64>() {
473 Ok(n) => SubscriptionId(n),
474 Err(_) => {
475 rollback_batch_members(state, &resolved).await;
476 return Err(BatchSubscribeError::InvalidRemoteId);
477 }
478 };
479 member_lingers.insert(subscription_id, state.clamp_linger(linger));
480 resolved.push(ResolvedBatchMember::Remote {
481 index,
482 subscription_id,
483 remote_sub: Box::new(remote_sub),
484 });
485 }
486 Err(e) => {
487 rollback_batch_members(state, &resolved).await;
488 return Err(BatchSubscribeError::Create(e));
489 }
490 }
491 }
492
493 reifydb_assertions! {
494 let resolved_len = resolved.len();
495 let query_len = queries.len();
496 assert!(
497 resolved_len == query_len,
498 "every query must resolve to exactly one batch member (resolved={resolved_len}, \
499 queries={query_len}); a count mismatch desyncs member indices from the \
500 BatchSubscribed ack and misroutes change frames to the wrong subscription"
501 );
502 }
503
504 Ok((resolved, local_hydrations, member_lingers))
505}
506
507#[inline]
508fn compute_effective_max_rows(local_hydrations: &[LocalHydration], server_cap: u64) -> HashMap<SubscriptionId, u64> {
509 let mut effective_max_rows: HashMap<SubscriptionId, u64> = HashMap::new();
510 for (sub_id, _, hydration, _) in local_hydrations {
511 let max_rows = match hydration.max_rows {
512 Some(n) if n > server_cap => {
513 warn!("clamping hydration.max_rows from {} to server cap {}", n, server_cap);
514 server_cap
515 }
516 Some(n) => n,
517 None => server_cap,
518 };
519 effective_max_rows.insert(*sub_id, max_rows);
520 }
521 effective_max_rows
522}
523
524#[inline]
525#[allow(clippy::too_many_arguments)]
526fn register_local_members<S: WireSink>(
527 state: &AppState,
528 connection_id: ConnectionId,
529 sink: &S,
530 registry: &Arc<SubscriptionRegistry<S>>,
531 format: S::Format,
532 resolved: &[ResolvedBatchMember],
533 local_hydrations: &[LocalHydration],
534 effective_max_rows: &HashMap<SubscriptionId, u64>,
535 server_cap: u64,
536) {
537 for member in resolved {
538 if let ResolvedBatchMember::Local {
539 subscription_id,
540 query,
541 ..
542 } = member
543 {
544 let warming_cap = local_hydrations
545 .iter()
546 .find(|(sid, _, _, _)| sid == subscription_id)
547 .and_then(|(_, _, h, _)| {
548 if h.enabled {
549 Some(*effective_max_rows.get(subscription_id).unwrap_or(&server_cap)
550 as usize)
551 } else {
552 None
553 }
554 });
555 let throttle = state.clamp_throttle(
556 local_hydrations
557 .iter()
558 .find(|(sid, _, _, _)| sid == subscription_id)
559 .and_then(|(_, _, _, t)| *t),
560 );
561 registry.subscribe(
562 *subscription_id,
563 connection_id,
564 query.clone(),
565 sink.clone(),
566 format,
567 warming_cap,
568 throttle,
569 );
570 }
571 }
572}
573
574#[inline]
575#[allow(clippy::too_many_arguments)]
576async fn register_batch_and_ack<S: WireSink>(
577 state: &AppState,
578 connection_id: ConnectionId,
579 sink: &S,
580 registry: &Arc<SubscriptionRegistry<S>>,
581 format: S::Format,
582 resolved: Vec<ResolvedBatchMember>,
583 member_lingers: &HashMap<SubscriptionId, Duration>,
584) -> Result<BatchAckParts, BatchSubscribeError> {
585 let members: Vec<(SubscriptionId, Duration)> = resolved
586 .iter()
587 .map(|m| {
588 let id = m.subscription_id();
589 (id, member_lingers.get(&id).copied().unwrap_or(Duration::zero()))
590 })
591 .collect();
592 let batch_id =
593 registry.register_batch(connection_id, members, sink.clone(), format, state.clock(), state.rng());
594
595 let mut remote_members_taken: Vec<(SubscriptionId, RemoteSubscription)> = Vec::new();
596 let mut members_for_ack: Vec<BatchMemberInfo> = Vec::with_capacity(resolved.len());
597 for member in resolved {
598 members_for_ack.push(BatchMemberInfo {
599 index: member.index(),
600 subscription_id: member.subscription_id(),
601 });
602 if let ResolvedBatchMember::Remote {
603 subscription_id,
604 remote_sub,
605 ..
606 } = member
607 {
608 remote_members_taken.push((subscription_id, *remote_sub));
609 }
610 }
611
612 let subscribed_members: Vec<BatchSubscribedMember> = members_for_ack
613 .iter()
614 .map(|m| BatchSubscribedMember {
615 index: m.index,
616 subscription_id: m.subscription_id,
617 })
618 .collect();
619 if !matches!(sink.send_batch_subscribed(batch_id, &subscribed_members), DeliveryResult::Delivered) {
620 registry.unsubscribe_batch(batch_id);
621 rollback_batch_members(state, &[]).await;
622 return Err(BatchSubscribeError::LeaseFailed {
623 code: "STREAM_CLOSED",
624 message: "Client stream closed before BatchSubscribed could be delivered".to_string(),
625 });
626 }
627
628 Ok((batch_id, members_for_ack, remote_members_taken))
629}
630
631#[inline]
632#[allow(clippy::too_many_arguments)]
633async fn hydrate_batch_locals<S: WireSink>(
634 state: &AppState,
635 identity: IdentityId,
636 sink: &S,
637 registry: &Arc<SubscriptionRegistry<S>>,
638 format: S::Format,
639 batch_id: BatchId,
640 local_hydrations: &[LocalHydration],
641 effective_max_rows: &HashMap<SubscriptionId, u64>,
642 server_cap: u64,
643) -> Result<(), BatchSubscribeError> {
644 let any_hydration = local_hydrations.iter().any(|(_, _, h, _)| h.enabled);
645 if any_hydration {
646 let lease = match state.engine().acquire_current_snapshot_lease() {
647 Ok((_, lease)) => lease,
648 Err(e) => {
649 let code = if e.0.code == "TXN_012" {
650 "HYDRATION_VERSION_EVICTED"
651 } else {
652 "PIN_VERSION_FAILED"
653 };
654 registry.unsubscribe_batch(batch_id);
655 return Err(BatchSubscribeError::LeaseFailed {
656 code,
657 message: e.to_string(),
658 });
659 }
660 };
661 for (sub_id, rql, hydration, _) in local_hydrations {
662 if !hydration.enabled {
663 continue;
664 }
665 let max_rows = *effective_max_rows.get(sub_id).unwrap_or(&server_cap);
666 if let Err(err) = run_member_hydrate(
667 state,
668 registry,
669 sink,
670 *sub_id,
671 rql,
672 identity,
673 lease.clone(),
674 max_rows,
675 format,
676 )
677 .await
678 {
679 registry.unsubscribe_batch(batch_id);
680 return Err(err.into_batch());
681 }
682 }
683 } else {
684 for (sub_id, _, _, _) in local_hydrations {
685 let _ = registry.promote_to_live(*sub_id);
686 }
687 }
688 Ok(())
689}
690
691#[inline]
692fn spawn_batch_remote_proxies<S: WireSink>(
693 registry: &Arc<SubscriptionRegistry<S>>,
694 batch_id: BatchId,
695 remote_members_taken: Vec<(SubscriptionId, RemoteSubscription)>,
696 shutdown: &WatchReceiver<bool>,
697) -> Vec<JoinHandle<()>> {
698 let mut remote_handles: Vec<JoinHandle<()>> = Vec::with_capacity(remote_members_taken.len());
699 for (subscription_id, remote_sub) in remote_members_taken {
700 let registry_clone = Arc::clone(registry);
701 let proxy_shutdown = shutdown.clone();
702 let handle = spawn(async move {
703 run_batch_remote_proxy(registry_clone, batch_id, subscription_id, remote_sub, proxy_shutdown)
704 .await;
705 });
706 remote_handles.push(handle);
707 }
708 remote_handles
709}
710
711pub async fn handle_batch_unsubscribe<S: WireSink>(
712 state: &AppState,
713 registry: &Arc<SubscriptionRegistry<S>>,
714 batch_id: BatchId,
715) -> Option<Vec<SubscriptionId>> {
716 let members = registry.unsubscribe_batch(batch_id)?;
717 for subscription_id in &members {
718 if let Err(e) = cleanup_subscription(state, *subscription_id).await {
719 warn!("Failed to cleanup batch member subscription {} from database: {:?}", subscription_id, e);
720 }
721 }
722 Some(members)
723}
724
725async fn run_batch_remote_proxy<S: WireSink>(
726 registry: Arc<SubscriptionRegistry<S>>,
727 batch_id: BatchId,
728 subscription_id: SubscriptionId,
729 remote_sub: RemoteSubscription,
730 shutdown: WatchReceiver<bool>,
731) {
732 let registry_push = Arc::clone(®istry);
733 proxy_remote_to_sink(remote_sub, shutdown, move |payload| {
734 let frames = payload.into_frames();
735 registry_push.push_batch_frames(batch_id, subscription_id, frames)
736 })
737 .await;
738 let _ = registry.emit_batch_member_closed(batch_id, subscription_id);
739}
740
741#[allow(clippy::too_many_arguments)]
742async fn run_member_hydrate<S: WireSink>(
743 state: &AppState,
744 registry: &Arc<SubscriptionRegistry<S>>,
745 sink: &S,
746 subscription_id: SubscriptionId,
747 rql: &str,
748 identity: IdentityId,
749 lease: VersionLeaseGuard,
750 max_rows: u64,
751 format: S::Format,
752) -> Result<(), MemberHydrateError> {
753 let service: SubscriptionServiceRef = match state.engine().services().ioc.resolve() {
754 Ok(s) => s,
755 Err(e) => {
756 abort_warming(state, registry, subscription_id).await;
757 return Err(MemberHydrateError {
758 rql: rql.to_string(),
759 max_rows,
760 kind: MemberHydrateErrorKind::ServiceUnavailable(e.to_string()),
761 });
762 }
763 };
764
765 let engine = state.engine_clone();
766
767 let (version, outcome): (CommitVersion, Option<(Columns, ExecutionMetrics)>) =
768 match run_hydrate(service, engine, subscription_id, identity, lease, max_rows).await {
769 Ok(t) => t,
770 Err(err) => {
771 abort_warming(state, registry, subscription_id).await;
772 return Err(MemberHydrateError {
773 rql: rql.to_string(),
774 max_rows,
775 kind: MemberHydrateErrorKind::Failed(err),
776 });
777 }
778 };
779
780 if let Some((cols, metrics)) = outcome {
781 let row_count = cols.row_count();
782 debug!(
783 subscription_id = subscription_id.0,
784 version = version.0,
785 total_us = metrics.total.microseconds().unwrap_or(0),
786 compute_us = metrics.compute.microseconds().unwrap_or(0),
787 statement_count = metrics.statements.len(),
788 row_count = row_count,
789 fingerprint = %metrics.fingerprint.to_hex(),
790 "hydrate completed"
791 );
792 if let Some(batch_id) = registry.batch_for(&subscription_id) {
793 let _ = sink.send_batch_envelope(
794 batch_id,
795 format,
796 vec![(subscription_id, vec![Frame::from(cols)])],
797 );
798 } else {
799 let _ = sink.send_change(subscription_id, cols, format);
800 }
801 }
802
803 match registry.promote_to_live(subscription_id) {
804 PromoteResult::Promoted(_)
805 | PromoteResult::NotWarming
806 | PromoteResult::NotFound
807 | PromoteResult::Disconnected => Ok(()),
808 PromoteResult::Overflowed => Err(MemberHydrateError {
809 rql: rql.to_string(),
810 max_rows,
811 kind: MemberHydrateErrorKind::Backpressure,
812 }),
813 }
814}
815
816struct MemberHydrateError {
817 rql: String,
818 max_rows: u64,
819 kind: MemberHydrateErrorKind,
820}
821
822enum MemberHydrateErrorKind {
823 ServiceUnavailable(String),
824 Failed(HydrateError),
825 Backpressure,
826}
827
828impl MemberHydrateError {
829 fn into_batch(self) -> BatchSubscribeError {
830 match self.kind {
831 MemberHydrateErrorKind::ServiceUnavailable(msg) => {
832 BatchSubscribeError::HydrationServiceUnavailable(msg)
833 }
834 MemberHydrateErrorKind::Failed(err) => BatchSubscribeError::HydrationFailed {
835 error: err,
836 rql: self.rql,
837 max_rows: self.max_rows,
838 },
839 MemberHydrateErrorKind::Backpressure => BatchSubscribeError::HydrationBackpressure,
840 }
841 }
842}
843
844impl From<MemberHydrateError> for SubscribeError {
845 fn from(e: MemberHydrateError) -> Self {
846 match e.kind {
847 MemberHydrateErrorKind::ServiceUnavailable(msg) => {
848 SubscribeError::HydrationServiceUnavailable(msg)
849 }
850 MemberHydrateErrorKind::Failed(err) => SubscribeError::HydrationFailed {
851 error: err,
852 rql: e.rql,
853 max_rows: e.max_rows,
854 },
855 MemberHydrateErrorKind::Backpressure => SubscribeError::HydrationBackpressure,
856 }
857 }
858}
859
860async fn rollback_batch_members(state: &AppState, resolved: &[ResolvedBatchMember]) {
861 for member in resolved {
862 if let ResolvedBatchMember::Local {
863 subscription_id,
864 ..
865 } = member && let Err(e) = cleanup_subscription(state, *subscription_id).await
866 {
867 warn!("Failed to cleanup partial batch member {} during rollback: {:?}", subscription_id, e);
868 }
869 }
870}
871
872async fn abort_warming<S: WireSink>(
873 state: &AppState,
874 registry: &Arc<SubscriptionRegistry<S>>,
875 subscription_id: SubscriptionId,
876) {
877 registry.unsubscribe(subscription_id);
878 if let Err(e) = cleanup_subscription(state, subscription_id).await {
879 warn!("Failed to cleanup subscription {} after hydrate failure: {:?}", subscription_id, e);
880 }
881}