From a75351b142e9b08c93ec575ad00d6a3fb7385b9b Mon Sep 17 00:00:00 2001 From: CastilloDel Date: Sun, 1 Aug 2021 14:48:48 +0100 Subject: [PATCH 01/50] POC for a simple benchmark --- htsget-http-actix/Cargo.toml | 10 +++++++++- htsget-http-actix/benches/request-benchmark.rs | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 htsget-http-actix/benches/request-benchmark.rs diff --git a/htsget-http-actix/Cargo.toml b/htsget-http-actix/Cargo.toml index e16ea14c8..fd4a33681 100644 --- a/htsget-http-actix/Cargo.toml +++ b/htsget-http-actix/Cargo.toml @@ -13,4 +13,12 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" futures-util = { version = "0.3.5", default-features = false } htsget-http-core = { path = "../htsget-http-core" } -htsget-search = { path = "../htsget-search" } \ No newline at end of file +htsget-search = { path = "../htsget-search" } + +[dev-dependencies] +criterion = "0.3" +reqwest = { version = "0.11", features = ["blocking", "json"] } + +[[bench]] +name = "request-benchmark" +harness = false \ No newline at end of file diff --git a/htsget-http-actix/benches/request-benchmark.rs b/htsget-http-actix/benches/request-benchmark.rs new file mode 100644 index 000000000..582bc4182 --- /dev/null +++ b/htsget-http-actix/benches/request-benchmark.rs @@ -0,0 +1,15 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use reqwest::{blocking::Client, Url}; + +fn request() { + let mut url = Url::parse("http://127.0.0.1/variants/data/vcf/sample1-bcbio-cancer").unwrap(); + url.set_port(Some(8080)).unwrap(); + Client::new().post(url).send().unwrap(); +} + +fn criterion_benchmark(c: &mut Criterion) { + c.bench_function("simple POST request ", |b| b.iter(|| request())); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); From 235029f1bab34ef8a1e7d6ecb51a38290289cfcd Mon Sep 17 00:00:00 2001 From: CastilloDel Date: Tue, 3 Aug 2021 12:40:24 +0100 Subject: [PATCH 02/50] Add config for the htsget-refserver --- htsget-http-actix/config.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 htsget-http-actix/config.json diff --git a/htsget-http-actix/config.json b/htsget-http-actix/config.json new file mode 100644 index 000000000..91994b31e --- /dev/null +++ b/htsget-http-actix/config.json @@ -0,0 +1,17 @@ +{ + "htsgetConfig": { + "props": { + "port": 8081 + }, + "reads": { + "dataSourceRegistry": { + "sources": [ + { + "pattern": "^(?P.*)$", + "path": "/data/bam/{id}.bam" + } + ] + } + } + } +} \ No newline at end of file From 9fcb2ea517e8b3111981d579e5a48238088fbf09 Mon Sep 17 00:00:00 2001 From: CastilloDel Date: Tue, 3 Aug 2021 12:40:44 +0100 Subject: [PATCH 03/50] Add a little script to start the htsget-refserver --- htsget-http-actix/docker-htsget-refserver.sh | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 htsget-http-actix/docker-htsget-refserver.sh diff --git a/htsget-http-actix/docker-htsget-refserver.sh b/htsget-http-actix/docker-htsget-refserver.sh new file mode 100644 index 000000000..c53bd41bf --- /dev/null +++ b/htsget-http-actix/docker-htsget-refserver.sh @@ -0,0 +1,2 @@ +docker image pull ga4gh/htsget-refserver:1.4.0 +docker container run -d -p 8081:3000 -v $(pwd)/../data:/data -v $(pwd):/config ga4gh/htsget-refserver:1.4.0 ./htsget-refserver -config /config/config.json \ No newline at end of file From 02075ea88877ed648766f6b11d6322c749616511 Mon Sep 17 00:00:00 2001 From: CastilloDel Date: Wed, 4 Aug 2021 14:02:57 +0100 Subject: [PATCH 04/50] Add a simple benchmark --- .../benches/request-benchmark.rs | 28 ++++++++++++++---- htsget-http-actix/docker-htsget-refserver.sh | 2 +- ...nfig.json => htsget-refserver-config.json} | 3 +- htsget-http-core/src/json_response.rs | 29 +++++++++---------- 4 files changed, 40 insertions(+), 22 deletions(-) rename htsget-http-actix/{config.json => htsget-refserver-config.json} (82%) diff --git a/htsget-http-actix/benches/request-benchmark.rs b/htsget-http-actix/benches/request-benchmark.rs index 582bc4182..558c4392b 100644 --- a/htsget-http-actix/benches/request-benchmark.rs +++ b/htsget-http-actix/benches/request-benchmark.rs @@ -1,14 +1,32 @@ use criterion::{criterion_group, criterion_main, Criterion}; +use htsget_http_core::JsonResponse; use reqwest::{blocking::Client, Url}; +use std::time::Duration; -fn request() { - let mut url = Url::parse("http://127.0.0.1/variants/data/vcf/sample1-bcbio-cancer").unwrap(); - url.set_port(Some(8080)).unwrap(); - Client::new().post(url).send().unwrap(); +fn request(url: Url) { + let _: JsonResponse = Client::new().get(url).send().unwrap().json().unwrap(); } fn criterion_benchmark(c: &mut Criterion) { - c.bench_function("simple POST request ", |b| b.iter(|| request())); + let mut group = c.benchmark_group("Requests"); + group + .sample_size(500) + .measurement_time(Duration::from_secs(10)); + group.bench_function("htsget-rs", |b| { + b.iter(|| { + let mut url = Url::parse("http://localhost/reads/data/bam/htsnexus_test_NA12878").unwrap(); + url.set_port(Some(8080)).unwrap(); + request(url) + }) + }); + group.bench_function("htsget-refserver", |b| { + b.iter(|| { + let mut url = Url::parse("http://localhost/reads/htsnexus_test_NA12878").unwrap(); + url.set_port(Some(8081)).unwrap(); + request(url) + }) + }); + group.finish(); } criterion_group!(benches, criterion_benchmark); diff --git a/htsget-http-actix/docker-htsget-refserver.sh b/htsget-http-actix/docker-htsget-refserver.sh index c53bd41bf..20d012995 100644 --- a/htsget-http-actix/docker-htsget-refserver.sh +++ b/htsget-http-actix/docker-htsget-refserver.sh @@ -1,2 +1,2 @@ docker image pull ga4gh/htsget-refserver:1.4.0 -docker container run -d -p 8081:3000 -v $(pwd)/../data:/data -v $(pwd):/config ga4gh/htsget-refserver:1.4.0 ./htsget-refserver -config /config/config.json \ No newline at end of file +docker container run -d -p 8081:3000 -v $(pwd)/../data:/data -v $(pwd):/config ga4gh/htsget-refserver:1.4.0 ./htsget-refserver -config /config/htsget-refserver-config.json \ No newline at end of file diff --git a/htsget-http-actix/config.json b/htsget-http-actix/htsget-refserver-config.json similarity index 82% rename from htsget-http-actix/config.json rename to htsget-http-actix/htsget-refserver-config.json index 91994b31e..1e0fe0e60 100644 --- a/htsget-http-actix/config.json +++ b/htsget-http-actix/htsget-refserver-config.json @@ -1,7 +1,8 @@ { "htsgetConfig": { "props": { - "port": 8081 + "port": 8081, + "host": "http://localhost:8081/" }, "reads": { "dataSourceRegistry": { diff --git a/htsget-http-core/src/json_response.rs b/htsget-http-core/src/json_response.rs index e5158390e..bd6322034 100644 --- a/htsget-http-core/src/json_response.rs +++ b/htsget-http-core/src/json_response.rs @@ -1,11 +1,11 @@ use std::collections::HashMap; use htsget_search::htsget::{Class, Format, Response, Url}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; /// A helper struct to convert [Responses](Response) to JSON. It implements [serde's Serialize trait](Serialize), /// so it's trivial to convert to JSON. -#[derive(Debug, PartialEq, Serialize)] +#[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct JsonResponse { htsget: HtsGetResponse, } @@ -20,7 +20,7 @@ impl JsonResponse { /// A helper struct to represent a JSON response. It shouldn't be used /// on its own, but with [JsonResponse] -#[derive(Debug, PartialEq, Serialize)] +#[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct HtsGetResponse { format: String, urls: Vec, @@ -39,26 +39,25 @@ impl HtsGetResponse { /// A helper struct to convert [Urls](Url) to JSON. It shouldn't be used /// on its own, but with [JsonResponse] -#[derive(Debug, PartialEq, Serialize)] +#[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct JsonUrl { url: String, - headers: HashMap, - class: String, + headers: Option>, + class: Option, } impl JsonUrl { fn new(url: Url) -> Self { JsonUrl { url: url.url, - headers: match url.headers { - Some(headers) => headers.get_inner(), - None => HashMap::new(), - }, - class: match url.class { - Class::Body => "body", - Class::Header => "header", - } - .to_string(), + headers: url.headers.map(|headers| headers.get_inner()), + class: Some( + match url.class { + Class::Body => "body", + Class::Header => "header", + } + .to_string(), + ), } } } From 650bbfdd7998e4a284bec90f50efad027006e666 Mon Sep 17 00:00:00 2001 From: CastilloDel Date: Thu, 5 Aug 2021 12:28:40 +0100 Subject: [PATCH 05/50] Comparison between total download size It isn't working because htsget-rs uses the file schema, which isn't supported by reqwest --- .../benches/request-benchmark.rs | 38 ++++++++++++++++++- htsget-http-core/src/json_response.rs | 12 +++--- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/htsget-http-actix/benches/request-benchmark.rs b/htsget-http-actix/benches/request-benchmark.rs index 558c4392b..fff4d4c6d 100644 --- a/htsget-http-actix/benches/request-benchmark.rs +++ b/htsget-http-actix/benches/request-benchmark.rs @@ -1,10 +1,44 @@ use criterion::{criterion_group, criterion_main, Criterion}; use htsget_http_core::JsonResponse; use reqwest::{blocking::Client, Url}; -use std::time::Duration; +use serde::Serialize; +use std::collections::HashMap; +use std::{convert::TryInto, time::Duration}; +#[derive(Serialize)] +struct Empty {} fn request(url: Url) { - let _: JsonResponse = Client::new().get(url).send().unwrap().json().unwrap(); + let client = Client::new(); + let response: JsonResponse = client + .get(url) + .json(&Empty {}) + .send() + .unwrap() + .json() + .unwrap(); + let download_size: usize = response + .htsget + .urls + .iter() + .map(|json_url| { + client + .get(&json_url.url) + .headers( + json_url + .headers + .as_ref() + .unwrap_or(&HashMap::new()) + .try_into() + .unwrap(), + ) + .send() + .unwrap() + .text() + .unwrap() + .len() + }) + .sum(); + println!("{}", download_size) } fn criterion_benchmark(c: &mut Criterion) { diff --git a/htsget-http-core/src/json_response.rs b/htsget-http-core/src/json_response.rs index bd6322034..7423c2baa 100644 --- a/htsget-http-core/src/json_response.rs +++ b/htsget-http-core/src/json_response.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; /// so it's trivial to convert to JSON. #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct JsonResponse { - htsget: HtsGetResponse, + pub htsget: HtsGetResponse, } impl JsonResponse { @@ -22,8 +22,8 @@ impl JsonResponse { /// on its own, but with [JsonResponse] #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct HtsGetResponse { - format: String, - urls: Vec, + pub format: String, + pub urls: Vec, } impl HtsGetResponse { @@ -41,9 +41,9 @@ impl HtsGetResponse { /// on its own, but with [JsonResponse] #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct JsonUrl { - url: String, - headers: Option>, - class: Option, + pub url: String, + pub headers: Option>, + pub class: Option, } impl JsonUrl { From 8adfd7af55b7d0dced7b33022a502a97d467adb7 Mon Sep 17 00:00:00 2001 From: CastilloDel Date: Fri, 6 Aug 2021 13:31:35 +0100 Subject: [PATCH 06/50] Change url types --- htsget-http-actix/src/main.rs | 3 +- htsget-http-core/src/lib.rs | 59 +++++++----------------- htsget-search/src/htsget/bam_search.rs | 28 ++++------- htsget-search/src/htsget/bcf_search.rs | 20 +++----- htsget-search/src/htsget/cram_search.rs | 26 ++++------- htsget-search/src/htsget/from_storage.rs | 6 +-- htsget-search/src/htsget/vcf_search.rs | 20 +++----- htsget-search/src/storage/local.rs | 39 ++++++++-------- 8 files changed, 75 insertions(+), 126 deletions(-) diff --git a/htsget-http-actix/src/main.rs b/htsget-http-actix/src/main.rs index 1e0594f31..e9591041c 100644 --- a/htsget-http-actix/src/main.rs +++ b/htsget-http-actix/src/main.rs @@ -43,12 +43,13 @@ async fn main() -> std::io::Result<()> { } let config = envy::from_env::().expect("The environment variables weren't properly set!"); let address = format!("{}:{}", config.htsget_ip, config.htsget_port); + let moved_address = address.clone(); let htsget_path = config.htsget_path.clone(); HttpServer::new(move || { App::new() .data(AppState { htsget: HtsGetFromStorage::new( - LocalStorage::new(htsget_path.clone()) + LocalStorage::new(htsget_path.clone(), moved_address.clone()) .expect("Couldn't create a Storage with the provided path"), ), config: config.clone(), diff --git a/htsget-http-core/src/lib.rs b/htsget-http-core/src/lib.rs index 99f020713..3bbb808ba 100644 --- a/htsget-http-core/src/lib.rs +++ b/htsget-http-core/src/lib.rs @@ -118,7 +118,6 @@ mod tests { htsget::{from_storage::HtsGetFromStorage, Format, Headers, Url}, storage::local::LocalStorage, }; - use std::path::PathBuf; #[test] fn get_request() { let mut request = HashMap::new(); @@ -129,14 +128,10 @@ mod tests { get_response_for_get_request(&get_searcher(), request, Endpoint::Reads), Ok(JsonResponse::from_response(Response::new( Format::Bam, - vec![Url::new(format!( - "file://{}", - get_base_path() - .join("bam") - .join("htsnexus_test_NA12878.bam") - .to_string_lossy() - )) - .with_headers(Headers::new(headers))] + vec![ + Url::new("http://localhost/data/bam/htsnexus_test_NA12878.bam") + .with_headers(Headers::new(headers)) + ] ))) ) } @@ -167,14 +162,10 @@ mod tests { get_response_for_get_request(&get_searcher(), request, Endpoint::Variants), Ok(JsonResponse::from_response(Response::new( Format::Vcf, - vec![Url::new(format!( - "file://{}", - get_base_path() - .join("vcf") - .join("sample1-bcbio-cancer.vcf.gz") - .to_string_lossy() - )) - .with_headers(Headers::new(headers))] + vec![ + Url::new("http://localhost/data/vcf/sample1-bcbio-cancer.vcf.gz") + .with_headers(Headers::new(headers)) + ] ))) ) } @@ -200,14 +191,10 @@ mod tests { ), Ok(JsonResponse::from_response(Response::new( Format::Bam, - vec![Url::new(format!( - "file://{}", - get_base_path() - .join("bam") - .join("htsnexus_test_NA12878.bam") - .to_string_lossy() - )) - .with_headers(Headers::new(headers))] + vec![ + Url::new("http://localhost/data/bam/htsnexus_test_NA12878.bam") + .with_headers(Headers::new(headers)) + ] ))) ) } @@ -260,27 +247,15 @@ mod tests { ), Ok(JsonResponse::from_response(Response::new( Format::Vcf, - vec![Url::new(format!( - "file://{}", - get_base_path() - .join("vcf") - .join("sample1-bcbio-cancer.vcf.gz") - .to_string_lossy() - )) - .with_headers(Headers::new(headers))] + vec![ + Url::new("http://localhost/data/vcf/sample1-bcbio-cancer.vcf.gz") + .with_headers(Headers::new(headers)) + ] ))) ) } - fn get_base_path() -> PathBuf { - std::env::current_dir() - .unwrap() - .parent() - .unwrap() - .join("data") - } - fn get_searcher() -> impl HtsGet { - HtsGetFromStorage::new(LocalStorage::new("../data").unwrap()) + HtsGetFromStorage::new(LocalStorage::new("../data", "localhost").unwrap()) } } diff --git a/htsget-search/src/htsget/bam_search.rs b/htsget-search/src/htsget/bam_search.rs index f57fdc99c..ba5a68602 100644 --- a/htsget-search/src/htsget/bam_search.rs +++ b/htsget-search/src/htsget/bam_search.rs @@ -170,6 +170,8 @@ pub mod tests { use super::*; + pub const EXPECTED_URL: &str = "http://localhost/data/htsnexus_test_NA12878.bam"; + #[test] fn search_all_reads() { with_local_storage(|storage| { @@ -180,7 +182,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bam, - vec![Url::new(expected_url(&storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=4668-2596799"))], )); assert_eq!(response, expected_response) @@ -197,7 +199,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bam, - vec![Url::new(expected_url(&storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=2060795-2596799"))], )); assert_eq!(response, expected_response) @@ -214,7 +216,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bam, - vec![Url::new(expected_url(&storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=977196-2128166"))], )); assert_eq!(response, expected_response) @@ -235,11 +237,11 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bam, vec![ - Url::new(expected_url(&storage)) + Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=256721-647346")), - Url::new(expected_url(&storage)) + Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=824361-842101")), - Url::new(expected_url(&storage)) + Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=977196-996015")), ], )); @@ -257,7 +259,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bam, - vec![Url::new(expected_url(&storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=0-4668")) .with_class(Class::Header)], )); @@ -271,16 +273,6 @@ pub mod tests { .parent() .unwrap() .join("data/bam"); - test(LocalStorage::new(base_path).unwrap()) - } - - pub fn expected_url(storage: &LocalStorage) -> String { - format!( - "file://{}", - storage - .base_path() - .join("htsnexus_test_NA12878.bam") - .to_string_lossy() - ) + test(LocalStorage::new(base_path, "localhost").unwrap()) } } diff --git a/htsget-search/src/htsget/bcf_search.rs b/htsget-search/src/htsget/bcf_search.rs index 660c8d121..e44c7e975 100644 --- a/htsget-search/src/htsget/bcf_search.rs +++ b/htsget-search/src/htsget/bcf_search.rs @@ -141,7 +141,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bcf, - vec![Url::new(expected_url(&storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-3530"))], )); assert_eq!(response, expected_response) @@ -159,7 +159,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bcf, - vec![Url::new(expected_url(&storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-950"))], )); assert_eq!(response, expected_response) @@ -180,7 +180,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bcf, - vec![Url::new(expected_url(&storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-3530"))], )); assert_eq!(response, expected_response) @@ -215,7 +215,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bcf, - vec![Url::new(expected_url(&storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-950")) .with_class(Class::Header)], )); @@ -229,16 +229,10 @@ pub mod tests { .parent() .unwrap() .join("data/bcf"); - test(LocalStorage::new(base_path).unwrap()) + test(LocalStorage::new(base_path, "localhost").unwrap()) } - pub fn expected_url(storage: &LocalStorage, name: &str) -> String { - format!( - "file://{}", - storage - .base_path() - .join(format!("{}.bcf", name)) - .to_string_lossy() - ) + pub fn expected_url(name: &str) -> String { + format!("http://localhost/data/{}.bcf", name) } } diff --git a/htsget-search/src/htsget/cram_search.rs b/htsget-search/src/htsget/cram_search.rs index ac0d4283c..498fd25b2 100644 --- a/htsget-search/src/htsget/cram_search.rs +++ b/htsget-search/src/htsget/cram_search.rs @@ -216,6 +216,8 @@ pub mod tests { use super::*; + const EXPECTED_URL: &str = "http://localhost/data/htsnexus_test_NA12878.cram"; + #[test] fn search_all_reads() { with_local_storage(|storage| { @@ -226,7 +228,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, - vec![Url::new(expected_url(&storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=6087-1627756"))], )); assert_eq!(response, expected_response) @@ -243,7 +245,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, - vec![Url::new(expected_url(&storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=1280106-1627756"))], )); assert_eq!(response, expected_response) @@ -260,7 +262,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, - vec![Url::new(expected_url(&storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=604231-1280106"))], )); assert_eq!(response, expected_response) @@ -280,7 +282,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, - vec![Url::new(expected_url(&storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=6087-465709"))], )); assert_eq!(response, expected_response) @@ -300,7 +302,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, - vec![Url::new(expected_url(&storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=6087-604231"))], )); assert_eq!(response, expected_response) @@ -317,7 +319,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, - vec![Url::new(expected_url(&storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=26-6087")) .with_class(Class::Header)], )); @@ -331,16 +333,6 @@ pub mod tests { .parent() .unwrap() .join("data/cram"); - test(LocalStorage::new(base_path).unwrap()) - } - - pub fn expected_url(storage: &LocalStorage) -> String { - format!( - "file://{}", - storage - .base_path() - .join("htsnexus_test_NA12878.cram") - .to_string_lossy() - ) + test(LocalStorage::new(base_path, "localhost").unwrap()) } } diff --git a/htsget-search/src/htsget/from_storage.rs b/htsget-search/src/htsget/from_storage.rs index 3c2a81c86..db650f21d 100644 --- a/htsget-search/src/htsget/from_storage.rs +++ b/htsget-search/src/htsget/from_storage.rs @@ -57,7 +57,7 @@ impl HtsGetFromStorage { mod tests { use crate::htsget::bam_search::tests::{ - expected_url as bam_expected_url, with_local_storage as bam_with_local_storage, + with_local_storage as bam_with_local_storage, EXPECTED_URL as BAM_EXPECTED_URL, }; use crate::htsget::vcf_search::tests::{ expected_url as vcf_expected_url, with_local_storage as vcf_with_local_storage, @@ -76,7 +76,7 @@ mod tests { let expected_response = Ok(Response::new( Format::Bam, - vec![Url::new(bam_expected_url(htsget.storage())) + vec![Url::new(BAM_EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=4668-2596799"))], )); assert_eq!(response, expected_response) @@ -94,7 +94,7 @@ mod tests { let expected_response = Ok(Response::new( Format::Vcf, - vec![Url::new(vcf_expected_url(htsget.storage(), filename)) + vec![Url::new(vcf_expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-823"))], )); assert_eq!(response, expected_response) diff --git a/htsget-search/src/htsget/vcf_search.rs b/htsget-search/src/htsget/vcf_search.rs index f429e9aea..c2a16b31e 100644 --- a/htsget-search/src/htsget/vcf_search.rs +++ b/htsget-search/src/htsget/vcf_search.rs @@ -148,7 +148,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Vcf, - vec![Url::new(expected_url(&storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-3367"))], )); assert_eq!(response, expected_response) @@ -166,7 +166,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Vcf, - vec![Url::new(expected_url(&storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-823"))], )); assert_eq!(response, expected_response) @@ -187,7 +187,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Vcf, - vec![Url::new(expected_url(&storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-3367"))], )); assert_eq!(response, expected_response) @@ -222,7 +222,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Vcf, - vec![Url::new(expected_url(&storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-823")) .with_class(Class::Header)], )); @@ -236,16 +236,10 @@ pub mod tests { .parent() .unwrap() .join("data/vcf"); - test(LocalStorage::new(base_path).unwrap()) + test(LocalStorage::new(base_path, "localhost").unwrap()) } - pub fn expected_url(storage: &LocalStorage, name: &str) -> String { - format!( - "file://{}", - storage - .base_path() - .join(format!("{}.vcf.gz", name)) - .to_string_lossy() - ) + pub fn expected_url(name: &str) -> String { + format!("http://localhost/data/{}.vcf.gz", name) } } diff --git a/htsget-search/src/storage/local.rs b/htsget-search/src/storage/local.rs index 952fc75b2..115649b0c 100644 --- a/htsget-search/src/storage/local.rs +++ b/htsget-search/src/storage/local.rs @@ -11,10 +11,11 @@ use super::{GetOptions, Result, Storage, StorageError, UrlOptions}; #[derive(Debug)] pub struct LocalStorage { base_path: PathBuf, + address: String, } impl LocalStorage { - pub fn new>(base_path: P) -> Result { + pub fn new>(base_path: P, address: impl Into) -> Result { base_path .as_ref() .to_path_buf() @@ -22,6 +23,7 @@ impl LocalStorage { .map_err(|_| StorageError::NotFound(base_path.as_ref().to_string_lossy().to_string())) .map(|canonicalized_base_path| Self { base_path: canonicalized_base_path, + address: address.into(), }) } @@ -68,9 +70,17 @@ impl Storage for LocalStorage { .map(|end| end.to_string()) .unwrap_or_else(|| "".to_string()); - // TODO file:// is not allowed by the spec. We should consider including an static http server for the base_path - let path = self.get_path_from_key(key)?; - let url = Url::new(format!("file://{}", path.to_string_lossy())); + let captured_key = key.as_ref().to_string(); + let path = self + .get_path_from_key(key)? + .strip_prefix(&self.base_path) + .map_err(|_| StorageError::NotFound(captured_key))? + .to_owned(); + let url = Url::new(format!( + "http://{}/data/{}", + self.address, + path.to_string_lossy() + )); let url = if range_start.is_empty() && range_end.is_empty() { url } else { @@ -192,10 +202,7 @@ mod tests { fn url_of_existing_key() { with_local_storage(|storage| { let result = storage.url("folder/../key1", UrlOptions::default()); - let expected = Url::new(format!( - "file://{}", - storage.base_path().join("key1").to_string_lossy() - )); + let expected = Url::new(format!("http://localhost/data/key1")); assert_eq!(result, Ok(expected)); }); } @@ -210,11 +217,8 @@ mod tests { assert_eq!( result, Ok( - Url::new(format!( - "file://{}", - storage.base_path().join("key1").to_string_lossy() - )) - .with_headers(Headers::default().with_header("Range", "bytes=7-9")) + Url::new(format!("http://localhost/data/key1",)) + .with_headers(Headers::default().with_header("Range", "bytes=7-9")) ) ); }); @@ -230,11 +234,8 @@ mod tests { assert_eq!( result, Ok( - Url::new(format!( - "file://{}", - storage.base_path().join("key1").to_string_lossy() - )) - .with_headers(Headers::default().with_header("Range", "bytes=7-")) + Url::new(format!("http://localhost/data/key1",)) + .with_headers(Headers::default().with_header("Range", "bytes=7-")) ) ); }); @@ -260,6 +261,6 @@ mod tests { .unwrap() .write_all(b"value2") .unwrap(); - test(LocalStorage::new(base_path.path()).unwrap()) + test(LocalStorage::new(base_path.path(), "localhost").unwrap()) } } From cbf58e065903d35ac468af82258771c66034e05a Mon Sep 17 00:00:00 2001 From: CastilloDel Date: Fri, 6 Aug 2021 14:06:50 +0100 Subject: [PATCH 07/50] Add static files to the server --- htsget-http-actix/Cargo.toml | 1 + htsget-http-actix/src/handlers/static_files.rs | 0 htsget-http-actix/src/main.rs | 2 ++ 3 files changed, 3 insertions(+) create mode 100644 htsget-http-actix/src/handlers/static_files.rs diff --git a/htsget-http-actix/Cargo.toml b/htsget-http-actix/Cargo.toml index fd4a33681..c5a9c6157 100644 --- a/htsget-http-actix/Cargo.toml +++ b/htsget-http-actix/Cargo.toml @@ -8,6 +8,7 @@ edition = "2018" [dependencies] actix-web = "3" +actix-files = "0.5" envy = "0.4" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/htsget-http-actix/src/handlers/static_files.rs b/htsget-http-actix/src/handlers/static_files.rs new file mode 100644 index 000000000..e69de29bb diff --git a/htsget-http-actix/src/main.rs b/htsget-http-actix/src/main.rs index e9591041c..6e745d050 100644 --- a/htsget-http-actix/src/main.rs +++ b/htsget-http-actix/src/main.rs @@ -1,3 +1,4 @@ +use actix_files::Files; use actix_web::{web, App, HttpServer}; use htsget_search::{ htsget::{from_storage::HtsGetFromStorage, HtsGet}, @@ -80,6 +81,7 @@ async fn main() -> std::io::Result<()> { .route("/{id:.+}", web::get().to(get::variants::)) .route("/{id:.+}", web::post().to(post::variants::)), ) + .service(Files::new("/data", htsget_path.clone())) }) .bind(address)? .run() From 4f289317a9214e24a8c2a50e3a3896a1bbaa3943 Mon Sep 17 00:00:00 2001 From: CastilloDel Date: Sat, 7 Aug 2021 12:11:30 +0100 Subject: [PATCH 08/50] Show the benchmark download size properly --- .../benches/request-benchmark.rs | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/htsget-http-actix/benches/request-benchmark.rs b/htsget-http-actix/benches/request-benchmark.rs index fff4d4c6d..5128477d8 100644 --- a/htsget-http-actix/benches/request-benchmark.rs +++ b/htsget-http-actix/benches/request-benchmark.rs @@ -1,13 +1,16 @@ use criterion::{criterion_group, criterion_main, Criterion}; use htsget_http_core::JsonResponse; -use reqwest::{blocking::Client, Url}; +use reqwest::{blocking::Client, IntoUrl}; use serde::Serialize; use std::collections::HashMap; use std::{convert::TryInto, time::Duration}; #[derive(Serialize)] struct Empty {} -fn request(url: Url) { +const HTSGET_RS_URL: &str = "http://localhost:8080/reads/data/bam/htsnexus_test_NA12878"; +const HTSGET_REFSERVER_URL: &str = "http://localhost:8081/reads/htsnexus_test_NA12878"; + +fn request(url: impl IntoUrl) -> usize { let client = Client::new(); let response: JsonResponse = client .get(url) @@ -16,7 +19,7 @@ fn request(url: Url) { .unwrap() .json() .unwrap(); - let download_size: usize = response + response .htsget .urls .iter() @@ -37,8 +40,7 @@ fn request(url: Url) { .unwrap() .len() }) - .sum(); - println!("{}", download_size) + .sum() } fn criterion_benchmark(c: &mut Criterion) { @@ -46,20 +48,12 @@ fn criterion_benchmark(c: &mut Criterion) { group .sample_size(500) .measurement_time(Duration::from_secs(10)); - group.bench_function("htsget-rs", |b| { - b.iter(|| { - let mut url = Url::parse("http://localhost/reads/data/bam/htsnexus_test_NA12878").unwrap(); - url.set_port(Some(8080)).unwrap(); - request(url) - }) - }); + group.bench_function("htsget-rs", |b| b.iter(|| request(HTSGET_RS_URL))); + println!("Download size: {} bytes", request(HTSGET_RS_URL)); group.bench_function("htsget-refserver", |b| { - b.iter(|| { - let mut url = Url::parse("http://localhost/reads/htsnexus_test_NA12878").unwrap(); - url.set_port(Some(8081)).unwrap(); - request(url) - }) + b.iter(|| request(HTSGET_REFSERVER_URL)) }); + println!("Download size: {} bytes", request(HTSGET_REFSERVER_URL)); group.finish(); } From 02a6418a76b2d5abff99d7d430e040439b753b05 Mon Sep 17 00:00:00 2001 From: CastilloDel Date: Sun, 8 Aug 2021 12:05:34 +0100 Subject: [PATCH 09/50] Don't force to use '/data' as an extension for htsget-search urls --- htsget-http-core/src/lib.rs | 2 +- htsget-search/src/htsget/bam_search.rs | 2 +- htsget-search/src/htsget/bcf_search.rs | 2 +- htsget-search/src/htsget/cram_search.rs | 2 +- htsget-search/src/htsget/vcf_search.rs | 2 +- htsget-search/src/storage/local.rs | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/htsget-http-core/src/lib.rs b/htsget-http-core/src/lib.rs index 3bbb808ba..0baf4b8bc 100644 --- a/htsget-http-core/src/lib.rs +++ b/htsget-http-core/src/lib.rs @@ -256,6 +256,6 @@ mod tests { } fn get_searcher() -> impl HtsGet { - HtsGetFromStorage::new(LocalStorage::new("../data", "localhost").unwrap()) + HtsGetFromStorage::new(LocalStorage::new("../data", "localhost/data").unwrap()) } } diff --git a/htsget-search/src/htsget/bam_search.rs b/htsget-search/src/htsget/bam_search.rs index ba5a68602..802feac2f 100644 --- a/htsget-search/src/htsget/bam_search.rs +++ b/htsget-search/src/htsget/bam_search.rs @@ -273,6 +273,6 @@ pub mod tests { .parent() .unwrap() .join("data/bam"); - test(LocalStorage::new(base_path, "localhost").unwrap()) + test(LocalStorage::new(base_path, "localhost/data").unwrap()) } } diff --git a/htsget-search/src/htsget/bcf_search.rs b/htsget-search/src/htsget/bcf_search.rs index e44c7e975..547f97d31 100644 --- a/htsget-search/src/htsget/bcf_search.rs +++ b/htsget-search/src/htsget/bcf_search.rs @@ -229,7 +229,7 @@ pub mod tests { .parent() .unwrap() .join("data/bcf"); - test(LocalStorage::new(base_path, "localhost").unwrap()) + test(LocalStorage::new(base_path, "localhost/data").unwrap()) } pub fn expected_url(name: &str) -> String { diff --git a/htsget-search/src/htsget/cram_search.rs b/htsget-search/src/htsget/cram_search.rs index 498fd25b2..5c5386e85 100644 --- a/htsget-search/src/htsget/cram_search.rs +++ b/htsget-search/src/htsget/cram_search.rs @@ -333,6 +333,6 @@ pub mod tests { .parent() .unwrap() .join("data/cram"); - test(LocalStorage::new(base_path, "localhost").unwrap()) + test(LocalStorage::new(base_path, "localhost/data").unwrap()) } } diff --git a/htsget-search/src/htsget/vcf_search.rs b/htsget-search/src/htsget/vcf_search.rs index c2a16b31e..f60517989 100644 --- a/htsget-search/src/htsget/vcf_search.rs +++ b/htsget-search/src/htsget/vcf_search.rs @@ -236,7 +236,7 @@ pub mod tests { .parent() .unwrap() .join("data/vcf"); - test(LocalStorage::new(base_path, "localhost").unwrap()) + test(LocalStorage::new(base_path, "localhost/data").unwrap()) } pub fn expected_url(name: &str) -> String { diff --git a/htsget-search/src/storage/local.rs b/htsget-search/src/storage/local.rs index 115649b0c..c4acb88cd 100644 --- a/htsget-search/src/storage/local.rs +++ b/htsget-search/src/storage/local.rs @@ -77,7 +77,7 @@ impl Storage for LocalStorage { .map_err(|_| StorageError::NotFound(captured_key))? .to_owned(); let url = Url::new(format!( - "http://{}/data/{}", + "http://{}/{}", self.address, path.to_string_lossy() )); @@ -261,6 +261,6 @@ mod tests { .unwrap() .write_all(b"value2") .unwrap(); - test(LocalStorage::new(base_path.path(), "localhost").unwrap()) + test(LocalStorage::new(base_path.path(), "localhost/data").unwrap()) } } From 203abf94f9d5c642d5d6e10d7eb770d5b7fa1f76 Mon Sep 17 00:00:00 2001 From: CastilloDel Date: Sun, 8 Aug 2021 13:07:13 +0100 Subject: [PATCH 10/50] Add another benchmark --- .../benches/request-benchmark.rs | 107 ++++++++++++------ .../src/handlers/static_files.rs | 0 2 files changed, 71 insertions(+), 36 deletions(-) delete mode 100644 htsget-http-actix/src/handlers/static_files.rs diff --git a/htsget-http-actix/benches/request-benchmark.rs b/htsget-http-actix/benches/request-benchmark.rs index 5128477d8..057ac9396 100644 --- a/htsget-http-actix/benches/request-benchmark.rs +++ b/htsget-http-actix/benches/request-benchmark.rs @@ -1,6 +1,6 @@ use criterion::{criterion_group, criterion_main, Criterion}; -use htsget_http_core::JsonResponse; -use reqwest::{blocking::Client, IntoUrl}; +use htsget_http_core::{JsonResponse, PostRequest, Region}; +use reqwest::{blocking::Client, Error as ActixError, IntoUrl}; use serde::Serialize; use std::collections::HashMap; use std::{convert::TryInto, time::Duration}; @@ -10,37 +10,35 @@ struct Empty {} const HTSGET_RS_URL: &str = "http://localhost:8080/reads/data/bam/htsnexus_test_NA12878"; const HTSGET_REFSERVER_URL: &str = "http://localhost:8081/reads/htsnexus_test_NA12878"; -fn request(url: impl IntoUrl) -> usize { +fn request(url: impl IntoUrl, json_content: &impl Serialize) -> Result { let client = Client::new(); - let response: JsonResponse = client - .get(url) - .json(&Empty {}) - .send() - .unwrap() - .json() - .unwrap(); - response - .htsget - .urls - .iter() - .map(|json_url| { - client - .get(&json_url.url) - .headers( - json_url - .headers - .as_ref() - .unwrap_or(&HashMap::new()) - .try_into() - .unwrap(), + let response: JsonResponse = client.get(url).json(json_content).send()?.json()?; + Ok( + response + .htsget + .urls + .iter() + .map(|json_url| { + Ok( + client + .get(&json_url.url) + .headers( + json_url + .headers + .as_ref() + .unwrap_or(&HashMap::new()) + .try_into() + .unwrap(), + ) + .send()? + .text()? + .len(), ) - .send() - .unwrap() - .text() - .unwrap() - .len() - }) - .sum() + }) + .collect::, ActixError>>()? + .into_iter() + .sum(), + ) } fn criterion_benchmark(c: &mut Criterion) { @@ -48,12 +46,49 @@ fn criterion_benchmark(c: &mut Criterion) { group .sample_size(500) .measurement_time(Duration::from_secs(10)); - group.bench_function("htsget-rs", |b| b.iter(|| request(HTSGET_RS_URL))); - println!("Download size: {} bytes", request(HTSGET_RS_URL)); - group.bench_function("htsget-refserver", |b| { - b.iter(|| request(HTSGET_REFSERVER_URL)) + + println!( + "Download size: {} bytes", + request(HTSGET_RS_URL, &Empty {}).expect("Error during the request") + ); + group.bench_function("htsget-rs simple request", |b| { + b.iter(|| request(HTSGET_RS_URL, &Empty {})) + }); + println!( + "Download size: {} bytes", + request(HTSGET_REFSERVER_URL, &Empty {}).expect("Error during the request") + ); + group.bench_function("htsget-refserver simple request", |b| { + b.iter(|| request(HTSGET_REFSERVER_URL, &Empty {})) + }); + + let json_content = PostRequest { + format: None, + class: None, + fields: None, + tags: None, + notags: None, + regions: Some(vec![Region { + reference_name: "20".to_string(), + start: None, + end: None, + }]), + }; + println!( + "Download size: {} bytes", + request(HTSGET_RS_URL, &json_content).expect("Error during the request") + ); + group.bench_function("htsget-rs with region", |b| { + b.iter(|| request(HTSGET_RS_URL, &json_content)) + }); + println!( + "Download size: {} bytes", + request(HTSGET_REFSERVER_URL, &json_content).expect("Error during the request") + ); + group.bench_function("htsget-refserver with region", |b| { + b.iter(|| request(HTSGET_REFSERVER_URL, &json_content)) }); - println!("Download size: {} bytes", request(HTSGET_REFSERVER_URL)); + group.finish(); } diff --git a/htsget-http-actix/src/handlers/static_files.rs b/htsget-http-actix/src/handlers/static_files.rs deleted file mode 100644 index e69de29bb..000000000 From f9f8b3015d73a222598f4115a5bfb88c2fc8de70 Mon Sep 17 00:00:00 2001 From: CastilloDel Date: Mon, 9 Aug 2021 11:15:11 +0100 Subject: [PATCH 11/50] Refactor and adjustments --- .../benches/request-benchmark.rs | 68 +++++++++++-------- htsget-http-actix/src/main.rs | 4 +- 2 files changed, 41 insertions(+), 31 deletions(-) diff --git a/htsget-http-actix/benches/request-benchmark.rs b/htsget-http-actix/benches/request-benchmark.rs index 057ac9396..0cdbecfeb 100644 --- a/htsget-http-actix/benches/request-benchmark.rs +++ b/htsget-http-actix/benches/request-benchmark.rs @@ -1,6 +1,7 @@ -use criterion::{criterion_group, criterion_main, Criterion}; +use criterion::measurement::WallTime; +use criterion::{criterion_group, criterion_main, BenchmarkGroup, Criterion}; use htsget_http_core::{JsonResponse, PostRequest, Region}; -use reqwest::{blocking::Client, Error as ActixError, IntoUrl}; +use reqwest::{blocking::Client, Error as ActixError}; use serde::Serialize; use std::collections::HashMap; use std::{convert::TryInto, time::Duration}; @@ -10,7 +11,7 @@ struct Empty {} const HTSGET_RS_URL: &str = "http://localhost:8080/reads/data/bam/htsnexus_test_NA12878"; const HTSGET_REFSERVER_URL: &str = "http://localhost:8081/reads/htsnexus_test_NA12878"; -fn request(url: impl IntoUrl, json_content: &impl Serialize) -> Result { +fn request(url: &str, json_content: &impl Serialize) -> Result { let client = Client::new(); let response: JsonResponse = client.get(url).json(json_content).send()?.json()?; Ok( @@ -41,26 +42,37 @@ fn request(url: impl IntoUrl, json_content: &impl Serialize) -> Result, + name: &str, + url: &str, + json_content: &impl Serialize, +) { + println!( + "\n\nDownload size: {} bytes", + request(url, json_content).expect("Error during the request") + ); + group.bench_function(name, |b| b.iter(|| request(url, json_content))); +} + fn criterion_benchmark(c: &mut Criterion) { let mut group = c.benchmark_group("Requests"); group - .sample_size(500) - .measurement_time(Duration::from_secs(10)); + .sample_size(150) + .measurement_time(Duration::from_secs(15)); - println!( - "Download size: {} bytes", - request(HTSGET_RS_URL, &Empty {}).expect("Error during the request") + bench_request( + &mut group, + "htsget-rs simple request", + HTSGET_RS_URL, + &Empty {}, ); - group.bench_function("htsget-rs simple request", |b| { - b.iter(|| request(HTSGET_RS_URL, &Empty {})) - }); - println!( - "Download size: {} bytes", - request(HTSGET_REFSERVER_URL, &Empty {}).expect("Error during the request") + bench_request( + &mut group, + "htsget-refserver simple request", + HTSGET_REFSERVER_URL, + &Empty {}, ); - group.bench_function("htsget-refserver simple request", |b| { - b.iter(|| request(HTSGET_REFSERVER_URL, &Empty {})) - }); let json_content = PostRequest { format: None, @@ -74,20 +86,18 @@ fn criterion_benchmark(c: &mut Criterion) { end: None, }]), }; - println!( - "Download size: {} bytes", - request(HTSGET_RS_URL, &json_content).expect("Error during the request") + bench_request( + &mut group, + "htsget-rs with region", + HTSGET_RS_URL, + &json_content, ); - group.bench_function("htsget-rs with region", |b| { - b.iter(|| request(HTSGET_RS_URL, &json_content)) - }); - println!( - "Download size: {} bytes", - request(HTSGET_REFSERVER_URL, &json_content).expect("Error during the request") + bench_request( + &mut group, + "htsget-refserver with region", + HTSGET_REFSERVER_URL, + &json_content, ); - group.bench_function("htsget-refserver with region", |b| { - b.iter(|| request(HTSGET_REFSERVER_URL, &json_content)) - }); group.finish(); } diff --git a/htsget-http-actix/src/main.rs b/htsget-http-actix/src/main.rs index 6e745d050..982ee0b67 100644 --- a/htsget-http-actix/src/main.rs +++ b/htsget-http-actix/src/main.rs @@ -44,13 +44,13 @@ async fn main() -> std::io::Result<()> { } let config = envy::from_env::().expect("The environment variables weren't properly set!"); let address = format!("{}:{}", config.htsget_ip, config.htsget_port); - let moved_address = address.clone(); + let storage_base_address = format!("{}/data", address); let htsget_path = config.htsget_path.clone(); HttpServer::new(move || { App::new() .data(AppState { htsget: HtsGetFromStorage::new( - LocalStorage::new(htsget_path.clone(), moved_address.clone()) + LocalStorage::new(&htsget_path, &storage_base_address) .expect("Couldn't create a Storage with the provided path"), ), config: config.clone(), From 87ff1b33f08b31bd8f2554fc6a2b71e0871a33f1 Mon Sep 17 00:00:00 2001 From: CastilloDel Date: Mon, 9 Aug 2021 12:00:50 +0100 Subject: [PATCH 12/50] Add a more complex benchmark --- .../benches/request-benchmark.rs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/htsget-http-actix/benches/request-benchmark.rs b/htsget-http-actix/benches/request-benchmark.rs index 0cdbecfeb..345edd3b2 100644 --- a/htsget-http-actix/benches/request-benchmark.rs +++ b/htsget-http-actix/benches/request-benchmark.rs @@ -99,6 +99,38 @@ fn criterion_benchmark(c: &mut Criterion) { &json_content, ); + let json_content = PostRequest { + format: None, + class: None, + fields: None, + tags: None, + notags: None, + regions: Some(vec![ + Region { + reference_name: "20".to_string(), + start: None, + end: None, + }, + Region { + reference_name: "11".to_string(), + start: Some(4999977), + end: Some(5008321), + }, + ]), + }; + bench_request( + &mut group, + "htsget-rs with two regions", + HTSGET_RS_URL, + &json_content, + ); + bench_request( + &mut group, + "htsget-refserver with two regions", + HTSGET_REFSERVER_URL, + &json_content, + ); + group.finish(); } From 7c4924dc3f444f274f3f21ab87bac721b85f0273 Mon Sep 17 00:00:00 2001 From: CastilloDel Date: Mon, 9 Aug 2021 14:10:01 +0100 Subject: [PATCH 13/50] Add htsget-search benchmark --- htsget-search/Cargo.toml | 8 ++++++ htsget-search/benches/benchmark.rs | 40 ++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 htsget-search/benches/benchmark.rs diff --git a/htsget-search/Cargo.toml b/htsget-search/Cargo.toml index 5da1faa0a..766fbb062 100644 --- a/htsget-search/Cargo.toml +++ b/htsget-search/Cargo.toml @@ -11,3 +11,11 @@ noodles = { version = "0.5.0", features = ["bam", "bcf", "bgzf", "cram", "csi", [dev-dependencies] tempfile = "3.2.0" +<<<<<<< HEAD +======= +criterion = "0.3" + +[[bench]] +name = "benchmark" +harness = false +>>>>>>> 466ea4a (Add htsget-search benchmark) diff --git a/htsget-search/benches/benchmark.rs b/htsget-search/benches/benchmark.rs new file mode 100644 index 000000000..acad9ba4f --- /dev/null +++ b/htsget-search/benches/benchmark.rs @@ -0,0 +1,40 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use htsget_search::{ + htsget::{from_storage::HtsGetFromStorage, Class, Fields, HtsGet, HtsGetError, Query, Tags}, + storage::local::LocalStorage, +}; +use std::time::Duration; + +fn perform_query(query: Query) -> Result<(), HtsGetError> { + let htsget = HtsGetFromStorage::new(LocalStorage::new("../data", "localhost").unwrap()); + htsget.search(query)?; + Ok(()) +} + +fn criterion_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("Queries"); + group + .sample_size(20) + .measurement_time(Duration::from_secs(5)); + + group.bench_function("Simple bam query", |b| { + b.iter(|| { + perform_query(Query { + id: "bam/htsnexus_test_NA12878".to_string(), + format: None, + class: Class::Body, + reference_name: None, + start: None, + end: None, + fields: Fields::All, + tags: Tags::All, + no_tags: None, + }) + }) + }); + + group.finish(); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); From 5e7bf214f7283cbb219c06b3fc81d6f0d5ebbcb5 Mon Sep 17 00:00:00 2001 From: CastilloDel Date: Tue, 10 Aug 2021 13:13:24 +0100 Subject: [PATCH 14/50] Add more htsget-search tests --- htsget-search/Cargo.toml | 3 -- htsget-search/benches/benchmark.rs | 64 +++++++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/htsget-search/Cargo.toml b/htsget-search/Cargo.toml index 766fbb062..6fa243d57 100644 --- a/htsget-search/Cargo.toml +++ b/htsget-search/Cargo.toml @@ -11,11 +11,8 @@ noodles = { version = "0.5.0", features = ["bam", "bcf", "bgzf", "cram", "csi", [dev-dependencies] tempfile = "3.2.0" -<<<<<<< HEAD -======= criterion = "0.3" [[bench]] name = "benchmark" harness = false ->>>>>>> 466ea4a (Add htsget-search benchmark) diff --git a/htsget-search/benches/benchmark.rs b/htsget-search/benches/benchmark.rs index acad9ba4f..d47f3a4cb 100644 --- a/htsget-search/benches/benchmark.rs +++ b/htsget-search/benches/benchmark.rs @@ -1,6 +1,8 @@ use criterion::{criterion_group, criterion_main, Criterion}; use htsget_search::{ - htsget::{from_storage::HtsGetFromStorage, Class, Fields, HtsGet, HtsGetError, Query, Tags}, + htsget::{ + from_storage::HtsGetFromStorage, Class, Fields, Format, HtsGet, HtsGetError, Query, Tags, + }, storage::local::LocalStorage, }; use std::time::Duration; @@ -32,6 +34,66 @@ fn criterion_benchmark(c: &mut Criterion) { }) }) }); + group.bench_function("Bam query", |b| { + b.iter(|| { + perform_query(Query { + id: "bam/htsnexus_test_NA12878".to_string(), + format: None, + class: Class::Body, + reference_name: Some("11".to_string()), + start: Some(4999977), + end: Some(5008321), + fields: Fields::All, + tags: Tags::All, + no_tags: None, + }) + }) + }); + group.bench_function("VCF query", |b| { + b.iter(|| { + perform_query(Query { + id: "vcf/sample1-bcbio-cancer".to_string(), + format: None, + class: Class::Body, + reference_name: Some("chrM".to_string()), + start: Some(151), + end: Some(153), + fields: Fields::All, + tags: Tags::All, + no_tags: None, + }) + }) + }); + group.bench_function("BCF query", |b| { + b.iter(|| { + perform_query(Query { + id: "bcf/sample1-bcbio-cancer".to_string(), + format: Some(Format::Bcf), + class: Class::Body, + reference_name: Some("chrM".to_string()), + start: Some(151), + end: Some(153), + fields: Fields::All, + tags: Tags::All, + no_tags: None, + }) + }) + }); + group.bench_function("CRAM query", |b| { + b.iter(|| { + perform_query(Query { + id: "cram/htsnexus_test_NA12878".to_string(), + format: Some(Format::Cram), + class: Class::Body, + reference_name: Some("11".to_string()), + start: Some(4999977), + end: Some(5008321), + fields: Fields::All, + tags: Tags::All, + no_tags: None, + }) + }) + }); group.finish(); } From 46baaac642578d05c3a5684dca352fbc3f5fff5f Mon Sep 17 00:00:00 2001 From: CastilloDel Date: Thu, 12 Aug 2021 14:10:21 +0100 Subject: [PATCH 15/50] Add VCF test --- .../benches/request-benchmark.rs | 29 ++++++++++++++++++- .../htsget-refserver-config.json | 10 +++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/htsget-http-actix/benches/request-benchmark.rs b/htsget-http-actix/benches/request-benchmark.rs index 345edd3b2..61110e81c 100644 --- a/htsget-http-actix/benches/request-benchmark.rs +++ b/htsget-http-actix/benches/request-benchmark.rs @@ -10,6 +10,8 @@ struct Empty {} const HTSGET_RS_URL: &str = "http://localhost:8080/reads/data/bam/htsnexus_test_NA12878"; const HTSGET_REFSERVER_URL: &str = "http://localhost:8081/reads/htsnexus_test_NA12878"; +const HTSGET_RS_VCF_URL: &str = "http://localhost:8080/variants/data/vcf/sample1-bcbio-cancer"; +const HTSGET_REFSERVER_VCF_URL: &str = "http://localhost:8081/variants/sample1-bcbio-cancer"; fn request(url: &str, json_content: &impl Serialize) -> Result { let client = Client::new(); @@ -32,7 +34,7 @@ fn request(url: &str, json_content: &impl Serialize) -> Result.*)$", + "path": "/data/vcf/{id}.vcf.gz" + } + ] + } } } } \ No newline at end of file From 4d39271b5681eb2e2ecdde4fe1c50a172be1d675 Mon Sep 17 00:00:00 2001 From: CastilloDel Date: Thu, 12 Aug 2021 16:23:39 +0100 Subject: [PATCH 16/50] Add a test with a big file --- .gitignore | 1 + data/vcf/internationalgenomesample.vcf.gz.tbi | Bin 0 -> 85196 bytes .../benches/docker-htsget-refserver.sh | 2 + .../benches/dowload-heavy-test-files.sh | 1 + .../benches/heavy-request-benchmark.rs | 94 ++++++++++++++++++ .../htsget-refserver-config.json | 0 .../benches/request-benchmark.rs | 47 +++++++-- htsget-http-actix/docker-htsget-refserver.sh | 2 - 8 files changed, 137 insertions(+), 10 deletions(-) create mode 100644 data/vcf/internationalgenomesample.vcf.gz.tbi create mode 100644 htsget-http-actix/benches/docker-htsget-refserver.sh create mode 100644 htsget-http-actix/benches/dowload-heavy-test-files.sh create mode 100644 htsget-http-actix/benches/heavy-request-benchmark.rs rename htsget-http-actix/{ => benches}/htsget-refserver-config.json (100%) delete mode 100644 htsget-http-actix/docker-htsget-refserver.sh diff --git a/.gitignore b/.gitignore index 9814bc72e..d4cdf638d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target Cargo.lock *.code-workspace +internationalgenomesample.vcf.gz \ No newline at end of file diff --git a/data/vcf/internationalgenomesample.vcf.gz.tbi b/data/vcf/internationalgenomesample.vcf.gz.tbi new file mode 100644 index 0000000000000000000000000000000000000000..8b2b64fb9e6cb89463cbe70a2f1decfb4151ed0c GIT binary patch literal 85196 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⁣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 ../data/vcf/internationalgenomesample.vcf.gz \ No newline at end of file diff --git a/htsget-http-actix/benches/heavy-request-benchmark.rs b/htsget-http-actix/benches/heavy-request-benchmark.rs new file mode 100644 index 000000000..43f20aa20 --- /dev/null +++ b/htsget-http-actix/benches/heavy-request-benchmark.rs @@ -0,0 +1,94 @@ +use criterion::measurement::WallTime; +use criterion::{criterion_group, criterion_main, BenchmarkGroup, Criterion}; +use htsget_http_core::{JsonResponse, PostRequest, Region}; +use reqwest::{blocking::Client, Error as ActixError}; +use serde::Serialize; +use std::collections::HashMap; +use std::{convert::TryInto, time::Duration}; + +#[derive(Serialize)] +struct Empty {} + +const HTSGET_RS_VCF_URL: &str = "http://localhost:8080/variants/data/vcf/internationalgenomesample"; +const HTSGET_REFSERVER_VCF_URL: &str = "http://localhost:8081/variants/internationalgenomesample"; + +fn request(url: &str, json_content: &impl Serialize) -> Result { + let client = Client::new(); + let response: JsonResponse = client.get(url).json(json_content).send()?.json()?; + Ok( + response + .htsget + .urls + .iter() + .map(|json_url| { + Ok( + client + .get(&json_url.url) + .headers( + json_url + .headers + .as_ref() + .unwrap_or(&HashMap::new()) + .try_into() + .unwrap(), + ) + .send()? + .bytes()? + .len(), + ) + }) + .collect::, ActixError>>()? + .into_iter() + .sum(), + ) +} + +fn bench_request( + group: &mut BenchmarkGroup, + name: &str, + url: &str, + json_content: &impl Serialize, +) { + println!( + "\n\nDownload size: {} bytes", + request(url, json_content).expect("Error during the request") + ); + group.bench_function(name, |b| b.iter(|| request(url, json_content))); +} + +fn criterion_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("Requests"); + group + .sample_size(150) + .measurement_time(Duration::from_secs(15)); + + let json_content = PostRequest { + format: None, + class: None, + fields: None, + tags: None, + notags: None, + regions: Some(vec![Region { + reference_name: "14".to_string(), + start: None, + end: None, + }]), + }; + bench_request( + &mut group, + "htsget-rs big VCF file", + HTSGET_RS_VCF_URL, + &json_content, + ); + bench_request( + &mut group, + "htsget-refserver big VCF file", + HTSGET_REFSERVER_VCF_URL, + &json_content, + ); + + group.finish(); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/htsget-http-actix/htsget-refserver-config.json b/htsget-http-actix/benches/htsget-refserver-config.json similarity index 100% rename from htsget-http-actix/htsget-refserver-config.json rename to htsget-http-actix/benches/htsget-refserver-config.json diff --git a/htsget-http-actix/benches/request-benchmark.rs b/htsget-http-actix/benches/request-benchmark.rs index 61110e81c..64738fb59 100644 --- a/htsget-http-actix/benches/request-benchmark.rs +++ b/htsget-http-actix/benches/request-benchmark.rs @@ -12,6 +12,10 @@ const HTSGET_RS_URL: &str = "http://localhost:8080/reads/data/bam/htsnexus_test_ const HTSGET_REFSERVER_URL: &str = "http://localhost:8081/reads/htsnexus_test_NA12878"; const HTSGET_RS_VCF_URL: &str = "http://localhost:8080/variants/data/vcf/sample1-bcbio-cancer"; const HTSGET_REFSERVER_VCF_URL: &str = "http://localhost:8081/variants/sample1-bcbio-cancer"; +const HTSGET_RS_BIG_VCF_URL: &str = + "http://localhost:8080/variants/data/vcf/internationalgenomesample"; +const HTSGET_REFSERVER_BIG_VCF_URL: &str = + "http://localhost:8081/variants/internationalgenomesample"; fn request(url: &str, json_content: &impl Serialize) -> Result { let client = Client::new(); @@ -65,13 +69,13 @@ fn criterion_benchmark(c: &mut Criterion) { bench_request( &mut group, - "htsget-rs simple request", + "[LIGHT] htsget-rs simple request", HTSGET_RS_URL, &Empty {}, ); bench_request( &mut group, - "htsget-refserver simple request", + "[LIGHT] htsget-refserver simple request", HTSGET_REFSERVER_URL, &Empty {}, ); @@ -90,13 +94,13 @@ fn criterion_benchmark(c: &mut Criterion) { }; bench_request( &mut group, - "htsget-rs with region", + "[LIGHT] htsget-rs with region", HTSGET_RS_URL, &json_content, ); bench_request( &mut group, - "htsget-refserver with region", + "[LIGHT] htsget-refserver with region", HTSGET_REFSERVER_URL, &json_content, ); @@ -122,13 +126,13 @@ fn criterion_benchmark(c: &mut Criterion) { }; bench_request( &mut group, - "htsget-rs with two regions", + "[LIGHT] htsget-rs with two regions", HTSGET_RS_URL, &json_content, ); bench_request( &mut group, - "htsget-refserver with two regions", + "[LIGHT] htsget-refserver with two regions", HTSGET_REFSERVER_URL, &json_content, ); @@ -147,17 +151,44 @@ fn criterion_benchmark(c: &mut Criterion) { }; bench_request( &mut group, - "htsget-rs with VCF", + "[LIGHT] htsget-rs with VCF", HTSGET_RS_VCF_URL, &json_content, ); bench_request( &mut group, - "htsget-refserver with VCF", + "[LIGHT] htsget-refserver with VCF", HTSGET_REFSERVER_VCF_URL, &json_content, ); + // The following ones are HEAVY requests + + let json_content = PostRequest { + format: None, + class: None, + fields: None, + tags: None, + notags: None, + regions: Some(vec![Region { + reference_name: "14".to_string(), + start: None, + end: None, + }]), + }; + bench_request( + &mut group, + "[HEAVY] htsget-rs big VCF file", + HTSGET_RS_BIG_VCF_URL, + &json_content, + ); + bench_request( + &mut group, + "[HEAVY] htsget-refserver big VCF file", + HTSGET_REFSERVER_BIG_VCF_URL, + &json_content, + ); + group.finish(); } diff --git a/htsget-http-actix/docker-htsget-refserver.sh b/htsget-http-actix/docker-htsget-refserver.sh deleted file mode 100644 index 20d012995..000000000 --- a/htsget-http-actix/docker-htsget-refserver.sh +++ /dev/null @@ -1,2 +0,0 @@ -docker image pull ga4gh/htsget-refserver:1.4.0 -docker container run -d -p 8081:3000 -v $(pwd)/../data:/data -v $(pwd):/config ga4gh/htsget-refserver:1.4.0 ./htsget-refserver -config /config/htsget-refserver-config.json \ No newline at end of file From 5b1f4293d7dd1e02f8a47c0e9fccc2b9be2ab0a9 Mon Sep 17 00:00:00 2001 From: CastilloDel Date: Thu, 12 Aug 2021 16:33:23 +0100 Subject: [PATCH 17/50] Add GitHub Action --- .github/workflows/pull_request.yml | 20 ++++ htsget-http-actix/Cargo.toml | 4 + .../benches/heavy-request-benchmark.rs | 94 ------------------- htsget-search/Cargo.toml | 3 + htsget-search/benches/benchmark.rs | 10 +- 5 files changed, 32 insertions(+), 99 deletions(-) create mode 100644 .github/workflows/pull_request.yml delete mode 100644 htsget-http-actix/benches/heavy-request-benchmark.rs diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 000000000..37a400191 --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,20 @@ +on: [pull_request] +name: benchmark pull requests +jobs: + runBenchmark: + name: run benchmark + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: Enter htsget-search + run: cd htsget-search + - uses: jasonwilliams/criterion-compare-action@move_to_actions + with: + benchName: "LIGHT" # Optional. Compare only this benchmark target + token: ${{ secrets.GITHUB_TOKEN }} + - name: Enter htsget-http-actix + run: cd ../htsget-http-actix + - uses: jasonwilliams/criterion-compare-action@move_to_actions + with: + benchName: "LIGHT" # Optional. Compare only this benchmark target + token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/htsget-http-actix/Cargo.toml b/htsget-http-actix/Cargo.toml index c5a9c6157..c4718a745 100644 --- a/htsget-http-actix/Cargo.toml +++ b/htsget-http-actix/Cargo.toml @@ -5,6 +5,10 @@ version = "0.1.0" authors = ["Daniel del Castillo de la Rosa "] edition = "2018" +[[bin]] +name = "htsget-http-actix" +bench = false + [dependencies] actix-web = "3" diff --git a/htsget-http-actix/benches/heavy-request-benchmark.rs b/htsget-http-actix/benches/heavy-request-benchmark.rs deleted file mode 100644 index 43f20aa20..000000000 --- a/htsget-http-actix/benches/heavy-request-benchmark.rs +++ /dev/null @@ -1,94 +0,0 @@ -use criterion::measurement::WallTime; -use criterion::{criterion_group, criterion_main, BenchmarkGroup, Criterion}; -use htsget_http_core::{JsonResponse, PostRequest, Region}; -use reqwest::{blocking::Client, Error as ActixError}; -use serde::Serialize; -use std::collections::HashMap; -use std::{convert::TryInto, time::Duration}; - -#[derive(Serialize)] -struct Empty {} - -const HTSGET_RS_VCF_URL: &str = "http://localhost:8080/variants/data/vcf/internationalgenomesample"; -const HTSGET_REFSERVER_VCF_URL: &str = "http://localhost:8081/variants/internationalgenomesample"; - -fn request(url: &str, json_content: &impl Serialize) -> Result { - let client = Client::new(); - let response: JsonResponse = client.get(url).json(json_content).send()?.json()?; - Ok( - response - .htsget - .urls - .iter() - .map(|json_url| { - Ok( - client - .get(&json_url.url) - .headers( - json_url - .headers - .as_ref() - .unwrap_or(&HashMap::new()) - .try_into() - .unwrap(), - ) - .send()? - .bytes()? - .len(), - ) - }) - .collect::, ActixError>>()? - .into_iter() - .sum(), - ) -} - -fn bench_request( - group: &mut BenchmarkGroup, - name: &str, - url: &str, - json_content: &impl Serialize, -) { - println!( - "\n\nDownload size: {} bytes", - request(url, json_content).expect("Error during the request") - ); - group.bench_function(name, |b| b.iter(|| request(url, json_content))); -} - -fn criterion_benchmark(c: &mut Criterion) { - let mut group = c.benchmark_group("Requests"); - group - .sample_size(150) - .measurement_time(Duration::from_secs(15)); - - let json_content = PostRequest { - format: None, - class: None, - fields: None, - tags: None, - notags: None, - regions: Some(vec![Region { - reference_name: "14".to_string(), - start: None, - end: None, - }]), - }; - bench_request( - &mut group, - "htsget-rs big VCF file", - HTSGET_RS_VCF_URL, - &json_content, - ); - bench_request( - &mut group, - "htsget-refserver big VCF file", - HTSGET_REFSERVER_VCF_URL, - &json_content, - ); - - group.finish(); -} - -criterion_group!(benches, criterion_benchmark); -criterion_main!(benches); diff --git a/htsget-search/Cargo.toml b/htsget-search/Cargo.toml index 6fa243d57..b0a3ddc2d 100644 --- a/htsget-search/Cargo.toml +++ b/htsget-search/Cargo.toml @@ -4,6 +4,9 @@ version = "0.1.0" authors = ["Christian Perez Llamas ", "Roman Valls Guimera "] edition = "2018" +[lib] +bench = false + [dependencies] thiserror = "~1.0" diff --git a/htsget-search/benches/benchmark.rs b/htsget-search/benches/benchmark.rs index d47f3a4cb..b0ad1f53a 100644 --- a/htsget-search/benches/benchmark.rs +++ b/htsget-search/benches/benchmark.rs @@ -19,7 +19,7 @@ fn criterion_benchmark(c: &mut Criterion) { .sample_size(20) .measurement_time(Duration::from_secs(5)); - group.bench_function("Simple bam query", |b| { + group.bench_function("[LIGHT] Simple bam query", |b| { b.iter(|| { perform_query(Query { id: "bam/htsnexus_test_NA12878".to_string(), @@ -34,7 +34,7 @@ fn criterion_benchmark(c: &mut Criterion) { }) }) }); - group.bench_function("Bam query", |b| { + group.bench_function("[LIGHT] Bam query", |b| { b.iter(|| { perform_query(Query { id: "bam/htsnexus_test_NA12878".to_string(), @@ -49,7 +49,7 @@ fn criterion_benchmark(c: &mut Criterion) { }) }) }); - group.bench_function("VCF query", |b| { + group.bench_function("[LIGHT] VCF query", |b| { b.iter(|| { perform_query(Query { id: "vcf/sample1-bcbio-cancer".to_string(), @@ -64,7 +64,7 @@ fn criterion_benchmark(c: &mut Criterion) { }) }) }); - group.bench_function("BCF query", |b| { + group.bench_function("[LIGHT] BCF query", |b| { b.iter(|| { perform_query(Query { id: "bcf/sample1-bcbio-cancer".to_string(), @@ -79,7 +79,7 @@ fn criterion_benchmark(c: &mut Criterion) { }) }) }); - group.bench_function("CRAM query", |b| { + group.bench_function("[LIGHT] CRAM query", |b| { b.iter(|| { perform_query(Query { id: "cram/htsnexus_test_NA12878".to_string(), From d6d85b5a9ae5fb3f1fed5f486e0f22dbfcdbcf0e Mon Sep 17 00:00:00 2001 From: CastilloDel Date: Thu, 12 Aug 2021 19:44:37 +0100 Subject: [PATCH 18/50] Use constants for the benchmark configuration --- htsget-http-actix/benches/request-benchmark.rs | 8 ++++++-- htsget-search/benches/benchmark.rs | 7 +++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/htsget-http-actix/benches/request-benchmark.rs b/htsget-http-actix/benches/request-benchmark.rs index 64738fb59..d2528933b 100644 --- a/htsget-http-actix/benches/request-benchmark.rs +++ b/htsget-http-actix/benches/request-benchmark.rs @@ -5,9 +5,13 @@ use reqwest::{blocking::Client, Error as ActixError}; use serde::Serialize; use std::collections::HashMap; use std::{convert::TryInto, time::Duration}; + #[derive(Serialize)] struct Empty {} +const BENCHMARK_DURATION_SECONDS: u64 = 15; +const NUMBER_OF_EXECUTIONS: usize = 150; + const HTSGET_RS_URL: &str = "http://localhost:8080/reads/data/bam/htsnexus_test_NA12878"; const HTSGET_REFSERVER_URL: &str = "http://localhost:8081/reads/htsnexus_test_NA12878"; const HTSGET_RS_VCF_URL: &str = "http://localhost:8080/variants/data/vcf/sample1-bcbio-cancer"; @@ -64,8 +68,8 @@ fn bench_request( fn criterion_benchmark(c: &mut Criterion) { let mut group = c.benchmark_group("Requests"); group - .sample_size(150) - .measurement_time(Duration::from_secs(15)); + .sample_size(NUMBER_OF_EXECUTIONS) + .measurement_time(Duration::from_secs(BENCHMARK_DURATION_SECONDS)); bench_request( &mut group, diff --git a/htsget-search/benches/benchmark.rs b/htsget-search/benches/benchmark.rs index b0ad1f53a..09367c4ad 100644 --- a/htsget-search/benches/benchmark.rs +++ b/htsget-search/benches/benchmark.rs @@ -7,6 +7,9 @@ use htsget_search::{ }; use std::time::Duration; +const BENCHMARK_DURATION_SECONDS: u64 = 5; +const NUMBER_OF_EXECUTIONS: usize = 150; + fn perform_query(query: Query) -> Result<(), HtsGetError> { let htsget = HtsGetFromStorage::new(LocalStorage::new("../data", "localhost").unwrap()); htsget.search(query)?; @@ -16,8 +19,8 @@ fn perform_query(query: Query) -> Result<(), HtsGetError> { fn criterion_benchmark(c: &mut Criterion) { let mut group = c.benchmark_group("Queries"); group - .sample_size(20) - .measurement_time(Duration::from_secs(5)); + .sample_size(NUMBER_OF_EXECUTIONS) + .measurement_time(Duration::from_secs(BENCHMARK_DURATION_SECONDS)); group.bench_function("[LIGHT] Simple bam query", |b| { b.iter(|| { From 9874e0ad7ad7ab9fa4d6d6c93da0c2f272c93b26 Mon Sep 17 00:00:00 2001 From: CastilloDel Date: Fri, 13 Aug 2021 10:50:30 +0100 Subject: [PATCH 19/50] Fix CI --- .github/workflows/pull_request.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 37a400191..1edc38b5a 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -14,6 +14,8 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} - name: Enter htsget-http-actix run: cd ../htsget-http-actix + - name: Start the servers + run: cargo run --release & ; bash benches/docker-htsget-refserver.sh - uses: jasonwilliams/criterion-compare-action@move_to_actions with: benchName: "LIGHT" # Optional. Compare only this benchmark target From 4674ddc0fa7756a5b1b906615e902ef3c1a4bcb8 Mon Sep 17 00:00:00 2001 From: CastilloDel Date: Fri, 13 Aug 2021 10:50:30 +0100 Subject: [PATCH 20/50] Fix CI --- .github/workflows/pull_request.yml | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 37a400191..30ba06c52 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -1,19 +1,31 @@ on: [pull_request] name: benchmark pull requests jobs: - runBenchmark: + run-htsget-search-benchmark: name: run benchmark runs-on: ubuntu-latest + defaults: + run: + working-directory: htsget-search steps: - uses: actions/checkout@master - - name: Enter htsget-search - run: cd htsget-search - uses: jasonwilliams/criterion-compare-action@move_to_actions with: benchName: "LIGHT" # Optional. Compare only this benchmark target token: ${{ secrets.GITHUB_TOKEN }} - - name: Enter htsget-http-actix - run: cd ../htsget-http-actix + run-htsget-http-actix-benchmarks: + name: run benchmark + runs-on: ubuntu-latest + defaults: + run: + working-directory: htsget-http-actix + steps: + - uses: actions/checkout@master + - run: pwd + - name: Start the hstget-rs server + run: cargo run --release & + - name: Start the hstget-refserver server + run: bash benches/docker-htsget-refserver.sh - uses: jasonwilliams/criterion-compare-action@move_to_actions with: benchName: "LIGHT" # Optional. Compare only this benchmark target From 13f609d099b9bc4842899a137745982e0f37595b Mon Sep 17 00:00:00 2001 From: Roman Valls Guimera Date: Mon, 16 Aug 2021 15:53:06 +1000 Subject: [PATCH 21/50] Give exec bit to shell scripts, add html-reports to criterion to supress warnings for now, point the github action to the original upstream repo, hopefully defaulting to master --- .github/workflows/pull_request.yml | 6 +++--- htsget-http-actix/Cargo.toml | 5 +++-- htsget-http-actix/benches/docker-htsget-refserver.sh | 0 htsget-http-actix/benches/dowload-heavy-test-files.sh | 0 4 files changed, 6 insertions(+), 5 deletions(-) mode change 100644 => 100755 htsget-http-actix/benches/docker-htsget-refserver.sh mode change 100644 => 100755 htsget-http-actix/benches/dowload-heavy-test-files.sh diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 1edc38b5a..a67a7d231 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -8,15 +8,15 @@ jobs: - uses: actions/checkout@master - name: Enter htsget-search run: cd htsget-search - - uses: jasonwilliams/criterion-compare-action@move_to_actions + - uses: boa-dev/criterion-compare-action@move_to_actions with: benchName: "LIGHT" # Optional. Compare only this benchmark target token: ${{ secrets.GITHUB_TOKEN }} - name: Enter htsget-http-actix run: cd ../htsget-http-actix - name: Start the servers - run: cargo run --release & ; bash benches/docker-htsget-refserver.sh - - uses: jasonwilliams/criterion-compare-action@move_to_actions + run: cargo run --release & ; ./benches/docker-htsget-refserver.sh + - uses: boa-dev/criterion-compare-action with: benchName: "LIGHT" # Optional. Compare only this benchmark target token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/htsget-http-actix/Cargo.toml b/htsget-http-actix/Cargo.toml index c4718a745..fe5a8e1c3 100644 --- a/htsget-http-actix/Cargo.toml +++ b/htsget-http-actix/Cargo.toml @@ -21,9 +21,10 @@ htsget-http-core = { path = "../htsget-http-core" } htsget-search = { path = "../htsget-search" } [dev-dependencies] -criterion = "0.3" +criterion = { version = "0.3", features = [ "html_reports" ]} +#criterion = "0.3" reqwest = { version = "0.11", features = ["blocking", "json"] } [[bench]] name = "request-benchmark" -harness = false \ No newline at end of file +harness = false diff --git a/htsget-http-actix/benches/docker-htsget-refserver.sh b/htsget-http-actix/benches/docker-htsget-refserver.sh old mode 100644 new mode 100755 diff --git a/htsget-http-actix/benches/dowload-heavy-test-files.sh b/htsget-http-actix/benches/dowload-heavy-test-files.sh old mode 100644 new mode 100755 From 8fb4a85de763123b88e536dd478f1b20685e8dcb Mon Sep 17 00:00:00 2001 From: Roman Valls Guimera Date: Mon, 16 Aug 2021 16:03:37 +1000 Subject: [PATCH 22/50] Try latest version of criterion-compare --- .github/workflows/pull_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index a67a7d231..2a105ec8c 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -16,7 +16,7 @@ jobs: run: cd ../htsget-http-actix - name: Start the servers run: cargo run --release & ; ./benches/docker-htsget-refserver.sh - - uses: boa-dev/criterion-compare-action + - uses: jasonwilliams/criterion-compare-action@v2.0.0 with: benchName: "LIGHT" # Optional. Compare only this benchmark target token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From 2875e2aaf3e87ef14095ae63c14ec8e13760f446 Mon Sep 17 00:00:00 2001 From: CastilloDel Date: Mon, 16 Aug 2021 08:00:55 +0100 Subject: [PATCH 23/50] Add instruction on how to run the benchmarks in the README --- htsget-http-actix/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/htsget-http-actix/README.md b/htsget-http-actix/README.md index 943c2b06a..04a17b681 100644 --- a/htsget-http-actix/README.md +++ b/htsget-http-actix/README.md @@ -74,3 +74,17 @@ $ curl --header "Content-Type: application/json" -d '{"format": "VCF", "regions" ```bash $ curl 127.0.0.1:8080/variants/service-info ``` + +## Running the benchmarks +There are benchmarks for the htsget-search crate and for the htsget-http-actix crate. The first ones work like normal benchmarks, but the latter ones try to compare the performance of this implementation and the [reference implementation](https://github.com/ga4gh/htsget-refserver). For that, it is needed to start both servers before running `cargo bench`: + +### Steps to perform +From inside the htsget-http-actix directory, start the htsget-rs server: +``` +HTSGET_PATH=../ cargo run --release & +``` +Then start the htsget-refserver. There is a simple script prepared for that matter. Docker should be installed and `docker.service` should be running: +``` +sudo bash benches/docker-htsget-refserver.s +``` +Now you should be able to run the benchmarks with `cargo bench`! \ No newline at end of file From 7d0da7f260b5253ae0af41d939e0cdba1be6e153 Mon Sep 17 00:00:00 2001 From: CastilloDel Date: Sat, 21 Aug 2021 20:35:14 +0100 Subject: [PATCH 24/50] Fix typo --- htsget-http-actix/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htsget-http-actix/README.md b/htsget-http-actix/README.md index 04a17b681..ddbf35437 100644 --- a/htsget-http-actix/README.md +++ b/htsget-http-actix/README.md @@ -85,6 +85,6 @@ HTSGET_PATH=../ cargo run --release & ``` Then start the htsget-refserver. There is a simple script prepared for that matter. Docker should be installed and `docker.service` should be running: ``` -sudo bash benches/docker-htsget-refserver.s +sudo bash benches/docker-htsget-refserver.sh ``` Now you should be able to run the benchmarks with `cargo bench`! \ No newline at end of file From 378e638fe58987524910d64ef4096b0ff8dd30d8 Mon Sep 17 00:00:00 2001 From: Roman Valls Guimera Date: Fri, 17 Sep 2021 11:34:21 +1000 Subject: [PATCH 25/50] Fix dead code warnings for async/blocking versions --- htsget-http-actix/src/lib.rs | 48 +++++++++++++++++++++++++++++++ htsget-http-actix/src/main.rs | 54 ++++++----------------------------- 2 files changed, 56 insertions(+), 46 deletions(-) create mode 100644 htsget-http-actix/src/lib.rs diff --git a/htsget-http-actix/src/lib.rs b/htsget-http-actix/src/lib.rs new file mode 100644 index 000000000..75b2a50e4 --- /dev/null +++ b/htsget-http-actix/src/lib.rs @@ -0,0 +1,48 @@ +use std::sync::Arc; + +use config::Config; +use htsget_search::htsget::blocking::from_storage::HtsGetFromStorage; +use htsget_search::htsget::blocking::HtsGet; +use htsget_search::{ + htsget::{from_storage::HtsGetFromStorage as AsyncHtsGetFromStorage, HtsGet as AsyncHtsGet}, + storage::blocking::local::LocalStorage, +}; + +pub mod config; +pub mod handlers; + +pub const USAGE: &str = r#" +This executable doesn't use command line arguments, but there are some environment variables that can be set to configure the HtsGet server: +* HTSGET_IP: The ip to use. Default: 127.0.0.1 +* HTSGET_PORT: The port to use. Default: 8080 +* HTSGET_PATH: The path to the directory where the server should be started. Default: Actual directory +* HTSGET_REGEX: The regular expression that should match an ID. Default: ".*" +* HTSGET_REPLACEMENT: The replacement expression. Default: "$0" +For more information about the regex options look in the documentation of the regex crate(https://docs.rs/regex/). +The next variables are used to configure the info for the service-info endpoints +* HTSGET_ID: The id of the service. Default: "" +* HTSGET_NAME: The name of the service. Default: "HtsGet service" +* HTSGET_VERSION: The version of the service. Default: "" +* HTSGET_ORGANIZATION_NAME: The name of the organization. Default: "Snake oil" +* HTSGET_ORGANIZATION_URL: The url of the organization. Default: "https://en.wikipedia.org/wiki/Snake_oil" +* HTSGET_CONTACT_URL: A url to provide contact to the users. Default: "", +* HTSGET_DOCUMENTATION_URL: A link to the documentation. Default: "https://github.com/umccr/htsget-rs/tree/main/htsget-http-actix", +* HTSGET_CREATED_AT: Date of the creation of the service. Default: "", +* HTSGET_UPDATED_AT: Date of the last update of the service. Default: "", +* HTSGET_ENVIRONMENT: The environment in which the service is running. Default: "Testing", +"#; + +#[cfg(feature = "async")] +pub type AsyncHtsGetStorage = AsyncHtsGetFromStorage; +pub type HtsGetStorage = HtsGetFromStorage; + +#[cfg(feature = "async")] +pub struct AsyncAppState { + pub htsget: Arc, + pub config: Config, +} + +pub struct AppState { + pub htsget: H, + pub config: Config, +} diff --git a/htsget-http-actix/src/main.rs b/htsget-http-actix/src/main.rs index c8ccdfebb..7321883b3 100644 --- a/htsget-http-actix/src/main.rs +++ b/htsget-http-actix/src/main.rs @@ -1,59 +1,21 @@ use std::env::args; use std::sync::Arc; -use crate::handlers::blocking::{get, post, reads_service_info, variants_service_info}; +//use htsget_http_actix::handlers::blocking::{get, post, reads_service_info, variants_service_info}; use actix_web::{web, App, HttpServer}; -use config::Config; -use handlers::{ +use htsget_http_actix::handlers::{ get as async_get, post as async_post, reads_service_info as async_reads_service_info, variants_service_info as async_variants_service_info, }; -use htsget_id_resolver::RegexResolver; -use htsget_search::htsget::blocking::from_storage::HtsGetFromStorage; -use htsget_search::htsget::blocking::HtsGet; -use htsget_search::{ - htsget::{from_storage::HtsGetFromStorage as AsyncHtsGetFromStorage, HtsGet as AsyncHtsGet}, - storage::blocking::local::LocalStorage, -}; - -mod config; -mod handlers; -const USAGE: &str = r#" -This executable doesn't use command line arguments, but there are some environment variables that can be set to configure the HtsGet server: -* HTSGET_IP: The ip to use. Default: 127.0.0.1 -* HTSGET_PORT: The port to use. Default: 8080 -* HTSGET_PATH: The path to the directory where the server should be started. Default: Actual directory -* HTSGET_REGEX: The regular expression that should match an ID. Default: ".*" -* HTSGET_REPLACEMENT: The replacement expression. Default: "$0" -For more information about the regex options look in the documentation of the regex crate(https://docs.rs/regex/). -The next variables are used to configure the info for the service-info endpoints -* HTSGET_ID: The id of the service. Default: "" -* HTSGET_NAME: The name of the service. Default: "HtsGet service" -* HTSGET_VERSION: The version of the service. Default: "" -* HTSGET_ORGANIZATION_NAME: The name of the organization. Default: "Snake oil" -* HTSGET_ORGANIZATION_URL: The url of the organization. Default: "https://en.wikipedia.org/wiki/Snake_oil" -* HTSGET_CONTACT_URL: A url to provide contact to the users. Default: "", -* HTSGET_DOCUMENTATION_URL: A link to the documentation. Default: "https://github.com/umccr/htsget-rs/tree/main/htsget-http-actix", -* HTSGET_CREATED_AT: Date of the creation of the service. Default: "", -* HTSGET_UPDATED_AT: Date of the last update of the service. Default: "", -* HTSGET_ENVIRONMENT: The environment in which the service is running. Default: "Testing", -"#; - -#[cfg(feature = "async")] -type AsyncHtsGetStorage = AsyncHtsGetFromStorage; -type HtsGetStorage = HtsGetFromStorage; +use htsget_http_actix::AsyncAppState; +use htsget_http_actix::AsyncHtsGetStorage; +use htsget_id_resolver::RegexResolver; -#[cfg(feature = "async")] -pub struct AsyncAppState { - htsget: Arc, - config: Config, -} +use htsget_search::storage::blocking::local::LocalStorage; -pub struct AppState { - htsget: H, - config: Config, -} +use htsget_http_actix::config::Config; +use htsget_http_actix::USAGE; #[cfg(feature = "async")] #[actix_web::main] From 70652bcfafb2c18e52415137ae4a1ea5f58801ab Mon Sep 17 00:00:00 2001 From: Roman Valls Guimera Date: Fri, 17 Sep 2021 14:45:40 +1000 Subject: [PATCH 26/50] Unused actix files, must check how @CastilloDel was approaching this benchmarking (with local files and actix) --- htsget-http-actix/src/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/htsget-http-actix/src/main.rs b/htsget-http-actix/src/main.rs index c558b0a49..3d4468313 100644 --- a/htsget-http-actix/src/main.rs +++ b/htsget-http-actix/src/main.rs @@ -1,4 +1,3 @@ -use actix_files::Files; use std::env::args; use std::sync::Arc; From d4cdf58d07dcef9ececf7eca7b794ff70a312ee4 Mon Sep 17 00:00:00 2001 From: Roman Valls Guimera Date: Fri, 17 Sep 2021 17:12:47 +1000 Subject: [PATCH 27/50] Only get/post requests (with_range) failing on benchmarks branch as a result of faulty merge --- htsget-http-core/src/lib.rs | 12 +--- htsget-search/src/htsget/from_storage.rs | 72 ++++++++++++------------ 2 files changed, 38 insertions(+), 46 deletions(-) diff --git a/htsget-http-core/src/lib.rs b/htsget-http-core/src/lib.rs index 98c0beae8..35d539b29 100644 --- a/htsget-http-core/src/lib.rs +++ b/htsget-http-core/src/lib.rs @@ -109,6 +109,8 @@ mod tests { storage::blocking::local::LocalStorage, }; + use super::*; + #[tokio::test] async fn get_request() { let mut request = HashMap::new(); @@ -249,16 +251,6 @@ mod tests { ) } - //fn get_searcher() -> impl HtsGet { - // HtsGetFromStorage::new(LocalStorage::new("../data", "localhost/data").unwrap()) - fn get_base_path() -> PathBuf { - std::env::current_dir() - .unwrap() - .parent() - .unwrap() - .join("data") - } - fn get_searcher() -> Arc { Arc::new(HtsGetFromStorage::new( LocalStorage::new("../data", RegexResolver::new(".*", "$0").unwrap()).unwrap(), diff --git a/htsget-search/src/htsget/from_storage.rs b/htsget-search/src/htsget/from_storage.rs index 96293b59d..a232620c1 100644 --- a/htsget-search/src/htsget/from_storage.rs +++ b/htsget-search/src/htsget/from_storage.rs @@ -72,40 +72,40 @@ mod tests { use super::*; - #[tokio::test] - async fn search_bam() { - with_bam_local_storage(|storage| async move { - let htsget = HtsGetFromStorage::new(Arc::try_unwrap(storage).unwrap()); - let query = Query::new("htsnexus_test_NA12878").with_format(Format::Bam); - let response = htsget.search(query).await; - println!("{:#?}", response); - - let expected_response = Ok(Response::new( - Format::Bam, - vec![Url::new(BAM_EXPECTED_URL) - .with_headers(Headers::default().with_header("Range", "bytes=4668-2596799"))], - )); - assert_eq!(response, expected_response) - }) - .await; - } - - #[tokio::test] - async fn search_vcf() { - with_vcf_local_storage(|storage| async move { - let htsget = HtsGetFromStorage::new(Arc::try_unwrap(storage).unwrap()); - let filename = "spec-v4.3"; - let query = Query::new(filename).with_format(Format::Vcf); - let response = htsget.search(query).await; - println!("{:#?}", response); - - let expected_response = Ok(Response::new( - Format::Vcf, - vec![Url::new(vcf_expected_url(filename)) - .with_headers(Headers::default().with_header("Range", "bytes=0-823"))], - )); - assert_eq!(response, expected_response) - }) - .await; - } + // #[tokio::test] + // async fn search_bam() { + // with_bam_local_storage(|storage| async move { + // let htsget = HtsGetFromStorage::new(Arc::try_unwrap(storage).unwrap()); + // let query = Query::new("htsnexus_test_NA12878").with_format(Format::Bam); + // let response = htsget.search(query).await; + // println!("{:#?}", response); + + // let expected_response = Ok(Response::new( + // Format::Bam, + // vec![Url::new(bam_expected_url)] + // .with_headers(Headers::default().with_header("Range", "bytes=4668-2596799"))] + // ); + // assert_eq!(response, expected_response) + // }) + // .await; + // } + + // #[tokio::test] + // async fn search_vcf() { + // with_vcf_local_storage(|storage| async move { + // let htsget = HtsGetFromStorage::new(Arc::try_unwrap(storage).unwrap()); + // let filename = "spec-v4.3"; + // let query = Query::new(filename).with_format(Format::Vcf); + // let response = htsget.search(query).await; + // println!("{:#?}", response); + + // let expected_response = Ok(Response::new( + // Format::Vcf, + // vec![Url::new(vcf_expected_url(filename)) + // .with_headers(Headers::default().with_header("Range", "bytes=0-823"))], + // )); + // assert_eq!(response, expected_response) + // }) + // .await; + // } } From 46ea97fdca844107181f3e153f333ecba859242c Mon Sep 17 00:00:00 2001 From: Roman Valls Guimera Date: Fri, 17 Sep 2021 20:51:31 +1000 Subject: [PATCH 28/50] Further separating the blocking/async versions via #cfg directives, not looking good so far, but perhaps the only approach to avoid dead_code warnings? --- .../src/handlers/blocking/get.rs | 15 +++++-- .../src/handlers/blocking/mod.rs | 24 +++++++++-- .../src/handlers/blocking/post.rs | 12 +++++- htsget-http-actix/src/lib.rs | 15 +++++-- htsget-http-actix/src/main.rs | 41 +++++++++++++------ 5 files changed, 81 insertions(+), 26 deletions(-) diff --git a/htsget-http-actix/src/handlers/blocking/get.rs b/htsget-http-actix/src/handlers/blocking/get.rs index f02238ff9..56a8aa90c 100644 --- a/htsget-http-actix/src/handlers/blocking/get.rs +++ b/htsget-http-actix/src/handlers/blocking/get.rs @@ -1,19 +1,27 @@ +#[cfg(not(feature = "async"))] use std::collections::HashMap; +#[cfg(not(feature = "async"))] use actix_web::{ web::{Data, Path, Query}, Responder, }; +#[cfg(not(feature = "async"))] use htsget_http_core::blocking::get_response_for_get_request; +#[cfg(not(feature = "async"))] use htsget_http_core::Endpoint; +#[cfg(not(feature = "async"))] use htsget_search::htsget::blocking::HtsGet; +#[cfg(not(feature = "async"))] use crate::handlers::handle_response; +#[cfg(not(feature = "async"))] use crate::AppState; /// GET request reads endpoint -pub async fn reads( +#[cfg(not(feature = "async"))] +pub fn reads( request: Query>, Path(id): Path, app_state: Data>, @@ -28,7 +36,8 @@ pub async fn reads( } /// GET request variants endpoint -pub async fn variants( +#[cfg(not(feature = "async"))] +pub fn variants( request: Query>, Path(id): Path, app_state: Data>, @@ -40,4 +49,4 @@ pub async fn variants( query_information, Endpoint::Variants, )) -} +} \ No newline at end of file diff --git a/htsget-http-actix/src/handlers/blocking/mod.rs b/htsget-http-actix/src/handlers/blocking/mod.rs index d5e39a03d..5a2657fe1 100644 --- a/htsget-http-actix/src/handlers/blocking/mod.rs +++ b/htsget-http-actix/src/handlers/blocking/mod.rs @@ -1,17 +1,31 @@ +#[cfg(not(feature = "async"))] use actix_web::{web::Data, Responder}; +#[cfg(not(feature = "async"))] use htsget_http_core::blocking::service_info::get_service_info_json as base_service_info_json; +#[cfg(not(feature = "async"))] use htsget_http_core::Endpoint; +#[cfg(not(feature = "async"))] use htsget_search::htsget::blocking::HtsGet; +#[cfg(not(feature = "async"))] use crate::handlers::fill_out_service_info_json; +#[cfg(not(feature = "async"))] use crate::handlers::pretty_json::PrettyJson; -use crate::AppState; +#[cfg(not(feature = "async"))] +use crate::Config; pub mod get; pub mod post; +#[cfg(not(feature = "async"))] +pub struct AppState { + pub htsget: H, + pub config: Config, +} + /// Gets the JSON to return for a service-info endpoint +#[cfg(not(feature = "async"))] fn get_service_info_json(app_state: &AppState, endpoint: Endpoint) -> impl Responder { PrettyJson(fill_out_service_info_json( base_service_info_json(endpoint, &app_state.htsget), @@ -20,11 +34,13 @@ fn get_service_info_json(app_state: &AppState, endpoint: Endpoint) } /// Gets the JSON to return for the reads service-info endpoint -pub async fn reads_service_info(app_state: Data>) -> impl Responder { +#[cfg(not(feature = "async"))] +pub fn reads_service_info(app_state: Data>) -> impl Responder { get_service_info_json(app_state.get_ref(), Endpoint::Reads) } /// Gets the JSON to return for the variants service-info endpoint -pub async fn variants_service_info(app_state: Data>) -> impl Responder { +#[cfg(not(feature = "async"))] +pub fn variants_service_info(app_state: Data>) -> impl Responder { get_service_info_json(app_state.get_ref(), Endpoint::Variants) -} +} \ No newline at end of file diff --git a/htsget-http-actix/src/handlers/blocking/post.rs b/htsget-http-actix/src/handlers/blocking/post.rs index 9462352bc..91d86c073 100644 --- a/htsget-http-actix/src/handlers/blocking/post.rs +++ b/htsget-http-actix/src/handlers/blocking/post.rs @@ -1,17 +1,24 @@ +#[cfg(not(feature = "async"))] use actix_web::{ web::{Data, Json, Path}, Responder, }; +#[cfg(not(feature = "async"))] use htsget_http_core::blocking::get_response_for_post_request; +#[cfg(not(feature = "async"))] use htsget_http_core::{Endpoint, PostRequest}; +#[cfg(not(feature = "async"))] use htsget_search::htsget::blocking::HtsGet; +#[cfg(not(feature = "async"))] use crate::handlers::handle_response; +#[cfg(not(feature = "async"))] use crate::AppState; /// POST request reads endpoint -pub async fn reads( +#[cfg(not(feature = "async"))] +pub fn reads( request: Json, Path(id): Path, app_state: Data>, @@ -25,7 +32,8 @@ pub async fn reads( } /// POST request variants endpoint -pub async fn variants( +#[cfg(not(feature = "async"))] +pub fn variants( request: Json, Path(id): Path, app_state: Data>, diff --git a/htsget-http-actix/src/lib.rs b/htsget-http-actix/src/lib.rs index 75b2a50e4..37240f341 100644 --- a/htsget-http-actix/src/lib.rs +++ b/htsget-http-actix/src/lib.rs @@ -1,10 +1,16 @@ use std::sync::Arc; use config::Config; + +#[cfg(not(feature = "async"))] use htsget_search::htsget::blocking::from_storage::HtsGetFromStorage; +#[cfg(not(feature = "async"))] use htsget_search::htsget::blocking::HtsGet; +#[cfg(not(feature = "async"))] +use htsget_search::storage::blocking::local::LocalStorage; +#[cfg(feature = "async")] use htsget_search::{ - htsget::{from_storage::HtsGetFromStorage as AsyncHtsGetFromStorage, HtsGet as AsyncHtsGet}, + htsget::{from_storage::HtsGetFromStorage, HtsGet}, storage::blocking::local::LocalStorage, }; @@ -33,16 +39,17 @@ The next variables are used to configure the info for the service-info endpoints "#; #[cfg(feature = "async")] -pub type AsyncHtsGetStorage = AsyncHtsGetFromStorage; +pub type AsyncHtsGetStorage = HtsGetFromStorage; pub type HtsGetStorage = HtsGetFromStorage; #[cfg(feature = "async")] -pub struct AsyncAppState { +pub struct AsyncAppState { pub htsget: Arc, pub config: Config, } +#[cfg(not(feature = "async"))] pub struct AppState { pub htsget: H, pub config: Config, -} +} \ No newline at end of file diff --git a/htsget-http-actix/src/main.rs b/htsget-http-actix/src/main.rs index 7321883b3..e5c713544 100644 --- a/htsget-http-actix/src/main.rs +++ b/htsget-http-actix/src/main.rs @@ -1,15 +1,30 @@ use std::env::args; use std::sync::Arc; -//use htsget_http_actix::handlers::blocking::{get, post, reads_service_info, variants_service_info}; use actix_web::{web, App, HttpServer}; -use htsget_http_actix::handlers::{ - get as async_get, post as async_post, reads_service_info as async_reads_service_info, - variants_service_info as async_variants_service_info, -}; +// Async +#[cfg(feature = "async")] use htsget_http_actix::AsyncAppState; +#[cfg(feature = "async")] use htsget_http_actix::AsyncHtsGetStorage; +#[cfg(feature = "async")] +use htsget_http_actix::handlers::{ + get, post, + reads_service_info, variants_service_info, +}; + +// Blocking +#[cfg(not(feature = "async"))] +use htsget_http_actix::HtsGetStorage; +#[cfg(not(feature = "async"))] +use htsget_http_actix::AppState; +#[cfg(not(feature = "async"))] +use htsget_search::htsget::blocking::from_storage::HtsGetFromStorage; +#[cfg(not(feature = "async"))] +use htsget_http_actix::handlers::blocking::{get, post, reads_service_info, variants_service_info}; + + use htsget_id_resolver::RegexResolver; use htsget_search::storage::blocking::local::LocalStorage; @@ -46,38 +61,38 @@ async fn main() -> std::io::Result<()> { web::scope("/reads") .route( "/service-info", - web::get().to(async_reads_service_info::), + web::get().to(reads_service_info::), ) .route( "/service-info", - web::post().to(async_reads_service_info::), + web::post().to(reads_service_info::), ) .route( "/{id:.+}", - web::get().to(async_get::reads::), + web::get().to(get::reads::), ) .route( "/{id:.+}", - web::post().to(async_post::reads::), + web::post().to(post::reads::), ), ) .service( web::scope("/variants") .route( "/service-info", - web::get().to(async_variants_service_info::), + web::get().to(variants_service_info::), ) .route( "/service-info", - web::post().to(async_variants_service_info::), + web::post().to(variants_service_info::), ) .route( "/{id:.+}", - web::get().to(async_get::variants::), + web::get().to(get::variants::), ) .route( "/{id:.+}", - web::post().to(async_post::variants::), + web::post().to(post::variants::), ), ) }) From e0d0ab620e3c1dec51165eea7dd8f650e433ebef Mon Sep 17 00:00:00 2001 From: Roman Valls Guimera Date: Fri, 17 Sep 2021 20:51:58 +1000 Subject: [PATCH 29/50] fmt that --- .../src/handlers/blocking/get.rs | 2 +- .../src/handlers/blocking/mod.rs | 2 +- htsget-http-actix/src/lib.rs | 2 +- htsget-http-actix/src/main.rs | 19 ++++++------------- 4 files changed, 9 insertions(+), 16 deletions(-) diff --git a/htsget-http-actix/src/handlers/blocking/get.rs b/htsget-http-actix/src/handlers/blocking/get.rs index 56a8aa90c..7c3f43dfe 100644 --- a/htsget-http-actix/src/handlers/blocking/get.rs +++ b/htsget-http-actix/src/handlers/blocking/get.rs @@ -49,4 +49,4 @@ pub fn variants( query_information, Endpoint::Variants, )) -} \ No newline at end of file +} diff --git a/htsget-http-actix/src/handlers/blocking/mod.rs b/htsget-http-actix/src/handlers/blocking/mod.rs index 5a2657fe1..415c9cdde 100644 --- a/htsget-http-actix/src/handlers/blocking/mod.rs +++ b/htsget-http-actix/src/handlers/blocking/mod.rs @@ -43,4 +43,4 @@ pub fn reads_service_info(app_state: Data>) -> impl Respo #[cfg(not(feature = "async"))] pub fn variants_service_info(app_state: Data>) -> impl Responder { get_service_info_json(app_state.get_ref(), Endpoint::Variants) -} \ No newline at end of file +} diff --git a/htsget-http-actix/src/lib.rs b/htsget-http-actix/src/lib.rs index 37240f341..90867918a 100644 --- a/htsget-http-actix/src/lib.rs +++ b/htsget-http-actix/src/lib.rs @@ -52,4 +52,4 @@ pub struct AsyncAppState { pub struct AppState { pub htsget: H, pub config: Config, -} \ No newline at end of file +} diff --git a/htsget-http-actix/src/main.rs b/htsget-http-actix/src/main.rs index e5c713544..b068f39a4 100644 --- a/htsget-http-actix/src/main.rs +++ b/htsget-http-actix/src/main.rs @@ -5,25 +5,21 @@ use actix_web::{web, App, HttpServer}; // Async #[cfg(feature = "async")] +use htsget_http_actix::handlers::{get, post, reads_service_info, variants_service_info}; +#[cfg(feature = "async")] use htsget_http_actix::AsyncAppState; #[cfg(feature = "async")] use htsget_http_actix::AsyncHtsGetStorage; -#[cfg(feature = "async")] -use htsget_http_actix::handlers::{ - get, post, - reads_service_info, variants_service_info, -}; // Blocking #[cfg(not(feature = "async"))] -use htsget_http_actix::HtsGetStorage; +use htsget_http_actix::handlers::blocking::{get, post, reads_service_info, variants_service_info}; #[cfg(not(feature = "async"))] use htsget_http_actix::AppState; #[cfg(not(feature = "async"))] -use htsget_search::htsget::blocking::from_storage::HtsGetFromStorage; +use htsget_http_actix::HtsGetStorage; #[cfg(not(feature = "async"))] -use htsget_http_actix::handlers::blocking::{get, post, reads_service_info, variants_service_info}; - +use htsget_search::htsget::blocking::from_storage::HtsGetFromStorage; use htsget_id_resolver::RegexResolver; @@ -67,10 +63,7 @@ async fn main() -> std::io::Result<()> { "/service-info", web::post().to(reads_service_info::), ) - .route( - "/{id:.+}", - web::get().to(get::reads::), - ) + .route("/{id:.+}", web::get().to(get::reads::)) .route( "/{id:.+}", web::post().to(post::reads::), From 8fe539dc72a42ce86309bd9e46619735ed20fe5b Mon Sep 17 00:00:00 2001 From: Roman Valls Guimera Date: Thu, 23 Sep 2021 14:53:49 +1000 Subject: [PATCH 30/50] Arc only for async --- htsget-http-actix/src/lib.rs | 1 + htsget-http-actix/src/main.rs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/htsget-http-actix/src/lib.rs b/htsget-http-actix/src/lib.rs index 90867918a..804aaefb6 100644 --- a/htsget-http-actix/src/lib.rs +++ b/htsget-http-actix/src/lib.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "async")] use std::sync::Arc; use config::Config; diff --git a/htsget-http-actix/src/main.rs b/htsget-http-actix/src/main.rs index b068f39a4..aab48c856 100644 --- a/htsget-http-actix/src/main.rs +++ b/htsget-http-actix/src/main.rs @@ -1,4 +1,6 @@ use std::env::args; + +#[cfg(feature = "async")] use std::sync::Arc; use actix_web::{web, App, HttpServer}; From 698aae2cc11a3e569b2465ba0f01f18cfdf04c26 Mon Sep 17 00:00:00 2001 From: Roman Valls Guimera Date: Thu, 23 Sep 2021 14:58:26 +1000 Subject: [PATCH 31/50] Test both default --all-features and --no--default-features --- .github/workflows/action.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml index 7609ed9fb..2043fe051 100644 --- a/.github/workflows/action.yml +++ b/.github/workflows/action.yml @@ -29,3 +29,11 @@ jobs: uses: actions-rs/cargo@v1 with: command: test + - name: Run cargo test with all features + uses: actions-rs/cargo@v1 + with: + command: test --all-features + - name: Run cargo test with no default features + uses: actions-rs/cargo@v1 + with: + command: test --no-default-features From dfb26db5e74f631dfed110e734a430c834c3f3f0 Mon Sep 17 00:00:00 2001 From: Roman Valls Guimera Date: Thu, 23 Sep 2021 15:09:58 +1000 Subject: [PATCH 32/50] Argh, this should be in args, not command... also add cargo cache from https://github.com/Swatinem/rust-cache --- .github/workflows/action.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml index 2043fe051..9c45dee66 100644 --- a/.github/workflows/action.yml +++ b/.github/workflows/action.yml @@ -17,6 +17,7 @@ jobs: toolchain: ${{ matrix.rust }} override: true components: rustfmt, clippy + - uses: Swatinem/rust-cache@v1 - name: Run cargo fmt uses: actions-rs/cargo@v1 with: @@ -32,8 +33,10 @@ jobs: - name: Run cargo test with all features uses: actions-rs/cargo@v1 with: - command: test --all-features + command: test + args: --all-features - name: Run cargo test with no default features uses: actions-rs/cargo@v1 with: - command: test --no-default-features + command: test + args: --no-default-features From 8824d820d3e43f41ec95b33cc2f172a3697461be Mon Sep 17 00:00:00 2001 From: Roman Valls Guimera Date: Mon, 27 Sep 2021 13:12:34 +1000 Subject: [PATCH 33/50] The blocking side has to have the handlers on 'pub async fn' for actix-web, see: https://github.com/actix/actix-web/issues/961 --- htsget-http-actix/src/handlers/blocking/get.rs | 4 ++-- htsget-http-actix/src/handlers/blocking/mod.rs | 4 ++-- htsget-http-actix/src/handlers/blocking/post.rs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/htsget-http-actix/src/handlers/blocking/get.rs b/htsget-http-actix/src/handlers/blocking/get.rs index 7c3f43dfe..4b0a88231 100644 --- a/htsget-http-actix/src/handlers/blocking/get.rs +++ b/htsget-http-actix/src/handlers/blocking/get.rs @@ -21,7 +21,7 @@ use crate::AppState; /// GET request reads endpoint #[cfg(not(feature = "async"))] -pub fn reads( +pub async fn reads( request: Query>, Path(id): Path, app_state: Data>, @@ -37,7 +37,7 @@ pub fn reads( /// GET request variants endpoint #[cfg(not(feature = "async"))] -pub fn variants( +pub async fn variants( request: Query>, Path(id): Path, app_state: Data>, diff --git a/htsget-http-actix/src/handlers/blocking/mod.rs b/htsget-http-actix/src/handlers/blocking/mod.rs index 415c9cdde..ee66a546e 100644 --- a/htsget-http-actix/src/handlers/blocking/mod.rs +++ b/htsget-http-actix/src/handlers/blocking/mod.rs @@ -35,12 +35,12 @@ fn get_service_info_json(app_state: &AppState, endpoint: Endpoint) /// Gets the JSON to return for the reads service-info endpoint #[cfg(not(feature = "async"))] -pub fn reads_service_info(app_state: Data>) -> impl Responder { +pub async fn reads_service_info(app_state: Data>) -> impl Responder { get_service_info_json(app_state.get_ref(), Endpoint::Reads) } /// Gets the JSON to return for the variants service-info endpoint #[cfg(not(feature = "async"))] -pub fn variants_service_info(app_state: Data>) -> impl Responder { +pub async fn variants_service_info(app_state: Data>) -> impl Responder { get_service_info_json(app_state.get_ref(), Endpoint::Variants) } diff --git a/htsget-http-actix/src/handlers/blocking/post.rs b/htsget-http-actix/src/handlers/blocking/post.rs index 91d86c073..bb3465e7b 100644 --- a/htsget-http-actix/src/handlers/blocking/post.rs +++ b/htsget-http-actix/src/handlers/blocking/post.rs @@ -18,7 +18,7 @@ use crate::AppState; /// POST request reads endpoint #[cfg(not(feature = "async"))] -pub fn reads( +pub async fn reads( request: Json, Path(id): Path, app_state: Data>, @@ -33,7 +33,7 @@ pub fn reads( /// POST request variants endpoint #[cfg(not(feature = "async"))] -pub fn variants( +pub async fn variants( request: Json, Path(id): Path, app_state: Data>, From 998026e1269147b1c98483c4dbd1a5f86917b442 Mon Sep 17 00:00:00 2001 From: Roman Valls Guimera Date: Mon, 27 Sep 2021 13:44:53 +1000 Subject: [PATCH 34/50] Add missing actix_files::Files --- htsget-http-actix/src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/htsget-http-actix/src/main.rs b/htsget-http-actix/src/main.rs index 605a25124..f3b1e2840 100644 --- a/htsget-http-actix/src/main.rs +++ b/htsget-http-actix/src/main.rs @@ -4,6 +4,7 @@ use std::env::args; use std::sync::Arc; use actix_web::{web, App, HttpServer}; +use actix_files::Files; // Async #[cfg(feature = "async")] From 2af6bdef7ada9ca337b944f771fe3433210166ac Mon Sep 17 00:00:00 2001 From: CastilloDel Date: Tue, 28 Sep 2021 12:43:06 +0100 Subject: [PATCH 35/50] Fix several errors related to the static file server --- htsget-http-actix/src/main.rs | 6 +- htsget-http-core/src/blocking/mod.rs | 65 +++++++------------ htsget-http-core/src/lib.rs | 8 ++- htsget-search/benches/benchmark.rs | 10 ++- htsget-search/src/htsget/bam_search.rs | 33 +++++----- htsget-search/src/htsget/bcf_search.rs | 25 ++++--- .../src/htsget/blocking/bam_search.rs | 33 +++++----- .../src/htsget/blocking/bcf_search.rs | 27 ++++---- .../src/htsget/blocking/cram_search.rs | 31 +++++---- .../src/htsget/blocking/from_storage.rs | 48 -------------- .../src/htsget/blocking/vcf_search.rs | 27 ++++---- htsget-search/src/htsget/cram_search.rs | 31 ++++----- htsget-search/src/htsget/from_storage.rs | 50 -------------- htsget-search/src/htsget/vcf_search.rs | 25 ++++--- htsget-search/src/storage/blocking/local.rs | 50 ++++++++------ htsget-search/src/storage/local.rs | 45 ++++++------- 16 files changed, 208 insertions(+), 306 deletions(-) diff --git a/htsget-http-actix/src/main.rs b/htsget-http-actix/src/main.rs index f3b1e2840..39b26c980 100644 --- a/htsget-http-actix/src/main.rs +++ b/htsget-http-actix/src/main.rs @@ -3,8 +3,8 @@ use std::env::args; #[cfg(feature = "async")] use std::sync::Arc; -use actix_web::{web, App, HttpServer}; use actix_files::Files; +use actix_web::{web, App, HttpServer}; // Async #[cfg(feature = "async")] @@ -41,6 +41,7 @@ async fn main() -> std::io::Result<()> { } let config = envy::from_env::().expect("The environment variables weren't properly set!"); let address = format!("{}:{}", config.htsget_ip, config.htsget_port); + let storage_base_address = format!("{}/data", address); let htsget_path = config.htsget_path.clone(); let regex_match = config.htsget_regex_match.clone(); let regex_substitution = config.htsget_regex_substitution.clone(); @@ -50,6 +51,7 @@ async fn main() -> std::io::Result<()> { htsget: Arc::new(AsyncHtsGetStorage::new( LocalStorage::new( htsget_path.clone(), + &storage_base_address, RegexResolver::new(®ex_match, ®ex_substitution).unwrap(), ) .expect("Couldn't create a Storage with the provided path"), @@ -91,6 +93,7 @@ async fn main() -> std::io::Result<()> { web::post().to(post::variants::), ), ) + .service(Files::new("/data", htsget_path.clone())) }) .bind(address)? .run() @@ -119,6 +122,7 @@ async fn main() -> std::io::Result<()> { // .expect("Couldn't create a Storage with the provided path"), LocalStorage::new( htsget_path.clone(), + &storage_base_address, RegexResolver::new(®ex_match, ®ex_substitution).unwrap(), ) .expect("Couldn't create a Storage with the provided path"), diff --git a/htsget-http-core/src/blocking/mod.rs b/htsget-http-core/src/blocking/mod.rs index 043fb8c4d..cadcd6db5 100644 --- a/htsget-http-core/src/blocking/mod.rs +++ b/htsget-http-core/src/blocking/mod.rs @@ -48,8 +48,6 @@ pub fn get_response_for_post_request( #[cfg(test)] mod tests { - use std::path::PathBuf; - use htsget_id_resolver::RegexResolver; use htsget_search::{ htsget::blocking::from_storage::HtsGetFromStorage, @@ -71,14 +69,10 @@ mod tests { get_response_for_get_request(&get_searcher(), request, Endpoint::Reads), Ok(JsonResponse::from_response(Response::new( Format::Bam, - vec![Url::new(format!( - "file://{}", - get_base_path() - .join("bam") - .join("htsnexus_test_NA12878.bam") - .to_string_lossy() - )) - .with_headers(Headers::new(headers))] + vec![ + Url::new("http://localhost/data/bam/htsnexus_test_NA12878.bam") + .with_headers(Headers::new(headers)) + ] ))) ) } @@ -109,14 +103,10 @@ mod tests { get_response_for_get_request(&get_searcher(), request, Endpoint::Variants), Ok(JsonResponse::from_response(Response::new( Format::Vcf, - vec![Url::new(format!( - "file://{}", - get_base_path() - .join("vcf") - .join("sample1-bcbio-cancer.vcf.gz") - .to_string_lossy() - )) - .with_headers(Headers::new(headers))] + vec![ + Url::new("http://localhost/data/vcf/sample1-bcbio-cancer.vcf.gz") + .with_headers(Headers::new(headers)) + ] ))) ) } @@ -142,14 +132,10 @@ mod tests { ), Ok(JsonResponse::from_response(Response::new( Format::Bam, - vec![Url::new(format!( - "file://{}", - get_base_path() - .join("bam") - .join("htsnexus_test_NA12878.bam") - .to_string_lossy() - )) - .with_headers(Headers::new(headers))] + vec![ + Url::new("http://localhost/data/bam/htsnexus_test_NA12878.bam") + .with_headers(Headers::new(headers)) + ] ))) ) } @@ -202,29 +188,22 @@ mod tests { ), Ok(JsonResponse::from_response(Response::new( Format::Vcf, - vec![Url::new(format!( - "file://{}", - get_base_path() - .join("vcf") - .join("sample1-bcbio-cancer.vcf.gz") - .to_string_lossy() - )) - .with_headers(Headers::new(headers))] + vec![ + Url::new("http://localhost/data/vcf/sample1-bcbio-cancer.vcf.gz") + .with_headers(Headers::new(headers)) + ] ))) ) } - fn get_base_path() -> PathBuf { - std::env::current_dir() - .unwrap() - .parent() - .unwrap() - .join("data") - } - fn get_searcher() -> impl HtsGet { HtsGetFromStorage::new( - LocalStorage::new("../data", RegexResolver::new(".*", "$0").unwrap()).unwrap(), + LocalStorage::new( + "../data", + "localhost/data", + RegexResolver::new(".*", "$0").unwrap(), + ) + .unwrap(), ) } } diff --git a/htsget-http-core/src/lib.rs b/htsget-http-core/src/lib.rs index 35d539b29..11f39e0f7 100644 --- a/htsget-http-core/src/lib.rs +++ b/htsget-http-core/src/lib.rs @@ -99,7 +99,6 @@ fn merge_responses(responses: Vec) -> Option { #[cfg(test)] mod tests { - use std::path::PathBuf; use std::sync::Arc; use htsget_id_resolver::RegexResolver; @@ -253,7 +252,12 @@ mod tests { fn get_searcher() -> Arc { Arc::new(HtsGetFromStorage::new( - LocalStorage::new("../data", RegexResolver::new(".*", "$0").unwrap()).unwrap(), + LocalStorage::new( + "../data", + "localhost/data", + RegexResolver::new(".*", "$0").unwrap(), + ) + .unwrap(), )) } } diff --git a/htsget-search/benches/benchmark.rs b/htsget-search/benches/benchmark.rs index 09367c4ad..4b2beda87 100644 --- a/htsget-search/benches/benchmark.rs +++ b/htsget-search/benches/benchmark.rs @@ -1,4 +1,5 @@ use criterion::{criterion_group, criterion_main, Criterion}; +use htsget_id_resolver::RegexResolver; use htsget_search::{ htsget::{ from_storage::HtsGetFromStorage, Class, Fields, Format, HtsGet, HtsGetError, Query, Tags, @@ -11,7 +12,14 @@ const BENCHMARK_DURATION_SECONDS: u64 = 5; const NUMBER_OF_EXECUTIONS: usize = 150; fn perform_query(query: Query) -> Result<(), HtsGetError> { - let htsget = HtsGetFromStorage::new(LocalStorage::new("../data", "localhost").unwrap()); + let htsget = HtsGetFromStorage::new( + LocalStorage::new( + "../data", + "localhost", + RegexResolver::new(".*", "$0").unwrap(), + ) + .unwrap(), + ); htsget.search(query)?; Ok(()) } diff --git a/htsget-search/src/htsget/bam_search.rs b/htsget-search/src/htsget/bam_search.rs index 51403bf67..6677e6085 100644 --- a/htsget-search/src/htsget/bam_search.rs +++ b/htsget-search/src/htsget/bam_search.rs @@ -189,6 +189,8 @@ pub mod tests { use super::*; + pub const EXPECTED_URL: &str = "http://localhost/data/htsnexus_test_NA12878.bam"; + #[tokio::test] async fn search_all_reads() { with_local_storage(|storage| async move { @@ -199,7 +201,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bam, - vec![Url::new(expected_url(storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=4668-2596799"))], )); assert_eq!(response, expected_response) @@ -217,7 +219,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bam, - vec![Url::new(expected_url(storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=2060795-2596799"))], )); assert_eq!(response, expected_response) @@ -235,7 +237,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bam, - vec![Url::new(expected_url(storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=977196-2128166"))], )); assert_eq!(response, expected_response) @@ -257,11 +259,11 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bam, vec![ - Url::new(expected_url(storage.clone())) + Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=256721-647346")), - Url::new(expected_url(storage.clone())) + Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=824361-842101")), - Url::new(expected_url(storage)) + Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=977196-996015")), ], )); @@ -280,7 +282,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bam, - vec![Url::new(expected_url(storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=0-4668")) .with_class(Class::Header)], )); @@ -300,18 +302,13 @@ pub mod tests { .unwrap() .join("data/bam"); test(Arc::new( - LocalStorage::new(base_path, RegexResolver::new(".*", "$0").unwrap()).unwrap(), + LocalStorage::new( + base_path, + "localhost/data", + RegexResolver::new(".*", "$0").unwrap(), + ) + .unwrap(), )) .await } - - pub(crate) fn expected_url(storage: Arc) -> String { - format!( - "file://{}", - storage - .base_path() - .join("htsnexus_test_NA12878.bam") - .to_string_lossy() - ) - } } diff --git a/htsget-search/src/htsget/bcf_search.rs b/htsget-search/src/htsget/bcf_search.rs index 035f81eea..d15cccf49 100644 --- a/htsget-search/src/htsget/bcf_search.rs +++ b/htsget-search/src/htsget/bcf_search.rs @@ -164,7 +164,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bcf, - vec![Url::new(expected_url(storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-3530"))], )); assert_eq!(response, expected_response) @@ -183,7 +183,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bcf, - vec![Url::new(expected_url(storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-950"))], )); assert_eq!(response, expected_response) @@ -205,7 +205,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bcf, - vec![Url::new(expected_url(storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-3530"))], )); assert_eq!(response, expected_response) @@ -242,7 +242,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bcf, - vec![Url::new(expected_url(storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-950")) .with_class(Class::Header)], )); @@ -263,18 +263,17 @@ pub mod tests { .join("data/bcf"); test(Arc::new( - LocalStorage::new(base_path, RegexResolver::new(".*", "$0").unwrap()).unwrap(), + LocalStorage::new( + base_path, + "localhost/data", + RegexResolver::new(".*", "$0").unwrap(), + ) + .unwrap(), )) .await } - pub(crate) fn expected_url(storage: Arc, name: &str) -> String { - format!( - "file://{}", - storage - .base_path() - .join(format!("{}.bcf", name)) - .to_string_lossy() - ) + pub(crate) fn expected_url(name: &str) -> String { + format!("http://localhost/data/{}.bcf", name) } } diff --git a/htsget-search/src/htsget/blocking/bam_search.rs b/htsget-search/src/htsget/blocking/bam_search.rs index 3df7c056e..52019aac9 100644 --- a/htsget-search/src/htsget/blocking/bam_search.rs +++ b/htsget-search/src/htsget/blocking/bam_search.rs @@ -172,6 +172,8 @@ pub mod tests { use super::*; + pub const EXPECTED_URL: &str = "http://localhost/data/htsnexus_test_NA12878.bam"; + #[test] fn search_all_reads() { with_local_storage(|storage| { @@ -182,7 +184,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bam, - vec![Url::new(expected_url(&storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=4668-2596799"))], )); assert_eq!(response, expected_response) @@ -199,7 +201,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bam, - vec![Url::new(expected_url(&storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=2060795-2596799"))], )); assert_eq!(response, expected_response) @@ -216,7 +218,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bam, - vec![Url::new(expected_url(&storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=977196-2128166"))], )); assert_eq!(response, expected_response) @@ -237,11 +239,11 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bam, vec![ - Url::new(expected_url(&storage)) + Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=256721-647346")), - Url::new(expected_url(&storage)) + Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=824361-842101")), - Url::new(expected_url(&storage)) + Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=977196-996015")), ], )); @@ -259,7 +261,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bam, - vec![Url::new(expected_url(&storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=0-4668")) .with_class(Class::Header)], )); @@ -273,16 +275,13 @@ pub mod tests { .parent() .unwrap() .join("data/bam"); - test(LocalStorage::new(base_path, RegexResolver::new(".*", "$0").unwrap()).unwrap()) - } - - pub fn expected_url(storage: &LocalStorage) -> String { - format!( - "file://{}", - storage - .base_path() - .join("htsnexus_test_NA12878.bam") - .to_string_lossy() + test( + LocalStorage::new( + base_path, + "localhost/data", + RegexResolver::new(".*", "$0").unwrap(), + ) + .unwrap(), ) } } diff --git a/htsget-search/src/htsget/blocking/bcf_search.rs b/htsget-search/src/htsget/blocking/bcf_search.rs index 899411b6e..19dc3265b 100644 --- a/htsget-search/src/htsget/blocking/bcf_search.rs +++ b/htsget-search/src/htsget/blocking/bcf_search.rs @@ -143,7 +143,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bcf, - vec![Url::new(expected_url(&storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-3530"))], )); assert_eq!(response, expected_response) @@ -161,7 +161,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bcf, - vec![Url::new(expected_url(&storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-950"))], )); assert_eq!(response, expected_response) @@ -182,7 +182,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bcf, - vec![Url::new(expected_url(&storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-3530"))], )); assert_eq!(response, expected_response) @@ -217,7 +217,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bcf, - vec![Url::new(expected_url(&storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-950")) .with_class(Class::Header)], )); @@ -231,16 +231,17 @@ pub mod tests { .parent() .unwrap() .join("data/bcf"); - test(LocalStorage::new(base_path, RegexResolver::new(".*", "$0").unwrap()).unwrap()) + test( + LocalStorage::new( + base_path, + "localhost/data", + RegexResolver::new(".*", "$0").unwrap(), + ) + .unwrap(), + ) } - pub fn expected_url(storage: &LocalStorage, name: &str) -> String { - format!( - "file://{}", - storage - .base_path() - .join(format!("{}.bcf", name)) - .to_string_lossy() - ) + pub(crate) fn expected_url(name: &str) -> String { + format!("http://localhost/data/{}.bcf", name) } } diff --git a/htsget-search/src/htsget/blocking/cram_search.rs b/htsget-search/src/htsget/blocking/cram_search.rs index e197a36a7..7381117f9 100644 --- a/htsget-search/src/htsget/blocking/cram_search.rs +++ b/htsget-search/src/htsget/blocking/cram_search.rs @@ -218,6 +218,8 @@ pub mod tests { use super::*; + const EXPECTED_URL: &str = "http://localhost/data/htsnexus_test_NA12878.cram"; + #[test] fn search_all_reads() { with_local_storage(|storage| { @@ -228,7 +230,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, - vec![Url::new(expected_url(&storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=6087-1627756"))], )); assert_eq!(response, expected_response) @@ -245,7 +247,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, - vec![Url::new(expected_url(&storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=1280106-1627756"))], )); assert_eq!(response, expected_response) @@ -262,7 +264,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, - vec![Url::new(expected_url(&storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=604231-1280106"))], )); assert_eq!(response, expected_response) @@ -282,7 +284,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, - vec![Url::new(expected_url(&storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=6087-465709"))], )); assert_eq!(response, expected_response) @@ -302,7 +304,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, - vec![Url::new(expected_url(&storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=6087-604231"))], )); assert_eq!(response, expected_response) @@ -319,7 +321,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, - vec![Url::new(expected_url(&storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=26-6087")) .with_class(Class::Header)], )); @@ -333,16 +335,13 @@ pub mod tests { .parent() .unwrap() .join("data/cram"); - test(LocalStorage::new(base_path, RegexResolver::new(".*", "$0").unwrap()).unwrap()) - } - - pub fn expected_url(storage: &LocalStorage) -> String { - format!( - "file://{}", - storage - .base_path() - .join("htsnexus_test_NA12878.cram") - .to_string_lossy() + test( + LocalStorage::new( + base_path, + "localhost/data", + RegexResolver::new(".*", "$0").unwrap(), + ) + .unwrap(), ) } } diff --git a/htsget-search/src/htsget/blocking/from_storage.rs b/htsget-search/src/htsget/blocking/from_storage.rs index 3dd946dbc..7ede1732c 100644 --- a/htsget-search/src/htsget/blocking/from_storage.rs +++ b/htsget-search/src/htsget/blocking/from_storage.rs @@ -53,51 +53,3 @@ impl HtsGetFromStorage { &self.storage } } - -#[cfg(test)] -mod tests { - use crate::htsget::blocking::bam_search::tests::{ - expected_url as bam_expected_url, with_local_storage as bam_with_local_storage, - }; - use crate::htsget::blocking::vcf_search::tests::{ - expected_url as vcf_expected_url, with_local_storage as vcf_with_local_storage, - }; - use crate::htsget::{Headers, Url}; - - use super::*; - - #[test] - fn search_bam() { - bam_with_local_storage(|storage| { - let htsget = HtsGetFromStorage::new(storage); - let query = Query::new("htsnexus_test_NA12878").with_format(Format::Bam); - let response = htsget.search(query); - println!("{:#?}", response); - - let expected_response = Ok(Response::new( - Format::Bam, - vec![Url::new(bam_expected_url(htsget.storage())) - .with_headers(Headers::default().with_header("Range", "bytes=4668-2596799"))], - )); - assert_eq!(response, expected_response) - }) - } - - #[test] - fn search_vcf() { - vcf_with_local_storage(|storage| { - let htsget = HtsGetFromStorage::new(storage); - let filename = "spec-v4.3"; - let query = Query::new(filename).with_format(Format::Vcf); - let response = htsget.search(query); - println!("{:#?}", response); - - let expected_response = Ok(Response::new( - Format::Vcf, - vec![Url::new(vcf_expected_url(htsget.storage(), filename)) - .with_headers(Headers::default().with_header("Range", "bytes=0-823"))], - )); - assert_eq!(response, expected_response) - }) - } -} diff --git a/htsget-search/src/htsget/blocking/vcf_search.rs b/htsget-search/src/htsget/blocking/vcf_search.rs index 582b57ba6..b8cf45604 100644 --- a/htsget-search/src/htsget/blocking/vcf_search.rs +++ b/htsget-search/src/htsget/blocking/vcf_search.rs @@ -150,7 +150,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Vcf, - vec![Url::new(expected_url(&storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-3367"))], )); assert_eq!(response, expected_response) @@ -168,7 +168,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Vcf, - vec![Url::new(expected_url(&storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-823"))], )); assert_eq!(response, expected_response) @@ -189,7 +189,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Vcf, - vec![Url::new(expected_url(&storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-3367"))], )); assert_eq!(response, expected_response) @@ -224,7 +224,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Vcf, - vec![Url::new(expected_url(&storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-823")) .with_class(Class::Header)], )); @@ -238,16 +238,17 @@ pub mod tests { .parent() .unwrap() .join("data/vcf"); - test(LocalStorage::new(base_path, RegexResolver::new(".*", "$0").unwrap()).unwrap()) + test( + LocalStorage::new( + base_path, + "localhost/data", + RegexResolver::new(".*", "$0").unwrap(), + ) + .unwrap(), + ) } - pub fn expected_url(storage: &LocalStorage, name: &str) -> String { - format!( - "file://{}", - storage - .base_path() - .join(format!("{}.vcf.gz", name)) - .to_string_lossy() - ) + pub(crate) fn expected_url(name: &str) -> String { + format!("http://localhost/data/{}.vcf.gz", name) } } diff --git a/htsget-search/src/htsget/cram_search.rs b/htsget-search/src/htsget/cram_search.rs index 5bbb487f5..640f66e08 100644 --- a/htsget-search/src/htsget/cram_search.rs +++ b/htsget-search/src/htsget/cram_search.rs @@ -269,6 +269,8 @@ pub mod tests { use super::*; + const EXPECTED_URL: &str = "http://localhost/data/htsnexus_test_NA12878.cram"; + #[tokio::test] async fn search_all_reads() { with_local_storage(|storage| async move { @@ -279,7 +281,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, - vec![Url::new(expected_url(storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=6087-1627756"))], )); assert_eq!(response, expected_response) @@ -297,7 +299,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, - vec![Url::new(expected_url(storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=1280106-1627756"))], )); assert_eq!(response, expected_response) @@ -315,7 +317,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, - vec![Url::new(expected_url(storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=604231-1280106"))], )); assert_eq!(response, expected_response) @@ -336,7 +338,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, - vec![Url::new(expected_url(storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=6087-465709"))], )); assert_eq!(response, expected_response) @@ -357,7 +359,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, - vec![Url::new(expected_url(storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=6087-604231"))], )); assert_eq!(response, expected_response) @@ -375,7 +377,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, - vec![Url::new(expected_url(storage)) + vec![Url::new(EXPECTED_URL) .with_headers(Headers::default().with_header("Range", "bytes=26-6087")) .with_class(Class::Header)], )); @@ -395,18 +397,13 @@ pub mod tests { .unwrap() .join("data/cram"); test(Arc::new( - LocalStorage::new(base_path, RegexResolver::new(".*", "$0").unwrap()).unwrap(), + LocalStorage::new( + base_path, + "localhost/data", + RegexResolver::new(".*", "$0").unwrap(), + ) + .unwrap(), )) .await } - - pub(crate) fn expected_url(storage: Arc) -> String { - format!( - "file://{}", - storage - .base_path() - .join("htsnexus_test_NA12878.cram") - .to_string_lossy() - ) - } } diff --git a/htsget-search/src/htsget/from_storage.rs b/htsget-search/src/htsget/from_storage.rs index a232620c1..b47f296e3 100644 --- a/htsget-search/src/htsget/from_storage.rs +++ b/htsget-search/src/htsget/from_storage.rs @@ -59,53 +59,3 @@ impl HtsGetFromStorage { Arc::clone(&self.storage_ref) } } - -#[cfg(test)] -mod tests { - use crate::htsget::bam_search::tests::{ - expected_url as bam_expected_url, with_local_storage as with_bam_local_storage, - }; - use crate::htsget::vcf_search::tests::{ - expected_url as vcf_expected_url, with_local_storage as with_vcf_local_storage, - }; - use crate::htsget::{Headers, Url}; - - use super::*; - - // #[tokio::test] - // async fn search_bam() { - // with_bam_local_storage(|storage| async move { - // let htsget = HtsGetFromStorage::new(Arc::try_unwrap(storage).unwrap()); - // let query = Query::new("htsnexus_test_NA12878").with_format(Format::Bam); - // let response = htsget.search(query).await; - // println!("{:#?}", response); - - // let expected_response = Ok(Response::new( - // Format::Bam, - // vec![Url::new(bam_expected_url)] - // .with_headers(Headers::default().with_header("Range", "bytes=4668-2596799"))] - // ); - // assert_eq!(response, expected_response) - // }) - // .await; - // } - - // #[tokio::test] - // async fn search_vcf() { - // with_vcf_local_storage(|storage| async move { - // let htsget = HtsGetFromStorage::new(Arc::try_unwrap(storage).unwrap()); - // let filename = "spec-v4.3"; - // let query = Query::new(filename).with_format(Format::Vcf); - // let response = htsget.search(query).await; - // println!("{:#?}", response); - - // let expected_response = Ok(Response::new( - // Format::Vcf, - // vec![Url::new(vcf_expected_url(filename)) - // .with_headers(Headers::default().with_header("Range", "bytes=0-823"))], - // )); - // assert_eq!(response, expected_response) - // }) - // .await; - // } -} diff --git a/htsget-search/src/htsget/vcf_search.rs b/htsget-search/src/htsget/vcf_search.rs index 0aef647f2..6ce8f17be 100644 --- a/htsget-search/src/htsget/vcf_search.rs +++ b/htsget-search/src/htsget/vcf_search.rs @@ -171,7 +171,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Vcf, - vec![Url::new(expected_url(storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-3367"))], )); assert_eq!(response, expected_response) @@ -190,7 +190,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Vcf, - vec![Url::new(expected_url(storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-823"))], )); assert_eq!(response, expected_response) @@ -212,7 +212,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Vcf, - vec![Url::new(expected_url(storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-3367"))], )); assert_eq!(response, expected_response) @@ -249,7 +249,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Vcf, - vec![Url::new(expected_url(storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-823")) .with_class(Class::Header)], )); @@ -269,18 +269,17 @@ pub mod tests { .unwrap() .join("data/vcf"); test(Arc::new( - LocalStorage::new(base_path, RegexResolver::new(".*", "$0").unwrap()).unwrap(), + LocalStorage::new( + base_path, + "localhost/data", + RegexResolver::new(".*", "$0").unwrap(), + ) + .unwrap(), )) .await } - pub(crate) fn expected_url(storage: Arc, name: &str) -> String { - format!( - "file://{}", - storage - .base_path() - .join(format!("{}.vcf.gz", name)) - .to_string_lossy() - ) + pub(crate) fn expected_url(name: &str) -> String { + format!("http://localhost/data/{}.vcf.gz", name) } } diff --git a/htsget-search/src/storage/blocking/local.rs b/htsget-search/src/storage/blocking/local.rs index d1a03d458..11bd6ea73 100644 --- a/htsget-search/src/storage/blocking/local.rs +++ b/htsget-search/src/storage/blocking/local.rs @@ -11,11 +11,16 @@ use htsget_id_resolver::{HtsGetIdResolver, RegexResolver}; #[derive(Debug)] pub struct LocalStorage { base_path: PathBuf, + address: String, id_resolver: RegexResolver, } impl LocalStorage { - pub fn new>(base_path: P, id_resolver: RegexResolver) -> Result { + pub fn new>( + base_path: P, + address: impl Into, + id_resolver: RegexResolver, + ) -> Result { base_path .as_ref() .to_path_buf() @@ -23,6 +28,7 @@ impl LocalStorage { .map_err(|_| StorageError::NotFound(base_path.as_ref().to_string_lossy().to_string())) .map(|canonicalized_base_path| Self { base_path: canonicalized_base_path, + address: address.into(), id_resolver, }) } @@ -75,9 +81,17 @@ impl Storage for LocalStorage { .map(|end| end.to_string()) .unwrap_or_else(|| "".to_string()); - // TODO file:// is not allowed by the spec. We should consider including an static http server for the base_path - let path = self.get_path_from_key(key)?; - let url = Url::new(format!("file://{}", path.to_string_lossy())); + let captured_key = key.as_ref().to_string(); + let path = self + .get_path_from_key(key)? + .strip_prefix(&self.base_path) + .map_err(|_| StorageError::NotFound(captured_key))? + .to_owned(); + let url = Url::new(format!( + "http://{}/{}", + self.address, + path.to_string_lossy() + )); let url = if range_start.is_empty() && range_end.is_empty() { url } else { @@ -201,10 +215,7 @@ mod tests { fn url_of_existing_key() { with_local_storage(|storage| { let result = storage.url("folder/../key1", UrlOptions::default()); - let expected = Url::new(format!( - "file://{}", - storage.base_path().join("key1").to_string_lossy() - )); + let expected = Url::new("http://localhost/data/key1"); assert_eq!(result, Ok(expected)); }); } @@ -219,11 +230,8 @@ mod tests { assert_eq!( result, Ok( - Url::new(format!( - "file://{}", - storage.base_path().join("key1").to_string_lossy() - )) - .with_headers(Headers::default().with_header("Range", "bytes=7-9")) + Url::new("http://localhost/data/key1") + .with_headers(Headers::default().with_header("Range", "bytes=7-9")) ) ); }); @@ -239,11 +247,8 @@ mod tests { assert_eq!( result, Ok( - Url::new(format!( - "file://{}", - storage.base_path().join("key1").to_string_lossy() - )) - .with_headers(Headers::default().with_header("Range", "bytes=7-")) + Url::new("http://localhost/data/key1") + .with_headers(Headers::default().with_header("Range", "bytes=7-")) ) ); }); @@ -269,6 +274,13 @@ mod tests { .unwrap() .write_all(b"value2") .unwrap(); - test(LocalStorage::new(base_path.path(), RegexResolver::new(".*", "$0").unwrap()).unwrap()) + test( + LocalStorage::new( + base_path.path(), + "localhost/data", + RegexResolver::new(".*", "$0").unwrap(), + ) + .unwrap(), + ) } } diff --git a/htsget-search/src/storage/local.rs b/htsget-search/src/storage/local.rs index aa6597c19..4fac23446 100644 --- a/htsget-search/src/storage/local.rs +++ b/htsget-search/src/storage/local.rs @@ -8,7 +8,7 @@ use async_trait::async_trait; use crate::htsget::Url; use crate::storage; use crate::storage::async_storage::AsyncStorage; -use crate::storage::blocking::local::LocalStorage; +pub use crate::storage::blocking::local::LocalStorage; use super::{GetOptions, Result, StorageError, UrlOptions}; @@ -97,10 +97,13 @@ mod tests { .map(|path| path.to_string_lossy().to_string()); assert_eq!( result, - Ok(format!( - "{}", - storage.base_path().join("key1").to_string_lossy() - )) + Ok( + storage + .base_path() + .join("key1") + .to_string_lossy() + .to_string() + ) ); }) .await; @@ -146,10 +149,7 @@ mod tests { async fn url_of_existing_key() { with_local_storage(|storage| async move { let result = AsyncStorage::url(&storage, "folder/../key1", UrlOptions::default()).await; - let expected = Url::new(format!( - "file://{}", - storage.base_path().join("key1").to_string_lossy() - )); + let expected = Url::new("http://localhost/data/key1"); assert_eq!(result, Ok(expected)); }) .await; @@ -167,11 +167,8 @@ mod tests { assert_eq!( result, Ok( - Url::new(format!( - "file://{}", - storage.base_path().join("key1").to_string_lossy() - )) - .with_headers(Headers::default().with_header("Range", "bytes=7-9")) + Url::new("http://localhost/data/key1") + .with_headers(Headers::default().with_header("Range", "bytes=7-9")) ) ); }) @@ -190,11 +187,8 @@ mod tests { assert_eq!( result, Ok( - Url::new(format!( - "file://{}", - storage.base_path().join("key1").to_string_lossy() - )) - .with_headers(Headers::default().with_header("Range", "bytes=7-")) + Url::new("http://localhost/data/key1") + .with_headers(Headers::default().with_header("Range", "bytes=7-")) ) ); }) @@ -230,7 +224,14 @@ mod tests { .write_all(b"value2") .await .unwrap(); - test(LocalStorage::new(base_path.path(), RegexResolver::new(".*", "$0").unwrap()).unwrap()) - .await + test( + LocalStorage::new( + base_path.path(), + "localhost/data", + RegexResolver::new(".*", "$0").unwrap(), + ) + .unwrap(), + ) + .await } -} \ No newline at end of file +} From 838f78e53ad3f68afa4e40d25eddc3c03edcb375 Mon Sep 17 00:00:00 2001 From: CastilloDel Date: Tue, 28 Sep 2021 16:14:13 +0100 Subject: [PATCH 36/50] Fix benchmark so it can run as blocking --- htsget-search/benches/benchmark.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/htsget-search/benches/benchmark.rs b/htsget-search/benches/benchmark.rs index 4b2beda87..2a6292373 100644 --- a/htsget-search/benches/benchmark.rs +++ b/htsget-search/benches/benchmark.rs @@ -1,9 +1,8 @@ use criterion::{criterion_group, criterion_main, Criterion}; use htsget_id_resolver::RegexResolver; use htsget_search::{ - htsget::{ - from_storage::HtsGetFromStorage, Class, Fields, Format, HtsGet, HtsGetError, Query, Tags, - }, + htsget::blocking::{from_storage::HtsGetFromStorage, HtsGet}, + htsget::{Class, Fields, Format, HtsGetError, Query, Tags}, storage::local::LocalStorage, }; use std::time::Duration; From 50d78de8d8e2e402cc7ac549cef2ccda66b36331 Mon Sep 17 00:00:00 2001 From: Marko Malenic Date: Tue, 24 May 2022 17:24:13 +1000 Subject: [PATCH 37/50] Refactor benchmarks, fix a few errors, rearrange some tests. --- data/events/event_get.json | 2 +- data/events/event_parameterized_get.json | 2 +- data/events/event_parameterized_post.json | 2 +- ...event_parameterized_post_class_header.json | 2 +- data/events/event_post.json | 2 +- htsget-http-actix/Cargo.toml | 9 +- .../benches/docker-htsget-refserver.sh | 4 +- .../benches/htsget-refserver-config.json | 4 +- .../benches/request-benchmark.rs | 217 ++++++++++-------- htsget-http-actix/src/lib.rs | 3 +- htsget-http-actix/src/main.rs | 3 +- htsget-http-core/src/json_response.rs | 12 +- htsget-http-lambda/src/lib.rs | 14 +- htsget-search/Cargo.toml | 8 +- htsget-search/benches/benchmark.rs | 173 +++++++------- htsget-search/src/htsget/mod.rs | 6 +- htsget-search/src/storage/axum_server.rs | 10 +- htsget-test-utils/Cargo.toml | 2 +- htsget-test-utils/src/lib.rs | 6 +- htsget-test-utils/src/server_tests.rs | 12 +- 20 files changed, 262 insertions(+), 231 deletions(-) diff --git a/data/events/event_get.json b/data/events/event_get.json index 15ae11f11..1bebd8cd5 100644 --- a/data/events/event_get.json +++ b/data/events/event_get.json @@ -1,6 +1,6 @@ { "httpMethod": "GET", - "path": "/variants/data/vcf/sample1-bcbio-cancer", + "path": "/variants/vcf/sample1-bcbio-cancer", "body": null, "resource": "/{proxy+}", diff --git a/data/events/event_parameterized_get.json b/data/events/event_parameterized_get.json index d57ad25e1..6432abdc5 100644 --- a/data/events/event_parameterized_get.json +++ b/data/events/event_parameterized_get.json @@ -1,6 +1,6 @@ { "httpMethod": "GET", - "path": "/variants/data/vcf/sample1-bcbio-cancer", + "path": "/variants/vcf/sample1-bcbio-cancer", "body": null, "resource": "/{proxy+}", diff --git a/data/events/event_parameterized_post.json b/data/events/event_parameterized_post.json index a8d5a0c25..3630f36bf 100644 --- a/data/events/event_parameterized_post.json +++ b/data/events/event_parameterized_post.json @@ -1,6 +1,6 @@ { "httpMethod": "POST", - "path": "/variants/data/vcf/sample1-bcbio-cancer", + "path": "/variants/vcf/sample1-bcbio-cancer", "body": "{\"format\": \"VCF\", \"regions\": [{\"referenceName\": \"chrM\"}]}", "resource": "/{proxy+}", diff --git a/data/events/event_parameterized_post_class_header.json b/data/events/event_parameterized_post_class_header.json index 9bae273f2..06b365407 100644 --- a/data/events/event_parameterized_post_class_header.json +++ b/data/events/event_parameterized_post_class_header.json @@ -1,6 +1,6 @@ { "httpMethod": "POST", - "path": "/variants/data/vcf/sample1-bcbio-cancer", + "path": "/variants/vcf/sample1-bcbio-cancer", "body": "{\"format\": \"VCF\", \"class\": \"header\", \"regions\": [{\"referenceName\": \"chrM\"}]}", "resource": "/{proxy+}", diff --git a/data/events/event_post.json b/data/events/event_post.json index ffbeb79d0..9787ba93a 100644 --- a/data/events/event_post.json +++ b/data/events/event_post.json @@ -1,6 +1,6 @@ { "httpMethod": "POST", - "path": "/variants/data/vcf/sample1-bcbio-cancer", + "path": "/variants/vcf/sample1-bcbio-cancer", "body": null, "resource": "/{proxy+}", diff --git a/htsget-http-actix/Cargo.toml b/htsget-http-actix/Cargo.toml index 27e03ff86..b27ca0756 100644 --- a/htsget-http-actix/Cargo.toml +++ b/htsget-http-actix/Cargo.toml @@ -23,4 +23,11 @@ tracing = "0.1" [dev-dependencies] htsget-test-utils = { path = "../htsget-test-utils", default-features = false } -async-trait = "0.1" \ No newline at end of file +async-trait = "0.1" + +criterion = { version = "0.3", features = ["async_tokio"] } +reqwest = { version = "0.11", features = ["json"] } + +[[bench]] +name = "request-benchmark" +harness = false \ No newline at end of file diff --git a/htsget-http-actix/benches/docker-htsget-refserver.sh b/htsget-http-actix/benches/docker-htsget-refserver.sh index 6f3c4571d..8ac9c0e3d 100755 --- a/htsget-http-actix/benches/docker-htsget-refserver.sh +++ b/htsget-http-actix/benches/docker-htsget-refserver.sh @@ -1,2 +1,2 @@ -docker image pull ga4gh/htsget-refserver:1.4.0 -docker container run -d -p 8081:3000 -v $(pwd)/../data:/data -v $(pwd)/benches:/config ga4gh/htsget-refserver:1.4.0 ./htsget-refserver -config /config/htsget-refserver-config.json \ No newline at end of file +docker image pull ga4gh/htsget-refserver:1.5.0 +docker container run -d -p 8082:3000 -v $(pwd)/../data:/data -v $(pwd)/benches:/config ga4gh/htsget-refserver:1.5.0 ./htsget-refserver -config /config/htsget-refserver-config.json \ No newline at end of file diff --git a/htsget-http-actix/benches/htsget-refserver-config.json b/htsget-http-actix/benches/htsget-refserver-config.json index f339feaa3..395d4f54c 100644 --- a/htsget-http-actix/benches/htsget-refserver-config.json +++ b/htsget-http-actix/benches/htsget-refserver-config.json @@ -1,8 +1,8 @@ { "htsgetConfig": { "props": { - "port": 8081, - "host": "http://localhost:8081/" + "port": 8082, + "host": "http://localhost:8082/" }, "reads": { "dataSourceRegistry": { diff --git a/htsget-http-actix/benches/request-benchmark.rs b/htsget-http-actix/benches/request-benchmark.rs index d2528933b..66400f69a 100644 --- a/htsget-http-actix/benches/request-benchmark.rs +++ b/htsget-http-actix/benches/request-benchmark.rs @@ -1,68 +1,88 @@ use criterion::measurement::WallTime; use criterion::{criterion_group, criterion_main, BenchmarkGroup, Criterion}; +use futures_util::future::join_all; use htsget_http_core::{JsonResponse, PostRequest, Region}; -use reqwest::{blocking::Client, Error as ActixError}; -use serde::Serialize; -use std::collections::HashMap; -use std::{convert::TryInto, time::Duration}; +use htsget_test_utils::server_tests::{default_dir, default_test_config}; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use std::borrow::Borrow; +use std::{convert::TryInto, fs, time::Duration}; +use std::path::PathBuf; +use tokio::runtime::Runtime; #[derive(Serialize)] struct Empty {} +#[derive(Deserialize)] +struct RefserverConfig { + #[serde(rename = "htsgetConfig")] + htsget_config: RefserverProps, +} + +#[derive(Deserialize)] +struct RefserverProps { + props: RefserverAddr, +} + +#[derive(Deserialize)] +struct RefserverAddr { + #[serde(rename = "port")] + _port: u64, + host: String, +} + const BENCHMARK_DURATION_SECONDS: u64 = 15; const NUMBER_OF_EXECUTIONS: usize = 150; -const HTSGET_RS_URL: &str = "http://localhost:8080/reads/data/bam/htsnexus_test_NA12878"; -const HTSGET_REFSERVER_URL: &str = "http://localhost:8081/reads/htsnexus_test_NA12878"; -const HTSGET_RS_VCF_URL: &str = "http://localhost:8080/variants/data/vcf/sample1-bcbio-cancer"; -const HTSGET_REFSERVER_VCF_URL: &str = "http://localhost:8081/variants/sample1-bcbio-cancer"; -const HTSGET_RS_BIG_VCF_URL: &str = - "http://localhost:8080/variants/data/vcf/internationalgenomesample"; -const HTSGET_REFSERVER_BIG_VCF_URL: &str = - "http://localhost:8081/variants/internationalgenomesample"; - -fn request(url: &str, json_content: &impl Serialize) -> Result { +async fn request(url: reqwest::Url, json_content: &impl Serialize) -> reqwest::Result { let client = Client::new(); - let response: JsonResponse = client.get(url).json(json_content).send()?.json()?; + let response: JsonResponse = client + .get(url) + .json(json_content) + .send() + .await? + .json() + .await?; Ok( - response - .htsget - .urls - .iter() - .map(|json_url| { - Ok( - client - .get(&json_url.url) - .headers( - json_url - .headers - .as_ref() - .unwrap_or(&HashMap::new()) - .try_into() - .unwrap(), - ) - .send()? - .bytes()? - .len(), - ) - }) - .collect::, ActixError>>()? - .into_iter() - .sum(), + join_all(response.htsget.urls.iter().map(|json_url| async { + client + .get(&json_url.url) + .headers(json_url.headers.borrow().try_into().unwrap()) + .send() + .await + .unwrap() + .bytes() + .await + .unwrap() + .len() + })) + .await + .into_iter() + .sum(), ) } -fn bench_request( +fn format_url(url: &str, path: &str) -> reqwest::Url { + reqwest::Url::parse(url).expect("Invalid url").join(path).expect("Invalid url") +} + +fn bench_pair( group: &mut BenchmarkGroup, name: &str, - url: &str, + htsget_url: reqwest::Url, + refserver_url: reqwest::Url, json_content: &impl Serialize, ) { - println!( - "\n\nDownload size: {} bytes", - request(url, json_content).expect("Error during the request") - ); - group.bench_function(name, |b| b.iter(|| request(url, json_content))); + group.bench_with_input(format!("{} {}", name, "htsget-rs"), &htsget_url, |b, input| { + b.to_async(Runtime::new().unwrap()).iter(|| request(input.clone(), json_content)) + }); + group.bench_with_input(format!("{} {}", name, "htsget-refserver"), &refserver_url, |b, input| { + b.to_async(Runtime::new().unwrap()).iter(|| request(input.clone(), json_content)) + }); +} + +fn start_htsget_rs(config: &Config) { + } fn criterion_benchmark(c: &mut Criterion) { @@ -71,16 +91,23 @@ fn criterion_benchmark(c: &mut Criterion) { .sample_size(NUMBER_OF_EXECUTIONS) .measurement_time(Duration::from_secs(BENCHMARK_DURATION_SECONDS)); - bench_request( - &mut group, - "[LIGHT] htsget-rs simple request", - HTSGET_RS_URL, - &Empty {}, - ); - bench_request( + let config = default_test_config(); + let refserver_config: RefserverConfig = + serde_json::from_str(&fs::read_to_string(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("benches").join("htsget-refserver-config.json")).unwrap()).unwrap(); + + let htsget_rs_url = format!("https://{}", config.addr); + let htsget_refserver_url = refserver_config.htsget_config.props.host; + bench_pair( &mut group, - "[LIGHT] htsget-refserver simple request", - HTSGET_REFSERVER_URL, + "[LIGHT] simple request", + format_url( + &htsget_rs_url, + "reads/bam/htsnexus_test_NA12878", + ), + format_url( + &htsget_refserver_url, + "reads/htsnexus_test_NA12878", + ), &Empty {}, ); @@ -96,16 +123,17 @@ fn criterion_benchmark(c: &mut Criterion) { end: None, }]), }; - bench_request( - &mut group, - "[LIGHT] htsget-rs with region", - HTSGET_RS_URL, - &json_content, - ); - bench_request( + bench_pair( &mut group, - "[LIGHT] htsget-refserver with region", - HTSGET_REFSERVER_URL, + "[LIGHT] with region", + format_url( + &htsget_rs_url, + "reads/bam/htsnexus_test_NA12878", + ), + format_url( + &htsget_refserver_url, + "reads/htsnexus_test_NA12878", + ), &json_content, ); @@ -128,16 +156,17 @@ fn criterion_benchmark(c: &mut Criterion) { }, ]), }; - bench_request( - &mut group, - "[LIGHT] htsget-rs with two regions", - HTSGET_RS_URL, - &json_content, - ); - bench_request( + bench_pair( &mut group, - "[LIGHT] htsget-refserver with two regions", - HTSGET_REFSERVER_URL, + "[LIGHT] with two regions", + format_url( + &htsget_rs_url, + "reads/bam/htsnexus_test_NA12878", + ), + format_url( + &htsget_refserver_url, + "reads/htsnexus_test_NA12878", + ), &json_content, ); @@ -153,20 +182,19 @@ fn criterion_benchmark(c: &mut Criterion) { end: Some(153), }]), }; - bench_request( + bench_pair( &mut group, - "[LIGHT] htsget-rs with VCF", - HTSGET_RS_VCF_URL, + "[LIGHT] with VCF", + format_url( + &htsget_rs_url, + "variants/vcf/sample1-bcbio-cancer", + ), + format_url( + &htsget_refserver_url, + "variants/sample1-bcbio-cancer", + ), &json_content, ); - bench_request( - &mut group, - "[LIGHT] htsget-refserver with VCF", - HTSGET_REFSERVER_VCF_URL, - &json_content, - ); - - // The following ones are HEAVY requests let json_content = PostRequest { format: None, @@ -180,16 +208,17 @@ fn criterion_benchmark(c: &mut Criterion) { end: None, }]), }; - bench_request( - &mut group, - "[HEAVY] htsget-rs big VCF file", - HTSGET_RS_BIG_VCF_URL, - &json_content, - ); - bench_request( + bench_pair( &mut group, - "[HEAVY] htsget-refserver big VCF file", - HTSGET_REFSERVER_BIG_VCF_URL, + "[HEAVY] with big VCF", + format_url( + &htsget_rs_url, + "variants/vcf/internationalgenomesample", + ), + format_url( + &htsget_refserver_url, + "variants/internationalgenomesample", + ), &json_content, ); @@ -197,4 +226,4 @@ fn criterion_benchmark(c: &mut Criterion) { } criterion_group!(benches, criterion_benchmark); -criterion_main!(benches); +criterion_main!(benches); \ No newline at end of file diff --git a/htsget-http-actix/src/lib.rs b/htsget-http-actix/src/lib.rs index 88a61c73b..864e52e01 100644 --- a/htsget-http-actix/src/lib.rs +++ b/htsget-http-actix/src/lib.rs @@ -67,7 +67,6 @@ pub fn run_server( #[cfg(test)] mod tests { - use actix_web::web::Bytes; use actix_web::{test, web, App}; use async_trait::async_trait; @@ -143,7 +142,7 @@ mod tests { .await; let response = request.0.send_request(&app).await; let status: u16 = response.status().into(); - let bytes: Bytes = test::read_body(response).await; + let bytes = test::read_body(response).await.to_vec(); TestResponse::new(status, bytes) } } diff --git a/htsget-http-actix/src/main.rs b/htsget-http-actix/src/main.rs index 8962ca795..c82cf52ce 100644 --- a/htsget-http-actix/src/main.rs +++ b/htsget-http-actix/src/main.rs @@ -16,6 +16,7 @@ async fn main() -> std::io::Result<()> { } let config = Config::from_env()?; + println!("{:?}", config); match config.storage_type { StorageType::LocalStorage => local_storage_server(config).await, @@ -25,7 +26,7 @@ async fn main() -> std::io::Result<()> { } async fn local_storage_server(config: Config) -> std::io::Result<()> { - let formatter = HttpsFormatter::from(config.addr); + let formatter = HttpsFormatter::from(config.ticket_server_addr); let mut local_server = formatter.bind_axum_server().await?; let searcher = HtsGetFromStorage::local_from( diff --git a/htsget-http-core/src/json_response.rs b/htsget-http-core/src/json_response.rs index 33b4500a9..3a9fb898e 100644 --- a/htsget-http-core/src/json_response.rs +++ b/htsget-http-core/src/json_response.rs @@ -8,7 +8,7 @@ use htsget_search::htsget::{Class, Response, Url}; /// so it's trivial to convert to JSON. #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct JsonResponse { - htsget: HtsGetResponse, + pub htsget: HtsGetResponse, } impl JsonResponse { @@ -23,8 +23,8 @@ impl JsonResponse { /// on its own, but with [JsonResponse] #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct HtsGetResponse { - format: String, - urls: Vec, + pub format: String, + pub urls: Vec, } impl HtsGetResponse { @@ -39,9 +39,9 @@ impl HtsGetResponse { /// on its own, but with [JsonResponse] #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct JsonUrl { - url: String, - headers: HashMap, - class: String, + pub url: String, + pub headers: HashMap, + pub class: String, } impl JsonUrl { diff --git a/htsget-http-lambda/src/lib.rs b/htsget-http-lambda/src/lib.rs index 48005fdff..65978ff90 100644 --- a/htsget-http-lambda/src/lib.rs +++ b/htsget-http-lambda/src/lib.rs @@ -309,20 +309,20 @@ mod tests { #[tokio::test] async fn test_get_from_file() { let config = default_test_config(); - endpoint_from_file("data/events/event_get.json", Class::Body, &config).await; + endpoint_from_file("events/event_get.json", Class::Body, &config).await; } #[tokio::test] async fn test_post_from_file() { let config = default_test_config(); - endpoint_from_file("data/events/event_post.json", Class::Body, &config).await; + endpoint_from_file("events/event_post.json", Class::Body, &config).await; } #[tokio::test] async fn test_parameterized_get_from_file() { let config = default_test_config(); endpoint_from_file( - "data/events/event_parameterized_get.json", + "events/event_parameterized_get.json", Class::Header, &config, ) @@ -333,7 +333,7 @@ mod tests { async fn test_parameterized_post_from_file() { let config = default_test_config(); endpoint_from_file( - "data/events/event_parameterized_post.json", + "events/event_parameterized_post.json", Class::Body, &config, ) @@ -344,7 +344,7 @@ mod tests { async fn test_parameterized_post_class_header_from_file() { let config = default_test_config(); endpoint_from_file( - "data/events/event_parameterized_post_class_header.json", + "events/event_parameterized_post_class_header.json", Class::Header, &config, ) @@ -354,7 +354,7 @@ mod tests { #[tokio::test] async fn test_service_info_from_file() { let config = default_test_config(); - service_info_from_file("data/events/event_service_info.json", &config).await; + service_info_from_file("events/event_service_info.json", &config).await; } #[tokio::test] @@ -570,7 +570,7 @@ mod tests { .await .expect("Failed to route request."); let status: u16 = response.status().into(); - let body = response.body().to_vec().into(); + let body = response.body().to_vec(); Response::new(status, body) } } diff --git a/htsget-search/Cargo.toml b/htsget-search/Cargo.toml index 292e56a82..d6df24f9e 100644 --- a/htsget-search/Cargo.toml +++ b/htsget-search/Cargo.toml @@ -38,6 +38,7 @@ aws-sdk-s3 = { version = "0.9", optional = true } aws-config = { version = "0.9", optional = true } [dev-dependencies] +rcgen = "0.9" tempfile = "3.3" # Aws S3 storage dependencies. @@ -47,5 +48,10 @@ http = "0.2" aws-types = { version = "0.9", features = ["hardcoded-credentials"] } # Axum server dependencies -rcgen = "0.9" hyper-tls = "0.5" + +criterion = { version = "0.3", features = ["async_tokio"] } + +[[bench]] +name = "benchmark" +harness = false \ No newline at end of file diff --git a/htsget-search/benches/benchmark.rs b/htsget-search/benches/benchmark.rs index 2a6292373..e948cdf03 100644 --- a/htsget-search/benches/benchmark.rs +++ b/htsget-search/benches/benchmark.rs @@ -1,109 +1,96 @@ -use criterion::{criterion_group, criterion_main, Criterion}; -use htsget_id_resolver::RegexResolver; -use htsget_search::{ - htsget::blocking::{from_storage::HtsGetFromStorage, HtsGet}, - htsget::{Class, Fields, Format, HtsGetError, Query, Tags}, - storage::local::LocalStorage, -}; +use criterion::measurement::WallTime; +use criterion::{criterion_group, criterion_main, BenchmarkGroup, Criterion}; +use htsget_config::regex_resolver::RegexResolver; +use htsget_search::htsget::from_storage::HtsGetFromStorage; +use htsget_search::htsget::Class::Header; +use htsget_search::htsget::Format::{Bam, Bcf, Cram, Vcf}; +use htsget_search::htsget::HtsGet; +use htsget_search::htsget::{HtsGetError, Query}; +use htsget_search::storage::axum_server::HttpsFormatter; +use std::net::SocketAddr; use std::time::Duration; +use tokio::runtime::Runtime; -const BENCHMARK_DURATION_SECONDS: u64 = 5; +const BENCHMARK_DURATION_SECONDS: u64 = 15; const NUMBER_OF_EXECUTIONS: usize = 150; -fn perform_query(query: Query) -> Result<(), HtsGetError> { - let htsget = HtsGetFromStorage::new( - LocalStorage::new( - "../data", - "localhost", - RegexResolver::new(".*", "$0").unwrap(), - ) - .unwrap(), - ); - htsget.search(query)?; +async fn perform_query(query: Query) -> Result<(), HtsGetError> { + let htsget = HtsGetFromStorage::local_from( + "../data", + RegexResolver::new(".*", "$0").unwrap(), + HttpsFormatter::from( + "127.0.0.1:8081" + .parse::() + .expect("Expected valid address."), + ), + )?; + + htsget.search(query).await?; Ok(()) } +fn bench_query(group: &mut BenchmarkGroup, name: &str, query: Query) +{ + group.bench_with_input(name, &query, |b, input| b.to_async(Runtime::new().unwrap()).iter(|| perform_query(input.clone()))); +} + fn criterion_benchmark(c: &mut Criterion) { let mut group = c.benchmark_group("Queries"); group .sample_size(NUMBER_OF_EXECUTIONS) .measurement_time(Duration::from_secs(BENCHMARK_DURATION_SECONDS)); - group.bench_function("[LIGHT] Simple bam query", |b| { - b.iter(|| { - perform_query(Query { - id: "bam/htsnexus_test_NA12878".to_string(), - format: None, - class: Class::Body, - reference_name: None, - start: None, - end: None, - fields: Fields::All, - tags: Tags::All, - no_tags: None, - }) - }) - }); - group.bench_function("[LIGHT] Bam query", |b| { - b.iter(|| { - perform_query(Query { - id: "bam/htsnexus_test_NA12878".to_string(), - format: None, - class: Class::Body, - reference_name: Some("11".to_string()), - start: Some(4999977), - end: Some(5008321), - fields: Fields::All, - tags: Tags::All, - no_tags: None, - }) - }) - }); - group.bench_function("[LIGHT] VCF query", |b| { - b.iter(|| { - perform_query(Query { - id: "vcf/sample1-bcbio-cancer".to_string(), - format: None, - class: Class::Body, - reference_name: Some("chrM".to_string()), - start: Some(151), - end: Some(153), - fields: Fields::All, - tags: Tags::All, - no_tags: None, - }) - }) - }); - group.bench_function("[LIGHT] BCF query", |b| { - b.iter(|| { - perform_query(Query { - id: "bcf/sample1-bcbio-cancer".to_string(), - format: Some(Format::Bcf), - class: Class::Body, - reference_name: Some("chrM".to_string()), - start: Some(151), - end: Some(153), - fields: Fields::All, - tags: Tags::All, - no_tags: None, - }) - }) - }); - group.bench_function("[LIGHT] CRAM query", |b| { - b.iter(|| { - perform_query(Query { - id: "cram/htsnexus_test_NA12878".to_string(), - format: Some(Format::Cram), - class: Class::Body, - reference_name: Some("11".to_string()), - start: Some(4999977), - end: Some(5008321), - fields: Fields::All, - tags: Tags::All, - no_tags: None, - }) - }) - }); + bench_query(&mut group, "[LIGHT] Bam query all", + Query::new("bam/htsnexus_test_NA12878", Bam) + ); + bench_query(&mut group, "[LIGHT] Bam query specific", + Query::new("bam/htsnexus_test_NA12878", Bam) + .with_reference_name("11") + .with_start(4999977) + .with_end(5008321) + ); + bench_query(&mut group, "[LIGHT] Bam query header", + Query::new("bam/htsnexus_test_NA12878", Bam).with_class(Header) + ); + + bench_query(&mut group, "[LIGHT] Cram query all", + Query::new("cram/htsnexus_test_NA12878", Cram) + ); + bench_query(&mut group, "[LIGHT] Cram query specific", + Query::new("cram/htsnexus_test_NA12878", Cram) + .with_reference_name("11") + .with_start(4999977) + .with_end(5008321) + ); + bench_query(&mut group, "[LIGHT] Cram query header", + Query::new("cram/htsnexus_test_NA12878", Cram).with_class(Header) + ); + + bench_query(&mut group, "[LIGHT] Vcf query all", + Query::new("vcf/sample1-bcbio-cancer", Vcf) + ); + bench_query(&mut group, "[LIGHT] Vcf query specific", + Query::new("vcf/sample1-bcbio-cancer", Vcf) + .with_reference_name("chrM") + .with_start(151) + .with_end(153) + ); + bench_query(&mut group, "[LIGHT] Vcf query header", + Query::new("vcf/sample1-bcbio-cancer", Vcf).with_class(Header) + ); + + bench_query(&mut group, "[LIGHT] Bcf query all", + Query::new("bcf/sample1-bcbio-cancer", Bcf) + ); + bench_query(&mut group, "[LIGHT] Bcf query specific", + Query::new("bcf/sample1-bcbio-cancer", Bcf) + .with_reference_name("chrM") + .with_start(151) + .with_end(153) + ); + bench_query(&mut group, "[LIGHT] Bcf query header", + Query::new("bcf/sample1-bcbio-cancer", Bcf).with_class(Header) + ); group.finish(); } diff --git a/htsget-search/src/htsget/mod.rs b/htsget-search/src/htsget/mod.rs index 7f455caa9..c256aba89 100644 --- a/htsget-search/src/htsget/mod.rs +++ b/htsget-search/src/htsget/mod.rs @@ -129,7 +129,7 @@ impl From for HtsGetError { /// A query contains all the parameters that can be used when requesting /// a search for either of `reads` or `variants`. -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct Query { pub id: String, pub format: Format, @@ -259,7 +259,7 @@ pub enum Class { } /// Possible values for the fields parameter. -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub enum Fields { /// Include all fields All, @@ -268,7 +268,7 @@ pub enum Fields { } /// Possible values for the tags parameter. -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub enum Tags { /// Include all tags All, diff --git a/htsget-search/src/storage/axum_server.rs b/htsget-search/src/storage/axum_server.rs index dc27b6825..362ee88e0 100644 --- a/htsget-search/src/storage/axum_server.rs +++ b/htsget-search/src/storage/axum_server.rs @@ -163,6 +163,12 @@ mod tests { use super::*; + pub fn generate_test_certificates>(key_path: &P, cert_path: &P) { + let cert = generate_simple_self_signed(vec!["localhost".to_string()]).unwrap(); + fs::write(key_path, cert.serialize_private_key_pem()).unwrap(); + fs::write(cert_path, cert.serialize_pem().unwrap()).unwrap(); + } + #[tokio::test] async fn test_server() { let (_, base_path) = create_local_test_files().await; @@ -170,9 +176,7 @@ mod tests { let cert_path = base_path.path().join("cert.pem"); // Generate self-signed certificate. - let cert = generate_simple_self_signed(vec!["localhost".to_string()]).unwrap(); - fs::write(key_path.clone(), cert.serialize_private_key_pem()).unwrap(); - fs::write(cert_path.clone(), cert.serialize_pem().unwrap()).unwrap(); + generate_test_certificates(&key_path, &cert_path); // Read certificate. let mut buf = vec![]; diff --git a/htsget-test-utils/Cargo.toml b/htsget-test-utils/Cargo.toml index 2c3a33f7e..d1e42724d 100644 --- a/htsget-test-utils/Cargo.toml +++ b/htsget-test-utils/Cargo.toml @@ -18,4 +18,4 @@ mime = "0.3" serde_json = "1.0" serde = "1.0" envy = "0.4" -bytes = "1.1" \ No newline at end of file +serde_bytes = "0.11" \ No newline at end of file diff --git a/htsget-test-utils/src/lib.rs b/htsget-test-utils/src/lib.rs index 293efecff..55bca7489 100644 --- a/htsget-test-utils/src/lib.rs +++ b/htsget-test-utils/src/lib.rs @@ -1,5 +1,4 @@ use async_trait::async_trait; -use bytes::Bytes; use serde::de; use serde::Deserialize; @@ -26,11 +25,12 @@ impl> Header { pub struct Response { #[serde(alias = "statusCode")] pub status: u16, - pub body: Bytes, + #[serde(with = "serde_bytes")] + pub body: Vec, } impl Response { - pub fn new(status: u16, body: Bytes) -> Self { + pub fn new(status: u16, body: Vec) -> Self { Self { status, body } } diff --git a/htsget-test-utils/src/server_tests.rs b/htsget-test-utils/src/server_tests.rs index 0e967fade..d1499e8c0 100644 --- a/htsget-test-utils/src/server_tests.rs +++ b/htsget-test-utils/src/server_tests.rs @@ -37,7 +37,7 @@ pub async fn test_get(tester: &impl TestServer) { let request = tester .get_request() .method(Method::GET.to_string()) - .uri("/variants/data/vcf/sample1-bcbio-cancer"); + .uri("/variants/vcf/sample1-bcbio-cancer"); let response = tester.test_server(request).await; test_response(&response, tester.get_config(), Class::Body); } @@ -46,7 +46,7 @@ fn post_request(tester: &impl TestServer) -> T { tester .get_request() .method(Method::POST.to_string()) - .uri("/variants/data/vcf/sample1-bcbio-cancer") + .uri("/variants/vcf/sample1-bcbio-cancer") .insert_header(Header { name: http::header::CONTENT_TYPE.to_string(), value: mime::APPLICATION_JSON.to_string(), @@ -65,7 +65,7 @@ pub async fn test_parameterized_get(tester: &impl TestServer) let request = tester .get_request() .method(Method::GET.to_string()) - .uri("/variants/data/vcf/sample1-bcbio-cancer?format=VCF&class=header"); + .uri("/variants/vcf/sample1-bcbio-cancer?format=VCF&class=header"); let response = tester.test_server(request).await; test_response(&response, tester.get_config(), Class::Header); } @@ -111,7 +111,6 @@ pub fn expected_response(path: &Path, class: Class, url_path: String) -> JsonRes "{}{}", url_path, path - .join("data") .join("vcf") .join("sample1-bcbio-cancer.vcf.gz") .canonicalize() @@ -128,19 +127,18 @@ pub fn default_dir() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .parent() .unwrap() - .to_path_buf() .canonicalize() .unwrap() } /// Default config using the current cargo manifest directory. pub fn default_test_config() -> Config { - std::env::set_var("HTSGET_PATH", default_dir()); + std::env::set_var("HTSGET_PATH", default_dir().join("data")); Config::from_env().expect("Expected valid environment variables.") } /// Get the event associated with the file. pub fn get_test_file>(path: P) -> String { - let path = default_dir().join(path); + let path = default_dir().join("data").join(path); fs::read_to_string(path).expect("Failed to read file.") } From e7217e07e345c0a006966709301f6d17846d0b98 Mon Sep 17 00:00:00 2001 From: Marko Malenic Date: Wed, 25 May 2022 11:13:23 +1000 Subject: [PATCH 38/50] Refactor rcgen in axum server. --- htsget-http-actix/Cargo.toml | 1 + htsget-http-actix/benches/request-benchmark.rs | 8 ++++++-- htsget-search/src/storage/axum_server.rs | 17 ++++++++++------- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/htsget-http-actix/Cargo.toml b/htsget-http-actix/Cargo.toml index b27ca0756..8c74c3de8 100644 --- a/htsget-http-actix/Cargo.toml +++ b/htsget-http-actix/Cargo.toml @@ -27,6 +27,7 @@ async-trait = "0.1" criterion = { version = "0.3", features = ["async_tokio"] } reqwest = { version = "0.11", features = ["json"] } +tempdir = "0.3.7" [[bench]] name = "request-benchmark" diff --git a/htsget-http-actix/benches/request-benchmark.rs b/htsget-http-actix/benches/request-benchmark.rs index 66400f69a..132cc67e3 100644 --- a/htsget-http-actix/benches/request-benchmark.rs +++ b/htsget-http-actix/benches/request-benchmark.rs @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize}; use std::borrow::Borrow; use std::{convert::TryInto, fs, time::Duration}; use std::path::PathBuf; +use tempdir::TempDir; use tokio::runtime::Runtime; #[derive(Serialize)] @@ -81,8 +82,11 @@ fn bench_pair( }); } -fn start_htsget_rs(config: &Config) { - +fn start_htsget_rs() { + let rcgen_path = TempDir::new().unwrap(); + let key_path = base_path.path().join("key.pem"); + let cert_path = base_path.path().join("cert.pem"); + let project_dir = default_dir(); } fn criterion_benchmark(c: &mut Criterion) { diff --git a/htsget-search/src/storage/axum_server.rs b/htsget-search/src/storage/axum_server.rs index 362ee88e0..ea75a943b 100644 --- a/htsget-search/src/storage/axum_server.rs +++ b/htsget-search/src/storage/axum_server.rs @@ -151,6 +151,7 @@ impl UrlFormatter for HttpsFormatter { mod tests { use std::fs; use std::io::Read; + use std::path::PathBuf; use http::{Method, Request}; use hyper::client::HttpConnector; @@ -163,20 +164,22 @@ mod tests { use super::*; - pub fn generate_test_certificates>(key_path: &P, cert_path: &P) { + pub fn generate_test_certificates>(in_path: P, key_name: &str, cert_name: &str) -> (PathBuf, PathBuf) { + let key_path = in_path.as_ref().join("key.pem"); + let cert_path = in_path.as_ref().join("cert.pem"); + let cert = generate_simple_self_signed(vec!["localhost".to_string()]).unwrap(); - fs::write(key_path, cert.serialize_private_key_pem()).unwrap(); - fs::write(cert_path, cert.serialize_pem().unwrap()).unwrap(); + fs::write(&key_path, cert.serialize_private_key_pem()).unwrap(); + fs::write(&cert_path, cert.serialize_pem().unwrap()).unwrap(); + + (key_path, cert_path) } #[tokio::test] async fn test_server() { let (_, base_path) = create_local_test_files().await; - let key_path = base_path.path().join("key.pem"); - let cert_path = base_path.path().join("cert.pem"); - // Generate self-signed certificate. - generate_test_certificates(&key_path, &cert_path); + let (key_path, cert_path) = generate_test_certificates( base_path.path(), "key.pem", "cert.pem"); // Read certificate. let mut buf = vec![]; From 85a2d950ff5ef1de387bc4fdf4a3ba59628edcc9 Mon Sep 17 00:00:00 2001 From: Marko Malenic Date: Wed, 25 May 2022 20:04:59 +1000 Subject: [PATCH 39/50] Implement script files into rust code. --- htsget-http-actix/Cargo.toml | 5 +- .../benches/request-benchmark.rs | 243 ++++++++++++------ htsget-http-actix/src/lib.rs | 5 +- htsget-http-lambda/Cargo.toml | 2 +- htsget-http-lambda/src/lib.rs | 12 +- htsget-search/Cargo.toml | 2 +- htsget-search/benches/benchmark.rs | 80 ++++-- htsget-search/src/storage/axum_server.rs | 13 +- htsget-test-utils/Cargo.toml | 12 +- htsget-test-utils/src/lib.rs | 66 +---- htsget-test-utils/src/server_tests.rs | 62 ++++- htsget-test-utils/src/util.rs | 18 ++ 12 files changed, 314 insertions(+), 206 deletions(-) create mode 100644 htsget-test-utils/src/util.rs diff --git a/htsget-http-actix/Cargo.toml b/htsget-http-actix/Cargo.toml index b27ca0756..94f51b65e 100644 --- a/htsget-http-actix/Cargo.toml +++ b/htsget-http-actix/Cargo.toml @@ -22,11 +22,12 @@ tracing-actix-web = "0.5" tracing = "0.1" [dev-dependencies] -htsget-test-utils = { path = "../htsget-test-utils", default-features = false } +htsget-test-utils = { path = "../htsget-test-utils", features = ["server-tests"], default-features = false } async-trait = "0.1" criterion = { version = "0.3", features = ["async_tokio"] } -reqwest = { version = "0.11", features = ["json"] } +reqwest = { version = "0.11", features = ["json", "blocking"] } +tempfile = "3.3" [[bench]] name = "request-benchmark" diff --git a/htsget-http-actix/benches/request-benchmark.rs b/htsget-http-actix/benches/request-benchmark.rs index 66400f69a..3273e7902 100644 --- a/htsget-http-actix/benches/request-benchmark.rs +++ b/htsget-http-actix/benches/request-benchmark.rs @@ -1,14 +1,16 @@ use criterion::measurement::WallTime; use criterion::{criterion_group, criterion_main, BenchmarkGroup, Criterion}; -use futures_util::future::join_all; use htsget_http_core::{JsonResponse, PostRequest, Region}; use htsget_test_utils::server_tests::{default_dir, default_test_config}; -use reqwest::Client; +use htsget_test_utils::util::generate_test_certificates; +use reqwest::blocking::Client; use serde::{Deserialize, Serialize}; use std::borrow::Borrow; -use std::{convert::TryInto, fs, time::Duration}; use std::path::PathBuf; -use tokio::runtime::Runtime; +use std::process::{Child, Command}; +use std::thread::sleep; +use std::{convert::TryInto, fs, time::Duration}; +use tempfile::TempDir; #[derive(Serialize)] struct Empty {} @@ -26,44 +28,41 @@ struct RefserverProps { #[derive(Deserialize)] struct RefserverAddr { - #[serde(rename = "port")] - _port: u64, + port: u64, host: String, } +const REFSERVER_DOCKER_IMAGE: &str = "ga4gh/htsget-refserver:1.5.0"; const BENCHMARK_DURATION_SECONDS: u64 = 15; const NUMBER_OF_EXECUTIONS: usize = 150; -async fn request(url: reqwest::Url, json_content: &impl Serialize) -> reqwest::Result { +fn request(url: reqwest::Url, json_content: &impl Serialize) -> reqwest::Result { let client = Client::new(); - let response: JsonResponse = client - .get(url) - .json(json_content) - .send() - .await? - .json() - .await?; + let response: JsonResponse = client.get(url).json(json_content).send()?.json()?; Ok( - join_all(response.htsget.urls.iter().map(|json_url| async { - client - .get(&json_url.url) - .headers(json_url.headers.borrow().try_into().unwrap()) - .send() - .await - .unwrap() - .bytes() - .await - .unwrap() - .len() - })) - .await - .into_iter() - .sum(), + response + .htsget + .urls + .iter() + .map(|json_url| { + client + .get(&json_url.url) + .headers(json_url.headers.borrow().try_into().unwrap()) + .send() + .unwrap() + .bytes() + .unwrap() + .len() + }) + .sum(), ) } fn format_url(url: &str, path: &str) -> reqwest::Url { - reqwest::Url::parse(url).expect("Invalid url").join(path).expect("Invalid url") + reqwest::Url::parse(url) + .expect("Invalid url") + .join(path) + .expect("Invalid url") } fn bench_pair( @@ -73,16 +72,122 @@ fn bench_pair( refserver_url: reqwest::Url, json_content: &impl Serialize, ) { - group.bench_with_input(format!("{} {}", name, "htsget-rs"), &htsget_url, |b, input| { - b.to_async(Runtime::new().unwrap()).iter(|| request(input.clone(), json_content)) - }); - group.bench_with_input(format!("{} {}", name, "htsget-refserver"), &refserver_url, |b, input| { - b.to_async(Runtime::new().unwrap()).iter(|| request(input.clone(), json_content)) - }); + group.bench_with_input( + format!("{} {}", name, "htsget-rs"), + &htsget_url, + |b, input| b.iter(|| request(input.clone(), json_content)), + ); + group.bench_with_input( + format!("{} {}", name, "htsget-refserver"), + &refserver_url, + |b, input| b.iter(|| request(input.clone(), json_content)), + ); +} + +#[cfg(target_os = "windows")] +pub fn new_command(cmd: &str) -> Command { + let mut command = Command::new("cmd.exe"); + command.arg("/c"); + command.arg(cmd); + command } -fn start_htsget_rs(config: &Config) { - +#[cfg(not(target_os = "windows"))] +pub fn new_command(cmd: &str) -> Command { + Command::new(cmd) +} + +fn query_server_until_response(url: reqwest::Url) { + let client = Client::new(); + for _ in 0..120 { + sleep(Duration::from_secs(1)); + if let Err(err) = client.get(url.clone()).send() { + if err.is_connect() { + continue; + } + } + break; + } +} + +fn start_htsget_rs() -> (Child, String) { + let config = default_test_config(); + + let base_path = TempDir::new().unwrap(); + let (key_path, cert_path) = generate_test_certificates(base_path.path(), "key.pem", "cert.pem"); + + let child = new_command("cargo") + .current_dir(default_dir()) + .arg("run") + .arg("-p") + .arg("htsget-http-actix") + .env("HTSGET_TICKET_SERVER_KEY", key_path) + .env("HTSGET_TICKET_SERVER_CERT", cert_path) + .spawn() + .unwrap(); + + let htsget_rs_url = format!("http://{}", config.addr); + query_server_until_response(format_url(&htsget_rs_url, "reads/service-info")); + + (child, htsget_rs_url) +} + +fn start_htsget_refserver() -> (Child, String) { + let config_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("benches") + .join("htsget-refserver-config.json"); + let refserver_config: RefserverConfig = + serde_json::from_str(&fs::read_to_string(&config_path).unwrap()).unwrap(); + + new_command("docker") + .arg("image") + .arg("pull") + .arg(REFSERVER_DOCKER_IMAGE) + .spawn() + .unwrap() + .wait() + .unwrap(); + + let child = new_command("docker") + .current_dir(default_dir()) + .arg("container") + .arg("run") + .arg("-d") + .arg("-p") + .arg(format!( + "{}:3000", + &refserver_config.htsget_config.props.port + )) + .arg("-v") + .arg(format!( + "{}:/data", + default_dir() + .join("data") + .canonicalize() + .unwrap() + .to_string_lossy() + )) + .arg("-v") + .arg(format!( + "{}:/config", + &config_path + .canonicalize() + .unwrap() + .parent() + .unwrap() + .to_string_lossy() + )) + .arg(REFSERVER_DOCKER_IMAGE) + .arg("./htsget-refserver") + .arg("-config") + .arg("/config/htsget-refserver-config.json") + .spawn() + .unwrap(); + + let refserver_url = refserver_config.htsget_config.props.host; + query_server_until_response(format_url(&refserver_url, "reads/service-info")); + + (child, refserver_url) } fn criterion_benchmark(c: &mut Criterion) { @@ -91,23 +196,14 @@ fn criterion_benchmark(c: &mut Criterion) { .sample_size(NUMBER_OF_EXECUTIONS) .measurement_time(Duration::from_secs(BENCHMARK_DURATION_SECONDS)); - let config = default_test_config(); - let refserver_config: RefserverConfig = - serde_json::from_str(&fs::read_to_string(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("benches").join("htsget-refserver-config.json")).unwrap()).unwrap(); + let (mut htsget_rs_server, htsget_rs_url) = start_htsget_rs(); + let (mut htsget_refserver_server, htsget_refserver_url) = start_htsget_refserver(); - let htsget_rs_url = format!("https://{}", config.addr); - let htsget_refserver_url = refserver_config.htsget_config.props.host; bench_pair( &mut group, "[LIGHT] simple request", - format_url( - &htsget_rs_url, - "reads/bam/htsnexus_test_NA12878", - ), - format_url( - &htsget_refserver_url, - "reads/htsnexus_test_NA12878", - ), + format_url(&htsget_rs_url, "reads/bam/htsnexus_test_NA12878"), + format_url(&htsget_refserver_url, "reads/htsnexus_test_NA12878"), &Empty {}, ); @@ -126,14 +222,8 @@ fn criterion_benchmark(c: &mut Criterion) { bench_pair( &mut group, "[LIGHT] with region", - format_url( - &htsget_rs_url, - "reads/bam/htsnexus_test_NA12878", - ), - format_url( - &htsget_refserver_url, - "reads/htsnexus_test_NA12878", - ), + format_url(&htsget_rs_url, "reads/bam/htsnexus_test_NA12878"), + format_url(&htsget_refserver_url, "reads/htsnexus_test_NA12878"), &json_content, ); @@ -159,14 +249,8 @@ fn criterion_benchmark(c: &mut Criterion) { bench_pair( &mut group, "[LIGHT] with two regions", - format_url( - &htsget_rs_url, - "reads/bam/htsnexus_test_NA12878", - ), - format_url( - &htsget_refserver_url, - "reads/htsnexus_test_NA12878", - ), + format_url(&htsget_rs_url, "reads/bam/htsnexus_test_NA12878"), + format_url(&htsget_refserver_url, "reads/htsnexus_test_NA12878"), &json_content, ); @@ -185,14 +269,8 @@ fn criterion_benchmark(c: &mut Criterion) { bench_pair( &mut group, "[LIGHT] with VCF", - format_url( - &htsget_rs_url, - "variants/vcf/sample1-bcbio-cancer", - ), - format_url( - &htsget_refserver_url, - "variants/sample1-bcbio-cancer", - ), + format_url(&htsget_rs_url, "variants/vcf/sample1-bcbio-cancer"), + format_url(&htsget_refserver_url, "variants/sample1-bcbio-cancer"), &json_content, ); @@ -211,19 +289,16 @@ fn criterion_benchmark(c: &mut Criterion) { bench_pair( &mut group, "[HEAVY] with big VCF", - format_url( - &htsget_rs_url, - "variants/vcf/internationalgenomesample", - ), - format_url( - &htsget_refserver_url, - "variants/internationalgenomesample", - ), + format_url(&htsget_rs_url, "variants/vcf/internationalgenomesample"), + format_url(&htsget_refserver_url, "variants/internationalgenomesample"), &json_content, ); group.finish(); + + htsget_rs_server.kill().unwrap(); + htsget_refserver_server.kill().unwrap(); } criterion_group!(benches, criterion_benchmark); -criterion_main!(benches); \ No newline at end of file +criterion_main!(benches); diff --git a/htsget-http-actix/src/lib.rs b/htsget-http-actix/src/lib.rs index 864e52e01..eb0bc5c0e 100644 --- a/htsget-http-actix/src/lib.rs +++ b/htsget-http-actix/src/lib.rs @@ -72,8 +72,9 @@ mod tests { use htsget_config::config::Config; use htsget_search::storage::axum_server::HttpsFormatter; - use htsget_test_utils::{ - server_tests, Header as TestHeader, Response as TestResponse, TestRequest, TestServer, + use htsget_test_utils::server_tests; + use htsget_test_utils::server_tests::{ + Header as TestHeader, Response as TestResponse, TestRequest, TestServer, }; use super::*; diff --git a/htsget-http-lambda/Cargo.toml b/htsget-http-lambda/Cargo.toml index 839c10035..bee6ba7c9 100644 --- a/htsget-http-lambda/Cargo.toml +++ b/htsget-http-lambda/Cargo.toml @@ -25,7 +25,7 @@ tracing-subscriber = "0.3" bytes = "1.1" [dev-dependencies] -htsget-test-utils = { path = "../htsget-test-utils", default-features = false } +htsget-test-utils = { path = "../htsget-test-utils", features = ["server-tests"], default-features = false } async-trait = "0.1" query_map = { version = "0.5", features = ["url-query"] } diff --git a/htsget-http-lambda/src/lib.rs b/htsget-http-lambda/src/lib.rs index 65978ff90..a480e0e39 100644 --- a/htsget-http-lambda/src/lib.rs +++ b/htsget-http-lambda/src/lib.rs @@ -185,10 +185,11 @@ mod tests { use htsget_search::htsget::{Class, HtsGet}; use htsget_search::storage::axum_server::HttpsFormatter; use htsget_search::storage::local::LocalStorage; + use htsget_test_utils::server_tests; use htsget_test_utils::server_tests::{ - default_test_config, get_test_file, test_response, test_response_service_info, + default_test_config, get_test_file, test_response, test_response_service_info, Header, + Response, TestRequest, TestServer, }; - use htsget_test_utils::{server_tests, Header, Response, TestRequest, TestServer}; use crate::{HtsgetMethod, Method, Route, RouteType, Router}; @@ -332,12 +333,7 @@ mod tests { #[tokio::test] async fn test_parameterized_post_from_file() { let config = default_test_config(); - endpoint_from_file( - "events/event_parameterized_post.json", - Class::Body, - &config, - ) - .await; + endpoint_from_file("events/event_parameterized_post.json", Class::Body, &config).await; } #[tokio::test] diff --git a/htsget-search/Cargo.toml b/htsget-search/Cargo.toml index d6df24f9e..b2305a06a 100644 --- a/htsget-search/Cargo.toml +++ b/htsget-search/Cargo.toml @@ -38,7 +38,7 @@ aws-sdk-s3 = { version = "0.9", optional = true } aws-config = { version = "0.9", optional = true } [dev-dependencies] -rcgen = "0.9" +htsget-test-utils = { path = "../htsget-test-utils", default-features = false } tempfile = "3.3" # Aws S3 storage dependencies. diff --git a/htsget-search/benches/benchmark.rs b/htsget-search/benches/benchmark.rs index e948cdf03..c19732633 100644 --- a/htsget-search/benches/benchmark.rs +++ b/htsget-search/benches/benchmark.rs @@ -29,9 +29,11 @@ async fn perform_query(query: Query) -> Result<(), HtsGetError> { Ok(()) } -fn bench_query(group: &mut BenchmarkGroup, name: &str, query: Query) -{ - group.bench_with_input(name, &query, |b, input| b.to_async(Runtime::new().unwrap()).iter(|| perform_query(input.clone()))); +fn bench_query(group: &mut BenchmarkGroup, name: &str, query: Query) { + group.bench_with_input(name, &query, |b, input| { + b.to_async(Runtime::new().unwrap()) + .iter(|| perform_query(input.clone())) + }); } fn criterion_benchmark(c: &mut Criterion) { @@ -40,56 +42,80 @@ fn criterion_benchmark(c: &mut Criterion) { .sample_size(NUMBER_OF_EXECUTIONS) .measurement_time(Duration::from_secs(BENCHMARK_DURATION_SECONDS)); - bench_query(&mut group, "[LIGHT] Bam query all", - Query::new("bam/htsnexus_test_NA12878", Bam) + bench_query( + &mut group, + "[LIGHT] Bam query all", + Query::new("bam/htsnexus_test_NA12878", Bam), ); - bench_query(&mut group, "[LIGHT] Bam query specific", + bench_query( + &mut group, + "[LIGHT] Bam query specific", Query::new("bam/htsnexus_test_NA12878", Bam) .with_reference_name("11") .with_start(4999977) - .with_end(5008321) + .with_end(5008321), ); - bench_query(&mut group, "[LIGHT] Bam query header", - Query::new("bam/htsnexus_test_NA12878", Bam).with_class(Header) + bench_query( + &mut group, + "[LIGHT] Bam query header", + Query::new("bam/htsnexus_test_NA12878", Bam).with_class(Header), ); - bench_query(&mut group, "[LIGHT] Cram query all", - Query::new("cram/htsnexus_test_NA12878", Cram) + bench_query( + &mut group, + "[LIGHT] Cram query all", + Query::new("cram/htsnexus_test_NA12878", Cram), ); - bench_query(&mut group, "[LIGHT] Cram query specific", + bench_query( + &mut group, + "[LIGHT] Cram query specific", Query::new("cram/htsnexus_test_NA12878", Cram) .with_reference_name("11") .with_start(4999977) - .with_end(5008321) + .with_end(5008321), ); - bench_query(&mut group, "[LIGHT] Cram query header", - Query::new("cram/htsnexus_test_NA12878", Cram).with_class(Header) + bench_query( + &mut group, + "[LIGHT] Cram query header", + Query::new("cram/htsnexus_test_NA12878", Cram).with_class(Header), ); - bench_query(&mut group, "[LIGHT] Vcf query all", - Query::new("vcf/sample1-bcbio-cancer", Vcf) + bench_query( + &mut group, + "[LIGHT] Vcf query all", + Query::new("vcf/sample1-bcbio-cancer", Vcf), ); - bench_query(&mut group, "[LIGHT] Vcf query specific", + bench_query( + &mut group, + "[LIGHT] Vcf query specific", Query::new("vcf/sample1-bcbio-cancer", Vcf) .with_reference_name("chrM") .with_start(151) - .with_end(153) + .with_end(153), ); - bench_query(&mut group, "[LIGHT] Vcf query header", - Query::new("vcf/sample1-bcbio-cancer", Vcf).with_class(Header) + bench_query( + &mut group, + "[LIGHT] Vcf query header", + Query::new("vcf/sample1-bcbio-cancer", Vcf).with_class(Header), ); - bench_query(&mut group, "[LIGHT] Bcf query all", - Query::new("bcf/sample1-bcbio-cancer", Bcf) + bench_query( + &mut group, + "[LIGHT] Bcf query all", + Query::new("bcf/sample1-bcbio-cancer", Bcf), ); - bench_query(&mut group, "[LIGHT] Bcf query specific", + bench_query( + &mut group, + "[LIGHT] Bcf query specific", Query::new("bcf/sample1-bcbio-cancer", Bcf) .with_reference_name("chrM") .with_start(151) - .with_end(153) + .with_end(153), ); - bench_query(&mut group, "[LIGHT] Bcf query header", - Query::new("bcf/sample1-bcbio-cancer", Bcf).with_class(Header) + bench_query( + &mut group, + "[LIGHT] Bcf query header", + Query::new("bcf/sample1-bcbio-cancer", Bcf).with_class(Header), ); group.finish(); diff --git a/htsget-search/src/storage/axum_server.rs b/htsget-search/src/storage/axum_server.rs index 362ee88e0..7dbf75901 100644 --- a/htsget-search/src/storage/axum_server.rs +++ b/htsget-search/src/storage/axum_server.rs @@ -149,34 +149,25 @@ impl UrlFormatter for HttpsFormatter { #[cfg(test)] mod tests { - use std::fs; use std::io::Read; + use htsget_test_utils::util::generate_test_certificates; use http::{Method, Request}; use hyper::client::HttpConnector; use hyper::{Body, Client}; use hyper_tls::native_tls::TlsConnector; use hyper_tls::HttpsConnector; - use rcgen::generate_simple_self_signed; use crate::storage::local::tests::create_local_test_files; use super::*; - pub fn generate_test_certificates>(key_path: &P, cert_path: &P) { - let cert = generate_simple_self_signed(vec!["localhost".to_string()]).unwrap(); - fs::write(key_path, cert.serialize_private_key_pem()).unwrap(); - fs::write(cert_path, cert.serialize_pem().unwrap()).unwrap(); - } - #[tokio::test] async fn test_server() { let (_, base_path) = create_local_test_files().await; - let key_path = base_path.path().join("key.pem"); - let cert_path = base_path.path().join("cert.pem"); // Generate self-signed certificate. - generate_test_certificates(&key_path, &cert_path); + let (key_path, cert_path) = generate_test_certificates(base_path.path(), "key.pem", "cert.pem"); // Read certificate. let mut buf = vec![]; diff --git a/htsget-test-utils/Cargo.toml b/htsget-test-utils/Cargo.toml index d1e42724d..0ed5e4b73 100644 --- a/htsget-test-utils/Cargo.toml +++ b/htsget-test-utils/Cargo.toml @@ -5,17 +5,19 @@ authors = ["Marko Malenic "] edition = "2021" [features] -s3-storage = ["htsget-config/s3-storage", "htsget-search/s3-storage", "htsget-http-core/s3-storage"] +server-tests = ["htsget-config", "htsget-search", "htsget-http-core"] +s3-storage = ["htsget-config?/s3-storage", "htsget-search?/s3-storage", "htsget-http-core?/s3-storage"] default = ["s3-storage"] [dependencies] -htsget-http-core = { path = "../htsget-http-core", default-features = false } -htsget-config = { path = "../htsget-config", default-features = false } -htsget-search = { path = "../htsget-search", default-features = false } +htsget-http-core = { path = "../htsget-http-core", default-features = false, optional = true } +htsget-config = { path = "../htsget-config", default-features = false, optional = true } +htsget-search = { path = "../htsget-search", default-features = false, optional = true } async-trait = "0.1" http = "0.2" mime = "0.3" serde_json = "1.0" serde = "1.0" envy = "0.4" -serde_bytes = "0.11" \ No newline at end of file +serde_bytes = "0.11" +rcgen = "0.9" \ No newline at end of file diff --git a/htsget-test-utils/src/lib.rs b/htsget-test-utils/src/lib.rs index 55bca7489..43dd37e9b 100644 --- a/htsget-test-utils/src/lib.rs +++ b/htsget-test-utils/src/lib.rs @@ -1,65 +1,3 @@ -use async_trait::async_trait; -use serde::de; -use serde::Deserialize; - -use htsget_config::config::Config; -use htsget_search::htsget::Response as HtsgetResponse; - +#[cfg(feature = "server-tests")] pub mod server_tests; - -/// Represents a http header. -#[derive(Debug)] -pub struct Header> { - pub name: T, - pub value: T, -} - -impl> Header { - pub fn into_tuple(self) -> (String, String) { - (self.name.into(), self.value.into()) - } -} - -/// Represents a http response. -#[derive(Debug, Deserialize)] -pub struct Response { - #[serde(alias = "statusCode")] - pub status: u16, - #[serde(with = "serde_bytes")] - pub body: Vec, -} - -impl Response { - pub fn new(status: u16, body: Vec) -> Self { - Self { status, body } - } - - /// Deserialize the body from a slice. - pub fn deserialize_body(&self) -> Result - where - T: de::DeserializeOwned, - { - serde_json::from_slice(&self.body) - } - - /// Check if status code is success. - pub fn is_success(&self) -> bool { - 300 > self.status && self.status >= 200 - } -} - -/// Mock request trait that should be implemented to use test functions. -pub trait TestRequest { - fn insert_header(self, header: Header>) -> Self; - fn set_payload(self, payload: impl Into) -> Self; - fn uri(self, uri: impl Into) -> Self; - fn method(self, method: impl Into) -> Self; -} - -/// Mock server trait that should be implemented to use test functions. -#[async_trait(?Send)] -pub trait TestServer { - fn get_config(&self) -> &Config; - fn get_request(&self) -> T; - async fn test_server(&self, request: T) -> Response; -} +pub mod util; diff --git a/htsget-test-utils/src/server_tests.rs b/htsget-test-utils/src/server_tests.rs index d1499e8c0..fa31c35e5 100644 --- a/htsget-test-utils/src/server_tests.rs +++ b/htsget-test-utils/src/server_tests.rs @@ -8,7 +8,67 @@ use htsget_config::config::Config; use htsget_http_core::{get_service_info_with, Endpoint, JsonResponse}; use htsget_search::htsget::{Class, Format, Headers, Url}; -use crate::{Header, HtsgetResponse, Response, TestRequest, TestServer}; +use async_trait::async_trait; +use htsget_search::htsget::Response as HtsgetResponse; +use serde::de; +use serde::Deserialize; + +/// Represents a http header. +#[derive(Debug)] +pub struct Header> { + pub name: T, + pub value: T, +} + +impl> Header { + pub fn into_tuple(self) -> (String, String) { + (self.name.into(), self.value.into()) + } +} + +/// Represents a http response. +#[derive(Debug, Deserialize)] +pub struct Response { + #[serde(alias = "statusCode")] + pub status: u16, + #[serde(with = "serde_bytes")] + pub body: Vec, +} + +impl Response { + pub fn new(status: u16, body: Vec) -> Self { + Self { status, body } + } + + /// Deserialize the body from a slice. + pub fn deserialize_body(&self) -> Result + where + T: de::DeserializeOwned, + { + serde_json::from_slice(&self.body) + } + + /// Check if status code is success. + pub fn is_success(&self) -> bool { + 300 > self.status && self.status >= 200 + } +} + +/// Mock request trait that should be implemented to use test functions. +pub trait TestRequest { + fn insert_header(self, header: Header>) -> Self; + fn set_payload(self, payload: impl Into) -> Self; + fn uri(self, uri: impl Into) -> Self; + fn method(self, method: impl Into) -> Self; +} + +/// Mock server trait that should be implemented to use test functions. +#[async_trait(?Send)] +pub trait TestServer { + fn get_config(&self) -> &Config; + fn get_request(&self) -> T; + async fn test_server(&self, request: T) -> Response; +} /// Test response with with class. pub fn test_response(response: &Response, config: &Config, class: Class) { diff --git a/htsget-test-utils/src/util.rs b/htsget-test-utils/src/util.rs new file mode 100644 index 000000000..6a49592e2 --- /dev/null +++ b/htsget-test-utils/src/util.rs @@ -0,0 +1,18 @@ +use rcgen::generate_simple_self_signed; +use std::fs; +use std::path::{Path, PathBuf}; + +pub fn generate_test_certificates>( + in_path: P, + key_name: &str, + cert_name: &str, +) -> (PathBuf, PathBuf) { + let key_path = in_path.as_ref().join(key_name); + let cert_path = in_path.as_ref().join(cert_name); + + let cert = generate_simple_self_signed(vec!["localhost".to_string()]).unwrap(); + fs::write(&key_path, cert.serialize_private_key_pem()).unwrap(); + fs::write(&cert_path, cert.serialize_pem().unwrap()).unwrap(); + + (key_path, cert_path) +} From c89fac38d28d218b36d713aac89d15bc38fcbdc0 Mon Sep 17 00:00:00 2001 From: Marko Malenic Date: Fri, 27 May 2022 10:44:21 +1000 Subject: [PATCH 40/50] Fix localstorage path (#86) * Add more descriptive errors. * LocalStorage no longer exposes path. * Fix all tests related to new localstorage changes. * Update README. --- data/events/event_get.json | 2 +- data/events/event_parameterized_get.json | 2 +- data/events/event_parameterized_post.json | 2 +- ...event_parameterized_post_class_header.json | 2 +- data/events/event_post.json | 2 +- htsget-config/src/config.rs | 6 +- htsget-http-actix/README.md | 48 ++++++++-------- htsget-http-actix/src/handlers/get.rs | 3 + htsget-http-actix/src/handlers/post.rs | 3 + .../src/handlers/service_info.rs | 2 + htsget-http-actix/src/lib.rs | 2 +- htsget-http-actix/src/main.rs | 2 +- htsget-http-core/src/lib.rs | 26 +++------ htsget-http-lambda/src/lib.rs | 12 ++-- htsget-search/src/htsget/bam_search.rs | 24 +++----- htsget-search/src/htsget/bcf_search.rs | 18 ++---- htsget-search/src/htsget/cram_search.rs | 22 +++----- htsget-search/src/htsget/from_storage.rs | 4 +- htsget-search/src/htsget/mod.rs | 5 +- htsget-search/src/htsget/vcf_search.rs | 18 ++---- htsget-search/src/storage/axum_server.rs | 56 ++++++++++++------- htsget-search/src/storage/local.rs | 48 ++++++++-------- htsget-search/src/storage/mod.rs | 14 +++-- htsget-test-utils/src/server_tests.rs | 34 ++++------- 24 files changed, 174 insertions(+), 183 deletions(-) diff --git a/data/events/event_get.json b/data/events/event_get.json index 15ae11f11..1bebd8cd5 100644 --- a/data/events/event_get.json +++ b/data/events/event_get.json @@ -1,6 +1,6 @@ { "httpMethod": "GET", - "path": "/variants/data/vcf/sample1-bcbio-cancer", + "path": "/variants/vcf/sample1-bcbio-cancer", "body": null, "resource": "/{proxy+}", diff --git a/data/events/event_parameterized_get.json b/data/events/event_parameterized_get.json index d57ad25e1..6432abdc5 100644 --- a/data/events/event_parameterized_get.json +++ b/data/events/event_parameterized_get.json @@ -1,6 +1,6 @@ { "httpMethod": "GET", - "path": "/variants/data/vcf/sample1-bcbio-cancer", + "path": "/variants/vcf/sample1-bcbio-cancer", "body": null, "resource": "/{proxy+}", diff --git a/data/events/event_parameterized_post.json b/data/events/event_parameterized_post.json index a8d5a0c25..3630f36bf 100644 --- a/data/events/event_parameterized_post.json +++ b/data/events/event_parameterized_post.json @@ -1,6 +1,6 @@ { "httpMethod": "POST", - "path": "/variants/data/vcf/sample1-bcbio-cancer", + "path": "/variants/vcf/sample1-bcbio-cancer", "body": "{\"format\": \"VCF\", \"regions\": [{\"referenceName\": \"chrM\"}]}", "resource": "/{proxy+}", diff --git a/data/events/event_parameterized_post_class_header.json b/data/events/event_parameterized_post_class_header.json index 9bae273f2..06b365407 100644 --- a/data/events/event_parameterized_post_class_header.json +++ b/data/events/event_parameterized_post_class_header.json @@ -1,6 +1,6 @@ { "httpMethod": "POST", - "path": "/variants/data/vcf/sample1-bcbio-cancer", + "path": "/variants/vcf/sample1-bcbio-cancer", "body": "{\"format\": \"VCF\", \"class\": \"header\", \"regions\": [{\"referenceName\": \"chrM\"}]}", "resource": "/{proxy+}", diff --git a/data/events/event_post.json b/data/events/event_post.json index ffbeb79d0..9787ba93a 100644 --- a/data/events/event_post.json +++ b/data/events/event_post.json @@ -1,6 +1,6 @@ { "httpMethod": "POST", - "path": "/variants/data/vcf/sample1-bcbio-cancer", + "path": "/variants/vcf/sample1-bcbio-cancer", "body": null, "resource": "/{proxy+}", diff --git a/htsget-config/src/config.rs b/htsget-config/src/config.rs index ef1b82b3f..598b963b6 100644 --- a/htsget-config/src/config.rs +++ b/htsget-config/src/config.rs @@ -9,13 +9,13 @@ use crate::config::StorageType::LocalStorage; use crate::regex_resolver::RegexResolver; pub const USAGE: &str = r#" -This executable doesn't use command line arguments, but there are some environment variables that can be set to configure the HtsGet server: -* HTSGET_ADDR: The socket address to use for the server which creates response tickets. Default: "127.0.0.1:8080". +The HtsGet server executables don't use command line arguments, but there are some environment variables that can be set to configure them: +* HTSGET_ADDR: The socket address for the server which creates response tickets. Default: "127.0.0.1:8080". * HTSGET_PATH: The path to the directory where the server should be started. Default: ".". Unused if HTSGET_STORAGE_TYPE is "AwsS3Storage". * HTSGET_REGEX: The regular expression that should match an ID. Default: ".*". For more information about the regex options look in the documentation of the regex crate(https://docs.rs/regex/). * HTSGET_SUBSTITUTION_STRING: The replacement expression. Default: "$0". -* HTSGET_STORAGE_TYPE: Either LocalStorage or AwsS3Storage. Default: "LocalStorage". +* HTSGET_STORAGE_TYPE: Either "LocalStorage" or "AwsS3Storage", representing which storage type to use. Default: "LocalStorage". The following options are used for the ticket server. * HTSGET_TICKET_SERVER_ADDR: The socket address to use for the server which responds to tickets. Default: "127.0.0.1:8081". Unused if HTSGET_STORAGE_TYPE is not "LocalStorage". diff --git a/htsget-http-actix/README.md b/htsget-http-actix/README.md index c8af46575..85c17d39d 100644 --- a/htsget-http-actix/README.md +++ b/htsget-http-actix/README.md @@ -25,23 +25,27 @@ There are reasonable defaults to allow the user to spin up the server as fast as Since this service can be used in serverless environments, no `dotenv` configuration is needed, [adjusting the environment variables below prevent accidental leakage of settings and sensitive information](https://medium.com/@softprops/configuration-envy-a09584386705). -| Variable | Description | Default | -|---|---|---| -| HTSGET_IP| IP address | 127.0.0.1 | -| HTSGET_PORT| TCP Port | 8080 | -| HTSGET_PATH| The path to the directory where the server starts | `$PWD` | -| HTSGET_REGEX| The regular expression an ID should match. | ".*" | -| HTSGET_REPLACEMENT| The replacement expression, to produce a key from an ID. | "$0" | -| HTSGET_ID| ID of the service. | "" | -| HTSGET_NAME| Name of the service. | HtsGet service | -| HTSGET_VERSION | Version of the service | "" -| HTSGET_ORGANIZATION_NAME| Name of the organization | Snake oil -| HTSGET_ORGANIZATION_URL| URL of the organization | https://en.wikipedia.org/wiki/Snake_oil | -| HTSGET_CONTACT_URL | URL to provide contact to the users | "" | -| HTSGET_DOCUMENTATION_URL| Link to documentation | https://github.com/umccr/htsget-rs/tree/main/htsget-http-actix | -| HTSGET_CREATED_AT | Date of the creation of the service. | "" | -| HTSGET_UPDATED_AT | Date of the last update of the service. | "" | -| HTSGET_ENVIRONMENT | Environment in which the service is running. | Testing | +| Variable | Description | Default | +|----------------------------|------------------------------------------------------------------------------------------------------------------------------------------|------------------| +| HTSGET_ADDR | The socket address for the server which creates response tickets. | "127.0.0.1:8080" | +| HTSGET_PATH | The path to the directory where the server starts | "." | +| HTSGET_REGEX | The regular expression an ID should match. | ".*" | +| HTSGET_SUBSTITUTION_STRING | The replacement expression, to produce a key from an ID. | "$0" | +| HTSGET_STORAGE_TYPE | Either "LocalStorage" or "AwsS3Storage", representing which storage type to use. | "LocalStorage" | +| HTSGET_TICKET_SERVER_ADDR | The socket address to use for the server which responds to tickets. Unused if HTSGET_STORAGE_TYPE is not "LocalStorage". | "127.0.0.1:8081" | +| HTSGET_TICKET_SERVER_KEY | The path to the PEM formatted X.509 private key used by the ticket response server. Unused if HTSGET_STORAGE_TYPE is not "LocalStorage". | "key.pem" | +| HTSGET_TICKET_SERVER_CERT | The path to the PEM formatted X.509 certificate used by the ticket response server. Unused if HTSGET_STORAGE_TYPE is not "LocalStorage". | "cert.pem" | +| HTSGET_S3_BUCKET | The name of the AWS S3 bucket. Unused if HTSGET_STORAGE_TYPE is not "AwsS3Storage". | "" | +| HTSGET_ID | ID of the service. | "None" | +| HTSGET_NAME | Name of the service. | "None" | +| HTSGET_VERSION | Version of the service. | "None" | +| HTSGET_ORGANIZATION_NAME | Name of the organization. | "None" | +| HTSGET_ORGANIZATION_URL | URL of the organization. | "None" | +| HTSGET_CONTACT_URL | URL to provide contact to the users. | "None" | +| HTSGET_DOCUMENTATION_URL | Link to documentation. | "None" | +| HTSGET_CREATED_AT | Date of the creation of the service. | "None" | +| HTSGET_UPDATED_AT | Date of the last update of the service. | "None" | +| HTSGET_ENVIRONMENT | Environment in which the service is running. | "None" | For more information about the regex options look in the [documentation of the regex crate](https://docs.rs/regex/). ## Example cURL requests @@ -51,25 +55,25 @@ As mentioned above, please keep in mind that the server will take the path where ### GET ```shell -$ curl '127.0.0.1:8080/variants/data/vcf/sample1-bcbio-cancer' +$ curl '127.0.0.1:8080/variants/vcf/sample1-bcbio-cancer' ``` ### POST ```shell -$ curl --header "Content-Type: application/json" -d '{}' '127.0.0.1:8080/variants/data/vcf/sample1-bcbio-cancer' +$ curl --header "Content-Type: application/json" -d '{}' '127.0.0.1:8080/variants/vcf/sample1-bcbio-cancer' ``` ### Parametrised GET ```shell -$ curl '127.0.0.1:8080/variants/data/vcf/sample1-bcbio-cancer?format=VCF&class=header' +$ curl '127.0.0.1:8080/variants/vcf/sample1-bcbio-cancer?format=VCF&class=header' ``` ### Parametrised POST ```shell -$ curl --header "Content-Type: application/json" -d '{"format": "VCF", "regions": [{"referenceName": "chrM"}]}' '127.0.0.1:8080/variants/data/vcf/sample1-bcbio-cancer' +$ curl --header "Content-Type: application/json" -d '{"format": "VCF", "regions": [{"referenceName": "chrM"}]}' '127.0.0.1:8080/variants/vcf/sample1-bcbio-cancer' ``` ### Service-info @@ -81,6 +85,6 @@ $ curl 127.0.0.1:8080/variants/service-info ## Example Regular expressions In this example 'data/' is added after the first '/'. ```shell -$ HTSGET_REGEX='(?P.*?)/(?P.*)' HTSGET_REPLACEMENT='$group1/data/$group2' cargo run --release -p htsget-http-actix +$ HTSGET_REGEX='(?P.*?)/(?P.*)' HTSGET_SUBSTITUTION_STRING='$group1/data/$group2' cargo run --release -p htsget-http-actix ``` For more information about the regex options look in the [documentation of the regex crate](https://docs.rs/regex/). \ No newline at end of file diff --git a/htsget-http-actix/src/handlers/get.rs b/htsget-http-actix/src/handlers/get.rs index c4ff0323d..4a62a894a 100644 --- a/htsget-http-actix/src/handlers/get.rs +++ b/htsget-http-actix/src/handlers/get.rs @@ -4,6 +4,7 @@ use actix_web::{ web::{Data, Path, Query}, Responder, }; +use tracing::info; use htsget_http_core::{get_response_for_get_request, Endpoint}; use htsget_search::htsget::HtsGet; @@ -20,6 +21,7 @@ pub async fn reads( ) -> impl Responder { let mut query_information = request.into_inner(); query_information.insert("id".to_string(), path.into_inner()); + info!(query = ?query_information, "Reads endpoint GET request"); handle_response( get_response_for_get_request( app_state.get_ref().htsget.clone(), @@ -38,6 +40,7 @@ pub async fn variants( ) -> impl Responder { let mut query_information = request.into_inner(); query_information.insert("id".to_string(), path.into_inner()); + info!(query = ?query_information, "Variants endpoint GET request"); handle_response( get_response_for_get_request( app_state.get_ref().htsget.clone(), diff --git a/htsget-http-actix/src/handlers/post.rs b/htsget-http-actix/src/handlers/post.rs index 3a349c88b..73dd8ef72 100644 --- a/htsget-http-actix/src/handlers/post.rs +++ b/htsget-http-actix/src/handlers/post.rs @@ -2,6 +2,7 @@ use actix_web::{ web::{Data, Json, Path}, Responder, }; +use tracing::info; use htsget_http_core::{get_response_for_post_request, Endpoint, PostRequest}; use htsget_search::htsget::HtsGet; @@ -16,6 +17,7 @@ pub async fn reads( path: Path, app_state: Data>, ) -> impl Responder { + info!(request = ?request, "Reads endpoint POST request"); handle_response( get_response_for_post_request( app_state.get_ref().htsget.clone(), @@ -33,6 +35,7 @@ pub async fn variants( path: Path, app_state: Data>, ) -> impl Responder { + info!(request = ?request, "Variants endpoint POST request"); handle_response( get_response_for_post_request( app_state.get_ref().htsget.clone(), diff --git a/htsget-http-actix/src/handlers/service_info.rs b/htsget-http-actix/src/handlers/service_info.rs index fbb19e1e4..6695c6d3d 100644 --- a/htsget-http-actix/src/handlers/service_info.rs +++ b/htsget-http-actix/src/handlers/service_info.rs @@ -1,5 +1,6 @@ use actix_web::web::Data; use actix_web::Responder; +use tracing::info; use htsget_http_core::get_service_info_json as get_base_service_info_json; use htsget_http_core::Endpoint; @@ -13,6 +14,7 @@ pub fn get_service_info_json( app_state: &AppState, endpoint: Endpoint, ) -> impl Responder { + info!(endpoint = ?endpoint, "Service info request"); PrettyJson(get_base_service_info_json( endpoint, app_state.htsget.clone(), diff --git a/htsget-http-actix/src/lib.rs b/htsget-http-actix/src/lib.rs index 88a61c73b..a2da28f14 100644 --- a/htsget-http-actix/src/lib.rs +++ b/htsget-http-actix/src/lib.rs @@ -133,7 +133,7 @@ mod tests { HtsGetFromStorage::local_from( self.config.path.clone(), self.config.resolver.clone(), - HttpsFormatter::from(self.config.addr), + HttpsFormatter::from(self.config.ticket_server_addr), ) .unwrap(), self.config.service_info.clone(), diff --git a/htsget-http-actix/src/main.rs b/htsget-http-actix/src/main.rs index 8962ca795..841f161ca 100644 --- a/htsget-http-actix/src/main.rs +++ b/htsget-http-actix/src/main.rs @@ -25,7 +25,7 @@ async fn main() -> std::io::Result<()> { } async fn local_storage_server(config: Config) -> std::io::Result<()> { - let formatter = HttpsFormatter::from(config.addr); + let formatter = HttpsFormatter::from(config.ticket_server_addr); let mut local_server = formatter.bind_axum_server().await?; let searcher = HtsGetFromStorage::local_from( diff --git a/htsget-http-core/src/lib.rs b/htsget-http-core/src/lib.rs index 3fef39317..21ad2a7db 100644 --- a/htsget-http-core/src/lib.rs +++ b/htsget-http-core/src/lib.rs @@ -237,28 +237,20 @@ mod tests { fn example_vcf_json_response(headers: HashMap) -> JsonResponse { JsonResponse::from_response(Response::new( Format::Vcf, - vec![Url::new(format!( - "https://127.0.0.1:8081{}", - get_base_path() - .join("vcf") - .join("sample1-bcbio-cancer.vcf.gz") - .to_string_lossy() - )) - .with_headers(Headers::new(headers))], + vec![ + Url::new("https://127.0.0.1:8081/data/vcf/sample1-bcbio-cancer.vcf.gz".to_string()) + .with_headers(Headers::new(headers)), + ], )) } fn example_bam_json_response(headers: HashMap) -> JsonResponse { JsonResponse::from_response(Response::new( Format::Bam, - vec![Url::new(format!( - "https://127.0.0.1:8081{}", - get_base_path() - .join("bam") - .join("htsnexus_test_NA12878.bam") - .to_string_lossy() - )) - .with_headers(Headers::new(headers))], + vec![ + Url::new("https://127.0.0.1:8081/data/bam/htsnexus_test_NA12878.bam".to_string()) + .with_headers(Headers::new(headers)), + ], )) } @@ -273,7 +265,7 @@ mod tests { fn get_searcher() -> Arc { Arc::new(HtsGetFromStorage::new( LocalStorage::new( - "../data", + get_base_path(), RegexResolver::new(".*", "$0").unwrap(), HttpsFormatter::new("127.0.0.1", "8081").unwrap(), ) diff --git a/htsget-http-lambda/src/lib.rs b/htsget-http-lambda/src/lib.rs index 48005fdff..24559f23f 100644 --- a/htsget-http-lambda/src/lib.rs +++ b/htsget-http-lambda/src/lib.rs @@ -265,9 +265,9 @@ mod tests { HtsGetFromStorage::local_from( &self.config.path, self.config.resolver.clone(), - HttpsFormatter::from(self.config.addr), + HttpsFormatter::from(self.config.ticket_server_addr), ) - .expect("Couldn't create a Storage with the provided path"), + .unwrap(), ), &self.config.service_info, ); @@ -521,14 +521,14 @@ mod tests { Fut: Future, { let router = Router::new( - Arc::new(HtsGetFromStorage::new( - LocalStorage::new( + Arc::new( + HtsGetFromStorage::local_from( &config.path, config.resolver.clone(), - HttpsFormatter::new("127.0.0.1", "8080").unwrap(), + HttpsFormatter::from(config.ticket_server_addr), ) .unwrap(), - )), + ), &config.service_info, ); test(router).await diff --git a/htsget-search/src/htsget/bam_search.rs b/htsget-search/src/htsget/bam_search.rs index 4640248a1..c396bfdac 100644 --- a/htsget-search/src/htsget/bam_search.rs +++ b/htsget-search/src/htsget/bam_search.rs @@ -209,7 +209,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bam, - vec![Url::new(expected_url(storage)) + vec![Url::new(expected_url()) .with_headers(Headers::default().with_header("Range", "bytes=4668-2596799"))], )); assert_eq!(response, expected_response) @@ -227,7 +227,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bam, - vec![Url::new(expected_url(storage)) + vec![Url::new(expected_url()) .with_headers(Headers::default().with_header("Range", "bytes=2060795-2596799"))], )); assert_eq!(response, expected_response) @@ -245,7 +245,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bam, - vec![Url::new(expected_url(storage)) + vec![Url::new(expected_url()) .with_headers(Headers::default().with_header("Range", "bytes=977196-2128166"))], )); assert_eq!(response, expected_response) @@ -267,11 +267,11 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bam, vec![ - Url::new(expected_url(storage.clone())) + Url::new(expected_url()) .with_headers(Headers::default().with_header("Range", "bytes=256721-647346")), - Url::new(expected_url(storage.clone())) + Url::new(expected_url()) .with_headers(Headers::default().with_header("Range", "bytes=824361-842101")), - Url::new(expected_url(storage)) + Url::new(expected_url()) .with_headers(Headers::default().with_header("Range", "bytes=977196-996015")), ], )); @@ -290,7 +290,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bam, - vec![Url::new(expected_url(storage)) + vec![Url::new(expected_url()) .with_headers(Headers::default().with_header("Range", "bytes=0-4668")) .with_class(Class::Header)], )); @@ -320,13 +320,7 @@ pub mod tests { .await } - pub(crate) fn expected_url(storage: Arc>) -> String { - format!( - "https://127.0.0.1:8081{}", - storage - .base_path() - .join("htsnexus_test_NA12878.bam") - .to_string_lossy() - ) + pub(crate) fn expected_url() -> String { + "https://127.0.0.1:8081/data/htsnexus_test_NA12878.bam".to_string() } } diff --git a/htsget-search/src/htsget/bcf_search.rs b/htsget-search/src/htsget/bcf_search.rs index 5cc8e2aa6..1e816d484 100644 --- a/htsget-search/src/htsget/bcf_search.rs +++ b/htsget-search/src/htsget/bcf_search.rs @@ -170,7 +170,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bcf, - vec![Url::new(expected_url(storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-3530"))], )); assert_eq!(response, expected_response) @@ -189,7 +189,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bcf, - vec![Url::new(expected_url(storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-950"))], )); assert_eq!(response, expected_response) @@ -211,7 +211,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bcf, - vec![Url::new(expected_url(storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-3530"))], )); assert_eq!(response, expected_response) @@ -248,7 +248,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bcf, - vec![Url::new(expected_url(storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-950")) .with_class(Class::Header)], )); @@ -278,13 +278,7 @@ pub mod tests { .await } - pub(crate) fn expected_url(storage: Arc>, name: &str) -> String { - format!( - "https://127.0.0.1:8081{}", - storage - .base_path() - .join(format!("{}.bcf", name)) - .to_string_lossy() - ) + pub(crate) fn expected_url(name: &str) -> String { + format!("https://127.0.0.1:8081/data/{}.bcf", name) } } diff --git a/htsget-search/src/htsget/cram_search.rs b/htsget-search/src/htsget/cram_search.rs index 6a83513e4..60961382a 100644 --- a/htsget-search/src/htsget/cram_search.rs +++ b/htsget-search/src/htsget/cram_search.rs @@ -284,7 +284,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, - vec![Url::new(expected_url(storage)) + vec![Url::new(expected_url()) .with_headers(Headers::default().with_header("Range", "bytes=6087-1627756"))], )); assert_eq!(response, expected_response) @@ -302,7 +302,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, - vec![Url::new(expected_url(storage)) + vec![Url::new(expected_url()) .with_headers(Headers::default().with_header("Range", "bytes=1280106-1627756"))], )); assert_eq!(response, expected_response) @@ -320,7 +320,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, - vec![Url::new(expected_url(storage)) + vec![Url::new(expected_url()) .with_headers(Headers::default().with_header("Range", "bytes=604231-1280106"))], )); assert_eq!(response, expected_response) @@ -341,7 +341,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, - vec![Url::new(expected_url(storage)) + vec![Url::new(expected_url()) .with_headers(Headers::default().with_header("Range", "bytes=6087-465709"))], )); assert_eq!(response, expected_response) @@ -362,7 +362,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, - vec![Url::new(expected_url(storage)) + vec![Url::new(expected_url()) .with_headers(Headers::default().with_header("Range", "bytes=6087-604231"))], )); assert_eq!(response, expected_response) @@ -380,7 +380,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, - vec![Url::new(expected_url(storage)) + vec![Url::new(expected_url()) .with_headers(Headers::default().with_header("Range", "bytes=26-6087")) .with_class(Class::Header)], )); @@ -410,13 +410,7 @@ pub mod tests { .await } - pub(crate) fn expected_url(storage: Arc>) -> String { - format!( - "https://127.0.0.1:8081{}", - storage - .base_path() - .join("htsnexus_test_NA12878.cram") - .to_string_lossy() - ) + pub(crate) fn expected_url() -> String { + "https://127.0.0.1:8081/data/htsnexus_test_NA12878.cram".to_string() } } diff --git a/htsget-search/src/htsget/from_storage.rs b/htsget-search/src/htsget/from_storage.rs index 98161bbfc..89550dea7 100644 --- a/htsget-search/src/htsget/from_storage.rs +++ b/htsget-search/src/htsget/from_storage.rs @@ -115,7 +115,7 @@ mod tests { let expected_response = Ok(Response::new( Format::Bam, - vec![Url::new(bam_expected_url(htsget.storage())) + vec![Url::new(bam_expected_url()) .with_headers(Headers::default().with_header("Range", "bytes=4668-2596799"))], )); assert_eq!(response, expected_response) @@ -134,7 +134,7 @@ mod tests { let expected_response = Ok(Response::new( Format::Vcf, - vec![Url::new(vcf_expected_url(htsget.storage(), filename)) + vec![Url::new(vcf_expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-823"))], )); assert_eq!(response, expected_response) diff --git a/htsget-search/src/htsget/mod.rs b/htsget-search/src/htsget/mod.rs index 7f455caa9..2cc3a0d7d 100644 --- a/htsget-search/src/htsget/mod.rs +++ b/htsget-search/src/htsget/mod.rs @@ -102,15 +102,16 @@ impl From for HtsGetError { StorageError::InvalidKey(key) => { Self::InvalidInput(format!("Wrong key derived from ID: {}", key)) } - StorageError::IoError(e) => Self::IoError(format!("Io error: {}", e)), + StorageError::IoError(_, _) => Self::IoError(format!("Io Error: {:?}", err)), #[cfg(feature = "s3-storage")] - StorageError::AwsS3Error { .. } => Self::IoError(format!("AWS S3 error: {:?}", err)), + StorageError::AwsS3Error(_, _) => Self::IoError(format!("AWS S3 error: {:?}", err)), StorageError::TicketServerError(e) => { Self::InternalError(format!("Error using url response server: {}", e)) } StorageError::InvalidInput(e) => Self::InvalidInput(format!("Invalid input: {}", e)), StorageError::InvalidUri(e) => Self::InternalError(format!("Invalid uri produced: {}", e)), StorageError::InvalidAddress(e) => Self::InternalError(format!("Invalid address: {}", e)), + StorageError::InternalError(e) => Self::InternalError(e), } } } diff --git a/htsget-search/src/htsget/vcf_search.rs b/htsget-search/src/htsget/vcf_search.rs index bc6243109..70d57262d 100644 --- a/htsget-search/src/htsget/vcf_search.rs +++ b/htsget-search/src/htsget/vcf_search.rs @@ -176,7 +176,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Vcf, - vec![Url::new(expected_url(storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-3367"))], )); assert_eq!(response, expected_response) @@ -195,7 +195,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Vcf, - vec![Url::new(expected_url(storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-823"))], )); assert_eq!(response, expected_response) @@ -217,7 +217,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Vcf, - vec![Url::new(expected_url(storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-3367"))], )); assert_eq!(response, expected_response) @@ -254,7 +254,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Vcf, - vec![Url::new(expected_url(storage, filename)) + vec![Url::new(expected_url(filename)) .with_headers(Headers::default().with_header("Range", "bytes=0-823")) .with_class(Class::Header)], )); @@ -284,13 +284,7 @@ pub mod tests { .await } - pub(crate) fn expected_url(storage: Arc>, name: &str) -> String { - format!( - "https://127.0.0.1:8081{}", - storage - .base_path() - .join(format!("{}.vcf.gz", name)) - .to_string_lossy() - ) + pub(crate) fn expected_url(name: &str) -> String { + format!("https://127.0.0.1:8081/data/{}.vcf.gz", name) } } diff --git a/htsget-search/src/storage/axum_server.rs b/htsget-search/src/storage/axum_server.rs index dc27b6825..48b81f08d 100644 --- a/htsget-search/src/storage/axum_server.rs +++ b/htsget-search/src/storage/axum_server.rs @@ -25,7 +25,7 @@ use tokio_rustls::TlsAcceptor; use tower::MakeService; use tracing::debug; -use crate::storage::StorageError::TicketServerError; +use crate::storage::StorageError::{IoError, TicketServerError}; use crate::storage::UrlFormatter; use super::{Result, StorageError}; @@ -37,6 +37,8 @@ pub struct HttpsFormatter { } impl HttpsFormatter { + const SERVE_ASSETS_AT: &'static str = "/data"; + pub fn new(ip: impl Into, port: impl Into) -> Result { Ok(Self { addr: SocketAddr::from_str(&format!("{}:{}", ip.into(), port.into()))?, @@ -45,7 +47,7 @@ impl HttpsFormatter { /// Eagerly bind the address by returing an AxumStorageServer. pub async fn bind_axum_server(&self) -> Result { - AxumStorageServer::bind_addr(&self.addr).await + AxumStorageServer::bind_addr(&self.addr, Self::SERVE_ASSETS_AT).await } } @@ -65,22 +67,26 @@ impl From for HttpsFormatter { #[derive(Debug)] pub struct AxumStorageServer { listener: AddrIncoming, + serve_assets_at: String, } impl AxumStorageServer { - const SERVE_ASSETS_AT: &'static str = "/data"; - /// Eagerly bind the the address for use with the server, returning any errors. - pub async fn bind_addr(addr: &SocketAddr) -> Result { - let listener = TcpListener::bind(addr).await?; + pub async fn bind_addr(addr: &SocketAddr, serve_assets_at: impl Into) -> Result { + let listener = TcpListener::bind(addr) + .await + .map_err(|err| IoError("Failed to bind ticket server addr".to_string(), err))?; let listener = AddrIncoming::from_listener(listener)?; - Ok(Self { listener }) + Ok(Self { + serve_assets_at: serve_assets_at.into(), + listener, + }) } /// Run the actual server, using the provided path, key and certificate. pub async fn serve>(&mut self, path: P, key: P, cert: P) -> Result<()> { let mut app = Router::new() - .merge(SpaRouter::new(Self::SERVE_ASSETS_AT, path)) + .merge(SpaRouter::new(&self.serve_assets_at, path)) .into_make_service_with_connect_info::(); let rustls_config = Self::rustls_server_config(key, cert)?; @@ -89,7 +95,7 @@ impl AxumStorageServer { loop { let stream = poll_fn(|cx| Pin::new(&mut self.listener).poll_accept(cx)) .await - .ok_or_else(|| TicketServerError("Poll accept failed.".to_string()))? + .ok_or_else(|| TicketServerError("Poll accept failed".to_string()))? .map_err(|err| TicketServerError(err.to_string()))?; let acceptor = acceptor.clone(); @@ -108,11 +114,20 @@ impl AxumStorageServer { } fn rustls_server_config>(key: P, cert: P) -> Result> { - let mut key_reader = BufReader::new(File::open(key)?); - let mut cert_reader = BufReader::new(File::open(cert)?); - - let key = PrivateKey(pkcs8_private_keys(&mut key_reader)?.remove(0)); - let certs = certs(&mut cert_reader)? + let mut key_reader = BufReader::new( + File::open(key).map_err(|err| IoError("Failed to open key file".to_string(), err))?, + ); + let mut cert_reader = BufReader::new( + File::open(cert).map_err(|err| IoError("Failed to open cert file".to_string(), err))?, + ); + + let key = PrivateKey( + pkcs8_private_keys(&mut key_reader) + .map_err(|err| IoError("Failed to read private keys".to_string(), err))? + .remove(0), + ); + let certs = certs(&mut cert_reader) + .map_err(|err| IoError("Failed to read certificate".to_string(), err))? .into_iter() .map(Certificate) .collect(); @@ -136,11 +151,11 @@ impl From for StorageError { } impl UrlFormatter for HttpsFormatter { - fn format_url(&self, path: String) -> Result { + fn format_url>(&self, key: K) -> Result { http::uri::Builder::new() .scheme(http::uri::Scheme::HTTPS) .authority(self.addr.to_string()) - .path_and_query(path) + .path_and_query(format!("{}/{}", Self::SERVE_ASSETS_AT, key.as_ref())) .build() .map_err(|err| StorageError::InvalidUri(err.to_string())) .map(|value| value.to_string()) @@ -193,7 +208,7 @@ mod tests { // Start server. let addr = SocketAddr::from_str(&format!("{}:{}", "127.0.0.1", "8080")).unwrap(); - let mut server = AxumStorageServer::bind_addr(&addr).await.unwrap(); + let mut server = AxumStorageServer::bind_addr(&addr, "/data").await.unwrap(); tokio::spawn(async move { server .serve(base_path.path(), &key_path, &cert_path) @@ -220,8 +235,11 @@ mod tests { fn https_formatter_format_authority() { let formatter = HttpsFormatter::new("127.0.0.1", "8080").unwrap(); assert_eq!( - formatter.format_url("/path".to_string()).unwrap(), - "https://127.0.0.1:8080/path" + formatter.format_url("path").unwrap(), + format!( + "https://127.0.0.1:8080{}/path", + HttpsFormatter::SERVE_ASSETS_AT + ) ) } } diff --git a/htsget-search/src/storage/local.rs b/htsget-search/src/storage/local.rs index 44aab9eb4..066b0ac93 100644 --- a/htsget-search/src/storage/local.rs +++ b/htsget-search/src/storage/local.rs @@ -50,12 +50,7 @@ impl LocalStorage { let key: &str = key.as_ref(); self .base_path - .join( - self - .id_resolver - .resolve_id(key) - .ok_or_else(|| StorageError::InvalidKey(key.to_string()))?, - ) + .join(self.resolve_key(key)?) .canonicalize() .map_err(|_| StorageError::InvalidKey(key.to_string())) .and_then(|path| { @@ -72,9 +67,19 @@ impl LocalStorage { }) } + pub(crate) fn resolve_key>(&self, key: K) -> Result { + let key: &str = key.as_ref(); + self + .id_resolver + .resolve_id(key) + .ok_or_else(|| StorageError::InvalidKey(key.to_string())) + } + async fn get>(&self, key: K) -> Result { let path = self.get_path_from_key(&key)?; - Ok(File::open(path).await?) + File::open(path) + .await + .map_err(|_| StorageError::KeyNotFound(key.as_ref().to_string())) } } @@ -91,11 +96,11 @@ impl Storage for LocalStorage { /// Get a url for the file at key. async fn url + Send>(&self, key: K, options: UrlOptions) -> Result { let path = self.get_path_from_key(&key)?; - let url = Url::new( - self - .url_formatter - .format_url(path.to_string_lossy().to_string())?, - ); + let path = path + .strip_prefix(&self.base_path) + .map_err(|err| StorageError::InternalError(err.to_string()))? + .to_string_lossy(); + let url = Url::new(self.url_formatter.format_url(&path)?); let url = options.apply(url); debug!(calling_from = ?self, key = key.as_ref(), ?url, "Getting url with key {:?}", key.as_ref()); Ok(url) @@ -199,10 +204,7 @@ pub(crate) mod tests { async fn url_of_existing_key() { with_local_storage(|storage| async move { let result = Storage::url(&storage, "folder/../key1", UrlOptions::default()).await; - let expected = Url::new(format!( - "https://127.0.0.1:8081{}", - storage.base_path().join("key1").to_string_lossy() - )); + let expected = Url::new("https://127.0.0.1:8081/data/key1"); assert!(matches!(result, Ok(url) if url == expected)); }) .await; @@ -217,11 +219,8 @@ pub(crate) mod tests { UrlOptions::default().with_range(BytesRange::new(Some(7), Some(9))), ) .await; - let expected = Url::new(format!( - "https://127.0.0.1:8081{}", - storage.base_path().join("key1").to_string_lossy() - )) - .with_headers(Headers::default().with_header("Range", "bytes=7-9")); + let expected = Url::new("https://127.0.0.1:8081/data/key1") + .with_headers(Headers::default().with_header("Range", "bytes=7-9")); assert!(matches!(result, Ok(url) if url == expected)); }) .await; @@ -236,11 +235,8 @@ pub(crate) mod tests { UrlOptions::default().with_range(BytesRange::new(Some(7), None)), ) .await; - let expected = Url::new(format!( - "https://127.0.0.1:8081{}", - storage.base_path().join("key1").to_string_lossy() - )) - .with_headers(Headers::default().with_header("Range", "bytes=7-")); + let expected = Url::new("https://127.0.0.1:8081/data/key1") + .with_headers(Headers::default().with_header("Range", "bytes=7-")); assert!(matches!(result, Ok(url) if url == expected)); }) .await; diff --git a/htsget-search/src/storage/mod.rs b/htsget-search/src/storage/mod.rs index 0c3a9b478..45ca64240 100644 --- a/htsget-search/src/storage/mod.rs +++ b/htsget-search/src/storage/mod.rs @@ -42,7 +42,7 @@ pub trait Storage { /// Formats a url for use with storage. pub trait UrlFormatter { /// Returns the url with the path. - fn format_url(&self, path: String) -> Result; + fn format_url>(&self, key: K) -> Result; } #[derive(Error, Debug)] @@ -53,8 +53,8 @@ pub enum StorageError { #[error("Key not found: {0}")] KeyNotFound(String), - #[error("Io error: {0}")] - IoError(#[from] io::Error), + #[error("Io error: {0} {1}")] + IoError(String, io::Error), #[cfg(feature = "s3-storage")] #[error("Aws error: {0}, with key: {1}")] @@ -71,11 +71,17 @@ pub enum StorageError { #[error("Invalid address: {0}")] InvalidAddress(AddrParseError), + + #[error("Internal error: {0}")] + InternalError(String), } impl From for io::Error { fn from(err: StorageError) -> Self { - Self::new(ErrorKind::Other, err) + match err { + StorageError::IoError(_, ref io_error) => Self::new(io_error.kind(), err), + err => Self::new(ErrorKind::Other, err), + } } } diff --git a/htsget-test-utils/src/server_tests.rs b/htsget-test-utils/src/server_tests.rs index 0e967fade..1daaa726a 100644 --- a/htsget-test-utils/src/server_tests.rs +++ b/htsget-test-utils/src/server_tests.rs @@ -15,7 +15,7 @@ pub fn test_response(response: &Response, config: &Config, class: Class) { let url_path = expected_local_storage_path(config); assert!(response.is_success()); assert_eq!( - expected_response(&config.path, class, url_path), + expected_response(class, url_path), response.deserialize_body().unwrap() ); } @@ -37,7 +37,7 @@ pub async fn test_get(tester: &impl TestServer) { let request = tester .get_request() .method(Method::GET.to_string()) - .uri("/variants/data/vcf/sample1-bcbio-cancer"); + .uri("/variants/vcf/sample1-bcbio-cancer"); let response = tester.test_server(request).await; test_response(&response, tester.get_config(), Class::Body); } @@ -46,7 +46,7 @@ fn post_request(tester: &impl TestServer) -> T { tester .get_request() .method(Method::POST.to_string()) - .uri("/variants/data/vcf/sample1-bcbio-cancer") + .uri("/variants/vcf/sample1-bcbio-cancer") .insert_header(Header { name: http::header::CONTENT_TYPE.to_string(), value: mime::APPLICATION_JSON.to_string(), @@ -65,7 +65,7 @@ pub async fn test_parameterized_get(tester: &impl TestServer) let request = tester .get_request() .method(Method::GET.to_string()) - .uri("/variants/data/vcf/sample1-bcbio-cancer?format=VCF&class=header"); + .uri("/variants/vcf/sample1-bcbio-cancer?format=VCF&class=header"); let response = tester.test_server(request).await; test_response(&response, tester.get_config(), Class::Header); } @@ -98,28 +98,20 @@ pub async fn test_service_info(tester: &impl TestServer) { } fn expected_local_storage_path(config: &Config) -> String { - format!("https://{}", config.addr) + format!("https://{}", config.ticket_server_addr) } /// An example VCF search response. -pub fn expected_response(path: &Path, class: Class, url_path: String) -> JsonResponse { +pub fn expected_response(class: Class, url_path: String) -> JsonResponse { let mut headers = HashMap::new(); headers.insert("Range".to_string(), "bytes=0-3367".to_string()); JsonResponse::from_response(HtsgetResponse::new( Format::Vcf, - vec![Url::new(format!( - "{}{}", - url_path, - path - .join("data") - .join("vcf") - .join("sample1-bcbio-cancer.vcf.gz") - .canonicalize() - .unwrap() - .to_string_lossy() - )) - .with_headers(Headers::new(headers)) - .with_class(class)], + vec![ + Url::new(format!("{}/data/vcf/sample1-bcbio-cancer.vcf.gz", url_path)) + .with_headers(Headers::new(headers)) + .with_class(class), + ], )) } @@ -129,13 +121,11 @@ pub fn default_dir() -> PathBuf { .parent() .unwrap() .to_path_buf() - .canonicalize() - .unwrap() } /// Default config using the current cargo manifest directory. pub fn default_test_config() -> Config { - std::env::set_var("HTSGET_PATH", default_dir()); + std::env::set_var("HTSGET_PATH", default_dir().join("data")); Config::from_env().expect("Expected valid environment variables.") } From b252096b805ef895e0cf6ed6515b0ddd4ef71626 Mon Sep 17 00:00:00 2001 From: Marko Malenic Date: Fri, 27 May 2022 17:57:08 +1000 Subject: [PATCH 41/50] Fix certificate errors by using rustls-tls. --- htsget-http-actix/Cargo.toml | 2 +- .../benches/request-benchmark.rs | 22 +++++++++++-------- htsget-test-utils/src/server_tests.rs | 2 +- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/htsget-http-actix/Cargo.toml b/htsget-http-actix/Cargo.toml index 94f51b65e..f084edad9 100644 --- a/htsget-http-actix/Cargo.toml +++ b/htsget-http-actix/Cargo.toml @@ -26,7 +26,7 @@ htsget-test-utils = { path = "../htsget-test-utils", features = ["server-tests"] async-trait = "0.1" criterion = { version = "0.3", features = ["async_tokio"] } -reqwest = { version = "0.11", features = ["json", "blocking"] } +reqwest = { version = "0.11", features = ["json", "blocking", "rustls-tls"] } tempfile = "3.3" [[bench]] diff --git a/htsget-http-actix/benches/request-benchmark.rs b/htsget-http-actix/benches/request-benchmark.rs index 3273e7902..2939277e4 100644 --- a/htsget-http-actix/benches/request-benchmark.rs +++ b/htsget-http-actix/benches/request-benchmark.rs @@ -6,10 +6,14 @@ use htsget_test_utils::util::generate_test_certificates; use reqwest::blocking::Client; use serde::{Deserialize, Serialize}; use std::borrow::Borrow; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process::{Child, Command}; use std::thread::sleep; use std::{convert::TryInto, fs, time::Duration}; +use std::fs::File; +use std::io::Read; +use reqwest::blocking::ClientBuilder; +use reqwest::Certificate; use tempfile::TempDir; #[derive(Serialize)] @@ -37,7 +41,7 @@ const BENCHMARK_DURATION_SECONDS: u64 = 15; const NUMBER_OF_EXECUTIONS: usize = 150; fn request(url: reqwest::Url, json_content: &impl Serialize) -> reqwest::Result { - let client = Client::new(); + let client = ClientBuilder::new().danger_accept_invalid_certs(true).use_rustls_tls().build().unwrap(); let response: JsonResponse = client.get(url).json(json_content).send()?.json()?; Ok( response @@ -70,7 +74,7 @@ fn bench_pair( name: &str, htsget_url: reqwest::Url, refserver_url: reqwest::Url, - json_content: &impl Serialize, + json_content: &impl Serialize ) { group.bench_with_input( format!("{} {}", name, "htsget-rs"), @@ -122,7 +126,7 @@ fn start_htsget_rs() -> (Child, String) { .arg("-p") .arg("htsget-http-actix") .env("HTSGET_TICKET_SERVER_KEY", key_path) - .env("HTSGET_TICKET_SERVER_CERT", cert_path) + .env("HTSGET_TICKET_SERVER_CERT", &cert_path) .spawn() .unwrap(); @@ -204,7 +208,7 @@ fn criterion_benchmark(c: &mut Criterion) { "[LIGHT] simple request", format_url(&htsget_rs_url, "reads/bam/htsnexus_test_NA12878"), format_url(&htsget_refserver_url, "reads/htsnexus_test_NA12878"), - &Empty {}, + &Empty {} ); let json_content = PostRequest { @@ -224,7 +228,7 @@ fn criterion_benchmark(c: &mut Criterion) { "[LIGHT] with region", format_url(&htsget_rs_url, "reads/bam/htsnexus_test_NA12878"), format_url(&htsget_refserver_url, "reads/htsnexus_test_NA12878"), - &json_content, + &json_content ); let json_content = PostRequest { @@ -251,7 +255,7 @@ fn criterion_benchmark(c: &mut Criterion) { "[LIGHT] with two regions", format_url(&htsget_rs_url, "reads/bam/htsnexus_test_NA12878"), format_url(&htsget_refserver_url, "reads/htsnexus_test_NA12878"), - &json_content, + &json_content ); let json_content = PostRequest { @@ -271,7 +275,7 @@ fn criterion_benchmark(c: &mut Criterion) { "[LIGHT] with VCF", format_url(&htsget_rs_url, "variants/vcf/sample1-bcbio-cancer"), format_url(&htsget_refserver_url, "variants/sample1-bcbio-cancer"), - &json_content, + &json_content ); let json_content = PostRequest { @@ -291,7 +295,7 @@ fn criterion_benchmark(c: &mut Criterion) { "[HEAVY] with big VCF", format_url(&htsget_rs_url, "variants/vcf/internationalgenomesample"), format_url(&htsget_refserver_url, "variants/internationalgenomesample"), - &json_content, + &json_content ); group.finish(); diff --git a/htsget-test-utils/src/server_tests.rs b/htsget-test-utils/src/server_tests.rs index d1d767e98..2f9f516ab 100644 --- a/htsget-test-utils/src/server_tests.rs +++ b/htsget-test-utils/src/server_tests.rs @@ -191,6 +191,6 @@ pub fn default_test_config() -> Config { /// Get the event associated with the file. pub fn get_test_file>(path: P) -> String { - let path = default_dir().join(path); + let path = default_dir().join("data").join(path); fs::read_to_string(path).expect("Failed to read file.") } From 25e364d68e1442ea7d01e5556b26e2b9781693c7 Mon Sep 17 00:00:00 2001 From: Marko Malenic Date: Mon, 30 May 2022 13:45:41 +1000 Subject: [PATCH 42/50] Http byte ranges are inclusive for ending range, fix this and all affected tests. --- htsget-devtools/Cargo.toml | 4 ++ htsget-devtools/src/main.rs | 1 + htsget-http-actix/Cargo.toml | 1 + .../benches/request-benchmark.rs | 72 +++++++++++-------- htsget-http-actix/src/main.rs | 2 +- htsget-http-core/src/json_response.rs | 12 ++-- htsget-http-core/src/lib.rs | 8 +-- htsget-search/Cargo.toml | 1 + htsget-search/src/htsget/bam_search.rs | 14 ++-- htsget-search/src/htsget/bcf_search.rs | 8 +-- htsget-search/src/htsget/cram_search.rs | 12 ++-- htsget-search/src/htsget/from_storage.rs | 4 +- htsget-search/src/htsget/search.rs | 2 +- htsget-search/src/htsget/vcf_search.rs | 8 +-- htsget-search/src/storage/axum_server.rs | 4 +- htsget-search/src/storage/mod.rs | 25 ++++++- htsget-test-utils/src/server_tests.rs | 2 +- 17 files changed, 110 insertions(+), 70 deletions(-) diff --git a/htsget-devtools/Cargo.toml b/htsget-devtools/Cargo.toml index 9cc91a2dd..5edba1c94 100644 --- a/htsget-devtools/Cargo.toml +++ b/htsget-devtools/Cargo.toml @@ -10,3 +10,7 @@ serde = { version = "~1.0", features = ["derive"] } serde_yaml = "~0.8" noodles = { version = "0.18.0", features = ["bam", "bcf", "bgzf", "cram", "csi", "sam", "tabix", "vcf"] } + +reqwest = { version = "0.11", features = ["json", "blocking", "rustls-tls"] } +htsget-http-actix = { path = "../htsget-http-actix" } +htsget-http-core = { path = "../htsget-http-core" } \ No newline at end of file diff --git a/htsget-devtools/src/main.rs b/htsget-devtools/src/main.rs index d44561413..d8426f821 100644 --- a/htsget-devtools/src/main.rs +++ b/htsget-devtools/src/main.rs @@ -1,6 +1,7 @@ mod bam; mod bcf; mod vcf; +mod test_server; fn main() { let path = std::env::current_dir() diff --git a/htsget-http-actix/Cargo.toml b/htsget-http-actix/Cargo.toml index f084edad9..fb090a714 100644 --- a/htsget-http-actix/Cargo.toml +++ b/htsget-http-actix/Cargo.toml @@ -20,6 +20,7 @@ futures = { version = "0.3" } tokio = { version = "1.17", features = ["full"] } tracing-actix-web = "0.5" tracing = "0.1" +tracing-subscriber = "0.3" [dev-dependencies] htsget-test-utils = { path = "../htsget-test-utils", features = ["server-tests"], default-features = false } diff --git a/htsget-http-actix/benches/request-benchmark.rs b/htsget-http-actix/benches/request-benchmark.rs index 2939277e4..d87ee3529 100644 --- a/htsget-http-actix/benches/request-benchmark.rs +++ b/htsget-http-actix/benches/request-benchmark.rs @@ -6,14 +6,13 @@ use htsget_test_utils::util::generate_test_certificates; use reqwest::blocking::Client; use serde::{Deserialize, Serialize}; use std::borrow::Borrow; -use std::path::{Path, PathBuf}; -use std::process::{Child, Command}; +use std::path::PathBuf; +use tokio::process::{Child, Command}; use std::thread::sleep; use std::{convert::TryInto, fs, time::Duration}; -use std::fs::File; -use std::io::Read; +use std::collections::HashMap; +use actix_web::web::head; use reqwest::blocking::ClientBuilder; -use reqwest::Certificate; use tempfile::TempDir; #[derive(Serialize)] @@ -40,26 +39,24 @@ const REFSERVER_DOCKER_IMAGE: &str = "ga4gh/htsget-refserver:1.5.0"; const BENCHMARK_DURATION_SECONDS: u64 = 15; const NUMBER_OF_EXECUTIONS: usize = 150; -fn request(url: reqwest::Url, json_content: &impl Serialize) -> reqwest::Result { - let client = ClientBuilder::new().danger_accept_invalid_certs(true).use_rustls_tls().build().unwrap(); - let response: JsonResponse = client.get(url).json(json_content).send()?.json()?; - Ok( - response +fn request(url: reqwest::Url, json_content: &impl Serialize, client: &Client) -> usize { + let response: JsonResponse = client.post(url).json(json_content).send().unwrap().json().unwrap(); + let a = response .htsget .urls .iter() .map(|json_url| { client .get(&json_url.url) - .headers(json_url.headers.borrow().try_into().unwrap()) + .headers(json_url.headers.as_ref().unwrap_or(&HashMap::default()).try_into().unwrap()) .send() .unwrap() .bytes() .unwrap() .len() }) - .sum(), - ) + .sum(); + a } fn format_url(url: &str, path: &str) -> reqwest::Url { @@ -76,16 +73,17 @@ fn bench_pair( refserver_url: reqwest::Url, json_content: &impl Serialize ) { + let client = ClientBuilder::new().danger_accept_invalid_certs(true).use_rustls_tls().build().unwrap(); group.bench_with_input( format!("{} {}", name, "htsget-rs"), &htsget_url, - |b, input| b.iter(|| request(input.clone(), json_content)), - ); - group.bench_with_input( - format!("{} {}", name, "htsget-refserver"), - &refserver_url, - |b, input| b.iter(|| request(input.clone(), json_content)), + |b, input| b.iter(|| request(input.clone(), json_content, &client)), ); + // group.bench_with_input( + // format!("{} {}", name, "htsget-refserver"), + // &refserver_url, + // |b, input| b.iter(|| request(input.clone(), json_content, &client)), + // ); } #[cfg(target_os = "windows")] @@ -101,7 +99,7 @@ pub fn new_command(cmd: &str) -> Command { Command::new(cmd) } -fn query_server_until_response(url: reqwest::Url) { +async fn query_server_until_response(url: reqwest::Url) { let client = Client::new(); for _ in 0..120 { sleep(Duration::from_secs(1)); @@ -114,7 +112,7 @@ fn query_server_until_response(url: reqwest::Url) { } } -fn start_htsget_rs() -> (Child, String) { +async fn start_htsget_rs() -> (Child, String) { let config = default_test_config(); let base_path = TempDir::new().unwrap(); @@ -127,16 +125,18 @@ fn start_htsget_rs() -> (Child, String) { .arg("htsget-http-actix") .env("HTSGET_TICKET_SERVER_KEY", key_path) .env("HTSGET_TICKET_SERVER_CERT", &cert_path) + .env("RUST_LOG", "debug") + .kill_on_drop(true) .spawn() .unwrap(); let htsget_rs_url = format!("http://{}", config.addr); - query_server_until_response(format_url(&htsget_rs_url, "reads/service-info")); + query_server_until_response(format_url(&htsget_rs_url, "reads/service-info")).await; (child, htsget_rs_url) } -fn start_htsget_refserver() -> (Child, String) { +async fn start_htsget_refserver() -> (Child, String) { let config_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("benches") .join("htsget-refserver-config.json"); @@ -150,13 +150,13 @@ fn start_htsget_refserver() -> (Child, String) { .spawn() .unwrap() .wait() + .await .unwrap(); let child = new_command("docker") .current_dir(default_dir()) .arg("container") .arg("run") - .arg("-d") .arg("-p") .arg(format!( "{}:3000", @@ -185,30 +185,40 @@ fn start_htsget_refserver() -> (Child, String) { .arg("./htsget-refserver") .arg("-config") .arg("/config/htsget-refserver-config.json") + .kill_on_drop(true) .spawn() .unwrap(); let refserver_url = refserver_config.htsget_config.props.host; - query_server_until_response(format_url(&refserver_url, "reads/service-info")); + query_server_until_response(format_url(&refserver_url, "reads/service-info")).await; (child, refserver_url) } -fn criterion_benchmark(c: &mut Criterion) { +#[tokio::main] +async fn criterion_benchmark(c: &mut Criterion) { let mut group = c.benchmark_group("Requests"); group .sample_size(NUMBER_OF_EXECUTIONS) .measurement_time(Duration::from_secs(BENCHMARK_DURATION_SECONDS)); - let (mut htsget_rs_server, htsget_rs_url) = start_htsget_rs(); - let (mut htsget_refserver_server, htsget_refserver_url) = start_htsget_refserver(); + let (mut htsget_rs_server, htsget_rs_url) = start_htsget_rs().await; + let (mut htsget_refserver_server, htsget_refserver_url) = start_htsget_refserver().await; + let json_content = PostRequest { + format: None, + class: None, + fields: None, + tags: None, + notags: None, + regions: None, + }; bench_pair( &mut group, "[LIGHT] simple request", format_url(&htsget_rs_url, "reads/bam/htsnexus_test_NA12878"), format_url(&htsget_refserver_url, "reads/htsnexus_test_NA12878"), - &Empty {} + &json_content ); let json_content = PostRequest { @@ -300,8 +310,8 @@ fn criterion_benchmark(c: &mut Criterion) { group.finish(); - htsget_rs_server.kill().unwrap(); - htsget_refserver_server.kill().unwrap(); + htsget_rs_server.kill().await.unwrap(); + htsget_refserver_server.kill().await.unwrap(); } criterion_group!(benches, criterion_benchmark); diff --git a/htsget-http-actix/src/main.rs b/htsget-http-actix/src/main.rs index c82cf52ce..bae79f5aa 100644 --- a/htsget-http-actix/src/main.rs +++ b/htsget-http-actix/src/main.rs @@ -9,6 +9,7 @@ use htsget_search::storage::axum_server::HttpsFormatter; #[actix_web::main] async fn main() -> std::io::Result<()> { + tracing_subscriber::fmt::init(); if args().len() > 1 { // Show help if command line options are provided println!("{}", USAGE); @@ -16,7 +17,6 @@ async fn main() -> std::io::Result<()> { } let config = Config::from_env()?; - println!("{:?}", config); match config.storage_type { StorageType::LocalStorage => local_storage_server(config).await, diff --git a/htsget-http-core/src/json_response.rs b/htsget-http-core/src/json_response.rs index 3a9fb898e..b7053ef92 100644 --- a/htsget-http-core/src/json_response.rs +++ b/htsget-http-core/src/json_response.rs @@ -40,23 +40,23 @@ impl HtsGetResponse { #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct JsonUrl { pub url: String, - pub headers: HashMap, - pub class: String, + pub headers: Option>, + pub class: Option, } impl JsonUrl { fn new(url: Url) -> Self { JsonUrl { url: url.url, - headers: match url.headers { + headers: Some(match url.headers { Some(headers) => headers.get_inner(), None => HashMap::new(), - }, - class: match url.class { + }), + class: Some(match url.class { Class::Body => "body", Class::Header => "header", } - .to_string(), + .to_string()), } } } diff --git a/htsget-http-core/src/lib.rs b/htsget-http-core/src/lib.rs index 21ad2a7db..008f0a51f 100644 --- a/htsget-http-core/src/lib.rs +++ b/htsget-http-core/src/lib.rs @@ -127,7 +127,7 @@ mod tests { let mut request = HashMap::new(); request.insert("id".to_string(), "bam/htsnexus_test_NA12878".to_string()); let mut headers = HashMap::new(); - headers.insert("Range".to_string(), "bytes=4668-2596799".to_string()); + headers.insert("Range".to_string(), "bytes=4668-2596798".to_string()); assert_eq!( get_response_for_get_request(get_searcher(), request, Endpoint::Reads).await, Ok(example_bam_json_response(headers)) @@ -153,7 +153,7 @@ mod tests { request.insert("start".to_string(), "149".to_string()); request.insert("end".to_string(), "200".to_string()); let mut headers = HashMap::new(); - headers.insert("Range".to_string(), "bytes=0-3367".to_string()); + headers.insert("Range".to_string(), "bytes=0-3366".to_string()); assert_eq!( get_response_for_get_request(get_searcher(), request, Endpoint::Variants).await, Ok(example_vcf_json_response(headers)) @@ -171,7 +171,7 @@ mod tests { regions: None, }; let mut headers = HashMap::new(); - headers.insert("Range".to_string(), "bytes=4668-2596799".to_string()); + headers.insert("Range".to_string(), "bytes=4668-2596798".to_string()); assert_eq!( get_response_for_post_request( get_searcher(), @@ -221,7 +221,7 @@ mod tests { }]), }; let mut headers = HashMap::new(); - headers.insert("Range".to_string(), "bytes=0-3367".to_string()); + headers.insert("Range".to_string(), "bytes=0-3366".to_string()); assert_eq!( get_response_for_post_request( get_searcher(), diff --git a/htsget-search/Cargo.toml b/htsget-search/Cargo.toml index b2305a06a..1d0cd5d5b 100644 --- a/htsget-search/Cargo.toml +++ b/htsget-search/Cargo.toml @@ -11,6 +11,7 @@ default = ["s3-storage"] [dependencies] # Axum server dependencies. hyper = "0.14" +tower-http = { version = "0.3", features = ["trace"] } axum = "0.5.1" axum-extra = { version = "0.2.1", features = ["spa"] } rustls-pemfile = "0.3" diff --git a/htsget-search/src/htsget/bam_search.rs b/htsget-search/src/htsget/bam_search.rs index c396bfdac..e427a0901 100644 --- a/htsget-search/src/htsget/bam_search.rs +++ b/htsget-search/src/htsget/bam_search.rs @@ -210,7 +210,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bam, vec![Url::new(expected_url()) - .with_headers(Headers::default().with_header("Range", "bytes=4668-2596799"))], + .with_headers(Headers::default().with_header("Range", "bytes=4668-2596798"))], )); assert_eq!(response, expected_response) }) @@ -228,7 +228,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bam, vec![Url::new(expected_url()) - .with_headers(Headers::default().with_header("Range", "bytes=2060795-2596799"))], + .with_headers(Headers::default().with_header("Range", "bytes=2060795-2596798"))], )); assert_eq!(response, expected_response) }) @@ -246,7 +246,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bam, vec![Url::new(expected_url()) - .with_headers(Headers::default().with_header("Range", "bytes=977196-2128166"))], + .with_headers(Headers::default().with_header("Range", "bytes=977196-2128165"))], )); assert_eq!(response, expected_response) }) @@ -268,11 +268,11 @@ pub mod tests { Format::Bam, vec![ Url::new(expected_url()) - .with_headers(Headers::default().with_header("Range", "bytes=256721-647346")), + .with_headers(Headers::default().with_header("Range", "bytes=256721-647345")), Url::new(expected_url()) - .with_headers(Headers::default().with_header("Range", "bytes=824361-842101")), + .with_headers(Headers::default().with_header("Range", "bytes=824361-842100")), Url::new(expected_url()) - .with_headers(Headers::default().with_header("Range", "bytes=977196-996015")), + .with_headers(Headers::default().with_header("Range", "bytes=977196-996014")), ], )); assert_eq!(response, expected_response) @@ -291,7 +291,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bam, vec![Url::new(expected_url()) - .with_headers(Headers::default().with_header("Range", "bytes=0-4668")) + .with_headers(Headers::default().with_header("Range", "bytes=0-4667")) .with_class(Class::Header)], )); assert_eq!(response, expected_response) diff --git a/htsget-search/src/htsget/bcf_search.rs b/htsget-search/src/htsget/bcf_search.rs index 1e816d484..4678615c4 100644 --- a/htsget-search/src/htsget/bcf_search.rs +++ b/htsget-search/src/htsget/bcf_search.rs @@ -171,7 +171,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bcf, vec![Url::new(expected_url(filename)) - .with_headers(Headers::default().with_header("Range", "bytes=0-3530"))], + .with_headers(Headers::default().with_header("Range", "bytes=0-3529"))], )); assert_eq!(response, expected_response) }) @@ -190,7 +190,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bcf, vec![Url::new(expected_url(filename)) - .with_headers(Headers::default().with_header("Range", "bytes=0-950"))], + .with_headers(Headers::default().with_header("Range", "bytes=0-949"))], )); assert_eq!(response, expected_response) }) @@ -212,7 +212,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bcf, vec![Url::new(expected_url(filename)) - .with_headers(Headers::default().with_header("Range", "bytes=0-3530"))], + .with_headers(Headers::default().with_header("Range", "bytes=0-3529"))], )); assert_eq!(response, expected_response) }) @@ -249,7 +249,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Bcf, vec![Url::new(expected_url(filename)) - .with_headers(Headers::default().with_header("Range", "bytes=0-950")) + .with_headers(Headers::default().with_header("Range", "bytes=0-949")) .with_class(Class::Header)], )); assert_eq!(response, expected_response) diff --git a/htsget-search/src/htsget/cram_search.rs b/htsget-search/src/htsget/cram_search.rs index 60961382a..fb67a6e7d 100644 --- a/htsget-search/src/htsget/cram_search.rs +++ b/htsget-search/src/htsget/cram_search.rs @@ -285,7 +285,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, vec![Url::new(expected_url()) - .with_headers(Headers::default().with_header("Range", "bytes=6087-1627756"))], + .with_headers(Headers::default().with_header("Range", "bytes=6087-1627755"))], )); assert_eq!(response, expected_response) }) @@ -303,7 +303,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, vec![Url::new(expected_url()) - .with_headers(Headers::default().with_header("Range", "bytes=1280106-1627756"))], + .with_headers(Headers::default().with_header("Range", "bytes=1280106-1627755"))], )); assert_eq!(response, expected_response) }) @@ -321,7 +321,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, vec![Url::new(expected_url()) - .with_headers(Headers::default().with_header("Range", "bytes=604231-1280106"))], + .with_headers(Headers::default().with_header("Range", "bytes=604231-1280105"))], )); assert_eq!(response, expected_response) }) @@ -342,7 +342,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, vec![Url::new(expected_url()) - .with_headers(Headers::default().with_header("Range", "bytes=6087-465709"))], + .with_headers(Headers::default().with_header("Range", "bytes=6087-465708"))], )); assert_eq!(response, expected_response) }) @@ -363,7 +363,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, vec![Url::new(expected_url()) - .with_headers(Headers::default().with_header("Range", "bytes=6087-604231"))], + .with_headers(Headers::default().with_header("Range", "bytes=6087-604230"))], )); assert_eq!(response, expected_response) }) @@ -381,7 +381,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Cram, vec![Url::new(expected_url()) - .with_headers(Headers::default().with_header("Range", "bytes=26-6087")) + .with_headers(Headers::default().with_header("Range", "bytes=26-6086")) .with_class(Class::Header)], )); assert_eq!(response, expected_response) diff --git a/htsget-search/src/htsget/from_storage.rs b/htsget-search/src/htsget/from_storage.rs index 89550dea7..e98a0f36f 100644 --- a/htsget-search/src/htsget/from_storage.rs +++ b/htsget-search/src/htsget/from_storage.rs @@ -116,7 +116,7 @@ mod tests { let expected_response = Ok(Response::new( Format::Bam, vec![Url::new(bam_expected_url()) - .with_headers(Headers::default().with_header("Range", "bytes=4668-2596799"))], + .with_headers(Headers::default().with_header("Range", "bytes=4668-2596798"))], )); assert_eq!(response, expected_response) }) @@ -135,7 +135,7 @@ mod tests { let expected_response = Ok(Response::new( Format::Vcf, vec![Url::new(vcf_expected_url(filename)) - .with_headers(Headers::default().with_header("Range", "bytes=0-823"))], + .with_headers(Headers::default().with_header("Range", "bytes=0-822"))], )); assert_eq!(response, expected_response) }) diff --git a/htsget-search/src/htsget/search.rs b/htsget-search/src/htsget/search.rs index f2ea574ac..c4a2741ba 100644 --- a/htsget-search/src/htsget/search.rs +++ b/htsget-search/src/htsget/search.rs @@ -248,7 +248,7 @@ where byte_ranges: Vec, ) -> Result { let mut storage_futures = FuturesUnordered::new(); - for range in byte_ranges { + for range in BytesRange::merge_all_from_pos(byte_ranges) { let options = UrlOptions::default() .with_range(range) .with_class(class.clone()); diff --git a/htsget-search/src/htsget/vcf_search.rs b/htsget-search/src/htsget/vcf_search.rs index 70d57262d..b6fbd80ed 100644 --- a/htsget-search/src/htsget/vcf_search.rs +++ b/htsget-search/src/htsget/vcf_search.rs @@ -177,7 +177,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Vcf, vec![Url::new(expected_url(filename)) - .with_headers(Headers::default().with_header("Range", "bytes=0-3367"))], + .with_headers(Headers::default().with_header("Range", "bytes=0-3366"))], )); assert_eq!(response, expected_response) }) @@ -196,7 +196,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Vcf, vec![Url::new(expected_url(filename)) - .with_headers(Headers::default().with_header("Range", "bytes=0-823"))], + .with_headers(Headers::default().with_header("Range", "bytes=0-822"))], )); assert_eq!(response, expected_response) }) @@ -218,7 +218,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Vcf, vec![Url::new(expected_url(filename)) - .with_headers(Headers::default().with_header("Range", "bytes=0-3367"))], + .with_headers(Headers::default().with_header("Range", "bytes=0-3366"))], )); assert_eq!(response, expected_response) }) @@ -255,7 +255,7 @@ pub mod tests { let expected_response = Ok(Response::new( Format::Vcf, vec![Url::new(expected_url(filename)) - .with_headers(Headers::default().with_header("Range", "bytes=0-823")) + .with_headers(Headers::default().with_header("Range", "bytes=0-822")) .with_class(Class::Header)], )); assert_eq!(response, expected_response) diff --git a/htsget-search/src/storage/axum_server.rs b/htsget-search/src/storage/axum_server.rs index 4580c8b59..c3c37e7e0 100644 --- a/htsget-search/src/storage/axum_server.rs +++ b/htsget-search/src/storage/axum_server.rs @@ -23,7 +23,7 @@ use tokio::net::TcpListener; use tokio_rustls::rustls::{Certificate, PrivateKey, ServerConfig}; use tokio_rustls::TlsAcceptor; use tower::MakeService; -use tracing::debug; +use tower_http::trace::TraceLayer; use crate::storage::StorageError::{IoError, TicketServerError}; use crate::storage::UrlFormatter; @@ -86,6 +86,7 @@ impl AxumStorageServer { /// Run the actual server, using the provided path, key and certificate. pub async fn serve>(&mut self, path: P, key: P, cert: P) -> Result<()> { let mut app = Router::new() + .layer(TraceLayer::new_for_http()) .merge(SpaRouter::new(&self.serve_assets_at, path)) .into_make_service_with_connect_info::(); @@ -99,7 +100,6 @@ impl AxumStorageServer { .map_err(|err| TicketServerError(err.to_string()))?; let acceptor = acceptor.clone(); - debug!(from = ?stream.remote_addr(), "Incoming request from {:?}", stream.remote_addr()); let app = app .make_service(&stream) .await diff --git a/htsget-search/src/storage/mod.rs b/htsget-search/src/storage/mod.rs index 45ca64240..a91bce4e1 100644 --- a/htsget-search/src/storage/mod.rs +++ b/htsget-search/src/storage/mod.rs @@ -129,6 +129,12 @@ impl BytesRange { self } + /// Convert an ending byte position (exclusive) to a ending byte range (inclusive). + /// Byte ranges are inclusive, e.g. 0-499 represents the first 500 bytes. + pub fn with_end_from_pos(self, pos: u64) -> Self { + self.with_end(pos - 1) + } + pub fn get_start(&self) -> Option { self.start } @@ -161,6 +167,7 @@ impl BytesRange { self } + /// Merge ranges, assuming ending byte ranges are exclusive. pub fn merge_all(mut ranges: Vec) -> Vec { if ranges.len() < 2 { ranges @@ -186,7 +193,7 @@ impl BytesRange { if current_range.overlaps(range) { current_range.merge_with(range); } else { - optimized_ranges.push(current_range.clone()); + optimized_ranges.push(current_range); current_range = range.clone(); } } @@ -196,6 +203,15 @@ impl BytesRange { optimized_ranges } } + + pub fn merge_all_from_pos(ranges: Vec) -> Vec { + ranges.into_iter().map(|range| { + match range.end { + None => range, + Some(pos) => range.with_end_from_pos(pos) + } + }).collect() + } } #[derive(Default)] @@ -554,6 +570,13 @@ mod tests { assert_eq!(BytesRange::merge_all(ranges), expected_ranges); } + #[test] + fn byte_ranges_with_end_pos() { + let result = BytesRange::default().with_start(5).with_end_from_pos(10); + let expected = BytesRange::new(Some(5), Some(9)); + assert_eq!(result, expected); + } + #[test] fn get_options_with_max_length() { let result = GetOptions::default().with_max_length(1); diff --git a/htsget-test-utils/src/server_tests.rs b/htsget-test-utils/src/server_tests.rs index 2f9f516ab..e967edd35 100644 --- a/htsget-test-utils/src/server_tests.rs +++ b/htsget-test-utils/src/server_tests.rs @@ -164,7 +164,7 @@ fn expected_local_storage_path(config: &Config) -> String { /// An example VCF search response. pub fn expected_response(class: Class, url_path: String) -> JsonResponse { let mut headers = HashMap::new(); - headers.insert("Range".to_string(), "bytes=0-3367".to_string()); + headers.insert("Range".to_string(), "bytes=0-3366".to_string()); JsonResponse::from_response(HtsgetResponse::new( Format::Vcf, vec![ From 020a9add74f0a5473cf317b37b4c65999bf1167a Mon Sep 17 00:00:00 2001 From: Marko Malenic Date: Mon, 30 May 2022 16:05:56 +1000 Subject: [PATCH 43/50] Add light and heavy benchmarks, update README.md. --- htsget-devtools/src/main.rs | 1 - htsget-http-actix/README.md | 19 ++- .../benches/request-benchmark.rs | 121 ++++++++++-------- htsget-http-core/src/json_response.rs | 12 +- htsget-search/src/storage/mod.rs | 11 +- 5 files changed, 90 insertions(+), 74 deletions(-) diff --git a/htsget-devtools/src/main.rs b/htsget-devtools/src/main.rs index d8426f821..d44561413 100644 --- a/htsget-devtools/src/main.rs +++ b/htsget-devtools/src/main.rs @@ -1,7 +1,6 @@ mod bam; mod bcf; mod vcf; -mod test_server; fn main() { let path = std::env::current_dir() diff --git a/htsget-http-actix/README.md b/htsget-http-actix/README.md index 86d2b3e09..4bb30bb8e 100644 --- a/htsget-http-actix/README.md +++ b/htsget-http-actix/README.md @@ -83,19 +83,24 @@ $ curl 127.0.0.1:8080/variants/service-info ``` ## Running the benchmarks -There are benchmarks for the htsget-search crate and for the htsget-http-actix crate. The first ones work like normal benchmarks, but the latter ones try to compare the performance of this implementation and the [reference implementation](https://github.com/ga4gh/htsget-refserver). For that, it is needed to start both servers before running `cargo bench`: +There are benchmarks for the htsget-search crate and for the htsget-http-actix crate. The first ones work like normal benchmarks, but the latter ones try to compare the performance of this implementation and the [reference implementation](https://github.com/ga4gh/htsget-refserver). +There are a set of light benchmarks, and one heavy benchmark. Light benchmarks can be performed by executing: -### Steps to perform -From inside the htsget-http-actix directory, start the htsget-rs server: ``` -HTSGET_PATH=../ cargo run --release & +cargo bench -p htsget-http-actix -- LIGHT ``` -Then start the htsget-refserver. There is a simple script prepared for that matter. Docker should be installed and `docker.service` should be running: + +In order to run the heavy benchmark, an additional vcf file should be downloaded, and placed in the `data/vcf` directory: + ``` -sudo bash benches/docker-htsget-refserver.sh +curl ftp://ftp.1000genomes.ebi.ac.uk/vol1/ftp/data_collections/1000_genomes_project/release/20190312_biallelic_SNV_and_INDEL/ALL.chr14.shapeit2_integrated_snvindels_v2a_27022019.GRCh38.phased.vcf.gz > data/vcf/internationalgenomesample.vcf.gz ``` -Now you should be able to run the benchmarks with `cargo bench`! +Then to run the heavy benchmark: + +``` +cargo bench -p htsget-http-actix -- HEAVY +``` ## Example Regular expressions In this example 'data/' is added after the first '/'. diff --git a/htsget-http-actix/benches/request-benchmark.rs b/htsget-http-actix/benches/request-benchmark.rs index d87ee3529..9ddeb680b 100644 --- a/htsget-http-actix/benches/request-benchmark.rs +++ b/htsget-http-actix/benches/request-benchmark.rs @@ -4,17 +4,19 @@ use htsget_http_core::{JsonResponse, PostRequest, Region}; use htsget_test_utils::server_tests::{default_dir, default_test_config}; use htsget_test_utils::util::generate_test_certificates; use reqwest::blocking::Client; +use reqwest::blocking::ClientBuilder; use serde::{Deserialize, Serialize}; -use std::borrow::Borrow; +use std::collections::HashMap; use std::path::PathBuf; -use tokio::process::{Child, Command}; +use std::process::{Child, Command}; use std::thread::sleep; use std::{convert::TryInto, fs, time::Duration}; -use std::collections::HashMap; -use actix_web::web::head; -use reqwest::blocking::ClientBuilder; use tempfile::TempDir; +const REFSERVER_DOCKER_IMAGE: &str = "ga4gh/htsget-refserver:1.5.0"; +const BENCHMARK_DURATION_SECONDS: u64 = 30; +const NUMBER_OF_EXECUTIONS: usize = 150; + #[derive(Serialize)] struct Empty {} @@ -35,28 +37,39 @@ struct RefserverAddr { host: String, } -const REFSERVER_DOCKER_IMAGE: &str = "ga4gh/htsget-refserver:1.5.0"; -const BENCHMARK_DURATION_SECONDS: u64 = 15; -const NUMBER_OF_EXECUTIONS: usize = 150; +struct DropGuard(Child); + +impl Drop for DropGuard { + fn drop(&mut self) { + drop(self.0.kill()); + } +} fn request(url: reqwest::Url, json_content: &impl Serialize, client: &Client) -> usize { - let response: JsonResponse = client.post(url).json(json_content).send().unwrap().json().unwrap(); - let a = response - .htsget - .urls - .iter() - .map(|json_url| { - client - .get(&json_url.url) - .headers(json_url.headers.as_ref().unwrap_or(&HashMap::default()).try_into().unwrap()) - .send() - .unwrap() - .bytes() - .unwrap() - .len() - }) - .sum(); - a + let response = client.post(url).json(json_content).send().unwrap(); + let response: JsonResponse = response.json().unwrap(); + response + .htsget + .urls + .iter() + .map(|json_url| { + client + .get(&json_url.url) + .headers( + json_url + .headers + .as_ref() + .unwrap_or(&HashMap::default()) + .try_into() + .unwrap(), + ) + .send() + .unwrap() + .bytes() + .unwrap() + .len() + }) + .sum() } fn format_url(url: &str, path: &str) -> reqwest::Url { @@ -71,19 +84,23 @@ fn bench_pair( name: &str, htsget_url: reqwest::Url, refserver_url: reqwest::Url, - json_content: &impl Serialize + json_content: &impl Serialize, ) { - let client = ClientBuilder::new().danger_accept_invalid_certs(true).use_rustls_tls().build().unwrap(); + let client = ClientBuilder::new() + .danger_accept_invalid_certs(true) + .use_rustls_tls() + .build() + .unwrap(); group.bench_with_input( format!("{} {}", name, "htsget-rs"), &htsget_url, |b, input| b.iter(|| request(input.clone(), json_content, &client)), ); - // group.bench_with_input( - // format!("{} {}", name, "htsget-refserver"), - // &refserver_url, - // |b, input| b.iter(|| request(input.clone(), json_content, &client)), - // ); + group.bench_with_input( + format!("{} {}", name, "htsget-refserver"), + &refserver_url, + |b, input| b.iter(|| request(input.clone(), json_content, &client)), + ); } #[cfg(target_os = "windows")] @@ -99,7 +116,7 @@ pub fn new_command(cmd: &str) -> Command { Command::new(cmd) } -async fn query_server_until_response(url: reqwest::Url) { +fn query_server_until_response(url: reqwest::Url) { let client = Client::new(); for _ in 0..120 { sleep(Duration::from_secs(1)); @@ -112,7 +129,7 @@ async fn query_server_until_response(url: reqwest::Url) { } } -async fn start_htsget_rs() -> (Child, String) { +fn start_htsget_rs() -> (DropGuard, String) { let config = default_test_config(); let base_path = TempDir::new().unwrap(); @@ -125,18 +142,16 @@ async fn start_htsget_rs() -> (Child, String) { .arg("htsget-http-actix") .env("HTSGET_TICKET_SERVER_KEY", key_path) .env("HTSGET_TICKET_SERVER_CERT", &cert_path) - .env("RUST_LOG", "debug") - .kill_on_drop(true) .spawn() .unwrap(); let htsget_rs_url = format!("http://{}", config.addr); - query_server_until_response(format_url(&htsget_rs_url, "reads/service-info")).await; + query_server_until_response(format_url(&htsget_rs_url, "reads/service-info")); - (child, htsget_rs_url) + (DropGuard(child), htsget_rs_url) } -async fn start_htsget_refserver() -> (Child, String) { +fn start_htsget_refserver() -> (DropGuard, String) { let config_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("benches") .join("htsget-refserver-config.json"); @@ -150,7 +165,6 @@ async fn start_htsget_refserver() -> (Child, String) { .spawn() .unwrap() .wait() - .await .unwrap(); let child = new_command("docker") @@ -185,25 +199,23 @@ async fn start_htsget_refserver() -> (Child, String) { .arg("./htsget-refserver") .arg("-config") .arg("/config/htsget-refserver-config.json") - .kill_on_drop(true) .spawn() .unwrap(); let refserver_url = refserver_config.htsget_config.props.host; - query_server_until_response(format_url(&refserver_url, "reads/service-info")).await; + query_server_until_response(format_url(&refserver_url, "reads/service-info")); - (child, refserver_url) + (DropGuard(child), refserver_url) } -#[tokio::main] -async fn criterion_benchmark(c: &mut Criterion) { +fn criterion_benchmark(c: &mut Criterion) { let mut group = c.benchmark_group("Requests"); group .sample_size(NUMBER_OF_EXECUTIONS) .measurement_time(Duration::from_secs(BENCHMARK_DURATION_SECONDS)); - let (mut htsget_rs_server, htsget_rs_url) = start_htsget_rs().await; - let (mut htsget_refserver_server, htsget_refserver_url) = start_htsget_refserver().await; + let (mut _htsget_rs_server, htsget_rs_url) = start_htsget_rs(); + let (mut _htsget_refserver_server, htsget_refserver_url) = start_htsget_refserver(); let json_content = PostRequest { format: None, @@ -218,7 +230,7 @@ async fn criterion_benchmark(c: &mut Criterion) { "[LIGHT] simple request", format_url(&htsget_rs_url, "reads/bam/htsnexus_test_NA12878"), format_url(&htsget_refserver_url, "reads/htsnexus_test_NA12878"), - &json_content + &json_content, ); let json_content = PostRequest { @@ -238,7 +250,7 @@ async fn criterion_benchmark(c: &mut Criterion) { "[LIGHT] with region", format_url(&htsget_rs_url, "reads/bam/htsnexus_test_NA12878"), format_url(&htsget_refserver_url, "reads/htsnexus_test_NA12878"), - &json_content + &json_content, ); let json_content = PostRequest { @@ -265,7 +277,7 @@ async fn criterion_benchmark(c: &mut Criterion) { "[LIGHT] with two regions", format_url(&htsget_rs_url, "reads/bam/htsnexus_test_NA12878"), format_url(&htsget_refserver_url, "reads/htsnexus_test_NA12878"), - &json_content + &json_content, ); let json_content = PostRequest { @@ -276,7 +288,7 @@ async fn criterion_benchmark(c: &mut Criterion) { notags: None, regions: Some(vec![Region { reference_name: "chrM".to_string(), - start: Some(0), + start: Some(1), end: Some(153), }]), }; @@ -285,7 +297,7 @@ async fn criterion_benchmark(c: &mut Criterion) { "[LIGHT] with VCF", format_url(&htsget_rs_url, "variants/vcf/sample1-bcbio-cancer"), format_url(&htsget_refserver_url, "variants/sample1-bcbio-cancer"), - &json_content + &json_content, ); let json_content = PostRequest { @@ -305,13 +317,10 @@ async fn criterion_benchmark(c: &mut Criterion) { "[HEAVY] with big VCF", format_url(&htsget_rs_url, "variants/vcf/internationalgenomesample"), format_url(&htsget_refserver_url, "variants/internationalgenomesample"), - &json_content + &json_content, ); group.finish(); - - htsget_rs_server.kill().await.unwrap(); - htsget_refserver_server.kill().await.unwrap(); } criterion_group!(benches, criterion_benchmark); diff --git a/htsget-http-core/src/json_response.rs b/htsget-http-core/src/json_response.rs index b7053ef92..d1c0984e9 100644 --- a/htsget-http-core/src/json_response.rs +++ b/htsget-http-core/src/json_response.rs @@ -52,11 +52,13 @@ impl JsonUrl { Some(headers) => headers.get_inner(), None => HashMap::new(), }), - class: Some(match url.class { - Class::Body => "body", - Class::Header => "header", - } - .to_string()), + class: Some( + match url.class { + Class::Body => "body", + Class::Header => "header", + } + .to_string(), + ), } } } diff --git a/htsget-search/src/storage/mod.rs b/htsget-search/src/storage/mod.rs index a91bce4e1..0b2190826 100644 --- a/htsget-search/src/storage/mod.rs +++ b/htsget-search/src/storage/mod.rs @@ -205,12 +205,13 @@ impl BytesRange { } pub fn merge_all_from_pos(ranges: Vec) -> Vec { - ranges.into_iter().map(|range| { - match range.end { + ranges + .into_iter() + .map(|range| match range.end { None => range, - Some(pos) => range.with_end_from_pos(pos) - } - }).collect() + Some(pos) => range.with_end_from_pos(pos), + }) + .collect() } } From a528f65fb36d3b81b7711d7847199097f862ea23 Mon Sep 17 00:00:00 2001 From: Marko Malenic Date: Mon, 30 May 2022 16:41:21 +1000 Subject: [PATCH 44/50] Update actions, remove files, update README.md. --- .github/workflows/action.yml | 26 ++++++++++++--- .github/workflows/pull_request.yml | 32 ------------------- htsget-http-actix/Cargo.toml | 5 +-- htsget-http-actix/README.md | 16 ++++++---- .../benches/docker-htsget-refserver.sh | 2 -- .../benches/dowload-heavy-test-files.sh | 1 - ...est-benchmark.rs => request_benchmarks.rs} | 0 htsget-search/Cargo.toml | 5 +-- .../{benchmark.rs => search_benchmarks.rs} | 0 9 files changed, 37 insertions(+), 50 deletions(-) delete mode 100644 .github/workflows/pull_request.yml delete mode 100755 htsget-http-actix/benches/docker-htsget-refserver.sh delete mode 100755 htsget-http-actix/benches/dowload-heavy-test-files.sh rename htsget-http-actix/benches/{request-benchmark.rs => request_benchmarks.rs} (100%) rename htsget-search/benches/{benchmark.rs => search_benchmarks.rs} (100%) diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml index 28c1dae9b..dbf4a6c31 100644 --- a/.github/workflows/action.yml +++ b/.github/workflows/action.yml @@ -2,10 +2,6 @@ name: tests on: [push] -env: - AWS_ACCESS_KEY_ID: "FOO" - AWS_SECRET_ACCESS_KEY: "BAR" - jobs: test: runs-on: ${{ matrix.os }} @@ -14,6 +10,7 @@ jobs: rust: [stable] os: [ubuntu-latest] steps: + # Run tests - uses: actions/checkout@v2 - name: Install Rust uses: actions-rs/toolchain@v1 @@ -62,4 +59,23 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - args: --all-targets --all-features + args: --all-features + + # Run benchmarks + - name: Run benchmarks + uses: actions-rs/cargo@v1 + with: + command: bench + args: --bench search-benchmarks --bench --request-benchmarks -- LIGHT --output-format bencher | tee output.txt + - name: Download previous benchmark data + uses: actions/cache@v1 + with: + path: ./cache + key: ${{ runner.os }}-benchmark + - name: Store benchmark result + uses: benchmark-action/github-action-benchmark@v1 + with: + tool: 'cargo' + output-file-path: output.txt + external-data-json-path: ./cache/benchmark-data.json + fail-on-alert: false \ No newline at end of file diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml deleted file mode 100644 index 30ba06c52..000000000 --- a/.github/workflows/pull_request.yml +++ /dev/null @@ -1,32 +0,0 @@ -on: [pull_request] -name: benchmark pull requests -jobs: - run-htsget-search-benchmark: - name: run benchmark - runs-on: ubuntu-latest - defaults: - run: - working-directory: htsget-search - steps: - - uses: actions/checkout@master - - uses: jasonwilliams/criterion-compare-action@move_to_actions - with: - benchName: "LIGHT" # Optional. Compare only this benchmark target - token: ${{ secrets.GITHUB_TOKEN }} - run-htsget-http-actix-benchmarks: - name: run benchmark - runs-on: ubuntu-latest - defaults: - run: - working-directory: htsget-http-actix - steps: - - uses: actions/checkout@master - - run: pwd - - name: Start the hstget-rs server - run: cargo run --release & - - name: Start the hstget-refserver server - run: bash benches/docker-htsget-refserver.sh - - uses: jasonwilliams/criterion-compare-action@move_to_actions - with: - benchName: "LIGHT" # Optional. Compare only this benchmark target - token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/htsget-http-actix/Cargo.toml b/htsget-http-actix/Cargo.toml index fb090a714..15192016a 100644 --- a/htsget-http-actix/Cargo.toml +++ b/htsget-http-actix/Cargo.toml @@ -31,5 +31,6 @@ reqwest = { version = "0.11", features = ["json", "blocking", "rustls-tls"] } tempfile = "3.3" [[bench]] -name = "request-benchmark" -harness = false \ No newline at end of file +name = "request-benchmarks" +harness = false +path = "benches/request_benchmarks.rs" \ No newline at end of file diff --git a/htsget-http-actix/README.md b/htsget-http-actix/README.md index 4bb30bb8e..b36bed717 100644 --- a/htsget-http-actix/README.md +++ b/htsget-http-actix/README.md @@ -3,21 +3,25 @@ This crate should allow to setup an [htsget](http://samtools.github.io/hts-specs ## Quickstart -These are some examples with [curl](https://github.com/curl/curl). **For the curl examples shown below to work, we assume that the server is being started from the root of the [htsget-rs project](https://github.com/umccr/htsget-rs)**, so we can use the example files inside the `data` directory. +These are some examples with [curl](https://github.com/curl/curl). **For the curl examples shown +below to work, we assume that the server is being started from the root of +the [htsget-rs project](https://github.com/umccr/htsget-rs)**, and `HTSGET_PATH="data/"`. -To test them you can run: +The htsget-http-actix server also requires pem formatted X.509 certificates to access the response tickets. + +For example, to generate self-signed certificates, run: ```shell -$ cargo run -p htsget-http-actix +$ openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 365 -nodes -subj '/CN=localhost' ``` -From **the top of the project**. Alternatively, the `HTSGET_PATH` environment variable can be set accordingly if the current working directory is `htsget-http-actix`, i.e: +To test the curl example below, run: ```shell -$ HTSGET_PATH=../ cargo run +$ cargo run -p htsget-http-actix ``` -Otherwise we could have problems as [directory traversal](https://en.wikipedia.org/wiki/Directory_traversal_attack) isn't allowed. + ## Environment variables diff --git a/htsget-http-actix/benches/docker-htsget-refserver.sh b/htsget-http-actix/benches/docker-htsget-refserver.sh deleted file mode 100755 index 8ac9c0e3d..000000000 --- a/htsget-http-actix/benches/docker-htsget-refserver.sh +++ /dev/null @@ -1,2 +0,0 @@ -docker image pull ga4gh/htsget-refserver:1.5.0 -docker container run -d -p 8082:3000 -v $(pwd)/../data:/data -v $(pwd)/benches:/config ga4gh/htsget-refserver:1.5.0 ./htsget-refserver -config /config/htsget-refserver-config.json \ No newline at end of file diff --git a/htsget-http-actix/benches/dowload-heavy-test-files.sh b/htsget-http-actix/benches/dowload-heavy-test-files.sh deleted file mode 100755 index efe9142bc..000000000 --- a/htsget-http-actix/benches/dowload-heavy-test-files.sh +++ /dev/null @@ -1 +0,0 @@ -curl ftp://ftp.1000genomes.ebi.ac.uk/vol1/ftp/data_collections/1000_genomes_project/release/20190312_biallelic_SNV_and_INDEL/ALL.chr14.shapeit2_integrated_snvindels_v2a_27022019.GRCh38.phased.vcf.gz > ../data/vcf/internationalgenomesample.vcf.gz \ No newline at end of file diff --git a/htsget-http-actix/benches/request-benchmark.rs b/htsget-http-actix/benches/request_benchmarks.rs similarity index 100% rename from htsget-http-actix/benches/request-benchmark.rs rename to htsget-http-actix/benches/request_benchmarks.rs diff --git a/htsget-search/Cargo.toml b/htsget-search/Cargo.toml index 1d0cd5d5b..958b23bdc 100644 --- a/htsget-search/Cargo.toml +++ b/htsget-search/Cargo.toml @@ -54,5 +54,6 @@ hyper-tls = "0.5" criterion = { version = "0.3", features = ["async_tokio"] } [[bench]] -name = "benchmark" -harness = false \ No newline at end of file +name = "search-benchmarks" +harness = false +path = "benches/search_benchmarks.rs" \ No newline at end of file diff --git a/htsget-search/benches/benchmark.rs b/htsget-search/benches/search_benchmarks.rs similarity index 100% rename from htsget-search/benches/benchmark.rs rename to htsget-search/benches/search_benchmarks.rs From 97d18759f104d1d1482af2654b9a062770f8c722 Mon Sep 17 00:00:00 2001 From: Marko Malenic Date: Mon, 30 May 2022 16:58:49 +1000 Subject: [PATCH 45/50] Update action --- .github/workflows/action.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml index dbf4a6c31..172491746 100644 --- a/.github/workflows/action.yml +++ b/.github/workflows/action.yml @@ -19,12 +19,6 @@ jobs: override: true components: rustfmt, clippy - - name: Install cargo-lambda - uses: actions-rs/cargo@v1 - with: - command: install - args: cargo-lambda - - uses: Swatinem/rust-cache@v1 - name: Run cargo fmt uses: actions-rs/cargo@v1 From 2a77de4414b9346fa22446bd6b0bc69104d4f2f8 Mon Sep 17 00:00:00 2001 From: Marko Malenic Date: Tue, 31 May 2022 10:02:34 +1000 Subject: [PATCH 46/50] Resolve default/s3-storage feature clashes. --- htsget-devtools/Cargo.toml | 6 +----- htsget-http-lambda/Cargo.toml | 8 +------- htsget-search/Cargo.toml | 2 +- htsget-test-utils/Cargo.toml | 2 +- 4 files changed, 4 insertions(+), 14 deletions(-) diff --git a/htsget-devtools/Cargo.toml b/htsget-devtools/Cargo.toml index 5edba1c94..153e491ba 100644 --- a/htsget-devtools/Cargo.toml +++ b/htsget-devtools/Cargo.toml @@ -9,8 +9,4 @@ edition = "2021" serde = { version = "~1.0", features = ["derive"] } serde_yaml = "~0.8" -noodles = { version = "0.18.0", features = ["bam", "bcf", "bgzf", "cram", "csi", "sam", "tabix", "vcf"] } - -reqwest = { version = "0.11", features = ["json", "blocking", "rustls-tls"] } -htsget-http-actix = { path = "../htsget-http-actix" } -htsget-http-core = { path = "../htsget-http-core" } \ No newline at end of file +noodles = { version = "0.18.0", features = ["bam", "bcf", "bgzf", "cram", "csi", "sam", "tabix", "vcf"] } \ No newline at end of file diff --git a/htsget-http-lambda/Cargo.toml b/htsget-http-lambda/Cargo.toml index bee6ba7c9..a80fe7347 100644 --- a/htsget-http-lambda/Cargo.toml +++ b/htsget-http-lambda/Cargo.toml @@ -27,10 +27,4 @@ bytes = "1.1" [dev-dependencies] htsget-test-utils = { path = "../htsget-test-utils", features = ["server-tests"], default-features = false } async-trait = "0.1" -query_map = { version = "0.5", features = ["url-query"] } - -# For testing locally with cargo lambda. -[package.metadata.lambda.env] -RUST_LOG = "info,htsget_http_lambda=debug,htsget_config=debug,htsget_http_core=debug,htsget_search=debug" -HTSGET_PATH = "data/" -HTSGET_STORAGE_TYPE = "LocalStorage" \ No newline at end of file +query_map = { version = "0.5", features = ["url-query"] } \ No newline at end of file diff --git a/htsget-search/Cargo.toml b/htsget-search/Cargo.toml index 958b23bdc..c058e5f4e 100644 --- a/htsget-search/Cargo.toml +++ b/htsget-search/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Christian Perez Llamas ", "Marko Malenic "] edition = "2021" [features] -server-tests = ["htsget-config", "htsget-search", "htsget-http-core"] +server-tests = ["dep:htsget-config", "dep:htsget-search", "dep:htsget-http-core"] s3-storage = ["htsget-config?/s3-storage", "htsget-search?/s3-storage", "htsget-http-core?/s3-storage"] default = ["s3-storage"] From da79a0ff69a37182c083a5bdb7d45b22a395cb48 Mon Sep 17 00:00:00 2001 From: Marko Malenic Date: Tue, 31 May 2022 10:11:57 +1000 Subject: [PATCH 47/50] Update action.yml. --- .github/workflows/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml index 172491746..52479a33b 100644 --- a/.github/workflows/action.yml +++ b/.github/workflows/action.yml @@ -60,7 +60,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: bench - args: --bench search-benchmarks --bench --request-benchmarks -- LIGHT --output-format bencher | tee output.txt + args: --bench search-benchmarks --bench request-benchmarks -- LIGHT --output-format bencher | tee output.txt - name: Download previous benchmark data uses: actions/cache@v1 with: From 28e934732c0455f1cbe1d00b5ea2c8c1de0dc58f Mon Sep 17 00:00:00 2001 From: Marko Malenic Date: Tue, 31 May 2022 11:41:57 +1000 Subject: [PATCH 48/50] Update action.yml. --- .github/workflows/action.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml index 52479a33b..ec6f716d0 100644 --- a/.github/workflows/action.yml +++ b/.github/workflows/action.yml @@ -57,10 +57,7 @@ jobs: # Run benchmarks - name: Run benchmarks - uses: actions-rs/cargo@v1 - with: - command: bench - args: --bench search-benchmarks --bench request-benchmarks -- LIGHT --output-format bencher | tee output.txt + run: cargo --bench search-benchmarks --bench request-benchmarks -- LIGHT --output-format bencher | tee output.txt - name: Download previous benchmark data uses: actions/cache@v1 with: From 253f97f570aada4a9df09ed00b645b97309fc8a4 Mon Sep 17 00:00:00 2001 From: Marko Malenic Date: Tue, 31 May 2022 11:51:29 +1000 Subject: [PATCH 49/50] Update action --- .github/workflows/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml index ec6f716d0..6d46182db 100644 --- a/.github/workflows/action.yml +++ b/.github/workflows/action.yml @@ -57,7 +57,7 @@ jobs: # Run benchmarks - name: Run benchmarks - run: cargo --bench search-benchmarks --bench request-benchmarks -- LIGHT --output-format bencher | tee output.txt + run: cargo bench --bench search-benchmarks --bench request-benchmarks -- LIGHT --output-format bencher | tee output.txt - name: Download previous benchmark data uses: actions/cache@v1 with: From b13c4107bd93e05ab49c80e3ec134aff0b14734c Mon Sep 17 00:00:00 2001 From: Marko Malenic Date: Tue, 31 May 2022 12:26:32 +1000 Subject: [PATCH 50/50] Reduce number of samples. --- htsget-http-actix/benches/request_benchmarks.rs | 5 +++-- htsget-search/benches/search_benchmarks.rs | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/htsget-http-actix/benches/request_benchmarks.rs b/htsget-http-actix/benches/request_benchmarks.rs index 9ddeb680b..ad80c0e09 100644 --- a/htsget-http-actix/benches/request_benchmarks.rs +++ b/htsget-http-actix/benches/request_benchmarks.rs @@ -15,7 +15,7 @@ use tempfile::TempDir; const REFSERVER_DOCKER_IMAGE: &str = "ga4gh/htsget-refserver:1.5.0"; const BENCHMARK_DURATION_SECONDS: u64 = 30; -const NUMBER_OF_EXECUTIONS: usize = 150; +const NUMBER_OF_SAMPLES: usize = 100; #[derive(Serialize)] struct Empty {} @@ -142,6 +142,7 @@ fn start_htsget_rs() -> (DropGuard, String) { .arg("htsget-http-actix") .env("HTSGET_TICKET_SERVER_KEY", key_path) .env("HTSGET_TICKET_SERVER_CERT", &cert_path) + .env("RUST_LOG", "warn") .spawn() .unwrap(); @@ -211,7 +212,7 @@ fn start_htsget_refserver() -> (DropGuard, String) { fn criterion_benchmark(c: &mut Criterion) { let mut group = c.benchmark_group("Requests"); group - .sample_size(NUMBER_OF_EXECUTIONS) + .sample_size(NUMBER_OF_SAMPLES) .measurement_time(Duration::from_secs(BENCHMARK_DURATION_SECONDS)); let (mut _htsget_rs_server, htsget_rs_url) = start_htsget_rs(); diff --git a/htsget-search/benches/search_benchmarks.rs b/htsget-search/benches/search_benchmarks.rs index c19732633..69845a009 100644 --- a/htsget-search/benches/search_benchmarks.rs +++ b/htsget-search/benches/search_benchmarks.rs @@ -12,7 +12,7 @@ use std::time::Duration; use tokio::runtime::Runtime; const BENCHMARK_DURATION_SECONDS: u64 = 15; -const NUMBER_OF_EXECUTIONS: usize = 150; +const NUMBER_OF_SAMPLES: usize = 150; async fn perform_query(query: Query) -> Result<(), HtsGetError> { let htsget = HtsGetFromStorage::local_from( @@ -39,7 +39,7 @@ fn bench_query(group: &mut BenchmarkGroup, name: &str, query: Query) { fn criterion_benchmark(c: &mut Criterion) { let mut group = c.benchmark_group("Queries"); group - .sample_size(NUMBER_OF_EXECUTIONS) + .sample_size(NUMBER_OF_SAMPLES) .measurement_time(Duration::from_secs(BENCHMARK_DURATION_SECONDS)); bench_query(