Skip to main content

rivetkit_core/actor/
state.rs

1use std::sync::atomic::Ordering;
2use std::sync::{Arc, Weak};
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, Notify, OwnedMutexGuard, mpsc, oneshot};
13use tokio::task::JoinHandle;
14
15use tracing::Instrument;
16
17use crate::actor::connection::{PersistedConnection, decode_persisted_connection};
18use crate::actor::context::ActorContext;
19use crate::actor::internal_storage;
20use crate::actor::lifecycle_hooks::Reply;
21use crate::actor::messages::{StateDelta, WorkflowKvWrite};
22use crate::actor::persist::decode_latest_with_embedded_version;
23
24use crate::actor::task::LifecycleEvent;
25use crate::actor::task_types::StateMutationReason;
26use crate::error::{ActorLifecycle, ActorRuntime};
27use crate::runtime::RuntimeSpawner;
28use crate::sqlite::{BindParam, ExecuteResult, SqliteTransaction};
29use crate::types::SaveStateOpts;
30
31
32
33pub type PersistedScheduleEvent = persist_v4::ScheduleEvent;
34pub type PersistedActor = persist_v4::Actor;
35
36
37
38pub(crate) fn decode_persisted_actor(payload: &[u8]) -> Result<PersistedActor> {
39	let actor = decode_latest_with_embedded_version::<persist_versioned::Actor>(
40		payload,
41		"persisted actor",
42	)?;
43	Ok(actor)
44}
45
46
47
48pub(crate) fn decode_last_pushed_alarm(payload: &[u8]) -> Result<Option<i64>> {
49	decode_latest_with_embedded_version::<persist_versioned::LastPushedAlarm>(
50		payload,
51		"last pushed alarm",
52	)
53}
54
55#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
56pub struct RequestSaveOpts {
57	pub immediate: bool,
58	pub max_wait_ms: Option<u32>,
59}
60
61pub(super) struct PendingSave {
62	scheduled_at: StdInstant,
63	handle: JoinHandle<()>,
64}
65
66pub struct OnStateChangeGuard {
67	ctx: Option<ActorContext>,
68}
69/// A SQLite transaction that owns the actor state-save exclusion until it is
70/// committed or rolled back.
71#[derive(Clone)]
72pub struct ActorStateTransaction {
73	owner: Arc<AsyncMutex<ActorStateTransactionOwner>>,
74}
75
76/// Which state transaction, if any, holds the actor's `save_guard`.
77///
78/// The actor task reads this to defer saves and workflow flushes instead of
79/// waiting on a lock that user code owns. Every transition except `Closed`
80/// happens while the guard is held, so a later transaction cannot be
81/// overwritten by an earlier one finishing.
82pub(super) enum StateTransactionSlot {
83	/// No state transaction holds `save_guard`.
84	Idle,
85	/// `begin_state_transaction` holds `save_guard` while it opens the SQLite
86	/// transaction. It moves to `Open` on success or back to `Idle` on failure
87	/// or cancellation.
88	Beginning,
89	/// An open transaction holds `save_guard` until it is committed, rolled back,
90	/// or dropped. Shutdown uses the weak owner to roll it back.
91	Open(Weak<AsyncMutex<ActorStateTransactionOwner>>),
92	/// Shutdown closed state transactions. This state is terminal: new
93	/// transactions are rejected, and a transaction that was beginning rolls
94	/// itself back instead of opening.
95	Closed,
96}
97
98/// Holds the actor's `save_guard` on behalf of one state transaction.
99///
100/// Dropping it returns the slot to `Idle` unless shutdown closed it, releases
101/// the guard, and wakes the actor task to resume saves and workflow flushes it
102/// deferred. This also runs when `begin_state_transaction` is cancelled, so a
103/// dropped begin never leaves the slot stuck in `Beginning`.
104struct StateSaveExclusion {
105	ctx: ActorContext,
106	save_guard: Option<OwnedMutexGuard<()>>,
107}
108
109impl Drop for StateSaveExclusion {
110	fn drop(&mut self) {
111		{
112			let mut slot = self.ctx.0.state_transaction.lock();
113			match *slot {
114				StateTransactionSlot::Closed => {}
115				StateTransactionSlot::Idle
116				| StateTransactionSlot::Beginning
117				| StateTransactionSlot::Open(_) => *slot = StateTransactionSlot::Idle,
118			}
119		}
120
121		// Release the guard before the wakeup so the actor task can take it.
122		self.save_guard.take();
123		self.ctx.notify_state_transaction_finished();
124	}
125}
126
127pub(super) struct ActorStateTransactionOwner {
128	ctx: ActorContext,
129	transaction: SqliteTransaction,
130	/// Released after the `Drop` body runs, so connection states are restored
131	/// and the epoch is advanced before a blocked save can take the guard.
132	save_exclusion: Option<StateSaveExclusion>,
133	write_guard: Option<InFlightWrite>,
134	finalized: bool,
135	committed: bool,
136	save_request_revision: u64,
137	statement_count: usize,
138	bind_payload_bytes: usize,
139	rollback_connection_states: Vec<(String, Vec<u8>)>,
140}
141
142impl ActorStateTransactionOwner {
143	fn restore_connection_states(&self) {
144		for (conn_id, state) in &self.rollback_connection_states {
145			if let Some(conn) = self.ctx.connection(conn_id) {
146				conn.set_state(state.clone());
147			}
148		}
149	}
150
151	fn advance_epoch(&self) {
152		self.ctx
153			.0
154			.state_transaction_epoch
155			.fetch_add(1, Ordering::SeqCst);
156	}
157}
158
159impl Drop for ActorStateTransactionOwner {
160	fn drop(&mut self) {
161		if !self.finalized {
162			self.restore_connection_states();
163			self.advance_epoch();
164			let transaction = self.transaction.clone();
165			#[cfg(not(feature = "wasm-runtime"))]
166			if let Ok(handle) = Handle::try_current() {
167				handle.spawn(async move {
168					if let Err(error) = transaction.rollback().await {
169						tracing::debug!(?error, "dropped actor state transaction rollback failed");
170					}
171				});
172			}
173			#[cfg(feature = "wasm-runtime")]
174			wasm_bindgen_futures::spawn_local(async move {
175				if let Err(error) = transaction.rollback().await {
176					tracing::debug!(?error, "dropped actor state transaction rollback failed");
177				}
178			});
179		}
180		if !self.committed {
181			self.ctx.schedule_save(None);
182		}
183	}
184}
185
186impl ActorStateTransaction {
187	pub async fn execute(
188		&self,
189		sql: impl Into<String>,
190		params: Option<Vec<BindParam>>,
191	) -> Result<ExecuteResult> {
192		let mut owner = self.owner.lock().await;
193		if owner.finalized {
194			return Err(anyhow::anyhow!(
195				"actor state transaction is already finalized"
196			));
197		}
198		let sql = sql.into();
199		if is_transaction_terminator(&sql) {
200			return Err(anyhow::anyhow!(
201				"cannot execute transaction terminator inside an actor state transaction; use the transaction commit or rollback method"
202			));
203		}
204		let payload_bytes = match params.as_deref() {
205			Some(params) => params
206				.iter()
207				.map(internal_storage::bind_param_payload_len)
208				.fold(0usize, usize::saturating_add),
209			None => sql.len(),
210		};
211		let result = owner.transaction.execute(sql, params).await?;
212		owner.statement_count = owner.statement_count.saturating_add(1);
213		owner.bind_payload_bytes = owner.bind_payload_bytes.saturating_add(payload_bytes);
214		Ok(result)
215	}
216
217	pub async fn commit(&self, deltas: Vec<StateDelta>) -> Result<()> {
218		let mut owner = self.owner.lock().await;
219		if owner.finalized {
220			return Err(anyhow::anyhow!(
221				"actor state transaction is already finalized"
222			));
223		}
224
225		let save_request_revision = owner.save_request_revision;
226		let transaction = owner.transaction.clone();
227		let statement_count = owner.statement_count;
228		let bind_payload_bytes = owner.bind_payload_bytes;
229		let result = owner
230			.ctx
231			.commit_state_transaction(
232				&transaction,
233				deltas,
234				save_request_revision,
235				statement_count,
236				bind_payload_bytes,
237			)
238			.await;
239		owner.finalized = true;
240		owner.committed = result.is_ok();
241		if result.is_err() {
242			owner.restore_connection_states();
243		}
244		owner.advance_epoch();
245		owner.write_guard.take();
246		owner.save_exclusion.take();
247		if result.is_err() {
248			owner.ctx.schedule_save(None);
249		}
250		result
251	}
252
253	pub async fn rollback(&self) -> Result<()> {
254		let mut owner = self.owner.lock().await;
255		if owner.finalized {
256			return Err(anyhow::anyhow!(
257				"actor state transaction is already finalized"
258			));
259		}
260		let result = owner.transaction.rollback().await;
261		owner.finalized = true;
262		owner.restore_connection_states();
263		owner.advance_epoch();
264		owner.write_guard.take();
265		owner.save_exclusion.take();
266		owner.ctx.schedule_save(None);
267		result
268	}
269}
270
271fn is_transaction_terminator(sql: &str) -> bool {
272	let mut offset = 0;
273	let Some(first) = next_sql_keyword(sql, &mut offset) else {
274		return false;
275	};
276	if first.eq_ignore_ascii_case("COMMIT") || first.eq_ignore_ascii_case("END") {
277		return true;
278	}
279	if !first.eq_ignore_ascii_case("ROLLBACK") {
280		return false;
281	}
282
283	let mut next = next_sql_keyword(sql, &mut offset);
284	if next.is_some_and(|keyword| keyword.eq_ignore_ascii_case("TRANSACTION")) {
285		next = next_sql_keyword(sql, &mut offset);
286	}
287	!next.is_some_and(|keyword| keyword.eq_ignore_ascii_case("TO"))
288}
289
290fn next_sql_keyword<'a>(sql: &'a str, offset: &mut usize) -> Option<&'a str> {
291	let bytes = sql.as_bytes();
292	loop {
293		while bytes.get(*offset).is_some_and(u8::is_ascii_whitespace) {
294			*offset += 1;
295		}
296		if bytes.get(*offset..*offset + 2) == Some(b"--") {
297			*offset += 2;
298			while bytes.get(*offset).is_some_and(|byte| *byte != b'\n') {
299				*offset += 1;
300			}
301			continue;
302		}
303		if bytes.get(*offset..*offset + 2) == Some(b"/*") {
304			*offset += 2;
305			while bytes.get(*offset..*offset + 2) != Some(b"*/") {
306				bytes.get(*offset)?;
307				*offset += 1;
308			}
309			*offset += 2;
310			continue;
311		}
312		break;
313	}
314
315	let start = *offset;
316	while bytes.get(*offset).is_some_and(u8::is_ascii_alphabetic) {
317		*offset += 1;
318	}
319	(start != *offset).then(|| &sql[start..*offset])
320}
321
322impl OnStateChangeGuard {
323	fn new(ctx: ActorContext) -> Self {
324		ctx.on_state_change_started();
325		Self { ctx: Some(ctx) }
326	}
327}
328
329impl Drop for OnStateChangeGuard {
330	fn drop(&mut self) {
331		if let Some(ctx) = self.ctx.take() {
332			ctx.on_state_change_finished();
333		}
334	}
335}
336
337impl ActorContext {
338	pub async fn begin_state_transaction(
339		&self,
340		timeout: Option<Duration>,
341	) -> Result<ActorStateTransaction> {
342		self.clear_pending_save();
343		let save_guard = Arc::clone(&self.0.save_guard).lock_owned().await;
344		{
345			// Holding the guard means no other transaction occupies the slot.
346			let mut slot = self.0.state_transaction.lock();
347			match *slot {
348				StateTransactionSlot::Closed => {
349					return Err(ActorLifecycle::Stopping.build());
350				}
351				StateTransactionSlot::Idle
352				| StateTransactionSlot::Beginning
353				| StateTransactionSlot::Open(_) => *slot = StateTransactionSlot::Beginning,
354			}
355		}
356		let save_exclusion = StateSaveExclusion {
357			ctx: self.clone(),
358			save_guard: Some(save_guard),
359		};
360		self.0
361			.state_transaction_epoch
362			.fetch_add(1, Ordering::SeqCst);
363		self.wait_for_in_flight_writes().await;
364		let rollback_connection_states = self
365			.iter_connections()
366			.filter(|conn| conn.is_hibernatable())
367			.map(|conn| (conn.id().to_owned(), conn.state()))
368			.collect();
369		let sql = self.invocation_sql();
370		let transaction = match sql.begin_transaction(timeout).await {
371			Ok(transaction) => transaction,
372			Err(error) => {
373				self.0
374					.state_transaction_epoch
375					.fetch_add(1, Ordering::SeqCst);
376				drop(save_exclusion);
377				self.schedule_save(None);
378				return Err(error);
379			}
380		};
381		let write_guard = self.begin_write();
382		let save_request_revision = self.save_request_revision();
383		let owner = Arc::new(AsyncMutex::new(ActorStateTransactionOwner {
384			ctx: self.clone(),
385			transaction,
386			save_exclusion: Some(save_exclusion),
387			write_guard: Some(write_guard),
388			finalized: false,
389			committed: false,
390			save_request_revision,
391			statement_count: 0,
392			bind_payload_bytes: 0,
393			rollback_connection_states,
394		}));
395
396		// Shutdown may have closed state transactions while this one was
397		// beginning. It cannot see a transaction that is not open yet, so the
398		// transaction rolls itself back instead of opening.
399		let opened = {
400			let mut slot = self.0.state_transaction.lock();
401			match *slot {
402				StateTransactionSlot::Closed => false,
403				StateTransactionSlot::Idle
404				| StateTransactionSlot::Beginning
405				| StateTransactionSlot::Open(_) => {
406					*slot = StateTransactionSlot::Open(Arc::downgrade(&owner));
407					true
408				}
409			}
410		};
411		let transaction = ActorStateTransaction { owner };
412		if !opened {
413			if let Err(error) = transaction.rollback().await {
414				tracing::debug!(?error, "state transaction rollback at shutdown failed");
415			}
416			return Err(ActorLifecycle::Stopping.build());
417		}
418		Ok(transaction)
419	}
420
421	/// True while a state transaction holds the state-save exclusion.
422	pub(crate) fn state_transaction_active(&self) -> bool {
423		match *self.0.state_transaction.lock() {
424			StateTransactionSlot::Idle | StateTransactionSlot::Closed => false,
425			StateTransactionSlot::Beginning | StateTransactionSlot::Open(_) => true,
426		}
427	}
428
429	/// Closes state transactions for the rest of this actor instance so the
430	/// final save does not wait on user code that never commits.
431	///
432	/// An open transaction is rolled back here, a transaction that is still
433	/// beginning rolls itself back instead of opening, and later begins are
434	/// rejected. A transaction already committing finishes first, and its
435	/// rollback is then a no-op.
436	pub(crate) async fn close_state_transactions(&self) {
437		let previous = std::mem::replace(
438			&mut *self.0.state_transaction.lock(),
439			StateTransactionSlot::Closed,
440		);
441		let owner = match previous {
442			StateTransactionSlot::Open(owner) => owner.upgrade(),
443			StateTransactionSlot::Idle
444			| StateTransactionSlot::Beginning
445			| StateTransactionSlot::Closed => None,
446		};
447		let Some(owner) = owner else {
448			return;
449		};
450
451		tracing::warn!(
452			actor_id = %self.actor_id(),
453			"rolling back state transaction left open at shutdown; the action did not finish within the grace period"
454		);
455		if let Err(error) = (ActorStateTransaction { owner }).rollback().await {
456			tracing::debug!(
457				?error,
458				"state transaction finalized before shutdown rollback"
459			);
460		}
461	}
462
463	/// Wakes the actor task after a state transaction releases `save_guard`.
464	fn notify_state_transaction_finished(&self) {
465		if let Some(sender) = self.lifecycle_event_sender() {
466			let _ = sender.send(LifecycleEvent::StateTransactionFinished);
467		}
468	}
469
470	pub fn state(&self) -> Vec<u8> {
471		self.0.current_state.read().clone()
472	}
473
474	pub(crate) async fn persist_state(&self, opts: SaveStateOpts) -> Result<()> {
475		if !self.is_dirty() {
476			return Ok(());
477		}
478
479		let result = if opts.immediate {
480			self.clear_pending_save();
481			self.persist_if_dirty().await
482		} else {
483			let delay = self.compute_save_delay(None);
484			if !delay.is_zero() {
485				sleep(delay).await;
486			}
487			self.persist_if_dirty().await
488		};
489		result?;
490		self.record_state_updated();
491		Ok(())
492	}
493
494	/// Foreign-runtime bootstrap hook for installing the actor state snapshot
495	/// before the actor starts handling lifecycle/dispatch work.
496	pub fn set_state_initial(&self, state: Vec<u8>) {
497		self.set_initial_state(state);
498	}
499
500	/// Fire-and-forget save request helper.
501	///
502	/// If the lifecycle event inbox is unavailable, this only logs a warning and
503	/// returns. That `warn!` is the sole failure signal for this path; callers do
504	/// not receive a `Result`. Call
505	/// [`Self::request_save_and_wait`] when the caller must observe
506	/// save-request delivery failures.
507	pub fn request_save(&self, opts: RequestSaveOpts) {
508		#[cfg(target_arch = "wasm32")]
509		{
510			self.request_save_best_effort(opts);
511		}
512
513		#[cfg(not(target_arch = "wasm32"))]
514		if let Err(error) = self.request_save_with_revision(opts) {
515			tracing::warn!(?error, "failed to request actor state save");
516		}
517	}
518
519	#[cfg(target_arch = "wasm32")]
520	fn request_save_best_effort(&self, opts: RequestSaveOpts) {
521		let immediate = opts.immediate;
522		let _save_request_revision =
523			self.0.save_request_revision.fetch_add(1, Ordering::SeqCst) + 1;
524		self.notify_request_save_hooks(opts);
525		let already_requested = self.0.save_requested.swap(true, Ordering::SeqCst);
526		let immediate_already_requested = if immediate {
527			self.0.save_requested_immediate.swap(true, Ordering::SeqCst)
528		} else {
529			self.0.save_requested_immediate.load(Ordering::SeqCst)
530		};
531
532		if let Some(max_wait_ms) = opts.max_wait_ms {
533			let deadline = StdInstant::now() + Duration::from_millis(u64::from(max_wait_ms));
534			let mut requested_deadline = self.0.save_requested_within_deadline.lock();
535			*requested_deadline = Some(match *requested_deadline {
536				Some(existing) => existing.min(deadline),
537				None => deadline,
538			});
539		}
540
541		let Some(sender) = self.lifecycle_event_sender() else {
542			return;
543		};
544
545		if opts.max_wait_ms.is_none()
546			&& already_requested
547			&& (!immediate || immediate_already_requested)
548		{
549			return;
550		}
551
552		let _ = sender.send(LifecycleEvent::SaveRequested { immediate });
553	}
554
555	/// Requests a save and resolves once the state current at the request has been written.
556	/// Fails with `actor/stopping` when the actor shuts down without writing it.
557	pub async fn request_save_and_wait(&self, opts: RequestSaveOpts) -> Result<()> {
558		let save_request_revision = self.request_save_with_revision(opts)?;
559		self.wait_for_save_request(save_request_revision).await
560	}
561
562	pub async fn save_state(&self, deltas: Vec<StateDelta>) -> Result<()> {
563		let save_request_revision = self.save_request_revision();
564		self.save_state_with_revision(deltas, save_request_revision)
565			.await
566	}
567
568	/// Requests one logical workflow-engine flush. The actor lifecycle owns
569	/// serializing actor state and committing both sides in one SQLite transaction.
570	pub async fn save_state_and_workflow_batch(
571		&self,
572		workflow_writes: Vec<WorkflowKvWrite>,
573	) -> Result<()> {
574		let Some(sender) = self.lifecycle_event_sender() else {
575			return Err(ActorRuntime::NotConfigured {
576				component: "lifecycle events".to_owned(),
577			}
578			.build());
579		};
580		let (reply_tx, reply_rx) = oneshot::channel();
581		sender
582			.send(LifecycleEvent::WorkflowFlushRequested {
583				writes: workflow_writes,
584				reply: Reply::from(reply_tx),
585			})
586			.map_err(|_| {
587				ActorRuntime::NotConfigured {
588					component: "lifecycle events".to_owned(),
589				}
590				.build()
591			})?;
592		reply_rx
593			.await
594			.context("receive workflow flush lifecycle reply")?
595	}
596
597	
598
599	pub(crate) fn request_save_with_revision(&self, opts: RequestSaveOpts) -> Result<u64> {
600		let immediate = opts.immediate;
601		let save_request_revision = self.0.save_request_revision.fetch_add(1, Ordering::SeqCst) + 1;
602		self.notify_request_save_hooks(opts);
603		let already_requested = self.0.save_requested.swap(true, Ordering::SeqCst);
604		let immediate_already_requested = if immediate {
605			self.0.save_requested_immediate.swap(true, Ordering::SeqCst)
606		} else {
607			self.0.save_requested_immediate.load(Ordering::SeqCst)
608		};
609
610		if let Some(max_wait_ms) = opts.max_wait_ms {
611			let deadline = StdInstant::now() + Duration::from_millis(u64::from(max_wait_ms));
612			let mut requested_deadline = self.0.save_requested_within_deadline.lock();
613			*requested_deadline = Some(match *requested_deadline {
614				Some(existing) => existing.min(deadline),
615				None => deadline,
616			});
617		}
618
619		let Some(sender) = self.lifecycle_event_sender() else {
620			return Err(ActorRuntime::NotConfigured {
621				component: "lifecycle events".to_owned(),
622			}
623			.build());
624		};
625
626		if opts.max_wait_ms.is_none()
627			&& already_requested
628			&& (!immediate || immediate_already_requested)
629		{
630			return Ok(save_request_revision);
631		}
632
633		sender
634			.send(LifecycleEvent::SaveRequested { immediate })
635			.map(|()| save_request_revision)
636			.map_err(|_| {
637				ActorRuntime::NotConfigured {
638					component: "lifecycle events".to_owned(),
639				}
640				.build()
641			})
642	}
643
644	pub(crate) async fn wait_for_save_request(&self, save_request_revision: u64) -> Result<()> {
645		wait_for_notified_condition(&self.0.save_completion, || {
646			self.0.save_completed_revision.load(Ordering::SeqCst) >= save_request_revision
647				|| self.0.save_failed_revision.load(Ordering::SeqCst) >= save_request_revision
648		})
649		.await;
650		if self.0.save_completed_revision.load(Ordering::SeqCst) >= save_request_revision {
651			return Ok(());
652		}
653		Err(ActorLifecycle::Stopping.build())
654	}
655
656	/// Keeps a failed save requested but drops its immediate priority and restarts the save
657	/// throttle from the failure, so a failure that repeats is retried once per save interval
658	/// instead of in a tight loop. The throttle otherwise counts from the last successful
659	/// save, which a failing actor never makes.
660	pub(crate) fn throttle_failed_save_request(&self) {
661		self.0
662			.save_requested_immediate
663			.store(false, Ordering::SeqCst);
664		*self.0.last_save_at.lock() = Some(StdInstant::now());
665	}
666
667	/// Fails every save request that has not been written, including any made later.
668	/// Shutdown calls this once no later save can run, so waiters never resolve as if a
669	/// write had happened and a request made afterwards fails instead of waiting forever.
670	pub(crate) fn fail_unpersisted_save_requests(&self) {
671		self.0
672			.save_failed_revision
673			.store(u64::MAX, Ordering::SeqCst);
674		self.0.save_completion.notify_waiters();
675	}
676
677	pub(crate) fn save_requested(&self) -> bool {
678		self.0.save_requested.load(Ordering::SeqCst)
679	}
680
681	pub(crate) fn save_requested_immediate(&self) -> bool {
682		self.0.save_requested_immediate.load(Ordering::SeqCst)
683	}
684
685	pub(crate) fn save_deadline(&self, immediate: bool) -> StdInstant {
686		self.compute_save_deadline(immediate)
687	}
688
689	pub(crate) fn compute_save_deadline(&self, immediate: bool) -> StdInstant {
690		if immediate || self.save_requested_immediate() {
691			return StdInstant::now();
692		}
693
694		let throttled_deadline = StdInstant::now() + self.compute_save_delay(None);
695		let requested_deadline = *self.0.save_requested_within_deadline.lock();
696
697		match requested_deadline {
698			Some(requested_deadline) => throttled_deadline.min(requested_deadline),
699			None => throttled_deadline,
700		}
701	}
702
703	pub(crate) fn save_request_revision(&self) -> u64 {
704		self.0.save_request_revision.load(Ordering::SeqCst)
705	}
706
707	pub(crate) async fn apply_state_deltas(
708		&self,
709		deltas: Vec<StateDelta>,
710		save_request_revision: u64,
711	) -> Result<()> {
712		self.apply_state_deltas_inner(deltas, None, save_request_revision, None)
713			.await
714			.map(|_| ())
715	}
716
717	pub(super) async fn apply_state_deltas_inner(
718		&self,
719		deltas: Vec<StateDelta>,
720		workflow_writes: Option<Vec<WorkflowKvWrite>>,
721		save_request_revision: u64,
722		expected_state_transaction_epoch: Option<u64>,
723	) -> Result<bool> {
724		let delta_count = deltas.len();
725		let delta_bytes: usize = deltas.iter().map(StateDelta::payload_len).sum();
726		let workflow_write_count = workflow_writes.as_ref().map_or(0, Vec::len);
727		let current_revision = self.0.state_revision.load(Ordering::SeqCst);
728		tracing::debug!(
729			delta_count,
730			delta_bytes,
731			state_revision = current_revision,
732			save_request_revision,
733			"applying actor state deltas"
734		);
735		self.clear_pending_save();
736
737		if deltas.is_empty() && workflow_write_count == 0 {
738			self.mark_save_request_completed(save_request_revision);
739			self.finish_save_request(save_request_revision);
740			tracing::debug!(
741				delta_count,
742				state_revision = current_revision,
743				save_request_revision,
744				"actor state deltas applied without kv write"
745			);
746			return Ok(true);
747		}
748
749		// The actor task passes an expected epoch and must not wait on a guard
750		// that a user state transaction may hold for as long as its callback
751		// runs. Contention discards the serialized state like an epoch change,
752		// and the task defers the retry until the transaction finishes.
753		let save_guard = match expected_state_transaction_epoch {
754			Some(_) => match self.0.save_guard.try_lock() {
755				Ok(save_guard) => save_guard,
756				Err(_) => {
757					tracing::debug!(
758						save_request_revision,
759						"state save guard is held, deferring actor task save"
760					);
761					return Ok(false);
762				}
763			},
764			None => self.0.save_guard.lock().await,
765		};
766		let prepared = {
767			let _save_guard = save_guard;
768			if expected_state_transaction_epoch.is_some_and(|expected| {
769				self.0.state_transaction_epoch.load(Ordering::SeqCst) != expected
770			}) {
771				None
772			} else {
773				let revision = self.0.state_revision.load(Ordering::SeqCst);
774				let mut persisted = self.persisted();
775				let mut next_state = None;
776				let mut actor_to_persist = None;
777				let mut connections_to_persist: Vec<PersistedConnection> = Vec::new();
778				let mut connections_to_delete = Vec::new();
779
780				for delta in deltas {
781					match delta {
782						StateDelta::ActorState(bytes) => {
783							next_state = Some(bytes.clone());
784							persisted.state = bytes.into();
785						}
786						StateDelta::ConnHibernation { conn: _, bytes } => {
787							connections_to_persist.push(
788								decode_persisted_connection(&bytes)
789									.context("decode hibernatable connection state delta")?,
790							);
791						}
792						StateDelta::ConnHibernationRemoved(conn) => {
793							connections_to_delete.push(conn);
794						}
795					}
796				}
797
798				if next_state.is_some() {
799					actor_to_persist = Some(persisted.clone());
800				}
801
802				Some((
803					next_state,
804					actor_to_persist,
805					connections_to_persist,
806					connections_to_delete,
807					revision,
808					self.begin_write(),
809				))
810			}
811		};
812		let Some((
813			next_state,
814			actor_to_persist,
815			connections_to_persist,
816			connections_to_delete,
817			revision,
818			_write_guard,
819		)) = prepared
820		else {
821			tracing::debug!(
822				save_request_revision,
823				"discarding state serialized across a state transaction boundary"
824			);
825			return Ok(false);
826		};
827
828		if let Some(workflow_writes) = workflow_writes.as_deref() {
829			internal_storage::persist_actor_core_connections_and_workflow(
830				self.sql(),
831				actor_to_persist.as_ref(),
832				&connections_to_persist,
833				&connections_to_delete,
834				workflow_writes,
835			)
836			.await
837			.context("atomically persist actor state, connection deltas, and workflow kv")?;
838		} else if actor_to_persist.is_some()
839			|| !connections_to_persist.is_empty()
840			|| !connections_to_delete.is_empty()
841		{
842			internal_storage::persist_actor_core_and_connections(
843				self.sql(),
844				actor_to_persist.as_ref(),
845				&connections_to_persist,
846				&connections_to_delete,
847			)
848			.await
849			.context("persist actor state and connection deltas to sqlite")?;
850		}
851
852		if let Some(state) = next_state {
853			self.0.persisted.write().state = state.clone().into();
854			*self.0.current_state.write() = state;
855		}
856		for connection in &connections_to_persist {
857			if let Some(handle) = self.connection(&connection.id) {
858				handle.set_state_initial(connection.state.clone().to_vec());
859			}
860		}
861
862		*self.0.last_save_at.lock() = Some(StdInstant::now());
863
864		if self.0.state_revision.load(Ordering::SeqCst) == revision {
865			self.0.state_dirty.store(false, Ordering::SeqCst);
866		}
867
868		self.mark_save_request_completed(save_request_revision);
869		self.finish_save_request(save_request_revision);
870		tracing::debug!(
871			delta_count,
872			delta_bytes,
873			workflow_write_count,
874			state_revision = self.0.state_revision.load(Ordering::SeqCst),
875			save_request_revision,
876			"actor state deltas applied"
877		);
878		Ok(true)
879	}
880	async fn commit_state_transaction(
881		&self,
882		transaction: &SqliteTransaction,
883		deltas: Vec<StateDelta>,
884		save_request_revision: u64,
885		statement_count: usize,
886		bind_payload_bytes: usize,
887	) -> Result<()> {
888		let (deltas, pending_hibernation_changes) = match self.prepare_state_deltas(deltas) {
889			Ok(prepared) => prepared,
890			Err(error) => {
891				let _ = transaction.rollback().await;
892				return Err(error);
893			}
894		};
895		let commit_result = async {
896			let revision = self.0.state_revision.load(Ordering::SeqCst);
897			let mut persisted = self.persisted();
898			let mut next_state = None;
899			let mut actor_to_persist = None;
900			let mut connections_to_persist: Vec<PersistedConnection> = Vec::new();
901			let mut connections_to_delete = Vec::new();
902
903			for delta in deltas {
904				match delta {
905					StateDelta::ActorState(bytes) => {
906						next_state = Some(bytes.clone());
907						persisted.state = bytes.into();
908					}
909					StateDelta::ConnHibernation { conn: _, bytes } => {
910						connections_to_persist.push(
911							decode_persisted_connection(&bytes)
912								.context("decode hibernatable connection state delta")?,
913						);
914					}
915					StateDelta::ConnHibernationRemoved(conn) => {
916						connections_to_delete.push(conn);
917					}
918				}
919			}
920
921			if next_state.is_some() {
922				actor_to_persist = Some(persisted);
923			}
924			let statements = internal_storage::build_actor_core_and_connection_statements(
925				actor_to_persist.as_ref(),
926				&connections_to_persist,
927				&connections_to_delete,
928			)?;
929			internal_storage::validate_atomic_state_transaction_budget(
930				statement_count.saturating_add(statements.len()),
931				bind_payload_bytes
932					.saturating_add(internal_storage::statement_bind_payload_len(&statements)),
933			)?;
934			for statement in statements {
935				transaction
936					.execute(statement.sql, statement.params)
937					.await
938					.context("persist actor state inside sqlite transaction")?;
939			}
940			transaction
941				.commit()
942				.await
943				.context("commit sqlite transaction with actor state")?;
944
945			if let Some(state) = next_state {
946				self.0.persisted.write().state = state.clone().into();
947				*self.0.current_state.write() = state;
948			}
949			for connection in &connections_to_persist {
950				if let Some(handle) = self.connection(&connection.id) {
951					handle.set_state_initial(connection.state.clone().to_vec());
952				}
953			}
954			*self.0.last_save_at.lock() = Some(StdInstant::now());
955			if self.0.state_revision.load(Ordering::SeqCst) == revision {
956				self.0.state_dirty.store(false, Ordering::SeqCst);
957			}
958			self.mark_save_request_completed(save_request_revision);
959			self.finish_save_request(save_request_revision);
960			self.record_state_updated();
961			Ok(())
962		}
963		.await;
964
965		if let Err(error) = commit_result {
966			self.restore_pending_hibernation_changes(pending_hibernation_changes);
967			let _ = transaction.rollback().await;
968			return Err(error);
969		}
970		Ok(())
971	}
972
973	pub(crate) async fn wait_for_pending_writes(&self) {
974		loop {
975			if let Some(handle) = self.take_tracked_persist() {
976				let _ = handle.await;
977				continue;
978			}
979
980			let save_guard = self.0.save_guard.lock().await;
981			if self.has_tracked_persist() {
982				drop(save_guard);
983				continue;
984			}
985
986			if self.0.in_flight_state_writes.load(Ordering::SeqCst) == 0 {
987				return;
988			}
989			drop(save_guard);
990
991			self.wait_for_in_flight_writes().await;
992		}
993	}
994
995	pub(crate) async fn wait_for_pending_state_writes(&self) {
996		self.wait_for_pending_writes().await;
997	}
998
999	pub fn begin_on_state_change(&self) -> OnStateChangeGuard {
1000		OnStateChangeGuard::new(self.clone())
1001	}
1002
1003	pub fn on_state_change_started(&self) {
1004		self.0
1005			.on_state_change_in_flight
1006			.fetch_add(1, Ordering::SeqCst);
1007		self.0.sleep.work.keep_awake.increment();
1008		self.reset_sleep_timer();
1009	}
1010
1011	pub fn on_state_change_finished(&self) {
1012		let previous = self.0.on_state_change_in_flight.try_update(
1013			Ordering::SeqCst,
1014			Ordering::SeqCst,
1015			|count| count.checked_sub(1),
1016		);
1017
1018		match previous {
1019			Ok(1) => {
1020				self.0.sleep.work.keep_awake.decrement();
1021				self.0.on_state_change_idle.notify_waiters();
1022				self.reset_sleep_timer();
1023			}
1024			Ok(_) => {
1025				self.0.sleep.work.keep_awake.decrement();
1026				self.reset_sleep_timer();
1027			}
1028			Err(_) => {
1029				tracing::warn!(
1030					actor_id = %self.actor_id(),
1031					"on_state_change finished without a matching start"
1032				);
1033			}
1034		}
1035	}
1036
1037	
1038
1039	pub fn persisted(&self) -> PersistedActor {
1040		self.0.persisted.read().clone()
1041	}
1042
1043	pub fn load_persisted(&self, persisted: PersistedActor) {
1044		let state = persisted.state.clone();
1045		*self.0.persisted.write() = persisted;
1046		*self.0.current_state.write() = state.to_vec();
1047		self.0.state_dirty.store(false, Ordering::SeqCst);
1048		self.finish_save_request(self.save_request_revision());
1049		self.0
1050			.metrics
1051			.inc_state_mutation(StateMutationReason::InternalReplace);
1052	}
1053
1054	pub(crate) fn load_last_pushed_alarm(&self, alarm_ts: Option<i64>) {
1055		*self.0.last_pushed_alarm.write() = alarm_ts;
1056	}
1057
1058	pub(crate) fn last_pushed_alarm(&self) -> Option<i64> {
1059		*self.0.last_pushed_alarm.read()
1060	}
1061
1062	/// Persists an acknowledged alarm push. The in-memory value was already set when the
1063	/// push was sent, and a newer push may have replaced it since, so this leaves it alone.
1064	pub(crate) async fn persist_last_pushed_alarm(&self, alarm_ts: Option<i64>) -> Result<()> {
1065		internal_storage::persist_last_pushed_alarm(self.sql(), alarm_ts)
1066			.await
1067			.context("persist last pushed alarm to sqlite")
1068	}
1069
1070	pub(crate) fn load_run_wake_at(&self, wake_at: Option<i64>) {
1071		*self.0.run_wake_at.write() = wake_at;
1072	}
1073
1074	pub(crate) fn run_wake_at(&self) -> Option<i64> {
1075		*self.0.run_wake_at.read()
1076	}
1077
1078	pub(crate) async fn persist_run_wake_at(&self, wake_at: Option<i64>) -> Result<()> {
1079		internal_storage::persist_run_wake_at(self.sql(), wake_at)
1080			.await
1081			.context("persist run wake deadline to sqlite")?;
1082		self.load_run_wake_at(wake_at);
1083		Ok(())
1084	}
1085
1086	pub(crate) fn set_initial_state(&self, state: Vec<u8>) {
1087		*self.0.current_state.write() = state.clone();
1088		self.0.persisted.write().state = state.into();
1089		self.0.state_dirty.store(true, Ordering::SeqCst);
1090		self.0.state_revision.fetch_add(1, Ordering::SeqCst);
1091	}
1092
1093	pub fn scheduled_events(&self) -> Vec<PersistedScheduleEvent> {
1094		self.0.persisted.read().scheduled_events.clone()
1095	}
1096
1097	pub fn set_scheduled_events(&self, scheduled_events: Vec<PersistedScheduleEvent>) {
1098		self.0.persisted.write().scheduled_events = scheduled_events;
1099		self.0
1100			.metrics
1101			.inc_state_mutation(StateMutationReason::ScheduledEventsUpdate);
1102		self.mark_dirty();
1103		self.schedule_save(None);
1104	}
1105
1106	pub fn set_input(&self, input: Option<Vec<u8>>) {
1107		self.0.persisted.write().input = input.map(Into::into);
1108		self.0
1109			.metrics
1110			.inc_state_mutation(StateMutationReason::InputSet);
1111		self.mark_dirty();
1112		self.schedule_save(None);
1113	}
1114
1115	pub fn input(&self) -> Option<Vec<u8>> {
1116		self.0.persisted.read().input.clone().map(Into::into)
1117	}
1118
1119	pub fn set_has_initialized(&self, has_initialized: bool) {
1120		{
1121			let mut persisted = self.0.persisted.write();
1122			if persisted.has_initialized == has_initialized {
1123				return;
1124			}
1125			persisted.has_initialized = has_initialized;
1126		}
1127		self.0
1128			.metrics
1129			.inc_state_mutation(StateMutationReason::HasInitialized);
1130		self.mark_dirty();
1131		self.schedule_save(None);
1132	}
1133
1134	pub fn has_initialized(&self) -> bool {
1135		self.0.persisted.read().has_initialized
1136	}
1137
1138	pub fn flush_on_shutdown(&self) {
1139		self.persist_now_tracked("shutdown_flush");
1140	}
1141
1142	pub fn on_request_save(&self, hook: Box<dyn Fn(RequestSaveOpts) + Send + Sync>) {
1143		self.0.request_save_hooks.write().push(Arc::from(hook));
1144	}
1145
1146	fn is_dirty(&self) -> bool {
1147		self.0.state_dirty.load(Ordering::SeqCst)
1148	}
1149
1150	fn mark_dirty(&self) {
1151		self.0.state_dirty.store(true, Ordering::SeqCst);
1152		self.0.state_revision.fetch_add(1, Ordering::SeqCst);
1153	}
1154
1155	fn lifecycle_event_sender(&self) -> Option<mpsc::UnboundedSender<LifecycleEvent>> {
1156		self.0.lifecycle_events.read().clone()
1157	}
1158
1159	fn compute_save_delay(&self, max_wait: Option<Duration>) -> Duration {
1160		let elapsed = self
1161			.0
1162			.last_save_at
1163			.lock()
1164			.map(|instant| instant.elapsed())
1165			.unwrap_or_default();
1166
1167		throttled_save_delay(self.0.state_save_interval, elapsed, max_wait)
1168	}
1169
1170	fn schedule_save(&self, max_wait: Option<Duration>) {
1171		if !self.is_dirty() {
1172			return;
1173		}
1174
1175		let delay = self.compute_save_delay(max_wait);
1176		let scheduled_at = StdInstant::now() + delay;
1177
1178		let mut pending_save = self.0.pending_save.lock();
1179
1180		if let Some(existing) = pending_save.as_ref() {
1181			if existing.scheduled_at <= scheduled_at {
1182				return;
1183			}
1184
1185			existing.handle.abort();
1186		}
1187
1188		let state = self.clone();
1189		// Intentionally detached but abortable: pending delayed saves are
1190		// retained in `pending_save`, replaced by newer saves, and awaited at
1191		// shutdown through the state save guard.
1192		let task = async move {
1193			if !delay.is_zero() {
1194				sleep(delay).await;
1195			}
1196
1197			state.take_pending_save();
1198
1199			if let Err(error) = state.persist_if_dirty().await {
1200				tracing::error!(?error, "failed to persist actor state");
1201			}
1202		}
1203		.in_current_span();
1204
1205		#[cfg(not(feature = "wasm-runtime"))]
1206		let handle = {
1207			let Ok(tokio_handle) = Handle::try_current() else {
1208				return;
1209			};
1210			tokio_handle.spawn(task)
1211		};
1212
1213		#[cfg(feature = "wasm-runtime")]
1214		let handle = RuntimeSpawner::spawn(task);
1215
1216		*pending_save = Some(PendingSave {
1217			scheduled_at,
1218			handle,
1219		});
1220	}
1221
1222	fn clear_pending_save(&self) {
1223		if let Some(pending_save) = self.take_pending_save() {
1224			pending_save.handle.abort();
1225		}
1226	}
1227
1228	pub(crate) fn persist_now_tracked(&self, description: &'static str) {
1229		self.clear_pending_save();
1230
1231		let state = self.clone();
1232		let mut tracked_persist = self.0.tracked_persist.lock();
1233		let previous = tracked_persist.take();
1234		let task = async move {
1235			if let Some(previous) = previous {
1236				let _ = previous.await;
1237			}
1238
1239			if let Err(error) = state.persist_state(SaveStateOpts { immediate: true }).await {
1240				tracing::error!(?error, description, "failed to persist actor state");
1241			}
1242		}
1243		.in_current_span();
1244
1245		#[cfg(not(feature = "wasm-runtime"))]
1246		let handle = {
1247			let Ok(tokio_handle) = Handle::try_current() else {
1248				tracing::warn!(
1249					description,
1250					"skipping tracked actor state persistence without runtime"
1251				);
1252				return;
1253			};
1254			tokio_handle.spawn(task)
1255		};
1256
1257		#[cfg(feature = "wasm-runtime")]
1258		let handle = RuntimeSpawner::spawn(task);
1259
1260		*tracked_persist = Some(handle);
1261	}
1262
1263	fn take_pending_save(&self) -> Option<PendingSave> {
1264		self.0.pending_save.lock().take()
1265	}
1266
1267	fn take_tracked_persist(&self) -> Option<JoinHandle<()>> {
1268		self.0.tracked_persist.lock().take()
1269	}
1270
1271	fn has_tracked_persist(&self) -> bool {
1272		self.0.tracked_persist.lock().is_some()
1273	}
1274
1275	
1276
1277	async fn persist_if_dirty(&self) -> Result<()> {
1278		if !self.is_dirty() {
1279			return Ok(());
1280		}
1281
1282		let (revision, actor_to_persist, _write_guard) = {
1283			let _save_guard = self.0.save_guard.lock().await;
1284			if !self.is_dirty() {
1285				return Ok(());
1286			}
1287
1288			let revision = self.0.state_revision.load(Ordering::SeqCst);
1289			let persisted = self.persisted();
1290			(revision, persisted, self.begin_write())
1291		};
1292
1293		internal_storage::persist_actor_snapshot(self.sql(), &actor_to_persist)
1294			.await
1295			.context("persist actor state to sqlite")?;
1296
1297		*self.0.last_save_at.lock() = Some(StdInstant::now());
1298
1299		if self.0.state_revision.load(Ordering::SeqCst) == revision {
1300			self.0.state_dirty.store(false, Ordering::SeqCst);
1301		}
1302
1303		Ok(())
1304	}
1305
1306	/// Runs the startup commit in the background, so the actor's first requests run while it is
1307	/// in flight; their responses wait for it in `wait_startup_commit`. A failed commit stops
1308	/// the actor with an error, as a failed startup does. The write counts as in flight from
1309	/// this call, so a shutdown that follows waits for it.
1310	pub(crate) fn run_startup_commit(
1311		&self,
1312		commit: impl std::future::Future<Output = Result<()>> + Send + 'static,
1313	) {
1314		self.0
1315			.startup_commit
1316			.send_modify(|startup| startup.pending += 1);
1317		let write = self.begin_write();
1318		let ctx = self.clone();
1319		RuntimeSpawner::spawn(
1320			async move {
1321				let result = commit.await;
1322				drop(write);
1323				let failed = result.is_err();
1324				let mut returned = false;
1325				ctx.0.startup_commit.send_modify(|startup| {
1326					startup.pending = startup.pending.saturating_sub(1);
1327					startup.failed |= failed;
1328					returned = startup.returned;
1329				});
1330				if let Err(error) = result {
1331					tracing::error!(
1332						?error,
1333						actor_id = %ctx.actor_id(),
1334						"actor startup state commit failed; stopping the actor"
1335					);
1336					// Before startup returns, its own check fails the start instead.
1337					if !returned {
1338						return;
1339					}
1340					if let Err(stop_error) = ctx.stop_with_error(format!("{error:#}")) {
1341						tracing::error!(
1342							?stop_error,
1343							actor_id = %ctx.actor_id(),
1344							"failed to report the startup commit failure to the engine"
1345						);
1346					}
1347				}
1348			}
1349			.in_current_span(),
1350		);
1351	}
1352
1353	/// Begins a startup's tracking of its background commits. A restart within this context
1354	/// starts over: an earlier generation's failure does not fail it.
1355	pub(crate) fn begin_startup_commits(&self) {
1356		self.0.startup_commit.send_modify(|startup| {
1357			startup.returned = false;
1358			startup.failed = false;
1359		});
1360	}
1361
1362	/// Marks startup as returned, and reports whether a startup commit already failed, in
1363	/// which case the start must fail. A commit that fails later stops the actor itself.
1364	pub(crate) fn finish_startup_commits(&self) -> Result<()> {
1365		let mut failed = false;
1366		self.0.startup_commit.send_modify(|startup| {
1367			startup.returned = true;
1368			failed = startup.failed;
1369		});
1370		if failed {
1371			return Err(ActorLifecycle::Stopping.build())
1372				.context("actor startup state commit failed");
1373		}
1374		Ok(())
1375	}
1376
1377	/// Waits until every startup commit of this generation is durable. An error means one
1378	/// failed and the actor is stopping, so a request served in the meantime must not report
1379	/// success.
1380	pub(crate) async fn wait_startup_commit(&self) -> Result<()> {
1381		let mut startup = self.0.startup_commit.subscribe();
1382		let failed = startup
1383			.wait_for(|startup| startup.pending == 0)
1384			.await
1385			.map(|startup| startup.failed);
1386		match failed {
1387			Ok(false) => Ok(()),
1388			Ok(true) | Err(_) => {
1389				Err(ActorLifecycle::Stopping.build()).context("actor startup state commit failed")
1390			}
1391		}
1392	}
1393
1394	fn begin_write(&self) -> InFlightWrite {
1395		self.0.in_flight_state_writes.fetch_add(1, Ordering::SeqCst);
1396		InFlightWrite { ctx: self.clone() }
1397	}
1398
1399	async fn wait_for_in_flight_writes(&self) {
1400		wait_for_notified_condition(&self.0.state_write_completion, || {
1401			self.0.in_flight_state_writes.load(Ordering::SeqCst) == 0
1402		})
1403		.await;
1404	}
1405
1406	fn finish_save_request(&self, save_request_revision: u64) {
1407		if self.0.save_request_revision.load(Ordering::SeqCst) == save_request_revision {
1408			self.0.save_requested.store(false, Ordering::SeqCst);
1409			self.0
1410				.save_requested_immediate
1411				.store(false, Ordering::SeqCst);
1412			*self.0.save_requested_within_deadline.lock() = None;
1413		}
1414	}
1415
1416	fn mark_save_request_completed(&self, save_request_revision: u64) {
1417		self.0
1418			.save_completed_revision
1419			.fetch_max(save_request_revision, Ordering::SeqCst);
1420		self.0.save_completion.notify_waiters();
1421	}
1422
1423	fn notify_request_save_hooks(&self, opts: RequestSaveOpts) {
1424		let hooks = self.0.request_save_hooks.read().clone();
1425		for hook in hooks {
1426			hook(opts);
1427		}
1428	}
1429}
1430
1431struct InFlightWrite {
1432	ctx: ActorContext,
1433}
1434
1435impl Drop for InFlightWrite {
1436	fn drop(&mut self) {
1437		if self
1438			.ctx
1439			.0
1440			.in_flight_state_writes
1441			.fetch_sub(1, Ordering::SeqCst)
1442			== 1
1443		{
1444			self.ctx.0.state_write_completion.notify_waiters();
1445		}
1446	}
1447}
1448
1449/// Waits until `ready` returns true, re-checking after each wakeup of `notify`.
1450///
1451/// Producers must update the state `ready` reads before calling
1452/// `notify.notify_waiters()`. The notification is enabled before every check,
1453/// so a producer that runs between the check and the await still wakes this
1454/// waiter even though `notify_waiters` stores no permit. Every concurrent
1455/// waiter is woken, not just one.
1456async fn wait_for_notified_condition(notify: &Notify, mut ready: impl FnMut() -> bool) {
1457	loop {
1458		let notified = notify.notified();
1459		tokio::pin!(notified);
1460		notified.as_mut().enable();
1461
1462		if ready() {
1463			return;
1464		}
1465
1466		notified.await;
1467	}
1468}
1469
1470fn throttled_save_delay(
1471	save_interval: Duration,
1472	time_since_last_save: Duration,
1473	max_wait: Option<Duration>,
1474) -> Duration {
1475	let save_delay = save_interval.saturating_sub(time_since_last_save);
1476	if let Some(max_wait) = max_wait {
1477		save_delay.min(max_wait)
1478	} else {
1479		save_delay
1480	}
1481}
1482
1483// Test shim keeps moved tests in crate-root tests/ with private-module access.
1484