use std::collections::BTreeMap;
use std::fs::{File, OpenOptions};
use std::io::{Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::Mutex;
use std::task::{Context, Poll, Waker};
use std::time::{Duration, Instant};
use serde::Serialize;
pub const MAX_WAIT_SECS: u64 = 60;
pub const MAX_EVENTS: usize = 1000;
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct Event {
pub seq: u64,
pub time: String,
pub kind: String,
pub iri: String,
pub by: String,
}
impl Event {
fn line(&self) -> String {
format!(
"{}\t{}\t{}\t{}\t{}\n",
self.seq, self.time, self.kind, self.iri, self.by
)
}
fn parse(line: &str) -> Option<Event> {
let mut parts = line.split('\t');
let event = Event {
seq: parts.next()?.parse().ok()?,
time: parts.next()?.to_string(),
kind: parts.next()?.to_string(),
iri: parts.next()?.to_string(),
by: parts.next()?.to_string(),
};
parts.next().is_none().then_some(event)
}
}
pub struct FeedLog {
path: PathBuf,
events: Vec<Event>,
len: u64,
owed: Vec<Event>,
}
impl FeedLog {
pub fn open(path: &Path) -> Result<FeedLog, String> {
let text = match std::fs::read_to_string(path) {
Ok(text) => text,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => String::new(),
Err(e) => return Err(format!("read {}: {e}", path.display())),
};
let whole = match text.rfind('\n') {
Some(end) => &text[..=end],
None => "",
};
if whole.len() != text.len() {
let file = OpenOptions::new()
.write(true)
.open(path)
.map_err(|e| format!("open {}: {e}", path.display()))?;
file.set_len(whole.len() as u64)
.and_then(|()| file.sync_all())
.map_err(|e| format!("truncate the torn tail of {}: {e}", path.display()))?;
}
let mut events = Vec::new();
for (n, line) in whole.lines().enumerate() {
let event = Event::parse(line)
.ok_or_else(|| format!("{}:{}: not a feed event", path.display(), n + 1))?;
if event.seq != n as u64 + 1 {
return Err(format!(
"{}:{}: sequence {} where {} was due: the feed is not gapless",
path.display(),
n + 1,
event.seq,
n + 1
));
}
events.push(event);
}
Ok(FeedLog {
path: path.to_path_buf(),
events,
len: whole.len() as u64,
owed: Vec::new(),
})
}
pub fn last(&self) -> u64 {
self.events.len() as u64
}
pub fn next(&self) -> u64 {
self.last() + self.owed.len() as u64 + 1
}
pub fn owes(&self) -> bool {
!self.owed.is_empty()
}
pub fn first_owed(&self) -> Option<&Event> {
self.owed.first()
}
pub fn append(&mut self, event: Event) -> Result<(), String> {
assert!(self.owed.is_empty(), "an owed event is logged first");
self.write(event)
}
fn write(&mut self, event: Event) -> Result<(), String> {
assert_eq!(event.seq, self.last() + 1, "a feed is gapless");
let line = event.line();
let written = OpenOptions::new()
.create(true)
.write(true)
.truncate(false)
.open(&self.path)
.and_then(|mut file| {
file.set_len(self.len)?;
file.seek(SeekFrom::Start(self.len))?;
file.write_all(line.as_bytes())?;
file.sync_data()
});
if let Err(e) = written {
if let Ok(file) = OpenOptions::new().write(true).open(&self.path) {
let _ = file.set_len(self.len);
}
return Err(format!("append to {}: {e}", self.path.display()));
}
self.len += line.len() as u64;
self.events.push(event);
Ok(())
}
pub fn owe(&mut self, event: Event) {
assert_eq!(event.seq, self.next(), "a feed is gapless");
self.owed.push(event);
}
pub fn settle(&mut self) -> Result<usize, String> {
let mut logged = 0;
while !self.owed.is_empty() {
let event = self.owed[0].clone();
self.write(event)?;
self.owed.remove(0);
logged += 1;
}
Ok(logged)
}
pub fn events(&self) -> &[Event] {
&self.events
}
pub fn after(&self, after: u64) -> &[Event] {
let start = (after as usize).min(self.events.len());
let end = (start + MAX_EVENTS).min(self.events.len());
&self.events[start..end]
}
}
pub(crate) fn touch(path: &Path) -> Result<(), String> {
OpenOptions::new()
.create(true)
.append(true)
.open(path)
.map(|_: File| ())
.map_err(|e| format!("create {}: {e}", path.display()))
}
pub fn parse_cursor(text: &str) -> Result<BTreeMap<String, u64>, String> {
let mut cursor = BTreeMap::new();
for part in text.split(',').map(str::trim).filter(|p| !p.is_empty()) {
let (layer, seq) = part
.rsplit_once(':')
.ok_or_else(|| format!("`{part}` is not `layer:seq`"))?;
let seq: u64 = seq
.parse()
.map_err(|_| format!("`{part}`: `{seq}` is not a sequence number"))?;
if cursor.insert(layer.to_string(), seq).is_some() {
return Err(format!("layer `{layer}` is named twice"));
}
}
if cursor.is_empty() {
return Err("names no layer (`layer:seq,…`)".into());
}
Ok(cursor)
}
pub fn format_cursor(cursor: &BTreeMap<String, u64>) -> String {
cursor
.iter()
.map(|(layer, seq)| format!("{layer}:{seq}"))
.collect::<Vec<_>>()
.join(",")
}
#[derive(Default)]
pub struct Notify {
state: Mutex<(u64, Vec<Waker>)>,
}
impl Notify {
pub fn generation(&self) -> u64 {
self.state.lock().expect("notify lock").0
}
pub fn bump(&self) {
let wakers = {
let mut state = self.state.lock().expect("notify lock");
state.0 += 1;
std::mem::take(&mut state.1)
};
for waker in wakers {
waker.wake();
}
}
pub fn changed(&self, seen: u64) -> Changed<'_> {
Changed { notify: self, seen }
}
}
pub struct Changed<'a> {
notify: &'a Notify,
seen: u64,
}
impl std::future::Future for Changed<'_> {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
let mut state = self.notify.state.lock().expect("notify lock");
if state.0 != self.seen {
return Poll::Ready(());
}
if !state.1.iter().any(|w| w.will_wake(cx.waker())) {
state.1.push(cx.waker().clone());
}
Poll::Pending
}
}
pub async fn wait_until(notify: &Notify, wait: Duration, mut ready: impl FnMut() -> bool) {
let deadline = Instant::now() + wait;
loop {
let seen = notify.generation();
if ready() {
return;
}
let now = Instant::now();
if now >= deadline {
return;
}
let changed = notify.changed(seen);
let timer = futures_timer::Delay::new(deadline - now);
futures::pin_mut!(changed);
futures::future::select(changed, timer).await;
}
}