lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 09065a610d95e57acdc895a14c07580fa099e7c3
parent da0bf7c5d3e2293f9e6e5a75a4609e58c4bb59c8
Author: triesap <tyson@radroots.org>
Date:   Thu,  6 Aug 2026 20:15:16 +0000

studio: support foreign executor polling

- retain the owning Tokio handle on each runtime actor
- construct command and shutdown timers in the owning runtime
- keep foreign UniFFI executors outside Tokio context safe
- cover bootstrap and close through a non-Tokio poller

Diffstat:
Mcrates/studio_runtime/src/runtime_actor.rs | 54+++++++++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 51 insertions(+), 3 deletions(-)

diff --git a/crates/studio_runtime/src/runtime_actor.rs b/crates/studio_runtime/src/runtime_actor.rs @@ -1,4 +1,5 @@ use std::collections::BTreeMap; +use std::future::Future; use std::num::{NonZeroU64, NonZeroUsize}; use std::path::Path; use std::sync::atomic::{AtomicU64, Ordering}; @@ -138,6 +139,7 @@ pub struct RuntimeActorHandle { session_generation: Arc<AtomicU64>, foreground_session: Arc<Mutex<Option<ForegroundSessionBinding>>>, installation_identity: InstallationIdentity, + runtime: Handle, actor_task: Arc<Mutex<Option<tokio::task::JoinHandle<()>>>>, actor_exit: watch::Receiver<bool>, } @@ -294,6 +296,7 @@ impl RuntimeActorHandle { session_generation, foreground_session, installation_identity, + runtime: runtime.clone(), actor_task: Arc::new(Mutex::new(Some(actor_task))), actor_exit, }) @@ -581,7 +584,7 @@ impl RuntimeActorHandle { let mut actor_exit = self.actor_exit.clone(); if !*actor_exit.borrow() { let remaining = deadline.saturating_duration_since(Instant::now()); - tokio::time::timeout(remaining, actor_exit.wait_for(|exited| *exited)) + self.timeout(remaining, actor_exit.wait_for(|exited| *exited)) .await .map_err(|_| command_timed_out())? .map_err(|_| runtime_closed())?; @@ -593,7 +596,7 @@ impl RuntimeActorHandle { .take(); if let Some(actor_task) = actor_task { let remaining = deadline.saturating_duration_since(Instant::now()); - tokio::time::timeout(remaining, actor_task) + self.timeout(remaining, actor_task) .await .map_err(|_| command_timed_out())? .map_err(|_| runtime_closed())?; @@ -678,7 +681,7 @@ impl RuntimeActorHandle { let receipt = match self.mailbox.submit(context, command) { CommandSubmission::Accepted(ticket) => { let remaining = deadline.saturating_duration_since(Instant::now()); - match tokio::time::timeout(remaining, ticket.receipt()).await { + match self.timeout(remaining, ticket.receipt()).await { Ok(receipt) => receipt, Err(_) => CommandReceipt::new(request_id, CommandResult::TimedOut), } @@ -695,6 +698,14 @@ impl RuntimeActorHandle { } } + fn timeout<F>(&self, duration: Duration, future: F) -> tokio::time::Timeout<F> + where + F: Future, + { + let _guard = self.runtime.enter(); + tokio::time::timeout(duration, future) + } + #[cfg(test)] async fn import_secret_key_test( &self, @@ -1364,9 +1375,12 @@ const fn observer_registration_failed() -> SafeError { #[cfg(test)] mod tests { + use std::future::Future; use std::num::NonZeroUsize; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Condvar, Mutex}; + use std::task::{Context, Poll, Wake, Waker}; + use std::thread::{self, Thread}; use std::time::{Duration, Instant}; use radroots_studio_application::{ @@ -1533,6 +1547,40 @@ mod tests { (actor, secrets) } + struct ThreadWake(Thread); + + impl Wake for ThreadWake { + fn wake(self: Arc<Self>) { + self.0.unpark(); + } + + fn wake_by_ref(self: &Arc<Self>) { + self.0.unpark(); + } + } + + fn block_on_without_runtime<F: Future>(future: F) -> F::Output { + let waker = Waker::from(Arc::new(ThreadWake(thread::current()))); + let mut context = Context::from_waker(&waker); + let mut future = std::pin::pin!(future); + loop { + match future.as_mut().poll(&mut context) { + Poll::Ready(output) => return output, + Poll::Pending => thread::park(), + } + } + } + + #[test] + fn actor_operations_support_foreign_executor_polling() { + let runtime = tokio::runtime::Runtime::new().expect("runtime"); + let (actor, _) = runtime.block_on(actor()); + + let snapshot = block_on_without_runtime(actor.bootstrap()).expect("bootstrap"); + assert_eq!(snapshot, actor.snapshot()); + block_on_without_runtime(actor.close()).expect("close"); + } + #[tokio::test(flavor = "multi_thread")] async fn installation_identity_survives_file_backed_runtime_restart() { let directory = tempfile::tempdir().expect("temporary directory");