use crate::daemon::{Coordinator, State};
use crate::job::{JobState, JobStatus};
use crate::proto::Response;
use crate::sys;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, VecDeque};
use std::io::Write;
use std::sync::Arc;
use std::time::{Duration, Instant};
pub const RETAINED: usize = 512;
const RETAINED_VAR: &str = "QEX_EVENTS_RETAINED";
const POLL: Duration = Duration::from_millis(250);
const WRITE_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Cursor {
Start,
Now,
After {
seq: u64,
stream: Option<uuid::Uuid>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Change {
State,
Reason,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "event", rename_all = "snake_case")]
pub enum Event {
Stream {
time: u64,
version: String,
pid: i32,
stream_id: uuid::Uuid,
coordinator_started_at: u64,
first_seq: u64,
last_seq: u64,
},
Job {
seq: u64,
time: u64,
id: uuid::Uuid,
name: String,
state: JobState,
previous: Option<JobState>,
change: Change,
job: Box<JobStatus>,
},
Gap {
time: u64,
missed: Option<u64>,
next_seq: u64,
reason: String,
},
Bye { time: u64, reason: String },
}
pub struct EventLog {
ring: VecDeque<(u64, Event)>,
next_seq: u64,
dropped: u64,
reported: BTreeMap<uuid::Uuid, (JobState, Option<String>)>,
readers: usize,
capacity: usize,
stream_id: uuid::Uuid,
}
impl Default for EventLog {
fn default() -> Self {
Self::new()
}
}
impl EventLog {
pub fn new() -> Self {
Self::with_capacity(
std::env::var(RETAINED_VAR)
.ok()
.and_then(|v| v.parse::<usize>().ok())
.filter(|n| *n > 0)
.unwrap_or(RETAINED),
)
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
ring: VecDeque::new(),
next_seq: 1,
dropped: 0,
reported: BTreeMap::new(),
readers: 0,
capacity,
stream_id: uuid::Uuid::new_v4(),
}
}
pub fn stream_id(&self) -> uuid::Uuid {
self.stream_id
}
pub fn first_seq(&self) -> u64 {
self.ring.front().map(|(s, _)| *s).unwrap_or(self.next_seq)
}
pub fn last_seq(&self) -> u64 {
self.next_seq - 1
}
pub fn dropped(&self) -> u64 {
self.dropped
}
fn push(&mut self, make: impl FnOnce(u64) -> Event) {
let seq = self.next_seq;
let event = make(seq);
self.next_seq += 1;
if self.ring.len() == self.capacity {
self.ring.pop_front();
self.dropped += 1;
}
self.ring.push_back((seq, event));
}
pub fn after(&self, after: u64) -> Vec<(u64, Event)> {
self.ring
.iter()
.filter(|(seq, _)| *seq > after)
.cloned()
.collect()
}
}
impl State {
pub fn publish_changes(&mut self) {
let now = sys::now_secs();
for (id, job) in self.jobs.iter() {
let state = job.status.state;
let reason = if state == JobState::Queued {
job.status.blocked_reason.clone()
} else {
None
};
let previous = self.events.reported.get(id).cloned();
let change = match &previous {
None => Change::State,
Some((s, _)) if *s != state => Change::State,
Some((_, r)) if *r != reason => Change::Reason,
Some(_) => continue,
};
let status = job.status.clone();
let name = status.name.clone();
let previous_state = previous.map(|(s, _)| s);
self.events.push(|seq| Event::Job {
seq,
time: now,
id: *id,
name,
state,
previous: previous_state,
change,
job: Box::new(status),
});
self.events.reported.insert(*id, (state, reason));
}
if self.events.reported.len() > self.jobs.len() {
self.events
.reported
.retain(|id, _| self.jobs.contains_key(id));
}
}
}
struct ReaderGuard(Arc<Coordinator>);
impl Drop for ReaderGuard {
fn drop(&mut self) {
let mut state = self.0.state.lock().unwrap();
state.events.readers = state.events.readers.saturating_sub(1);
}
}
pub fn stream(
coord: &Arc<Coordinator>,
out: &mut std::os::unix::net::UnixStream,
cursor: Cursor,
) -> anyhow::Result<()> {
out.set_write_timeout(Some(WRITE_TIMEOUT)).ok();
let mut lead: Vec<Event> = Vec::new();
let mut next: u64;
{
let mut state = coord.state.lock().unwrap();
state.publish_changes();
state.events.readers += 1;
let first = state.events.first_seq();
let last = state.events.last_seq();
let now = sys::now_secs();
let stream_id = state.events.stream_id();
lead.push(Event::Stream {
time: now,
version: crate::version::VERSION.to_string(),
pid: std::process::id() as i32,
stream_id,
coordinator_started_at: state.started_at,
first_seq: first,
last_seq: last,
});
let other_stream = |given: uuid::Uuid| Event::Gap {
time: now,
missed: None,
next_seq: first,
reason: format!(
"your number comes from the stream {given}, and this stream is {stream_id}. \
The coordinator that gave you that number stopped, and the numbers of this \
coordinator start again at 1. The stream continues with the events that this \
coordinator holds ({first} to {last}). Its records are the same records."
),
};
next = match cursor {
Cursor::Start => {
if state.events.dropped() > 0 {
lead.push(Event::Gap {
time: now,
missed: Some(state.events.dropped()),
next_seq: first,
reason: "the coordinator keeps the last events only, and these events \
left it before you connected"
.to_string(),
});
}
first
}
Cursor::Now => last + 1,
Cursor::After {
stream: Some(given),
..
} if given != stream_id => {
lead.push(other_stream(given));
first
}
Cursor::After { seq, stream: None } if seq > last => {
lead.push(Event::Gap {
time: now,
missed: None,
next_seq: first,
reason: format!(
"you asked for the events after {seq}, and this stream ({stream_id}) \
holds {first} to {last}. It never made that number, so a different \
coordinator gave it to you. Give the stream with the number, as \
`--since <stream>:<seq>`, and qex compares them for you. The stream \
continues with the events that this coordinator holds."
),
});
first
}
Cursor::After { seq, .. } if seq + 1 < first => {
lead.push(Event::Gap {
time: now,
missed: Some(first - (seq + 1)),
next_seq: first,
reason: "the coordinator keeps the last events only, and these events left \
it before you connected"
.to_string(),
});
first
}
Cursor::After { seq, .. } => seq + 1,
};
}
let _guard = ReaderGuard(Arc::clone(coord));
for event in lead {
send(out, &event)?;
}
loop {
let (batch, gap, stopping) = {
let state = coord.state.lock().unwrap();
let first = state.events.first_seq();
let gap = if next < first {
Some(Event::Gap {
time: sys::now_secs(),
missed: Some(first - next),
next_seq: first,
reason: "you did not read the stream fast enough, and the coordinator \
dropped these events. Read the stream in a loop, and do the work \
in a different thread or process."
.to_string(),
})
} else {
None
};
if gap.is_some() {
next = first;
}
(state.events.after(next - 1), gap, state.stop)
};
if let Some(event) = gap {
send(out, &event)?;
}
for (seq, event) in batch {
next = seq + 1;
send(out, &event)?;
}
if stopping {
send(
out,
&Event::Bye {
time: sys::now_secs(),
reason: "the coordinator stops, because no job operates and no command \
arrived for the idle time. The records of the jobs stay on the \
disk. The next qex command starts a coordinator."
.to_string(),
},
)?;
return Ok(());
}
if reader_is_gone(out) {
return Ok(());
}
let state = coord.state.lock().unwrap();
let _ = coord.changed.wait_timeout(state, POLL).unwrap();
}
}
fn reader_is_gone(out: &std::os::unix::net::UnixStream) -> bool {
use std::os::unix::io::AsRawFd;
let fd = out.as_raw_fd();
let mut poll = libc::pollfd {
fd,
events: libc::POLLIN,
revents: 0,
};
let ready = unsafe { libc::poll(&mut poll, 1, 0) };
if ready <= 0 {
return false;
}
if poll.revents & (libc::POLLHUP | libc::POLLERR | libc::POLLNVAL) != 0 {
return true;
}
if poll.revents & libc::POLLIN == 0 {
return false;
}
let mut byte = 0u8;
let got = unsafe {
libc::recv(
fd,
&mut byte as *mut u8 as *mut libc::c_void,
1,
libc::MSG_PEEK | libc::MSG_DONTWAIT,
)
};
got == 0
}
fn send(out: &mut std::os::unix::net::UnixStream, event: &Event) -> anyhow::Result<()> {
let response = Response::Event {
event: Box::new(event.clone()),
};
let mut line = serde_json::to_string(&response)?;
line.push('\n');
out.write_all(line.as_bytes())?;
out.flush()?;
Ok(())
}
pub fn wait_for_readers(coord: &Arc<Coordinator>, limit: Duration) {
coord.changed.notify_all();
let deadline = Instant::now() + limit;
while Instant::now() < deadline {
if coord.state.lock().unwrap().events.readers == 0 {
return;
}
std::thread::sleep(Duration::from_millis(10));
}
}
#[cfg(test)]
mod tests {
use super::*;
fn event(seq: u64) -> Event {
Event::Bye {
time: seq,
reason: String::new(),
}
}
#[test]
fn each_stream_has_a_name_of_its_own() {
let a = EventLog::new();
let b = EventLog::new();
assert_ne!(a.stream_id(), b.stream_id());
}
#[test]
fn each_event_fits_one_line_of_json() {
let lines = [
Event::Stream {
time: 1,
version: "0.8.0".into(),
pid: 7,
stream_id: uuid::Uuid::new_v4(),
coordinator_started_at: 1,
first_seq: 1,
last_seq: 3,
},
Event::Gap {
time: 2,
missed: Some(4),
next_seq: 9,
reason: "a reason with \"quotation marks\"\nand a newline".into(),
},
Event::Bye {
time: 3,
reason: "the coordinator stops".into(),
},
];
for line in lines {
let text = serde_json::to_string(&line).unwrap();
assert!(!text.contains('\n'), "an event must fit one line: {text}");
let back: Event = serde_json::from_str(&text).unwrap();
assert_eq!(serde_json::to_string(&back).unwrap(), text);
}
}
#[test]
fn the_ring_holds_the_last_events_and_counts_the_others() {
let mut log = EventLog::with_capacity(RETAINED);
assert_eq!(log.first_seq(), 1, "an empty log starts at the next number");
assert_eq!(log.last_seq(), 0);
for _ in 0..RETAINED {
log.push(event);
}
assert_eq!(log.first_seq(), 1);
assert_eq!(log.last_seq(), RETAINED as u64);
assert_eq!(log.dropped(), 0);
for _ in 0..10 {
log.push(event);
}
assert_eq!(log.dropped(), 10);
assert_eq!(log.first_seq(), 11);
assert_eq!(log.last_seq(), RETAINED as u64 + 10);
assert_eq!(log.after(0).len(), RETAINED);
}
#[test]
fn a_reader_receives_the_events_after_its_number_only() {
let mut log = EventLog::with_capacity(RETAINED);
for _ in 0..5 {
log.push(event);
}
let got: Vec<u64> = log.after(2).iter().map(|(seq, _)| *seq).collect();
assert_eq!(got, vec![3, 4, 5]);
assert!(
log.after(5).is_empty(),
"a reader that is current gets nothing"
);
}
#[test]
fn the_numbers_never_repeat() {
let mut log = EventLog::with_capacity(RETAINED);
let mut seen = Vec::new();
for _ in 0..(RETAINED + 20) {
log.push(event);
seen.push(log.last_seq());
}
let mut sorted = seen.clone();
sorted.dedup();
assert_eq!(sorted.len(), seen.len(), "a number repeated");
assert!(seen.windows(2).all(|w| w[1] == w[0] + 1));
}
fn one_job_state() -> State {
let spec = crate::spec::JobSpec {
id: uuid::Uuid::new_v4(),
name: "t".into(),
cwd: "/".into(),
command: vec!["true".into()],
env: Default::default(),
cpu: 1,
mem: 1 << 20,
timeout: None,
tags: vec![],
priority: 0,
env_capture: crate::config::EnvCapture::None,
claim_source: "explicit".into(),
group: None,
group_name: None,
locks: vec![],
retries: 0,
needs: vec![],
after: vec![],
nice: None,
max_queue_time: None,
dedupe_key: None,
dedupe_window: 0,
learn_key: None,
submitted_at: 0,
};
let status = JobStatus::new(&spec);
let mut jobs = BTreeMap::new();
jobs.insert(
spec.id,
crate::daemon::Job {
spec,
status,
supervisor_pid: None,
},
);
State {
cfg: Default::default(),
jobs,
queue: Vec::new(),
last_contact: Instant::now(),
idle_since: None,
next_sequence: 1,
started_at: 0,
dedupe: BTreeMap::new(),
config_seen: 0,
config_settling: None,
config_error: None,
events: EventLog::with_capacity(RETAINED),
paused: crate::pause::Paused::default(),
stop: false,
}
}
#[test]
fn each_change_gives_one_line_and_a_repeat_gives_none() {
let mut state = one_job_state();
let id = *state.jobs.keys().next().unwrap();
state.publish_changes();
assert_eq!(state.events.last_seq(), 1, "the admission must give a line");
state.publish_changes();
state.publish_changes();
assert_eq!(state.events.last_seq(), 1, "a repeat must give no line");
match &state.events.after(0)[0].1 {
Event::Job {
state,
previous,
change,
..
} => {
assert_eq!(*state, JobState::Queued);
assert_eq!(
*previous, None,
"the first line of a job has no previous state"
);
assert_eq!(*change, Change::State);
}
other => panic!("expected a job event, got {other:?}"),
}
state.jobs.get_mut(&id).unwrap().status.blocked_reason = Some("waits for memory".into());
state.publish_changes();
assert_eq!(state.events.last_seq(), 2);
match &state.events.after(1)[0].1 {
Event::Job { change, job, .. } => {
assert_eq!(*change, Change::Reason);
assert_eq!(job.blocked_reason.as_deref(), Some("waits for memory"));
}
other => panic!("expected a job event, got {other:?}"),
}
state.jobs.get_mut(&id).unwrap().status.state = JobState::Running;
state.publish_changes();
assert_eq!(state.events.last_seq(), 3);
match &state.events.after(2)[0].1 {
Event::Job {
state,
previous,
change,
..
} => {
assert_eq!(*state, JobState::Running);
assert_eq!(*previous, Some(JobState::Queued));
assert_eq!(*change, Change::State);
}
other => panic!("expected a job event, got {other:?}"),
}
state.jobs.remove(&id);
state.publish_changes();
assert_eq!(state.events.last_seq(), 3, "a deleted record gives no line");
assert!(state.events.reported.is_empty());
}
#[test]
fn each_cursor_form_survives_the_wire() {
let stream = uuid::Uuid::new_v4();
for c in [
Cursor::Start,
Cursor::Now,
Cursor::After {
seq: 42,
stream: None,
},
Cursor::After {
seq: 42,
stream: Some(stream),
},
] {
let text = serde_json::to_string(&c).unwrap();
assert!(!text.contains('\n'), "a request must fit one line: {text}");
assert_eq!(serde_json::from_str::<Cursor>(&text).unwrap(), c);
}
assert_eq!(serde_json::to_string(&Cursor::Start).unwrap(), "\"start\"");
}
}