Skip to main content

reifydb_sub_server/subscription/
handler.rs

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