Skip to main content

rivetkit_core/actor/
sleep.rs

1use parking_lot::Mutex;
2use rivet_envoy_client::async_counter::AsyncCounter;
3use rivet_envoy_client::handle::EnvoyHandle;
4use std::future::Future;
5use std::sync::Arc;
6#[cfg(test)]
7use std::sync::atomic::AtomicUsize as TestAtomicUsize;
8use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
9#[cfg(not(feature = "wasm-runtime"))]
10use tokio::runtime::Handle;
11use tokio::sync::Notify;
12use tokio::task::JoinHandle;
13use tracing::Instrument;
14
15use crate::actor::config::ActorConfig;
16use crate::actor::context::ActorContext;
17#[cfg(not(feature = "wasm-runtime"))]
18use crate::actor::context::ActorWorkRegion;
19use crate::actor::task_types::ShutdownKind;
20#[cfg(not(feature = "wasm-runtime"))]
21use crate::actor::work_registry::ActorWorkPolicy;
22#[cfg(feature = "wasm-runtime")]
23use crate::actor::work_registry::LocalShutdownTask;
24use crate::actor::work_registry::{ActorWorkKind, CountGuard, RegionGuard, WorkRegistry};
25#[cfg(feature = "wasm-runtime")]
26use crate::runtime::RuntimeSpawner;
27#[cfg(test)]
28use crate::time::sleep_until;
29use crate::time::{Instant, sleep};
30#[cfg(test)]
31use crate::types::ActorKey;
32#[cfg(feature = "wasm-runtime")]
33use futures::channel::oneshot as futures_oneshot;
34#[cfg(feature = "wasm-runtime")]
35use futures::future::{AbortHandle, Abortable};
36
37/// Per-actor sleep state.
38///
39/// `ActorContext::reset_sleep_timer()` is invoked on every mutation that changes
40/// a sleep predicate input. Production actors wake the owning `ActorTask` via a
41/// single `Notify`; contexts not wired to an `ActorTask` use the detached
42/// compatibility timer below.
43pub(crate) struct SleepState {
44	// Forced-sync: sleep controller config/runtime handles are synchronous
45	// wiring slots cloned before actor I/O.
46	pub(super) config: Mutex<ActorConfig>,
47	pub(super) envoy_handle: Mutex<Option<EnvoyHandle>>,
48	pub(super) generation: Mutex<Option<u32>>,
49	pub(super) http_request_counter: Mutex<Option<Arc<AsyncCounter>>>,
50	// Forced-sync: written once by whichever caller wins the destroy-request
51	// swap, then consumed when the stop intent is sent to the envoy.
52	pub(super) destroy_error: Mutex<Option<String>>,
53	#[cfg(test)]
54	sleep_request_count: TestAtomicUsize,
55	#[cfg(test)]
56	destroy_request_count: TestAtomicUsize,
57	pub(super) lifecycle_started: AtomicBool,
58	pub(super) run_handler_active_count: AtomicUsize,
59	// Forced-sync: the compatibility sleep timer is aborted from sync paths.
60	pub(super) sleep_timer: Mutex<Option<JoinHandle<()>>>,
61	pub(super) work: WorkRegistry,
62}
63
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
65pub(crate) enum CanSleep {
66	Yes,
67	NotReady,
68	NoSleep,
69	ActiveHttpRequests,
70	ActiveKeepAwake,
71	ActiveInternalKeepAwake,
72	ActiveRunHandler,
73	ActiveDisconnectCallbacks,
74	ActiveConnections,
75	ActiveWebSocketCallbacks,
76}
77
78impl SleepState {
79	pub fn new(config: ActorConfig) -> Self {
80		Self {
81			config: Mutex::new(config),
82			envoy_handle: Mutex::new(None),
83			generation: Mutex::new(None),
84			http_request_counter: Mutex::new(None),
85			destroy_error: Mutex::new(None),
86			#[cfg(test)]
87			sleep_request_count: TestAtomicUsize::new(0),
88			#[cfg(test)]
89			destroy_request_count: TestAtomicUsize::new(0),
90			lifecycle_started: AtomicBool::new(false),
91			run_handler_active_count: AtomicUsize::new(0),
92			sleep_timer: Mutex::new(None),
93			work: WorkRegistry::new(),
94		}
95	}
96}
97
98impl Default for SleepState {
99	fn default() -> Self {
100		Self::new(ActorConfig::default())
101	}
102}
103
104impl std::fmt::Debug for SleepState {
105	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106		f.debug_struct("SleepState")
107			.field(
108				"lifecycle_started",
109				&self.lifecycle_started.load(Ordering::SeqCst),
110			)
111			.field(
112				"run_handler_active_count",
113				&self.run_handler_active_count.load(Ordering::SeqCst),
114			)
115			.field("keep_awake_count", &self.work.keep_awake.load())
116			.field(
117				"internal_keep_awake_count",
118				&self.work.internal_keep_awake.load(),
119			)
120			.field(
121				"websocket_callback_count",
122				&self.work.websocket_callback.load(),
123			)
124			.field(
125				"disconnect_callback_count",
126				&self.work.disconnect_callback.load(),
127			)
128			.finish()
129	}
130}
131
132impl ActorContext {
133	#[cfg(test)]
134	pub(crate) fn new_for_sleep_tests(actor_id: impl Into<String>) -> Self {
135		Self::new(actor_id, "sleep-test", ActorKey::default(), "local")
136	}
137
138	pub(crate) fn configure_sleep_state(&self, config: ActorConfig) {
139		*self.0.sleep.config.lock() = config;
140	}
141
142	pub(crate) fn configure_sleep_envoy(&self, envoy_handle: EnvoyHandle, generation: Option<u32>) {
143		*self.0.sleep.envoy_handle.lock() = Some(envoy_handle);
144		*self.0.sleep.generation.lock() = generation;
145		*self.0.sleep.http_request_counter.lock() =
146			self.lookup_http_request_counter(self.actor_id());
147	}
148
149	pub(crate) fn sleep_envoy_handle(&self) -> Option<EnvoyHandle> {
150		self.0.sleep.envoy_handle.lock().clone()
151	}
152
153	pub(crate) fn sleep_generation(&self) -> Option<u32> {
154		*self.0.sleep.generation.lock()
155	}
156
157	pub(crate) fn request_sleep_from_envoy(&self) {
158		#[cfg(test)]
159		self.0
160			.sleep
161			.sleep_request_count
162			.fetch_add(1, Ordering::SeqCst);
163		let envoy_handle = self.0.sleep.envoy_handle.lock().clone();
164		let generation = *self.0.sleep.generation.lock();
165		if let Some(envoy_handle) = envoy_handle {
166			envoy_handle.sleep_actor(self.actor_id().to_owned(), generation);
167		}
168	}
169
170	pub(crate) fn request_destroy_from_envoy(&self) {
171		#[cfg(test)]
172		self.0
173			.sleep
174			.destroy_request_count
175			.fetch_add(1, Ordering::SeqCst);
176		let envoy_handle = self.0.sleep.envoy_handle.lock().clone();
177		let generation = *self.0.sleep.generation.lock();
178		let error = self.0.sleep.destroy_error.lock().take();
179		if let Some(envoy_handle) = envoy_handle {
180			envoy_handle.stop_actor(self.actor_id().to_owned(), generation, error);
181		}
182	}
183
184	pub(crate) fn set_lifecycle_started(&self, started: bool) {
185		let previous = self
186			.0
187			.sleep
188			.lifecycle_started
189			.swap(started, Ordering::SeqCst);
190		if previous != started {
191			self.reset_sleep_timer();
192		}
193	}
194
195	pub(crate) fn lifecycle_started(&self) -> bool {
196		self.0.sleep.lifecycle_started.load(Ordering::SeqCst)
197	}
198
199	#[doc(hidden)]
200	pub fn begin_run_handler(&self) {
201		let previous = self
202			.0
203			.sleep
204			.run_handler_active_count
205			.fetch_add(1, Ordering::SeqCst);
206		if previous == 0 {
207			self.reset_sleep_timer();
208		}
209	}
210
211	#[doc(hidden)]
212	pub fn end_run_handler(&self) {
213		match self.0.sleep.run_handler_active_count.fetch_update(
214			Ordering::SeqCst,
215			Ordering::SeqCst,
216			|count| count.checked_sub(1),
217		) {
218			Ok(1) => self.reset_sleep_timer(),
219			Ok(_) => {}
220			Err(_) => {
221				tracing::warn!(
222					actor_id = %self.actor_id(),
223					"run handler active counter underflow"
224				);
225			}
226		}
227	}
228
229	pub(crate) fn run_handler_active(&self) -> bool {
230		self.0.sleep.run_handler_active_count.load(Ordering::SeqCst) > 0
231	}
232
233	#[cfg(test)]
234	pub(crate) fn sleep_request_count(&self) -> usize {
235		self.0.sleep.sleep_request_count.load(Ordering::SeqCst)
236	}
237
238	pub(crate) async fn can_arm_sleep_timer(&self) -> CanSleep {
239		let config = self.sleep_state_config();
240		if !self.0.sleep.lifecycle_started.load(Ordering::SeqCst) {
241			return CanSleep::NotReady;
242		}
243		if config.no_sleep {
244			return CanSleep::NoSleep;
245		}
246		if self.active_http_request_count() > 0 {
247			return CanSleep::ActiveHttpRequests;
248		}
249		if self.sleep_keep_awake_count() > 0 {
250			return CanSleep::ActiveKeepAwake;
251		}
252		if self.sleep_internal_keep_awake_count() > 0 {
253			return CanSleep::ActiveInternalKeepAwake;
254		}
255		// Queue receives are sleep-compatible: sleep aborts the wait via the
256		// actor abort token, then the next generation restarts the run loop.
257		if self.run_handler_active() && self.active_queue_wait_count() == 0 {
258			return CanSleep::ActiveRunHandler;
259		}
260		if self.pending_disconnect_count() > 0 {
261			return CanSleep::ActiveDisconnectCallbacks;
262		}
263		if !self.conns().is_empty() {
264			return CanSleep::ActiveConnections;
265		}
266		if self.websocket_callback_count() > 0 {
267			return CanSleep::ActiveWebSocketCallbacks;
268		}
269
270		CanSleep::Yes
271	}
272
273	pub(crate) fn can_finalize_shutdown(&self, reason: ShutdownKind) -> bool {
274		self.0.sleep.work.core_dispatched_hooks.load() == 0
275			// Sleep is a reversible pause, so let the actor run handler finish
276			// naturally during grace. Destroy is terminal and may cut through it.
277			&& (matches!(reason, ShutdownKind::Destroy) || !self.run_handler_active())
278			&& self.shutdown_task_count() == 0
279			&& self.sleep_keep_awake_count() == 0
280			&& self.sleep_internal_keep_awake_count() == 0
281			&& self.active_http_request_count() == 0
282			&& self.websocket_callback_count() == 0
283			&& self.pending_disconnect_count() == 0
284	}
285
286	/// Spawn the fallback sleep timer used by `ActorContext`s that are not
287	/// bound to an `ActorTask`.
288	///
289	/// This path only engages when `configure_lifecycle_events` has not been
290	/// wired, which in practice means test contexts. Production actors built
291	/// through the registry always have an `ActorTask` and never spawn this
292	/// detached timer.
293	pub(crate) fn reset_sleep_timer_state(&self) {
294		self.cancel_sleep_timer();
295
296		#[cfg(not(feature = "wasm-runtime"))]
297		let Ok(runtime) = Handle::try_current() else {
298			tracing::debug!(
299				actor_id = %self.actor_id(),
300				"sleep activity reset skipped without tokio runtime"
301			);
302			return;
303		};
304
305		tracing::debug!(
306			actor_id = %self.actor_id(),
307			sleep_timeout_ms = self.0.sleep.config.lock().sleep_timeout.as_millis() as u64,
308			"sleep activity reset"
309		);
310
311		let ctx = self.clone();
312		let task_body = async move {
313			let can_sleep = ctx.can_sleep().await;
314			if can_sleep != CanSleep::Yes {
315				tracing::debug!(
316					actor_id = %ctx.actor_id(),
317					reason = ?can_sleep,
318					"sleep idle timer skipped"
319				);
320				return;
321			}
322
323			let timeout = ctx.sleep_config().sleep_timeout;
324			sleep(timeout).await;
325
326			let can_sleep = ctx.can_sleep().await;
327			if can_sleep == CanSleep::Yes {
328				tracing::debug!(
329					actor_id = %ctx.actor_id(),
330					sleep_timeout_ms = timeout.as_millis() as u64,
331					"sleep idle timer elapsed"
332				);
333				if let Err(err) = ctx.sleep() {
334					tracing::debug!(
335						actor_id = %ctx.actor_id(),
336						?err,
337						"sleep idle timer request suppressed"
338					);
339				}
340			} else {
341				tracing::warn!(
342					actor_id = %ctx.actor_id(),
343					reason = ?can_sleep,
344					"sleep idle timer elapsed but actor stayed awake"
345				);
346			}
347		};
348
349		#[cfg(not(feature = "wasm-runtime"))]
350		let task = runtime.spawn(task_body);
351
352		#[cfg(feature = "wasm-runtime")]
353		let task = RuntimeSpawner::spawn(task_body);
354
355		*self.0.sleep.sleep_timer.lock() = Some(task);
356	}
357
358	pub(crate) fn cancel_sleep_timer(&self) {
359		let timer = self.0.sleep.sleep_timer.lock().take();
360		if let Some(timer) = timer {
361			timer.abort();
362		}
363	}
364
365	pub(crate) async fn wait_for_internal_keep_awake_idle(&self, deadline: Instant) -> bool {
366		self.0
367			.sleep
368			.work
369			.internal_keep_awake
370			.wait_zero(deadline)
371			.await
372	}
373
374	#[cfg(test)]
375	pub(crate) async fn wait_for_sleep_idle_window(&self, deadline: Instant) -> bool {
376		loop {
377			let activity = self.sleep_activity_notify();
378			let activity_notified = activity.notified();
379			tokio::pin!(activity_notified);
380			activity_notified.as_mut().enable();
381			let idle = self.0.sleep.work.idle_notify.notified();
382			tokio::pin!(idle);
383			idle.as_mut().enable();
384
385			if self.can_finalize_shutdown(ShutdownKind::Sleep) {
386				return true;
387			}
388
389			tokio::select! {
390				_ = &mut activity_notified => {}
391				_ = &mut idle => {}
392				_ = sleep_until(deadline) => return false,
393			}
394		}
395	}
396
397	#[cfg(test)]
398	pub(crate) async fn wait_for_shutdown_tasks(&self, deadline: Instant) -> bool {
399		loop {
400			let activity = self.sleep_activity_notify();
401			let notified = activity.notified();
402			tokio::pin!(notified);
403			notified.as_mut().enable();
404
405			let shutdown_count = self.shutdown_task_count();
406			let websocket_count = self.websocket_callback_count();
407			if shutdown_count == 0 && websocket_count == 0 {
408				return true;
409			}
410
411			tokio::select! {
412				drained = self.0.sleep.work.shutdown_counter.wait_zero(deadline), if shutdown_count > 0 => {
413					if !drained {
414						return false;
415					}
416				}
417				drained = self.0.sleep.work.websocket_callback.wait_zero(deadline), if websocket_count > 0 => {
418					if !drained {
419						return false;
420					}
421				}
422				_ = &mut notified => {}
423				_ = sleep_until(deadline) => return false,
424			}
425		}
426	}
427
428	pub async fn wait_for_tracked_shutdown_work(&self) -> bool {
429		let shutdown_deadline = self.shutdown_deadline_token();
430		tokio::select! {
431			_ = self.wait_for_tracked_shutdown_work_drained() => true,
432			_ = shutdown_deadline.cancelled() => false,
433		}
434	}
435
436	pub async fn wait_for_tracked_shutdown_work_unbounded(&self) {
437		self.wait_for_tracked_shutdown_work_drained().await;
438	}
439
440	async fn wait_for_tracked_shutdown_work_drained(&self) {
441		loop {
442			let shutdown_count = self.shutdown_task_count();
443			let websocket_count = self.websocket_callback_count();
444			if shutdown_count == 0 && websocket_count == 0 {
445				return;
446			}
447
448			tokio::select! {
449				_ = self.0.sleep.work.shutdown_counter.wait_zero_unbounded(), if shutdown_count > 0 => {}
450				_ = self.0.sleep.work.websocket_callback.wait_zero_unbounded(), if websocket_count > 0 => {}
451			}
452		}
453	}
454
455	pub(crate) async fn wait_for_http_requests_drained(&self, deadline: Instant) -> bool {
456		let Some(counter) = self.http_request_counter() else {
457			return true;
458		};
459		counter.wait_zero(deadline).await
460	}
461
462	pub(crate) async fn wait_for_http_requests_idle(&self) {
463		loop {
464			let idle = self.0.sleep.work.idle_notify.notified();
465			tokio::pin!(idle);
466			idle.as_mut().enable();
467
468			if self.active_http_request_count() == 0 {
469				return;
470			}
471
472			idle.await;
473		}
474	}
475
476	pub(crate) fn keep_awake_region_state(&self) -> RegionGuard {
477		self.0.sleep.work.keep_awake_guard()
478	}
479
480	pub(crate) fn sleep_keep_awake_count(&self) -> usize {
481		self.0.sleep.work.keep_awake.load()
482	}
483
484	pub(crate) fn internal_keep_awake_region(&self) -> RegionGuard {
485		self.0.sleep.work.internal_keep_awake_guard()
486	}
487
488	pub(crate) fn sleep_internal_keep_awake_count(&self) -> usize {
489		self.0.sleep.work.internal_keep_awake.load()
490	}
491
492	fn active_queue_wait_count(&self) -> usize {
493		self.0.active_queue_wait_count.load(Ordering::SeqCst) as usize
494	}
495
496	pub(crate) fn websocket_callback_region_state(&self) -> RegionGuard {
497		self.0.sleep.work.websocket_callback_guard()
498	}
499
500	pub(crate) fn websocket_callback_count(&self) -> usize {
501		self.0.sleep.work.websocket_callback.load()
502	}
503
504	pub(crate) fn disconnect_callback_region_state(&self) -> RegionGuard {
505		self.0.sleep.work.disconnect_callback_guard()
506	}
507
508	#[cfg(not(feature = "wasm-runtime"))]
509	pub(crate) fn spawn_work_inner<F>(&self, kind: ActorWorkKind, fut: F) -> bool
510	where
511		F: Future<Output = ()> + Send + 'static,
512	{
513		if Handle::try_current().is_err() {
514			tracing::warn!(
515				kind = kind.label(),
516				"actor work spawned without tokio runtime"
517			);
518			return false;
519		}
520
521		let policy = kind.policy();
522		if policy.aborts_at_shutdown_deadline {
523			let mut shutdown_tasks = self.0.sleep.work.shutdown_tasks.lock();
524			if self.0.sleep.work.teardown_started.load(Ordering::Acquire) {
525				tracing::warn!(
526					kind = kind.label(),
527					"actor work spawned after teardown; aborting immediately"
528				);
529				return false;
530			}
531			// Reap completed tasks so the JoinSet stays bounded to in-flight
532			// work. Without this the set is only drained at shutdown, so a
533			// long-lived actor that keeps spawning work (for example a workflow
534			// loop registering keep-awake tasks each tick) accumulates finished
535			// task handles for its entire lifetime.
536			while shutdown_tasks.try_join_next().is_some() {}
537			let region = self.begin_work_region(kind);
538			shutdown_tasks.spawn(self.build_spawned_work_task(kind, policy, region, fut));
539		} else {
540			let mut unabortable_shutdown_tasks =
541				self.0.sleep.work.unabortable_shutdown_tasks.lock();
542			if self.0.sleep.work.teardown_started.load(Ordering::Acquire) {
543				tracing::warn!(
544					kind = kind.label(),
545					"actor work spawned after teardown; aborting immediately"
546				);
547				return false;
548			}
549			while unabortable_shutdown_tasks.try_join_next().is_some() {}
550			let region = self.begin_work_region(kind);
551			unabortable_shutdown_tasks
552				.spawn(self.build_spawned_work_task(kind, policy, region, fut));
553		}
554		self.reset_sleep_timer();
555		true
556	}
557
558	#[cfg(not(feature = "wasm-runtime"))]
559	fn build_spawned_work_task<F>(
560		&self,
561		kind: ActorWorkKind,
562		policy: ActorWorkPolicy,
563		region: ActorWorkRegion,
564		fut: F,
565	) -> impl Future<Output = ()> + Send + 'static
566	where
567		F: Future<Output = ()> + Send + 'static,
568	{
569		let ctx = self.clone();
570		async move {
571			let _region = region;
572			if policy.aborts_at_shutdown_deadline {
573				let shutdown_deadline = ctx.shutdown_deadline_token();
574				tokio::select! {
575					_ = fut => {}
576					_ = shutdown_deadline.cancelled() => {
577						tracing::warn!(
578							actor_id = %ctx.actor_id(),
579							kind = kind.label(),
580							reason = "shutdown_deadline_elapsed",
581							"actor work cancelled by shutdown deadline"
582						);
583					}
584				}
585			} else {
586				fut.await;
587			}
588			ctx.reset_sleep_timer();
589		}
590		.in_current_span()
591	}
592
593	#[cfg(feature = "wasm-runtime")]
594	pub(crate) fn spawn_work_inner<F>(&self, kind: ActorWorkKind, fut: F) -> bool
595	where
596		F: Future<Output = ()> + 'static,
597	{
598		let mut local_shutdown_tasks = self.0.sleep.work.local_shutdown_tasks.lock();
599		if self.0.sleep.work.teardown_started.load(Ordering::Acquire) {
600			tracing::warn!(
601				kind = kind.label(),
602				"actor work spawned after teardown; aborting immediately"
603			);
604			return false;
605		}
606
607		// Reap finished tasks so the Vec stays bounded to in-flight work
608		// instead of only draining at shutdown.
609		local_shutdown_tasks.retain_mut(|task| matches!(task.complete_rx.try_recv(), Ok(None)));
610
611		let policy = kind.policy();
612		let region = self.begin_work_region(kind);
613		let ctx = self.clone();
614		let (complete_tx, complete_rx) = futures_oneshot::channel();
615		let (abort_handle, abort_registration) = AbortHandle::new_pair();
616		local_shutdown_tasks.push(LocalShutdownTask {
617			abort_handle,
618			complete_rx,
619			aborts_at_shutdown_deadline: policy.aborts_at_shutdown_deadline,
620		});
621		drop(local_shutdown_tasks);
622		let ctx_for_task = ctx.clone();
623		wasm_bindgen_futures::spawn_local(
624			async move {
625				let task = async move {
626					let _region = region;
627					if policy.aborts_at_shutdown_deadline {
628						let shutdown_deadline = ctx_for_task.shutdown_deadline_token();
629						tokio::select! {
630							_ = fut => {}
631							_ = shutdown_deadline.cancelled() => {
632								tracing::warn!(
633									actor_id = %ctx_for_task.actor_id(),
634									kind = kind.label(),
635									reason = "shutdown_deadline_elapsed",
636									"actor work cancelled by shutdown deadline"
637								);
638							}
639						}
640					} else {
641						fut.await;
642					}
643					let _ = complete_tx.send(());
644					ctx_for_task.reset_sleep_timer();
645				};
646				if Abortable::new(task, abort_registration).await.is_err() {
647					ctx.reset_sleep_timer();
648				}
649			}
650			.in_current_span(),
651		);
652		self.reset_sleep_timer();
653		true
654	}
655
656	#[cfg(not(feature = "wasm-runtime"))]
657	pub(crate) fn track_shutdown_task<F>(&self, fut: F) -> bool
658	where
659		F: Future<Output = ()> + Send + 'static,
660	{
661		if Handle::try_current().is_err() {
662			tracing::warn!("shutdown task spawned without tokio runtime; running fallback");
663			return false;
664		}
665
666		let mut shutdown_tasks = self.0.sleep.work.shutdown_tasks.lock();
667		if self.0.sleep.work.teardown_started.load(Ordering::Acquire) {
668			tracing::warn!("shutdown task spawned after teardown; aborting immediately");
669			return false;
670		}
671		let counter = self.0.sleep.work.shutdown_counter.clone();
672		counter.increment();
673		let guard = CountGuard::from_incremented(counter);
674		let ctx = self.clone();
675		// The guard must drop before resetting the sleep timer so the lifecycle
676		// task observes the post-completion counter state on the wakeup.
677		shutdown_tasks.spawn(
678			async move {
679				{
680					let _guard = guard;
681					fut.await;
682				}
683				ctx.reset_sleep_timer();
684			}
685			.in_current_span(),
686		);
687		drop(shutdown_tasks);
688		self.reset_sleep_timer();
689		true
690	}
691
692	#[cfg(feature = "wasm-runtime")]
693	pub(crate) fn track_shutdown_task<F>(&self, fut: F) -> bool
694	where
695		F: Future<Output = ()> + 'static,
696	{
697		let mut local_shutdown_tasks = self.0.sleep.work.local_shutdown_tasks.lock();
698		if self.0.sleep.work.teardown_started.load(Ordering::Acquire) {
699			tracing::warn!("shutdown task spawned after teardown; aborting immediately");
700			return false;
701		}
702		let counter = self.0.sleep.work.shutdown_counter.clone();
703		counter.increment();
704		let guard = CountGuard::from_incremented(counter);
705		let ctx = self.clone();
706		// Wasm tasks cannot be joined through Tokio. Store an explicit
707		// completion channel plus abort handle so teardown can either drain or
708		// cancel them using the same policy as native shutdown.
709		let (complete_tx, complete_rx) = futures_oneshot::channel();
710		let (abort_handle, abort_registration) = AbortHandle::new_pair();
711		local_shutdown_tasks.push(LocalShutdownTask {
712			abort_handle,
713			complete_rx,
714			aborts_at_shutdown_deadline: true,
715		});
716		drop(local_shutdown_tasks);
717		let ctx_for_task = ctx.clone();
718		wasm_bindgen_futures::spawn_local(
719			async move {
720				let task = async move {
721					{
722						let _guard = guard;
723						fut.await;
724					}
725					let _ = complete_tx.send(());
726					ctx_for_task.reset_sleep_timer();
727				};
728				if Abortable::new(task, abort_registration).await.is_err() {
729					ctx.reset_sleep_timer();
730				}
731			}
732			.in_current_span(),
733		);
734		self.reset_sleep_timer();
735		true
736	}
737
738	pub(crate) fn shutdown_task_count(&self) -> usize {
739		self.0.sleep.work.shutdown_counter.load()
740	}
741
742	pub(crate) fn mark_shutdown_deadline_reached(&self) {
743		self.0
744			.sleep
745			.work
746			.shutdown_deadline_reached
747			.store(true, Ordering::Release);
748	}
749
750	pub(crate) fn begin_core_dispatched_hook(&self) {
751		self.0.sleep.work.core_dispatched_hooks.increment();
752		self.reset_sleep_timer();
753	}
754
755	pub fn mark_core_dispatched_hook_completed(&self) {
756		self.0.sleep.work.core_dispatched_hooks.decrement();
757		self.reset_sleep_timer();
758	}
759
760	pub(crate) fn core_dispatched_hook_count(&self) -> usize {
761		self.0.sleep.work.core_dispatched_hooks.load()
762	}
763
764	pub(crate) async fn teardown_sleep_state(&self) {
765		let abort_remaining = self
766			.0
767			.sleep
768			.work
769			.shutdown_deadline_reached
770			.swap(false, Ordering::AcqRel);
771		// Normal shutdown drains tracked work. Once the grace deadline fires,
772		// teardown switches to cancellation so a stuck waitUntil cannot hold the
773		// actor instance forever.
774		if abort_remaining {
775			self.0
776				.sleep
777				.work
778				.teardown_started
779				.store(true, Ordering::Release);
780		}
781
782		#[cfg(feature = "wasm-runtime")]
783		{
784			loop {
785				let local_shutdown_tasks = {
786					let mut guard = self.0.sleep.work.local_shutdown_tasks.lock();
787					let taken = std::mem::take(&mut *guard);
788					if taken.is_empty() {
789						self.0
790							.sleep
791							.work
792							.teardown_started
793							.store(true, Ordering::Release);
794						return;
795					}
796					taken
797				};
798
799				if abort_remaining {
800					for task in local_shutdown_tasks {
801						if task.aborts_at_shutdown_deadline {
802							task.abort_handle.abort();
803						}
804						if task.complete_rx.await.is_err() {
805							tracing::debug!("aborted shutdown task during teardown");
806						}
807					}
808					self.0
809						.sleep
810						.work
811						.teardown_started
812						.store(true, Ordering::Release);
813					return;
814				}
815
816				for task in local_shutdown_tasks {
817					if task.complete_rx.await.is_err() {
818						tracing::debug!("shutdown task completion dropped during teardown");
819					}
820				}
821			}
822		}
823
824		#[cfg(not(feature = "wasm-runtime"))]
825		loop {
826			let mut abortable_shutdown_tasks = {
827				let mut guard = self.0.sleep.work.shutdown_tasks.lock();
828				let taken = std::mem::take(&mut *guard);
829				let mut unabortable_guard = self.0.sleep.work.unabortable_shutdown_tasks.lock();
830				let unabortable_taken = std::mem::take(&mut *unabortable_guard);
831				if taken.is_empty() && unabortable_taken.is_empty() {
832					self.0
833						.sleep
834						.work
835						.teardown_started
836						.store(true, Ordering::Release);
837					return;
838				}
839				(taken, unabortable_taken)
840			};
841
842			abortable_shutdown_tasks.0.shutdown().await;
843			while let Some(result) = abortable_shutdown_tasks.0.join_next().await {
844				if let Err(error) = result
845					&& !error.is_cancelled()
846				{
847					tracing::error!(?error, "shutdown task join failed during teardown");
848				}
849			}
850			while let Some(result) = abortable_shutdown_tasks.1.join_next().await {
851				if let Err(error) = result
852					&& !error.is_cancelled()
853				{
854					tracing::error!(?error, "shutdown task join failed during teardown");
855				}
856			}
857		}
858	}
859
860	pub(crate) fn sleep_state_config(&self) -> ActorConfig {
861		self.0.sleep.config.lock().clone()
862	}
863
864	pub(crate) fn active_http_request_count(&self) -> usize {
865		self.http_request_counter()
866			.map(|counter| counter.load())
867			.unwrap_or(0)
868	}
869
870	pub(crate) fn sleep_activity_notify(&self) -> Arc<Notify> {
871		self.0.sleep.work.activity_notify.clone()
872	}
873
874	fn http_request_counter(&self) -> Option<Arc<AsyncCounter>> {
875		if let Some(counter) = self.0.sleep.http_request_counter.lock().clone() {
876			return Some(counter);
877		}
878
879		let counter = self.lookup_http_request_counter(self.actor_id())?;
880		*self.0.sleep.http_request_counter.lock() = Some(counter.clone());
881		Some(counter)
882	}
883
884	fn lookup_http_request_counter(&self, actor_id: &str) -> Option<Arc<AsyncCounter>> {
885		let envoy_handle = self.0.sleep.envoy_handle.lock().clone();
886		let generation = *self.0.sleep.generation.lock();
887		let envoy_handle = envoy_handle?;
888		let counter = envoy_handle.http_request_counter(actor_id, generation)?;
889		counter.register_zero_notify(&self.0.sleep.work.idle_notify);
890		// The HTTP counter is owned by envoy-client, so neither increment nor
891		// decrement goes through a rivetkit-core guard. Hook every transition
892		// into the sleep activity notify so the sleep deadline gets
893		// re-evaluated when a request starts or completes.
894		let ctx = self.clone();
895		counter.register_change_callback(Arc::new(move || {
896			ctx.0
897				.metrics
898				.set_http_requests_active(ctx.active_http_request_count());
899			ctx.reset_sleep_timer();
900		}));
901		self.0.metrics.set_http_requests_active(counter.load());
902		Some(counter)
903	}
904}
905
906// Test shim keeps moved tests in crate-root tests/ with private-module access.
907#[cfg(test)]
908#[path = "../../tests/sleep.rs"]
909mod tests;