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