runa/crates/runa-image-api/src/lib.rs

230 lines
6.6 KiB
Rust
Raw Normal View History

//! 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<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,
) -> RuntimeResult<ActorId> {
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<ActorSnapshot> {
self.runtime.inspect(actor)
}
pub fn actors(&self) -> Vec<ActorSnapshot> {
self.runtime.actors()
}
pub fn events(&mut self) -> Vec<Event> {
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<ImplementationId> {
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<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,
"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(())
}
}