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