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