use std::process::ExitCode;
use serde::Serialize;
use ytsaurus_client::{Client, MapSpec};
use ytsaurus_yson::{Scan, YsonFormat, YsonNode, YsonValue, from_slice, scan::scan_value};
const BASE: &str = "//tmp/ytsaurus_rs_launch";
const WORKER: &str = "target/x86_64-unknown-linux-musl/release-worker/cat";
const SAMPLE: [Row; 3] = [
Row {
key: "alpha",
count: 1,
blob: &[0xDE, 0xAD],
ratio: 0.5,
flag: true,
},
Row {
key: "beta",
count: -2,
blob: &[0x00, 0xFF],
ratio: -1.25,
flag: false,
},
Row {
key: "",
count: i64::MAX,
blob: &[],
ratio: 0.0,
flag: true,
},
];
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("\nlaunch failed: {e}");
let mut source = std::error::Error::source(&e);
while let Some(cause) = source {
eprintln!(" caused by: {cause}");
source = cause.source();
}
ExitCode::FAILURE
}
}
}
fn run() -> Result<(), ytsaurus_client::ClientError> {
let client = Client::from_env()?;
if !std::path::Path::new(WORKER).exists() {
eprintln!("worker not found at {WORKER}");
eprintln!("build it first: scripts/build-worker.sh cat");
return Err(ytsaurus_client::ClientError::Config(
"the worker binary has not been built".to_owned(),
));
}
step("Preparing Cypress");
client.remove_tree(BASE)?;
client.create("map_node", BASE)?;
client.create("table", &format!("{BASE}/input"))?;
client.create("table", &format!("{BASE}/output"))?;
done(BASE);
step("Uploading the worker");
client.upload_worker(WORKER, &format!("{BASE}/cat"))?;
done(&format!("{BASE}/cat (executable)"));
step("Writing input rows");
client.write_table_rows(format!("{BASE}/input"), SAMPLE)?;
done(&format!(
"{} rows",
client.row_count(&format!("{BASE}/input"))?
));
step("Starting the map operation");
let spec = MapSpec::new(
"./cat",
[format!("{BASE}/input")],
[format!("{BASE}/output")],
)
.with_local_file(format!("{BASE}/cat"))
.with_memory_limit(512 * 1024 * 1024);
let id = client.start_map(&spec)?;
done(&format!("operation {id}"));
step("Waiting for it to finish");
client.wait_for_operation(&id)?;
done("completed");
step("Checking the result");
let before = client.read_table(format!("{BASE}/input"))?;
let after = client.read_table(format!("{BASE}/output"))?;
if before != after {
eprintln!(
" output differs from input: {} vs {} bytes",
before.len(),
after.len()
);
return Err(ytsaurus_client::ClientError::Config(
"the identity map changed its input".to_owned(),
));
}
done(&format!("identical ({} bytes)", after.len()));
step("Decoding a row, to prove it is real data");
let first = first_record(&after).ok_or_else(|| {
ytsaurus_client::ClientError::Config("the output table has no first row".to_owned())
})?;
let value: YsonValue = from_slice(first, YsonFormat::Binary).map_err(|e| {
ytsaurus_client::ClientError::Decode {
command: "read_table".to_owned(),
reason: e.to_string(),
}
})?;
let YsonNode::Map(m) = &value.node else {
return Err(ytsaurus_client::ClientError::Config(
"the first record of the output is not a row".to_owned(),
));
};
let columns: Vec<String> = m
.keys()
.map(|k| String::from_utf8_lossy(k).into_owned())
.collect();
let mut found = columns.clone();
found.sort();
if found != ["blob", "count", "flag", "key", "ratio"] {
eprintln!(" FAIL first row has columns {columns:?}");
return Err(ytsaurus_client::ClientError::Config(
"the row that came back is not the row that went in".to_owned(),
));
}
done(&format!("first row has columns {columns:?}"));
println!("\nAll done — no Python was involved.");
println!("Tables left at {BASE}");
Ok(())
}
fn step(what: &str) {
println!("\n== {what}");
}
fn done(what: &str) {
println!(" ok {what}");
}
fn first_record(data: &[u8]) -> Option<&[u8]> {
let trimmed = data.strip_prefix(b";").unwrap_or(data);
match scan_value(trimmed, YsonFormat::Binary).ok()? {
Scan::Complete { len } => Some(&trimmed[..len]),
Scan::Incomplete => None,
}
}
#[derive(Serialize)]
struct Row {
key: &'static str,
count: i64,
#[serde(with = "serde_bytes")]
blob: &'static [u8],
ratio: f64,
flag: bool,
}