2026-11-02 12:20:45 +00:00
|
|
|
//! Trusted, in-process application services for a running Runa image.
|
|
|
|
|
|
|
|
|
|
use std::num::NonZeroUsize;
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
2026-11-02 14:19:25 +00:00
|
|
|
use runa_actor::{ActorRef, ActorSnapshot, Event, Message};
|
2026-11-02 12:20:45 +00:00
|
|
|
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<u64>,
|
|
|
|
|
source: &str,
|
|
|
|
|
) -> RuntimeResult<BehaviorSnapshot> {
|
|
|
|
|
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<BehaviorSnapshot> {
|
|
|
|
|
self.code
|
|
|
|
|
.rollback_behavior(name, expected_revision, target_revision)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn behavior(&self, name: &str) -> Option<BehaviorSnapshot> {
|
|
|
|
|
self.code.behavior(name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn behaviors(&self) -> Vec<BehaviorSnapshot> {
|
|
|
|
|
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,
|
2026-11-02 14:19:25 +00:00
|
|
|
) -> RuntimeResult<ActorRef> {
|
2026-11-02 12:20:45 +00:00
|
|
|
let behavior = self
|
|
|
|
|
.code
|
|
|
|
|
.behavior(behavior_name)
|
|
|
|
|
.ok_or("unknown behavior")?;
|
|
|
|
|
self.runtime.spawn(behavior.current, state, capacity)
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-02 14:19:25 +00:00
|
|
|
pub fn send(&mut self, actor: ActorRef, message: Message) -> RuntimeResult<()> {
|
2026-11-02 12:20:45 +00:00
|
|
|
self.runtime.send(actor, message)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn tick(&mut self, ticks: usize, budget: NonZeroUsize) {
|
|
|
|
|
for _ in 0..ticks {
|
|
|
|
|
self.runtime.tick(budget);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-02 14:19:25 +00:00
|
|
|
pub fn inspect(&self, actor: ActorRef) -> RuntimeResult<ActorSnapshot> {
|
2026-11-02 12:20:45 +00:00
|
|
|
self.runtime.inspect(actor)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn actors(&self) -> Vec<ActorSnapshot> {
|
|
|
|
|
self.runtime.actors()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn events(&mut self) -> Vec<Event> {
|
|
|
|
|
self.runtime.drain_events()
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-02 14:19:25 +00:00
|
|
|
pub fn stop(&mut self, actor: ActorRef) -> RuntimeResult<()> {
|
2026-11-02 12:20:45 +00:00
|
|
|
self.runtime.stop(actor)
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-02 14:19:25 +00:00
|
|
|
pub fn restart(&mut self, actor: ActorRef) -> RuntimeResult<()> {
|
2026-11-02 12:20:45 +00:00
|
|
|
self.runtime.restart(actor)
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-02 14:19:25 +00:00
|
|
|
pub fn pause(&mut self, actor: ActorRef) -> RuntimeResult<()> {
|
2026-11-02 12:20:45 +00:00
|
|
|
self.runtime.pause(actor)
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-02 14:19:25 +00:00
|
|
|
pub fn resume(&mut self, actor: ActorRef) -> RuntimeResult<()> {
|
2026-11-02 12:20:45 +00:00
|
|
|
self.runtime.resume(actor)
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-02 14:19:25 +00:00
|
|
|
pub fn cancel(&mut self, actor: ActorRef) -> RuntimeResult<()> {
|
2026-11-02 12:20:45 +00:00
|
|
|
self.runtime.cancel(actor)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn install(
|
|
|
|
|
&mut self,
|
2026-11-02 14:19:25 +00:00
|
|
|
actor: ActorRef,
|
2026-11-02 12:20:45 +00:00
|
|
|
behavior_name: &str,
|
|
|
|
|
) -> RuntimeResult<ImplementationId> {
|
|
|
|
|
self.install_with_migration(actor, behavior_name, Ok)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn install_with_migration(
|
|
|
|
|
&mut self,
|
2026-11-02 14:19:25 +00:00
|
|
|
actor: ActorRef,
|
2026-11-02 12:20:45 +00:00
|
|
|
behavior_name: &str,
|
|
|
|
|
migrate: impl FnOnce(i64) -> RuntimeResult<i64>,
|
|
|
|
|
) -> RuntimeResult<ImplementationId> {
|
|
|
|
|
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,
|
2026-11-02 14:19:25 +00:00
|
|
|
"pub fn handle(state, kind, value, target_ref) { (state + value, []) }",
|
2026-11-02 12:20:45 +00:00
|
|
|
)?;
|
|
|
|
|
assert_eq!(
|
|
|
|
|
image.behavior_source("counter", behavior.revision)?,
|
2026-11-02 14:19:25 +00:00
|
|
|
"pub fn handle(state, kind, value, target_ref) { (state + value, []) }"
|
2026-11-02 12:20:45 +00:00
|
|
|
);
|
|
|
|
|
let actor = image.spawn("counter", 5, capacity(2))?;
|
2026-11-02 14:19:25 +00:00
|
|
|
image.send(
|
|
|
|
|
actor,
|
|
|
|
|
Message {
|
|
|
|
|
kind: 0,
|
|
|
|
|
value: 7,
|
|
|
|
|
target_ref: None,
|
|
|
|
|
},
|
|
|
|
|
)?;
|
2026-11-02 12:20:45 +00:00
|
|
|
image.tick(100, capacity(10));
|
|
|
|
|
|
|
|
|
|
assert_eq!(image.inspect(actor)?.state, 12);
|
|
|
|
|
assert_eq!(image.inspect(actor)?.implementation, behavior.current);
|
|
|
|
|
assert!(
|
|
|
|
|
image
|
|
|
|
|
.events()
|
|
|
|
|
.iter()
|
2026-11-02 14:19:25 +00:00
|
|
|
.any(|event| matches!(event, Event::Committed(id) if id == &actor))
|
2026-11-02 12:20:45 +00:00
|
|
|
);
|
|
|
|
|
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,
|
2026-11-02 14:19:25 +00:00
|
|
|
"pub fn handle(state, kind, value, target_ref) { (state + value, []) }",
|
2026-11-02 12:20:45 +00:00
|
|
|
)?;
|
|
|
|
|
assert!(
|
|
|
|
|
image
|
|
|
|
|
.publish_behavior(
|
|
|
|
|
"counter",
|
|
|
|
|
None,
|
2026-11-02 14:19:25 +00:00
|
|
|
"pub fn handle(state, kind, value, target_ref) { (state - value, []) }"
|
2026-11-02 12:20:45 +00:00
|
|
|
)
|
|
|
|
|
.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,
|
2026-11-02 14:19:25 +00:00
|
|
|
"pub fn handle(state, kind, value, target_ref) { (state + value, []) }",
|
2026-11-02 12:20:45 +00:00
|
|
|
)?;
|
|
|
|
|
image.publish_behavior(
|
|
|
|
|
"counter",
|
|
|
|
|
Some(first.revision),
|
2026-11-02 14:19:25 +00:00
|
|
|
"pub fn handle(state, kind, value, target_ref) { (state * value, []) }",
|
2026-11-02 12:20:45 +00:00
|
|
|
)?;
|
|
|
|
|
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(())
|
|
|
|
|
}
|
|
|
|
|
}
|