diff --git a/docs-internal/engine/rivetkit-core-internals.md b/docs-internal/engine/rivetkit-core-internals.md index 5fe7ad5bd8..dd0a2e658b 100644 --- a/docs-internal/engine/rivetkit-core-internals.md +++ b/docs-internal/engine/rivetkit-core-internals.md @@ -117,9 +117,9 @@ Persistence order: 1. Immediate state save. 2. Pending state write wait. -3. Alarm write wait. -4. SQLite cleanup. -5. Driver alarm cancellation. +3. Sync the driver alarm for sleep, or cancel it without querying SQLite for destroy. +4. Alarm write wait. +5. SQLite cleanup. ## ActorConfig diff --git a/rivetkit-rust/packages/rivetkit-core/src/actor/task.rs b/rivetkit-rust/packages/rivetkit-core/src/actor/task.rs index c3cdbe3d22..4ac8bc8967 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/actor/task.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/actor/task.rs @@ -1864,17 +1864,10 @@ impl ActorTask { step = "wait_for_pending_state_writes", "actor shutdown cleanup step completed" ); - ctx.sync_alarm_logged().await; - tracing::debug!( - actor_id = %actor_id, - reason = reason_label, - step = "sync_alarm", - "actor shutdown cleanup step completed" - ); - // Destroy cancels the engine alarm here so the persist it spawns is awaited by - // `wait_for_pending_alarm_writes` below and cannot race the SQLite teardown. match reason { ShutdownKind::Destroy => { + // A foreign runtime may close SQLite during its destroy callback. Skip + // the SQLite-backed alarm sync because this alarm is cleared anyway. ctx.cancel_driver_alarm_logged(); tracing::debug!( actor_id = %actor_id, @@ -1883,7 +1876,15 @@ impl ActorTask { "actor shutdown cleanup step completed" ); } - ShutdownKind::Sleep => {} + ShutdownKind::Sleep => { + ctx.sync_alarm_logged().await; + tracing::debug!( + actor_id = %actor_id, + reason = reason_label, + step = "sync_alarm", + "actor shutdown cleanup step completed" + ); + } } ctx.wait_for_pending_alarm_writes().await; tracing::debug!( diff --git a/rivetkit-rust/packages/rivetkit-core/tests/task.rs b/rivetkit-rust/packages/rivetkit-core/tests/task.rs index 587776925e..b4e498b8be 100644 --- a/rivetkit-rust/packages/rivetkit-core/tests/task.rs +++ b/rivetkit-rust/packages/rivetkit-core/tests/task.rs @@ -3289,6 +3289,42 @@ pub(crate) mod moved_tests { assert_eq!(ctx.test_driver_alarm_cancel_count(), 1); } + #[tokio::test] + async fn destroy_cleanup_does_not_sync_alarm_after_runtime_closes_sqlite() { + let ctx = new_with_kv( + "actor-destroy-closed-sqlite", + "task-destroy-closed-sqlite", + Vec::new(), + "local", + new_in_memory(), + ); + ctx.sql() + .close() + .await + .expect("foreign runtime sqlite close should succeed"); + + let records = Arc::new(Mutex::new(Vec::new())); + let subscriber = Registry::default().with(ActorTaskLogLayer { + records: records.clone(), + }); + let dispatch = tracing::Dispatch::new(subscriber); + ActorTask::finish_shutdown_cleanup_with_ctx(ctx.clone(), ShutdownKind::Destroy) + .with_subscriber(dispatch) + .await + .expect("destroy cleanup should succeed after sqlite closes"); + + assert_eq!(ctx.test_driver_alarm_cancel_count(), 1); + assert!( + !records + .lock() + .expect("actor-task log lock poisoned") + .iter() + .any(|record| record.message.as_deref() + == Some("failed to sync scheduled actor alarm")), + "destroy cleanup must not query the closed sqlite coordinator" + ); + } + #[tokio::test(start_paused = true)] async fn sleep_shutdown_without_in_flight_work_finishes_under_baseline() { let ctx = new_with_kv(