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	#[doc(hidden)]
230	pub fn run_handler_active(&self) -> bool {
231		self.0.sleep.run_handler_active_count.load(Ordering::SeqCst) > 0
232	}
233
234	#[cfg(test)]
235	pub(crate) fn sleep_request_count(&self) -> usize {
236		self.0.sleep.sleep_request_count.load(Ordering::SeqCst)
237	}
238
239	pub(crate) async fn can_arm_sleep_timer(&self) -> CanSleep {
240		let config = self.sleep_state_config();
241		if !self.0.sleep.lifecycle_started.load(Ordering::SeqCst) {
242			return CanSleep::NotReady;
243		}
244		if config.no_sleep {
245			return CanSleep::NoSleep;
246		}
247		if self.active_http_request_count() > 0 {
248			return CanSleep::ActiveHttpRequests;
249		}
250		if self.sleep_keep_awake_count() > 0 {
251			return CanSleep::ActiveKeepAwake;
252		}
253		if self.sleep_internal_keep_awake_count() > 0 {
254			return CanSleep::ActiveInternalKeepAwake;
255		}
256		// Queue receives are sleep-compatible: sleep aborts the wait via the
257		// actor abort token, then the next generation restarts the run loop.
258		if self.run_handler_active() && self.active_queue_wait_count() == 0 {
259			return CanSleep::ActiveRunHandler;
260		}
261		if self.pending_disconnect_count() > 0 {
262			return CanSleep::ActiveDisconnectCallbacks;
263		}
264		if !self.conns().is_empty() {
265			return CanSleep::ActiveConnections;
266		}
267		if self.websocket_callback_count() > 0 {
268			return CanSleep::ActiveWebSocketCallbacks;
269		}
270
271		CanSleep::Yes
272	}
273
274	pub(crate) fn can_finalize_shutdown(&self, reason: ShutdownKind) -> bool {
275		self.0.sleep.work.core_dispatched_hooks.load() == 0
276			// Sleep is a reversible pause, so let the actor run handler finish
277			// naturally during grace. Destroy is terminal and may cut through it.
278			&& (matches!(reason, ShutdownKind::Destroy) || !self.run_handler_active())
279			&& self.shutdown_task_count() == 0
280			&& self.sleep_keep_awake_count() == 0
281			&& self.sleep_internal_keep_awake_count() == 0
282			&& self.active_http_request_count() == 0
283			&& self.websocket_callback_count() == 0
284			&& self.pending_disconnect_count() == 0
285	}
286
287	/// Spawn the fallback sleep timer used by `ActorContext`s that are not
288	/// bound to an `ActorTask`.
289	///
290	/// This path only engages when `configure_lifecycle_events` has not been
291	/// wired, which in practice means test contexts. Production actors built
292	/// through the registry always have an `ActorTask` and never spawn this
293	/// detached timer.
294	pub(crate) fn reset_sleep_timer_state(&self) {
295		self.cancel_sleep_timer();
296
297		#[cfg(not(feature = "wasm-runtime"))]
298		let Ok(runtime) = Handle::try_current() else {
299			tracing::debug!(
300				actor_id = %self.actor_id(),
301				"sleep activity reset skipped without tokio runtime"
302			);
303			return;
304		};
305
306		tracing::debug!(
307			actor_id = %self.actor_id(),
308			sleep_timeout_ms = self.0.sleep.config.lock().sleep_timeout.as_millis() as u64,
309			"sleep activity reset"
310		);
311
312		let ctx = self.clone();
313		let task_body = async move {
314			let can_sleep = ctx.can_sleep().await;
315			if can_sleep != CanSleep::Yes {
316				tracing::debug!(
317					actor_id = %ctx.actor_id(),
318					reason = ?can_sleep,
319					"sleep idle timer skipped"
320				);
321				return;
322			}
323
324			let timeout = ctx.sleep_config().sleep_timeout;
325			sleep(timeout).await;
326
327			let can_sleep = ctx.can_sleep().await;
328			if can_sleep == CanSleep::Yes {
329				tracing::debug!(
330					actor_id = %ctx.actor_id(),
331					sleep_timeout_ms = timeout.as_millis() as u64,
332					"sleep idle timer elapsed"
333				);
334				if let Err(err) = ctx.sleep() {
335					tracing::debug!(
336						actor_id = %ctx.actor_id(),
337						?err,
338						"sleep idle timer request suppressed"
339					);
340				}
341			} else {
342				tracing::warn!(
343					actor_id = %ctx.actor_id(),
344					reason = ?can_sleep,
345					"sleep idle timer elapsed but actor stayed awake"
346				);
347			}
348		};
349
350		#[cfg(not(feature = "wasm-runtime"))]
351		let task = runtime.spawn(task_body);
352
353		#[cfg(feature = "wasm-runtime")]
354		let task = RuntimeSpawner::spawn(task_body);
355
356		*self.0.sleep.sleep_timer.lock() = Some(task);
357	}
358
359	pub(crate) fn cancel_sleep_timer(&self) {
360		let timer = self.0.sleep.sleep_timer.lock().take();
361		if let Some(timer) = timer {
362			timer.abort();
363		}
364	}
365
366	pub(crate) async fn wait_for_internal_keep_awake_idle(&self, deadline: Instant) -> bool {
367		self.0
368			.sleep
369			.work
370			.internal_keep_awake
371			.wait_zero(deadline)
372			.await
373	}
374
375	#[cfg(test)]
376	pub(crate) async fn wait_for_sleep_idle_window(&self, deadline: Instant) -> bool {
377		loop {
378			let activity = self.sleep_activity_notify();
379			let activity_notified = activity.notified();
380			tokio::pin!(activity_notified);
381			activity_notified.as_mut().enable();
382			let idle = self.0.sleep.work.idle_notify.notified();
383			tokio::pin!(idle);
384			idle.as_mut().enable();
385
386			if self.can_finalize_shutdown(ShutdownKind::Sleep) {
387				return true;
388			}
389
390			tokio::select! {
391				_ = &mut activity_notified => {}
392				_ = &mut idle => {}
393				_ = sleep_until(deadline) => return false,
394			}
395		}
396	}
397
398	#[cfg(test)]
399	pub(crate) async fn wait_for_shutdown_tasks(&self, deadline: Instant) -> bool {
400		loop {
401			let activity = self.sleep_activity_notify();
402			let notified = activity.notified();
403			tokio::pin!(notified);
404			notified.as_mut().enable();
405
406			let shutdown_count = self.shutdown_task_count();
407			let websocket_count = self.websocket_callback_count();
408			if shutdown_count == 0 && websocket_count == 0 {
409				return true;
410			}
411
412			tokio::select! {
413				drained = self.0.sleep.work.shutdown_counter.wait_zero(deadline), if shutdown_count > 0 => {
414					if !drained {
415						return false;
416					}
417				}
418				drained = self.0.sleep.work.websocket_callback.wait_zero(deadline), if websocket_count > 0 => {
419					if !drained {
420						return false;
421					}
422				}
423				_ = &mut notified => {}
424				_ = sleep_until(deadline) => return false,
425			}
426		}
427	}
428
429	pub async fn wait_for_tracked_shutdown_work(&self) -> bool {
430		let shutdown_deadline = self.shutdown_deadline_token();
431		tokio::select! {
432			_ = self.wait_for_tracked_shutdown_work_drained() => true,
433			_ = shutdown_deadline.cancelled() => false,
434		}
435	}
436
437	pub async fn wait_for_tracked_shutdown_work_unbounded(&self) {
438		self.wait_for_tracked_shutdown_work_drained().await;
439	}
440
441	async fn wait_for_tracked_shutdown_work_drained(&self) {
442		loop {
443			let shutdown_count = self.shutdown_task_count();
444			let websocket_count = self.websocket_callback_count();
445			if shutdown_count == 0 && websocket_count == 0 {
446				return;
447			}
448
449			tokio::select! {
450				_ = self.0.sleep.work.shutdown_counter.wait_zero_unbounded(), if shutdown_count > 0 => {}
451				_ = self.0.sleep.work.websocket_callback.wait_zero_unbounded(), if websocket_count > 0 => {}
452			}
453		}
454	}
455
456	pub(crate) async fn wait_for_http_requests_drained(&self, deadline: Instant) -> bool {
457		let Some(counter) = self.http_request_counter() else {
458			return true;
459		};
460		counter.wait_zero(deadline).await
461	}
462
463	pub(crate) async fn wait_for_http_requests_idle(&self) {
464		loop {
465			let idle = self.0.sleep.work.idle_notify.notified();
466			tokio::pin!(idle);
467			idle.as_mut().enable();
468
469			if self.active_http_request_count() == 0 {
470				return;
471			}
472
473			idle.await;
474		}
475	}
476
477	pub(crate) fn keep_awake_region_state(&self) -> RegionGuard {
478		self.0.sleep.work.keep_awake_guard()
479	}
480
481	pub(crate) fn sleep_keep_awake_count(&self) -> usize {
482		self.0.sleep.work.keep_awake.load()
483	}
484
485	pub(crate) fn internal_keep_awake_region(&self) -> RegionGuard {
486		self.0.sleep.work.internal_keep_awake_guard()
487	}
488
489	pub(crate) fn sleep_internal_keep_awake_count(&self) -> usize {
490		self.0.sleep.work.internal_keep_awake.load()
491	}
492
493	fn active_queue_wait_count(&self) -> usize {
494		self.0.active_queue_wait_count.load(Ordering::SeqCst) as usize
495	}
496
497	pub(crate) fn websocket_callback_region_state(&self) -> RegionGuard {
498		self.0.sleep.work.websocket_callback_guard()
499	}
500
501	pub(crate) fn websocket_callback_count(&self) -> usize {
502		self.0.sleep.work.websocket_callback.load()
503	}
504
505	pub(crate) fn disconnect_callback_region_state(&self) -> RegionGuard {
506		self.0.sleep.work.disconnect_callback_guard()
507	}
508
509	#[cfg(not(feature = "wasm-runtime"))]
510	pub(crate) fn spawn_work_inner<F>(&self, kind: ActorWorkKind, fut: F) -> bool
511	where
512		F: Future<Output = ()> + Send + 'static,
513	{
514		if Handle::try_current().is_err() {
515			tracing::warn!(
516				kind = kind.label(),
517				"actor work spawned without tokio runtime"
518			);
519			return false;
520		}
521
522		let policy = kind.policy();
523		if policy.aborts_at_shutdown_deadline {
524			let mut shutdown_tasks = self.0.sleep.work.shutdown_tasks.lock();
525			if self.0.sleep.work.teardown_started.load(Ordering::Acquire) {
526				tracing::warn!(
527					kind = kind.label(),
528					"actor work spawned after teardown; aborting immediately"
529				);
530				return false;
531			}
532			// Reap completed tasks so the JoinSet stays bounded to in-flight
533			// work. Without this the set is only drained at shutdown, so a
534			// long-lived actor that keeps spawning work (for example a workflow
535			// loop registering keep-awake tasks each tick) accumulates finished
536			// task handles for its entire lifetime.
537			while shutdown_tasks.try_join_next().is_some() {}
538			let region = self.begin_work_region(kind);
539			shutdown_tasks.spawn(self.build_spawned_work_task(kind, policy, region, fut));
540		} else {
541			let mut unabortable_shutdown_tasks =
542				self.0.sleep.work.unabortable_shutdown_tasks.lock();
543			if self.0.sleep.work.teardown_started.load(Ordering::Acquire) {
544				tracing::warn!(
545					kind = kind.label(),
546					"actor work spawned after teardown; aborting immediately"
547				);
548				return false;
549			}
550			while unabortable_shutdown_tasks.try_join_next().is_some() {}
551			let region = self.begin_work_region(kind);
552			unabortable_shutdown_tasks
553				.spawn(self.build_spawned_work_task(kind, policy, region, fut));
554		}
555		self.reset_sleep_timer();
556		true
557	}
558
559	#[cfg(not(feature = "wasm-runtime"))]
560	fn build_spawned_work_task<F>(
561		&self,
562		kind: ActorWorkKind,
563		policy: ActorWorkPolicy,
564		region: ActorWorkRegion,
565		fut: F,
566	) -> impl Future<Output = ()> + Send + 'static
567	where
568		F: Future<Output = ()> + Send + 'static,
569	{
570		let ctx = self.clone();
571		async move {
572			let _region = region;
573			if policy.aborts_at_shutdown_deadline {
574				let shutdown_deadline = ctx.shutdown_deadline_token();
575				tokio::select! {
576					_ = fut => {}
577					_ = shutdown_deadline.cancelled() => {
578						tracing::warn!(
579							actor_id = %ctx.actor_id(),
580							kind = kind.label(),
581							reason = "shutdown_deadline_elapsed",
582							"actor work cancelled by shutdown deadline"
583						);
584					}
585				}
586			} else {
587				fut.await;
588			}
589			ctx.reset_sleep_timer();
590		}
591		.in_current_span()
592	}
593
594	#[cfg(feature = "wasm-runtime")]
595	pub(crate) fn spawn_work_inner<F>(&self, kind: ActorWorkKind, fut: F) -> bool
596	where
597		F: Future<Output = ()> + 'static,
598	{
599		let mut local_shutdown_tasks = self.0.sleep.work.local_shutdown_tasks.lock();
600		if self.0.sleep.work.teardown_started.load(Ordering::Acquire) {
601			tracing::warn!(
602				kind = kind.label(),
603				"actor work spawned after teardown; aborting immediately"
604			);
605			return false;
606		}
607
608		// Reap finished tasks so the Vec stays bounded to in-flight work
609		// instead of only draining at shutdown.
610		local_shutdown_tasks.retain_mut(|task| matches!(task.complete_rx.try_recv(), Ok(None)));
611
612		let policy = kind.policy();
613		let region = self.begin_work_region(kind);
614		let ctx = self.clone();
615		let (complete_tx, complete_rx) = futures_oneshot::channel();
616		let (abort_handle, abort_registration) = AbortHandle::new_pair();
617		local_shutdown_tasks.push(LocalShutdownTask {
618			abort_handle,
619			complete_rx,
620			aborts_at_shutdown_deadline: policy.aborts_at_shutdown_deadline,
621		});
622		drop(local_shutdown_tasks);
623		let ctx_for_task = ctx.clone();
624		wasm_bindgen_futures::spawn_local(
625			async move {
626				let task = async move {
627					let _region = region;
628					if policy.aborts_at_shutdown_deadline {
629						let shutdown_deadline = ctx_for_task.shutdown_deadline_token();
630						tokio::select! {
631							_ = fut => {}
632							_ = shutdown_deadline.cancelled() => {
633								tracing::warn!(
634									actor_id = %ctx_for_task.actor_id(),
635									kind = kind.label(),
636									reason = "shutdown_deadline_elapsed",
637									"actor work cancelled by shutdown deadline"
638								);
639							}
640						}
641					} else {
642						fut.await;
643					}
644					let _ = complete_tx.send(());
645					ctx_for_task.reset_sleep_timer();
646				};
647				if Abortable::new(task, abort_registration).await.is_err() {
648					ctx.reset_sleep_timer();
649				}
650			}
651			.in_current_span(),
652		);
653		self.reset_sleep_timer();
654		true
655	}
656
657	#[cfg(not(feature = "wasm-runtime"))]
658	pub(crate) fn track_shutdown_task<F>(&self, fut: F) -> bool
659	where
660		F: Future<Output = ()> + Send + 'static,
661	{
662		if Handle::try_current().is_err() {
663			tracing::warn!("shutdown task spawned without tokio runtime; running fallback");
664			return false;
665		}
666
667		let mut shutdown_tasks = self.0.sleep.work.shutdown_tasks.lock();
668		if self.0.sleep.work.teardown_started.load(Ordering::Acquire) {
669			tracing::warn!("shutdown task spawned after teardown; aborting immediately");
670			return false;
671		}
672		let counter = self.0.sleep.work.shutdown_counter.clone();
673		counter.increment();
674		let guard = CountGuard::from_incremented(counter);
675		let ctx = self.clone();
676		// The guard must drop before resetting the sleep timer so the lifecycle
677		// task observes the post-completion counter state on the wakeup.
678		shutdown_tasks.spawn(
679			async move {
680				{
681					let _guard = guard;
682					fut.await;
683				}
684				ctx.reset_sleep_timer();
685			}
686			.in_current_span(),
687		);
688		drop(shutdown_tasks);
689		self.reset_sleep_timer();
690		true
691	}
692
693	#[cfg(feature = "wasm-runtime")]
694	pub(crate) fn track_shutdown_task<F>(&self, fut: F) -> bool
695	where
696		F: Future<Output = ()> + 'static,
697	{
698		let mut local_shutdown_tasks = self.0.sleep.work.local_shutdown_tasks.lock();
699		if self.0.sleep.work.teardown_started.load(Ordering::Acquire) {
700			tracing::warn!("shutdown task spawned after teardown; aborting immediately");
701			return false;
702		}
703		let counter = self.0.sleep.work.shutdown_counter.clone();
704		counter.increment();
705		let guard = CountGuard::from_incremented(counter);
706		let ctx = self.clone();
707		// Wasm tasks cannot be joined through Tokio. Store an explicit
708		// completion channel plus abort handle so teardown can either drain or
709		// cancel them using the same policy as native shutdown.
710		let (complete_tx, complete_rx) = futures_oneshot::channel();
711		let (abort_handle, abort_registration) = AbortHandle::new_pair();
712		local_shutdown_tasks.push(LocalShutdownTask {
713			abort_handle,
714			complete_rx,
715			aborts_at_shutdown_deadline: true,
716		});
717		drop(local_shutdown_tasks);
718		let ctx_for_task = ctx.clone();
719		wasm_bindgen_futures::spawn_local(
720			async move {
721				let task = async move {
722					{
723						let _guard = guard;
724						fut.await;
725					}
726					let _ = complete_tx.send(());
727					ctx_for_task.reset_sleep_timer();
728				};
729				if Abortable::new(task, abort_registration).await.is_err() {
730					ctx.reset_sleep_timer();
731				}
732			}
733			.in_current_span(),
734		);
735		self.reset_sleep_timer();
736		true
737	}
738
739	pub(crate) fn shutdown_task_count(&self) -> usize {
740		self.0.sleep.work.shutdown_counter.load()
741	}
742
743	pub(crate) fn mark_shutdown_deadline_reached(&self) {
744		self.0
745			.sleep
746			.work
747			.shutdown_deadline_reached
748			.store(true, Ordering::Release);
749	}
750
751	pub(crate) fn begin_core_dispatched_hook(&self) {
752		self.0.sleep.work.core_dispatched_hooks.increment();
753		self.reset_sleep_timer();
754	}
755
756	pub fn mark_core_dispatched_hook_completed(&self) {
757		self.0.sleep.work.core_dispatched_hooks.decrement();
758		self.reset_sleep_timer();
759	}
760
761	pub(crate) fn core_dispatched_hook_count(&self) -> usize {
762		self.0.sleep.work.core_dispatched_hooks.load()
763	}
764
765	pub(crate) async fn teardown_sleep_state(&self) {
766		let abort_remaining = self
767			.0
768			.sleep
769			.work
770			.shutdown_deadline_reached
771			.swap(false, Ordering::AcqRel);
772		// Normal shutdown drains tracked work. Once the grace deadline fires,
773		// teardown switches to cancellation so a stuck waitUntil cannot hold the
774		// actor instance forever.
775		if abort_remaining {
776			self.0
777				.sleep
778				.work
779				.teardown_started
780				.store(true, Ordering::Release);
781		}
782
783		#[cfg(feature = "wasm-runtime")]
784		{
785			loop {
786				let local_shutdown_tasks = {
787					let mut guard = self.0.sleep.work.local_shutdown_tasks.lock();
788					let taken = std::mem::take(&mut *guard);
789					if taken.is_empty() {
790						self.0
791							.sleep
792							.work
793							.teardown_started
794							.store(true, Ordering::Release);
795						return;
796					}
797					taken
798				};
799
800				if abort_remaining {
801					for task in local_shutdown_tasks {
802						if task.aborts_at_shutdown_deadline {
803							task.abort_handle.abort();
804						}
805						if task.complete_rx.await.is_err() {
806							tracing::debug!("aborted shutdown task during teardown");
807						}
808					}
809					self.0
810						.sleep
811						.work
812						.teardown_started
813						.store(true, Ordering::Release);
814					return;
815				}
816
817				for task in local_shutdown_tasks {
818					if task.complete_rx.await.is_err() {
819						tracing::debug!("shutdown task completion dropped during teardown");
820					}
821				}
822			}
823		}
824
825		#[cfg(not(feature = "wasm-runtime"))]
826		loop {
827			let mut abortable_shutdown_tasks = {
828				let mut guard = self.0.sleep.work.shutdown_tasks.lock();
829				let taken = std::mem::take(&mut *guard);
830				let mut unabortable_guard = self.0.sleep.work.unabortable_shutdown_tasks.lock();
831				let unabortable_taken = std::mem::take(&mut *unabortable_guard);
832				if taken.is_empty() && unabortable_taken.is_empty() {
833					self.0
834						.sleep
835						.work
836						.teardown_started
837						.store(true, Ordering::Release);
838					return;
839				}
840				(taken, unabortable_taken)
841			};
842
843			abortable_shutdown_tasks.0.shutdown().await;
844			while let Some(result) = abortable_shutdown_tasks.0.join_next().await {
845				if let Err(error) = result
846					&& !error.is_cancelled()
847				{
848					tracing::error!(?error, "shutdown task join failed during teardown");
849				}
850			}
851			while let Some(result) = abortable_shutdown_tasks.1.join_next().await {
852				if let Err(error) = result
853					&& !error.is_cancelled()
854				{
855					tracing::error!(?error, "shutdown task join failed during teardown");
856				}
857			}
858		}
859	}
860
861	pub(crate) fn sleep_state_config(&self) -> ActorConfig {
862		self.0.sleep.config.lock().clone()
863	}
864
865	pub(crate) fn active_http_request_count(&self) -> usize {
866		self.http_request_counter()
867			.map(|counter| counter.load())
868			.unwrap_or(0)
869	}
870
871	pub(crate) fn sleep_activity_notify(&self) -> Arc<Notify> {
872		self.0.sleep.work.activity_notify.clone()
873	}
874
875	fn http_request_counter(&self) -> Option<Arc<AsyncCounter>> {
876		if let Some(counter) = self.0.sleep.http_request_counter.lock().clone() {
877			return Some(counter);
878		}
879
880		let counter = self.lookup_http_request_counter(self.actor_id())?;
881		*self.0.sleep.http_request_counter.lock() = Some(counter.clone());
882		Some(counter)
883	}
884
885	fn lookup_http_request_counter(&self, actor_id: &str) -> Option<Arc<AsyncCounter>> {
886		let envoy_handle = self.0.sleep.envoy_handle.lock().clone();
887		let generation = *self.0.sleep.generation.lock();
888		let envoy_handle = envoy_handle?;
889		let counter = envoy_handle.http_request_counter(actor_id, generation)?;
890		counter.register_zero_notify(&self.0.sleep.work.idle_notify);
891		// The HTTP counter is owned by envoy-client, so neither increment nor
892		// decrement goes through a rivetkit-core guard. Hook every transition
893		// into the sleep activity notify so the sleep deadline gets
894		// re-evaluated when a request starts or completes.
895		let ctx = self.clone();
896		counter.register_change_callback(Arc::new(move || {
897			ctx.0
898				.metrics
899				.set_http_requests_active(ctx.active_http_request_count());
900			ctx.reset_sleep_timer();
901		}));
902		self.0.metrics.set_http_requests_active(counter.load());
903		Some(counter)
904	}
905}
906
907// Test shim keeps moved tests in crate-root tests/ with private-module access.
908#[cfg(test)]
909#[path = "../../tests/sleep.rs"]
910mod tests;