use std::process::ExitCode;
use std::time::Instant;
use serde::Serialize;
use ytsaurus_client::{CachedFile, Client, ClientError, MapSpec};
const BASE: &str = "//tmp/ytsaurus_rs_cached";
const CACHE: &str = "//tmp/ytsaurus_rs_cached_cache";
const WORKER: &str = "target/x86_64-unknown-linux-musl/release-worker/cat";
const SAMPLE: [Row; 2] = [Row { key: "a", count: 1 }, Row { key: "b", count: 2 }];
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("\ncached_upload failed: {e}");
ExitCode::FAILURE
}
}
}
fn run() -> Result<(), ClientError> {
let cache = std::env::var("YT_FILE_CACHE")
.map(|named| named.trim().to_owned())
.ok()
.filter(|named| !named.is_empty())
.unwrap_or_else(|| CACHE.to_owned());
let client = Client::from_env()?.with_file_cache(&cache);
if !std::path::Path::new(WORKER).exists() {
eprintln!("worker not found at {WORKER}");
eprintln!("build it first: scripts/build-worker.sh cat");
return Err(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"))?;
client.write_table_rows(format!("{BASE}/input"), SAMPLE)?;
let size = std::fs::metadata(WORKER).map(|m| m.len()).unwrap_or(0);
done(&format!("{BASE}, worker is {} KiB", size / 1024));
done(&format!("caching into {cache}"));
step("Clearing this binary out of the cache, so the first call is a miss");
let digest = md5_of(WORKER)?;
if let Some(cached) = client.file_from_cache(&digest)? {
if let Err(refused) = client.remove(&cached) {
return Err(nothing_to_clear(&cache, &cached, &refused));
}
done(&format!("removed {cached}"));
} else {
done("nothing cached");
}
step("First upload");
let (first, cold) = timed(|| client.upload_worker_cached(WORKER))?;
describe(&first, cold);
check("the first call uploaded it", first.uploaded)?;
if !first.cached {
return Err(nothing_to_demonstrate(&first));
}
step("Second upload of the same binary");
let (second, warm) = timed(|| client.upload_worker_cached(WORKER))?;
describe(&second, warm);
check("the second call skipped the upload", !second.uploaded)?;
check("and found it in the cache", second.cached)?;
check("and found the same file", second.path == first.path)?;
check(
&format!(
"and was quicker: {:.0} ms against {:.0} ms",
warm.as_secs_f64() * 1000.0,
cold.as_secs_f64() * 1000.0
),
warm < cold,
)?;
step("Running the cached binary");
let spec = MapSpec::new(
"./cat",
[format!("{BASE}/input")],
[format!("{BASE}/output")],
)
.with_local_file_named(&second.path, &second.name)
.with_memory_limit(512 * 1024 * 1024);
let id = client.start_map(&spec)?;
client.wait_for_operation(&id)?;
let before = client.read_table(format!("{BASE}/input"))?;
let after = client.read_table(format!("{BASE}/output"))?;
check(
"the identity map reproduced its input",
before == after && !after.is_empty(),
)?;
println!("\nOne upload, any number of launches. Tables left at {BASE}, cache at {cache}");
Ok(())
}
fn timed<T>(
action: impl FnOnce() -> Result<T, ClientError>,
) -> Result<(T, std::time::Duration), ClientError> {
let started = Instant::now();
let value = action()?;
Ok((value, started.elapsed()))
}
fn describe(file: &CachedFile, took: std::time::Duration) {
println!(
" {} in {:.0} ms -> {}{}",
if file.uploaded {
"uploaded"
} else {
"cache hit"
},
took.as_secs_f64() * 1000.0,
file.path,
if file.cached { "" } else { " (not cached)" }
);
}
fn nothing_to_demonstrate(first: &CachedFile) -> ClientError {
eprintln!(" FAIL nothing went into the cache, so there is no hit to demonstrate");
eprintln!(" the worker went to {} instead", first.path);
eprintln!(" every launch will send the whole binary again until that changes");
eprintln!(" the warning printed above names the cache that refused it and quotes");
eprintln!(" the cluster; unset YT_FILE_CACHE to let the example use the one it");
eprintln!(" brings ({CACHE}), or point it at a path you can write");
eprintln!(" to, and then the example has something to show");
ClientError::Config(
"the file cache would not take the worker, so there is no cache hit to demonstrate"
.to_owned(),
)
}
fn nothing_to_clear(cache: &str, entry: &str, refused: &ClientError) -> ClientError {
eprintln!(" FAIL {entry} is in the cache and this installation will not remove it");
eprintln!(" {refused}");
eprintln!(" so the first upload would be a hit, and there is no cold call to time");
eprintln!(" unset YT_FILE_CACHE to use the cache this example brings ({CACHE}),");
eprintln!(" or point it at one you can write to — {cache} is not that");
ClientError::Config(format!(
"the worker is already in {cache} and this caller may not clear it, \
so there is no cold upload to measure"
))
}
fn md5_of(path: &str) -> Result<String, ClientError> {
let bytes = std::fs::read(path).map_err(|source| ClientError::Io {
path: path.to_owned(),
source,
})?;
Ok(format!("{:x}", md5::compute(&bytes)))
}
#[derive(Serialize)]
struct Row {
key: &'static str,
count: i64,
}
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}")))
}