Skip to main content

rivetkit_core/actor/
work_registry.rs

1use std::sync::Arc;
2use std::sync::atomic::AtomicBool;
3
4#[cfg(feature = "wasm-runtime")]
5use futures::channel::oneshot as futures_oneshot;
6#[cfg(feature = "wasm-runtime")]
7use futures::future::AbortHandle;
8use parking_lot::Mutex;
9use rivet_envoy_client::async_counter::AsyncCounter;
10use tokio::sync::Notify;
11use tokio::task::JoinSet;
12
13use crate::actor::task_types::UserTaskKind;
14
15/// Classifies actor work so sleep can apply one policy model across different APIs.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum ActorWorkKind {
18	/// Dispatched action work awaiting its reply from the runtime adapter.
19	Action,
20	/// User work that keeps the actor out of idle sleep while it runs.
21	KeepAwake,
22	/// Runtime-owned work that should behave like keep-awake without exposing a user API.
23	InternalKeepAwake,
24	/// User work that may continue into sleep grace but should not block idle sleep.
25	WaitUntil,
26	/// Detached runtime task that drains during shutdown.
27	RegisteredTask,
28	/// Async WebSocket callback work that must hold sleep while a callback is running.
29	WebSocketCallback,
30	/// Disconnect callback work that must finish before sleep or destroy finalizes.
31	DisconnectCallback,
32}
33
34/// Defines how a work kind participates in idle sleep and shutdown grace.
35#[derive(Debug, Clone, Copy)]
36pub struct ActorWorkPolicy {
37	/// True when active work should prevent the actor from entering idle sleep.
38	pub blocks_idle_sleep: bool,
39	/// True when active work should delay sleep-grace runtime cleanup.
40	pub drains_shutdown_grace: bool,
41	/// True when detached work should be cancelled after the shutdown deadline.
42	pub aborts_at_shutdown_deadline: bool,
43	/// User-facing task kind used for metrics and lifecycle diagnostics.
44	pub user_task_kind: Option<UserTaskKind>,
45}
46
47impl ActorWorkKind {
48	/// Returns the lifecycle policy owned by this work kind.
49	pub fn policy(self) -> ActorWorkPolicy {
50		match self {
51			ActorWorkKind::Action => ActorWorkPolicy {
52				blocks_idle_sleep: true,
53				drains_shutdown_grace: true,
54				aborts_at_shutdown_deadline: true,
55				user_task_kind: Some(UserTaskKind::Action),
56			},
57			ActorWorkKind::KeepAwake => ActorWorkPolicy {
58				blocks_idle_sleep: true,
59				drains_shutdown_grace: true,
60				aborts_at_shutdown_deadline: true,
61				user_task_kind: None,
62			},
63			ActorWorkKind::InternalKeepAwake => ActorWorkPolicy {
64				blocks_idle_sleep: true,
65				drains_shutdown_grace: true,
66				aborts_at_shutdown_deadline: false,
67				user_task_kind: None,
68			},
69			ActorWorkKind::WaitUntil => ActorWorkPolicy {
70				blocks_idle_sleep: false,
71				drains_shutdown_grace: true,
72				aborts_at_shutdown_deadline: true,
73				user_task_kind: Some(UserTaskKind::WaitUntil),
74			},
75			ActorWorkKind::RegisteredTask => ActorWorkPolicy {
76				blocks_idle_sleep: false,
77				drains_shutdown_grace: true,
78				aborts_at_shutdown_deadline: true,
79				user_task_kind: Some(UserTaskKind::RegisteredTask),
80			},
81			ActorWorkKind::WebSocketCallback => ActorWorkPolicy {
82				blocks_idle_sleep: true,
83				drains_shutdown_grace: true,
84				aborts_at_shutdown_deadline: true,
85				user_task_kind: Some(UserTaskKind::WebSocketCallback),
86			},
87			ActorWorkKind::DisconnectCallback => ActorWorkPolicy {
88				blocks_idle_sleep: true,
89				drains_shutdown_grace: true,
90				aborts_at_shutdown_deadline: true,
91				user_task_kind: Some(UserTaskKind::DisconnectCallback),
92			},
93		}
94	}
95
96	/// Returns a stable label for logs and metric fields.
97	pub(crate) fn label(self) -> &'static str {
98		match self {
99			ActorWorkKind::Action => "action",
100			ActorWorkKind::KeepAwake => "keep_awake",
101			ActorWorkKind::InternalKeepAwake => "internal_keep_awake",
102			ActorWorkKind::WaitUntil => "wait_until",
103			ActorWorkKind::RegisteredTask => "registered_task",
104			ActorWorkKind::WebSocketCallback => "websocket_callback",
105			ActorWorkKind::DisconnectCallback => "disconnect_callback",
106		}
107	}
108}
109
110/// Holds per-kind counters and task sets used by actor sleep and shutdown.
111pub(crate) struct WorkRegistry {
112	/// Counts user keep-awake regions that block idle sleep.
113	pub(crate) keep_awake: Arc<AsyncCounter>,
114	/// Counts runtime-owned keep-awake regions that block idle sleep.
115	pub(crate) internal_keep_awake: Arc<AsyncCounter>,
116	/// Counts async WebSocket callbacks currently running.
117	pub(crate) websocket_callback: Arc<AsyncCounter>,
118	/// Counts disconnect callbacks currently running.
119	pub(crate) disconnect_callback: Arc<AsyncCounter>,
120	/// Counts work that must drain before sleep-grace runtime cleanup.
121	pub(crate) shutdown_counter: Arc<AsyncCounter>,
122	/// Counts lifecycle hooks dispatched by core into the actor runtime.
123	pub(crate) core_dispatched_hooks: Arc<AsyncCounter>,
124	// Forced-sync: shutdown tasks are inserted from sync paths and moved out
125	// before awaiting shutdown.
126	/// Detached shutdown work that can be aborted during final teardown.
127	pub(crate) shutdown_tasks: Mutex<JoinSet<()>>,
128	/// Detached shutdown work that must be joined even after the grace deadline.
129	pub(crate) unabortable_shutdown_tasks: Mutex<JoinSet<()>>,
130	#[cfg(feature = "wasm-runtime")]
131	/// Wasm-local shutdown tasks tracked by completion channel and abort handle.
132	pub(crate) local_shutdown_tasks: Mutex<Vec<LocalShutdownTask>>,
133	/// Wakes sleep waiters when core-owned idle blockers reach zero.
134	pub(crate) idle_notify: Arc<Notify>,
135	/// Woken on every transition of a sleep-affecting counter that is not
136	/// otherwise guarded by `ActorWorkRegion`. In practice this covers
137	/// externally-owned counters like the envoy HTTP request counter whose
138	/// increments happen outside rivetkit-core.
139	pub(crate) activity_notify: Arc<Notify>,
140	/// Set once final teardown starts so new detached work is refused.
141	pub(crate) teardown_started: AtomicBool,
142	/// Set when the grace deadline has elapsed and abortable work should be cancelled.
143	pub(crate) shutdown_deadline_reached: AtomicBool,
144}
145
146#[cfg(feature = "wasm-runtime")]
147pub(crate) struct LocalShutdownTask {
148	pub(crate) abort_handle: AbortHandle,
149	pub(crate) complete_rx: futures_oneshot::Receiver<()>,
150	pub(crate) aborts_at_shutdown_deadline: bool,
151}
152
153impl WorkRegistry {
154	/// Creates an empty registry and wires idle notifications for idle-blocking counters.
155	pub(crate) fn new() -> Self {
156		let idle_notify = Arc::new(Notify::new());
157		let keep_awake = Arc::new(AsyncCounter::new());
158		keep_awake.register_zero_notify(&idle_notify);
159		let internal_keep_awake = Arc::new(AsyncCounter::new());
160		internal_keep_awake.register_zero_notify(&idle_notify);
161		let websocket_callback = Arc::new(AsyncCounter::new());
162		websocket_callback.register_zero_notify(&idle_notify);
163		let disconnect_callback = Arc::new(AsyncCounter::new());
164		disconnect_callback.register_zero_notify(&idle_notify);
165
166		Self {
167			keep_awake,
168			internal_keep_awake,
169			websocket_callback,
170			disconnect_callback,
171			shutdown_counter: Arc::new(AsyncCounter::new()),
172			core_dispatched_hooks: Arc::new(AsyncCounter::new()),
173			shutdown_tasks: Mutex::new(JoinSet::new()),
174			unabortable_shutdown_tasks: Mutex::new(JoinSet::new()),
175			#[cfg(feature = "wasm-runtime")]
176			local_shutdown_tasks: Mutex::new(Vec::new()),
177			idle_notify,
178			activity_notify: Arc::new(Notify::new()),
179			teardown_started: AtomicBool::new(false),
180			shutdown_deadline_reached: AtomicBool::new(false),
181		}
182	}
183
184	/// Starts a user keep-awake region.
185	pub(crate) fn keep_awake_guard(&self) -> RegionGuard {
186		RegionGuard::new(self.keep_awake.clone())
187	}
188
189	/// Starts a runtime-owned keep-awake region.
190	pub(crate) fn internal_keep_awake_guard(&self) -> RegionGuard {
191		RegionGuard::new(self.internal_keep_awake.clone())
192	}
193
194	/// Starts an async WebSocket callback region.
195	pub(crate) fn websocket_callback_guard(&self) -> RegionGuard {
196		RegionGuard::new(self.websocket_callback.clone())
197	}
198
199	/// Starts a disconnect callback region.
200	pub(crate) fn disconnect_callback_guard(&self) -> RegionGuard {
201		RegionGuard::new(self.disconnect_callback.clone())
202	}
203}
204
205impl Default for WorkRegistry {
206	fn default() -> Self {
207		Self::new()
208	}
209}
210
211/// RAII guard that decrements an actor work counter when dropped.
212pub(crate) struct RegionGuard {
213	counter: Arc<AsyncCounter>,
214	log_kind: Option<&'static str>,
215	log_actor_id: Option<String>,
216}
217
218impl RegionGuard {
219	/// Increments a counter and returns a guard that will decrement it.
220	fn new(counter: Arc<AsyncCounter>) -> Self {
221		counter.increment();
222		Self {
223			counter,
224			log_kind: None,
225			log_actor_id: None,
226		}
227	}
228
229	/// Wraps a counter that has already been incremented.
230	pub(crate) fn from_incremented(counter: Arc<AsyncCounter>) -> Self {
231		Self {
232			counter,
233			log_kind: None,
234			log_actor_id: None,
235		}
236	}
237
238	/// Enables paired debug logs for the lifetime of this guard.
239	pub(crate) fn with_log_fields(mut self, kind: &'static str, actor_id: Option<String>) -> Self {
240		let count = self.counter.load();
241		match actor_id.as_deref() {
242			Some(actor_id) => tracing::debug!(actor_id, kind, count, "sleep keep-awake engaged"),
243			None => tracing::debug!(kind, count, "sleep keep-awake engaged"),
244		}
245		self.log_kind = Some(kind);
246		self.log_actor_id = actor_id;
247		self
248	}
249}
250
251impl Drop for RegionGuard {
252	fn drop(&mut self) {
253		self.counter.decrement();
254		let Some(kind) = self.log_kind else {
255			return;
256		};
257		let count = self.counter.load();
258		match self.log_actor_id.as_deref() {
259			Some(actor_id) => tracing::debug!(actor_id, kind, count, "sleep keep-awake disengaged"),
260			None => tracing::debug!(kind, count, "sleep keep-awake disengaged"),
261		}
262	}
263}
264
265/// `CountGuard` is the same RAII shape as `RegionGuard`, but used for task-counting sites.
266pub(crate) type CountGuard = RegionGuard;
267
268// Test shim keeps moved tests in crate-root tests/ with private-module access.
269#[cfg(test)]
270#[path = "../../tests/work_registry.rs"]
271mod tests;