Skip to main content

reifydb_sub_server/subscription/
handler.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright (c) 2026 ReifyDB
3
4use 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(&registry);
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}