use crate::record::{
Lsn, Name, Origin, PAUSED, ParentRef, Payload, Record, RecordBuf, RunId, Seq, Status, ToName,
ToPayload, WaitOutcome, decode_canonical_all, policy,
};
use crate::table::{self, Entry, RunTable, Slot, flag};
use crate::wheel::Wheel;
use crate::wire::{checksum64, hash_id};
use rustc_hash::{FxHashMap, FxHashSet};
use std::cmp::Reverse;
use std::collections::{BinaryHeap, VecDeque};
use std::sync::Arc;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum TimerKind {
Sleep = 0,
Retry = 1,
WaitTimeout = 2,
Deadline = 3,
}
impl TimerKind {
pub fn from_u8(v: u8) -> Option<Self> {
Some(match v {
0 => Self::Sleep,
1 => Self::Retry,
2 => Self::WaitTimeout,
3 => Self::Deadline,
_ => return None,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TimerKey {
pub run: RunId,
pub seq: Seq,
pub kind: TimerKind,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event {
Started { run: RunId },
Timer {
run: RunId,
seq: Seq,
kind: TimerKind,
},
SignalReady {
run: RunId,
seq: Seq,
ordinal: u64,
payload: Payload,
},
Finished { run: RunId },
NeedLoad { run: RunId },
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Output {
Ack(u64),
Reject(u64),
Result {
run: RunId,
status: Status,
payload: Payload,
},
ChildStart {
parent: ParentRef,
wtype: Name,
id: Name,
flags: u8,
deadline_ms: u64,
input: Payload,
},
ChildResult {
parent: ParentRef,
status: Status,
payload: Payload,
},
Signal {
target: Name,
name: Name,
payload: Payload,
dedup: u64,
},
UpdateResult {
run: RunId,
ordinal: u64,
id: Name,
status: Status,
payload: Payload,
},
Reply {
token: u64,
status: Status,
payload: Payload,
},
Refuse { token: u64, message: Payload },
Close {
target: Name,
wtype: Name,
parent: Option<ParentRef>,
terminate: bool,
reason: Payload,
},
}
pub const CANCEL_SIGNAL: &str = "fusee.cancel";
#[derive(Clone, Debug)]
pub struct UpdateSlot {
pub id: Name,
pub name: Name,
pub caller: Option<ParentRef>,
pub result: Option<(Status, Payload, Lsn)>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct BlobRef {
pub offset: u64,
pub len: u32,
}
#[derive(Clone, Debug)]
struct SignalSlot {
ordinal: u64,
name: Name,
payload: Payload,
offered: Option<Seq>,
}
#[derive(Clone, Debug)]
struct WaitSlot {
seq: Seq,
name: Name,
offered: Option<u64>,
}
#[derive(Clone, Debug)]
pub struct LoadedRun {
pub id: Name,
pub wtype: Name,
pub version: u32,
pub codec: u8,
pub parent: Option<ParentRef>,
pub started_ms: u64,
pub deadline_ms: u64,
pub last_lsn: Lsn,
pub done: Option<(Status, u64)>,
pub result: Option<Payload>,
pub blob: Option<BlobRef>,
pub blob_chain: u64,
pub blob_depth: u8,
pub journal: Vec<u8>,
pub journal_records: u32,
signal_count: u64,
signals: VecDeque<SignalSlot>,
waits: Vec<WaitSlot>,
armed: Vec<(Seq, TimerKind, u64)>,
dedup: FxHashSet<u64>,
children: Vec<Seq>,
pub policy: u8,
pub from: Option<Origin>,
copies_left: u32,
pub updates: Vec<UpdateSlot>,
update_ids: FxHashMap<Name, u64>,
pub awaiting: FxHashMap<Seq, u32>,
kids: Vec<(Seq, Name, Name, u8)>,
pub attrs: Vec<Payload>,
pub streams: bool,
queued: u32,
}
impl LoadedRun {
pub fn is_done(&self) -> bool {
self.done.is_some()
}
pub fn has_queued_events(&self) -> bool {
self.queued > 0
}
pub fn armed_timers(&self) -> impl Iterator<Item = (Seq, TimerKind, u64)> + '_ {
self.armed.iter().copied()
}
pub fn waiting_on(&self) -> impl Iterator<Item = (Seq, &str)> + '_ {
self.waits.iter().map(|w| (w.seq, &*w.name))
}
pub fn pending_children(&self) -> &[Seq] {
&self.children
}
pub fn buffered_signals(&self) -> usize {
self.signals.len()
}
pub fn update(&self, id: &str) -> Option<(u64, &UpdateSlot)> {
let o = *self.update_ids.get(id)?;
Some((o, self.updates.get(o as usize)?))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ParkedRun {
offset: u64,
len: u32,
done: u8,
}
impl ParkedRun {
pub fn new(blob: BlobRef, done: Option<Status>) -> Self {
Self {
offset: blob.offset,
len: blob.len,
done: done.map(|s| 1 + s as u8).unwrap_or(0),
}
}
fn of(e: &Entry) -> Self {
Self::new(
BlobRef {
offset: e.offset(),
len: e.len(),
},
e.done(),
)
}
pub fn blob(&self) -> BlobRef {
BlobRef {
offset: self.offset,
len: self.len,
}
}
pub fn done(&self) -> Option<Status> {
self.done
.checked_sub(1)
.and_then(|s| Status::from_u8(s).ok())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Found {
Loaded(RunId),
Parked(RunId),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Mode {
pub journal: bool,
pub outputs: bool,
pub events: bool,
}
impl Mode {
pub const LIVE: Self = Self {
journal: true,
outputs: true,
events: true,
};
pub const RECOVER: Self = Self {
journal: true,
outputs: true,
events: false,
};
pub const BLOB: Self = Self {
journal: false,
outputs: false,
events: false,
};
pub const SUFFIX: Self = Self {
journal: true,
outputs: false,
events: false,
};
}
pub struct ShardCore {
pub shard: u32,
pub shards: u32,
pub retention_ms: u64,
next_run: RunId,
loaded: FxHashMap<RunId, Box<LoadedRun>>,
runs: RunTable,
parked_count: usize,
wheel: Wheel<TimerKey>,
near: Wheel<RunId>,
far: Vec<u64>,
far_min: u64,
sweep_at: u64,
sweep_full: bool,
slots_held: bool,
outputs: VecDeque<(Lsn, Output)>,
events: VecDeque<Event>,
retention: BinaryHeap<Reverse<(u64, RunId)>>,
fired: Vec<TimerKey>,
due: Vec<TimerKey>,
#[allow(clippy::vec_box)]
spare: Vec<Box<LoadedRun>>,
backlog: VecDeque<TimerKey>,
backlog_at: u64,
near_fired: Vec<RunId>,
near_backlog: VecDeque<RunId>,
near_backlog_at: u64,
changed: Vec<RunId>,
deleted: Vec<RunId>,
index_rebase: bool,
journal_bytes: usize,
}
pub const HORIZON_MS: u64 = 1 << 16;
const SWEEP_MS: u64 = HORIZON_MS / 16;
pub const TIMER_CHUNK: usize = 4096;
const BUCKET_BITS: u32 = 12;
#[derive(Debug)]
pub struct NewestFirst {
below: RunId,
probes: usize,
rest: Option<Vec<RunId>>,
}
impl NewestFirst {
pub fn next_batch(&mut self, core: &ShardCore, n: usize) -> Vec<RunId> {
let mut out = Vec::with_capacity(n.min(4096));
let budget = core.runs.len().saturating_mul(2).saturating_add(1024);
while out.len() < n {
if let Some(rest) = &mut self.rest {
match rest.pop() {
Some(r) => out.push(r),
None => break,
}
continue;
}
if self.below <= 1 {
break;
}
if self.probes >= budget {
let below = self.below;
let mut rest: Vec<RunId> = core
.runs
.iter()
.map(|(_, e)| e.run())
.filter(|&r| r < below)
.collect();
rest.sort_unstable();
self.rest = Some(rest);
continue;
}
self.below -= 1;
self.probes += 1;
if core.runs.has_run(self.below) {
out.push(self.below);
}
}
out
}
}
pub fn shard_of(id: &str, shards: u32) -> u32 {
(hash_id(id) % u64::from(shards.max(1))) as u32
}
pub fn send_dedup(shard: u32, run: RunId, seq: Seq) -> u64 {
let mut key = [0u8; 16];
key[..4].copy_from_slice(&shard.to_le_bytes());
key[4..12].copy_from_slice(&run.to_le_bytes());
key[12..].copy_from_slice(&seq.to_le_bytes());
checksum64(&[&key]) | 1
}
impl ShardCore {
pub fn new(shard: u32, shards: u32, retention_ms: u64, now_ms: u64) -> Self {
Self {
shard,
shards,
retention_ms,
next_run: 1,
loaded: FxHashMap::default(),
runs: RunTable::new(),
parked_count: 0,
wheel: Wheel::new(now_ms),
near: Wheel::new(now_ms),
far: Vec::new(),
far_min: u64::MAX,
sweep_at: now_ms.saturating_sub(SWEEP_MS),
sweep_full: false,
slots_held: false,
outputs: VecDeque::new(),
events: VecDeque::new(),
retention: BinaryHeap::new(),
fired: Vec::new(),
due: Vec::new(),
spare: Vec::new(),
backlog: VecDeque::new(),
backlog_at: 0,
near_fired: Vec::new(),
near_backlog: VecDeque::new(),
near_backlog_at: 0,
changed: Vec::new(),
deleted: Vec::new(),
index_rebase: false,
journal_bytes: 0,
}
}
fn waits_far(e: &Entry) -> Option<u64> {
let t = e.time();
let waits = t != 0
&& !e.has(flag::NEAR)
&& !e.has(flag::PENDING)
&& (e.state() == table::LOADED || e.state() == table::PARKED);
waits.then_some(t)
}
fn noted(&mut self, slot: Slot, was_parked: bool) {
let e = self.runs.get(slot);
let parked = e.is_parked();
self.parked_count = self.parked_count + usize::from(parked) - usize::from(was_parked);
if let Some(t) = Self::waits_far(e) {
let c = (slot >> BUCKET_BITS) as usize;
if self.far.len() <= c {
self.far.resize(c + 1, u64::MAX);
}
self.far[c] = self.far[c].min(t);
self.far_min = self.far_min.min(t);
}
}
fn edit(&mut self, slot: Slot, f: impl FnOnce(&mut Entry)) {
let was = self.runs.get(slot).is_parked();
f(self.runs.get_mut(slot));
self.noted(slot, was);
}
fn table_insert(&mut self, e: Entry) -> Slot {
let slot = self.runs.insert(e);
self.noted(slot, false);
slot
}
fn table_remove(&mut self, slot: Slot) {
let e = self.runs.remove(slot);
self.parked_count -= usize::from(e.is_parked());
if !self.index_rebase {
self.deleted.push(e.run());
self.check_index_lists();
}
}
fn mark_changed(&mut self, slot: Slot) {
let e = self.runs.get_mut(slot);
if !e.has(flag::DIRTY) {
e.set(flag::DIRTY, true);
let run = e.run();
if !self.index_rebase {
self.changed.push(run);
self.check_index_lists();
}
}
}
fn index_list_cap(&self) -> usize {
(self.parked_count / 8).max(1 << 16)
}
fn check_index_lists(&mut self) {
if self.changed.len() + self.deleted.len() > self.index_list_cap() {
self.index_rebase = true;
self.changed = Vec::new();
self.deleted = Vec::new();
}
}
fn note_loaded(&mut self, run: RunId, id_hash: u64) {
let slot = match self.runs.find_run(run) {
Some(slot) => {
self.edit(slot, |e| {
e.set_state(table::LOADED);
e.set_time(0);
e.set(flag::NEAR | flag::PENDING, false);
});
slot
}
None => self.table_insert(Entry::new(run, id_hash, table::LOADED)),
};
self.runs.index_id(slot);
}
pub fn next_run_id(&self) -> RunId {
self.next_run
}
pub fn set_next_run_id(&mut self, next: RunId) {
self.next_run = self.next_run.max(next);
}
pub fn allocate_run(&mut self) -> RunId {
let r = self.next_run;
self.next_run += 1;
r
}
pub fn find(&self, id: &str) -> Option<Found> {
let e = self.runs.get(self.runs.find_id(hash_id(id))?);
let run = e.run();
if !e.is_parked() {
let l = self.loaded.get(&run)?;
return (*l.id == *id).then_some(Found::Loaded(run));
}
Some(Found::Parked(run))
}
pub fn get(&self, run: RunId) -> Option<&LoadedRun> {
self.loaded.get(&run).map(|l| &**l)
}
fn parked_entry(&self, run: RunId) -> Option<&Entry> {
let e = self.runs.get(self.runs.find_run(run)?);
e.is_parked().then_some(e)
}
pub fn parked(&self, run: RunId) -> Option<ParkedRun> {
self.parked_entry(run).map(ParkedRun::of)
}
pub fn is_parked(&self, run: RunId) -> bool {
self.parked_entry(run).is_some()
}
pub fn chained_runs(&self) -> impl Iterator<Item = (RunId, BlobRef)> + '_ {
self.runs
.iter()
.filter(|(_, e)| e.is_parked() && e.has(flag::CHAINED))
.map(|(_, e)| {
(
e.run(),
BlobRef {
offset: e.offset(),
len: e.len(),
},
)
})
}
pub fn reparked(&mut self, run: RunId, blob: BlobRef) {
if let Some(slot) = self.runs.find_run(run)
&& self.runs.get(slot).is_parked()
{
self.runs.get_mut(slot).set_location(blob.offset, blob.len);
self.runs.get_mut(slot).set(flag::CHAINED, false);
self.mark_changed(slot);
}
}
pub fn loaded_runs(&self) -> impl Iterator<Item = (RunId, &LoadedRun)> {
self.loaded.iter().map(|(k, v)| (*k, &**v))
}
pub fn parked_runs(&self) -> impl Iterator<Item = (RunId, ParkedRun)> + '_ {
self.runs
.iter()
.filter(|(_, e)| e.is_parked())
.map(|(_, e)| (e.run(), ParkedRun::of(e)))
}
pub fn newest_first(&self) -> NewestFirst {
NewestFirst {
below: self.next_run,
probes: 0,
rest: None,
}
}
pub fn loaded_len(&self) -> usize {
self.loaded.len()
}
pub fn parked_len(&self) -> usize {
self.parked_count
}
pub fn timers_len(&self) -> usize {
self.wheel.len() + self.backlog.len() + self.near.len() + self.near_backlog.len()
}
pub fn table_bytes(&self) -> usize {
self.runs.heap_bytes() + self.retention.capacity() * 16 + self.near.len() * 16
}
pub fn seen_signal(&self, run: RunId, dedup: u64) -> bool {
dedup != 0
&& self
.loaded
.get(&run)
.is_some_and(|l| l.dedup.contains(&dedup))
}
pub fn add_output(&mut self, lsn: Lsn, output: Output) {
self.outputs.push_back((lsn, output));
}
pub fn release(&mut self, durable: Lsn, out: &mut Vec<Output>) {
while let Some((lsn, _)) = self.outputs.front() {
if *lsn > durable {
break;
}
if let Some((_, o)) = self.outputs.pop_front() {
out.push(o);
}
}
}
pub fn pending_outputs(&self) -> usize {
self.outputs.len()
}
pub fn unreleased(&self) -> impl Iterator<Item = &Output> {
self.outputs.iter().map(|(_, o)| o)
}
pub fn take_event(&mut self) -> Option<Event> {
let ev = self.events.pop_front()?;
if let Event::Timer { run, .. } | Event::SignalReady { run, .. } = &ev
&& let Some(l) = self.loaded.get_mut(run)
{
l.queued = l.queued.saturating_sub(1);
}
Some(ev)
}
pub fn has_events(&self) -> bool {
!self.events.is_empty()
}
pub fn next_timer(&self) -> Option<u64> {
if !self.backlog.is_empty() {
return Some(self.backlog_at);
}
if !self.near_backlog.is_empty() {
return Some(self.near_backlog_at);
}
let sweep = if self.sweep_full {
Some(self.sweep_at)
} else {
(self.far_min != u64::MAX).then(|| {
self.far_min
.saturating_sub(HORIZON_MS - 1)
.max(self.sweep_at.saturating_add(SWEEP_MS))
})
};
fn wake<K: Copy>(w: &Wheel<K>) -> Option<u64> {
if w.is_empty() { None } else { w.next_wake() }
}
[wake(&self.wheel), wake(&self.near), sweep]
.into_iter()
.flatten()
.min()
}
fn sweep(&mut self, now_ms: u64) {
if !self.sweep_full
&& (now_ms < self.sweep_at.saturating_add(SWEEP_MS)
|| self.far_min >= now_ms.saturating_add(HORIZON_MS))
{
return;
}
if self.sweep_full {
let chunks = (self.runs.used() as usize).div_ceil(1 << BUCKET_BITS);
self.far = vec![0; chunks];
self.sweep_full = false;
}
self.sweep_at = now_ms;
let end = now_ms.saturating_add(HORIZON_MS);
let used = self.runs.used();
let mut far_min = u64::MAX;
for c in 0..self.far.len() {
if self.far[c] >= end {
far_min = far_min.min(self.far[c]);
continue;
}
let mut min = u64::MAX;
let from = (c as u32) << BUCKET_BITS;
let to = from.saturating_add(1 << BUCKET_BITS).min(used);
for slot in from..to {
let Some(e) = self.runs.live_at(slot) else {
continue;
};
let Some(t) = Self::waits_far(e) else {
continue;
};
if t >= end {
min = min.min(t);
continue;
}
let (run, state) = (e.run(), e.state());
if state == table::PARKED {
self.near.insert(t, run);
self.runs.get_mut(slot).set(flag::NEAR, true);
} else {
self.rearm(run, slot, end);
if let Some(t) = Self::waits_far(self.runs.get(slot)) {
min = min.min(t);
}
}
}
self.far[c] = min;
far_min = far_min.min(min);
}
self.far_min = far_min;
}
fn horizon_end(&self) -> u64 {
self.wheel
.elapsed()
.max(self.near.elapsed())
.max(self.sweep_at)
.saturating_add(HORIZON_MS)
}
fn rearm(&mut self, run_id: RunId, slot: Slot, end: u64) {
let mut later = 0u64;
if let Some(run) = self.loaded.get(&run_id) {
for &(seq, kind, at) in &run.armed {
if at < end {
self.wheel.insert(
at,
TimerKey {
run: run_id,
seq,
kind,
},
);
} else if later == 0 || at < later {
later = at;
}
}
}
self.edit(slot, |e| e.set_time(later));
}
pub fn poll_timers(&mut self, now_ms: u64) {
self.sweep(now_ms);
if self.near_backlog.is_empty() {
let mut fired = std::mem::take(&mut self.near_fired);
fired.clear();
self.near.poll(now_ms, &mut fired);
self.near_backlog.extend(fired.drain(..));
self.near_fired = fired;
self.near_backlog_at = now_ms;
}
for _ in 0..TIMER_CHUNK {
let Some(run) = self.near_backlog.pop_front() else {
break;
};
self.fire_parked(run, now_ms, true);
}
if self.backlog.is_empty() {
let mut fired = std::mem::take(&mut self.fired);
fired.clear();
self.wheel.poll(now_ms, &mut fired);
self.backlog.extend(fired.drain(..));
self.fired = fired;
self.backlog_at = now_ms;
}
for _ in 0..TIMER_CHUNK {
let Some(key) = self.backlog.pop_front() else {
break;
};
self.fire(key, now_ms);
}
}
pub fn peek_event(&self) -> Option<&Event> {
self.events.front()
}
fn fire_parked(&mut self, run: RunId, now_ms: u64, near: bool) {
let Some(slot) = self.runs.find_run(run) else {
return;
};
let e = self.runs.get_mut(slot);
if e.state() != table::PARKED
|| e.has(flag::PENDING)
|| e.time() == 0
|| e.time() > now_ms
|| (near && !e.has(flag::NEAR))
{
return;
}
e.set(flag::NEAR, false);
e.set(flag::PENDING, true);
self.events.push_back(Event::NeedLoad { run });
}
fn fire(&mut self, key: TimerKey, now_ms: u64) {
let Some(run) = self.loaded.get_mut(&key.run) else {
self.fire_parked(key.run, now_ms, false);
return;
};
if run.done.is_some() {
return;
}
let Some(pos) = run
.armed
.iter()
.position(|(s, k, at)| *s == key.seq && *k == key.kind && *at <= now_ms)
else {
return;
};
run.armed.swap_remove(pos);
run.queued += 1;
self.events.push_back(Event::Timer {
run: key.run,
seq: key.seq,
kind: key.kind,
});
}
fn arm(&mut self, run: RunId, seq: Seq, kind: TimerKind, at: u64) {
if let Some(l) = self.loaded.get_mut(&run)
&& !l
.armed
.iter()
.any(|(s, k, a)| *s == seq && *k == kind && *a == at)
{
l.armed.push((seq, kind, at));
}
self.schedule(run, seq, kind, at);
}
fn schedule(&mut self, run: RunId, seq: Seq, kind: TimerKind, at: u64) {
if at < self.horizon_end() {
self.wheel.insert(at, TimerKey { run, seq, kind });
} else if let Some(slot) = self.runs.find_run(run) {
let t = self.runs.get(slot).time();
if !self.runs.get(slot).is_parked() && (t == 0 || at < t) {
self.edit(slot, |e| e.set_time(at));
}
}
}
fn disarm(run: &mut LoadedRun, seq: Seq, kind: TimerKind) {
run.armed.retain(|(s, k, _)| !(*s == seq && *k == kind));
}
fn offer(&mut self, run_id: RunId, events: bool) {
let Some(run) = self.loaded.get_mut(&run_id) else {
return;
};
for w in run.waits.iter_mut() {
if w.offered.is_some() {
continue;
}
if let Some(sig) = run
.signals
.iter_mut()
.find(|s| s.offered.is_none() && s.name == w.name)
{
sig.offered = Some(w.seq);
w.offered = Some(sig.ordinal);
if events {
run.queued += 1;
self.events.push_back(Event::SignalReady {
run: run_id,
seq: w.seq,
ordinal: sig.ordinal,
payload: sig.payload.clone(),
});
}
}
}
}
pub fn reoffer(&mut self, run_id: RunId) {
if let Some(run) = self.loaded.get_mut(&run_id) {
for w in run.waits.iter_mut() {
w.offered = None;
}
for s in run.signals.iter_mut() {
s.offered = None;
}
}
self.offer(run_id, true);
}
pub fn apply<S: ToName, B: ToPayload>(&mut self, lsn: Lsn, rec: &Record<S, B>, mode: Mode) {
self.apply_with(lsn, rec, mode, None);
}
pub fn apply_encoded<S: ToName, B: ToPayload>(
&mut self,
lsn: Lsn,
rec: &Record<S, B>,
mode: Mode,
canonical: &[u8],
) {
self.apply_with(lsn, rec, mode, Some(canonical));
}
fn apply_with<S: ToName, B: ToPayload>(
&mut self,
lsn: Lsn,
rec: &Record<S, B>,
mode: Mode,
canonical: Option<&[u8]>,
) {
let run_id = rec.run();
if let Record::Start {
run,
id,
wtype,
version,
codec,
started_ms,
deadline_ms,
parent,
policy,
from,
copies,
update,
..
} = rec
{
if self.loaded.contains_key(run) {
return;
}
let id: Name = id.to_name();
self.next_run = self.next_run.max(run + 1);
self.note_loaded(*run, hash_id(&id));
let mut l = self
.spare
.pop()
.unwrap_or_else(|| Box::new(LoadedRun::empty()));
if let Some(u) = update {
let id = u.id.to_name();
l.update_ids.insert(id.clone(), 0);
l.updates.push(UpdateSlot {
id,
name: u.name.to_name(),
caller: None,
result: None,
});
}
l.id = id;
l.wtype = wtype.to_name();
l.version = *version;
l.codec = *codec;
l.parent = *parent;
l.started_ms = *started_ms;
l.deadline_ms = *deadline_ms;
l.last_lsn = lsn;
l.done = None;
l.result = None;
l.blob = None;
l.blob_chain = 0;
l.blob_depth = 0;
l.journal_records = 0;
l.signal_count = 0;
l.policy = *policy;
l.from = *from;
l.copies_left = *copies;
l.streams = false;
l.queued = 0;
self.loaded.insert(*run, l);
if *deadline_ms > 0 {
self.arm(*run, 0, TimerKind::Deadline, *deadline_ms);
}
if mode.events {
self.events.push_back(Event::Started { run: *run });
}
}
let shard = self.shard;
let Some(run) = self.loaded.get_mut(&run_id) else {
return;
};
run.last_lsn = run.last_lsn.max(lsn);
if mode.journal {
let before = run.journal.len();
match canonical {
Some(bytes) => run.journal.extend_from_slice(bytes),
None => crate::record::encode_canonical(rec, &mut run.journal),
}
self.journal_bytes += run.journal.len() - before;
run.journal_records += 1;
}
let copied = !matches!(rec, Record::Start { .. }) && run.copies_left > 0;
if copied {
run.copies_left -= 1;
}
match rec {
Record::Start { .. } => {}
Record::Step { seq, .. } => Self::disarm(run, *seq, TimerKind::Retry),
Record::Attempt {
seq,
retry_at_ms: PAUSED,
..
}
| Record::Resume { seq, .. } => Self::disarm(run, *seq, TimerKind::Retry),
Record::Attempt {
seq, retry_at_ms, ..
} => {
let (seq, at) = (*seq, *retry_at_ms);
self.arm(run_id, seq, TimerKind::Retry, at);
}
Record::Timer {
seq, fire_at_ms, ..
} => {
let (seq, at) = (*seq, *fire_at_ms);
self.arm(run_id, seq, TimerKind::Sleep, at);
}
Record::Fired { seq, .. } => Self::disarm(run, *seq, TimerKind::Sleep),
Record::Signal {
name,
dedup,
payload,
..
} => {
let ordinal = run.signal_count;
run.signal_count += 1;
if *dedup != 0 {
run.dedup.insert(*dedup);
}
run.signals.push_back(SignalSlot {
ordinal,
name: name.to_name(),
payload: payload.to_payload(),
offered: None,
});
self.offer(run_id, mode.events);
}
Record::Wait {
seq,
name,
deadline_ms,
..
} => {
run.waits.push(WaitSlot {
seq: *seq,
name: name.to_name(),
offered: None,
});
let (seq, at) = (*seq, *deadline_ms);
if at > 0 {
self.arm(run_id, seq, TimerKind::WaitTimeout, at);
}
self.offer(run_id, mode.events);
}
Record::Waited { seq, outcome, .. } => {
Self::disarm(run, *seq, TimerKind::WaitTimeout);
let offered = run
.waits
.iter()
.position(|w| w.seq == *seq)
.map(|i| run.waits.remove(i).offered);
match outcome {
WaitOutcome::Signal(ordinal) => {
if let Some(i) = run.signals.iter().position(|s| s.ordinal == *ordinal) {
run.signals.remove(i);
}
if let Some(Some(o)) = offered
&& o != *ordinal
&& let Some(s) = run.signals.iter_mut().find(|s| s.ordinal == o)
{
s.offered = None;
}
}
WaitOutcome::TimedOut | WaitOutcome::Abandoned => {
if let Some(Some(o)) = offered
&& let Some(s) = run.signals.iter_mut().find(|s| s.ordinal == o)
{
s.offered = None;
}
}
}
self.offer(run_id, mode.events);
}
Record::Value { .. } => {}
Record::Child {
seq,
wtype,
id,
flags,
deadline_ms,
input,
..
} => {
if flags & policy::DETACHED == 0 {
run.children.push(*seq);
}
if !run.kids.iter().any(|k| k.0 == *seq) {
run.kids.push((*seq, id.to_name(), wtype.to_name(), *flags));
}
if mode.outputs {
let parent = ParentRef {
shard,
run: run_id,
seq: *seq,
};
self.outputs.push_back((
lsn,
Output::ChildStart {
parent,
wtype: wtype.to_name(),
id: id.to_name(),
flags: *flags,
deadline_ms: *deadline_ms,
input: input.to_payload(),
},
));
}
}
Record::ChildDone { seq, .. } => {
run.children.retain(|s| s != seq);
run.kids.retain(|k| k.0 != *seq);
}
Record::Send {
seq,
target,
name,
payload,
..
} => {
if mode.outputs && !copied {
let target: Name = target.to_name();
let out = if name.as_ref() == CANCEL_SIGNAL {
Output::Close {
target,
wtype: Arc::from(""),
parent: None,
terminate: false,
reason: payload.to_payload(),
}
} else {
Output::Signal {
target,
name: name.to_name(),
payload: payload.to_payload(),
dedup: send_dedup(shard, run_id, *seq),
}
};
self.outputs.push_back((lsn, out));
}
}
Record::Cancel { .. } => {}
Record::Update {
id, name, caller, ..
} => {
let ordinal = run.updates.len() as u64;
let id: Name = id.to_name();
run.update_ids.insert(id.clone(), ordinal);
run.updates.push(UpdateSlot {
id,
name: name.to_name(),
caller: *caller,
result: None,
});
}
Record::UpdateDone {
ordinal,
status,
payload,
..
} => {
let payload: Payload = payload.to_payload();
if let Some(slot) = run.updates.get_mut(*ordinal as usize) {
slot.result = Some((*status, payload.clone(), lsn));
if mode.outputs {
let id = slot.id.clone();
if let (Some(parent), false) = (slot.caller, copied) {
self.outputs.push_back((
lsn,
Output::ChildResult {
parent,
status: *status,
payload: payload.clone(),
},
));
}
self.outputs.push_back((
lsn,
Output::UpdateResult {
run: run_id,
ordinal: *ordinal,
id,
status: *status,
payload,
},
));
}
}
}
Record::Heartbeat { .. } | Record::Patch { .. } => {}
Record::Awaiting { seq, attempt, .. } => {
run.awaiting.insert(*seq, *attempt);
}
Record::Completed { seq, .. } => {
run.awaiting.remove(seq);
}
Record::Attributes { payload, .. } => run.attrs.push(payload.to_payload()),
Record::Chunk { .. } | Record::StreamEnd { .. } => run.streams = true,
Record::Done {
status,
finished_ms,
payload,
..
} => {
if run.done.is_some() {
return;
}
if status.is_handover() {
let next = decode_canonical_all(payload.as_ref())
.ok()
.and_then(|mut v| (v.len() == 1).then(|| v.remove(0)))
.filter(|r| matches!(r, Record::Start { .. }));
let (status, finished) = (*status, *finished_ms);
self.close_handover(lsn, run_id, status, finished, mode);
if let Some(next) = next
&& mode.outputs
&& self.runs.find_run(next.run()).is_none()
{
self.apply(lsn, &next, mode);
}
return;
}
run.done = Some((*status, *finished_ms));
let payload: Payload = payload.to_payload();
run.result = Some(payload.clone());
run.armed.clear();
run.waits.clear();
run.children.clear();
run.awaiting.clear();
let parent = run.parent;
let detached = run.policy & policy::DETACHED != 0;
let kids = std::mem::take(&mut run.kids);
self.retention.push(Reverse((*finished_ms, run_id)));
if mode.outputs {
self.outputs.push_back((
lsn,
Output::Result {
run: run_id,
status: *status,
payload: payload.clone(),
},
));
if let (Some(parent), false) = (parent, detached) {
self.outputs.push_back((
lsn,
Output::ChildResult {
parent,
status: *status,
payload,
},
));
}
self.close_children(lsn, run_id, kids);
}
if mode.events {
self.events.push_back(Event::Finished { run: run_id });
}
}
}
}
fn close_children(&mut self, lsn: Lsn, run: RunId, kids: Vec<(Seq, Name, Name, u8)>) {
for (seq, id, wtype, flags) in kids {
let terminate = match flags & policy::CLOSE_MASK {
policy::CLOSE_ABANDON => continue,
policy::CLOSE_CANCEL => false,
_ => true,
};
self.outputs.push_back((
lsn,
Output::Close {
target: id,
wtype,
parent: Some(ParentRef {
shard: self.shard,
run,
seq,
}),
terminate,
reason: Arc::from(&b"parent closed"[..]),
},
));
}
}
fn close_handover(&mut self, lsn: Lsn, run_id: RunId, status: Status, at: u64, mode: Mode) {
let Some(run) = self.loaded.get_mut(&run_id) else {
return;
};
if run.done.is_some() {
return;
}
run.last_lsn = run.last_lsn.max(lsn);
run.done = Some((status, at));
run.result = Some(Arc::from(&[][..]));
run.armed.clear();
run.waits.clear();
run.children.clear();
run.awaiting.clear();
let kids = std::mem::take(&mut run.kids);
self.retention.push(Reverse((at, run_id)));
if mode.outputs {
self.outputs.push_back((
lsn,
Output::Result {
run: run_id,
status,
payload: Arc::from(&[][..]),
},
));
if status == Status::ContinuedAsNew {
self.close_children(lsn, run_id, kids);
}
}
if mode.events {
self.events.push_back(Event::Finished { run: run_id });
}
}
pub fn park(&mut self, run_id: RunId, blob: BlobRef, chained: bool) -> Option<ParkedRun> {
let run = self.loaded.remove(&run_id)?;
self.journal_bytes = self.journal_bytes.saturating_sub(run.journal.len());
let (state, time) = match run.done {
Some((status, finished)) => (table::DONE + status as u8, finished),
None => (
table::PARKED,
run.armed.iter().map(|a| a.2).min().unwrap_or(0),
),
};
let slot = match self.runs.find_run(run_id) {
Some(slot) => slot,
None => {
let slot = self.table_insert(Entry::new(run_id, hash_id(&run.id), state));
self.runs.index_id(slot);
slot
}
};
let near = state == table::PARKED && time != 0 && time < self.horizon_end();
self.edit(slot, |e| {
e.set_state(state);
e.set_location(blob.offset, blob.len);
e.set_time(time);
e.set(flag::PENDING, false);
e.set(flag::NEAR, near);
e.set(flag::CHAINED, chained);
});
self.mark_changed(slot);
if near {
self.near.insert(time, run_id);
}
let parked = ParkedRun::new(blob, run.done.map(|d| d.0));
self.recycle(run);
Some(parked)
}
pub fn shrink_spare(&mut self, keep: usize) -> usize {
let freed = self.spare.len().saturating_sub(keep);
if freed > 0 {
self.spare.truncate(keep);
self.spare.shrink_to(keep);
}
freed
}
fn recycle(&mut self, mut run: Box<LoadedRun>) {
if self.spare.len() < SPARE_RUNS && run.empty_for_reuse() {
self.spare.push(run);
}
}
pub fn insert_parked(
&mut self,
run: RunId,
blob: BlobRef,
done: Option<Status>,
chained: bool,
id_hash: u64,
time: u64,
) {
self.next_run = self.next_run.max(run + 1);
if self.loaded.contains_key(&run) {
return;
}
let state = match done {
Some(s) => table::DONE + s as u8,
None => table::PARKED,
};
let slot = match self.runs.find_run(run) {
Some(slot) => slot,
None => self.table_insert(Entry::new(run, id_hash, state)),
};
self.edit(slot, |e| {
e.set_state(state);
e.set_location(blob.offset, blob.len);
e.set_time(time);
e.set(flag::CHAINED, chained);
});
self.runs.index_id(slot);
if done.is_some() {
self.retention.push(Reverse((time, run)));
}
}
pub fn load_index(&mut self, records: &[u8]) -> Result<(), &'static str> {
let (records, trailing) = records.as_chunks::<{ table::RECORD }>();
if !trailing.is_empty() {
return Err("index frame is not whole records");
}
for r in records {
let e = Entry::from_record(r).ok_or("bad index record")?;
let run = e.run();
if e.state() == table::DELETED {
if let Some(slot) = self.runs.find_run(run) {
self.table_remove(slot);
}
continue;
}
let known = run < self.next_run;
self.next_run = self.next_run.max(run + 1);
let found = if known { self.runs.find_run(run) } else { None };
let slot = match found {
Some(slot) => {
self.edit(slot, |x| {
let indexed = x.has(flag::IN_ID);
*x = e;
x.set(flag::IN_ID, indexed);
});
slot
}
None => self.table_insert(e),
};
self.runs.index_id(slot);
}
self.deleted.clear();
Ok(())
}
pub fn reserve_runs(&mut self, n: usize) {
self.runs.reserve(n);
}
pub fn index_loaded(&mut self) {
let done: Vec<(u64, RunId)> = self
.runs
.iter()
.filter(|(_, e)| e.done().is_some())
.map(|(_, e)| (e.time(), e.run()))
.collect();
self.retention.extend(done.into_iter().map(Reverse));
}
pub fn begin_index_snapshot(&mut self) {
self.changed = Vec::new();
self.deleted = Vec::new();
self.index_rebase = false;
}
pub fn index_snapshot(&mut self, from: Slot, max: usize, out: &mut Vec<u8>) -> Option<Slot> {
let mut n = 0;
for slot in from..self.runs.used() {
if n == max {
return Some(slot);
}
let e = self.runs.get_mut(slot);
if e.run() == 0 {
continue;
}
e.set(flag::DIRTY, false);
if e.is_parked() {
out.extend_from_slice(&e.record());
n += 1;
}
}
None
}
pub fn hold_slots(&mut self, on: bool) {
self.slots_held = on;
}
pub fn index_needs_base(&self) -> bool {
self.index_rebase
}
pub fn index_delta(&mut self, out: &mut Vec<u8>) {
for run in std::mem::take(&mut self.changed) {
let Some(slot) = self.runs.find_run(run) else {
continue;
};
let e = self.runs.get_mut(slot);
e.set(flag::DIRTY, false);
if e.is_parked() {
out.extend_from_slice(&e.record());
}
}
for run in std::mem::take(&mut self.deleted) {
out.extend_from_slice(&Entry::new(run, 0, table::DELETED).record());
}
}
pub fn index_pending(&self) -> usize {
self.changed.len() + self.deleted.len()
}
pub fn journal_bytes(&self) -> usize {
self.journal_bytes
}
pub fn unpark(
&mut self,
run_id: RunId,
last_lsn: Lsn,
blob_records: &[RecordBuf],
now_ms: u64,
) -> bool {
let Some(slot) = self.runs.find_run(run_id) else {
return false;
};
let e = *self.runs.get(slot);
if !e.is_parked() || !matches!(blob_records.first(), Some(Record::Start { .. })) {
return false;
}
let pending = e.has(flag::PENDING);
for rec in blob_records {
self.apply(last_lsn, rec, Mode::BLOB);
}
let Some(l) = self.loaded.get_mut(&run_id) else {
return false;
};
l.blob = Some(BlobRef {
offset: e.offset(),
len: e.len(),
});
l.blob_chain = u64::from(e.len());
l.blob_depth = 0;
self.journal_bytes = self.journal_bytes.saturating_sub(l.journal.len());
l.journal.clear();
l.journal_records = 0;
l.last_lsn = last_lsn;
if pending {
let mut due = std::mem::take(&mut self.due);
due.clear();
due.extend(
l.armed
.iter()
.filter(|a| a.2 <= now_ms)
.map(|&(seq, kind, _)| TimerKey {
run: run_id,
seq,
kind,
}),
);
for &key in &due {
self.fire(key, now_ms);
}
self.due = due;
}
true
}
pub fn next_expiry(&self) -> Option<u64> {
self.retention
.peek()
.map(|Reverse((at, _))| at.saturating_add(self.retention_ms))
}
pub fn relocate_blob(&mut self, run: RunId, offset: u64) {
if let Some(blob) = self.loaded.get_mut(&run).and_then(|l| l.blob.as_mut()) {
blob.offset = offset;
} else if let Some(slot) = self.runs.find_run(run)
&& self.runs.get(slot).is_parked()
{
let len = self.runs.get(slot).len();
self.runs.get_mut(slot).set_location(offset, len);
self.mark_changed(slot);
}
}
pub fn expire(&mut self, now_ms: u64, out: &mut Vec<RunId>) -> u64 {
let mut released = 0;
while let Some(&Reverse((finished, run))) = self.retention.peek() {
if finished.saturating_add(self.retention_ms) > now_ms {
break;
}
self.retention.pop();
let Some(slot) = self.runs.find_run(run) else {
continue;
};
let e = *self.runs.get(slot);
if e.is_parked() {
if e.done().is_none() {
continue;
}
released += u64::from(e.len());
} else {
match self.loaded.get(&run) {
Some(l) if l.done.is_some() => {
released += l.blob.map_or(0, |_| l.blob_chain);
self.forget_loaded(run);
}
_ => continue,
}
}
self.table_remove(slot);
out.push(run);
}
released
}
pub fn remove(&mut self, run: RunId) {
self.forget_loaded(run);
if let Some(slot) = self.runs.find_run(run) {
self.table_remove(slot);
}
}
pub fn shrink(&mut self) {
fn fit<K: std::hash::Hash + Eq, V>(m: &mut FxHashMap<K, V>) {
if m.capacity() > 1024 && m.capacity() > 4 * m.len() {
m.shrink_to(m.len() * 2);
}
}
fit(&mut self.loaded);
if !self.slots_held && self.runs.compact() {
self.sweep_full = true;
}
if self.outputs.capacity() > 1024 && self.outputs.capacity() > 4 * self.outputs.len() {
self.outputs.shrink_to(self.outputs.len() * 2);
}
if self.retention.capacity() > 1024 && self.retention.capacity() > 4 * self.retention.len()
{
self.retention.shrink_to(self.retention.len() * 2);
}
}
pub fn spilled(&mut self, run: RunId, blob: BlobRef, chain: u64, depth: u8) {
if let Some(l) = self.loaded.get_mut(&run) {
l.blob = Some(blob);
l.blob_chain = chain;
l.blob_depth = depth;
self.journal_bytes = self.journal_bytes.saturating_sub(l.journal.len());
l.journal.clear();
l.journal_records = 0;
}
}
pub fn set_blob(&mut self, run: RunId, blob: BlobRef, chain: u64, depth: u8) {
if let Some(l) = self.loaded.get_mut(&run) {
l.blob = Some(blob);
l.blob_chain = chain;
l.blob_depth = depth;
}
}
fn forget_loaded(&mut self, run: RunId) {
if let Some(l) = self.loaded.remove(&run) {
self.journal_bytes = self.journal_bytes.saturating_sub(l.journal.len());
self.recycle(l);
}
}
}
const SPARE_RUNS: usize = 2048;
pub const SPARE_RUNS_IDLE: usize = 64;
impl LoadedRun {
fn empty() -> Self {
let blank: Name = Name::from("");
Self {
id: blank.clone(),
wtype: blank,
version: 0,
codec: 0,
parent: None,
started_ms: 0,
deadline_ms: 0,
last_lsn: 0,
done: None,
result: None,
blob: None,
blob_chain: 0,
blob_depth: 0,
journal: Vec::new(),
journal_records: 0,
signal_count: 0,
signals: VecDeque::new(),
waits: Vec::new(),
armed: Vec::new(),
dedup: FxHashSet::default(),
children: Vec::new(),
policy: 0,
from: None,
copies_left: 0,
updates: Vec::new(),
update_ids: FxHashMap::default(),
awaiting: FxHashMap::default(),
kids: Vec::new(),
attrs: Vec::new(),
streams: false,
queued: 0,
}
}
fn empty_for_reuse(&mut self) -> bool {
const MAX: usize = 64;
if self.journal.capacity() > 4096
|| self.signals.capacity() > MAX
|| self.waits.capacity() > MAX
|| self.armed.capacity() > MAX
|| self.dedup.capacity() > MAX
|| self.children.capacity() > MAX
|| self.updates.capacity() > MAX
|| self.update_ids.capacity() > MAX
|| self.awaiting.capacity() > MAX
|| self.kids.capacity() > MAX
|| self.attrs.capacity() > MAX
{
return false;
}
self.result = None;
self.journal.clear();
self.signals.clear();
self.waits.clear();
self.armed.clear();
self.dedup.clear();
self.children.clear();
self.updates.clear();
self.update_ids.clear();
self.awaiting.clear();
self.kids.clear();
self.attrs.clear();
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::record::RecordRef;
fn start(run: RunId, id: &'static str, deadline: u64) -> RecordRef<'static> {
Record::Start {
run,
id,
wtype: "t",
version: 1,
codec: 0,
started_ms: 0,
deadline_ms: deadline,
parent: None,
policy: 0,
from: None,
copies: 0,
update: None,
input: b"",
meta: None,
}
}
#[test]
fn runs_come_newest_first() {
for gap in [false, true] {
let mut core = ShardCore::new(0, 1, 1000, 0);
for run in 1..=300u64 {
let id: &'static str = Box::leak(format!("r{run}").into_boxed_str());
core.apply(run, &start(run, id, 0), Mode::LIVE);
}
if gap {
core.set_next_run_id(1_000_000);
}
let mut cursor = core.newest_first();
assert_eq!(
cursor.next_batch(&core, 50),
(251..=300).rev().collect::<Vec<_>>()
);
let mut rest = Vec::new();
loop {
let batch = cursor.next_batch(&core, 64);
if batch.is_empty() {
break;
}
rest.extend(batch);
}
assert_eq!(rest, (1..=250).rev().collect::<Vec<_>>(), "gap {gap}");
}
}
#[test]
fn continue_as_new_is_one_record() {
let mut core = ShardCore::new(0, 1, 1000, 0);
core.apply(1, &start(1, "a", 0), Mode::LIVE);
let next = Record::Start {
run: 2,
id: Arc::from("a"),
wtype: Arc::from("t"),
version: 1,
codec: 1,
started_ms: 5,
deadline_ms: 0,
parent: None,
policy: 0,
from: Some(Origin {
run: 1,
cause: crate::record::origin::CONTINUED,
}),
copies: 0,
update: None,
input: Arc::from(&b"{}"[..]),
meta: None,
};
let mut payload = Vec::new();
crate::record::encode_canonical(&next, &mut payload);
core.apply(
2,
&RecordRef::Done {
run: 1,
status: Status::ContinuedAsNew,
finished_ms: 5,
payload: &payload,
by: None,
},
Mode::LIVE,
);
assert_eq!(core.get(1).unwrap().done, Some((Status::ContinuedAsNew, 5)));
assert_eq!(core.find("a"), Some(Found::Loaded(2)));
let j = crate::record::decode_canonical_all(&core.get(2).unwrap().journal).unwrap();
assert!(matches!(j[0], Record::Start { run: 2, .. }));
core.apply(
3,
&RecordRef::Signal {
run: 2,
name: "s",
dedup: 0,
payload: b"",
by: None,
},
Mode::LIVE,
);
core.apply(2, &next, Mode::RECOVER);
assert_eq!(core.get(2).unwrap().buffered_signals(), 1);
}
#[test]
fn loading_a_continued_run_back_leaves_its_successor_alone() {
let mut core = ShardCore::new(0, 1, 1_000_000, 0);
core.apply(1, &start(1, "a", 0), Mode::LIVE);
let next = Record::Start {
run: 2,
id: Arc::from("a"),
wtype: Arc::from("t"),
version: 1,
codec: 1,
started_ms: 5,
deadline_ms: 0,
parent: None,
policy: 0,
from: Some(Origin {
run: 1,
cause: crate::record::origin::CONTINUED,
}),
copies: 0,
update: None,
input: Arc::from(&b"{}"[..]),
meta: None,
};
let mut payload = Vec::new();
crate::record::encode_canonical(&next, &mut payload);
let done = RecordRef::Done {
run: 1,
status: Status::ContinuedAsNew,
finished_ms: 5,
payload: &payload,
by: None,
};
core.apply(2, &done, Mode::LIVE);
let history = vec![start(1, "a", 0).to_buf(), done.to_buf()];
core.park(1, BlobRef { offset: 0, len: 1 }, false);
core.apply(
3,
&RecordRef::Done {
run: 2,
status: Status::Ok,
finished_ms: 6,
payload: b"7",
by: None,
},
Mode::LIVE,
);
core.park(2, BlobRef { offset: 9, len: 1 }, false);
assert!(core.unpark(1, 2, &history, 10));
assert_eq!(core.parked(2).and_then(|p| p.done()), Some(Status::Ok));
assert_eq!(core.find("a"), Some(Found::Parked(2)));
}
#[test]
fn parent_close_policy_closes_children() {
let mut core = ShardCore::new(0, 1, 1000, 0);
core.apply(1, &start(1, "p", 0), Mode::LIVE);
for (seq, id, flags) in [
(0, "kill", policy::CLOSE_TERMINATE),
(1, "ask", policy::CLOSE_CANCEL | policy::DETACHED),
(2, "keep", policy::CLOSE_ABANDON),
] {
core.apply(
2 + u64::from(seq),
&RecordRef::Child {
run: 1,
seq,
wtype: "t",
id,
flags,
deadline_ms: 0,
input: b"",
},
Mode::LIVE,
);
}
core.apply(
9,
&RecordRef::Done {
run: 1,
status: Status::Terminated,
finished_ms: 1,
payload: b"",
by: None,
},
Mode::LIVE,
);
let mut out = Vec::new();
core.release(9, &mut out);
let closes: Vec<(String, bool)> = out
.iter()
.filter_map(|o| match o {
Output::Close {
target, terminate, ..
} => Some((target.to_string(), *terminate)),
_ => None,
})
.collect();
assert_eq!(
closes,
vec![("kill".to_string(), true), ("ask".to_string(), false)]
);
}
#[test]
fn signals_match_waits_and_timeouts_release_offers() {
let mut core = ShardCore::new(0, 1, 1000, 0);
core.apply(1, &start(1, "a", 0), Mode::LIVE);
assert_eq!(core.take_event(), Some(Event::Started { run: 1 }));
core.apply(
2,
&RecordRef::Signal {
run: 1,
name: "go",
dedup: 0,
payload: b"x",
by: None,
},
Mode::LIVE,
);
assert_eq!(core.take_event(), None);
core.apply(
3,
&RecordRef::Wait {
run: 1,
seq: 0,
name: "go",
deadline_ms: 50,
},
Mode::LIVE,
);
let Some(Event::SignalReady {
seq: 0, ordinal: 0, ..
}) = core.take_event()
else {
panic!("expected the buffered signal to be offered");
};
core.apply(
4,
&RecordRef::Waited {
run: 1,
seq: 0,
outcome: WaitOutcome::TimedOut,
},
Mode::LIVE,
);
core.apply(
5,
&RecordRef::Wait {
run: 1,
seq: 1,
name: "go",
deadline_ms: 0,
},
Mode::LIVE,
);
assert!(matches!(
core.take_event(),
Some(Event::SignalReady {
seq: 1,
ordinal: 0,
..
})
));
core.poll_timers(100);
assert_eq!(core.take_event(), None);
}
#[test]
fn outputs_wait_for_durability() {
let mut core = ShardCore::new(0, 1, 1000, 0);
core.apply(1, &start(1, "a", 0), Mode::LIVE);
core.add_output(1, Output::Ack(7));
core.apply(
2,
&RecordRef::Done {
run: 1,
status: Status::Ok,
finished_ms: 5,
payload: b"r",
by: None,
},
Mode::LIVE,
);
let mut out = Vec::new();
core.release(1, &mut out);
assert_eq!(out, vec![Output::Ack(7)]);
out.clear();
core.release(2, &mut out);
assert!(matches!(out[0], Output::Result { run: 1, .. }));
let mut expired = Vec::new();
core.expire(1004, &mut expired);
assert!(expired.is_empty());
core.expire(1005, &mut expired);
assert_eq!(expired, vec![1]);
assert_eq!(core.find("a"), None);
}
#[test]
fn parked_timers_request_a_load() {
let mut core = ShardCore::new(0, 1, 1000, 0);
core.apply(1, &start(1, "a", 0), Mode::LIVE);
core.apply(
2,
&RecordRef::Timer {
run: 1,
seq: 0,
name: "nap",
fire_at_ms: 10,
},
Mode::LIVE,
);
core.take_event();
let journal = crate::record::decode_canonical_all(&core.get(1).unwrap().journal).unwrap();
core.park(1, BlobRef { offset: 0, len: 1 }, false);
assert_eq!(std::mem::size_of::<ParkedRun>(), 16);
assert_eq!(core.find("a"), Some(Found::Parked(1)));
core.poll_timers(10);
assert_eq!(core.take_event(), Some(Event::NeedLoad { run: 1 }));
assert!(core.unpark(1, 2, &journal, 10));
assert_eq!(
core.take_event(),
Some(Event::Timer {
run: 1,
seq: 0,
kind: TimerKind::Sleep
})
);
}
fn sleeper(core: &mut ShardCore, run: RunId, id: &'static str, at: u64) -> Vec<RecordBuf> {
core.apply(run * 10, &start(run, id, 0), Mode::LIVE);
core.apply(
run * 10 + 1,
&RecordRef::Timer {
run,
seq: 0,
name: "nap",
fire_at_ms: at,
},
Mode::LIVE,
);
while core.take_event().is_some() {}
crate::record::decode_canonical_all(&core.get(run).unwrap().journal).unwrap()
}
#[test]
fn far_timers_wait_in_their_bucket_until_it_approaches() {
let mut core = ShardCore::new(0, 1, 1000, 0);
let far = 10 * HORIZON_MS + 7;
let mut journals = Vec::new();
for run in 1..=3 {
journals.push(sleeper(
&mut core,
run,
["a", "b", "c"][run as usize - 1],
far,
));
}
assert_eq!(core.wheel.len(), 0);
core.park(1, BlobRef { offset: 0, len: 1 }, false);
core.park(2, BlobRef { offset: 9, len: 1 }, false);
assert_eq!(core.near.len(), 0);
assert_eq!(core.next_timer(), Some(far - HORIZON_MS + 1));
core.poll_timers(far - HORIZON_MS - 1);
assert_eq!((core.near.len(), core.wheel.len()), (0, 0));
core.poll_timers(far - HORIZON_MS);
assert_eq!((core.near.len(), core.wheel.len()), (0, 0));
assert!(core.next_timer().is_some_and(|t| t > far - HORIZON_MS));
core.poll_timers(far - HORIZON_MS + 1);
assert_eq!((core.near.len(), core.wheel.len()), (2, 1));
assert_eq!(core.take_event(), None);
core.poll_timers(far);
let mut loads: Vec<_> = std::iter::from_fn(|| core.take_event()).collect();
loads.sort_by_key(|e| format!("{e:?}"));
assert_eq!(
loads,
vec![
Event::NeedLoad { run: 1 },
Event::NeedLoad { run: 2 },
Event::Timer {
run: 3,
seq: 0,
kind: TimerKind::Sleep
}
]
);
assert!(core.unpark(1, 11, &journals[0], far));
assert_eq!(
core.take_event(),
Some(Event::Timer {
run: 1,
seq: 0,
kind: TimerKind::Sleep
})
);
}
#[test]
fn the_index_restores_parked_runs_and_their_buckets() {
let mut core = ShardCore::new(0, 1, 1000, 0);
let far = 5 * HORIZON_MS;
for run in 1..=50 {
let id: &'static str = Box::leak(format!("r{run}").into_boxed_str());
sleeper(&mut core, run, id, far + run);
core.park(
run,
BlobRef {
offset: run * 100,
len: 40,
},
run % 2 == 0,
);
}
let mut base = Vec::new();
core.begin_index_snapshot();
let mut from = Some(0);
while let Some(at) = from {
from = core.index_snapshot(at, 7, &mut base);
}
assert_eq!(base.len(), 50 * table::RECORD);
core.relocate_blob(3, 9_999);
core.remove(4);
let mut delta = Vec::new();
core.index_delta(&mut delta);
assert_eq!(delta.len(), 2 * table::RECORD);
let mut back = ShardCore::new(0, 1, 1000, 0);
back.reserve_runs(52);
back.load_index(&base).unwrap();
back.load_index(&delta).unwrap();
back.index_loaded();
assert_eq!(back.parked_len(), 49);
assert_eq!(back.next_run_id(), 51);
assert_eq!(back.parked(3).unwrap().blob().offset, 9_999);
assert!(back.parked(4).is_none());
assert_eq!(back.find("r7"), Some(Found::Parked(7)));
assert!(back.chained_runs().all(|(r, _)| r % 2 == 0));
assert_eq!(back.chained_runs().count(), 24);
assert_eq!(back.next_timer(), Some(far + 2 - HORIZON_MS));
back.poll_timers(far + 50);
let loads = std::iter::from_fn(|| back.take_event()).count();
assert_eq!(loads, 49);
assert!(back.load_index(&[1; 31]).is_err());
}
#[test]
fn workflow_deadline_fires() {
let mut core = ShardCore::new(0, 1, 1000, 0);
core.apply(1, &start(1, "a", 30), Mode::LIVE);
core.take_event();
core.poll_timers(29);
assert_eq!(core.take_event(), None);
core.poll_timers(30);
assert_eq!(
core.take_event(),
Some(Event::Timer {
run: 1,
seq: 0,
kind: TimerKind::Deadline
})
);
}
}