diff --git a/Cargo.lock b/Cargo.lock index 8a2d22bbed..f663c72543 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -762,6 +762,32 @@ dependencies = [ "serde", ] +[[package]] +name = "cacache" +version = "12.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142316461ed3a3dfcba10417317472da5bfd0461e4d276bf7c07b330766d9490" +dependencies = [ + "async-std", + "digest 0.10.7", + "either", + "futures", + "hex", + "libc", + "memmap2", + "miette 5.10.0", + "reflink-copy", + "serde", + "serde_derive", + "serde_json", + "sha1", + "sha2 0.10.8", + "ssri", + "tempfile", + "thiserror", + "walkdir", +] + [[package]] name = "cache_control" version = "0.2.0" @@ -2125,6 +2151,7 @@ checksum = "5b5ab65432bbdfe8490dfde21d0366353a8d39f2bc24aca0146889f931b0b4b5" dependencies = [ "async-trait", "bincode", + "cacache", "http 0.2.12", "http-cache-semantics", "httpdate", @@ -2159,6 +2186,7 @@ checksum = "7aec9f678bca3f4a15194b980f20ed9bfe0dd38e8d298c65c559a93dfbd6380a" dependencies = [ "http 0.2.12", "http-serde 1.1.3", + "reqwest", "serde", "time", ] @@ -2340,7 +2368,7 @@ dependencies = [ "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows-core", + "windows-core 0.52.0", ] [[package]] @@ -2931,6 +2959,27 @@ version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +[[package]] +name = "memmap2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +dependencies = [ + "libc", +] + +[[package]] +name = "miette" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" +dependencies = [ + "miette-derive 5.10.0", + "once_cell", + "thiserror", + "unicode-width", +] + [[package]] name = "miette" version = "7.2.0" @@ -2938,11 +2987,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4edc8853320c2a0dab800fbda86253c8938f6ea88510dc92c5f1ed20e794afc1" dependencies = [ "cfg-if", - "miette-derive", + "miette-derive 7.2.0", "thiserror", "unicode-width", ] +[[package]] +name = "miette-derive" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" +dependencies = [ + "proc-macro2 1.0.82", + "quote 1.0.36", + "syn 2.0.64", +] + [[package]] name = "miette-derive" version = "7.2.0" @@ -3811,7 +3871,7 @@ checksum = "6f5eec97d5d34bdd17ad2db2219aabf46b054c6c41bd5529767c9ce55be5898f" dependencies = [ "base64 0.22.1", "logos 0.14.0", - "miette", + "miette 7.2.0", "once_cell", "prost", "prost-types", @@ -3942,7 +4002,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a29b3c5596eb23a849deba860b53ffd468199d9ad5fe4402a7d55379e16aa2d2" dependencies = [ "bytes", - "miette", + "miette 7.2.0", "prost", "prost-reflect", "prost-types", @@ -3957,7 +4017,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "033b939d76d358f7c32120c86c71f515bae45e64f2bde455200356557276276c" dependencies = [ "logos 0.13.0", - "miette", + "miette 7.2.0", "prost-types", "thiserror", ] @@ -4118,6 +4178,17 @@ dependencies = [ "thiserror", ] +[[package]] +name = "reflink-copy" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c3138c30c59ed9b8572f82bed97ea591ecd7e45012566046cc39e72679cff22" +dependencies = [ + "cfg-if", + "rustix 0.38.34", + "windows 0.56.0", +] + [[package]] name = "regex" version = "1.10.4" @@ -4878,6 +4949,23 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "ssri" +version = "9.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7a2b3c2bc9693bcb40870c4e9b5bf0d79f9cb46273321bf855ec513e919082" +dependencies = [ + "base64 0.21.7", + "digest 0.10.7", + "hex", + "miette 5.10.0", + "serde", + "sha-1 0.10.1", + "sha2 0.10.8", + "thiserror", + "xxhash-rust", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -5129,6 +5217,7 @@ dependencies = [ "strum_macros", "tailcall-fixtures", "tailcall-hasher", + "tailcall-http-cache", "tailcall-macros", "tailcall-prettier", "tailcall-tracker", @@ -5203,6 +5292,22 @@ dependencies = [ "fxhash", ] +[[package]] +name = "tailcall-http-cache" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "http 0.2.12", + "http-cache", + "http-cache-reqwest", + "http-cache-semantics", + "reqwest", + "serde", + "tokio", + "url", +] + [[package]] name = "tailcall-macros" version = "0.1.0" @@ -6206,8 +6311,8 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.48.0", + "windows-interface 0.48.0", "windows-targets 0.48.5", ] @@ -6217,7 +6322,17 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ - "windows-core", + "windows-core 0.52.0", + "windows-targets 0.52.5", +] + +[[package]] +name = "windows" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132" +dependencies = [ + "windows-core 0.56.0", "windows-targets 0.52.5", ] @@ -6230,6 +6345,18 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "windows-core" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" +dependencies = [ + "windows-implement 0.56.0", + "windows-interface 0.56.0", + "windows-result", + "windows-targets 0.52.5", +] + [[package]] name = "windows-implement" version = "0.48.0" @@ -6241,6 +6368,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "windows-implement" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" +dependencies = [ + "proc-macro2 1.0.82", + "quote 1.0.36", + "syn 2.0.64", +] + [[package]] name = "windows-interface" version = "0.48.0" @@ -6252,6 +6390,26 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "windows-interface" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" +dependencies = [ + "proc-macro2 1.0.82", + "quote 1.0.36", + "syn 2.0.64", +] + +[[package]] +name = "windows-result" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "749f0da9cc72d82e600d8d2e44cadd0b9eedb9038f71a1c58556ac1c5791813b" +dependencies = [ + "windows-targets 0.52.5", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -6526,6 +6684,12 @@ dependencies = [ "syn 2.0.64", ] +[[package]] +name = "xxhash-rust" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927da81e25be1e1a2901d59b81b37dd2efd1fc9c9345a55007f09bf5a2d3ee03" + [[package]] name = "yansi" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 8639f25b61..e217d46cd9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,8 @@ opentelemetry-otlp = { version = "0.15.0", features = [ "tls-roots", ], optional = true } opentelemetry-system-metrics = { version = "0.1.8", optional = true } +tailcall-http-cache = {path = "tailcall-http-cache", optional = true } + # dependencies safe for wasm: @@ -193,7 +195,8 @@ cli = [ "opentelemetry_sdk/rt-tokio", "dep:opentelemetry-otlp", "dep:opentelemetry-system-metrics", - "dep:tailcall-tracker" + "dep:tailcall-tracker", + "dep:tailcall-http-cache", ] # Feature flag to enable all default features. @@ -214,7 +217,9 @@ members = [ "tailcall-fixtures", "tailcall-upstream-grpc", "tailcall-tracker", - "tailcall-hasher"] + "tailcall-hasher", + "tailcall-http-cache", +] # Boost execution_spec snapshot diffing performance [profile.dev.package] diff --git a/benches/http_execute_bench.rs b/benches/http_execute_bench.rs new file mode 100644 index 0000000000..468f9e07f0 --- /dev/null +++ b/benches/http_execute_bench.rs @@ -0,0 +1,29 @@ +use criterion::Criterion; +use hyper::Method; +use tailcall::cli::runtime::NativeHttp; +use tailcall::core::blueprint::Blueprint; +use tailcall::core::HttpIO; + +pub fn benchmark_http_execute_method(c: &mut Criterion) { + let tokio_runtime = tokio::runtime::Runtime::new().unwrap(); + + let mut blueprint = Blueprint::default(); + blueprint.upstream.http_cache = true; // allow http caching for bench test. + let native_http = NativeHttp::init(&blueprint.upstream, &blueprint.telemetry); + let request_url = String::from("http://jsonplaceholder.typicode.com/users"); + + tokio_runtime.block_on(async { + // cache the 1st request in order the evaluate the perf of underlying cache. + let request = reqwest::Request::new(Method::GET, request_url.parse().unwrap()); + let _result = native_http.execute(request).await; + }); + + c.bench_function("test_http_execute_method", |b| { + b.iter(|| { + tokio_runtime.block_on(async { + let request = reqwest::Request::new(Method::GET, request_url.parse().unwrap()); + let _result = native_http.execute(request).await; + }) + }); + }); +} diff --git a/benches/impl_path_string_for_evaluation_context.rs b/benches/impl_path_string_for_evaluation_context.rs index e77a88a404..c5eb9ea971 100644 --- a/benches/impl_path_string_for_evaluation_context.rs +++ b/benches/impl_path_string_for_evaluation_context.rs @@ -7,7 +7,7 @@ use async_graphql::context::SelectionField; use async_graphql::{Name, Value}; use async_trait::async_trait; use criterion::{BenchmarkId, Criterion}; -use http_cache_reqwest::{Cache, CacheMode, HttpCache, HttpCacheOptions, MokaManager}; +use http_cache_reqwest::{Cache, CacheMode, HttpCache, HttpCacheOptions}; use hyper::body::Bytes; use hyper::header::HeaderValue; use hyper::HeaderMap; @@ -22,6 +22,7 @@ use tailcall::core::lambda::{EvaluationContext, ResolverContextLike}; use tailcall::core::path::PathString; use tailcall::core::runtime::TargetRuntime; use tailcall::core::{EnvIO, FileIO, HttpIO}; +use tailcall_http_cache::HttpCacheManager; struct Http { client: ClientWithMiddleware, @@ -59,7 +60,7 @@ impl Http { if upstream.http_cache { client = client.with(Cache(HttpCache { mode: CacheMode::Default, - manager: MokaManager::default(), + manager: HttpCacheManager::default(), options: HttpCacheOptions::default(), })) } diff --git a/benches/tailcall_benches.rs b/benches/tailcall_benches.rs index 4bd032fd56..09336bcdc4 100644 --- a/benches/tailcall_benches.rs +++ b/benches/tailcall_benches.rs @@ -2,6 +2,7 @@ use criterion::{criterion_group, criterion_main, Criterion}; mod data_loader_bench; mod handle_request_bench; +mod http_execute_bench; mod impl_path_string_for_evaluation_context; mod json_like_bench; mod protobuf_convert_output; @@ -15,6 +16,7 @@ fn all_benchmarks(c: &mut Criterion) { protobuf_convert_output::benchmark_convert_output(c); request_template_bench::benchmark_to_request(c); handle_request_bench::benchmark_handle_request(c); + http_execute_bench::benchmark_http_execute_method(c); } criterion_group! { diff --git a/src/cli/runtime/http.rs b/src/cli/runtime/http.rs index bac30271bc..e05660fbc0 100644 --- a/src/cli/runtime/http.rs +++ b/src/cli/runtime/http.rs @@ -1,7 +1,7 @@ use std::time::Duration; use anyhow::Result; -use http_cache_reqwest::{Cache, CacheMode, HttpCache, HttpCacheOptions, MokaManager}; +use http_cache_reqwest::{Cache, CacheMode, HttpCache, HttpCacheOptions}; use hyper::body::Bytes; use once_cell::sync::Lazy; use opentelemetry::metrics::Counter; @@ -13,6 +13,7 @@ use opentelemetry_semantic_conventions::trace::{ }; use reqwest::Client; use reqwest_middleware::{ClientBuilder, ClientWithMiddleware}; +use tailcall_http_cache::HttpCacheManager; use tracing_opentelemetry::OpenTelemetrySpanExt; use super::HttpIO; @@ -114,7 +115,7 @@ impl NativeHttp { if upstream.http_cache { client = client.with(Cache(HttpCache { mode: CacheMode::Default, - manager: MokaManager::default(), + manager: HttpCacheManager::default(), options: HttpCacheOptions::default(), })) } diff --git a/src/cli/runtime/mod.rs b/src/cli/runtime/mod.rs index c1810c586f..6ae85c5f80 100644 --- a/src/cli/runtime/mod.rs +++ b/src/cli/runtime/mod.rs @@ -5,6 +5,8 @@ mod http; use std::hash::Hash; use std::sync::Arc; +pub use http::NativeHttp; + use crate::core::blueprint::Blueprint; use crate::core::cache::InMemoryCache; use crate::core::runtime::TargetRuntime; diff --git a/src/core/runtime.rs b/src/core/runtime.rs index e9a1f03fdd..a6d0b45c66 100644 --- a/src/core/runtime.rs +++ b/src/core/runtime.rs @@ -42,10 +42,11 @@ pub mod test { use std::time::Duration; use anyhow::{anyhow, Result}; - use http_cache_reqwest::{Cache, CacheMode, HttpCache, HttpCacheOptions, MokaManager}; + use http_cache_reqwest::{Cache, CacheMode, HttpCache, HttpCacheOptions}; use hyper::body::Bytes; use reqwest::Client; use reqwest_middleware::{ClientBuilder, ClientWithMiddleware}; + use tailcall_http_cache::HttpCacheManager; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use crate::cli::javascript; @@ -97,7 +98,7 @@ pub mod test { if upstream.http_cache { client = client.with(Cache(HttpCache { mode: CacheMode::Default, - manager: MokaManager::default(), + manager: HttpCacheManager::default(), options: HttpCacheOptions::default(), })) } diff --git a/tailcall-http-cache/Cargo.toml b/tailcall-http-cache/Cargo.toml new file mode 100644 index 0000000000..c972e67997 --- /dev/null +++ b/tailcall-http-cache/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "tailcall-http-cache" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +http-cache-reqwest = { version = "0.13.0", default-features = false, features = ["manager-moka"] } +http-cache-semantics = { version = "1.0.1", default-features = false, features = ["with_serde", "reqwest"]} +serde = "1.0.202" +async-trait = "0.1.80" + +[dev-dependencies] +tokio = {version = "1.37.0", features = ["full"]} +url = { version = "2.5.0", features = ["serde"] } +reqwest = { workspace = true } +http = "0.2.12" +http-cache = "0.18.0" +anyhow = { workspace = true } \ No newline at end of file diff --git a/tailcall-http-cache/src/cache.rs b/tailcall-http-cache/src/cache.rs new file mode 100644 index 0000000000..317d12aee1 --- /dev/null +++ b/tailcall-http-cache/src/cache.rs @@ -0,0 +1,152 @@ +use http_cache_reqwest::{CacheManager, HttpResponse, MokaCache}; +use http_cache_semantics::CachePolicy; +use serde::{Deserialize, Serialize}; +pub type BoxError = Box; +pub type Result = std::result::Result; + +use std::sync::Arc; + +pub struct HttpCacheManager { + pub cache: Arc>, +} + +impl Default for HttpCacheManager { + fn default() -> Self { + Self::new(MokaCache::new(42)) + } +} + +#[derive(Clone, Deserialize, Serialize)] +pub struct Store { + response: HttpResponse, + policy: CachePolicy, +} + +impl HttpCacheManager { + pub fn new(cache: MokaCache) -> Self { + Self { cache: Arc::new(cache) } + } + + pub async fn clear(&self) -> Result<()> { + self.cache.invalidate_all(); + self.cache.run_pending_tasks().await; + Ok(()) + } +} + +#[async_trait::async_trait] +impl CacheManager for HttpCacheManager { + async fn get(&self, cache_key: &str) -> Result> { + let store: Store = match self.cache.get(cache_key).await { + Some(d) => d, + None => return Ok(None), + }; + Ok(Some((store.response, store.policy))) + } + + async fn put( + &self, + cache_key: String, + response: HttpResponse, + policy: CachePolicy, + ) -> Result { + let data = Store { response: response.clone(), policy }; + self.cache.insert(cache_key, data).await; + self.cache.run_pending_tasks().await; + Ok(response) + } + + async fn delete(&self, cache_key: &str) -> Result<()> { + self.cache.invalidate(cache_key).await; + self.cache.run_pending_tasks().await; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use anyhow::Ok; + use http_cache::HttpVersion; + use reqwest::{Method, Response, ResponseBuilderExt}; + use url::Url; + + use super::*; + + fn convert_response(response: HttpResponse) -> anyhow::Result { + let ret_res = http::Response::builder() + .status(response.status) + .url(response.url) + .version(response.version.into()) + .body(response.body)?; + + Ok(Response::from(ret_res)) + } + + async fn insert_key_into_cache(manager: &HttpCacheManager) { + let request_url = "http://localhost:8080/test"; + let url = Url::parse(request_url).unwrap(); + + let http_resp = HttpResponse { + headers: HashMap::default(), + body: vec![1, 2, 3], + status: 200, + url: url.clone(), + version: HttpVersion::Http11, + }; + let resp = convert_response(http_resp.clone()).unwrap(); + let request: reqwest::Request = + reqwest::Request::new(Method::GET, request_url.parse().unwrap()); + + let _ = manager + .put( + "test".to_string(), + http_resp, + CachePolicy::new(&request, &resp), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn test_put() { + let manager = HttpCacheManager::default(); + insert_key_into_cache(&manager).await; + assert!(manager.cache.contains_key("test")); + } + + #[tokio::test] + async fn test_get_when_key_present() { + let manager = HttpCacheManager::default(); + insert_key_into_cache(&manager).await; + let value = manager.get("test").await.unwrap(); + assert!(value.is_some()); + } + + #[tokio::test] + async fn test_get_when_key_not_present() { + let manager = HttpCacheManager::default(); + let result = manager.get("test").await.unwrap(); + assert!(result.is_none()); + } + + #[tokio::test] + async fn test_delete_when_key_present() { + let manager = HttpCacheManager::default(); + insert_key_into_cache(&manager).await; + + assert!(manager.cache.iter().count() as i32 == 1); + let _ = manager.delete("test").await; + assert!(manager.cache.iter().count() as i32 == 0); + } + + #[tokio::test] + async fn test_clear() { + let manager = HttpCacheManager::default(); + insert_key_into_cache(&manager).await; + assert!(manager.cache.iter().count() as i32 == 1); + let _ = manager.clear().await; + assert!(manager.cache.iter().count() as i32 == 0); + } +} diff --git a/tailcall-http-cache/src/lib.rs b/tailcall-http-cache/src/lib.rs new file mode 100644 index 0000000000..2c88e4d9d3 --- /dev/null +++ b/tailcall-http-cache/src/lib.rs @@ -0,0 +1,3 @@ +mod cache; + +pub use cache::HttpCacheManager; diff --git a/tests/server_spec.rs b/tests/server_spec.rs index 02b32dbaec..42903203af 100644 --- a/tests/server_spec.rs +++ b/tests/server_spec.rs @@ -6,7 +6,7 @@ pub mod test { use std::time::Duration; use anyhow::{anyhow, Result}; - use http_cache_reqwest::{Cache, CacheMode, HttpCache, HttpCacheOptions, MokaManager}; + use http_cache_reqwest::{Cache, CacheMode, HttpCache, HttpCacheOptions}; use hyper::body::Bytes; use reqwest::Client; use reqwest_middleware::{ClientBuilder, ClientWithMiddleware}; @@ -16,6 +16,7 @@ pub mod test { use tailcall::core::http::Response; use tailcall::core::runtime::TargetRuntime; use tailcall::core::{EnvIO, FileIO, HttpIO}; + use tailcall_http_cache::HttpCacheManager; use tokio::io::{AsyncReadExt, AsyncWriteExt}; #[derive(Clone)] @@ -60,7 +61,7 @@ pub mod test { if upstream.http_cache { client = client.with(Cache(HttpCache { mode: CacheMode::Default, - manager: MokaManager::default(), + manager: HttpCacheManager::default(), options: HttpCacheOptions::default(), })) }