1use std::sync::Arc;
2use std::sync::atomic::Ordering;
3use std::time::Duration;
4
5use crate::time::{SystemTime, UNIX_EPOCH, sleep};
6
7use anyhow::Result;
8use futures::future::BoxFuture;
9use rivet_envoy_client::handle::EnvoyHandle;
10use tokio::runtime::Handle;
11use tokio::sync::oneshot;
12use tracing::Instrument;
13use uuid::Uuid;
14
15use crate::actor::context::ActorContext;
16use crate::actor::state::PersistedScheduleEvent;
17use crate::error::ActorRuntime;
18#[cfg(feature = "wasm-runtime")]
19use crate::runtime::RuntimeSpawner;
20
21pub(super) type InternalKeepAwakeCallback =
22 Arc<dyn Fn(BoxFuture<'static, Result<()>>) -> BoxFuture<'static, Result<()>> + Send + Sync>;
23pub(super) type LocalAlarmCallback = Arc<dyn Fn() -> BoxFuture<'static, ()> + Send + Sync>;
24
25impl ActorContext {
26 #[cfg(test)]
27 pub(crate) fn new_for_schedule_tests(actor_id: impl Into<String>) -> Self {
28 Self::new(actor_id, "schedule-test", Vec::new(), "local")
29 }
30
31 pub fn after(&self, duration: Duration, action_name: &str, args: &[u8]) {
32 let duration_ms = i64::try_from(duration.as_millis()).unwrap_or(i64::MAX);
33 let timestamp_ms = now_timestamp_ms().saturating_add(duration_ms);
34 self.at(timestamp_ms, action_name, args);
35 }
36
37 pub fn at(&self, timestamp_ms: i64, action_name: &str, args: &[u8]) {
38 if let Err(error) = self.schedule_event(timestamp_ms, action_name, args) {
39 tracing::error!(
40 actor_id = %self.actor_id(),
41 ?error,
42 action_name,
43 timestamp_ms,
44 "failed to schedule actor event"
45 );
46 }
47 }
48
49 pub(crate) fn set_schedule_alarm(&self, timestamp_ms: Option<i64>) -> Result<()> {
50 let envoy_handle = self.0.schedule_envoy_handle.lock().clone().ok_or_else(|| {
51 ActorRuntime::NotConfigured {
52 component: "schedule alarm handle".to_owned(),
53 }
54 .build()
55 })?;
56 let generation = *self.0.schedule_generation.lock();
57 self.set_alarm_tracked(envoy_handle, timestamp_ms, generation);
58 Ok(())
59 }
60
61 pub(crate) fn configure_schedule_envoy(
62 &self,
63 envoy_handle: EnvoyHandle,
64 generation: Option<u32>,
65 ) {
66 *self.0.schedule_envoy_handle.lock() = Some(envoy_handle);
67 *self.0.schedule_generation.lock() = generation;
68 }
69
70 pub(crate) fn set_internal_keep_awake(&self, callback: Option<InternalKeepAwakeCallback>) {
71 *self.0.schedule_internal_keep_awake.lock() = callback;
72 }
73
74 pub(crate) fn set_local_alarm_callback(&self, callback: Option<LocalAlarmCallback>) {
75 *self.0.schedule_local_alarm_callback.lock() = callback;
76 }
77
78 pub(crate) fn cancel_scheduled_event(&self, event_id: &str) -> bool {
79 let removed = self.update_scheduled_events(|events| {
80 let before = events.len();
81 events.retain(|event| event.event_id != event_id);
82 before != events.len()
83 });
84
85 if removed {
86 tracing::debug!(
87 actor_id = %self.actor_id(),
88 event_id,
89 "scheduled actor event cancelled"
90 );
91 self.mark_dirty_since_push();
92 self.persist_scheduled_events("schedule_cancel");
93 self.sync_alarm_logged();
94 }
95
96 removed
97 }
98
99 pub(crate) fn next_event(&self) -> Option<PersistedScheduleEvent> {
100 self.scheduled_events().into_iter().next()
101 }
102
103 pub(crate) fn all_events(&self) -> Vec<PersistedScheduleEvent> {
104 self.scheduled_events()
105 }
106
107 pub(crate) fn cancel_local_alarm_timeouts(&self) {
108 self.0
109 .schedule_local_alarm_epoch
110 .fetch_add(1, Ordering::SeqCst);
111 if let Some(handle) = self.0.schedule_local_alarm_task.lock().take() {
112 handle.abort();
113 }
114 }
115
116 pub(crate) fn cancel_driver_alarm_logged(&self) {
117 self.cancel_local_alarm_timeouts();
118 #[cfg(test)]
119 self.0
120 .schedule_driver_alarm_cancel_count
121 .fetch_add(1, Ordering::SeqCst);
122
123 let envoy_handle = self.0.schedule_envoy_handle.lock().clone();
124 let Some(envoy_handle) = envoy_handle else {
125 return;
126 };
127
128 let generation = *self.0.schedule_generation.lock();
129 self.set_alarm_tracked(envoy_handle, None, generation);
130 }
131
132 #[cfg(test)]
133 pub(crate) fn test_driver_alarm_cancel_count(&self) -> usize {
134 self.0
135 .schedule_driver_alarm_cancel_count
136 .load(Ordering::SeqCst)
137 }
138
139 pub(crate) async fn wait_for_pending_alarm_writes(&self) {
140 let pending = {
141 let mut guard = self.0.schedule_pending_alarm_writes.lock();
142 std::mem::take(&mut *guard)
143 };
144 tracing::debug!(
145 actor_id = %self.actor_id(),
146 pending_alarm_writes = pending.len(),
147 "waiting for pending actor alarm writes"
148 );
149
150 for ack_rx in pending {
151 let _ = ack_rx.await;
152 }
153 tracing::debug!(
154 actor_id = %self.actor_id(),
155 "pending actor alarm writes drained"
156 );
157 }
158
159 pub(crate) fn due_scheduled_events(&self, now_ms: i64) -> Vec<PersistedScheduleEvent> {
160 if !self
161 .0
162 .schedule_alarm_dispatch_enabled
163 .load(Ordering::SeqCst)
164 {
165 return Vec::new();
166 }
167
168 self.all_events()
169 .into_iter()
170 .filter(|event| event.timestamp <= now_ms)
171 .collect()
172 }
173
174 fn schedule_event(&self, timestamp_ms: i64, action_name: &str, args: &[u8]) -> Result<()> {
175 let event = PersistedScheduleEvent {
176 event_id: Uuid::new_v4().to_string(),
177 timestamp: timestamp_ms,
178 action: action_name.to_owned(),
179 args: (!args.is_empty()).then(|| args.to_vec()),
180 };
181 let event_id = event.event_id.clone();
182 let args_len = event.args.as_ref().map_or(0, Vec::len);
183
184 self.insert_event_sorted(event);
185 tracing::debug!(
186 actor_id = %self.actor_id(),
187 event_id,
188 action_name,
189 timestamp_ms,
190 args_len,
191 "scheduled actor event added"
192 );
193 self.mark_dirty_since_push();
194 self.persist_scheduled_events("schedule_insert");
195 self.sync_alarm()
196 }
197
198 fn insert_event_sorted(&self, event: PersistedScheduleEvent) {
199 self.update_scheduled_events(|events| {
200 let position = events
201 .binary_search_by(|existing| {
202 existing
203 .timestamp
204 .cmp(&event.timestamp)
205 .then_with(|| existing.event_id.cmp(&event.event_id))
206 })
207 .unwrap_or_else(|index| index);
208 events.insert(position, event);
209 });
210 }
211
212 fn persist_scheduled_events(&self, description: &'static str) {
213 self.persist_now_tracked(description);
214 }
215
216 fn mark_dirty_since_push(&self) {
217 self.0
218 .schedule_dirty_since_push
219 .store(true, Ordering::SeqCst);
220 }
221
222 fn sync_alarm(&self) -> Result<()> {
223 let should_push = self
224 .0
225 .schedule_dirty_since_push
226 .swap(false, Ordering::SeqCst);
227 let next_alarm = self.next_event().map(|event| event.timestamp);
228 self.arm_local_alarm(next_alarm);
229 if !should_push {
230 return Ok(());
231 }
232 if next_alarm.is_some() && self.last_pushed_alarm() == next_alarm {
235 return Ok(());
236 }
237
238 let envoy_handle = self.0.schedule_envoy_handle.lock().clone();
239
240 let Some(envoy_handle) = envoy_handle else {
241 self.mark_dirty_since_push();
242 tracing::warn!(
243 actor_id = self.actor_id(),
244 sleep_timeout_ms = self.sleep_state_config().sleep_timeout.as_millis() as u64,
245 "schedule alarm sync skipped because envoy handle is not configured"
246 );
247 return Ok(());
248 };
249
250 let generation = *self.0.schedule_generation.lock();
251 self.set_alarm_tracked(envoy_handle, next_alarm, generation);
252 Ok(())
253 }
254
255 fn sync_future_alarm(&self) -> Result<()> {
256 let should_push = self
257 .0
258 .schedule_dirty_since_push
259 .swap(false, Ordering::SeqCst);
260 let now_ms = now_timestamp_ms();
261 let next_alarm = self
262 .next_event()
263 .and_then(|event| (event.timestamp > now_ms).then_some(event.timestamp));
264 self.arm_local_alarm(next_alarm);
265 if !should_push {
266 return Ok(());
267 }
268 if next_alarm.is_some() && self.last_pushed_alarm() == next_alarm {
271 return Ok(());
272 }
273
274 let envoy_handle = self.0.schedule_envoy_handle.lock().clone();
275
276 let Some(envoy_handle) = envoy_handle else {
277 self.mark_dirty_since_push();
278 tracing::warn!(
279 actor_id = self.actor_id(),
280 sleep_timeout_ms = self.sleep_state_config().sleep_timeout.as_millis() as u64,
281 "future schedule alarm sync skipped because envoy handle is not configured"
282 );
283 return Ok(());
284 };
285
286 let generation = *self.0.schedule_generation.lock();
287 self.set_alarm_tracked(envoy_handle, next_alarm, generation);
288 Ok(())
289 }
290
291 fn set_alarm_tracked(
292 &self,
293 envoy_handle: EnvoyHandle,
294 timestamp_ms: Option<i64>,
295 generation: Option<u32>,
296 ) {
297 let previous_alarm = self.last_pushed_alarm();
298 tracing::debug!(
299 actor_id = %self.actor_id(),
300 generation,
301 old_timestamp_ms = previous_alarm,
302 new_timestamp_ms = timestamp_ms,
303 "pushing actor alarm to envoy"
304 );
305 let (ack_tx, ack_rx) = oneshot::channel();
306 envoy_handle.set_alarm_with_ack(
307 self.actor_id().to_owned(),
308 timestamp_ms,
309 generation,
310 Some(ack_tx),
311 );
312 self.load_last_pushed_alarm(timestamp_ms);
313 if let Ok(handle) = Handle::try_current() {
314 let state_ctx = self.clone();
315 let (persist_done_tx, persist_done_rx) = oneshot::channel();
316 handle.spawn(
317 async move {
318 let _ = ack_rx.await;
319 if let Err(error) = state_ctx.persist_last_pushed_alarm(timestamp_ms).await {
320 tracing::error!(
321 ?error,
322 ?timestamp_ms,
323 "failed to persist last pushed actor alarm"
324 );
325 }
326 let _ = persist_done_tx.send(());
327 }
328 .in_current_span(),
329 );
330 self.0
331 .schedule_pending_alarm_writes
332 .lock()
333 .push(persist_done_rx);
334 return;
335 }
336
337 self.0.schedule_pending_alarm_writes.lock().push(ack_rx);
338 }
339
340 fn arm_local_alarm(&self, next_alarm: Option<i64>) {
341 self.cancel_local_alarm_timeouts();
342
343 let Some(next_alarm) = next_alarm else {
344 return;
345 };
346
347 let has_callback = self.0.schedule_local_alarm_callback.lock().is_some();
348 if !has_callback {
349 return;
350 }
351
352 #[cfg(not(feature = "wasm-runtime"))]
353 let tokio_handle = match Handle::try_current() {
354 Ok(handle) => handle,
355 Err(_) => return,
356 };
357
358 let delay_ms = next_alarm.saturating_sub(now_timestamp_ms()).max(0) as u64;
359 let local_alarm_epoch = self.0.schedule_local_alarm_epoch.load(Ordering::SeqCst);
360 let schedule = self.clone();
361 tracing::debug!(
362 actor_id = %self.actor_id(),
363 timestamp_ms = next_alarm,
364 delay_ms,
365 local_alarm_epoch,
366 "local actor alarm armed"
367 );
368 let task = async move {
371 sleep(Duration::from_millis(delay_ms)).await;
372 if schedule.0.schedule_local_alarm_epoch.load(Ordering::SeqCst) != local_alarm_epoch {
373 return;
374 }
375 tracing::debug!(
376 timestamp_ms = next_alarm,
377 local_alarm_epoch,
378 "local actor alarm fired"
379 );
380 let Some(callback) = schedule.0.schedule_local_alarm_callback.lock().clone() else {
381 return;
382 };
383 callback().await;
384 }
385 .in_current_span();
386
387 #[cfg(not(feature = "wasm-runtime"))]
388 let handle = tokio_handle.spawn(task);
389
390 #[cfg(feature = "wasm-runtime")]
391 let handle = RuntimeSpawner::spawn(task);
392
393 *self.0.schedule_local_alarm_task.lock() = Some(handle);
394 }
395
396 pub(crate) fn sync_alarm_logged(&self) {
397 if let Err(error) = self.sync_alarm() {
398 tracing::error!(
399 actor_id = %self.actor_id(),
400 ?error,
401 "failed to sync scheduled actor alarm"
402 );
403 }
404 }
405
406 pub(crate) fn sync_future_alarm_logged(&self) {
407 if let Err(error) = self.sync_future_alarm() {
408 tracing::error!(
409 actor_id = %self.actor_id(),
410 ?error,
411 "failed to sync future scheduled actor alarm"
412 );
413 }
414 }
415
416 pub(crate) fn suspend_alarm_dispatch(&self) {
417 self.0
418 .schedule_alarm_dispatch_enabled
419 .store(false, Ordering::SeqCst);
420 }
421}
422
423fn now_timestamp_ms() -> i64 {
424 let duration = SystemTime::now()
425 .duration_since(UNIX_EPOCH)
426 .unwrap_or_default();
427 i64::try_from(duration.as_millis()).unwrap_or(i64::MAX)
428}
429
430#[cfg(test)]
432#[path = "../../tests/schedule.rs"]
433mod tests;