Skip to main content

rivetkit_core/actor/
state.rs

1use std::sync::Arc;
2use std::sync::atomic::Ordering;
3use std::time::Duration;
4
5use crate::time::Instant as StdInstant;
6use crate::time::sleep;
7
8use anyhow::{Context, Result};
9use rivetkit_actor_persist::{generated::v4 as persist_v4, versioned as persist_versioned};
10#[cfg(not(feature = "wasm-runtime"))]
11use tokio::runtime::Handle;
12use tokio::sync::{Mutex as AsyncMutex, OwnedMutexGuard, mpsc, oneshot};
13use tokio::task::JoinHandle;
14#[cfg(test)]
15use tokio::time::timeout;
16use tracing::Instrument;
17
18use crate::actor::connection::{PersistedConnection, decode_persisted_connection};
19use crate::actor::context::ActorContext;
20use crate::actor::internal_storage;
21use crate::actor::lifecycle_hooks::Reply;
22use crate::actor::messages::{StateDelta, WorkflowKvWrite};
23use crate::actor::persist::decode_latest_with_embedded_version;
24#[cfg(test)]
25use crate::actor::persist::encode_latest_with_embedded_version;
26use crate::actor::task::LifecycleEvent;
27use crate::actor::task_types::StateMutationReason;
28use crate::error::ActorRuntime;
29#[cfg(feature = "wasm-runtime")]
30use crate::runtime::RuntimeSpawner;
31use crate::sqlite::{BindParam, ExecuteResult, SqliteTransaction};
32use crate::types::SaveStateOpts;
33
34#[cfg(test)]
35const LAST_PUSHED_ALARM_VERSION: u16 = 1;
36
37pub type PersistedScheduleEvent = persist_v4::ScheduleEvent;
38pub type PersistedActor = persist_v4::Actor;
39
40#[cfg(test)]
41pub(crate) fn encode_persisted_actor(actor: &PersistedActor) -> Result<Vec<u8>> {
42	encode_latest_with_embedded_version::<persist_versioned::Actor>(
43		actor.clone(),
44		rivetkit_actor_persist::CURRENT_VERSION,
45		"persisted actor",
46	)
47}
48
49pub(crate) fn decode_persisted_actor(payload: &[u8]) -> Result<PersistedActor> {
50	let actor = decode_latest_with_embedded_version::<persist_versioned::Actor>(
51		payload,
52		"persisted actor",
53	)?;
54	Ok(actor)
55}
56
57#[cfg(test)]
58pub(crate) fn encode_last_pushed_alarm(alarm_ts: Option<i64>) -> Result<Vec<u8>> {
59	encode_latest_with_embedded_version::<persist_versioned::LastPushedAlarm>(
60		alarm_ts,
61		LAST_PUSHED_ALARM_VERSION,
62		"last pushed alarm",
63	)
64}
65
66pub(crate) fn decode_last_pushed_alarm(payload: &[u8]) -> Result<Option<i64>> {
67	decode_latest_with_embedded_version::<persist_versioned::LastPushedAlarm>(
68		payload,
69		"last pushed alarm",
70	)
71}
72
73#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
74pub struct RequestSaveOpts {
75	pub immediate: bool,
76	pub max_wait_ms: Option<u32>,
77}
78
79pub(super) struct PendingSave {
80	scheduled_at: StdInstant,
81	handle: JoinHandle<()>,
82}
83
84pub struct OnStateChangeGuard {
85	ctx: Option<ActorContext>,
86}
87/// A SQLite transaction that owns the actor state-save exclusion until it is
88/// committed or rolled back.
89#[derive(Clone)]
90pub struct ActorStateTransaction {
91	owner: Arc<AsyncMutex<ActorStateTransactionOwner>>,
92}
93
94struct ActorStateTransactionOwner {
95	ctx: ActorContext,
96	transaction: SqliteTransaction,
97	save_guard: Option<OwnedMutexGuard<()>>,
98	write_guard: Option<InFlightWrite>,
99	finalized: bool,
100	committed: bool,
101	save_request_revision: u64,
102	statement_count: usize,
103	bind_payload_bytes: usize,
104	rollback_connection_states: Vec<(String, Vec<u8>)>,
105}
106
107impl ActorStateTransactionOwner {
108	fn restore_connection_states(&self) {
109		for (conn_id, state) in &self.rollback_connection_states {
110			if let Some(conn) = self.ctx.connection(conn_id) {
111				conn.set_state(state.clone());
112			}
113		}
114	}
115
116	fn advance_epoch(&self) {
117		self.ctx
118			.0
119			.state_transaction_epoch
120			.fetch_add(1, Ordering::SeqCst);
121	}
122}
123
124impl Drop for ActorStateTransactionOwner {
125	fn drop(&mut self) {
126		if !self.finalized {
127			self.restore_connection_states();
128			self.advance_epoch();
129			let transaction = self.transaction.clone();
130			#[cfg(not(feature = "wasm-runtime"))]
131			if let Ok(handle) = Handle::try_current() {
132				handle.spawn(async move {
133					if let Err(error) = transaction.rollback().await {
134						tracing::debug!(?error, "dropped actor state transaction rollback failed");
135					}
136				});
137			}
138			#[cfg(feature = "wasm-runtime")]
139			wasm_bindgen_futures::spawn_local(async move {
140				if let Err(error) = transaction.rollback().await {
141					tracing::debug!(?error, "dropped actor state transaction rollback failed");
142				}
143			});
144		}
145		if !self.committed {
146			self.ctx.schedule_save(None);
147		}
148	}
149}
150
151impl ActorStateTransaction {
152	pub async fn execute(
153		&self,
154		sql: impl Into<String>,
155		params: Option<Vec<BindParam>>,
156	) -> Result<ExecuteResult> {
157		let mut owner = self.owner.lock().await;
158		if owner.finalized {
159			return Err(anyhow::anyhow!(
160				"actor state transaction is already finalized"
161			));
162		}
163		let sql = sql.into();
164		if is_transaction_terminator(&sql) {
165			return Err(anyhow::anyhow!(
166				"cannot execute transaction terminator inside an actor state transaction; use the transaction commit or rollback method"
167			));
168		}
169		let payload_bytes = match params.as_deref() {
170			Some(params) => params
171				.iter()
172				.map(internal_storage::bind_param_payload_len)
173				.fold(0usize, usize::saturating_add),
174			None => sql.len(),
175		};
176		let result = owner.transaction.execute(sql, params).await?;
177		owner.statement_count = owner.statement_count.saturating_add(1);
178		owner.bind_payload_bytes = owner.bind_payload_bytes.saturating_add(payload_bytes);
179		Ok(result)
180	}
181
182	pub async fn commit(&self, deltas: Vec<StateDelta>) -> Result<()> {
183		let mut owner = self.owner.lock().await;
184		if owner.finalized {
185			return Err(anyhow::anyhow!(
186				"actor state transaction is already finalized"
187			));
188		}
189
190		let save_request_revision = owner.save_request_revision;
191		let transaction = owner.transaction.clone();
192		let statement_count = owner.statement_count;
193		let bind_payload_bytes = owner.bind_payload_bytes;
194		let result = owner
195			.ctx
196			.commit_state_transaction(
197				&transaction,
198				deltas,
199				save_request_revision,
200				statement_count,
201				bind_payload_bytes,
202			)
203			.await;
204		owner.finalized = true;
205		owner.committed = result.is_ok();
206		if result.is_err() {
207			owner.restore_connection_states();
208		}
209		owner.advance_epoch();
210		owner.write_guard.take();
211		owner.save_guard.take();
212		if result.is_err() {
213			owner.ctx.schedule_save(None);
214		}
215		result
216	}
217
218	pub async fn rollback(&self) -> Result<()> {
219		let mut owner = self.owner.lock().await;
220		if owner.finalized {
221			return Err(anyhow::anyhow!(
222				"actor state transaction is already finalized"
223			));
224		}
225		let result = owner.transaction.rollback().await;
226		owner.finalized = true;
227		owner.restore_connection_states();
228		owner.advance_epoch();
229		owner.write_guard.take();
230		owner.save_guard.take();
231		owner.ctx.schedule_save(None);
232		result
233	}
234}
235
236fn is_transaction_terminator(sql: &str) -> bool {
237	let mut offset = 0;
238	let Some(first) = next_sql_keyword(sql, &mut offset) else {
239		return false;
240	};
241	if first.eq_ignore_ascii_case("COMMIT") || first.eq_ignore_ascii_case("END") {
242		return true;
243	}
244	if !first.eq_ignore_ascii_case("ROLLBACK") {
245		return false;
246	}
247
248	let mut next = next_sql_keyword(sql, &mut offset);
249	if next.is_some_and(|keyword| keyword.eq_ignore_ascii_case("TRANSACTION")) {
250		next = next_sql_keyword(sql, &mut offset);
251	}
252	!next.is_some_and(|keyword| keyword.eq_ignore_ascii_case("TO"))
253}
254
255fn next_sql_keyword<'a>(sql: &'a str, offset: &mut usize) -> Option<&'a str> {
256	let bytes = sql.as_bytes();
257	loop {
258		while bytes.get(*offset).is_some_and(u8::is_ascii_whitespace) {
259			*offset += 1;
260		}
261		if bytes.get(*offset..*offset + 2) == Some(b"--") {
262			*offset += 2;
263			while bytes.get(*offset).is_some_and(|byte| *byte != b'\n') {
264				*offset += 1;
265			}
266			continue;
267		}
268		if bytes.get(*offset..*offset + 2) == Some(b"/*") {
269			*offset += 2;
270			while bytes.get(*offset..*offset + 2) != Some(b"*/") {
271				bytes.get(*offset)?;
272				*offset += 1;
273			}
274			*offset += 2;
275			continue;
276		}
277		break;
278	}
279
280	let start = *offset;
281	while bytes.get(*offset).is_some_and(u8::is_ascii_alphabetic) {
282		*offset += 1;
283	}
284	(start != *offset).then(|| &sql[start..*offset])
285}
286
287impl OnStateChangeGuard {
288	fn new(ctx: ActorContext) -> Self {
289		ctx.on_state_change_started();
290		Self { ctx: Some(ctx) }
291	}
292}
293
294impl Drop for OnStateChangeGuard {
295	fn drop(&mut self) {
296		if let Some(ctx) = self.ctx.take() {
297			ctx.on_state_change_finished();
298		}
299	}
300}
301
302impl ActorContext {
303	pub async fn begin_state_transaction(
304		&self,
305		timeout: Option<Duration>,
306	) -> Result<ActorStateTransaction> {
307		self.clear_pending_save();
308		let save_guard = Arc::clone(&self.0.save_guard).lock_owned().await;
309		self.0
310			.state_transaction_epoch
311			.fetch_add(1, Ordering::SeqCst);
312		self.wait_for_in_flight_writes().await;
313		let rollback_connection_states = self
314			.iter_connections()
315			.filter(|conn| conn.is_hibernatable())
316			.map(|conn| (conn.id().to_owned(), conn.state()))
317			.collect();
318		let sql = self.invocation_sql();
319		let transaction = match sql.begin_transaction(timeout).await {
320			Ok(transaction) => transaction,
321			Err(error) => {
322				self.0
323					.state_transaction_epoch
324					.fetch_add(1, Ordering::SeqCst);
325				drop(save_guard);
326				self.schedule_save(None);
327				return Err(error);
328			}
329		};
330		let write_guard = self.begin_write();
331		let save_request_revision = self.save_request_revision();
332		Ok(ActorStateTransaction {
333			owner: Arc::new(AsyncMutex::new(ActorStateTransactionOwner {
334				ctx: self.clone(),
335				transaction,
336				save_guard: Some(save_guard),
337				write_guard: Some(write_guard),
338				finalized: false,
339				committed: false,
340				save_request_revision,
341				statement_count: 0,
342				bind_payload_bytes: 0,
343				rollback_connection_states,
344			})),
345		})
346	}
347	pub fn state(&self) -> Vec<u8> {
348		self.0.current_state.read().clone()
349	}
350
351	pub(crate) async fn persist_state(&self, opts: SaveStateOpts) -> Result<()> {
352		if !self.is_dirty() {
353			return Ok(());
354		}
355
356		let result = if opts.immediate {
357			self.clear_pending_save();
358			self.persist_if_dirty().await
359		} else {
360			let delay = self.compute_save_delay(None);
361			if !delay.is_zero() {
362				sleep(delay).await;
363			}
364			self.persist_if_dirty().await
365		};
366		result?;
367		self.record_state_updated();
368		Ok(())
369	}
370
371	/// Foreign-runtime bootstrap hook for installing the actor state snapshot
372	/// before the actor starts handling lifecycle/dispatch work.
373	pub fn set_state_initial(&self, state: Vec<u8>) {
374		self.set_initial_state(state);
375	}
376
377	/// Fire-and-forget save request helper.
378	///
379	/// If the lifecycle event inbox is unavailable, this only logs a warning and
380	/// returns. That `warn!` is the sole failure signal for this path; callers do
381	/// not receive a `Result`. Call
382	/// [`Self::request_save_and_wait`] when the caller must observe
383	/// save-request delivery failures.
384	pub fn request_save(&self, opts: RequestSaveOpts) {
385		#[cfg(target_arch = "wasm32")]
386		{
387			self.request_save_best_effort(opts);
388		}
389
390		#[cfg(not(target_arch = "wasm32"))]
391		if let Err(error) = self.request_save_with_revision(opts) {
392			tracing::warn!(?error, "failed to request actor state save");
393		}
394	}
395
396	#[cfg(target_arch = "wasm32")]
397	fn request_save_best_effort(&self, opts: RequestSaveOpts) {
398		let immediate = opts.immediate;
399		let _save_request_revision =
400			self.0.save_request_revision.fetch_add(1, Ordering::SeqCst) + 1;
401		self.notify_request_save_hooks(opts);
402		let already_requested = self.0.save_requested.swap(true, Ordering::SeqCst);
403		let immediate_already_requested = if immediate {
404			self.0.save_requested_immediate.swap(true, Ordering::SeqCst)
405		} else {
406			self.0.save_requested_immediate.load(Ordering::SeqCst)
407		};
408
409		if let Some(max_wait_ms) = opts.max_wait_ms {
410			let deadline = StdInstant::now() + Duration::from_millis(u64::from(max_wait_ms));
411			let mut requested_deadline = self.0.save_requested_within_deadline.lock();
412			*requested_deadline = Some(match *requested_deadline {
413				Some(existing) => existing.min(deadline),
414				None => deadline,
415			});
416		}
417
418		let Some(sender) = self.lifecycle_event_sender() else {
419			return;
420		};
421
422		if opts.max_wait_ms.is_none()
423			&& already_requested
424			&& (!immediate || immediate_already_requested)
425		{
426			return;
427		}
428
429		let _ = sender.send(LifecycleEvent::SaveRequested { immediate });
430	}
431
432	pub async fn request_save_and_wait(&self, opts: RequestSaveOpts) -> Result<()> {
433		let save_request_revision = self.request_save_with_revision(opts)?;
434		self.wait_for_save_request(save_request_revision).await;
435		Ok(())
436	}
437
438	pub async fn save_state(&self, deltas: Vec<StateDelta>) -> Result<()> {
439		let save_request_revision = self.save_request_revision();
440		self.save_state_with_revision(deltas, save_request_revision)
441			.await
442	}
443
444	/// Requests one logical workflow-engine flush. The actor lifecycle owns
445	/// serializing actor state and committing both sides in one SQLite transaction.
446	pub async fn save_state_and_workflow_batch(
447		&self,
448		workflow_writes: Vec<WorkflowKvWrite>,
449	) -> Result<()> {
450		let Some(sender) = self.lifecycle_event_sender() else {
451			return Err(ActorRuntime::NotConfigured {
452				component: "lifecycle events".to_owned(),
453			}
454			.build());
455		};
456		let (reply_tx, reply_rx) = oneshot::channel();
457		sender
458			.send(LifecycleEvent::WorkflowFlushRequested {
459				writes: workflow_writes,
460				reply: Reply::from(reply_tx),
461			})
462			.map_err(|_| {
463				ActorRuntime::NotConfigured {
464					component: "lifecycle events".to_owned(),
465				}
466				.build()
467			})?;
468		reply_rx
469			.await
470			.context("receive workflow flush lifecycle reply")?
471	}
472
473	/// Commits an already serialized snapshot and workflow flush atomically for
474	/// storage-level fault tests. Runtime bridges must use the lifecycle-owned API.
475	#[cfg(test)]
476	pub(crate) async fn commit_serialized_state_and_workflow_batch(
477		&self,
478		deltas: Vec<StateDelta>,
479		workflow_writes: Vec<WorkflowKvWrite>,
480	) -> Result<()> {
481		let save_request_revision = self.save_request_revision();
482		self.save_state_and_workflow_batch_with_revision(
483			deltas,
484			workflow_writes,
485			save_request_revision,
486		)
487		.await
488	}
489
490	pub(crate) fn request_save_with_revision(&self, opts: RequestSaveOpts) -> Result<u64> {
491		let immediate = opts.immediate;
492		let save_request_revision = self.0.save_request_revision.fetch_add(1, Ordering::SeqCst) + 1;
493		self.notify_request_save_hooks(opts);
494		let already_requested = self.0.save_requested.swap(true, Ordering::SeqCst);
495		let immediate_already_requested = if immediate {
496			self.0.save_requested_immediate.swap(true, Ordering::SeqCst)
497		} else {
498			self.0.save_requested_immediate.load(Ordering::SeqCst)
499		};
500
501		if let Some(max_wait_ms) = opts.max_wait_ms {
502			let deadline = StdInstant::now() + Duration::from_millis(u64::from(max_wait_ms));
503			let mut requested_deadline = self.0.save_requested_within_deadline.lock();
504			*requested_deadline = Some(match *requested_deadline {
505				Some(existing) => existing.min(deadline),
506				None => deadline,
507			});
508		}
509
510		let Some(sender) = self.lifecycle_event_sender() else {
511			return Err(ActorRuntime::NotConfigured {
512				component: "lifecycle events".to_owned(),
513			}
514			.build());
515		};
516
517		if opts.max_wait_ms.is_none()
518			&& already_requested
519			&& (!immediate || immediate_already_requested)
520		{
521			return Ok(save_request_revision);
522		}
523
524		sender
525			.send(LifecycleEvent::SaveRequested { immediate })
526			.map(|()| save_request_revision)
527			.map_err(|_| {
528				ActorRuntime::NotConfigured {
529					component: "lifecycle events".to_owned(),
530				}
531				.build()
532			})
533	}
534
535	pub(crate) async fn wait_for_save_request(&self, save_request_revision: u64) {
536		loop {
537			if self.0.save_completed_revision.load(Ordering::SeqCst) >= save_request_revision {
538				return;
539			}
540
541			self.0.save_completion.notified().await;
542		}
543	}
544
545	pub(crate) fn save_requested(&self) -> bool {
546		self.0.save_requested.load(Ordering::SeqCst)
547	}
548
549	pub(crate) fn save_requested_immediate(&self) -> bool {
550		self.0.save_requested_immediate.load(Ordering::SeqCst)
551	}
552
553	pub(crate) fn save_deadline(&self, immediate: bool) -> StdInstant {
554		self.compute_save_deadline(immediate)
555	}
556
557	pub(crate) fn compute_save_deadline(&self, immediate: bool) -> StdInstant {
558		if immediate || self.save_requested_immediate() {
559			return StdInstant::now();
560		}
561
562		let throttled_deadline = StdInstant::now() + self.compute_save_delay(None);
563		let requested_deadline = *self.0.save_requested_within_deadline.lock();
564
565		match requested_deadline {
566			Some(requested_deadline) => throttled_deadline.min(requested_deadline),
567			None => throttled_deadline,
568		}
569	}
570
571	pub(crate) fn save_request_revision(&self) -> u64 {
572		self.0.save_request_revision.load(Ordering::SeqCst)
573	}
574
575	pub(crate) async fn apply_state_deltas(
576		&self,
577		deltas: Vec<StateDelta>,
578		save_request_revision: u64,
579	) -> Result<()> {
580		self.apply_state_deltas_inner(deltas, None, save_request_revision, None)
581			.await
582			.map(|_| ())
583	}
584
585	pub(super) async fn apply_state_deltas_inner(
586		&self,
587		deltas: Vec<StateDelta>,
588		workflow_writes: Option<Vec<WorkflowKvWrite>>,
589		save_request_revision: u64,
590		expected_state_transaction_epoch: Option<u64>,
591	) -> Result<bool> {
592		let delta_count = deltas.len();
593		let delta_bytes: usize = deltas.iter().map(StateDelta::payload_len).sum();
594		let workflow_write_count = workflow_writes.as_ref().map_or(0, Vec::len);
595		let current_revision = self.0.state_revision.load(Ordering::SeqCst);
596		tracing::debug!(
597			delta_count,
598			delta_bytes,
599			state_revision = current_revision,
600			save_request_revision,
601			"applying actor state deltas"
602		);
603		self.clear_pending_save();
604
605		if deltas.is_empty() && workflow_write_count == 0 {
606			self.mark_save_request_completed(save_request_revision);
607			self.finish_save_request(save_request_revision);
608			tracing::debug!(
609				delta_count,
610				state_revision = current_revision,
611				save_request_revision,
612				"actor state deltas applied without kv write"
613			);
614			return Ok(true);
615		}
616
617		let prepared = {
618			let _save_guard = self.0.save_guard.lock().await;
619			if expected_state_transaction_epoch.is_some_and(|expected| {
620				self.0.state_transaction_epoch.load(Ordering::SeqCst) != expected
621			}) {
622				None
623			} else {
624				let revision = self.0.state_revision.load(Ordering::SeqCst);
625				let mut persisted = self.persisted();
626				let mut next_state = None;
627				let mut actor_to_persist = None;
628				let mut connections_to_persist: Vec<PersistedConnection> = Vec::new();
629				let mut connections_to_delete = Vec::new();
630
631				for delta in deltas {
632					match delta {
633						StateDelta::ActorState(bytes) => {
634							next_state = Some(bytes.clone());
635							persisted.state = bytes;
636						}
637						StateDelta::ConnHibernation { conn: _, bytes } => {
638							connections_to_persist.push(
639								decode_persisted_connection(&bytes)
640									.context("decode hibernatable connection state delta")?,
641							);
642						}
643						StateDelta::ConnHibernationRemoved(conn) => {
644							connections_to_delete.push(conn);
645						}
646					}
647				}
648
649				if next_state.is_some() {
650					actor_to_persist = Some(persisted.clone());
651				}
652
653				Some((
654					next_state,
655					actor_to_persist,
656					connections_to_persist,
657					connections_to_delete,
658					revision,
659					self.begin_write(),
660				))
661			}
662		};
663		let Some((
664			next_state,
665			actor_to_persist,
666			connections_to_persist,
667			connections_to_delete,
668			revision,
669			_write_guard,
670		)) = prepared
671		else {
672			tracing::debug!(
673				save_request_revision,
674				"discarding state serialized across a state transaction boundary"
675			);
676			return Ok(false);
677		};
678
679		if let Some(workflow_writes) = workflow_writes.as_deref() {
680			internal_storage::persist_actor_core_connections_and_workflow(
681				self.sql(),
682				actor_to_persist.as_ref(),
683				&connections_to_persist,
684				&connections_to_delete,
685				workflow_writes,
686			)
687			.await
688			.context("atomically persist actor state, connection deltas, and workflow kv")?;
689		} else if actor_to_persist.is_some()
690			|| !connections_to_persist.is_empty()
691			|| !connections_to_delete.is_empty()
692		{
693			internal_storage::persist_actor_core_and_connections(
694				self.sql(),
695				actor_to_persist.as_ref(),
696				&connections_to_persist,
697				&connections_to_delete,
698			)
699			.await
700			.context("persist actor state and connection deltas to sqlite")?;
701		}
702
703		if let Some(state) = next_state {
704			self.0.persisted.write().state = state.clone();
705			*self.0.current_state.write() = state;
706		}
707		for connection in &connections_to_persist {
708			if let Some(handle) = self.connection(&connection.id) {
709				handle.set_state_initial(connection.state.clone());
710			}
711		}
712
713		*self.0.last_save_at.lock() = Some(StdInstant::now());
714
715		if self.0.state_revision.load(Ordering::SeqCst) == revision {
716			self.0.state_dirty.store(false, Ordering::SeqCst);
717		}
718
719		self.mark_save_request_completed(save_request_revision);
720		self.finish_save_request(save_request_revision);
721		tracing::debug!(
722			delta_count,
723			delta_bytes,
724			workflow_write_count,
725			state_revision = self.0.state_revision.load(Ordering::SeqCst),
726			save_request_revision,
727			"actor state deltas applied"
728		);
729		Ok(true)
730	}
731	async fn commit_state_transaction(
732		&self,
733		transaction: &SqliteTransaction,
734		deltas: Vec<StateDelta>,
735		save_request_revision: u64,
736		statement_count: usize,
737		bind_payload_bytes: usize,
738	) -> Result<()> {
739		let (deltas, pending_hibernation_changes) = match self.prepare_state_deltas(deltas) {
740			Ok(prepared) => prepared,
741			Err(error) => {
742				let _ = transaction.rollback().await;
743				return Err(error);
744			}
745		};
746		let commit_result = async {
747			let revision = self.0.state_revision.load(Ordering::SeqCst);
748			let mut persisted = self.persisted();
749			let mut next_state = None;
750			let mut actor_to_persist = None;
751			let mut connections_to_persist: Vec<PersistedConnection> = Vec::new();
752			let mut connections_to_delete = Vec::new();
753
754			for delta in deltas {
755				match delta {
756					StateDelta::ActorState(bytes) => {
757						next_state = Some(bytes.clone());
758						persisted.state = bytes;
759					}
760					StateDelta::ConnHibernation { conn: _, bytes } => {
761						connections_to_persist.push(
762							decode_persisted_connection(&bytes)
763								.context("decode hibernatable connection state delta")?,
764						);
765					}
766					StateDelta::ConnHibernationRemoved(conn) => {
767						connections_to_delete.push(conn);
768					}
769				}
770			}
771
772			if next_state.is_some() {
773				actor_to_persist = Some(persisted);
774			}
775			let statements = internal_storage::build_actor_core_and_connection_statements(
776				actor_to_persist.as_ref(),
777				&connections_to_persist,
778				&connections_to_delete,
779			)?;
780			internal_storage::validate_atomic_state_transaction_budget(
781				statement_count.saturating_add(statements.len()),
782				bind_payload_bytes
783					.saturating_add(internal_storage::statement_bind_payload_len(&statements)),
784			)?;
785			for statement in statements {
786				transaction
787					.execute(statement.sql, statement.params)
788					.await
789					.context("persist actor state inside sqlite transaction")?;
790			}
791			transaction
792				.commit()
793				.await
794				.context("commit sqlite transaction with actor state")?;
795
796			if let Some(state) = next_state {
797				self.0.persisted.write().state = state.clone();
798				*self.0.current_state.write() = state;
799			}
800			for connection in &connections_to_persist {
801				if let Some(handle) = self.connection(&connection.id) {
802					handle.set_state_initial(connection.state.clone());
803				}
804			}
805			*self.0.last_save_at.lock() = Some(StdInstant::now());
806			if self.0.state_revision.load(Ordering::SeqCst) == revision {
807				self.0.state_dirty.store(false, Ordering::SeqCst);
808			}
809			self.mark_save_request_completed(save_request_revision);
810			self.finish_save_request(save_request_revision);
811			self.record_state_updated();
812			Ok(())
813		}
814		.await;
815
816		if let Err(error) = commit_result {
817			self.restore_pending_hibernation_changes(pending_hibernation_changes);
818			let _ = transaction.rollback().await;
819			return Err(error);
820		}
821		Ok(())
822	}
823
824	pub(crate) async fn wait_for_pending_writes(&self) {
825		loop {
826			if let Some(handle) = self.take_tracked_persist() {
827				let _ = handle.await;
828				continue;
829			}
830
831			let save_guard = self.0.save_guard.lock().await;
832			if self.has_tracked_persist() {
833				drop(save_guard);
834				continue;
835			}
836
837			if self.0.in_flight_state_writes.load(Ordering::SeqCst) == 0 {
838				return;
839			}
840			drop(save_guard);
841
842			self.wait_for_in_flight_writes().await;
843		}
844	}
845
846	pub(crate) async fn wait_for_pending_state_writes(&self) {
847		self.wait_for_pending_writes().await;
848	}
849
850	pub fn begin_on_state_change(&self) -> OnStateChangeGuard {
851		OnStateChangeGuard::new(self.clone())
852	}
853
854	pub fn on_state_change_started(&self) {
855		self.0
856			.on_state_change_in_flight
857			.fetch_add(1, Ordering::SeqCst);
858		self.0.sleep.work.keep_awake.increment();
859		self.reset_sleep_timer();
860	}
861
862	pub fn on_state_change_finished(&self) {
863		let previous = self.0.on_state_change_in_flight.fetch_update(
864			Ordering::SeqCst,
865			Ordering::SeqCst,
866			|count| count.checked_sub(1),
867		);
868
869		match previous {
870			Ok(1) => {
871				self.0.sleep.work.keep_awake.decrement();
872				self.0.on_state_change_idle.notify_waiters();
873				self.reset_sleep_timer();
874			}
875			Ok(_) => {
876				self.0.sleep.work.keep_awake.decrement();
877				self.reset_sleep_timer();
878			}
879			Err(_) => {
880				tracing::warn!(
881					actor_id = %self.actor_id(),
882					"on_state_change finished without a matching start"
883				);
884			}
885		}
886	}
887
888	#[cfg(test)]
889	#[allow(dead_code)]
890	pub(crate) async fn wait_for_on_state_change_idle(&self, timeout_duration: Duration) -> bool {
891		if self.0.on_state_change_in_flight.load(Ordering::SeqCst) == 0 {
892			return true;
893		}
894
895		timeout(timeout_duration, async {
896			loop {
897				let idle = self.0.on_state_change_idle.notified();
898				tokio::pin!(idle);
899				idle.as_mut().enable();
900
901				if self.0.on_state_change_in_flight.load(Ordering::SeqCst) == 0 {
902					return;
903				}
904
905				idle.await;
906			}
907		})
908		.await
909		.is_ok()
910	}
911
912	pub fn persisted(&self) -> PersistedActor {
913		self.0.persisted.read().clone()
914	}
915
916	pub fn load_persisted(&self, persisted: PersistedActor) {
917		let state = persisted.state.clone();
918		*self.0.persisted.write() = persisted;
919		*self.0.current_state.write() = state;
920		self.0.state_dirty.store(false, Ordering::SeqCst);
921		self.finish_save_request(self.save_request_revision());
922		self.0
923			.metrics
924			.inc_state_mutation(StateMutationReason::InternalReplace);
925	}
926
927	pub(crate) fn load_last_pushed_alarm(&self, alarm_ts: Option<i64>) {
928		*self.0.last_pushed_alarm.write() = alarm_ts;
929	}
930
931	pub(crate) fn last_pushed_alarm(&self) -> Option<i64> {
932		*self.0.last_pushed_alarm.read()
933	}
934
935	pub(crate) async fn persist_last_pushed_alarm(&self, alarm_ts: Option<i64>) -> Result<()> {
936		internal_storage::persist_last_pushed_alarm(self.sql(), alarm_ts)
937			.await
938			.context("persist last pushed alarm to sqlite")?;
939		self.load_last_pushed_alarm(alarm_ts);
940		Ok(())
941	}
942
943	pub(crate) fn load_run_wake_at(&self, wake_at: Option<i64>) {
944		*self.0.run_wake_at.write() = wake_at;
945	}
946
947	pub(crate) fn run_wake_at(&self) -> Option<i64> {
948		*self.0.run_wake_at.read()
949	}
950
951	pub(crate) async fn persist_run_wake_at(&self, wake_at: Option<i64>) -> Result<()> {
952		internal_storage::persist_run_wake_at(self.sql(), wake_at)
953			.await
954			.context("persist run wake deadline to sqlite")?;
955		self.load_run_wake_at(wake_at);
956		Ok(())
957	}
958
959	pub(crate) fn set_initial_state(&self, state: Vec<u8>) {
960		*self.0.current_state.write() = state.clone();
961		self.0.persisted.write().state = state;
962		self.0.state_dirty.store(true, Ordering::SeqCst);
963		self.0.state_revision.fetch_add(1, Ordering::SeqCst);
964	}
965
966	pub fn scheduled_events(&self) -> Vec<PersistedScheduleEvent> {
967		self.0.persisted.read().scheduled_events.clone()
968	}
969
970	pub fn set_scheduled_events(&self, scheduled_events: Vec<PersistedScheduleEvent>) {
971		self.0.persisted.write().scheduled_events = scheduled_events;
972		self.0
973			.metrics
974			.inc_state_mutation(StateMutationReason::ScheduledEventsUpdate);
975		self.mark_dirty();
976		self.schedule_save(None);
977	}
978
979	pub fn set_input(&self, input: Option<Vec<u8>>) {
980		self.0.persisted.write().input = input;
981		self.0
982			.metrics
983			.inc_state_mutation(StateMutationReason::InputSet);
984		self.mark_dirty();
985		self.schedule_save(None);
986	}
987
988	pub fn input(&self) -> Option<Vec<u8>> {
989		self.0.persisted.read().input.clone()
990	}
991
992	pub fn set_has_initialized(&self, has_initialized: bool) {
993		{
994			let mut persisted = self.0.persisted.write();
995			if persisted.has_initialized == has_initialized {
996				return;
997			}
998			persisted.has_initialized = has_initialized;
999		}
1000		self.0
1001			.metrics
1002			.inc_state_mutation(StateMutationReason::HasInitialized);
1003		self.mark_dirty();
1004		self.schedule_save(None);
1005	}
1006
1007	pub fn has_initialized(&self) -> bool {
1008		self.0.persisted.read().has_initialized
1009	}
1010
1011	pub fn flush_on_shutdown(&self) {
1012		self.persist_now_tracked("shutdown_flush");
1013	}
1014
1015	pub fn on_request_save(&self, hook: Box<dyn Fn(RequestSaveOpts) + Send + Sync>) {
1016		self.0.request_save_hooks.write().push(Arc::from(hook));
1017	}
1018
1019	fn is_dirty(&self) -> bool {
1020		self.0.state_dirty.load(Ordering::SeqCst)
1021	}
1022
1023	fn mark_dirty(&self) {
1024		self.0.state_dirty.store(true, Ordering::SeqCst);
1025		self.0.state_revision.fetch_add(1, Ordering::SeqCst);
1026	}
1027
1028	fn lifecycle_event_sender(&self) -> Option<mpsc::UnboundedSender<LifecycleEvent>> {
1029		self.0.lifecycle_events.read().clone()
1030	}
1031
1032	fn compute_save_delay(&self, max_wait: Option<Duration>) -> Duration {
1033		let elapsed = self
1034			.0
1035			.last_save_at
1036			.lock()
1037			.map(|instant| instant.elapsed())
1038			.unwrap_or_default();
1039
1040		throttled_save_delay(self.0.state_save_interval, elapsed, max_wait)
1041	}
1042
1043	fn schedule_save(&self, max_wait: Option<Duration>) {
1044		if !self.is_dirty() {
1045			return;
1046		}
1047
1048		let delay = self.compute_save_delay(max_wait);
1049		let scheduled_at = StdInstant::now() + delay;
1050
1051		let mut pending_save = self.0.pending_save.lock();
1052
1053		if let Some(existing) = pending_save.as_ref() {
1054			if existing.scheduled_at <= scheduled_at {
1055				return;
1056			}
1057
1058			existing.handle.abort();
1059		}
1060
1061		let state = self.clone();
1062		// Intentionally detached but abortable: pending delayed saves are
1063		// retained in `pending_save`, replaced by newer saves, and awaited at
1064		// shutdown through the state save guard.
1065		let task = async move {
1066			if !delay.is_zero() {
1067				sleep(delay).await;
1068			}
1069
1070			state.take_pending_save();
1071
1072			if let Err(error) = state.persist_if_dirty().await {
1073				tracing::error!(?error, "failed to persist actor state");
1074			}
1075		}
1076		.in_current_span();
1077
1078		#[cfg(not(feature = "wasm-runtime"))]
1079		let handle = {
1080			let Ok(tokio_handle) = Handle::try_current() else {
1081				return;
1082			};
1083			tokio_handle.spawn(task)
1084		};
1085
1086		#[cfg(feature = "wasm-runtime")]
1087		let handle = RuntimeSpawner::spawn(task);
1088
1089		*pending_save = Some(PendingSave {
1090			scheduled_at,
1091			handle,
1092		});
1093	}
1094
1095	fn clear_pending_save(&self) {
1096		if let Some(pending_save) = self.take_pending_save() {
1097			pending_save.handle.abort();
1098		}
1099	}
1100
1101	pub(crate) fn persist_now_tracked(&self, description: &'static str) {
1102		self.clear_pending_save();
1103
1104		let state = self.clone();
1105		let mut tracked_persist = self.0.tracked_persist.lock();
1106		let previous = tracked_persist.take();
1107		let task = async move {
1108			if let Some(previous) = previous {
1109				let _ = previous.await;
1110			}
1111
1112			if let Err(error) = state.persist_state(SaveStateOpts { immediate: true }).await {
1113				tracing::error!(?error, description, "failed to persist actor state");
1114			}
1115		}
1116		.in_current_span();
1117
1118		#[cfg(not(feature = "wasm-runtime"))]
1119		let handle = {
1120			let Ok(tokio_handle) = Handle::try_current() else {
1121				tracing::warn!(
1122					description,
1123					"skipping tracked actor state persistence without runtime"
1124				);
1125				return;
1126			};
1127			tokio_handle.spawn(task)
1128		};
1129
1130		#[cfg(feature = "wasm-runtime")]
1131		let handle = RuntimeSpawner::spawn(task);
1132
1133		*tracked_persist = Some(handle);
1134	}
1135
1136	fn take_pending_save(&self) -> Option<PendingSave> {
1137		self.0.pending_save.lock().take()
1138	}
1139
1140	fn take_tracked_persist(&self) -> Option<JoinHandle<()>> {
1141		self.0.tracked_persist.lock().take()
1142	}
1143
1144	fn has_tracked_persist(&self) -> bool {
1145		self.0.tracked_persist.lock().is_some()
1146	}
1147
1148	#[cfg(test)]
1149	pub(crate) fn tracked_persist_pending(&self) -> bool {
1150		self.has_tracked_persist()
1151	}
1152
1153	async fn persist_if_dirty(&self) -> Result<()> {
1154		if !self.is_dirty() {
1155			return Ok(());
1156		}
1157
1158		let (revision, actor_to_persist, _write_guard) = {
1159			let _save_guard = self.0.save_guard.lock().await;
1160			if !self.is_dirty() {
1161				return Ok(());
1162			}
1163
1164			let revision = self.0.state_revision.load(Ordering::SeqCst);
1165			let persisted = self.persisted();
1166			(revision, persisted, self.begin_write())
1167		};
1168
1169		internal_storage::persist_actor_snapshot(self.sql(), &actor_to_persist)
1170			.await
1171			.context("persist actor state to sqlite")?;
1172
1173		*self.0.last_save_at.lock() = Some(StdInstant::now());
1174
1175		if self.0.state_revision.load(Ordering::SeqCst) == revision {
1176			self.0.state_dirty.store(false, Ordering::SeqCst);
1177		}
1178
1179		Ok(())
1180	}
1181
1182	fn begin_write(&self) -> InFlightWrite {
1183		self.0.in_flight_state_writes.fetch_add(1, Ordering::SeqCst);
1184		InFlightWrite { ctx: self.clone() }
1185	}
1186
1187	async fn wait_for_in_flight_writes(&self) {
1188		loop {
1189			if self.0.in_flight_state_writes.load(Ordering::SeqCst) == 0 {
1190				return;
1191			}
1192			self.0.state_write_completion.notified().await;
1193		}
1194	}
1195
1196	fn finish_save_request(&self, save_request_revision: u64) {
1197		if self.0.save_request_revision.load(Ordering::SeqCst) == save_request_revision {
1198			self.0.save_requested.store(false, Ordering::SeqCst);
1199			self.0
1200				.save_requested_immediate
1201				.store(false, Ordering::SeqCst);
1202			*self.0.save_requested_within_deadline.lock() = None;
1203		}
1204	}
1205
1206	fn mark_save_request_completed(&self, save_request_revision: u64) {
1207		self.0
1208			.save_completed_revision
1209			.fetch_max(save_request_revision, Ordering::SeqCst);
1210		self.0.save_completion.notify_waiters();
1211	}
1212
1213	fn notify_request_save_hooks(&self, opts: RequestSaveOpts) {
1214		let hooks = self.0.request_save_hooks.read().clone();
1215		for hook in hooks {
1216			hook(opts);
1217		}
1218	}
1219}
1220
1221struct InFlightWrite {
1222	ctx: ActorContext,
1223}
1224
1225impl Drop for InFlightWrite {
1226	fn drop(&mut self) {
1227		if self
1228			.ctx
1229			.0
1230			.in_flight_state_writes
1231			.fetch_sub(1, Ordering::SeqCst)
1232			== 1
1233		{
1234			self.ctx.0.state_write_completion.notify_waiters();
1235			self.ctx.0.state_write_completion.notify_one();
1236		}
1237	}
1238}
1239
1240fn throttled_save_delay(
1241	save_interval: Duration,
1242	time_since_last_save: Duration,
1243	max_wait: Option<Duration>,
1244) -> Duration {
1245	let save_delay = save_interval.saturating_sub(time_since_last_save);
1246	if let Some(max_wait) = max_wait {
1247		save_delay.min(max_wait)
1248	} else {
1249		save_delay
1250	}
1251}
1252
1253// Test shim keeps moved tests in crate-root tests/ with private-module access.
1254#[cfg(test)]
1255#[path = "../../tests/state.rs"]
1256mod tests;