use std::collections::BTreeMap;
use std::fs;
use std::io::{self, BufRead, BufReader};
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
use anyhow::{bail, Context};
use serde_json::Value;
use crate::query::{ensure_dest_is_not_plain_file, fmt_ms, is_bundle, resolve_backing};
use crate::records::{Reader, Rec};
use crate::store::{self, now_ms, Config, Store};
pub enum Delivery {
Atomic,
Streaming,
}
pub enum Clock {
Now,
FromStamps,
}
#[allow(clippy::too_many_arguments)]
pub fn cmd_records_sink(
source: Option<&Path>,
dest: &Path,
cfg: Config,
delivery: Delivery,
clock: Clock,
retain: Option<&str>,
retain_size: Option<&str>,
op: &str,
) -> anyhow::Result<()> {
retain.map(crate::append::parse_duration_ms).transpose()?;
retain_size
.map(crate::append::parse_size_bytes)
.transpose()?;
if is_bundle(dest) {
bail!(
"{} is a .timber transfer bundle — bundles are read-only; \
sink the stream into a log instead",
dest.display()
);
}
ensure_dest_is_not_plain_file(dest, op)?;
let (dir, name) = resolve_backing(dest)?;
fs::create_dir_all(&dir)?;
let _dir_lock = match store::lock_backing_shared(&dir)? {
Some(f) => f,
None => {
let mounted = store::read_lock_mountpoint(&dir)
.map(|m| format!(" (mounted on {})", m.display()))
.unwrap_or_default();
bail!(
"backing directory {} is served by a timberfs mount{mounted}; \
write through the mount instead, or unmount first",
dir.display()
);
}
};
let file_lock = match store::lock_file_exclusive(&dir, &name)? {
Some(f) => f,
None => {
bail!("{name} already has a writer (another timberfs appender or a rotation)");
}
};
store::write_lock_info(
&file_lock,
&format!("records sink pid={}\n", std::process::id()),
)?;
if retain.is_some() || retain_size.is_some() {
let mut map = crate::bark::load(&dir, &name).unwrap_or_default();
if let Some(r) = retain {
map.insert("retain".to_string(), Value::String(r.to_string()));
}
if let Some(r) = retain_size {
map.insert("retain_size".to_string(), Value::String(r.to_string()));
}
crate::bark::save(&dir, &name, &map)?;
}
let st = Arc::new(Mutex::new(Store {
dir: dir.clone(),
cfg,
files: BTreeMap::new(),
}));
st.lock().unwrap().create(&name)?;
let reader: Box<dyn BufRead> = match source {
Some(p) => Box::new(BufReader::new(
fs::File::open(p).with_context(|| format!("opening {}", p.display()))?,
)),
None => Box::new(BufReader::new(io::stdin())),
};
let mut rec_reader = Reader::new(reader);
let streaming = matches!(delivery, Delivery::Streaming);
if !streaming {
st.lock().unwrap().files.get_mut(&name).unwrap().stage();
}
if streaming {
crate::append::install_signal_handlers();
}
let stop = Arc::new(AtomicBool::new(false));
let maint = if streaming {
let st = Arc::clone(&st);
let stop = Arc::clone(&stop);
let dir = dir.clone();
let name = name.clone();
let op = op.to_string();
Some(thread::spawn(move || {
while !stop.load(Ordering::Relaxed) {
thread::sleep(Duration::from_millis(1000));
if crate::append::stopping() {
let mut s = st.lock().unwrap();
let cfg = s.cfg;
let f = s.files.get_mut(&name).unwrap();
let _ = f.flush_chunk(&cfg);
let _ = f.sync(&cfg);
drop(s);
if crate::bark::load(&dir, &name).is_none() {
if let Ok(m) =
crate::bark::with_identity(crate::bark::derived_map(None, &op))
{
let _ = crate::bark::save(&dir, &name, &m);
}
}
std::process::exit(0);
}
if stop.load(Ordering::Relaxed) {
break;
}
st.lock().unwrap().flush_aged();
match crate::bark::declared_retention(&dir, &name) {
Ok(policy) if policy.is_some() => {
if let Err(e) = st.lock().unwrap().enforce_retention(
&name,
policy.max_age_ms,
policy.max_comp_bytes,
) {
eprintln!("timberfs: {name}: background retention failed: {e}");
}
}
_ => {}
}
}
}))
} else {
None
};
let mut entries: u64 = 0;
let mut carried: u64 = 0;
let mut last_ts: Option<u64> = None;
let mut start_fields: Vec<(String, String)> = Vec::new();
let result = loop {
match rec_reader.next_rec() {
Ok(Some(Rec::Start(fields))) => start_fields = fields,
Ok(Some(Rec::Source(_))) => {}
Ok(Some(Rec::Entry(e))) => {
if let Some(t) = e.ts {
last_ts = Some(t);
}
let (wf, wl) = match (e.wf, e.wl) {
(Some(a), Some(b)) => {
carried += 1;
(a, b)
}
_ => match clock {
Clock::Now => {
let n = now_ms();
(n, n)
}
Clock::FromStamps => {
let t = e.ts.or(last_ts).unwrap_or_else(now_ms);
(t, t)
}
},
};
let mut s = st.lock().unwrap();
let cfg = s.cfg;
if let Err(e) = s
.files
.get_mut(&name)
.unwrap()
.append_windowed(&e.payload, wf, wl, &cfg)
{
break Err(anyhow::Error::from(e));
}
entries += 1;
}
Ok(Some(Rec::End(_))) => {}
Ok(None) => break Ok(()),
Err(e) => break Err(e),
}
};
stop.store(true, Ordering::Relaxed);
if let Some(h) = maint {
let _ = h.join();
}
{
let mut s = st.lock().unwrap();
let cfg = s.cfg;
let f = s.files.get_mut(&name).unwrap();
match (&result, &delivery) {
(Ok(()), Delivery::Atomic) => f.commit_stage(&cfg)?,
(Ok(()), Delivery::Streaming) => {
f.flush_chunk(&cfg)?;
f.sync(&cfg)?;
}
(Err(_), Delivery::Atomic) => {
f.abort_stage()?;
drop(s);
return result.context("nothing imported — the store is unchanged (atomic sink)");
}
(Err(_), Delivery::Streaming) => {
f.flush_chunk(&cfg)?;
f.sync(&cfg)?;
drop(s);
return result.context(format!(
"{entries} entr{} received before the break are appended \
(streaming sink; re-running may duplicate)",
if entries == 1 { "y" } else { "ies" }
));
}
}
}
let mut map = match crate::bark::load(&dir, &name) {
Some(m) => m,
None => crate::bark::derived_map(None, op),
};
map.insert(
"command".to_string(),
Value::String(crate::grep::command_line()),
);
let selection: Vec<String> = start_fields
.iter()
.filter(|(k, _)| k == "from" || k == "to" || k == "has" || k == "any")
.map(|(k, v)| format!("{k}={v}"))
.collect();
if !selection.is_empty() {
map.insert(
"stream_selection".to_string(),
Value::String(selection.join(" ")),
);
}
let stages: Vec<String> = start_fields
.iter()
.filter(|(k, _)| k == "stage")
.map(|(_, v)| v.clone())
.collect();
if !stages.is_empty() {
map.insert(
"stream_stages".to_string(),
Value::String(stages.join(" | ")),
);
}
let map = crate::bark::with_identity(map)?;
crate::bark::save(&dir, &name, &map)?;
match crate::bark::declared_retention(&dir, &name) {
Ok(policy) if policy.is_some() => {
if let Some(stats) = st.lock().unwrap().enforce_retention(
&name,
policy.max_age_ms,
policy.max_comp_bytes,
)? {
crate::note!(
"timberfs: {name}: retention dropped {} chunk(s), {} compressed bytes",
stats.chunks_moved,
stats.comp_bytes
);
}
}
Ok(_) => {}
Err(e) => eprintln!("timberfs: {name}: manifest unreadable ({e}); retention not applied"),
}
if crate::bark::index_declared(&dir, &name) {
crate::grain::extend_grain(&dir, &name)?;
}
let (chunk_count, first, last) = {
let s = st.lock().unwrap();
let f = s.files.get(&name).unwrap();
(f.chunks.len(), f.first_write_ms(), f.last_write_ms())
};
let span = match (first, last) {
(Some(a), Some(b)) => format!(", spanning {} .. {}", fmt_ms(a), fmt_ms(b)),
_ => String::new(),
};
crate::note!(
"timberfs: {name}: {entries} entr{} from the record stream \
({carried} with their original write windows); now {chunk_count} chunk(s){span}",
if entries == 1 { "y" } else { "ies" },
);
Ok(())
}