use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ContextId(u64);
impl ContextId {
fn new(id: u64) -> Self {
Self(id)
}
pub fn as_u64(self) -> u64 {
self.0
}
}
impl std::fmt::Display for ContextId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ctx-{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Task {
Echo(String),
Increment(String),
SetState(String, String),
Shutdown,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Output {
Echoed(String),
Counter(String, i64),
StateSet(String, String),
Shutdown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Guard {
Open,
Paused,
Stopped,
}
#[derive(Debug)]
pub struct Context {
name: String,
state: HashMap<String, String>,
counters: HashMap<String, i64>,
guard: Guard,
outputs: Vec<Output>,
tasks: Vec<Task>,
}
impl Context {
fn new(name: String) -> Self {
Self {
name,
state: HashMap::new(),
counters: HashMap::new(),
guard: Guard::Open,
outputs: Vec::new(),
tasks: Vec::new(),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn guard(&self) -> Guard {
self.guard
}
pub fn set_guard(&mut self, guard: Guard) {
self.guard = guard;
}
pub fn state(&self, key: &str) -> Option<&str> {
self.state.get(key).map(|s| s.as_str())
}
pub fn state_entries(&self) -> &HashMap<String, String> {
&self.state
}
pub fn counter(&self, name: &str) -> i64 {
self.counters.get(name).copied().unwrap_or(0)
}
pub fn outputs(&self) -> &[Output] {
&self.outputs
}
}
#[derive(Debug)]
pub struct Runtime {
contexts: HashMap<ContextId, Context>,
next_id: u64,
log: Vec<String>,
}
impl Runtime {
pub fn new() -> Self {
Self {
contexts: HashMap::new(),
next_id: 0,
log: Vec::new(),
}
}
pub fn spawn(&mut self, name: impl Into<String>) -> ContextId {
let id = ContextId::new(self.next_id);
self.next_id += 1;
self.contexts.insert(id, Context::new(name.into()));
self.log.push(format!("spawned {}", id));
id
}
pub fn context(&self, id: ContextId) -> Option<&Context> {
self.contexts.get(&id)
}
pub fn context_mut(&mut self, id: ContextId) -> Option<&mut Context> {
self.contexts.get_mut(&id)
}
pub fn submit(&mut self, id: ContextId, task: Task) -> Result<(), RuntimeError> {
let ctx = self
.contexts
.get_mut(&id)
.ok_or(RuntimeError::NoSuchContext(id))?;
if ctx.guard == Guard::Stopped {
return Err(RuntimeError::ContextStopped(id));
}
ctx.tasks.push(task);
Ok(())
}
pub fn run_all(&mut self) {
let ids: Vec<ContextId> = self.contexts.keys().copied().collect();
for id in ids {
self.run_context(id);
}
}
pub fn run_context(&mut self, id: ContextId) {
let tasks: Vec<Task> = {
let ctx = match self.contexts.get_mut(&id) {
Some(c) => c,
None => return,
};
if ctx.guard != Guard::Open {
return;
}
std::mem::take(&mut ctx.tasks)
};
for task in tasks {
self.execute_task(id, task);
}
}
fn execute_task(&mut self, id: ContextId, task: Task) {
let ctx = match self.contexts.get_mut(&id) {
Some(c) => c,
None => return,
};
match task {
Task::Echo(msg) => {
ctx.outputs.push(Output::Echoed(msg));
}
Task::Increment(name) => {
let val = ctx.counters.entry(name.clone()).or_insert(0);
*val += 1;
ctx.outputs.push(Output::Counter(name, *val));
}
Task::SetState(key, value) => {
ctx.state.insert(key.clone(), value.clone());
ctx.outputs.push(Output::StateSet(key, value));
}
Task::Shutdown => {
ctx.guard = Guard::Stopped;
ctx.outputs.push(Output::Shutdown);
}
}
}
pub fn log(&self) -> &[String] {
&self.log
}
pub fn active_count(&self) -> usize {
self.contexts
.values()
.filter(|c| c.guard != Guard::Stopped)
.count()
}
pub fn total_count(&self) -> usize {
self.contexts.len()
}
pub fn output(&self, id: ContextId) -> Vec<String> {
self.contexts
.get(&id)
.map(|c| {
c.outputs()
.iter()
.filter_map(|o| match o {
Output::Echoed(s) => Some(s.clone()),
_ => None,
})
.collect()
})
.unwrap_or_default()
}
}
impl Default for Runtime {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RuntimeError {
NoSuchContext(ContextId),
ContextStopped(ContextId),
}
impl std::fmt::Display for RuntimeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RuntimeError::NoSuchContext(id) => write!(f, "no context with id {}", id),
RuntimeError::ContextStopped(id) => write!(f, "context {} is stopped", id),
}
}
}
impl std::error::Error for RuntimeError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn spawn_and_context() {
let mut rt = Runtime::new();
let id = rt.spawn("worker");
assert_eq!(rt.context(id).unwrap().name(), "worker");
assert_eq!(rt.total_count(), 1);
}
#[test]
fn submit_echo() {
let mut rt = Runtime::new();
let id = rt.spawn("test");
rt.submit(id, Task::Echo("hello".into())).unwrap();
rt.run_all();
let outputs = rt.output(id);
assert_eq!(outputs, vec!["hello"]);
}
#[test]
fn submit_increment() {
let mut rt = Runtime::new();
let id = rt.spawn("counter");
rt.submit(id, Task::Increment("hits".into())).unwrap();
rt.submit(id, Task::Increment("hits".into())).unwrap();
rt.run_all();
let c = rt.context(id).unwrap();
assert_eq!(c.counter("hits"), 2);
}
#[test]
fn submit_set_state() {
let mut rt = Runtime::new();
let id = rt.spawn("stateful");
rt.submit(id, Task::SetState("key".into(), "val".into()))
.unwrap();
rt.run_all();
let c = rt.context(id).unwrap();
assert_eq!(c.state("key"), Some("val"));
}
#[test]
fn shutdown_stops_context() {
let mut rt = Runtime::new();
let id = rt.spawn("stopper");
rt.submit(id, Task::Shutdown).unwrap();
rt.run_all();
assert_eq!(rt.context(id).unwrap().guard(), Guard::Stopped);
assert_eq!(rt.active_count(), 0);
}
#[test]
fn cannot_submit_to_stopped() {
let mut rt = Runtime::new();
let id = rt.spawn("dead");
rt.submit(id, Task::Shutdown).unwrap();
rt.run_all();
let result = rt.submit(id, Task::Echo("nope".into()));
assert_eq!(result, Err(RuntimeError::ContextStopped(id)));
}
#[test]
fn submit_to_nonexistent() {
let mut rt = Runtime::new();
let id = ContextId::new(999);
let result = rt.submit(id, Task::Echo("x".into()));
assert_eq!(result, Err(RuntimeError::NoSuchContext(id)));
}
#[test]
fn runtime_log() {
let mut rt = Runtime::new();
rt.spawn("a");
rt.spawn("b");
assert_eq!(rt.log().len(), 2);
assert!(rt.log()[0].contains("spawned"));
}
#[test]
fn context_id_display() {
let id = ContextId::new(42);
assert_eq!(id.to_string(), "ctx-42");
assert_eq!(id.as_u64(), 42);
}
#[test]
fn runtime_default() {
let rt = Runtime::default();
assert_eq!(rt.total_count(), 0);
}
}