//! Trusted, in-process application services for a running Runa image. use std::num::NonZeroUsize; use std::sync::Arc; use runa_actor::{ActorId, ActorSnapshot, Event, Message}; use runa_code::{BehaviorSnapshot, CodeStore}; use runa_core::ImplementationId; use runa_runtime::{Runtime, RuntimeResult}; pub struct ImageApi { code: CodeStore, runtime: Runtime, } impl Default for ImageApi { fn default() -> Self { Self::new() } } impl ImageApi { pub fn new() -> Self { Self { code: CodeStore::default(), runtime: Runtime::default(), } } pub fn publish_behavior( &mut self, name: &str, expected_revision: Option, source: &str, ) -> RuntimeResult { let artifact = self.code.compile(source)?; self.runtime.publish(Arc::clone(&artifact))?; self.code.publish_behavior(name, expected_revision, source) } pub fn rollback_behavior( &mut self, name: &str, expected_revision: u64, target_revision: u64, ) -> RuntimeResult { self.code .rollback_behavior(name, expected_revision, target_revision) } pub fn behavior(&self, name: &str) -> Option { self.code.behavior(name) } pub fn behaviors(&self) -> Vec { self.code.behaviors() } pub fn behavior_source(&self, name: &str, revision: u64) -> RuntimeResult<&str> { let behavior = self.code.behavior(name).ok_or("unknown behavior")?; let version = behavior .history .iter() .find(|version| version.revision == revision) .ok_or("unknown behavior revision")?; self.code .artifact(version.implementation_id) .map(|artifact| artifact.original_source()) .ok_or_else(|| "behavior artifact is unavailable".into()) } pub fn spawn( &mut self, behavior_name: &str, state: i64, capacity: NonZeroUsize, ) -> RuntimeResult { let behavior = self .code .behavior(behavior_name) .ok_or("unknown behavior")?; self.runtime.spawn(behavior.current, state, capacity) } pub fn send(&mut self, actor: ActorId, message: Message) -> RuntimeResult<()> { self.runtime.send(actor, message) } pub fn tick(&mut self, ticks: usize, budget: NonZeroUsize) { for _ in 0..ticks { self.runtime.tick(budget); } } pub fn inspect(&self, actor: ActorId) -> RuntimeResult { self.runtime.inspect(actor) } pub fn actors(&self) -> Vec { self.runtime.actors() } pub fn events(&mut self) -> Vec { self.runtime.drain_events() } pub fn stop(&mut self, actor: ActorId) -> RuntimeResult<()> { self.runtime.stop(actor) } pub fn restart(&mut self, actor: ActorId) -> RuntimeResult<()> { self.runtime.restart(actor) } pub fn pause(&mut self, actor: ActorId) -> RuntimeResult<()> { self.runtime.pause(actor) } pub fn resume(&mut self, actor: ActorId) -> RuntimeResult<()> { self.runtime.resume(actor) } pub fn cancel(&mut self, actor: ActorId) -> RuntimeResult<()> { self.runtime.cancel(actor) } pub fn install( &mut self, actor: ActorId, behavior_name: &str, ) -> RuntimeResult { self.install_with_migration(actor, behavior_name, Ok) } pub fn install_with_migration( &mut self, actor: ActorId, behavior_name: &str, migrate: impl FnOnce(i64) -> RuntimeResult, ) -> RuntimeResult { let behavior = self .code .behavior(behavior_name) .ok_or("unknown behavior")?; self.runtime .install_with_migration(actor, behavior.current, migrate)?; Ok(behavior.current) } } #[cfg(test)] mod tests { use super::*; fn capacity(n: usize) -> NonZeroUsize { NonZeroUsize::new(n).expect("capacity must be positive") } #[test] fn image_api_publishes_spawns_inspects_and_exposes_events() -> RuntimeResult<()> { let mut image = ImageApi::new(); let behavior = image.publish_behavior( "counter", None, "pub fn handle(state, kind, value) { (state + value, []) }", )?; assert_eq!( image.behavior_source("counter", behavior.revision)?, "pub fn handle(state, kind, value) { (state + value, []) }" ); let actor = image.spawn("counter", 5, capacity(2))?; image.send(actor, Message { kind: 0, value: 7 })?; image.tick(100, capacity(10)); assert_eq!(image.inspect(actor)?.state, 12); assert_eq!(image.inspect(actor)?.implementation, behavior.current); assert!( image .events() .iter() .any(|event| matches!(event, Event::Committed(id) if *id == actor)) ); Ok(()) } #[test] fn image_api_rejects_stale_behavior_updates_without_changing_current() -> RuntimeResult<()> { let mut image = ImageApi::new(); let first = image.publish_behavior( "counter", None, "pub fn handle(state, kind, value) { (state + value, []) }", )?; assert!( image .publish_behavior( "counter", None, "pub fn handle(state, kind, value) { (state - value, []) }" ) .is_err() ); assert_eq!(image.behavior("counter").unwrap().current, first.current); Ok(()) } #[test] fn image_api_installs_behavior_with_atomic_state_migration() -> RuntimeResult<()> { let mut image = ImageApi::new(); let first = image.publish_behavior( "counter", None, "pub fn handle(state, kind, value) { (state + value, []) }", )?; image.publish_behavior( "counter", Some(first.revision), "pub fn handle(state, kind, value) { (state * value, []) }", )?; let actor = image.spawn("counter", 5, capacity(2))?; let implementation = image.install_with_migration(actor, "counter", |state| { Ok(state.checked_mul(10).ok_or("migration overflow")?) })?; assert_eq!(image.inspect(actor)?.state, 50); assert_eq!(image.inspect(actor)?.implementation, implementation); Ok(()) } }