use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, Weak};
use std::task::Poll;
use std::time::{Duration, Instant};
use serde::{Deserialize, Serialize};
use tokio::sync::{mpsc, oneshot};
use tracing::debug;
use super::fabric::{Fabric, Inner};
use super::neuron::{Cx, Firing, Neuron, NeuronKind};
use super::signal::{Command, Feedback, NeuronId, Payload, Reported, Signal};
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum MotionError {
Failed(String),
Unrouted(String),
TimedOut,
Cancelled,
}
impl MotionError {
pub fn failed(reason: impl Into<String>) -> Self {
Self::Failed(reason.into())
}
fn status(&self) -> &'static str {
match self {
Self::Failed(_) | Self::Unrouted(_) => "failed",
Self::TimedOut => "timed_out",
Self::Cancelled => "cancelled",
}
}
}
impl std::fmt::Display for MotionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Failed(reason) => f.write_str(reason),
Self::Unrouted(name) => write!(f, "no effector or motor neuron named '{name}'"),
Self::TimedOut => f.write_str("the deadline passed"),
Self::Cancelled => f.write_str("stopped"),
}
}
}
impl std::error::Error for MotionError {}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ActionSpec {
pub name: String,
pub description: String,
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
pub args: serde_json::Value,
}
type MotionFuture = Pin<Box<dyn Future<Output = Reported> + Send + 'static>>;
type Program = dyn Fn(Motion) -> MotionFuture + Send + Sync;
struct Running {
generation: u64,
_stop: oneshot::Sender<()>,
}
#[derive(Default)]
struct MotorCounters {
started: AtomicU64,
done: AtomicU64,
failed: AtomicU64,
timed_out: AtomicU64,
cancelled: AtomicU64,
}
struct MotorInner {
name: Arc<str>,
program: Box<Program>,
running: Mutex<Option<Running>>,
generations: AtomicU64,
counters: MotorCounters,
}
impl MotorInner {
fn running(&self) -> std::sync::MutexGuard<'_, Option<Running>> {
self.running.lock().unwrap_or_else(|e| e.into_inner())
}
}
pub struct Motor {
inner: Arc<MotorInner>,
}
impl Motor {
pub fn new<F, Fut>(name: &str, program: F) -> Self
where
F: Fn(Motion) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<serde_json::Value, MotionError>> + Send + 'static,
{
Self {
inner: Arc::new(MotorInner {
name: Arc::from(name),
program: Box::new(move |motion| Box::pin(program(motion))),
running: Mutex::new(None),
generations: AtomicU64::new(0),
counters: MotorCounters::default(),
}),
}
}
}
impl Neuron for Motor {
fn name(&self) -> &str {
&self.inner.name
}
fn kind(&self) -> NeuronKind {
NeuronKind::Motor
}
fn stimulate(&self, mut signal: Signal, cx: &Cx<'_>) -> Firing {
let inner = &self.inner;
let outcome = signal.feedback.take().and_then(|f| f.take_outcome());
let args = match signal.payload.as_command() {
Some(command) => command.args.clone(),
None => signal.payload.to_json().unwrap_or_default(),
};
let (stop, stopped) = oneshot::channel();
let generation = inner.generations.fetch_add(1, Ordering::Relaxed);
let preempted = inner.running().replace(Running {
generation,
_stop: stop,
});
drop(preempted);
inner.counters.started.fetch_add(1, Ordering::Relaxed);
let deadline = signal.urgency.deadline;
let upward = outcome.is_none().then(|| signal.clone());
let program = (inner.program)(Motion {
fabric: Arc::downgrade(&cx.fabric().0),
id: cx.id,
name: inner.name.clone(),
args,
signal,
});
let started = Started {
inner: inner.clone(),
fabric: Arc::downgrade(&cx.fabric().0),
id: cx.id,
generation,
stopped,
outcome,
deadline,
upward,
};
cx.efferent().spawn(started.run(program));
Firing::Fire(Vec::new())
}
fn stop(&self) -> bool {
self.inner.running().take().is_some()
}
fn describe(&self) -> serde_json::Value {
serde_json::json!({"running": self.inner.running().is_some()})
}
fn metrics(&self) -> serde_json::Value {
let c = &self.inner.counters;
let load = |a: &AtomicU64| a.load(Ordering::Relaxed);
serde_json::json!({
"started": load(&c.started),
"done": load(&c.done),
"failed": load(&c.failed),
"timed_out": load(&c.timed_out),
"cancelled": load(&c.cancelled),
"running": u8::from(self.inner.running().is_some()),
})
}
}
struct Started {
inner: Arc<MotorInner>,
fabric: Weak<Inner>,
id: NeuronId,
generation: u64,
stopped: oneshot::Receiver<()>,
outcome: Option<oneshot::Sender<Reported>>,
deadline: Option<Instant>,
upward: Option<Signal>,
}
impl Started {
async fn run(mut self, program: MotionFuture) {
let result = {
let outcome = &mut self.outcome;
let abandoned = async {
match outcome.as_mut() {
Some(sender) => sender.closed().await,
None => std::future::pending().await,
}
};
let deadline = self.deadline;
let expired = async {
match deadline {
Some(deadline) => tokio::time::sleep_until(deadline.into()).await,
None => std::future::pending().await,
}
};
tokio::select! {
biased;
_ = &mut self.stopped => Err(MotionError::Cancelled),
() = abandoned => Err(MotionError::Cancelled),
() = expired => Err(MotionError::TimedOut),
result = program => result,
}
};
let inner = &self.inner;
{
let mut running = inner.running();
if running
.as_ref()
.is_some_and(|r| r.generation == self.generation)
{
*running = None;
}
}
let counters = &inner.counters;
let counter = match &result {
Ok(_) => &counters.done,
Err(MotionError::TimedOut) => &counters.timed_out,
Err(MotionError::Cancelled) => &counters.cancelled,
Err(_) => &counters.failed,
};
counter.fetch_add(1, Ordering::Relaxed);
match &result {
Ok(_) => debug!("nerve: motor '{}' done", inner.name),
Err(e) => debug!("nerve: motor '{}' {}: {e}", inner.name, e.status()),
}
if let Some(sender) = self.outcome {
let _ = sender.send(result);
} else if let (Some(signal), Some(fabric)) = (self.upward, self.fabric.upgrade()) {
let mut report = serde_json::json!({"action": &*inner.name});
match result {
Ok(value) => {
report["status"] = "done".into();
report["result"] = value;
}
Err(e) => {
report["status"] = e.status().into();
report["reason"] = e.to_string().into();
}
}
let mut report = signal.derive(Payload::json(report));
report.ticket = None;
Fabric(fabric).emit(self.id, report);
}
}
}
pub struct Motion {
fabric: Weak<Inner>,
id: NeuronId,
name: Arc<str>,
args: serde_json::Value,
signal: Signal,
}
impl Motion {
pub fn args(&self) -> &serde_json::Value {
&self.args
}
pub fn signal(&self) -> &Signal {
&self.signal
}
pub fn name(&self) -> &str {
&self.name
}
pub fn deadline(&self) -> Option<Instant> {
self.signal.urgency.deadline
}
fn fabric(&self) -> Result<Fabric, MotionError> {
self.fabric
.upgrade()
.map(Fabric)
.ok_or(MotionError::Cancelled)
}
fn send(&self, to: &str, args: serde_json::Value) -> oneshot::Receiver<Reported> {
let (feedback, receiver) = Feedback::outcome();
let mut signal = self.signal.command(to, args);
signal.feedback = Some(feedback);
match self.fabric() {
Ok(fabric) => {
fabric.emit(self.id, signal);
}
Err(e) => signal.report(Err(e)),
}
receiver
}
pub async fn command(
&self,
to: &str,
args: serde_json::Value,
) -> Result<serde_json::Value, MotionError> {
outcome(self.send(to, args).await)
}
pub async fn together(
&self,
commands: impl IntoIterator<Item = Command>,
) -> Result<Vec<serde_json::Value>, MotionError> {
let mut pending: Vec<Option<oneshot::Receiver<Reported>>> = commands
.into_iter()
.map(|c| Some(self.send(&c.effector, c.args)))
.collect();
let mut values = vec![serde_json::Value::Null; pending.len()];
std::future::poll_fn(|cx| {
for (slot, value) in pending.iter_mut().zip(values.iter_mut()) {
let Some(receiver) = slot else {
continue;
};
if let Poll::Ready(received) = Pin::new(receiver).poll(cx) {
*slot = None;
match outcome(received) {
Ok(v) => *value = v,
Err(e) => return Poll::Ready(Err(e)),
}
}
}
if pending.iter().all(Option::is_none) {
Poll::Ready(Ok(std::mem::take(&mut values)))
} else {
Poll::Pending
}
})
.await
}
pub fn sense(&self, neuron: &str) -> Result<Sense, MotionError> {
let fabric = self.fabric()?;
let id = fabric
.neuron(neuron)
.ok_or_else(|| MotionError::Unrouted(neuron.to_string()))?;
let (sender, receiver) = mpsc::channel(SENSE_CAPACITY);
fabric.0.cells[id.index()].taps.add(sender);
Ok(Sense { receiver })
}
pub async fn ask(&self, question: Payload) -> Result<Vec<Signal>, MotionError> {
let (feedback, mut answers) = Feedback::answers(self.id);
let mut signal = self.signal.derive(question);
signal.feedback = Some(feedback);
let outcome = {
let fabric = self.fabric()?;
if fabric.0.cells[self.id.index()].out.is_empty() {
return Err(MotionError::failed(format!(
"'{}' has no pathway to ask along",
self.name
)));
}
fabric.emit(self.id, signal)
};
if outcome.no_pathway > 0 {
return Err(MotionError::failed(
"no pathway is fast enough for the deadline",
));
}
if outcome.dropped > 0 {
return Err(MotionError::failed("the pathway's queue is full"));
}
let mut received = Vec::new();
while let Some(answer) = answers.recv().await {
received.push(answer);
}
Ok(received)
}
pub async fn within<T>(
&self,
limit: Duration,
step: impl Future<Output = Result<T, MotionError>>,
) -> Result<T, MotionError> {
tokio::time::timeout(limit, step)
.await
.unwrap_or(Err(MotionError::TimedOut))
}
pub fn stop(&self, action: &str) -> bool {
self.fabric().is_ok_and(|f| f.stop(action))
}
}
fn outcome(received: Result<Reported, oneshot::error::RecvError>) -> Reported {
received.unwrap_or(Ok(serde_json::Value::Null))
}
const SENSE_CAPACITY: usize = 64;
pub struct Sense {
receiver: mpsc::Receiver<Signal>,
}
impl Sense {
pub async fn next(&mut self) -> Option<Signal> {
self.receiver.recv().await
}
pub async fn until(&mut self, mut f: impl FnMut(&Signal) -> bool) -> Option<Signal> {
while let Some(signal) = self.next().await {
if f(&signal) {
return Some(signal);
}
}
None
}
}
#[derive(Default)]
pub(crate) struct Taps {
count: AtomicU64,
senders: Mutex<Vec<mpsc::Sender<Signal>>>,
}
impl Taps {
fn add(&self, sender: mpsc::Sender<Signal>) {
let mut senders = self.senders.lock().unwrap_or_else(|e| e.into_inner());
senders.push(sender);
self.count.store(senders.len() as u64, Ordering::Relaxed);
}
pub(crate) fn copy(&self, signal: &Signal) {
if self.count.load(Ordering::Relaxed) == 0 {
return;
}
let mut senders = self.senders.lock().unwrap_or_else(|e| e.into_inner());
senders.retain(|s| {
!matches!(
s.try_send(signal.observed()),
Err(mpsc::error::TrySendError::Closed(_))
)
});
self.count.store(senders.len() as u64, Ordering::Relaxed);
}
}
#[cfg(test)]
mod tests;