Skip to main content

rivetkit_core/actor/
connection.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt;
3use std::ops::Bound::{Excluded, Unbounded};
4use std::sync::Arc;
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::time::Duration;
7
8use anyhow::{Context, Result};
9use futures::future::BoxFuture;
10use parking_lot::{RwLock, RwLockReadGuard};
11use rivet_error::RivetError;
12use rivetkit_actor_persist::{generated::v4 as persist_v4, versioned as persist_versioned};
13use serde::Serialize;
14use uuid::Uuid;
15
16use tokio::sync::oneshot;
17
18use crate::actor::config::ActorConfig;
19use crate::actor::context::ActorContext;
20use crate::actor::internal_storage;
21use crate::actor::lifecycle_hooks::Reply;
22use crate::actor::messages::{ActorEvent, Request};
23use crate::actor::persist::{
24	decode_latest_with_embedded_version, encode_latest_with_embedded_version,
25};
26use crate::actor::state::RequestSaveOpts;
27use crate::error::ActorRuntime;
28use crate::time::timeout;
29use crate::types::ConnId;
30
31pub(crate) type EventSendCallback = Arc<dyn Fn(OutgoingEvent) -> Result<()> + Send + Sync>;
32pub(crate) type DisconnectCallback =
33	Arc<dyn Fn(Option<String>) -> BoxFuture<'static, Result<()>> + Send + Sync>;
34type StateChangeCallback = Arc<dyn Fn(&ConnHandle) + Send + Sync>;
35
36#[derive(Clone, Debug, PartialEq, Eq)]
37pub(crate) struct OutgoingEvent {
38	pub name: String,
39	pub args: Vec<u8>,
40}
41
42#[derive(Clone, Debug, Default, PartialEq, Eq)]
43pub(crate) struct HibernatableConnectionMetadata {
44	pub gateway_id: [u8; 4],
45	pub request_id: [u8; 4],
46	pub server_message_index: u16,
47	pub client_message_index: u16,
48	pub request_path: String,
49	pub request_headers: BTreeMap<String, String>,
50}
51
52pub(crate) type PersistedSubscription = persist_v4::Subscription;
53pub(crate) type PersistedConnection = persist_v4::Conn;
54
55#[derive(RivetError, Serialize)]
56#[error(
57	"actor",
58	"invalid_request",
59	"Invalid hibernatable websocket connection ID",
60	"Hibernatable websocket {field} must be exactly 4 bytes, got {actual_len}."
61)]
62struct InvalidHibernatableConnectionId {
63	field: String,
64	actual_len: usize,
65}
66
67#[derive(RivetError, Serialize)]
68#[error(
69	"connection",
70	"not_configured",
71	"Connection callback is not configured",
72	"Connection {component} is not configured."
73)]
74struct ConnectionNotConfigured {
75	component: String,
76}
77
78#[derive(RivetError, Serialize)]
79#[error(
80	"connection",
81	"not_found",
82	"Connection was not found",
83	"Connection '{conn_id}' was not found."
84)]
85struct ConnectionNotFound {
86	conn_id: String,
87}
88
89#[derive(RivetError, Serialize)]
90#[error(
91	"connection",
92	"not_hibernatable",
93	"Connection is not hibernatable",
94	"Connection '{conn_id}' is not hibernatable."
95)]
96struct ConnectionNotHibernatable {
97	conn_id: String,
98}
99
100#[derive(RivetError, Serialize)]
101#[error(
102	"connection",
103	"restore_not_found",
104	"Hibernatable connection restore target was not found"
105)]
106struct ConnectionRestoreNotFound;
107
108#[derive(RivetError, Serialize)]
109#[error(
110	"connection",
111	"disconnect_failed",
112	"Connection disconnect failed",
113	"Disconnect transport failed for {count} connection(s): {details}"
114)]
115struct ConnectionDisconnectFailed {
116	count: usize,
117	details: String,
118}
119
120pub(crate) fn hibernatable_id_from_slice(field: &'static str, bytes: &[u8]) -> Result<[u8; 4]> {
121	bytes.try_into().map_err(|_| {
122		InvalidHibernatableConnectionId {
123			field: field.to_owned(),
124			actual_len: bytes.len(),
125		}
126		.build()
127	})
128}
129
130pub(crate) fn encode_persisted_connection(connection: &PersistedConnection) -> Result<Vec<u8>> {
131	encode_latest_with_embedded_version::<persist_versioned::Conn>(
132		connection.clone(),
133		rivetkit_actor_persist::CURRENT_VERSION,
134		"persisted connection",
135	)
136}
137
138pub(crate) fn decode_persisted_connection(payload: &[u8]) -> Result<PersistedConnection> {
139	let connection = decode_latest_with_embedded_version::<persist_versioned::Conn>(
140		payload,
141		"persisted connection",
142	)?;
143	Ok(connection)
144}
145
146#[derive(Clone)]
147pub struct ConnHandle(Arc<ConnHandleInner>);
148
149struct ConnHandleInner {
150	id: ConnId,
151	params: Vec<u8>,
152	// Forced-sync: connection handles expose synchronous state and callback
153	// methods to foreign runtimes; callbacks are cloned before async work.
154	state: RwLock<Vec<u8>>,
155	is_hibernatable: bool,
156	dirty: AtomicBool,
157	subscriptions: RwLock<BTreeSet<String>>,
158	hibernation: RwLock<Option<HibernatableConnectionMetadata>>,
159	state_change_handler: RwLock<Option<StateChangeCallback>>,
160	event_sender: RwLock<Option<EventSendCallback>>,
161	transport_disconnect_handler: RwLock<Option<DisconnectCallback>>,
162	disconnect_handler: RwLock<Option<DisconnectCallback>>,
163}
164
165impl ConnHandle {
166	pub fn new(
167		id: impl Into<ConnId>,
168		params: Vec<u8>,
169		state: Vec<u8>,
170		is_hibernatable: bool,
171	) -> Self {
172		Self(Arc::new(ConnHandleInner {
173			id: id.into(),
174			params,
175			state: RwLock::new(state),
176			is_hibernatable,
177			dirty: AtomicBool::new(false),
178			subscriptions: RwLock::new(BTreeSet::new()),
179			hibernation: RwLock::new(None),
180			state_change_handler: RwLock::new(None),
181			event_sender: RwLock::new(None),
182			transport_disconnect_handler: RwLock::new(None),
183			disconnect_handler: RwLock::new(None),
184		}))
185	}
186
187	pub fn id(&self) -> &str {
188		&self.0.id
189	}
190
191	pub fn params(&self) -> Vec<u8> {
192		self.0.params.clone()
193	}
194
195	pub fn state(&self) -> Vec<u8> {
196		self.0.state.read().clone()
197	}
198
199	pub fn set_state(&self, state: Vec<u8>) {
200		self.set_state_inner(state, true);
201	}
202
203	#[doc(hidden)]
204	pub fn set_state_initial(&self, state: Vec<u8>) {
205		self.set_state_inner(state, false);
206	}
207
208	fn set_state_inner(&self, state: Vec<u8>, mark_dirty: bool) {
209		*self.0.state.write() = state;
210		if mark_dirty {
211			self.mark_hibernation_dirty();
212		}
213	}
214
215	fn mark_hibernation_dirty(&self) {
216		if !self.is_hibernatable() {
217			return;
218		}
219		self.0.dirty.store(true, Ordering::SeqCst);
220		let handler = self.0.state_change_handler.read().clone();
221		if let Some(handler) = handler {
222			handler(self);
223		}
224	}
225
226	pub(crate) fn clear_hibernation_dirty(&self) {
227		self.0.dirty.store(false, Ordering::SeqCst);
228	}
229
230	pub fn is_hibernatable(&self) -> bool {
231		self.0.is_hibernatable
232	}
233
234	pub fn send(&self, name: &str, args: &[u8]) {
235		if let Err(error) = self.try_send(name, args) {
236			tracing::error!(
237				?error,
238				conn_id = self.id(),
239				event_name = name,
240				"failed to send event to connection"
241			);
242		}
243	}
244
245	pub async fn disconnect(&self, reason: Option<&str>) -> Result<()> {
246		if let Some(handler) = self.transport_disconnect_handler() {
247			handler(reason.map(str::to_owned)).await?;
248		}
249		let handler = self.disconnect_handler()?;
250		handler(reason.map(str::to_owned)).await
251	}
252
253	pub(crate) fn configure_event_sender(&self, event_sender: Option<EventSendCallback>) {
254		*self.0.event_sender.write() = event_sender;
255	}
256
257	pub(crate) fn configure_disconnect_handler(
258		&self,
259		disconnect_handler: Option<DisconnectCallback>,
260	) {
261		*self.0.disconnect_handler.write() = disconnect_handler;
262	}
263
264	pub(crate) fn configure_transport_disconnect_handler(
265		&self,
266		disconnect_handler: Option<DisconnectCallback>,
267	) {
268		*self.0.transport_disconnect_handler.write() = disconnect_handler;
269	}
270
271	pub(crate) fn subscribe(&self, event_name: impl Into<String>) -> bool {
272		self.0.subscriptions.write().insert(event_name.into())
273	}
274
275	pub(crate) fn unsubscribe(&self, event_name: &str) -> bool {
276		self.0.subscriptions.write().remove(event_name)
277	}
278
279	pub(crate) fn is_subscribed(&self, event_name: &str) -> bool {
280		self.0.subscriptions.read().contains(event_name)
281	}
282
283	pub(crate) fn subscriptions(&self) -> Vec<String> {
284		self.0.subscriptions.read().iter().cloned().collect()
285	}
286
287	pub(crate) fn clear_subscriptions(&self) {
288		self.0.subscriptions.write().clear();
289	}
290
291	pub(crate) fn configure_hibernation(
292		&self,
293		hibernation: Option<HibernatableConnectionMetadata>,
294	) {
295		*self.0.hibernation.write() = hibernation;
296	}
297
298	pub(crate) fn hibernation(&self) -> Option<HibernatableConnectionMetadata> {
299		self.0.hibernation.read().clone()
300	}
301
302	pub(crate) fn configure_state_change_handler(&self, handler: Option<StateChangeCallback>) {
303		*self.0.state_change_handler.write() = handler;
304	}
305
306	pub(crate) fn set_server_message_index(
307		&self,
308		message_index: u16,
309	) -> Option<HibernatableConnectionMetadata> {
310		let mut hibernation = self.0.hibernation.write();
311		let hibernation = hibernation.as_mut()?;
312		hibernation.server_message_index = message_index;
313		Some(hibernation.clone())
314	}
315
316	pub(crate) fn persisted_with_state(&self, state: Vec<u8>) -> Option<PersistedConnection> {
317		let hibernation = self.0.hibernation.read().clone()?;
318
319		Some(PersistedConnection {
320			id: self.id().to_owned(),
321			parameters: self.params(),
322			state,
323			subscriptions: self
324				.subscriptions()
325				.into_iter()
326				.map(|event_name| PersistedSubscription { event_name })
327				.collect(),
328			gateway_id: hibernation.gateway_id,
329			request_id: hibernation.request_id,
330			server_message_index: hibernation.server_message_index,
331			client_message_index: hibernation.client_message_index,
332			request_path: hibernation.request_path,
333			request_headers: hibernation.request_headers.into_iter().collect(),
334		})
335	}
336
337	pub(crate) fn from_persisted(persisted: PersistedConnection) -> Self {
338		let conn = Self::new(
339			persisted.id.clone(),
340			persisted.parameters,
341			persisted.state,
342			true,
343		);
344		conn.configure_hibernation(Some(HibernatableConnectionMetadata {
345			gateway_id: persisted.gateway_id,
346			request_id: persisted.request_id,
347			server_message_index: persisted.server_message_index,
348			client_message_index: persisted.client_message_index,
349			request_path: persisted.request_path,
350			request_headers: persisted.request_headers.into_iter().collect(),
351		}));
352		for subscription in persisted.subscriptions {
353			conn.subscribe(subscription.event_name);
354		}
355		conn.clear_hibernation_dirty();
356		conn
357	}
358
359	pub(crate) fn try_send(&self, name: &str, args: &[u8]) -> Result<()> {
360		let event_sender = self.event_sender()?;
361		event_sender(OutgoingEvent {
362			name: name.to_owned(),
363			args: args.to_vec(),
364		})
365	}
366
367	fn event_sender(&self) -> Result<EventSendCallback> {
368		self.0
369			.event_sender
370			.read()
371			.clone()
372			.ok_or_else(|| connection_not_configured("event sender"))
373	}
374
375	fn disconnect_handler(&self) -> Result<DisconnectCallback> {
376		self.0
377			.disconnect_handler
378			.read()
379			.clone()
380			.ok_or_else(|| connection_not_configured("disconnect handler"))
381	}
382
383	pub(crate) fn managed_disconnect_handler(&self) -> Result<DisconnectCallback> {
384		self.disconnect_handler()
385	}
386
387	pub(crate) async fn disconnect_transport_only(&self) -> Result<()> {
388		let Some(handler) = self.transport_disconnect_handler() else {
389			return Ok(());
390		};
391		handler(None).await
392	}
393
394	fn transport_disconnect_handler(&self) -> Option<DisconnectCallback> {
395		self.0.transport_disconnect_handler.read().clone()
396	}
397}
398
399impl Default for ConnHandle {
400	fn default() -> Self {
401		Self::new("", Vec::new(), Vec::new(), false)
402	}
403}
404
405impl fmt::Debug for ConnHandle {
406	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
407		f.debug_struct("ConnHandle")
408			.field("id", &self.0.id)
409			.field("is_hibernatable", &self.0.is_hibernatable)
410			.field("subscriptions", &self.subscriptions())
411			.finish()
412	}
413}
414
415#[derive(Default)]
416pub(crate) struct PendingHibernationChanges {
417	pub updated: BTreeSet<ConnId>,
418	pub removed: BTreeSet<ConnId>,
419}
420
421/// Lock-backed iterator over live connection handles.
422///
423/// Do not hold this iterator across `.await`. It keeps a read lock on the
424/// connection map until dropped, which blocks writers such as add/remove or
425/// connection reconfiguration.
426#[must_use = "connection iterators hold a read lock until dropped"]
427pub struct ConnHandles<'a> {
428	guard: RwLockReadGuard<'a, BTreeMap<ConnId, ConnHandle>>,
429	next_after: Option<ConnId>,
430}
431
432impl<'a> ConnHandles<'a> {
433	fn new(guard: RwLockReadGuard<'a, BTreeMap<ConnId, ConnHandle>>) -> Self {
434		Self {
435			guard,
436			next_after: None,
437		}
438	}
439
440	pub fn len(&self) -> usize {
441		self.guard.len()
442	}
443
444	pub fn is_empty(&self) -> bool {
445		self.guard.is_empty()
446	}
447}
448
449impl Iterator for ConnHandles<'_> {
450	type Item = ConnHandle;
451
452	fn next(&mut self) -> Option<Self::Item> {
453		let (conn_id, conn) = match self.next_after.as_ref() {
454			Some(conn_id) => self
455				.guard
456				.range((Excluded(conn_id.clone()), Unbounded))
457				.next()?,
458			None => self.guard.iter().next()?,
459		};
460		self.next_after = Some(conn_id.clone());
461		Some(conn.clone())
462	}
463}
464
465impl ActorContext {
466	pub(crate) fn configure_connection_storage(&self, config: ActorConfig) {
467		*self.0.connection_config.write() = config;
468	}
469
470	pub(crate) fn iter_connections(&self) -> ConnHandles<'_> {
471		ConnHandles::new(self.0.connections.read())
472	}
473
474	pub(crate) fn active_connection_count(&self) -> u32 {
475		self.0
476			.connections
477			.read()
478			.len()
479			.try_into()
480			.unwrap_or(u32::MAX)
481	}
482
483	pub(crate) fn insert_existing(&self, conn: ConnHandle) {
484		let conn_id = conn.id().to_owned();
485		let is_hibernatable = conn.is_hibernatable();
486		let active_count = {
487			let mut connections = self.0.connections.write();
488			connections.insert(conn_id.clone(), conn);
489			connections.len()
490		};
491		self.0.metrics.set_active_connections(active_count);
492		tracing::debug!(
493			actor_id = %self.actor_id(),
494			conn_id = %conn_id,
495			is_hibernatable,
496			active_count,
497			"connection added"
498		);
499	}
500
501	pub(crate) fn remove_existing(&self, conn_id: &str) -> Option<ConnHandle> {
502		let (removed, active_count) = {
503			let mut connections = self.0.connections.write();
504			let removed = connections.remove(conn_id);
505			(removed, connections.len())
506		};
507		self.0.metrics.set_active_connections(active_count);
508		tracing::debug!(
509			actor_id = %self.actor_id(),
510			conn_id,
511			removed = removed.is_some(),
512			active_count,
513			"connection removed"
514		);
515		removed
516	}
517
518	fn remove_existing_for_disconnect(&self, conn_id: &str) -> Option<ConnHandle> {
519		let _disconnect_state = self.0.connection_disconnect_state.lock();
520		let (removed, active_count) = {
521			let mut connections = self.0.connections.write();
522			let removed = connections.remove(conn_id)?;
523
524			if removed.is_hibernatable() {
525				self.0.pending_hibernation_updates.write().remove(conn_id);
526				self.0
527					.pending_hibernation_removals
528					.write()
529					.insert(conn_id.to_owned());
530			}
531
532			(removed, connections.len())
533		};
534		self.0.metrics.set_active_connections(active_count);
535		tracing::debug!(
536			actor_id = %self.actor_id(),
537			conn_id,
538			is_hibernatable = removed.is_hibernatable(),
539			active_count,
540			"connection removed for disconnect"
541		);
542		Some(removed)
543	}
544
545	pub(crate) fn queue_hibernation_update(&self, conn_id: impl Into<ConnId>) {
546		let _disconnect_state = self.0.connection_disconnect_state.lock();
547		let conn_id = conn_id.into();
548		self.0
549			.pending_hibernation_updates
550			.write()
551			.insert(conn_id.clone());
552		self.0.pending_hibernation_removals.write().remove(&conn_id);
553		tracing::debug!(
554			actor_id = %self.actor_id(),
555			conn_id = %conn_id,
556			"hibernatable connection transport queued for save"
557		);
558	}
559
560	pub(crate) fn dirty_hibernatable_conns_inner(&self) -> Vec<ConnHandle> {
561		let _disconnect_state = self.0.connection_disconnect_state.lock();
562		let update_ids: Vec<_> = self
563			.0
564			.pending_hibernation_updates
565			.read()
566			.iter()
567			.cloned()
568			.collect();
569		let connections = self.0.connections.read();
570		update_ids
571			.into_iter()
572			.filter_map(|conn_id| connections.get(&conn_id).cloned())
573			.filter(|conn| conn.is_hibernatable() && conn.hibernation().is_some())
574			.collect()
575	}
576
577	pub(crate) fn queue_hibernation_removal_inner(&self, conn_id: impl Into<ConnId>) {
578		let _disconnect_state = self.0.connection_disconnect_state.lock();
579		let conn_id = conn_id.into();
580		self.0.pending_hibernation_updates.write().remove(&conn_id);
581		self.0
582			.pending_hibernation_removals
583			.write()
584			.insert(conn_id.clone());
585		tracing::debug!(
586			actor_id = %self.actor_id(),
587			conn_id = %conn_id,
588			"hibernatable connection transport queued for removal"
589		);
590	}
591
592	pub(crate) fn take_pending_hibernation_changes_inner(&self) -> PendingHibernationChanges {
593		let _disconnect_state = self.0.connection_disconnect_state.lock();
594		PendingHibernationChanges {
595			updated: std::mem::take(&mut *self.0.pending_hibernation_updates.write()),
596			removed: std::mem::take(&mut *self.0.pending_hibernation_removals.write()),
597		}
598	}
599
600	pub(crate) fn pending_hibernation_removals(&self) -> Vec<ConnId> {
601		let _disconnect_state = self.0.connection_disconnect_state.lock();
602		self.0
603			.pending_hibernation_removals
604			.read()
605			.iter()
606			.cloned()
607			.collect()
608	}
609
610	pub(crate) fn has_pending_hibernation_changes_inner(&self) -> bool {
611		let _disconnect_state = self.0.connection_disconnect_state.lock();
612		let has_updates = !self.0.pending_hibernation_updates.read().is_empty();
613		let has_removals = !self.0.pending_hibernation_removals.read().is_empty();
614		has_updates || has_removals
615	}
616
617	pub(crate) fn restore_pending_hibernation_changes(&self, pending: PendingHibernationChanges) {
618		let _disconnect_state = self.0.connection_disconnect_state.lock();
619		if !pending.updated.is_empty() {
620			self.0
621				.pending_hibernation_updates
622				.write()
623				.extend(pending.updated);
624		}
625		if !pending.removed.is_empty() {
626			self.0
627				.pending_hibernation_removals
628				.write()
629				.extend(pending.removed);
630		}
631	}
632
633	pub(crate) async fn connect_with_state<F>(
634		&self,
635		params: Vec<u8>,
636		is_hibernatable: bool,
637		hibernation: Option<HibernatableConnectionMetadata>,
638		request: Option<Request>,
639		create_state: F,
640	) -> Result<ConnHandle>
641	where
642		F: std::future::Future<Output = Result<Vec<u8>>> + Send,
643	{
644		self.connect_with_state_and_prepare(
645			params,
646			is_hibernatable,
647			hibernation,
648			request,
649			create_state,
650			|_| Ok(()),
651		)
652		.await
653	}
654
655	pub(crate) async fn connect_with_state_and_prepare<F, P>(
656		&self,
657		params: Vec<u8>,
658		is_hibernatable: bool,
659		hibernation: Option<HibernatableConnectionMetadata>,
660		request: Option<Request>,
661		create_state: F,
662		prepare_connection: P,
663	) -> Result<ConnHandle>
664	where
665		F: std::future::Future<Output = Result<Vec<u8>>> + Send,
666		P: FnOnce(&ConnHandle) -> Result<()>,
667	{
668		let config = self.connection_config();
669
670		let state = timeout(config.create_conn_state_timeout, create_state)
671			.await
672			.with_context(|| {
673				timeout_message("create_conn_state", config.create_conn_state_timeout)
674			})??;
675
676		let conn = ConnHandle::new(
677			Uuid::new_v4().to_string(),
678			params.clone(),
679			state,
680			is_hibernatable,
681		);
682		conn.configure_hibernation(hibernation);
683		self.prepare_managed_conn(&conn);
684
685		if let Err(error) = prepare_connection(&conn) {
686			return Err(error);
687		}
688
689		self.emit_connection_preflight(&conn, params.clone(), request.clone())
690			.await?;
691		self.insert_existing(conn.clone());
692
693		if let Err(error) = self.emit_connection_open(&conn, request).await {
694			self.remove_existing(conn.id());
695			return Err(error);
696		}
697		self.0.metrics.inc_connections_total();
698		self.record_connections_updated();
699		self.reset_sleep_timer();
700
701		Ok(conn)
702	}
703
704	pub(crate) fn encode_hibernation_delta(
705		&self,
706		conn_id: &str,
707		bytes: Vec<u8>,
708	) -> Result<Vec<u8>> {
709		let conn = self.connection(conn_id).ok_or_else(|| {
710			ConnectionNotFound {
711				conn_id: conn_id.to_owned(),
712			}
713			.build()
714		})?;
715		let persisted = conn.persisted_with_state(bytes).ok_or_else(|| {
716			ConnectionNotHibernatable {
717				conn_id: conn_id.to_owned(),
718			}
719			.build()
720		})?;
721		encode_persisted_connection(&persisted).context("encode persisted connection")
722	}
723
724	pub(crate) async fn restore_persisted(&self) -> Result<Vec<ConnHandle>> {
725		let persisted_connections = internal_storage::load_connections(self.sql())
726			.await
727			.context("load hibernatable connections from sqlite")?;
728		let mut restored = Vec::new();
729
730		for persisted in persisted_connections {
731			let conn = ConnHandle::from_persisted(persisted);
732			self.prepare_managed_conn(&conn);
733			self.insert_existing(conn.clone());
734			tracing::debug!(
735				actor_id = %self.actor_id(),
736				conn_id = conn.id(),
737				"hibernatable connection restored"
738			);
739			restored.push(conn);
740		}
741
742		Ok(restored)
743	}
744
745	pub(crate) fn reconnect_hibernatable(
746		&self,
747		gateway_id: &[u8],
748		request_id: &[u8],
749	) -> Result<ConnHandle> {
750		let gateway_id = hibernatable_id_from_slice("gateway_id", gateway_id)?;
751		let request_id = hibernatable_id_from_slice("request_id", request_id)?;
752		let Some(conn) = self
753			.iter_connections()
754			.find(|conn| match conn.hibernation() {
755				Some(hibernation) => {
756					hibernation.gateway_id == gateway_id && hibernation.request_id == request_id
757				}
758				None => false,
759			})
760		else {
761			return Err(ConnectionRestoreNotFound.build());
762		};
763
764		self.record_connections_updated();
765		self.reset_sleep_timer();
766		tracing::debug!(
767			actor_id = %self.actor_id(),
768			conn_id = conn.id(),
769			"hibernatable connection transport restored"
770		);
771		Ok(conn)
772	}
773
774	fn prepare_managed_conn(&self, conn: &ConnHandle) {
775		let ctx = self.downgrade();
776		let conn_id = conn.id().to_owned();
777
778		conn.configure_state_change_handler(Some(Arc::new({
779			let ctx = ctx.clone();
780			move |conn| {
781				let Some(ctx) = ActorContext::from_weak(&ctx) else {
782					tracing::warn!(
783						conn_id = conn.id(),
784						"skipping hibernatable connection state save without actor context"
785					);
786					return;
787				};
788				ctx.queue_hibernation_update(conn.id().to_owned());
789				ctx.request_save(RequestSaveOpts::default());
790			}
791		})));
792
793		conn.configure_disconnect_handler(Some(Arc::new(move |reason| {
794			let ctx = ctx.clone();
795			let conn_id = conn_id.clone();
796			Box::pin(async move {
797				let ctx = ActorContext::from_weak(&ctx).ok_or_else(|| {
798					ActorRuntime::NotConfigured {
799						component: "actor context".to_owned(),
800					}
801					.build()
802				})?;
803				ctx.with_disconnect_callback(|| async {
804					ctx.disconnect_managed(&conn_id, reason).await
805				})
806				.await
807			})
808		})));
809	}
810
811	fn connection_config(&self) -> ActorConfig {
812		self.0.connection_config.read().clone()
813	}
814
815	#[cfg(test)]
816	pub(crate) fn connection_config_for_tests(&self) -> ActorConfig {
817		self.connection_config()
818	}
819
820	async fn disconnect_managed(&self, conn_id: &str, reason: Option<String>) -> Result<()> {
821		let Some(conn) = self.remove_existing_for_disconnect(conn_id) else {
822			tracing::debug!(
823				actor_id = %self.actor_id(),
824				conn_id,
825				reason = ?reason.as_deref(),
826				"connection disconnect skipped because connection was already removed"
827			);
828			return Ok(());
829		};
830		conn.clear_subscriptions();
831
832		self.try_send_actor_event(ActorEvent::ConnectionClosed { conn }, "connection_closed")
833			.with_context(|| disconnect_message(conn_id, reason.as_deref()))?;
834
835		self.record_connections_updated();
836		self.reset_sleep_timer();
837		tracing::debug!(
838			actor_id = %self.actor_id(),
839			conn_id,
840			reason = ?reason.as_deref(),
841			"connection disconnected"
842		);
843		Ok(())
844	}
845
846	async fn emit_connection_open(
847		&self,
848		conn: &ConnHandle,
849		request: Option<Request>,
850	) -> Result<()> {
851		let config = self.connection_config();
852		let (reply_tx, reply_rx) = oneshot::channel();
853		self.try_send_actor_event(
854			ActorEvent::ConnectionOpen {
855				conn: conn.clone(),
856				request,
857				reply: Reply::from(reply_tx),
858			},
859			"connection_open",
860		)?;
861		timeout(config.on_connect_timeout, reply_rx)
862			.await
863			.with_context(|| timeout_message("connection_open", config.on_connect_timeout))?
864			.context("receive connection_open reply")??;
865		Ok(())
866	}
867
868	async fn emit_connection_preflight(
869		&self,
870		conn: &ConnHandle,
871		params: Vec<u8>,
872		request: Option<Request>,
873	) -> Result<()> {
874		let config = self.connection_config();
875		let timeout_duration = config
876			.on_before_connect_timeout
877			.saturating_add(config.create_conn_state_timeout);
878		let (reply_tx, reply_rx) = oneshot::channel();
879		self.try_send_actor_event(
880			ActorEvent::ConnectionPreflight {
881				conn: conn.clone(),
882				params,
883				request,
884				reply: Reply::from(reply_tx),
885			},
886			"connection_preflight",
887		)?;
888		timeout(timeout_duration, reply_rx)
889			.await
890			.with_context(|| timeout_message("connection_preflight", timeout_duration))?
891			.context("receive connection_preflight reply")??;
892		Ok(())
893	}
894
895	pub(crate) fn connection(&self, conn_id: &str) -> Option<ConnHandle> {
896		self.0.connections.read().get(conn_id).cloned()
897	}
898
899	pub(crate) async fn disconnect_transport_only<F>(&self, mut predicate: F) -> Result<()>
900	where
901		F: FnMut(&ConnHandle) -> bool,
902	{
903		let connections: Vec<_> = self
904			.iter_connections()
905			.filter(|conn| predicate(conn))
906			.collect();
907		let mut disconnected_ids = Vec::new();
908		let mut failures = Vec::new();
909
910		for conn in &connections {
911			match conn.disconnect_transport_only().await {
912				Ok(()) => {
913					tracing::debug!(
914						actor_id = %self.actor_id(),
915						conn_id = conn.id(),
916						"connection transport disconnect completed"
917					);
918					disconnected_ids.push(conn.id().to_owned());
919				}
920				Err(error) => {
921					tracing::error!(
922						conn_id = %conn.id(),
923						?error,
924						"failed transport-only connection disconnect"
925					);
926					failures.push((conn.id().to_owned(), format!("{error:#}")));
927				}
928			}
929		}
930
931		let mut removed_any = false;
932		for conn_id in disconnected_ids {
933			let Some(conn) = self.remove_existing_for_disconnect(&conn_id) else {
934				tracing::debug!(
935					actor_id = %self.actor_id(),
936					conn_id = %conn_id,
937					"connection transport removal skipped because connection was already removed"
938				);
939				continue;
940			};
941			conn.clear_subscriptions();
942			removed_any = true;
943			tracing::debug!(
944				actor_id = %self.actor_id(),
945				conn_id = %conn_id,
946				"connection transport removed"
947			);
948		}
949
950		if removed_any {
951			self.record_connections_updated();
952			self.reset_sleep_timer();
953		}
954
955		if failures.is_empty() {
956			return Ok(());
957		}
958
959		let count = failures.len();
960		Err(ConnectionDisconnectFailed {
961			count,
962			details: failures
963				.into_iter()
964				.map(|(conn_id, error)| format!("{conn_id}: {error}"))
965				.collect::<Vec<_>>()
966				.join("; "),
967		}
968		.build())
969	}
970}
971
972fn connection_not_configured(component: &str) -> anyhow::Error {
973	ConnectionNotConfigured {
974		component: component.to_owned(),
975	}
976	.build()
977}
978
979fn timeout_message(callback_name: &str, timeout: Duration) -> String {
980	format!(
981		"`{callback_name}` timed out after {} ms",
982		timeout.as_millis()
983	)
984}
985
986fn disconnect_message(conn_id: &str, reason: Option<&str>) -> String {
987	match reason {
988		Some(reason) => format!("disconnect connection `{conn_id}` with reason `{reason}`"),
989		None => format!("disconnect connection `{conn_id}`"),
990	}
991}
992
993// Test shim keeps moved tests in crate-root tests/ with private-module access.
994#[cfg(test)]
995#[path = "../../tests/connection.rs"]
996mod tests;