Skip to main content

rivetkit_core/actor/
config.rs

1use std::fmt;
2use std::sync::Arc;
3use std::time::Duration;
4
5use rivet_envoy_client::config::HttpRequest;
6
7use crate::inspector::InspectorTabEntry;
8
9const DEFAULT_STATE_SAVE_INTERVAL: Duration = Duration::from_secs(1);
10const DEFAULT_CREATE_VARS_TIMEOUT: Duration = Duration::from_secs(5);
11const DEFAULT_CREATE_CONN_STATE_TIMEOUT: Duration = Duration::from_secs(5);
12const DEFAULT_ON_BEFORE_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
13const DEFAULT_ON_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
14const DEFAULT_ON_MIGRATE_TIMEOUT: Duration = Duration::from_secs(30);
15const DEFAULT_ACTION_TIMEOUT: Duration = Duration::from_secs(60);
16const DEFAULT_SLEEP_TIMEOUT: Duration = Duration::from_secs(30);
17const DEFAULT_SLEEP_GRACE_PERIOD: Duration = Duration::from_secs(15);
18const DEFAULT_CONNECTION_LIVENESS_TIMEOUT: Duration = Duration::from_millis(2500);
19const DEFAULT_CONNECTION_LIVENESS_INTERVAL: Duration = Duration::from_secs(5);
20const DEFAULT_MAX_QUEUE_SIZE: u32 = 1000;
21pub const DEFAULT_MAX_SCHEDULES: u32 = 1_000;
22const DEFAULT_MAX_QUEUE_MESSAGE_SIZE: u32 = 65_536;
23const DEFAULT_MAX_INCOMING_MESSAGE_SIZE: u32 = 65_536;
24const DEFAULT_MAX_OUTGOING_MESSAGE_SIZE: u32 = 1_048_576;
25pub(crate) const MAX_SQLITE_TRANSACTION_TRACE_STATEMENTS: usize = 32;
26
27#[derive(Clone)]
28pub enum CanHibernateWebSocket {
29	Bool(bool),
30	Callback(Arc<dyn Fn(&HttpRequest) -> bool + Send + Sync>),
31}
32
33impl fmt::Debug for CanHibernateWebSocket {
34	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35		match self {
36			Self::Bool(value) => f.debug_tuple("Bool").field(value).finish(),
37			Self::Callback(_) => f.write_str("Callback(..)"),
38		}
39	}
40}
41
42impl Default for CanHibernateWebSocket {
43	fn default() -> Self {
44		Self::Bool(false)
45	}
46}
47
48#[derive(Clone, Debug, Default)]
49pub struct ActorConfigOverrides {
50	pub sleep_grace_period: Option<Duration>,
51}
52
53#[derive(Clone, Debug)]
54pub struct ActionDefinition {
55	pub name: String,
56}
57
58/// Experimental SQLite profiling configuration.
59///
60/// This entire configuration surface, including every field, is subject to
61/// change without notice.
62#[derive(Clone, Debug)]
63pub struct SqliteProfilingConfig {
64	pub enabled: bool,
65	pub max_tracked_statement_fingerprints: usize,
66	pub max_tracked_transaction_fingerprints: usize,
67	pub max_prometheus_series: usize,
68	pub max_statements_per_transaction_trace: usize,
69	pub max_get_pages_requests_per_trace: usize,
70	pub max_transaction_name_bytes: usize,
71	pub slow_operation_threshold_ms: u64,
72	pub baseline_sample_rate: f64,
73	pub max_diagnostic_events_per_minute: usize,
74	pub diagnostic_event_queue_capacity: usize,
75}
76
77impl Default for SqliteProfilingConfig {
78	fn default() -> Self {
79		Self {
80			enabled: true,
81			max_tracked_statement_fingerprints: 128,
82			max_tracked_transaction_fingerprints: 8,
83			max_prometheus_series: 25_000,
84			max_statements_per_transaction_trace: 32,
85			max_get_pages_requests_per_trace: 16,
86			max_transaction_name_bytes: 128,
87			slow_operation_threshold_ms: 10,
88			baseline_sample_rate: 0.001,
89			max_diagnostic_events_per_minute: 120,
90			diagnostic_event_queue_capacity: 256,
91		}
92	}
93}
94
95/// Sparse experimental SQLite profiling configuration used at runtime
96/// boundaries.
97///
98/// This entire configuration surface, including every field, is subject to
99/// change without notice.
100#[derive(Clone, Debug, Default)]
101pub struct SqliteProfilingConfigInput {
102	pub enabled: Option<bool>,
103	pub max_tracked_statement_fingerprints: Option<u32>,
104	pub max_tracked_transaction_fingerprints: Option<u32>,
105	pub max_prometheus_series: Option<u32>,
106	pub max_statements_per_transaction_trace: Option<u32>,
107	pub max_get_pages_requests_per_trace: Option<u32>,
108	pub max_transaction_name_bytes: Option<u32>,
109	pub slow_operation_threshold_ms: Option<u32>,
110	pub baseline_sample_rate: Option<f64>,
111	pub max_diagnostic_events_per_minute: Option<u32>,
112	pub diagnostic_event_queue_capacity: Option<u32>,
113}
114
115impl SqliteProfilingConfig {
116	fn from_input(input: SqliteProfilingConfigInput) -> Self {
117		let mut config = Self::default();
118		macro_rules! set_usize {
119			($field:ident) => {
120				if let Some(value) = input.$field {
121					config.$field = value as usize;
122				}
123			};
124		}
125		if let Some(value) = input.enabled {
126			config.enabled = value;
127		}
128		set_usize!(max_tracked_statement_fingerprints);
129		set_usize!(max_tracked_transaction_fingerprints);
130		set_usize!(max_prometheus_series);
131		set_usize!(max_statements_per_transaction_trace);
132		set_usize!(max_get_pages_requests_per_trace);
133		set_usize!(max_transaction_name_bytes);
134		if let Some(value) = input.slow_operation_threshold_ms {
135			config.slow_operation_threshold_ms = u64::from(value);
136		}
137		if let Some(value) = input.baseline_sample_rate {
138			config.baseline_sample_rate = value;
139		}
140		set_usize!(max_diagnostic_events_per_minute);
141		set_usize!(diagnostic_event_queue_capacity);
142		config
143	}
144}
145
146#[derive(Clone, Debug)]
147pub struct ActorConfig {
148	pub name: Option<String>,
149	pub icon: Option<String>,
150	/// Whether the user declared a SQLite database for this actor (`db({...})`
151	/// on the TS side). Gates the inspector database tab.
152	pub has_database: bool,
153	pub remote_sqlite: bool,
154	pub sqlite_profiling: SqliteProfilingConfig,
155	/// Enables the experimental Actor Runtime Socket.
156	pub enable_actor_runtime_socket: bool,
157	/// Whether the user declared actor state (`state: ...` or `createState`).
158	/// Gates the inspector state tab and state-subscription messages.
159	pub has_state: bool,
160	pub can_hibernate_websocket: CanHibernateWebSocket,
161	pub state_save_interval: Duration,
162	pub create_vars_timeout: Duration,
163	pub create_conn_state_timeout: Duration,
164	pub on_before_connect_timeout: Duration,
165	pub on_connect_timeout: Duration,
166	pub on_migrate_timeout: Duration,
167	pub action_timeout: Duration,
168	pub sleep_timeout: Duration,
169	pub no_sleep: bool,
170	pub sleep_grace_period: Duration,
171	pub sleep_grace_period_overridden: bool,
172	pub connection_liveness_timeout: Duration,
173	pub connection_liveness_interval: Duration,
174	pub max_queue_size: u32,
175	pub max_schedules: u32,
176	pub max_queue_message_size: u32,
177	pub max_incoming_message_size: u32,
178	pub max_outgoing_message_size: u32,
179	pub overrides: Option<ActorConfigOverrides>,
180	pub actions: Vec<ActionDefinition>,
181	/// Author-declared inspector tab entries (custom tabs + built-in
182	/// hides). Validated upstream (Zod / builder).
183	pub inspector_tabs: Vec<InspectorTabEntry>,
184}
185
186/// Sparse, serialization-friendly actor configuration. All fields are optional with millisecond integers instead of Duration. Used at runtime boundaries (NAPI, config files). Convert to ActorConfig via ActorConfig::from_input().
187#[derive(Clone, Debug, Default)]
188pub struct ActorConfigInput {
189	pub name: Option<String>,
190	pub icon: Option<String>,
191	pub has_database: Option<bool>,
192	pub remote_sqlite: Option<bool>,
193	pub sqlite_profiling: Option<SqliteProfilingConfigInput>,
194	pub enable_actor_runtime_socket: Option<bool>,
195	pub has_state: Option<bool>,
196	pub can_hibernate_websocket: Option<bool>,
197	pub state_save_interval_ms: Option<u32>,
198	pub create_vars_timeout_ms: Option<u32>,
199	pub create_conn_state_timeout_ms: Option<u32>,
200	pub on_before_connect_timeout_ms: Option<u32>,
201	pub on_connect_timeout_ms: Option<u32>,
202	pub on_migrate_timeout_ms: Option<u32>,
203	pub action_timeout_ms: Option<u32>,
204	pub sleep_timeout_ms: Option<u32>,
205	pub no_sleep: Option<bool>,
206	pub sleep_grace_period_ms: Option<u32>,
207	pub connection_liveness_timeout_ms: Option<u32>,
208	pub connection_liveness_interval_ms: Option<u32>,
209	pub max_queue_size: Option<u32>,
210	pub max_schedules: Option<u32>,
211	pub max_queue_message_size: Option<u32>,
212	pub max_incoming_message_size: Option<u32>,
213	pub max_outgoing_message_size: Option<u32>,
214	pub actions: Option<Vec<ActionDefinition>>,
215	pub inspector_tabs: Option<Vec<InspectorTabEntry>>,
216}
217
218impl ActorConfig {
219	pub fn from_input(config: ActorConfigInput) -> Self {
220		let mut actor_config = Self {
221			name: config.name,
222			icon: config.icon,
223			has_database: config.has_database.unwrap_or(false),
224			remote_sqlite: config.remote_sqlite.unwrap_or(false),
225			sqlite_profiling: config
226				.sqlite_profiling
227				.map(SqliteProfilingConfig::from_input)
228				.unwrap_or_default(),
229			enable_actor_runtime_socket: config.enable_actor_runtime_socket.unwrap_or(false),
230			has_state: config.has_state.unwrap_or(false),
231			..Self::default()
232		};
233		if let Some(can_hibernate_websocket) = config.can_hibernate_websocket {
234			actor_config.can_hibernate_websocket =
235				CanHibernateWebSocket::Bool(can_hibernate_websocket);
236		}
237		if let Some(value) = config.state_save_interval_ms {
238			actor_config.state_save_interval = duration_ms(value);
239		}
240		if let Some(value) = config.create_vars_timeout_ms {
241			actor_config.create_vars_timeout = duration_ms(value);
242		}
243		if let Some(value) = config.create_conn_state_timeout_ms {
244			actor_config.create_conn_state_timeout = duration_ms(value);
245		}
246		if let Some(value) = config.on_before_connect_timeout_ms {
247			actor_config.on_before_connect_timeout = duration_ms(value);
248		}
249		if let Some(value) = config.on_connect_timeout_ms {
250			actor_config.on_connect_timeout = duration_ms(value);
251		}
252		if let Some(value) = config.on_migrate_timeout_ms {
253			actor_config.on_migrate_timeout = duration_ms(value);
254		}
255		if let Some(value) = config.action_timeout_ms {
256			actor_config.action_timeout = duration_ms(value);
257		}
258		if let Some(value) = config.sleep_timeout_ms {
259			actor_config.sleep_timeout = duration_ms(value);
260		}
261		if let Some(value) = config.no_sleep {
262			actor_config.no_sleep = value;
263		}
264		if let Some(value) = config.sleep_grace_period_ms {
265			actor_config.sleep_grace_period = duration_ms(value);
266			actor_config.sleep_grace_period_overridden = true;
267		}
268		if let Some(value) = config.connection_liveness_timeout_ms {
269			actor_config.connection_liveness_timeout = duration_ms(value);
270		}
271		if let Some(value) = config.connection_liveness_interval_ms {
272			actor_config.connection_liveness_interval = duration_ms(value);
273		}
274		if let Some(value) = config.max_queue_size {
275			actor_config.max_queue_size = value;
276		}
277		if let Some(value) = config.max_schedules {
278			actor_config.max_schedules = value;
279		}
280		if let Some(value) = config.max_queue_message_size {
281			actor_config.max_queue_message_size = value;
282		}
283		if let Some(value) = config.max_incoming_message_size {
284			actor_config.max_incoming_message_size = value;
285		}
286		if let Some(value) = config.max_outgoing_message_size {
287			actor_config.max_outgoing_message_size = value;
288		}
289		if let Some(actions) = config.actions {
290			actor_config.actions = actions;
291		}
292		if let Some(tabs) = config.inspector_tabs {
293			actor_config.inspector_tabs = tabs;
294		}
295
296		actor_config
297	}
298
299	pub fn effective_sleep_grace_period(&self) -> Duration {
300		cap_duration(
301			self.sleep_grace_period,
302			self.overrides
303				.as_ref()
304				.and_then(|overrides| overrides.sleep_grace_period),
305		)
306	}
307
308	/// Runtime authority for rejecting malformed config that bypassed the
309	/// TypeScript Zod layer (direct Rust builders, NAPI shim corruption,
310	/// etc.). Call this before constructing an `ActorFactory` from this
311	/// config so the actor never starts with garbage state.
312	pub fn validate(&self) -> anyhow::Result<()> {
313		crate::inspector::validate_inspector_tabs(&self.inspector_tabs)?;
314		anyhow::ensure!(
315			self.sqlite_profiling.baseline_sample_rate.is_finite()
316				&& (0.0..=1.0).contains(&self.sqlite_profiling.baseline_sample_rate),
317			"SQLite profiling baselineSampleRate must be between 0 and 1"
318		);
319		anyhow::ensure!(
320			self.sqlite_profiling.max_get_pages_requests_per_trace <= 16,
321			"SQLite profiling maxGetPagesRequestsPerTrace must be at most 16"
322		);
323		anyhow::ensure!(
324			self.sqlite_profiling.max_statements_per_transaction_trace
325				<= MAX_SQLITE_TRANSACTION_TRACE_STATEMENTS,
326			"SQLite profiling maxStatementsPerTransactionTrace must be at most 32"
327		);
328		anyhow::ensure!(
329			self.sqlite_profiling.max_transaction_name_bytes > 0,
330			"SQLite profiling maxTransactionNameBytes must be greater than zero"
331		);
332		Ok(())
333	}
334}
335
336impl Default for ActorConfig {
337	fn default() -> Self {
338		Self {
339			name: None,
340			icon: None,
341			has_database: false,
342			remote_sqlite: false,
343			sqlite_profiling: SqliteProfilingConfig::default(),
344			enable_actor_runtime_socket: false,
345			has_state: false,
346			can_hibernate_websocket: CanHibernateWebSocket::default(),
347			state_save_interval: DEFAULT_STATE_SAVE_INTERVAL,
348			create_vars_timeout: DEFAULT_CREATE_VARS_TIMEOUT,
349			create_conn_state_timeout: DEFAULT_CREATE_CONN_STATE_TIMEOUT,
350			on_before_connect_timeout: DEFAULT_ON_BEFORE_CONNECT_TIMEOUT,
351			on_connect_timeout: DEFAULT_ON_CONNECT_TIMEOUT,
352			on_migrate_timeout: DEFAULT_ON_MIGRATE_TIMEOUT,
353			action_timeout: DEFAULT_ACTION_TIMEOUT,
354			sleep_timeout: DEFAULT_SLEEP_TIMEOUT,
355			no_sleep: false,
356			sleep_grace_period: DEFAULT_SLEEP_GRACE_PERIOD,
357			sleep_grace_period_overridden: false,
358			connection_liveness_timeout: DEFAULT_CONNECTION_LIVENESS_TIMEOUT,
359			connection_liveness_interval: DEFAULT_CONNECTION_LIVENESS_INTERVAL,
360			max_queue_size: DEFAULT_MAX_QUEUE_SIZE,
361			max_schedules: DEFAULT_MAX_SCHEDULES,
362			max_queue_message_size: DEFAULT_MAX_QUEUE_MESSAGE_SIZE,
363			max_incoming_message_size: DEFAULT_MAX_INCOMING_MESSAGE_SIZE,
364			max_outgoing_message_size: DEFAULT_MAX_OUTGOING_MESSAGE_SIZE,
365			overrides: None,
366			actions: Vec::new(),
367			inspector_tabs: Vec::new(),
368		}
369	}
370}
371
372fn cap_duration(duration: Duration, override_duration: Option<Duration>) -> Duration {
373	if let Some(override_duration) = override_duration {
374		duration.min(override_duration)
375	} else {
376		duration
377	}
378}
379
380fn duration_ms(value: u32) -> Duration {
381	Duration::from_millis(u64::from(value))
382}
383
384// Test shim keeps moved tests in crate-root tests/ with private-module access.
385#[cfg(test)]
386#[path = "../../tests/config.rs"]
387mod tests;