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::{
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(&registry);
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}