use super::Progress;
use crate::proto;
use fusee::runtime::Due;
use fusee_core::wire::{put_bytes, put_u32, put_u64};
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::Receiver;
use std::sync::{Mutex, MutexGuard};
use std::time::{Duration, Instant};
pub(crate) const LINGER: Duration = Duration::from_millis(250);
pub(crate) const ACK_WAIT: Duration = Duration::from_secs(3);
pub(crate) const QUIET_FOR: Duration = Duration::from_millis(100);
const ASK_AGAIN: Duration = Duration::from_millis(20);
fn margin(budget: Duration) -> Duration {
(budget / 10).min(Duration::from_secs(5))
}
type Reply = Result<Vec<u8>, String>;
struct Report {
due: Due,
closed: bool,
at: Instant,
now_ms: u64,
unacked_since: Option<Instant>,
}
enum Drain {
No,
Sent(Receiver<Reply>),
Done(Instant),
}
struct State {
reports: Vec<Option<Report>>,
calls: HashMap<u64, bool>,
drain: Drain,
awaiting: Vec<String>,
expired: bool,
asked: Vec<Option<Instant>>,
traced: Option<Instant>,
quiet_since: Option<Instant>,
}
pub(crate) struct Invoked {
pub(crate) id: u64,
stop_at: Instant,
want: Vec<AtomicBool>,
state: Mutex<State>,
finished: AtomicBool,
leaving: AtomicBool,
}
impl Invoked {
pub(crate) fn new(id: u64, shards: u32, remaining: Duration) -> Self {
Self {
id,
stop_at: Instant::now() + remaining.saturating_sub(margin(remaining)),
want: (0..shards).map(|_| AtomicBool::new(false)).collect(),
state: Mutex::new(State {
reports: (0..shards).map(|_| None).collect(),
calls: HashMap::new(),
drain: Drain::No,
awaiting: Vec::new(),
expired: false,
asked: (0..shards).map(|_| None).collect(),
traced: None,
quiet_since: None,
}),
finished: AtomicBool::new(false),
leaving: AtomicBool::new(false),
}
}
fn state(&self) -> MutexGuard<'_, State> {
self.state.lock().unwrap_or_else(|e| e.into_inner())
}
pub(crate) fn wanted(&self, shard: u32) -> bool {
self.want
.get(shard as usize)
.is_some_and(|w| w.load(Ordering::Relaxed))
}
pub(crate) fn report(&self, shard: u32, due: Due, closed: bool) {
if let Some(w) = self.want.get(shard as usize) {
w.store(false, Ordering::Relaxed);
}
let mut st = self.state();
if let Some(slot) = st.reports.get_mut(shard as usize) {
let now = Instant::now();
let unacked_since = match slot.as_ref().and_then(|r| r.unacked_since) {
_ if due.unacked == 0 => None,
Some(t) => Some(t),
None => Some(now),
};
*slot = Some(Report {
due,
closed,
at: now,
now_ms: fusee::local::unix_ms(),
unacked_since,
});
}
}
pub(crate) fn called(&self, relay: u64, method: &str) {
let waits = matches!(method, "result" | "streamRead");
self.state().calls.insert(relay, waits);
}
pub(crate) fn answered(&self, relay: u64, failed: bool) -> bool {
let waits = self.state().calls.remove(&relay);
!(failed && waits == Some(true) && self.leaving.load(Ordering::Acquire))
}
fn idle(r: &Report, awaiting: &[String]) -> bool {
let lacks_code = |t: &String| awaiting.contains(t) || r.due.awaiting.contains(t);
!r.due.busy
&& r.unacked_since.is_none_or(|t| t.elapsed() >= ACK_WAIT)
&& r.due.running.iter().all(lacks_code)
&& r.due
.at_ms
.is_none_or(|at| at >= r.now_ms.saturating_add(LINGER.as_millis() as u64))
}
pub(crate) fn poll(
&self,
held: &[u32],
awaiting: &[String],
notify: &dyn Fn(u32),
drain: &dyn Fn(&[u32]) -> std::io::Result<Receiver<Reply>>,
) -> Progress {
let p = self.look(held, awaiting, notify, drain);
if p != Progress::Running && !self.leaving.swap(true, Ordering::AcqRel) {
let undrained = matches!(self.state().drain, Drain::No);
if p == Progress::Expired && undrained && !held.is_empty() {
let _ = drain(held);
}
}
p
}
fn look(
&self,
held: &[u32],
awaiting: &[String],
notify: &dyn Fn(u32),
drain: &dyn Fn(&[u32]) -> std::io::Result<Receiver<Reply>>,
) -> Progress {
if self.finished.load(Ordering::Acquire) {
return Progress::Idle;
}
let now = Instant::now();
let mut st = self.state();
st.awaiting = awaiting.to_vec();
if now >= self.stop_at {
st.expired = true;
return Progress::Expired;
}
if held.is_empty() {
return Progress::Idle;
}
for &s in held {
let Some(w) = self.want.get(s as usize) else {
continue;
};
let stale = st.asked[s as usize].is_none_or(|t| t.elapsed() >= ASK_AGAIN);
if !w.swap(true, Ordering::Relaxed) || stale {
st.asked[s as usize] = Some(now);
drop(st);
notify(s);
st = self.state();
}
}
if super::tracing()
&& st
.traced
.is_none_or(|t| t.elapsed() >= Duration::from_secs(1))
{
st.traced = Some(now);
for &s in held {
if let Some(r) = st.reports.get(s as usize).and_then(Option::as_ref) {
super::trace!(
"invocation {:016x} shard {s}: busy {} running {:?} awaiting {:?} due in {:?} ms, unacked {}, report {} ms old, calls {:?}",
self.id,
r.due.busy,
r.due.running,
r.due.awaiting,
r.due
.at_ms
.map(|a| a as i64 - fusee::local::unix_ms() as i64),
r.due.unacked,
r.at.elapsed().as_millis(),
st.calls
);
} else {
super::trace!("invocation {:016x} shard {s}: no report yet", self.id);
}
}
}
let since = match &st.drain {
Drain::Done(at) => Some(*at),
_ => None,
};
let quiet = st.calls.values().all(|waits| *waits)
&& held.iter().all(|&s| {
st.reports
.get(s as usize)
.and_then(Option::as_ref)
.is_some_and(|r| since.is_none_or(|t| r.at > t) && Self::idle(r, awaiting))
});
if !quiet {
st.quiet_since = None;
} else if st.quiet_since.is_none() {
st.quiet_since = Some(now);
}
let settled = st.quiet_since.is_some_and(|t| now - t >= QUIET_FOR);
match &st.drain {
Drain::No if settled => {
drop(st);
let sent = drain(held);
st = self.state();
match sent {
Ok(rx) => st.drain = Drain::Sent(rx),
Err(_) => return Progress::Lost,
}
}
Drain::Sent(rx) => match rx.try_recv() {
Ok(_) => st.drain = Drain::Done(Instant::now()),
Err(std::sync::mpsc::TryRecvError::Empty) => {}
Err(std::sync::mpsc::TryRecvError::Disconnected) => return Progress::Lost,
},
Drain::Done(_) if quiet => return Progress::Idle,
_ => {}
}
Progress::Running
}
pub(crate) fn finish(&self, held: &[(u32, u64)]) -> Option<Vec<u8>> {
if self.finished.swap(true, Ordering::AcqRel) {
return None;
}
let st = self.state();
let now_ms = fusee::local::unix_ms();
let mut entries = Vec::new();
for &(shard, epoch) in held {
let Some(r) = st
.reports
.get(shard as usize)
.and_then(Option::as_ref)
.filter(|r| r.closed)
else {
continue;
};
let lacks_code = |t: &String| st.awaiting.contains(t) || r.due.awaiting.contains(t);
let busy = r.due.busy || r.due.running.iter().any(|t| !lacks_code(t));
let mut types: Vec<&String> = r.due.awaiting.iter().collect();
for t in r.due.running.iter().chain(&st.awaiting) {
if lacks_code(t) && r.due.running.contains(t) && !types.contains(&t) {
types.push(t);
}
}
let mut flags = 0;
if busy {
flags |= proto::IDLE_BUSY;
}
if st.expired {
flags |= proto::IDLE_EXPIRED;
}
let due_in = if busy {
0
} else {
r.due.at_ms.map_or(u64::MAX, |at| at.saturating_sub(now_ms))
};
let mut e = Vec::new();
put_u32(&mut e, shard);
put_u64(&mut e, epoch);
e.push(flags);
put_u64(&mut e, due_in);
put_u64(&mut e, r.due.steps);
put_u64(&mut e, r.due.open_us);
put_u32(&mut e, types.len() as u32);
for t in types {
put_bytes(&mut e, t.as_bytes());
}
entries.push(e);
}
let mut body = Vec::new();
proto::head(
&mut body,
proto::Head {
id: 0,
shard: 0,
epoch: 0,
},
);
put_u32(&mut body, entries.len() as u32);
for e in entries {
body.extend_from_slice(&e);
}
Some(body)
}
pub(crate) fn steps(&self) -> u64 {
self.state()
.reports
.iter()
.flatten()
.map(|r| r.due.steps)
.sum()
}
pub(crate) fn expired(&self) -> bool {
self.state().expired
}
}