use std::error::Error;
use std::io;
use std::sync::{Arc, mpsc};
use std::thread;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use acta::{
Array, Column, LogicalType, Reader, RecordBatch, Schema, TimeUnit, TimeZone, TimestampArray,
Utf8Array, Writer, WriterOptions,
};
const UPDATE_COUNT: usize = 30;
const UPDATE_INTERVAL: Duration = Duration::from_secs(1);
const POLL_INTERVAL: Duration = Duration::from_millis(25);
const EXAMPLE_TIMEOUT: Duration = Duration::from_secs(10);
type AnyError = Box<dyn Error + Send + Sync>;
fn main() -> Result<(), AnyError> {
let path = std::env::temp_dir().join(format!(
"acta-live-append-example-{}.acta",
std::process::id()
));
let _ = std::fs::remove_file(&path);
let schema = Schema::new(
1,
vec![
Column::new(
1,
"last_update",
LogicalType::Timestamp {
unit: TimeUnit::Microsecond,
timezone: TimeZone::Utc,
},
false,
),
Column::new(2, "message", LogicalType::Utf8, false),
],
Some(1),
);
let _ = Writer::create(&path, schema, WriterOptions::default())?.finish()?;
let (tail_ready_tx, tail_ready_rx) = mpsc::channel();
let reader_path = path.clone();
let reader_thread = thread::spawn(move || -> Result<(), AnyError> {
let mut reader = Reader::open(&reader_path)?;
let mut tail = reader.tail();
tail_ready_tx.send(())?;
let deadline = Instant::now() + EXAMPLE_TIMEOUT;
let mut updates_seen = 0;
while updates_seen < UPDATE_COUNT {
match tail.poll_next()? {
Some(batch) => {
print_batch(&batch)?;
updates_seen += batch.row_count();
}
None if Instant::now() < deadline => thread::sleep(POLL_INTERVAL),
None => {
return Err(io::Error::new(
io::ErrorKind::TimedOut,
format!("received only {updates_seen} of {UPDATE_COUNT} updates"),
)
.into());
}
}
}
println!("reader: received all {updates_seen} updates");
Ok(())
});
let writer_path = path.clone();
let writer_thread = thread::spawn(move || -> Result<(), AnyError> {
tail_ready_rx.recv()?;
let mut writer = Writer::open(&writer_path, WriterOptions::default())?;
let schema = Arc::clone(writer.schema());
for update in 1..=UPDATE_COUNT {
let last_update = unix_timestamp_micros()?;
let message = format!("update {update}");
writer.append(update_batch(&schema, last_update, message.clone())?)?;
writer.flush()?;
println!("writer: committed {message}");
thread::sleep(UPDATE_INTERVAL);
}
let _ = writer.finish()?;
Ok(())
});
let writer_result = writer_thread
.join()
.map_err(|_| io::Error::other("writer thread panicked"))?;
let reader_result = reader_thread
.join()
.map_err(|_| io::Error::other("reader thread panicked"))?;
let _ = std::fs::remove_file(&path);
writer_result?;
reader_result?;
Ok(())
}
fn update_batch(
schema: &Arc<Schema>,
last_update: i64,
message: String,
) -> acta::Result<RecordBatch> {
RecordBatch::try_new(
Arc::clone(schema),
vec![
Array::Timestamp(TimestampArray::new(
vec![last_update],
None,
TimeUnit::Microsecond,
TimeZone::Utc,
)),
Array::Utf8(Utf8Array::new(vec![message], None)),
],
1,
)
}
fn print_batch(batch: &RecordBatch) -> Result<(), AnyError> {
let Some(Array::Timestamp(last_updates)) = batch.column(0) else {
return Err(io::Error::other("last_update did not decode as timestamp64").into());
};
let Some(Array::Utf8(messages)) = batch.column(1) else {
return Err(io::Error::other("message did not decode as UTF-8").into());
};
for row in 0..batch.row_count() {
let last_update = format_utc_time(last_updates.values()[row]);
println!(
"reader: last_update={last_update}, message={}",
messages.values()[row]
);
}
Ok(())
}
fn format_utc_time(timestamp_micros: i64) -> String {
const MICROS_PER_SECOND: i64 = 1_000_000;
const SECONDS_PER_DAY: i64 = 24 * 60 * 60;
let whole_seconds = timestamp_micros.div_euclid(MICROS_PER_SECOND);
let micros = timestamp_micros.rem_euclid(MICROS_PER_SECOND);
let seconds_today = whole_seconds.rem_euclid(SECONDS_PER_DAY);
let hours = seconds_today / (60 * 60);
let minutes = seconds_today / 60 % 60;
let seconds = seconds_today % 60;
format!("{hours:02}:{minutes:02}:{seconds:02}.{micros:06} UTC")
}
fn unix_timestamp_micros() -> Result<i64, AnyError> {
let micros = SystemTime::now().duration_since(UNIX_EPOCH)?.as_micros();
Ok(i64::try_from(micros)?)
}