Skip to main content

roder_core/runtime/
owner_handoff.rs

1//! Planned owner handoff is separate from cancellation-based shutdown.
2use super::Runtime;
3use std::sync::atomic::Ordering;
4
5impl Runtime {
6    /// Permanently seal an owned runtime only after all admitted turns, tool
7    /// futures, and terminal cleanup have finished. Busy runtimes keep working;
8    /// the host must stop new inbound work while polling this operation.
9    ///
10    /// True proves local quiescence, including after lease expiry or revocation.
11    /// The host must separately confirm loss of the durable generation before
12    /// reporting a completed handoff. False means work remains. Persistence
13    /// failures still require reconciliation and cannot be reported as drained.
14    pub async fn seal_idle_owner(&self) -> anyhow::Result<bool> {
15        let _admission = self.turn_admission.lock().await;
16        let lease = self
17            .execution_lease
18            .as_ref()
19            .ok_or_else(|| anyhow::anyhow!("owner handoff requires an execution lease"))?;
20        if !self.active_turns.read().await.is_empty() || !self.turn_drains.read().await.is_empty() {
21            return Ok(false);
22        }
23        anyhow::ensure!(
24            self.lifecycle_persistence_failures.load(Ordering::Acquire) == 0,
25            "owner handoff requires reconciliation of failed lifecycle persistence"
26        );
27        if !lease.seal_if_idle()? {
28            return Ok(false);
29        }
30        self.accepting_turns.store(false, Ordering::Release);
31        Ok(true)
32    }
33}
34
35#[cfg(test)]
36mod tests {
37    use super::*;
38    use crate::{RuntimeConfig, RuntimeExecutionLease, fake_provider::FakeInferenceEngine};
39    use roder_api::extension::ExtensionRegistryBuilder;
40    use std::{
41        sync::Arc,
42        time::{Duration, Instant},
43    };
44
45    #[tokio::test]
46    async fn expired_idle_owner_can_seal_but_persistence_failure_blocks_proof() {
47        let lease = Arc::new(RuntimeExecutionLease::new(Instant::now()));
48        let mut builder = ExtensionRegistryBuilder::new();
49        builder.inference_engine(Arc::new(FakeInferenceEngine));
50        let runtime = Runtime::new(builder.build().unwrap(), RuntimeConfig::default())
51            .unwrap()
52            .with_execution_lease(lease.clone());
53        runtime
54            .lifecycle_persistence_failures
55            .store(1, Ordering::Release);
56        assert!(runtime.seal_idle_owner().await.is_err());
57        runtime
58            .lifecycle_persistence_failures
59            .store(0, Ordering::Release);
60        assert!(runtime.seal_idle_owner().await.unwrap());
61        assert!(runtime.seal_idle_owner().await.unwrap());
62        assert!(runtime.ensure_execution_authority().is_err());
63        assert!(
64            lease
65                .renew(Instant::now() + Duration::from_secs(30))
66                .is_err()
67        );
68    }
69}