1use std::{collections::HashMap, sync::Arc, time::Duration};
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::value::{frame::frame::Frame, identity::IdentityId};
18use tokio::{spawn, sync::watch::Receiver as WatchReceiver, task::JoinHandle};
19use tracing::{info, warn};
20
21use crate::{
22 interceptor::RequestMetadata,
23 state::AppState,
24 subscription::{
25 cleanup::cleanup_subscription,
26 create::{CreateSubscriptionResult, create_subscription},
27 errors::CreateSubscriptionError,
28 hydrate::run_hydrate,
29 registry::{ConnectionId, PromoteResult, SubscriptionRegistry},
30 wire_sink::{BatchSubscribedMember, WireSink},
31 },
32};
33
34#[derive(Debug, Clone)]
35pub struct BatchMemberInfo {
36 pub index: usize,
37 pub subscription_id: SubscriptionId,
38}
39
40pub struct BatchAck {
41 pub batch_id: BatchId,
42 pub members: Vec<BatchMemberInfo>,
43 pub remote_handles: Vec<JoinHandle<()>>,
44}
45
46pub struct SubscribeAck {
47 pub subscription_id: SubscriptionId,
48 pub remote_handle: Option<JoinHandle<()>>,
49}
50
51#[derive(Debug)]
52pub enum SubscribeError {
53 Create(CreateSubscriptionError),
54 RemoteConnect(String),
55 InvalidRemoteId,
56 LeaseFailed {
57 code: &'static str,
58 message: String,
59 },
60 HydrationBackpressure,
61 HydrationFailed {
62 error: HydrateError,
63 rql: String,
64 max_rows: u64,
65 },
66 HydrationServiceUnavailable(String),
67}
68
69#[derive(Debug)]
70pub enum BatchSubscribeError {
71 Empty,
72 Create(CreateSubscriptionError),
73 RemoteConnect(String),
74 InvalidRemoteId,
75 LeaseFailed {
76 code: &'static str,
77 message: String,
78 },
79 HydrationBackpressure,
80 HydrationFailed {
81 error: HydrateError,
82 rql: String,
83 max_rows: u64,
84 },
85 HydrationServiceUnavailable(String),
86}
87
88enum ResolvedBatchMember {
89 Local {
90 index: usize,
91 subscription_id: SubscriptionId,
92 query: String,
93 },
94 Remote {
95 index: usize,
96 subscription_id: SubscriptionId,
97 remote_sub: Box<RemoteSubscription>,
98 },
99}
100
101impl ResolvedBatchMember {
102 fn index(&self) -> usize {
103 match self {
104 Self::Local {
105 index,
106 ..
107 }
108 | Self::Remote {
109 index,
110 ..
111 } => *index,
112 }
113 }
114
115 fn subscription_id(&self) -> SubscriptionId {
116 match self {
117 Self::Local {
118 subscription_id,
119 ..
120 }
121 | Self::Remote {
122 subscription_id,
123 ..
124 } => *subscription_id,
125 }
126 }
127}
128
129#[allow(clippy::too_many_arguments)]
130pub async fn handle_subscribe<S: WireSink>(
131 state: &AppState,
132 connection_id: ConnectionId,
133 identity: IdentityId,
134 rql: String,
135 sink: S,
136 registry: &Arc<SubscriptionRegistry<S>>,
137 format: S::Format,
138 shutdown: WatchReceiver<bool>,
139 metadata: RequestMetadata,
140) -> Result<SubscribeAck, SubscribeError> {
141 match create_subscription(state, identity, &rql, metadata).await {
142 Ok(CreateSubscriptionResult::Local {
143 id: subscription_id,
144 hydration,
145 throttle,
146 }) => {
147 let server_cap = state.subscribe_max_hydration_rows();
148 let throttle = state.clamp_throttle(throttle);
149 let max_rows = match hydration.max_rows {
150 Some(n) if n > server_cap => {
151 warn!("clamping hydration.max_rows from {} to server cap {}", n, server_cap);
152 server_cap
153 }
154 Some(n) => n,
155 None => server_cap,
156 };
157 let warming_cap = if hydration.enabled {
158 Some(max_rows as usize)
159 } else {
160 None
161 };
162
163 registry.subscribe(
164 subscription_id,
165 connection_id,
166 rql.clone(),
167 sink.clone(),
168 format,
169 warming_cap,
170 throttle,
171 );
172
173 if !matches!(sink.send_subscribed(subscription_id), DeliveryResult::Delivered) {
174 abort_warming(state, registry, subscription_id).await;
175 return Err(SubscribeError::LeaseFailed {
176 code: "STREAM_CLOSED",
177 message: "Client stream closed before Subscribed could be delivered"
178 .to_string(),
179 });
180 }
181
182 if hydration.enabled {
183 let lease = match state.engine().acquire_current_snapshot_lease() {
184 Ok((_, lease)) => lease,
185 Err(e) => {
186 let code = if e.0.code == "TXN_012" {
187 "HYDRATION_VERSION_EVICTED"
188 } else {
189 "PIN_VERSION_FAILED"
190 };
191 abort_warming(state, registry, subscription_id).await;
192 return Err(SubscribeError::LeaseFailed {
193 code,
194 message: e.to_string(),
195 });
196 }
197 };
198
199 if let Err(err) = run_member_hydrate(
200 state,
201 registry,
202 &sink,
203 subscription_id,
204 &rql,
205 identity,
206 lease,
207 max_rows,
208 format,
209 )
210 .await
211 {
212 return Err(err.into());
213 }
214 } else {
215 let _ = registry.promote_to_live(subscription_id);
216 }
217
218 info!(
219 "Connection {} subscribed: subscription_id={} hydration_enabled={}",
220 connection_id, subscription_id, hydration.enabled
221 );
222
223 Ok(SubscribeAck {
224 subscription_id,
225 remote_handle: None,
226 })
227 }
228 Ok(CreateSubscriptionResult::Remote {
229 address,
230 body,
231 token: ns_token,
232 hydration,
233 throttle,
234 }) => {
235 let client_format = S::client_wire_format(format);
236 let config = ClientSubscriptionConfig {
237 hydration: ClientHydrationConfig {
238 enabled: hydration.enabled,
239 max_rows: hydration.max_rows,
240 },
241 throttle,
242 };
243 let remote_sub = connect_remote(&address, &body, config, ns_token.as_deref(), client_format)
244 .await
245 .map_err(|e| SubscribeError::RemoteConnect(e.to_string()))?;
246 let remote_id = remote_sub.subscription_id().to_string();
247 let subscription_id =
248 SubscriptionId(remote_id.parse::<u64>().map_err(|_| SubscribeError::InvalidRemoteId)?);
249
250 if !matches!(sink.send_subscribed(subscription_id), DeliveryResult::Delivered) {
251 return Err(SubscribeError::LeaseFailed {
252 code: "STREAM_CLOSED",
253 message: "Client stream closed before Subscribed could be delivered"
254 .to_string(),
255 });
256 }
257
258 let sink_for_proxy = sink.clone();
259 let sink_for_close = sink.clone();
260 let handle = spawn(async move {
261 proxy_remote_to_sink(remote_sub, shutdown, move |payload| {
262 matches!(
263 sink_for_proxy.send_remote_change(subscription_id, payload, format),
264 DeliveryResult::Delivered
265 )
266 })
267 .await;
268 let _ = sink_for_close.send_closed(subscription_id);
269 });
270
271 info!("Connection {} subscribed to remote: subscription_id={}", connection_id, subscription_id);
272
273 Ok(SubscribeAck {
274 subscription_id,
275 remote_handle: Some(handle),
276 })
277 }
278 Err(e) => Err(SubscribeError::Create(e)),
279 }
280}
281
282#[allow(clippy::too_many_arguments)]
283pub async fn handle_batch_subscribe<S: WireSink>(
284 state: &AppState,
285 connection_id: ConnectionId,
286 identity: IdentityId,
287 queries: &[String],
288 sink: S,
289 registry: &Arc<SubscriptionRegistry<S>>,
290 format: S::Format,
291 shutdown: WatchReceiver<bool>,
292 metadata: RequestMetadata,
293) -> Result<BatchAck, BatchSubscribeError> {
294 if queries.is_empty() {
295 return Err(BatchSubscribeError::Empty);
296 }
297
298 let mut resolved: Vec<ResolvedBatchMember> = Vec::with_capacity(queries.len());
299 let mut local_hydrations: Vec<(SubscriptionId, String, HydrationConfig, Option<Duration>)> = Vec::new();
300
301 for (index, user_rql) in queries.iter().enumerate() {
302 match create_subscription(state, identity, user_rql, metadata.clone()).await {
303 Ok(CreateSubscriptionResult::Local {
304 id: subscription_id,
305 hydration,
306 throttle,
307 }) => {
308 local_hydrations.push((subscription_id, user_rql.clone(), hydration, throttle));
309 resolved.push(ResolvedBatchMember::Local {
310 index,
311 subscription_id,
312 query: user_rql.clone(),
313 });
314 }
315 Ok(CreateSubscriptionResult::Remote {
316 address,
317 body,
318 token: ns_token,
319 hydration,
320 throttle,
321 }) => {
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,
329 };
330 let remote_sub = match connect_remote(
331 &address,
332 &body,
333 config,
334 ns_token.as_deref(),
335 client_format,
336 )
337 .await
338 {
339 Ok(s) => s,
340 Err(e) => {
341 rollback_batch_members(state, &resolved).await;
342 return Err(BatchSubscribeError::RemoteConnect(e.to_string()));
343 }
344 };
345 let remote_id = remote_sub.subscription_id().to_string();
346 let subscription_id = match remote_id.parse::<u64>() {
347 Ok(n) => SubscriptionId(n),
348 Err(_) => {
349 rollback_batch_members(state, &resolved).await;
350 return Err(BatchSubscribeError::InvalidRemoteId);
351 }
352 };
353 resolved.push(ResolvedBatchMember::Remote {
354 index,
355 subscription_id,
356 remote_sub: Box::new(remote_sub),
357 });
358 }
359 Err(e) => {
360 rollback_batch_members(state, &resolved).await;
361 return Err(BatchSubscribeError::Create(e));
362 }
363 }
364 }
365
366 let server_cap = state.subscribe_max_hydration_rows();
367 let mut effective_max_rows: HashMap<SubscriptionId, u64> = HashMap::new();
368 for (sub_id, _, hydration, _) in &local_hydrations {
369 let max_rows = match hydration.max_rows {
370 Some(n) if n > server_cap => {
371 warn!("clamping hydration.max_rows from {} to server cap {}", n, server_cap);
372 server_cap
373 }
374 Some(n) => n,
375 None => server_cap,
376 };
377 effective_max_rows.insert(*sub_id, max_rows);
378 }
379
380 for member in &resolved {
381 if let ResolvedBatchMember::Local {
382 subscription_id,
383 query,
384 ..
385 } = member
386 {
387 let warming_cap = local_hydrations
388 .iter()
389 .find(|(sid, _, _, _)| sid == subscription_id)
390 .and_then(|(_, _, h, _)| {
391 if h.enabled {
392 Some(*effective_max_rows.get(subscription_id).unwrap_or(&server_cap)
393 as usize)
394 } else {
395 None
396 }
397 });
398 let throttle = state.clamp_throttle(
399 local_hydrations
400 .iter()
401 .find(|(sid, _, _, _)| sid == subscription_id)
402 .and_then(|(_, _, _, t)| *t),
403 );
404 registry.subscribe(
405 *subscription_id,
406 connection_id,
407 query.clone(),
408 sink.clone(),
409 format,
410 warming_cap,
411 throttle,
412 );
413 }
414 }
415
416 let member_ids: Vec<SubscriptionId> = resolved.iter().map(|m| m.subscription_id()).collect();
417 let batch_id =
418 registry.register_batch(connection_id, member_ids, sink.clone(), format, state.clock(), state.rng());
419
420 let mut remote_members_taken: Vec<(SubscriptionId, RemoteSubscription)> = Vec::new();
421 let mut members_for_ack: Vec<BatchMemberInfo> = Vec::with_capacity(resolved.len());
422 for member in resolved {
423 members_for_ack.push(BatchMemberInfo {
424 index: member.index(),
425 subscription_id: member.subscription_id(),
426 });
427 if let ResolvedBatchMember::Remote {
428 subscription_id,
429 remote_sub,
430 ..
431 } = member
432 {
433 remote_members_taken.push((subscription_id, *remote_sub));
434 }
435 }
436
437 let subscribed_members: Vec<BatchSubscribedMember> = members_for_ack
438 .iter()
439 .map(|m| BatchSubscribedMember {
440 index: m.index,
441 subscription_id: m.subscription_id,
442 })
443 .collect();
444 if !matches!(sink.send_batch_subscribed(batch_id, &subscribed_members), DeliveryResult::Delivered) {
445 registry.unsubscribe_batch(batch_id);
446 rollback_batch_members(state, &[]).await;
447 return Err(BatchSubscribeError::LeaseFailed {
448 code: "STREAM_CLOSED",
449 message: "Client stream closed before BatchSubscribed could be delivered".to_string(),
450 });
451 }
452
453 let any_hydration = local_hydrations.iter().any(|(_, _, h, _)| h.enabled);
454 if any_hydration {
455 let lease = match state.engine().acquire_current_snapshot_lease() {
456 Ok((_, lease)) => lease,
457 Err(e) => {
458 let code = if e.0.code == "TXN_012" {
459 "HYDRATION_VERSION_EVICTED"
460 } else {
461 "PIN_VERSION_FAILED"
462 };
463 registry.unsubscribe_batch(batch_id);
464 return Err(BatchSubscribeError::LeaseFailed {
465 code,
466 message: e.to_string(),
467 });
468 }
469 };
470 for (sub_id, rql, hydration, _) in &local_hydrations {
471 if !hydration.enabled {
472 continue;
473 }
474 let max_rows = *effective_max_rows.get(sub_id).unwrap_or(&server_cap);
475 if let Err(err) = run_member_hydrate(
476 state,
477 registry,
478 &sink,
479 *sub_id,
480 rql,
481 identity,
482 lease.clone(),
483 max_rows,
484 format,
485 )
486 .await
487 {
488 registry.unsubscribe_batch(batch_id);
489 return Err(err.into_batch());
490 }
491 }
492 } else {
493 for (sub_id, _, _, _) in &local_hydrations {
494 let _ = registry.promote_to_live(*sub_id);
495 }
496 }
497
498 let mut remote_handles: Vec<JoinHandle<()>> = Vec::with_capacity(remote_members_taken.len());
499 for (subscription_id, remote_sub) in remote_members_taken {
500 let registry_clone = Arc::clone(registry);
501 let proxy_shutdown = shutdown.clone();
502 let handle = spawn(async move {
503 run_batch_remote_proxy(registry_clone, batch_id, subscription_id, remote_sub, proxy_shutdown)
504 .await;
505 });
506 remote_handles.push(handle);
507 }
508
509 info!("Connection {} created batch {} with {} members", connection_id, batch_id, members_for_ack.len());
510
511 Ok(BatchAck {
512 batch_id,
513 members: members_for_ack,
514 remote_handles,
515 })
516}
517
518pub async fn handle_batch_unsubscribe<S: WireSink>(
519 state: &AppState,
520 registry: &Arc<SubscriptionRegistry<S>>,
521 batch_id: BatchId,
522) -> Option<Vec<SubscriptionId>> {
523 let members = registry.unsubscribe_batch(batch_id)?;
524 for subscription_id in &members {
525 if let Err(e) = cleanup_subscription(state, *subscription_id).await {
526 warn!("Failed to cleanup batch member subscription {} from database: {:?}", subscription_id, e);
527 }
528 }
529 Some(members)
530}
531
532async fn run_batch_remote_proxy<S: WireSink>(
533 registry: Arc<SubscriptionRegistry<S>>,
534 batch_id: BatchId,
535 subscription_id: SubscriptionId,
536 remote_sub: RemoteSubscription,
537 shutdown: WatchReceiver<bool>,
538) {
539 let registry_push = Arc::clone(®istry);
540 proxy_remote_to_sink(remote_sub, shutdown, move |payload| {
541 let frames = payload.into_frames();
542 registry_push.push_batch_frames(batch_id, subscription_id, frames)
543 })
544 .await;
545 let _ = registry.emit_batch_member_closed(batch_id, subscription_id);
546}
547
548#[allow(clippy::too_many_arguments)]
549async fn run_member_hydrate<S: WireSink>(
550 state: &AppState,
551 registry: &Arc<SubscriptionRegistry<S>>,
552 sink: &S,
553 subscription_id: SubscriptionId,
554 rql: &str,
555 identity: IdentityId,
556 lease: VersionLeaseGuard,
557 max_rows: u64,
558 format: S::Format,
559) -> Result<(), MemberHydrateError> {
560 let service: SubscriptionServiceRef = match state.engine().services().ioc.resolve() {
561 Ok(s) => s,
562 Err(e) => {
563 abort_warming(state, registry, subscription_id).await;
564 return Err(MemberHydrateError {
565 rql: rql.to_string(),
566 max_rows,
567 kind: MemberHydrateErrorKind::ServiceUnavailable(e.to_string()),
568 });
569 }
570 };
571
572 let engine = state.engine_clone();
573
574 let (version, outcome): (CommitVersion, Option<(Columns, ExecutionMetrics)>) =
575 match run_hydrate(service, engine, subscription_id, identity, lease, max_rows).await {
576 Ok(t) => t,
577 Err(err) => {
578 abort_warming(state, registry, subscription_id).await;
579 return Err(MemberHydrateError {
580 rql: rql.to_string(),
581 max_rows,
582 kind: MemberHydrateErrorKind::Failed(err),
583 });
584 }
585 };
586
587 if let Some((cols, metrics)) = outcome {
588 let row_count = cols.row_count();
589 info!(
590 subscription_id = subscription_id.0,
591 version = version.0,
592 total_us = metrics.total.microseconds().unwrap_or(0),
593 compute_us = metrics.compute.microseconds().unwrap_or(0),
594 statement_count = metrics.statements.len(),
595 row_count = row_count,
596 fingerprint = %metrics.fingerprint.to_hex(),
597 "hydrate completed"
598 );
599 if let Some(batch_id) = registry.batch_for(&subscription_id) {
600 let _ = sink.send_batch_envelope(
601 batch_id,
602 format,
603 vec![(subscription_id, vec![Frame::from(cols)])],
604 );
605 } else {
606 let _ = sink.send_change(subscription_id, cols, format);
607 }
608 }
609
610 match registry.promote_to_live(subscription_id) {
611 PromoteResult::Promoted(_)
612 | PromoteResult::NotWarming
613 | PromoteResult::NotFound
614 | PromoteResult::Disconnected => Ok(()),
615 PromoteResult::Overflowed => Err(MemberHydrateError {
616 rql: rql.to_string(),
617 max_rows,
618 kind: MemberHydrateErrorKind::Backpressure,
619 }),
620 }
621}
622
623struct MemberHydrateError {
624 rql: String,
625 max_rows: u64,
626 kind: MemberHydrateErrorKind,
627}
628
629enum MemberHydrateErrorKind {
630 ServiceUnavailable(String),
631 Failed(HydrateError),
632 Backpressure,
633}
634
635impl MemberHydrateError {
636 fn into_batch(self) -> BatchSubscribeError {
637 match self.kind {
638 MemberHydrateErrorKind::ServiceUnavailable(msg) => {
639 BatchSubscribeError::HydrationServiceUnavailable(msg)
640 }
641 MemberHydrateErrorKind::Failed(err) => BatchSubscribeError::HydrationFailed {
642 error: err,
643 rql: self.rql,
644 max_rows: self.max_rows,
645 },
646 MemberHydrateErrorKind::Backpressure => BatchSubscribeError::HydrationBackpressure,
647 }
648 }
649}
650
651impl From<MemberHydrateError> for SubscribeError {
652 fn from(e: MemberHydrateError) -> Self {
653 match e.kind {
654 MemberHydrateErrorKind::ServiceUnavailable(msg) => {
655 SubscribeError::HydrationServiceUnavailable(msg)
656 }
657 MemberHydrateErrorKind::Failed(err) => SubscribeError::HydrationFailed {
658 error: err,
659 rql: e.rql,
660 max_rows: e.max_rows,
661 },
662 MemberHydrateErrorKind::Backpressure => SubscribeError::HydrationBackpressure,
663 }
664 }
665}
666
667async fn rollback_batch_members(state: &AppState, resolved: &[ResolvedBatchMember]) {
668 for member in resolved {
669 if let ResolvedBatchMember::Local {
670 subscription_id,
671 ..
672 } = member && let Err(e) = cleanup_subscription(state, *subscription_id).await
673 {
674 warn!("Failed to cleanup partial batch member {} during rollback: {:?}", subscription_id, e);
675 }
676 }
677}
678
679async fn abort_warming<S: WireSink>(
680 state: &AppState,
681 registry: &Arc<SubscriptionRegistry<S>>,
682 subscription_id: SubscriptionId,
683) {
684 registry.unsubscribe(subscription_id);
685 if let Err(e) = cleanup_subscription(state, subscription_id).await {
686 warn!("Failed to cleanup subscription {} after hydrate failure: {:?}", subscription_id, e);
687 }
688}