use std::io::Read;
use std::process::ExitCode;
use ytsaurus_client::{Client, ClientError};
use ytsaurus_job::{Event, JobReader};
const BASE: &str = "//tmp/ytsaurus_rs_streaming";
const DEFAULT_MIB: u64 = 64;
const CHUNK: usize = 64 * 1024;
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("\nstreaming failed: {e}");
ExitCode::FAILURE
}
}
}
fn run() -> Result<(), ClientError> {
let client = Client::from_env()?;
let path = format!("{BASE}/rows");
let mib = std::env::var("YT_STREAM_MIB")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(DEFAULT_MIB);
let target = mib * 1024 * 1024;
step("Preparing Cypress");
client.remove_tree(BASE)?;
client.create("table", &path)?;
let baseline = peak_rss();
done(&format!("{path}, peak RSS so far {}", megabytes(baseline)));
step(&format!("Writing about {mib} MiB from a generator"));
let rows = Rows::of_at_least(target);
let expected = rows.rows_to_come();
client.write_table_streaming(&path, rows)?;
let after_write = peak_rss();
done(&format!(
"{expected} rows, {} on the cluster, peak RSS {}",
megabytes(
client
.get(&format!("{path}/@uncompressed_data_size"))?
.as_i64()
.unwrap_or(0) as u64
),
megabytes(after_write)
));
step("Reading it back as a stream");
let mut reader = JobReader::binary(client.read_table_streaming(&path)?);
let mut counted = 0_u64;
let mut total = 0_i64;
while let Some(event) = reader.next_event().map_err(decoding)? {
if let Event::Row(row) = event {
counted += 1;
total += row.parse::<Counted>().map_err(decoding)?.n;
}
}
let after_stream = peak_rss();
check(
&format!(
"{counted} rows counted, peak RSS {}",
megabytes(after_stream)
),
counted == expected,
)?;
check(
"and their values add up to what was written",
total == expected_total(expected),
)?;
step("The same table, read into memory");
let whole = client.read_table(&path)?;
let after_buffered = peak_rss();
done(&format!(
"{} in hand, peak RSS {}",
megabytes(whole.len() as u64),
megabytes(after_buffered)
));
println!(
"\nStreaming the {} table cost {} of peak RSS; reading it in cost {}.",
megabytes(whole.len() as u64),
megabytes(after_stream.saturating_sub(baseline)),
megabytes(after_buffered.saturating_sub(after_stream))
);
println!("Table left at {path}");
Ok(())
}
#[derive(serde::Deserialize)]
struct Counted {
n: i64,
}
struct Rows {
remaining: u64,
total: u64,
buffer: Vec<u8>,
position: usize,
}
impl Rows {
fn of_at_least(bytes: u64) -> Self {
let per_row = encode(0).len() as u64;
let count = bytes.div_ceil(per_row);
Self {
remaining: count,
total: count,
buffer: Vec::with_capacity(CHUNK + 128),
position: 0,
}
}
fn rows_to_come(&self) -> u64 {
self.total
}
}
impl Read for Rows {
fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
if self.position == self.buffer.len() {
if self.remaining == 0 {
return Ok(0);
}
self.buffer.clear();
self.position = 0;
while self.buffer.len() < CHUNK && self.remaining > 0 {
self.buffer
.extend_from_slice(&encode(self.total - self.remaining));
self.remaining -= 1;
}
}
let n = out.len().min(self.buffer.len() - self.position);
out[..n].copy_from_slice(&self.buffer[self.position..self.position + n]);
self.position += n;
Ok(n)
}
}
fn encode(n: u64) -> Vec<u8> {
use serde::Serialize;
use ytsaurus_yson::{YsonFormat, to_vec};
#[derive(Serialize)]
struct Row<'a> {
n: i64,
payload: &'a str,
}
let row = Row {
n: n as i64,
payload: "0123456789abcdef0123456789abcdef",
};
let mut bytes = to_vec(&row, YsonFormat::Binary).expect("encodes");
bytes.push(b';');
bytes
}
fn expected_total(count: u64) -> i64 {
let count = count as i64;
count * (count - 1) / 2
}
fn decoding(e: ytsaurus_job::JobError) -> ClientError {
ClientError::Decode {
command: "read_table".to_owned(),
reason: e.to_string(),
}
}
fn megabytes(bytes: u64) -> String {
format!("{:.1} MiB", bytes as f64 / (1024.0 * 1024.0))
}
#[cfg(unix)]
fn peak_rss() -> u64 {
#[repr(C)]
#[derive(Default)]
struct Timeval {
tv_sec: i64,
tv_usec: i64,
}
#[repr(C)]
#[derive(Default)]
struct Rusage {
ru_utime: Timeval,
ru_stime: Timeval,
ru_maxrss: i64,
rest: [i64; 13],
}
unsafe extern "C" {
fn getrusage(who: i32, usage: *mut Rusage) -> i32;
}
let mut usage = Rusage::default();
let rc = unsafe { getrusage(0, &raw mut usage) };
if rc != 0 {
return 0;
}
let scale = if cfg!(target_os = "macos") { 1 } else { 1024 };
(usage.ru_maxrss.max(0) as u64) * scale
}
#[cfg(not(unix))]
fn peak_rss() -> u64 {
0
}
fn step(what: &str) {
println!("\n== {what}");
}
fn done(what: &str) {
println!(" ok {what}");
}
fn check(what: &str, passed: bool) -> Result<(), ClientError> {
if passed {
done(what);
return Ok(());
}
eprintln!(" FAIL {what}");
Err(ClientError::Config(format!("check failed: {what}")))
}