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