Skip to content

Commit 1939547

Browse files
committed
fix(rivetkit-core): return stopping not starting when sleep/destroy called mid-shutdown
1 parent c7f9133 commit 1939547

2 files changed

Lines changed: 42 additions & 3 deletions

File tree

rivetkit-rust/packages/rivetkit-core/src/actor/context.rs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -388,9 +388,19 @@ impl ActorContext {
388388
}
389389

390390
pub fn sleep(&self) -> Result<()> {
391+
// `started` is cleared when the lifecycle state machine transitions
392+
// into SleepGrace / DestroyGrace, so `started=false` covers both
393+
// "never started" and "already shutting down". Distinguish with the
394+
// request flags for an accurate diagnostic.
391395
if !self.0.sleep.started.load(Ordering::SeqCst) {
392-
return Err(ActorLifecycleError::Starting.build())
393-
.context("cannot request sleep before actor startup completes");
396+
let already_stopping = self.0.sleep_requested.load(Ordering::SeqCst)
397+
|| self.0.destroy_requested.load(Ordering::SeqCst);
398+
return if already_stopping {
399+
Err(ActorLifecycleError::Stopping.build()).context("actor is already shutting down")
400+
} else {
401+
Err(ActorLifecycleError::Starting.build())
402+
.context("cannot request sleep before actor startup completes")
403+
};
394404
}
395405
if self.0.sleep_requested.swap(true, Ordering::SeqCst) {
396406
return Err(ActorLifecycleError::Stopping.build())
@@ -415,7 +425,13 @@ impl ActorContext {
415425
}
416426

417427
pub fn destroy(&self) -> Result<()> {
418-
if !self.0.sleep.started.load(Ordering::SeqCst) {
428+
// See `sleep` for why the request flags disambiguate `started=false`.
429+
// destroy() is allowed after sleep() has been requested because
430+
// destroy is a stronger signal that escalates an in-flight sleep.
431+
if !self.0.sleep.started.load(Ordering::SeqCst)
432+
&& !self.0.sleep_requested.load(Ordering::SeqCst)
433+
&& !self.0.destroy_requested.load(Ordering::SeqCst)
434+
{
419435
return Err(ActorLifecycleError::Starting.build())
420436
.context("cannot request destroy before actor startup completes");
421437
}

rivetkit-rust/packages/rivetkit-core/src/actor/sleep.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -927,4 +927,27 @@ mod tests {
927927
ctx.cancel_shutdown_deadline();
928928
assert!(token.is_cancelled());
929929
}
930+
931+
#[tokio::test(start_paused = true)]
932+
async fn sleep_after_grace_clears_started_returns_stopping_not_starting() {
933+
// Simulate the lifecycle state machine clearing `started` when it
934+
// transitions into SleepGrace. Calls into `sleep()` after that point
935+
// must surface `Stopping`, not `Starting`.
936+
let ctx = ActorContext::new_for_sleep_tests("actor-sleep-after-grace");
937+
ctx.set_sleep_started(true);
938+
939+
ctx.sleep().expect("first sleep call should be accepted");
940+
941+
// Lifecycle machine clears `started` on transition into SleepGrace.
942+
ctx.set_sleep_started(false);
943+
944+
let err = ctx.sleep().expect_err("second sleep should fail");
945+
let rivet_err = rivet_error::RivetError::extract(&err);
946+
assert_eq!(rivet_err.group(), "actor");
947+
assert_eq!(
948+
rivet_err.code(),
949+
"stopping",
950+
"started=false during shutdown must surface stopping, not starting"
951+
);
952+
}
930953
}

0 commit comments

Comments
 (0)