use std::process::ExitCode;
use std::time::{Duration, Instant};
use ytsaurus_client::{Client, ClientError, VanillaSpec, VanillaTask, yson_build};
const SLEEP_SECONDS: u32 = 300;
const PATIENCE: Duration = Duration::from_secs(120);
const REASON: &str = "stopped by the abort example";
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("\nabort failed: {e}");
ExitCode::FAILURE
}
}
}
fn run() -> Result<(), ClientError> {
let client = Client::from_env()?;
step(&format!(
"Starting something that would run for {SLEEP_SECONDS}s"
));
let mut sleeper = Sleeper::start(&client)?;
let id = sleeper.id.clone();
println!(" operation {id}");
let waited = wait_for_state(&client, &id, "running", PATIENCE)?;
done(&format!("it is running, {:.1}s in", waited.as_secs_f64()));
let job = wait_for_a_running_job(&client, &id, PATIENCE)?;
done(&format!(
"with a job running on {}",
job.address.as_deref().unwrap_or("?")
));
step("Aborting it, with a reason");
let asked = Instant::now();
client.abort_operation(&id, Some(REASON))?;
sleeper.finished();
let acknowledged = asked.elapsed();
done(&format!(
"the scheduler took the request in {:.0} ms",
acknowledged.as_secs_f64() * 1000.0
));
let stopped = wait_for_state(&client, &id, "aborted", PATIENCE)?;
done(&format!(
"and it was already `aborted` — {:.1}s of waiting",
stopped.as_secs_f64()
));
let drained = wait_for_no_running_jobs(&client, &id, PATIENCE)?;
done(&format!(
"and its job stopped {:.1}s after the operation did",
drained.as_secs_f64()
));
step("Reading back why it stopped");
let error = client
.operation_result_error(&id)?
.unwrap_or_else(|| "(the cluster recorded no error at all)".to_owned());
println!(" {error}");
check(
"the cluster recorded the abort as a user request",
error.contains("aborted by user request"),
)?;
check(
&format!("and kept the reason given: {REASON:?}"),
error.contains(REASON),
)?;
step("Aborting it again");
match client.abort_operation(&id, None) {
Ok(()) => {
eprintln!(" FAIL a finished operation accepted a second abort");
return Err(ClientError::Config(
"abort_operation is idempotent after all, and the docs say it is not".to_owned(),
));
}
Err(e) => {
check(
"is refused, because the scheduler has let go of it",
e.to_string().contains("No such operation"),
)?;
println!(" {e}");
}
}
step("Aborting one that has not started running yet");
let awkward = "he said \"stop\"\nand meant it";
let mut sleeper = Sleeper::start(&client)?;
let id = sleeper.id.clone();
let state = client.operation_state(&id)?;
client.abort_operation(&id, Some(awkward))?;
sleeper.finished();
let early = wait_for_state(&client, &id, "aborted", PATIENCE)?;
done(&format!(
"aborted from `{state}` in {:.1}s",
early.as_secs_f64()
));
let error = client.operation_result_error(&id)?.unwrap_or_default();
check(
"and the quote and the newline came back unharmed",
error.contains(awkward),
)?;
step("Pulling an operation out from under a wait");
let mut sleeper = Sleeper::start(&client)?;
let id = sleeper.id.clone();
sleeper.finished(); let stopper = client.clone();
let stopping = id.clone();
let hand = std::thread::spawn(move || {
std::thread::sleep(Duration::from_secs(5));
stopper.abort_operation(&stopping, Some("stopped by somebody else"))
});
let waited = match client.wait_for_operation(&id) {
Ok(()) => {
eprintln!(" FAIL an aborted operation was waited for successfully");
return Err(ClientError::Config(
"wait_for_operation returned Ok for an operation that was aborted".to_owned(),
));
}
Err(e) => e,
};
hand.join().expect("the aborting thread finished")?;
let reported = waited.to_string();
check(
"the wait fails, naming the state and the reason",
reported.contains("aborted") && reported.contains("stopped by somebody else"),
)?;
println!(" {}", reported.replace('\n', "\n "));
println!("\nAn operation nobody is waiting for is an operation nobody should be paying");
println!(
"for. Asked at {:.0} ms, stopped {:.1}s later.",
acknowledged.as_secs_f64() * 1000.0,
stopped.as_secs_f64()
);
Ok(())
}
struct Sleeper<'a> {
client: &'a Client,
id: String,
finished: bool,
}
impl<'a> Sleeper<'a> {
fn start(client: &'a Client) -> Result<Self, ClientError> {
Ok(Self {
id: start_sleeping(client)?,
client,
finished: false,
})
}
fn finished(&mut self) {
self.finished = true;
}
}
impl Drop for Sleeper<'_> {
fn drop(&mut self) {
if !self.finished {
let _ = self
.client
.abort_operation(&self.id, Some("the abort example gave up"));
}
}
}
fn start_sleeping(client: &Client) -> Result<String, ClientError> {
let spec = VanillaSpec::new(
VanillaTask::new("sleeper", format!("sleep {SLEEP_SECONDS}"), 1)
.with_memory_limit(256 * 1024 * 1024),
)
.with_raw("max_failed_job_count", yson_build::int(1));
client.start_vanilla(&spec)
}
fn wait_for_a_running_job(
client: &Client,
id: &str,
patience: Duration,
) -> Result<ytsaurus_client::JobInfo, ClientError> {
let started = Instant::now();
while started.elapsed() < patience {
if let Some(job) = client
.list_jobs(id, Some("running"), 10)?
.into_iter()
.next()
{
return Ok(job);
}
std::thread::sleep(Duration::from_millis(250));
}
Err(ClientError::Config(format!(
"operation {id} had no running job within {:.0}s",
patience.as_secs_f64()
)))
}
fn wait_for_no_running_jobs(
client: &Client,
id: &str,
patience: Duration,
) -> Result<Duration, ClientError> {
let started = Instant::now();
while started.elapsed() < patience {
if client.list_jobs(id, Some("running"), 1)?.is_empty() {
return Ok(started.elapsed());
}
std::thread::sleep(Duration::from_millis(250));
}
Err(ClientError::Config(format!(
"operation {id} still had a running job within {:.0}s",
patience.as_secs_f64()
)))
}
fn wait_for_state(
client: &Client,
id: &str,
wanted: &str,
patience: Duration,
) -> Result<Duration, ClientError> {
let started = Instant::now();
while started.elapsed() < patience {
let state = client.operation_state(id)?;
if state == wanted {
return Ok(started.elapsed());
}
if matches!(state.as_str(), "completed" | "failed") {
return Err(ClientError::Config(format!(
"operation {id} reached {state}, and was waiting for {wanted}"
)));
}
std::thread::sleep(Duration::from_millis(250));
}
Err(ClientError::Config(format!(
"operation {id} did not reach {wanted} within {:.0}s",
patience.as_secs_f64()
)))
}
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}")))
}