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