use std::io::{BufRead, BufReader, Read, Write};
use std::net::{TcpListener, TcpStream};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use ytsaurus_client::{Client, ClientError, RetryPolicy};
const CACHE: &str = "//tmp/yt_wrapper/file_storage/new_cache";
const IN_THE_CACHE: &str = "//tmp/yt_wrapper/file_storage/new_cache/ab/abcdef";
const MISS: &str = r#""""#;
#[test]
fn a_cache_the_installation_keeps_to_itself_still_launches_the_worker() {
let cluster = cluster(|sent| match sent.command.as_str() {
"get_file_from_cache" => Answer::Body(MISS.to_owned()),
_ if sent.mentions(CACHE) => Answer::Denied,
_ => Answer::Body("{}".to_owned()),
});
let worker = worker_file("managed");
let uploaded = cluster
.client()
.upload_worker_cached(&worker)
.expect("a cache that refuses this caller is not a failed upload");
assert!(
uploaded.uploaded,
"the fallback is an upload, and says so: {uploaded:?}"
);
assert!(
!uploaded.cached,
"the fallback claims the cache took it: {uploaded:?}"
);
assert!(
!uploaded.path.starts_with(CACHE) && uploaded.path.starts_with("//tmp/"),
"the worker went somewhere the caller cannot write: {}",
uploaded.path
);
assert_eq!(uploaded.name, "managed");
let sent = cluster.sent();
assert_eq!(
commands(&sent),
[
"get_file_from_cache",
"create",
"create",
"write_file",
"set"
],
"the lookup, the refused cache directory, then a plain upload: {sent:?}"
);
assert!(
!sent.iter().any(|s| s.command == "put_file_to_cache"),
"the cache was still asked to take the file: {sent:?}"
);
for wrote in sent.iter().filter(|s| s.command == "write_file") {
assert!(
wrote.mentions(&uploaded.path) && !wrote.mentions(CACHE),
"the bytes went into the cache after all: {wrote:?}"
);
}
}
#[test]
fn a_refusal_the_cluster_wrapped_is_still_the_cache_refusing() {
let cluster = cluster(|sent| match sent.command.as_str() {
"get_file_from_cache" => Answer::Body(MISS.to_owned()),
_ if sent.mentions(CACHE) => Answer::DeniedInside,
_ => Answer::Body("{}".to_owned()),
});
let worker = worker_file("wrapped");
let uploaded = cluster
.client()
.upload_worker_cached(&worker)
.expect("a refusal one level down is the same refusal");
assert!(!uploaded.cached, "{uploaded:?}");
assert_eq!(
commands(&cluster.sent()),
[
"get_file_from_cache",
"create",
"create",
"write_file",
"set"
],
"the wrapped refusal was reported rather than fallen back from: {:?}",
cluster.sent()
);
}
#[test]
fn a_cache_that_takes_the_bytes_and_refuses_the_handover_falls_back_too() {
let cluster = cluster(|sent| match sent.command.as_str() {
"get_file_from_cache" => Answer::Body(MISS.to_owned()),
"put_file_to_cache" => Answer::Denied,
_ => Answer::Body("{}".to_owned()),
});
let worker = worker_file("handover");
let uploaded = cluster
.client()
.upload_worker_cached(&worker)
.expect("a handover the cache refuses is not a failed upload");
assert!(
!uploaded.path.starts_with(CACHE),
"the path is the cache's, and the cache refused it: {}",
uploaded.path
);
assert!(
uploaded.uploaded && !uploaded.cached,
"the bytes went up and the cache did not keep them: {uploaded:?}"
);
let sent = cluster.sent();
assert_eq!(
commands(&sent),
[
"get_file_from_cache",
"create",
"create",
"write_file",
"set",
"put_file_to_cache",
"remove",
"create",
"write_file",
"set",
],
"{sent:?}"
);
let writes: Vec<&Sent> = sent.iter().filter(|s| s.command == "write_file").collect();
assert_eq!(writes.len(), 2, "the bytes are sent twice: {sent:?}");
assert!(writes[0].mentions(CACHE), "{:?}", writes[0]);
assert!(
writes[1].mentions(&uploaded.path) && !writes[1].mentions(CACHE),
"the second upload went back into the cache: {:?}",
writes[1]
);
}
#[test]
fn a_cluster_that_allows_the_cache_still_uses_it() {
let cluster = cluster(|sent| match sent.command.as_str() {
"get_file_from_cache" => Answer::Body(MISS.to_owned()),
"put_file_to_cache" => Answer::Body(format!("{IN_THE_CACHE:?}")),
_ => Answer::Body("{}".to_owned()),
});
let worker = worker_file("ordinary");
let uploaded = cluster
.client()
.upload_worker_cached(&worker)
.expect("uploads");
assert_eq!(uploaded.path, IN_THE_CACHE);
assert!(uploaded.uploaded && uploaded.cached, "{uploaded:?}");
let sent = cluster.sent();
assert_eq!(
commands(&sent),
[
"get_file_from_cache",
"create",
"create",
"write_file",
"set",
"put_file_to_cache",
"remove",
"set",
],
"{sent:?}"
);
assert!(
sent.last()
.expect("something was sent")
.mentions(IN_THE_CACHE),
"the last thing done is to the cached node: {sent:?}"
);
for wrote in sent.iter().filter(|s| s.command == "write_file") {
assert!(wrote.mentions(CACHE), "an upload left the cache: {wrote:?}");
}
}
#[test]
fn a_cache_hit_uploads_nothing_and_falls_back_to_nothing() {
let cluster = cluster(|sent| match sent.command.as_str() {
"get_file_from_cache" => Answer::Body(format!("{IN_THE_CACHE:?}")),
_ => Answer::Body("{}".to_owned()),
});
let worker = worker_file("hit");
let uploaded = cluster
.client()
.upload_worker_cached(&worker)
.expect("finds it");
assert_eq!(uploaded.path, IN_THE_CACHE);
assert!(!uploaded.uploaded, "a hit uploaded something: {uploaded:?}");
assert!(uploaded.cached, "a hit is not cached: {uploaded:?}");
assert_eq!(commands(&cluster.sent()), ["get_file_from_cache"]);
}
#[test]
fn a_create_that_failed_for_some_other_reason_is_not_a_cache_to_give_up_on() {
let cluster = cluster(|sent| match sent.command.as_str() {
"get_file_from_cache" => Answer::Body(MISS.to_owned()),
"create" => Answer::Failed(500, "Error resolving path //tmp/yt_wrapper"),
_ => Answer::Body("{}".to_owned()),
});
let worker = worker_file("resolve");
let error = cluster
.client()
.upload_worker_cached(&worker)
.expect_err("a resolve error is the caller's to hear about");
assert!(
matches!(&error, ClientError::Cluster { code: 500, command, .. } if command == "create"),
"{error:?}"
);
assert_eq!(
commands(&cluster.sent()),
["get_file_from_cache", "create"],
"the failure was carried on from rather than reported: {:?}",
cluster.sent()
);
}
#[test]
fn an_access_denied_that_is_not_the_caches_is_not_swallowed() {
let cluster = cluster(|sent| match sent.command.as_str() {
"get_file_from_cache" => Answer::Body(MISS.to_owned()),
"write_file" => Answer::Denied,
_ => Answer::Body("{}".to_owned()),
});
let worker = worker_file("denied-write");
let error = cluster
.client()
.upload_worker_cached(&worker)
.expect_err("a denial that is not about the cache is still a denial");
assert!(
matches!(&error, ClientError::Cluster { code: 901, command, .. } if command == "write_file"),
"{error:?}"
);
let sent = cluster.sent();
assert_eq!(
sent.iter().filter(|s| s.command == "write_file").count(),
1,
"the refused upload was tried again somewhere else: {sent:?}"
);
assert!(
!sent
.iter()
.any(|s| s.command == "create" && !s.mentions(CACHE)),
"a fallback upload was started outside the cache: {sent:?}"
);
}
#[derive(Clone, Debug)]
struct Sent {
command: String,
parameters: String,
}
impl Sent {
fn mentions(&self, text: &str) -> bool {
self.parameters.contains(text)
}
}
const ACCESS_DENIED: &str = "Access denied for user \"tester\": \"write | modify_children\" \
permission for node //tmp/yt_wrapper/file_storage/new_cache \
is not allowed by any matching ACE";
enum Answer {
Body(String),
Denied,
DeniedInside,
Failed(i64, &'static str),
}
impl Answer {
fn parts(&self) -> (&'static str, Option<String>, String) {
match self {
Answer::Body(body) => ("200 OK", None, body.clone()),
Answer::Denied => (
"403 Forbidden",
Some(document(901, ACCESS_DENIED)),
String::new(),
),
Answer::DeniedInside => (
"403 Forbidden",
Some(wrapping(
1,
"Error creating node //tmp/yt_wrapper/file_storage/new_cache",
&document(901, ACCESS_DENIED),
)),
String::new(),
),
Answer::Failed(code, message) => (
"400 Bad Request",
Some(document(*code, message)),
String::new(),
),
}
}
}
fn document(code: i64, message: &str) -> String {
format!(r#"{{"code":{code},"message":"{}"}}"#, escape(message))
}
fn wrapping(code: i64, message: &str, inner: &str) -> String {
format!(
r#"{{"code":{code},"message":"{}","inner_errors":[{inner}]}}"#,
escape(message)
)
}
fn escape(message: &str) -> String {
message.replace('\\', r"\\").replace('"', "\\\"")
}
type Answering = Arc<dyn Fn(&Sent) -> Answer + Send + Sync>;
struct Cluster {
proxy: String,
sent: Arc<Mutex<Vec<Sent>>>,
}
impl Cluster {
fn client(&self) -> Client {
Client::new(&self.proxy).with_retries(RetryPolicy::none())
}
fn sent(&self) -> Vec<Sent> {
self.sent.lock().expect("not poisoned").clone()
}
}
fn commands(sent: &[Sent]) -> Vec<&str> {
sent.iter().map(|s| s.command.as_str()).collect()
}
fn cluster(answer: impl Fn(&Sent) -> Answer + Send + Sync + 'static) -> Cluster {
let listener = TcpListener::bind("127.0.0.1:0").expect("binds");
let proxy = format!("http://{}", listener.local_addr().expect("has an address"));
let sent = Arc::new(Mutex::new(Vec::new()));
let answering: Answering = Arc::new(answer);
let log = Arc::clone(&sent);
std::thread::spawn(move || {
for stream in listener.incoming() {
let Ok(stream) = stream else { return };
let answering = Arc::clone(&answering);
let log = Arc::clone(&log);
std::thread::spawn(move || serve(&stream, &answering, &log));
}
});
Cluster { proxy, sent }
}
fn serve(stream: &TcpStream, answer: &Answering, log: &Mutex<Vec<Sent>>) {
stream
.set_read_timeout(Some(Duration::from_secs(30)))
.expect("sets a timeout");
let mut writer = stream.try_clone().expect("clones");
let mut reader = BufReader::new(stream.try_clone().expect("clones"));
while let Some(request) = read_request(&mut reader) {
log.lock().expect("not poisoned").push(request.clone());
let (status, error, body) = answer(&request).parts();
let mut reply = format!("HTTP/1.1 {status}\r\nContent-Length: {}\r\n", body.len());
if let Some(error) = error {
reply.push_str(&format!("X-YT-Error: {error}\r\n"));
}
reply.push_str("Content-Type: application/x-yt-yson-text\r\n\r\n");
reply.push_str(&body);
if writer.write_all(reply.as_bytes()).is_err() {
return;
}
writer.flush().ok();
}
}
fn read_request(reader: &mut BufReader<TcpStream>) -> Option<Sent> {
let mut head = String::new();
loop {
let mut line = String::new();
match reader.read_line(&mut line) {
Ok(0) | Err(_) => return None,
Ok(_) if line == "\r\n" => break,
Ok(_) => head.push_str(&line),
}
}
if let Some(length) = header(&head, "content-length").and_then(|v| v.parse().ok()) {
let mut body = vec![0_u8; length];
reader.read_exact(&mut body).ok()?;
}
Some(Sent {
command: head
.lines()
.next()?
.split_whitespace()
.nth(1)?
.rsplit('/')
.next()?
.to_owned(),
parameters: header(&head, "x-yt-parameters").unwrap_or_default(),
})
}
fn header(head: &str, name: &str) -> Option<String> {
head.lines()
.find(|line| {
line.to_lowercase()
.starts_with(&format!("{}:", name.to_lowercase()))
})
.map(|line| line[line.find(':').unwrap_or(0) + 1..].trim().to_owned())
}
fn worker_file(name: &str) -> std::path::PathBuf {
let directory = std::env::temp_dir().join(format!("ytsaurus-rs-cache-{}", std::process::id()));
std::fs::create_dir_all(&directory).expect("creates");
let path = directory.join(name);
std::fs::write(&path, format!("not really a worker: {name}")).expect("writes");
path
}