diff --git a/Dockerfile b/Dockerfile index bded35d1f9..48d9d68959 100644 --- a/Dockerfile +++ b/Dockerfile @@ -69,8 +69,6 @@ COPY --from=base /bin/numaflow /bin/numaflow COPY --from=base /bin/numaflow-rs /bin/numaflow-rs COPY ui/build /ui/build -COPY ./rust/serving/config config - ENTRYPOINT [ "/bin/numaflow" ] #################################################################################################### @@ -89,4 +87,4 @@ RUN chmod +x /bin/e2eapi #################################################################################################### FROM scratch AS e2eapi COPY --from=testbase /bin/e2eapi . -ENTRYPOINT ["/e2eapi"] \ No newline at end of file +ENTRYPOINT ["/e2eapi"] diff --git a/rust/Cargo.lock b/rust/Cargo.lock index ec51332105..a210284fcd 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -17,18 +17,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -38,12 +26,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - [[package]] name = "android-tzdata" version = "0.1.1" @@ -71,12 +53,6 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" -[[package]] -name = "arraydeque" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" - [[package]] name = "async-nats" version = "0.35.1" @@ -388,9 +364,6 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" -dependencies = [ - "serde", -] [[package]] name = "block-buffer" @@ -503,60 +476,12 @@ dependencies = [ "tokio-util", ] -[[package]] -name = "config" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf" -dependencies = [ - "async-trait", - "convert_case", - "json5", - "nom", - "pathdiff", - "ron", - "rust-ini", - "serde", - "serde_json", - "toml", - "yaml-rust2", -] - [[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "const-random" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" -dependencies = [ - "const-random-macro", -] - -[[package]] -name = "const-random-macro" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" -dependencies = [ - "getrandom", - "once_cell", - "tiny-keccak", -] - -[[package]] -name = "convert_case" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -607,12 +532,6 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" -[[package]] -name = "crunchy" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" - [[package]] name = "crypto-common" version = "0.1.6" @@ -697,15 +616,6 @@ dependencies = [ "syn 2.0.90", ] -[[package]] -name = "dlv-list" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" -dependencies = [ - "const-random", -] - [[package]] name = "dtoa" version = "1.0.9" @@ -984,31 +894,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", - "allocator-api2", -] - [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" -[[package]] -name = "hashlink" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" -dependencies = [ - "hashbrown 0.14.5", -] - [[package]] name = "headers" version = "0.4.0" @@ -1493,17 +1384,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json5" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" -dependencies = [ - "pest", - "pest_derive", - "serde", -] - [[package]] name = "jsonpath-rust" version = "0.5.1" @@ -1873,6 +1753,7 @@ dependencies = [ "semver", "serde", "serde_json", + "serving", "tempfile", "thiserror 2.0.3", "tokio", @@ -1954,16 +1835,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "ordered-multimap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" -dependencies = [ - "dlv-list", - "hashbrown 0.14.5", -] - [[package]] name = "overload" version = "0.1.1" @@ -1999,12 +1870,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" -[[package]] -name = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - [[package]] name = "pem" version = "3.0.4" @@ -2630,28 +2495,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "ron" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" -dependencies = [ - "base64 0.21.7", - "bitflags 2.6.0", - "serde", - "serde_derive", -] - -[[package]] -name = "rust-ini" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a" -dependencies = [ - "cfg-if", - "ordered-multimap", -] - [[package]] name = "rustc-demangle" version = "0.1.24" @@ -2955,15 +2798,6 @@ dependencies = [ "syn 2.0.90", ] -[[package]] -name = "serde_spanned" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" -dependencies = [ - "serde", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3011,7 +2845,6 @@ dependencies = [ "backoff", "base64 0.22.1", "chrono", - "config", "hyper-util", "numaflow-models", "parking_lot", @@ -3319,15 +3152,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - [[package]] name = "tinystr" version = "0.7.6" @@ -3458,40 +3282,6 @@ dependencies = [ "tokio-util", ] -[[package]] -name = "toml" -version = "0.8.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" -dependencies = [ - "indexmap 2.7.0", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] - [[package]] name = "tonic" version = "0.12.3" @@ -3718,12 +3508,6 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - [[package]] name = "unicode-width" version = "0.1.14" @@ -4133,15 +3917,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "winnow" -version = "0.6.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" -dependencies = [ - "memchr", -] - [[package]] name = "winreg" version = "0.50.0" @@ -4164,17 +3939,6 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" -[[package]] -name = "yaml-rust2" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" -dependencies = [ - "arraydeque", - "encoding_rs", - "hashlink", -] - [[package]] name = "yasna" version = "0.5.2" diff --git a/rust/numaflow-core/Cargo.toml b/rust/numaflow-core/Cargo.toml index 73c15c489c..b4688a135b 100644 --- a/rust/numaflow-core/Cargo.toml +++ b/rust/numaflow-core/Cargo.toml @@ -17,6 +17,7 @@ tracing.workspace = true numaflow-pulsar.workspace = true numaflow-models.workspace = true numaflow-pb.workspace = true +serving.workspace = true backoff.workspace = true axum.workspace = true axum-server.workspace = true diff --git a/rust/numaflow-core/src/config/components.rs b/rust/numaflow-core/src/config/components.rs index 9b26c1d5d7..f17331ddaa 100644 --- a/rust/numaflow-core/src/config/components.rs +++ b/rust/numaflow-core/src/config/components.rs @@ -3,6 +3,8 @@ pub(crate) mod source { const DEFAULT_SOURCE_SOCKET: &str = "/var/run/numaflow/source.sock"; const DEFAULT_SOURCE_SERVER_INFO_FILE: &str = "/var/run/numaflow/sourcer-server-info"; + use std::collections::HashMap; + use std::env; use std::{fmt::Debug, time::Duration}; use bytes::Bytes; @@ -31,6 +33,7 @@ pub(crate) mod source { Generator(GeneratorConfig), UserDefined(UserDefinedConfig), Pulsar(PulsarSourceConfig), + Serving(serving::Settings), } impl From> for SourceType { @@ -96,6 +99,55 @@ pub(crate) mod source { } } + impl TryFrom> for SourceType { + type Error = Error; + // FIXME: Currently, the same settings comes from user-defined settings and env variables. + // We parse both, with user-defined values having higher precedence. + // There should be only one option (user-defined) to define the settings. + fn try_from(cfg: Box) -> Result { + let env_vars = env::vars().collect::>(); + + let mut settings: serving::Settings = env_vars + .try_into() + .map_err(|e: serving::Error| Error::Config(e.to_string()))?; + + settings.tid_header = cfg.msg_id_header_key; + + if let Some(auth) = cfg.auth { + if let Some(token) = auth.token { + let secret = crate::shared::create_components::get_secret_from_volume( + &token.name, + &token.key, + ) + .map_err(|e| Error::Config(format!("Reading API auth token secret: {e:?}")))?; + settings.api_auth_token = Some(secret); + } else { + tracing::warn!("Authentication token for Serving API is specified, but the secret is empty"); + }; + } + + if let Some(ttl) = cfg.store.ttl { + if ttl.is_negative() { + return Err(Error::Config(format!( + "TTL value for the store can not be negative. Provided value = {ttl:?}" + ))); + } + let ttl: std::time::Duration = ttl.into(); + let ttl_secs = ttl.as_secs() as u32; + // TODO: Identify a minimum value + if ttl_secs < 1 { + return Err(Error::Config(format!( + "TTL value for the store must not be less than 1 second. Provided value = {ttl:?}" + ))); + } + settings.redis.ttl_secs = Some(ttl_secs); + } + settings.redis.addr = cfg.store.url; + + Ok(SourceType::Serving(settings)) + } + } + impl TryFrom> for SourceType { type Error = Error; diff --git a/rust/numaflow-core/src/shared/create_components.rs b/rust/numaflow-core/src/shared/create_components.rs index 26516e34d9..c2295e797b 100644 --- a/rust/numaflow-core/src/shared/create_components.rs +++ b/rust/numaflow-core/src/shared/create_components.rs @@ -266,6 +266,9 @@ pub async fn create_source( None, )) } + SourceType::Serving(_) => { + unimplemented!("Serving as built-in source is not yet implemented") + } } } diff --git a/rust/numaflow/src/main.rs b/rust/numaflow/src/main.rs index e25fbf9c66..60e26ef850 100644 --- a/rust/numaflow/src/main.rs +++ b/rust/numaflow/src/main.rs @@ -1,13 +1,14 @@ +use std::collections::HashMap; use std::env; +use std::error::Error; +use std::sync::Arc; -use tracing::{error, info}; +use tracing::error; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; #[tokio::main] -async fn main() { - let args: Vec = env::args().collect(); - +async fn main() -> Result<(), Box> { // Set up the tracing subscriber. RUST_LOG can be used to set the log level. // The default log level is `info`. The `axum::rejection=trace` enables showing // rejections from built-in extractors at `TRACE` level. @@ -20,21 +21,31 @@ async fn main() { ) .with(tracing_subscriber::fmt::layer().with_ansi(false)) .init(); + if let Err(e) = run().await { + error!("{e:?}"); + return Err(e); + } + Ok(()) +} +async fn run() -> Result<(), Box> { + let args: Vec = env::args().collect(); // Based on the argument, run the appropriate component. if args.contains(&"--serving".to_string()) { - if let Err(e) = serving::serve().await { - error!("Error running serving: {}", e); - } + let env_vars: HashMap = env::vars().collect(); + let settings: serving::Settings = env_vars.try_into()?; + let settings = Arc::new(settings); + serving::serve(settings) + .await + .map_err(|e| format!("Error running serving: {e:?}"))?; } else if args.contains(&"--servesink".to_string()) { - if let Err(e) = servesink::servesink().await { - info!("Error running servesink: {}", e); - } + servesink::servesink() + .await + .map_err(|e| format!("Error running servesink: {e:?}"))?; } else if args.contains(&"--rust".to_string()) { - if let Err(e) = numaflow_core::run().await { - error!("Error running rust binary: {}", e); - } - } else { - error!("Invalid argument. Use --serving, --servesink, or --rust."); + numaflow_core::run() + .await + .map_err(|e| format!("Error running rust binary: {e:?}"))? } + Err("Invalid argument. Use --serving, --servesink, or --rust".into()) } diff --git a/rust/serving/Cargo.toml b/rust/serving/Cargo.toml index d62a1d2d8f..de2f8bb820 100644 --- a/rust/serving/Cargo.toml +++ b/rust/serving/Cargo.toml @@ -27,7 +27,6 @@ tower = "0.4.13" tower-http = { version = "0.5.2", features = ["trace", "timeout"] } uuid = { version = "1.10.0", features = ["v4"] } redis = { version = "0.26.0", features = ["tokio-comp", "aio", "connection-manager"] } -config = "0.14.0" trait-variant = "0.1.2" chrono = { version = "0.4", features = ["serde"] } base64 = "0.22.1" diff --git a/rust/serving/config/default.toml b/rust/serving/config/default.toml deleted file mode 100644 index 448672abc1..0000000000 --- a/rust/serving/config/default.toml +++ /dev/null @@ -1,17 +0,0 @@ -tid_header = "ID" -app_listen_port = 3000 -metrics_server_listen_port = 3001 -upstream_addr = "localhost:8888" -drain_timeout_secs = 10 -host_ip = "localhost" - -[jetstream] -stream = "default" -url = "localhost:4222" - -[redis] -addr = "redis://127.0.0.1/" -max_tasks = 50 -retries = 5 -retries_duration_millis = 100 -ttl_secs = 1 diff --git a/rust/serving/config/jetstream.conf b/rust/serving/config/jetstream.conf deleted file mode 100644 index e09998c0ac..0000000000 --- a/rust/serving/config/jetstream.conf +++ /dev/null @@ -1,4 +0,0 @@ -jetstream: { - max_mem_store: 1MiB, - max_file_store: 1GiB -} \ No newline at end of file diff --git a/rust/serving/config/pipeline_spec.json b/rust/serving/config/pipeline_spec.json deleted file mode 100644 index 1698329e86..0000000000 --- a/rust/serving/config/pipeline_spec.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "vertices": [ - { - "name": "in" - }, { - "name": "cat" - }, { - "name": "out" - } - ], - "edges": [ - { - "from": "in", - "to": "cat" - }, - { - "from": "cat", - "to": "out" - } - ] -} \ No newline at end of file diff --git a/rust/serving/src/app.rs b/rust/serving/src/app.rs index d1d29c4c21..56d4a33cb3 100644 --- a/rust/serving/src/app.rs +++ b/rust/serving/src/app.rs @@ -1,5 +1,5 @@ -use std::env; use std::net::SocketAddr; +use std::sync::Arc; use std::time::Duration; use async_nats::jetstream; @@ -17,7 +17,7 @@ use tokio::signal; use tower::ServiceBuilder; use tower_http::timeout::TimeoutLayer; use tower_http::trace::{DefaultOnResponse, TraceLayer}; -use tracing::{debug, info, info_span, Level}; +use tracing::{info, info_span, Level}; use uuid::Uuid; use self::{ @@ -26,9 +26,11 @@ use self::{ }; use crate::app::callback::store::Store; use crate::app::tracker::MessageGraph; -use crate::pipeline::min_pipeline_spec; -use crate::Error::{InitError, MetricsServer}; -use crate::{app::callback::state::State as CallbackState, config, metrics::capture_metrics}; +use crate::config::JetStreamConfig; +use crate::pipeline::PipelineDCG; +use crate::Error::InitError; +use crate::Settings; +use crate::{app::callback::state::State as CallbackState, metrics::capture_metrics}; /// manage callbacks pub(crate) mod callback; @@ -41,10 +43,6 @@ mod message_path; // TODO: merge message_path and tracker mod response; mod tracker; -const ENV_NUMAFLOW_SERVING_JETSTREAM_USER: &str = "NUMAFLOW_ISBSVC_JETSTREAM_USER"; -const ENV_NUMAFLOW_SERVING_JETSTREAM_PASSWORD: &str = "NUMAFLOW_ISBSVC_JETSTREAM_PASSWORD"; -const ENV_NUMAFLOW_SERVING_AUTH_TOKEN: &str = "NUMAFLOW_SERVING_AUTH_TOKEN"; - /// Everything for numaserve starts here. The routing, middlewares, proxying, etc. // TODO // - [ ] implement an proxy and pass in UUID in the header if not present @@ -52,19 +50,23 @@ const ENV_NUMAFLOW_SERVING_AUTH_TOKEN: &str = "NUMAFLOW_SERVING_AUTH_TOKEN"; /// Start the main application Router and the axum server. pub(crate) async fn start_main_server( - addr: SocketAddr, + settings: Arc, tls_config: RustlsConfig, + pipeline_spec: PipelineDCG, ) -> crate::Result<()> { - debug!(?addr, "App server started"); + let app_addr: SocketAddr = format!("0.0.0.0:{}", &settings.app_listen_port) + .parse() + .map_err(|e| InitError(format!("{e:?}")))?; + let tid_header = settings.tid_header.clone(); let layers = ServiceBuilder::new() // Add tracing to all requests .layer( TraceLayer::new_for_http() - .make_span_with(|req: &Request| { + .make_span_with(move |req: &Request| { let tid = req .headers() - .get(&config().tid_header) + .get(&tid_header) .and_then(|v| v.to_str().ok()) .map(|v| v.to_string()) .unwrap_or_else(|| Uuid::new_v4().to_string()); @@ -83,13 +85,16 @@ pub(crate) async fn start_main_server( .layer( // Graceful shutdown will wait for outstanding requests to complete. Add a timeout so // requests don't hang forever. - TimeoutLayer::new(Duration::from_secs(config().drain_timeout_secs)), + TimeoutLayer::new(Duration::from_secs(settings.drain_timeout_secs)), ) // Add auth middleware to all user facing routes - .layer(middleware::from_fn(auth_middleware)); + .layer(middleware::from_fn_with_state( + settings.api_auth_token.clone(), + auth_middleware, + )); // Create the message graph from the pipeline spec and the redis store - let msg_graph = MessageGraph::from_pipeline(min_pipeline_spec()).map_err(|e| { + let msg_graph = MessageGraph::from_pipeline(&pipeline_spec).map_err(|e| { InitError(format!( "Creating message graph from pipeline spec: {:?}", e @@ -97,11 +102,8 @@ pub(crate) async fn start_main_server( })?; // Create a redis store to store the callbacks and the custom responses - let redis_store = callback::store::redisstore::RedisConnection::new( - &config().redis.addr, - config().redis.max_tasks, - ) - .await?; + let redis_store = + callback::store::redisstore::RedisConnection::new(settings.redis.clone()).await?; let state = CallbackState::new(msg_graph, redis_store).await?; let handle = Handle::new(); @@ -109,14 +111,17 @@ pub(crate) async fn start_main_server( tokio::spawn(graceful_shutdown(handle.clone())); // Create a Jetstream context - let js_context = create_js_context().await?; + let js_context = create_js_context(&settings.jetstream).await?; + + let router = setup_app(settings, js_context, state).await?.layer(layers); + + info!(?app_addr, "Starting application server"); - let router = setup_app(js_context, state).await?.layer(layers); - axum_server::bind_rustls(addr, tls_config) + axum_server::bind_rustls(app_addr, tls_config) .handle(handle) .serve(router.into_make_service()) .await - .map_err(|e| MetricsServer(format!("Starting web server for metrics: {}", e)))?; + .map_err(|e| InitError(format!("Starting web server for metrics: {}", e)))?; Ok(()) } @@ -149,19 +154,16 @@ async fn graceful_shutdown(handle: Handle) { handle.graceful_shutdown(Some(Duration::from_secs(30))); } -async fn create_js_context() -> crate::Result { - // Check for user and password in the Jetstream configuration - let js_config = &config().jetstream; - +async fn create_js_context(js_config: &JetStreamConfig) -> crate::Result { // Connect to Jetstream with user and password if they are set - let js_client = match ( - env::var(ENV_NUMAFLOW_SERVING_JETSTREAM_USER), - env::var(ENV_NUMAFLOW_SERVING_JETSTREAM_PASSWORD), - ) { - (Ok(user), Ok(password)) => { + let js_client = match js_config.auth.as_ref() { + Some(auth) => { async_nats::connect_with_options( &js_config.url, - async_nats::ConnectOptions::with_user_and_password(user, password), + async_nats::ConnectOptions::with_user_and_password( + auth.username.clone(), + auth.password.clone(), + ), ) .await } @@ -170,8 +172,7 @@ async fn create_js_context() -> crate::Result { .map_err(|e| { InitError(format!( "Connecting to jetstream server {}: {}", - &config().jetstream.url, - e + &js_config.url, e )) })?; Ok(jetstream::new(js_client)) @@ -185,7 +186,11 @@ const PUBLISH_ENDPOINTS: [&str; 3] = [ // auth middleware to do token based authentication for all user facing routes // if auth is enabled. -async fn auth_middleware(request: axum::extract::Request, next: Next) -> Response { +async fn auth_middleware( + State(api_auth_token): State>, + request: axum::extract::Request, + next: Next, +) -> Response { let path = request.uri().path(); // we only need to check for the presence of the auth token in the request headers for the publish endpoints @@ -193,8 +198,8 @@ async fn auth_middleware(request: axum::extract::Request, next: Next) -> Respons return next.run(request).await; } - match env::var(ENV_NUMAFLOW_SERVING_AUTH_TOKEN) { - Ok(token) => { + match api_auth_token { + Some(token) => { // Check for the presence of the auth token in the request headers let auth_token = match request.headers().get("Authorization") { Some(token) => token, @@ -216,22 +221,35 @@ async fn auth_middleware(request: axum::extract::Request, next: Next) -> Respons next.run(request).await } } - Err(_) => { + None => { // If the auth token is not set, we don't need to check for the presence of the auth token in the request headers next.run(request).await } } } +#[derive(Clone)] +pub(crate) struct AppState { + pub(crate) settings: Arc, + pub(crate) callback_state: CallbackState, + pub(crate) context: Context, +} + async fn setup_app( + settings: Arc, context: Context, - state: CallbackState, + callback_state: CallbackState, ) -> crate::Result { + let app_state = AppState { + settings, + callback_state: callback_state.clone(), + context: context.clone(), + }; let parent = Router::new() .route("/health", get(health_check)) .route("/livez", get(livez)) // Liveliness check .route("/readyz", get(readyz)) - .with_state((state.clone(), context.clone())); // Readiness check + .with_state(app_state.clone()); // Readiness check // a pool based client implementation for direct proxy, this client is cloneable. let client: direct_proxy::Client = @@ -240,8 +258,11 @@ async fn setup_app( // let's nest each endpoint let app = parent - .nest("/v1/direct", direct_proxy(client)) - .nest("/v1/process", routes(context, state).await?); + .nest( + "/v1/direct", + direct_proxy(client, app_state.settings.upstream_addr.clone()), + ) + .nest("/v1/process", routes(app_state).await?); Ok(app) } @@ -250,16 +271,20 @@ async fn health_check() -> impl IntoResponse { "ok" } -async fn livez( - State((_state, _context)): State<(CallbackState, Context)>, -) -> impl IntoResponse { +async fn livez() -> impl IntoResponse { StatusCode::NO_CONTENT } async fn readyz( - State((mut state, context)): State<(CallbackState, Context)>, + State(app): State>, ) -> impl IntoResponse { - if state.ready().await && context.get_stream(&config().jetstream.stream).await.is_ok() { + if app.callback_state.clone().ready().await + && app + .context + .get_stream(&app.settings.jetstream.stream) + .await + .is_ok() + { StatusCode::NO_CONTENT } else { StatusCode::INTERNAL_SERVER_ERROR @@ -267,11 +292,14 @@ async fn readyz( } async fn routes( - context: Context, - state: CallbackState, + app_state: AppState, ) -> crate::Result { - let jetstream_proxy = jetstream_proxy(context, state.clone()).await?; - let callback_router = callback_handler(state.clone()); + let state = app_state.callback_state.clone(); + let jetstream_proxy = jetstream_proxy(app_state.clone()).await?; + let callback_router = callback_handler( + app_state.settings.tid_header.clone(), + app_state.callback_state.clone(), + ); let message_path_handler = get_message_path(state); Ok(jetstream_proxy .merge(callback_router) @@ -280,8 +308,6 @@ async fn routes( #[cfg(test)] mod tests { - use std::net::SocketAddr; - use async_nats::jetstream::stream; use axum::http::StatusCode; use tokio::time::{sleep, Duration}; @@ -291,6 +317,8 @@ mod tests { use crate::app::callback::store::memstore::InMemoryStore; use crate::config::generate_certs; + const PIPELINE_SPEC_ENCODED: &str = "eyJ2ZXJ0aWNlcyI6W3sibmFtZSI6ImluIiwic291cmNlIjp7InNlcnZpbmciOnsiYXV0aCI6bnVsbCwic2VydmljZSI6dHJ1ZSwibXNnSURIZWFkZXJLZXkiOiJYLU51bWFmbG93LUlkIiwic3RvcmUiOnsidXJsIjoicmVkaXM6Ly9yZWRpczo2Mzc5In19fSwiY29udGFpbmVyVGVtcGxhdGUiOnsicmVzb3VyY2VzIjp7fSwiaW1hZ2VQdWxsUG9saWN5IjoiTmV2ZXIiLCJlbnYiOlt7Im5hbWUiOiJSVVNUX0xPRyIsInZhbHVlIjoiZGVidWcifV19LCJzY2FsZSI6eyJtaW4iOjF9LCJ1cGRhdGVTdHJhdGVneSI6eyJ0eXBlIjoiUm9sbGluZ1VwZGF0ZSIsInJvbGxpbmdVcGRhdGUiOnsibWF4VW5hdmFpbGFibGUiOiIyNSUifX19LHsibmFtZSI6InBsYW5uZXIiLCJ1ZGYiOnsiY29udGFpbmVyIjp7ImltYWdlIjoiYXNjaWk6MC4xIiwiYXJncyI6WyJwbGFubmVyIl0sInJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sImJ1aWx0aW4iOm51bGwsImdyb3VwQnkiOm51bGx9LCJjb250YWluZXJUZW1wbGF0ZSI6eyJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9LCJzY2FsZSI6eyJtaW4iOjF9LCJ1cGRhdGVTdHJhdGVneSI6eyJ0eXBlIjoiUm9sbGluZ1VwZGF0ZSIsInJvbGxpbmdVcGRhdGUiOnsibWF4VW5hdmFpbGFibGUiOiIyNSUifX19LHsibmFtZSI6InRpZ2VyIiwidWRmIjp7ImNvbnRhaW5lciI6eyJpbWFnZSI6ImFzY2lpOjAuMSIsImFyZ3MiOlsidGlnZXIiXSwicmVzb3VyY2VzIjp7fSwiaW1hZ2VQdWxsUG9saWN5IjoiTmV2ZXIifSwiYnVpbHRpbiI6bnVsbCwiZ3JvdXBCeSI6bnVsbH0sImNvbnRhaW5lclRlbXBsYXRlIjp7InJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sInNjYWxlIjp7Im1pbiI6MX0sInVwZGF0ZVN0cmF0ZWd5Ijp7InR5cGUiOiJSb2xsaW5nVXBkYXRlIiwicm9sbGluZ1VwZGF0ZSI6eyJtYXhVbmF2YWlsYWJsZSI6IjI1JSJ9fX0seyJuYW1lIjoiZG9nIiwidWRmIjp7ImNvbnRhaW5lciI6eyJpbWFnZSI6ImFzY2lpOjAuMSIsImFyZ3MiOlsiZG9nIl0sInJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sImJ1aWx0aW4iOm51bGwsImdyb3VwQnkiOm51bGx9LCJjb250YWluZXJUZW1wbGF0ZSI6eyJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9LCJzY2FsZSI6eyJtaW4iOjF9LCJ1cGRhdGVTdHJhdGVneSI6eyJ0eXBlIjoiUm9sbGluZ1VwZGF0ZSIsInJvbGxpbmdVcGRhdGUiOnsibWF4VW5hdmFpbGFibGUiOiIyNSUifX19LHsibmFtZSI6ImVsZXBoYW50IiwidWRmIjp7ImNvbnRhaW5lciI6eyJpbWFnZSI6ImFzY2lpOjAuMSIsImFyZ3MiOlsiZWxlcGhhbnQiXSwicmVzb3VyY2VzIjp7fSwiaW1hZ2VQdWxsUG9saWN5IjoiTmV2ZXIifSwiYnVpbHRpbiI6bnVsbCwiZ3JvdXBCeSI6bnVsbH0sImNvbnRhaW5lclRlbXBsYXRlIjp7InJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sInNjYWxlIjp7Im1pbiI6MX0sInVwZGF0ZVN0cmF0ZWd5Ijp7InR5cGUiOiJSb2xsaW5nVXBkYXRlIiwicm9sbGluZ1VwZGF0ZSI6eyJtYXhVbmF2YWlsYWJsZSI6IjI1JSJ9fX0seyJuYW1lIjoiYXNjaWlhcnQiLCJ1ZGYiOnsiY29udGFpbmVyIjp7ImltYWdlIjoiYXNjaWk6MC4xIiwiYXJncyI6WyJhc2NpaWFydCJdLCJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9LCJidWlsdGluIjpudWxsLCJncm91cEJ5IjpudWxsfSwiY29udGFpbmVyVGVtcGxhdGUiOnsicmVzb3VyY2VzIjp7fSwiaW1hZ2VQdWxsUG9saWN5IjoiTmV2ZXIifSwic2NhbGUiOnsibWluIjoxfSwidXBkYXRlU3RyYXRlZ3kiOnsidHlwZSI6IlJvbGxpbmdVcGRhdGUiLCJyb2xsaW5nVXBkYXRlIjp7Im1heFVuYXZhaWxhYmxlIjoiMjUlIn19fSx7Im5hbWUiOiJzZXJ2ZS1zaW5rIiwic2luayI6eyJ1ZHNpbmsiOnsiY29udGFpbmVyIjp7ImltYWdlIjoic2VydmVzaW5rOjAuMSIsImVudiI6W3sibmFtZSI6Ik5VTUFGTE9XX0NBTExCQUNLX1VSTF9LRVkiLCJ2YWx1ZSI6IlgtTnVtYWZsb3ctQ2FsbGJhY2stVXJsIn0seyJuYW1lIjoiTlVNQUZMT1dfTVNHX0lEX0hFQURFUl9LRVkiLCJ2YWx1ZSI6IlgtTnVtYWZsb3ctSWQifV0sInJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn19LCJyZXRyeVN0cmF0ZWd5Ijp7fX0sImNvbnRhaW5lclRlbXBsYXRlIjp7InJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sInNjYWxlIjp7Im1pbiI6MX0sInVwZGF0ZVN0cmF0ZWd5Ijp7InR5cGUiOiJSb2xsaW5nVXBkYXRlIiwicm9sbGluZ1VwZGF0ZSI6eyJtYXhVbmF2YWlsYWJsZSI6IjI1JSJ9fX0seyJuYW1lIjoiZXJyb3Itc2luayIsInNpbmsiOnsidWRzaW5rIjp7ImNvbnRhaW5lciI6eyJpbWFnZSI6InNlcnZlc2luazowLjEiLCJlbnYiOlt7Im5hbWUiOiJOVU1BRkxPV19DQUxMQkFDS19VUkxfS0VZIiwidmFsdWUiOiJYLU51bWFmbG93LUNhbGxiYWNrLVVybCJ9LHsibmFtZSI6Ik5VTUFGTE9XX01TR19JRF9IRUFERVJfS0VZIiwidmFsdWUiOiJYLU51bWFmbG93LUlkIn1dLCJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9fSwicmV0cnlTdHJhdGVneSI6e319LCJjb250YWluZXJUZW1wbGF0ZSI6eyJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9LCJzY2FsZSI6eyJtaW4iOjF9LCJ1cGRhdGVTdHJhdGVneSI6eyJ0eXBlIjoiUm9sbGluZ1VwZGF0ZSIsInJvbGxpbmdVcGRhdGUiOnsibWF4VW5hdmFpbGFibGUiOiIyNSUifX19XSwiZWRnZXMiOlt7ImZyb20iOiJpbiIsInRvIjoicGxhbm5lciIsImNvbmRpdGlvbnMiOm51bGx9LHsiZnJvbSI6InBsYW5uZXIiLCJ0byI6ImFzY2lpYXJ0IiwiY29uZGl0aW9ucyI6eyJ0YWdzIjp7Im9wZXJhdG9yIjoib3IiLCJ2YWx1ZXMiOlsiYXNjaWlhcnQiXX19fSx7ImZyb20iOiJwbGFubmVyIiwidG8iOiJ0aWdlciIsImNvbmRpdGlvbnMiOnsidGFncyI6eyJvcGVyYXRvciI6Im9yIiwidmFsdWVzIjpbInRpZ2VyIl19fX0seyJmcm9tIjoicGxhbm5lciIsInRvIjoiZG9nIiwiY29uZGl0aW9ucyI6eyJ0YWdzIjp7Im9wZXJhdG9yIjoib3IiLCJ2YWx1ZXMiOlsiZG9nIl19fX0seyJmcm9tIjoicGxhbm5lciIsInRvIjoiZWxlcGhhbnQiLCJjb25kaXRpb25zIjp7InRhZ3MiOnsib3BlcmF0b3IiOiJvciIsInZhbHVlcyI6WyJlbGVwaGFudCJdfX19LHsiZnJvbSI6InRpZ2VyIiwidG8iOiJzZXJ2ZS1zaW5rIiwiY29uZGl0aW9ucyI6bnVsbH0seyJmcm9tIjoiZG9nIiwidG8iOiJzZXJ2ZS1zaW5rIiwiY29uZGl0aW9ucyI6bnVsbH0seyJmcm9tIjoiZWxlcGhhbnQiLCJ0byI6InNlcnZlLXNpbmsiLCJjb25kaXRpb25zIjpudWxsfSx7ImZyb20iOiJhc2NpaWFydCIsInRvIjoic2VydmUtc2luayIsImNvbmRpdGlvbnMiOm51bGx9LHsiZnJvbSI6InBsYW5uZXIiLCJ0byI6ImVycm9yLXNpbmsiLCJjb25kaXRpb25zIjp7InRhZ3MiOnsib3BlcmF0b3IiOiJvciIsInZhbHVlcyI6WyJlcnJvciJdfX19XSwibGlmZWN5Y2xlIjp7fSwid2F0ZXJtYXJrIjp7fX0="; + type Result = core::result::Result; type Error = Box; @@ -302,9 +330,14 @@ mod tests { .await .unwrap(); - let addr = SocketAddr::from(([127, 0, 0, 1], 0)); + let settings = Arc::new(Settings { + app_listen_port: 0, + ..Settings::default() + }); + let server = tokio::spawn(async move { - let result = start_main_server(addr, tls_config).await; + let pipeline_spec = PIPELINE_SPEC_ENCODED.parse().unwrap(); + let result = start_main_server(settings, tls_config, pipeline_spec).await; assert!(result.is_ok()) }); @@ -319,9 +352,10 @@ mod tests { #[cfg(feature = "all-tests")] #[tokio::test] async fn test_setup_app() -> Result<()> { - let client = async_nats::connect(&config().jetstream.url).await?; + let settings = Arc::new(Settings::default()); + let client = async_nats::connect(&settings.jetstream.url).await?; let context = jetstream::new(client); - let stream_name = &config().jetstream.stream; + let stream_name = &settings.jetstream.stream; let stream = context .get_or_create_stream(stream::Config { @@ -334,11 +368,12 @@ mod tests { assert!(stream.is_ok()); let mem_store = InMemoryStore::new(); - let msg_graph = MessageGraph::from_pipeline(min_pipeline_spec())?; + let pipeline_spec = PIPELINE_SPEC_ENCODED.parse().unwrap(); + let msg_graph = MessageGraph::from_pipeline(&pipeline_spec)?; let callback_state = CallbackState::new(msg_graph, mem_store).await?; - let result = setup_app(context, callback_state).await; + let result = setup_app(settings, context, callback_state).await; assert!(result.is_ok()); Ok(()) } @@ -346,9 +381,10 @@ mod tests { #[cfg(feature = "all-tests")] #[tokio::test] async fn test_livez() -> Result<()> { - let client = async_nats::connect(&config().jetstream.url).await?; + let settings = Arc::new(Settings::default()); + let client = async_nats::connect(&settings.jetstream.url).await?; let context = jetstream::new(client); - let stream_name = &config().jetstream.stream; + let stream_name = &settings.jetstream.stream; let stream = context .get_or_create_stream(stream::Config { @@ -361,11 +397,12 @@ mod tests { assert!(stream.is_ok()); let mem_store = InMemoryStore::new(); - let msg_graph = MessageGraph::from_pipeline(min_pipeline_spec())?; + let pipeline_spec = PIPELINE_SPEC_ENCODED.parse().unwrap(); + let msg_graph = MessageGraph::from_pipeline(&pipeline_spec)?; let callback_state = CallbackState::new(msg_graph, mem_store).await?; - let result = setup_app(context, callback_state).await; + let result = setup_app(settings, context, callback_state).await; let request = Request::builder().uri("/livez").body(Body::empty())?; @@ -377,9 +414,10 @@ mod tests { #[cfg(feature = "all-tests")] #[tokio::test] async fn test_readyz() -> Result<()> { - let client = async_nats::connect(&config().jetstream.url).await?; + let settings = Arc::new(Settings::default()); + let client = async_nats::connect(&settings.jetstream.url).await?; let context = jetstream::new(client); - let stream_name = &config().jetstream.stream; + let stream_name = &settings.jetstream.stream; let stream = context .get_or_create_stream(stream::Config { @@ -392,11 +430,12 @@ mod tests { assert!(stream.is_ok()); let mem_store = InMemoryStore::new(); - let msg_graph = MessageGraph::from_pipeline(min_pipeline_spec())?; + let pipeline_spec = PIPELINE_SPEC_ENCODED.parse().unwrap(); + let msg_graph = MessageGraph::from_pipeline(&pipeline_spec)?; let callback_state = CallbackState::new(msg_graph, mem_store).await?; - let result = setup_app(context, callback_state).await; + let result = setup_app(settings, context, callback_state).await; let request = Request::builder().uri("/readyz").body(Body::empty())?; @@ -415,9 +454,10 @@ mod tests { #[cfg(feature = "all-tests")] #[tokio::test] async fn test_auth_middleware() -> Result<()> { - let client = async_nats::connect(&config().jetstream.url).await?; + let settings = Arc::new(Settings::default()); + let client = async_nats::connect(&settings.jetstream.url).await?; let context = jetstream::new(client); - let stream_name = &config().jetstream.stream; + let stream_name = &settings.jetstream.stream; let stream = context .get_or_create_stream(stream::Config { @@ -430,17 +470,23 @@ mod tests { assert!(stream.is_ok()); let mem_store = InMemoryStore::new(); - let msg_graph = MessageGraph::from_pipeline(min_pipeline_spec())?; + let pipeline_spec = PIPELINE_SPEC_ENCODED.parse().unwrap(); + let msg_graph = MessageGraph::from_pipeline(&pipeline_spec)?; let callback_state = CallbackState::new(msg_graph, mem_store).await?; + let app_state = AppState { + settings, + callback_state, + context, + }; + let app = Router::new() - .nest( - "/v1/process", - routes(context, callback_state).await.unwrap(), - ) - .layer(middleware::from_fn(auth_middleware)); + .nest("/v1/process", routes(app_state).await.unwrap()) + .layer(middleware::from_fn_with_state( + Some("test_token".to_owned()), + auth_middleware, + )); - env::set_var(ENV_NUMAFLOW_SERVING_AUTH_TOKEN, "test_token"); let res = app .oneshot( axum::extract::Request::builder() @@ -451,7 +497,6 @@ mod tests { .await?; assert_eq!(res.status(), StatusCode::UNAUTHORIZED); - env::remove_var(ENV_NUMAFLOW_SERVING_AUTH_TOKEN); Ok(()) } } diff --git a/rust/serving/src/app/callback.rs b/rust/serving/src/app/callback.rs index 2fe7a2f6fe..b4d43868ee 100644 --- a/rust/serving/src/app/callback.rs +++ b/rust/serving/src/app/callback.rs @@ -1,14 +1,14 @@ use axum::{body::Bytes, extract::State, http::HeaderMap, routing, Json, Router}; use serde::{Deserialize, Serialize}; -use state::State as CallbackState; use tracing::error; use self::store::Store; use crate::app::response::ApiError; -use crate::config; /// in-memory state store including connection tracking pub(crate) mod state; +use state::State as CallbackState; + /// store for storing the state pub(crate) mod store; @@ -21,38 +21,58 @@ pub(crate) struct CallbackRequest { pub(crate) tags: Option>, } +#[derive(Clone)] +struct CallbackAppState { + tid_header: String, + callback_state: CallbackState, +} + pub fn callback_handler( - callback_store: CallbackState, + tid_header: String, + callback_state: CallbackState, ) -> Router { + let app_state = CallbackAppState { + tid_header, + callback_state, + }; Router::new() .route("/callback", routing::post(callback)) .route("/callback_save", routing::post(callback_save)) - .with_state(callback_store) + .with_state(app_state) } async fn callback_save( - State(mut proxy_state): State>, + State(app_state): State>, headers: HeaderMap, body: Bytes, ) -> Result<(), ApiError> { let id = headers - .get(&config().tid_header) + .get(&app_state.tid_header) .map(|id| String::from_utf8_lossy(id.as_bytes()).to_string()) .ok_or_else(|| ApiError::BadRequest("Missing id header".to_string()))?; - proxy_state.save_response(id, body).await.map_err(|e| { - error!(error=?e, "Saving body from callback save request"); - ApiError::InternalServerError("Failed to save body from callback save request".to_string()) - })?; + app_state + .callback_state + .clone() + .save_response(id, body) + .await + .map_err(|e| { + error!(error=?e, "Saving body from callback save request"); + ApiError::InternalServerError( + "Failed to save body from callback save request".to_string(), + ) + })?; Ok(()) } async fn callback( - State(mut proxy_state): State>, + State(app_state): State>, Json(payload): Json>, ) -> Result<(), ApiError> { - proxy_state + app_state + .callback_state + .clone() .insert_callback_requests(payload) .await .map_err(|e| { @@ -72,16 +92,20 @@ mod tests { use tower::ServiceExt; use super::*; + use crate::app::callback::state::State as CallbackState; use crate::app::callback::store::memstore::InMemoryStore; use crate::app::tracker::MessageGraph; - use crate::pipeline::min_pipeline_spec; + use crate::pipeline::PipelineDCG; + + const PIPELINE_SPEC_ENCODED: &str = "eyJ2ZXJ0aWNlcyI6W3sibmFtZSI6ImluIiwic291cmNlIjp7InNlcnZpbmciOnsiYXV0aCI6bnVsbCwic2VydmljZSI6dHJ1ZSwibXNnSURIZWFkZXJLZXkiOiJYLU51bWFmbG93LUlkIiwic3RvcmUiOnsidXJsIjoicmVkaXM6Ly9yZWRpczo2Mzc5In19fSwiY29udGFpbmVyVGVtcGxhdGUiOnsicmVzb3VyY2VzIjp7fSwiaW1hZ2VQdWxsUG9saWN5IjoiTmV2ZXIiLCJlbnYiOlt7Im5hbWUiOiJSVVNUX0xPRyIsInZhbHVlIjoiZGVidWcifV19LCJzY2FsZSI6eyJtaW4iOjF9LCJ1cGRhdGVTdHJhdGVneSI6eyJ0eXBlIjoiUm9sbGluZ1VwZGF0ZSIsInJvbGxpbmdVcGRhdGUiOnsibWF4VW5hdmFpbGFibGUiOiIyNSUifX19LHsibmFtZSI6InBsYW5uZXIiLCJ1ZGYiOnsiY29udGFpbmVyIjp7ImltYWdlIjoiYXNjaWk6MC4xIiwiYXJncyI6WyJwbGFubmVyIl0sInJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sImJ1aWx0aW4iOm51bGwsImdyb3VwQnkiOm51bGx9LCJjb250YWluZXJUZW1wbGF0ZSI6eyJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9LCJzY2FsZSI6eyJtaW4iOjF9LCJ1cGRhdGVTdHJhdGVneSI6eyJ0eXBlIjoiUm9sbGluZ1VwZGF0ZSIsInJvbGxpbmdVcGRhdGUiOnsibWF4VW5hdmFpbGFibGUiOiIyNSUifX19LHsibmFtZSI6InRpZ2VyIiwidWRmIjp7ImNvbnRhaW5lciI6eyJpbWFnZSI6ImFzY2lpOjAuMSIsImFyZ3MiOlsidGlnZXIiXSwicmVzb3VyY2VzIjp7fSwiaW1hZ2VQdWxsUG9saWN5IjoiTmV2ZXIifSwiYnVpbHRpbiI6bnVsbCwiZ3JvdXBCeSI6bnVsbH0sImNvbnRhaW5lclRlbXBsYXRlIjp7InJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sInNjYWxlIjp7Im1pbiI6MX0sInVwZGF0ZVN0cmF0ZWd5Ijp7InR5cGUiOiJSb2xsaW5nVXBkYXRlIiwicm9sbGluZ1VwZGF0ZSI6eyJtYXhVbmF2YWlsYWJsZSI6IjI1JSJ9fX0seyJuYW1lIjoiZG9nIiwidWRmIjp7ImNvbnRhaW5lciI6eyJpbWFnZSI6ImFzY2lpOjAuMSIsImFyZ3MiOlsiZG9nIl0sInJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sImJ1aWx0aW4iOm51bGwsImdyb3VwQnkiOm51bGx9LCJjb250YWluZXJUZW1wbGF0ZSI6eyJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9LCJzY2FsZSI6eyJtaW4iOjF9LCJ1cGRhdGVTdHJhdGVneSI6eyJ0eXBlIjoiUm9sbGluZ1VwZGF0ZSIsInJvbGxpbmdVcGRhdGUiOnsibWF4VW5hdmFpbGFibGUiOiIyNSUifX19LHsibmFtZSI6ImVsZXBoYW50IiwidWRmIjp7ImNvbnRhaW5lciI6eyJpbWFnZSI6ImFzY2lpOjAuMSIsImFyZ3MiOlsiZWxlcGhhbnQiXSwicmVzb3VyY2VzIjp7fSwiaW1hZ2VQdWxsUG9saWN5IjoiTmV2ZXIifSwiYnVpbHRpbiI6bnVsbCwiZ3JvdXBCeSI6bnVsbH0sImNvbnRhaW5lclRlbXBsYXRlIjp7InJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sInNjYWxlIjp7Im1pbiI6MX0sInVwZGF0ZVN0cmF0ZWd5Ijp7InR5cGUiOiJSb2xsaW5nVXBkYXRlIiwicm9sbGluZ1VwZGF0ZSI6eyJtYXhVbmF2YWlsYWJsZSI6IjI1JSJ9fX0seyJuYW1lIjoiYXNjaWlhcnQiLCJ1ZGYiOnsiY29udGFpbmVyIjp7ImltYWdlIjoiYXNjaWk6MC4xIiwiYXJncyI6WyJhc2NpaWFydCJdLCJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9LCJidWlsdGluIjpudWxsLCJncm91cEJ5IjpudWxsfSwiY29udGFpbmVyVGVtcGxhdGUiOnsicmVzb3VyY2VzIjp7fSwiaW1hZ2VQdWxsUG9saWN5IjoiTmV2ZXIifSwic2NhbGUiOnsibWluIjoxfSwidXBkYXRlU3RyYXRlZ3kiOnsidHlwZSI6IlJvbGxpbmdVcGRhdGUiLCJyb2xsaW5nVXBkYXRlIjp7Im1heFVuYXZhaWxhYmxlIjoiMjUlIn19fSx7Im5hbWUiOiJzZXJ2ZS1zaW5rIiwic2luayI6eyJ1ZHNpbmsiOnsiY29udGFpbmVyIjp7ImltYWdlIjoic2VydmVzaW5rOjAuMSIsImVudiI6W3sibmFtZSI6Ik5VTUFGTE9XX0NBTExCQUNLX1VSTF9LRVkiLCJ2YWx1ZSI6IlgtTnVtYWZsb3ctQ2FsbGJhY2stVXJsIn0seyJuYW1lIjoiTlVNQUZMT1dfTVNHX0lEX0hFQURFUl9LRVkiLCJ2YWx1ZSI6IlgtTnVtYWZsb3ctSWQifV0sInJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn19LCJyZXRyeVN0cmF0ZWd5Ijp7fX0sImNvbnRhaW5lclRlbXBsYXRlIjp7InJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sInNjYWxlIjp7Im1pbiI6MX0sInVwZGF0ZVN0cmF0ZWd5Ijp7InR5cGUiOiJSb2xsaW5nVXBkYXRlIiwicm9sbGluZ1VwZGF0ZSI6eyJtYXhVbmF2YWlsYWJsZSI6IjI1JSJ9fX0seyJuYW1lIjoiZXJyb3Itc2luayIsInNpbmsiOnsidWRzaW5rIjp7ImNvbnRhaW5lciI6eyJpbWFnZSI6InNlcnZlc2luazowLjEiLCJlbnYiOlt7Im5hbWUiOiJOVU1BRkxPV19DQUxMQkFDS19VUkxfS0VZIiwidmFsdWUiOiJYLU51bWFmbG93LUNhbGxiYWNrLVVybCJ9LHsibmFtZSI6Ik5VTUFGTE9XX01TR19JRF9IRUFERVJfS0VZIiwidmFsdWUiOiJYLU51bWFmbG93LUlkIn1dLCJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9fSwicmV0cnlTdHJhdGVneSI6e319LCJjb250YWluZXJUZW1wbGF0ZSI6eyJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9LCJzY2FsZSI6eyJtaW4iOjF9LCJ1cGRhdGVTdHJhdGVneSI6eyJ0eXBlIjoiUm9sbGluZ1VwZGF0ZSIsInJvbGxpbmdVcGRhdGUiOnsibWF4VW5hdmFpbGFibGUiOiIyNSUifX19XSwiZWRnZXMiOlt7ImZyb20iOiJpbiIsInRvIjoicGxhbm5lciIsImNvbmRpdGlvbnMiOm51bGx9LHsiZnJvbSI6InBsYW5uZXIiLCJ0byI6ImFzY2lpYXJ0IiwiY29uZGl0aW9ucyI6eyJ0YWdzIjp7Im9wZXJhdG9yIjoib3IiLCJ2YWx1ZXMiOlsiYXNjaWlhcnQiXX19fSx7ImZyb20iOiJwbGFubmVyIiwidG8iOiJ0aWdlciIsImNvbmRpdGlvbnMiOnsidGFncyI6eyJvcGVyYXRvciI6Im9yIiwidmFsdWVzIjpbInRpZ2VyIl19fX0seyJmcm9tIjoicGxhbm5lciIsInRvIjoiZG9nIiwiY29uZGl0aW9ucyI6eyJ0YWdzIjp7Im9wZXJhdG9yIjoib3IiLCJ2YWx1ZXMiOlsiZG9nIl19fX0seyJmcm9tIjoicGxhbm5lciIsInRvIjoiZWxlcGhhbnQiLCJjb25kaXRpb25zIjp7InRhZ3MiOnsib3BlcmF0b3IiOiJvciIsInZhbHVlcyI6WyJlbGVwaGFudCJdfX19LHsiZnJvbSI6InRpZ2VyIiwidG8iOiJzZXJ2ZS1zaW5rIiwiY29uZGl0aW9ucyI6bnVsbH0seyJmcm9tIjoiZG9nIiwidG8iOiJzZXJ2ZS1zaW5rIiwiY29uZGl0aW9ucyI6bnVsbH0seyJmcm9tIjoiZWxlcGhhbnQiLCJ0byI6InNlcnZlLXNpbmsiLCJjb25kaXRpb25zIjpudWxsfSx7ImZyb20iOiJhc2NpaWFydCIsInRvIjoic2VydmUtc2luayIsImNvbmRpdGlvbnMiOm51bGx9LHsiZnJvbSI6InBsYW5uZXIiLCJ0byI6ImVycm9yLXNpbmsiLCJjb25kaXRpb25zIjp7InRhZ3MiOnsib3BlcmF0b3IiOiJvciIsInZhbHVlcyI6WyJlcnJvciJdfX19XSwibGlmZWN5Y2xlIjp7fSwid2F0ZXJtYXJrIjp7fX0="; #[tokio::test] async fn test_callback_failure() { let store = InMemoryStore::new(); - let msg_graph = MessageGraph::from_pipeline(min_pipeline_spec()).unwrap(); + let pipeline_spec: PipelineDCG = PIPELINE_SPEC_ENCODED.parse().unwrap(); + let msg_graph = MessageGraph::from_pipeline(&pipeline_spec).unwrap(); let state = CallbackState::new(msg_graph, store).await.unwrap(); - let app = callback_handler(state); + let app = callback_handler("ID".to_owned(), state); let payload = vec![CallbackRequest { id: "test_id".to_string(), @@ -106,7 +130,8 @@ mod tests { #[tokio::test] async fn test_callback_success() { let store = InMemoryStore::new(); - let msg_graph = MessageGraph::from_pipeline(min_pipeline_spec()).unwrap(); + let pipeline_spec: PipelineDCG = PIPELINE_SPEC_ENCODED.parse().unwrap(); + let msg_graph = MessageGraph::from_pipeline(&pipeline_spec).unwrap(); let mut state = CallbackState::new(msg_graph, store).await.unwrap(); let x = state.register("test_id".to_string()); @@ -115,7 +140,7 @@ mod tests { let _ = x.await.unwrap(); }); - let app = callback_handler(state); + let app = callback_handler("ID".to_owned(), state); let payload = vec![ CallbackRequest { @@ -157,9 +182,10 @@ mod tests { #[tokio::test] async fn test_callback_save() { let store = InMemoryStore::new(); - let msg_graph = MessageGraph::from_pipeline(min_pipeline_spec()).unwrap(); + let pipeline_spec: PipelineDCG = PIPELINE_SPEC_ENCODED.parse().unwrap(); + let msg_graph = MessageGraph::from_pipeline(&pipeline_spec).unwrap(); let state = CallbackState::new(msg_graph, store).await.unwrap(); - let app = callback_handler(state); + let app = callback_handler("ID".to_owned(), state); let res = Request::builder() .method("POST") @@ -176,9 +202,10 @@ mod tests { #[tokio::test] async fn test_without_id_header() { let store = InMemoryStore::new(); - let msg_graph = MessageGraph::from_pipeline(min_pipeline_spec()).unwrap(); + let pipeline_spec: PipelineDCG = PIPELINE_SPEC_ENCODED.parse().unwrap(); + let msg_graph = MessageGraph::from_pipeline(&pipeline_spec).unwrap(); let state = CallbackState::new(msg_graph, store).await.unwrap(); - let app = callback_handler(state); + let app = callback_handler("ID".to_owned(), state); let res = Request::builder() .method("POST") diff --git a/rust/serving/src/app/callback/state.rs b/rust/serving/src/app/callback/state.rs index db145f5beb..293478ead2 100644 --- a/rust/serving/src/app/callback/state.rs +++ b/rust/serving/src/app/callback/state.rs @@ -236,11 +236,14 @@ mod tests { use super::*; use crate::app::callback::store::memstore::InMemoryStore; - use crate::pipeline::min_pipeline_spec; + use crate::pipeline::PipelineDCG; + + const PIPELINE_SPEC_ENCODED: &str = "eyJ2ZXJ0aWNlcyI6W3sibmFtZSI6ImluIiwic291cmNlIjp7InNlcnZpbmciOnsiYXV0aCI6bnVsbCwic2VydmljZSI6dHJ1ZSwibXNnSURIZWFkZXJLZXkiOiJYLU51bWFmbG93LUlkIiwic3RvcmUiOnsidXJsIjoicmVkaXM6Ly9yZWRpczo2Mzc5In19fSwiY29udGFpbmVyVGVtcGxhdGUiOnsicmVzb3VyY2VzIjp7fSwiaW1hZ2VQdWxsUG9saWN5IjoiTmV2ZXIiLCJlbnYiOlt7Im5hbWUiOiJSVVNUX0xPRyIsInZhbHVlIjoiZGVidWcifV19LCJzY2FsZSI6eyJtaW4iOjF9LCJ1cGRhdGVTdHJhdGVneSI6eyJ0eXBlIjoiUm9sbGluZ1VwZGF0ZSIsInJvbGxpbmdVcGRhdGUiOnsibWF4VW5hdmFpbGFibGUiOiIyNSUifX19LHsibmFtZSI6InBsYW5uZXIiLCJ1ZGYiOnsiY29udGFpbmVyIjp7ImltYWdlIjoiYXNjaWk6MC4xIiwiYXJncyI6WyJwbGFubmVyIl0sInJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sImJ1aWx0aW4iOm51bGwsImdyb3VwQnkiOm51bGx9LCJjb250YWluZXJUZW1wbGF0ZSI6eyJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9LCJzY2FsZSI6eyJtaW4iOjF9LCJ1cGRhdGVTdHJhdGVneSI6eyJ0eXBlIjoiUm9sbGluZ1VwZGF0ZSIsInJvbGxpbmdVcGRhdGUiOnsibWF4VW5hdmFpbGFibGUiOiIyNSUifX19LHsibmFtZSI6InRpZ2VyIiwidWRmIjp7ImNvbnRhaW5lciI6eyJpbWFnZSI6ImFzY2lpOjAuMSIsImFyZ3MiOlsidGlnZXIiXSwicmVzb3VyY2VzIjp7fSwiaW1hZ2VQdWxsUG9saWN5IjoiTmV2ZXIifSwiYnVpbHRpbiI6bnVsbCwiZ3JvdXBCeSI6bnVsbH0sImNvbnRhaW5lclRlbXBsYXRlIjp7InJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sInNjYWxlIjp7Im1pbiI6MX0sInVwZGF0ZVN0cmF0ZWd5Ijp7InR5cGUiOiJSb2xsaW5nVXBkYXRlIiwicm9sbGluZ1VwZGF0ZSI6eyJtYXhVbmF2YWlsYWJsZSI6IjI1JSJ9fX0seyJuYW1lIjoiZG9nIiwidWRmIjp7ImNvbnRhaW5lciI6eyJpbWFnZSI6ImFzY2lpOjAuMSIsImFyZ3MiOlsiZG9nIl0sInJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sImJ1aWx0aW4iOm51bGwsImdyb3VwQnkiOm51bGx9LCJjb250YWluZXJUZW1wbGF0ZSI6eyJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9LCJzY2FsZSI6eyJtaW4iOjF9LCJ1cGRhdGVTdHJhdGVneSI6eyJ0eXBlIjoiUm9sbGluZ1VwZGF0ZSIsInJvbGxpbmdVcGRhdGUiOnsibWF4VW5hdmFpbGFibGUiOiIyNSUifX19LHsibmFtZSI6ImVsZXBoYW50IiwidWRmIjp7ImNvbnRhaW5lciI6eyJpbWFnZSI6ImFzY2lpOjAuMSIsImFyZ3MiOlsiZWxlcGhhbnQiXSwicmVzb3VyY2VzIjp7fSwiaW1hZ2VQdWxsUG9saWN5IjoiTmV2ZXIifSwiYnVpbHRpbiI6bnVsbCwiZ3JvdXBCeSI6bnVsbH0sImNvbnRhaW5lclRlbXBsYXRlIjp7InJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sInNjYWxlIjp7Im1pbiI6MX0sInVwZGF0ZVN0cmF0ZWd5Ijp7InR5cGUiOiJSb2xsaW5nVXBkYXRlIiwicm9sbGluZ1VwZGF0ZSI6eyJtYXhVbmF2YWlsYWJsZSI6IjI1JSJ9fX0seyJuYW1lIjoiYXNjaWlhcnQiLCJ1ZGYiOnsiY29udGFpbmVyIjp7ImltYWdlIjoiYXNjaWk6MC4xIiwiYXJncyI6WyJhc2NpaWFydCJdLCJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9LCJidWlsdGluIjpudWxsLCJncm91cEJ5IjpudWxsfSwiY29udGFpbmVyVGVtcGxhdGUiOnsicmVzb3VyY2VzIjp7fSwiaW1hZ2VQdWxsUG9saWN5IjoiTmV2ZXIifSwic2NhbGUiOnsibWluIjoxfSwidXBkYXRlU3RyYXRlZ3kiOnsidHlwZSI6IlJvbGxpbmdVcGRhdGUiLCJyb2xsaW5nVXBkYXRlIjp7Im1heFVuYXZhaWxhYmxlIjoiMjUlIn19fSx7Im5hbWUiOiJzZXJ2ZS1zaW5rIiwic2luayI6eyJ1ZHNpbmsiOnsiY29udGFpbmVyIjp7ImltYWdlIjoic2VydmVzaW5rOjAuMSIsImVudiI6W3sibmFtZSI6Ik5VTUFGTE9XX0NBTExCQUNLX1VSTF9LRVkiLCJ2YWx1ZSI6IlgtTnVtYWZsb3ctQ2FsbGJhY2stVXJsIn0seyJuYW1lIjoiTlVNQUZMT1dfTVNHX0lEX0hFQURFUl9LRVkiLCJ2YWx1ZSI6IlgtTnVtYWZsb3ctSWQifV0sInJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn19LCJyZXRyeVN0cmF0ZWd5Ijp7fX0sImNvbnRhaW5lclRlbXBsYXRlIjp7InJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sInNjYWxlIjp7Im1pbiI6MX0sInVwZGF0ZVN0cmF0ZWd5Ijp7InR5cGUiOiJSb2xsaW5nVXBkYXRlIiwicm9sbGluZ1VwZGF0ZSI6eyJtYXhVbmF2YWlsYWJsZSI6IjI1JSJ9fX0seyJuYW1lIjoiZXJyb3Itc2luayIsInNpbmsiOnsidWRzaW5rIjp7ImNvbnRhaW5lciI6eyJpbWFnZSI6InNlcnZlc2luazowLjEiLCJlbnYiOlt7Im5hbWUiOiJOVU1BRkxPV19DQUxMQkFDS19VUkxfS0VZIiwidmFsdWUiOiJYLU51bWFmbG93LUNhbGxiYWNrLVVybCJ9LHsibmFtZSI6Ik5VTUFGTE9XX01TR19JRF9IRUFERVJfS0VZIiwidmFsdWUiOiJYLU51bWFmbG93LUlkIn1dLCJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9fSwicmV0cnlTdHJhdGVneSI6e319LCJjb250YWluZXJUZW1wbGF0ZSI6eyJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9LCJzY2FsZSI6eyJtaW4iOjF9LCJ1cGRhdGVTdHJhdGVneSI6eyJ0eXBlIjoiUm9sbGluZ1VwZGF0ZSIsInJvbGxpbmdVcGRhdGUiOnsibWF4VW5hdmFpbGFibGUiOiIyNSUifX19XSwiZWRnZXMiOlt7ImZyb20iOiJpbiIsInRvIjoicGxhbm5lciIsImNvbmRpdGlvbnMiOm51bGx9LHsiZnJvbSI6InBsYW5uZXIiLCJ0byI6ImFzY2lpYXJ0IiwiY29uZGl0aW9ucyI6eyJ0YWdzIjp7Im9wZXJhdG9yIjoib3IiLCJ2YWx1ZXMiOlsiYXNjaWlhcnQiXX19fSx7ImZyb20iOiJwbGFubmVyIiwidG8iOiJ0aWdlciIsImNvbmRpdGlvbnMiOnsidGFncyI6eyJvcGVyYXRvciI6Im9yIiwidmFsdWVzIjpbInRpZ2VyIl19fX0seyJmcm9tIjoicGxhbm5lciIsInRvIjoiZG9nIiwiY29uZGl0aW9ucyI6eyJ0YWdzIjp7Im9wZXJhdG9yIjoib3IiLCJ2YWx1ZXMiOlsiZG9nIl19fX0seyJmcm9tIjoicGxhbm5lciIsInRvIjoiZWxlcGhhbnQiLCJjb25kaXRpb25zIjp7InRhZ3MiOnsib3BlcmF0b3IiOiJvciIsInZhbHVlcyI6WyJlbGVwaGFudCJdfX19LHsiZnJvbSI6InRpZ2VyIiwidG8iOiJzZXJ2ZS1zaW5rIiwiY29uZGl0aW9ucyI6bnVsbH0seyJmcm9tIjoiZG9nIiwidG8iOiJzZXJ2ZS1zaW5rIiwiY29uZGl0aW9ucyI6bnVsbH0seyJmcm9tIjoiZWxlcGhhbnQiLCJ0byI6InNlcnZlLXNpbmsiLCJjb25kaXRpb25zIjpudWxsfSx7ImZyb20iOiJhc2NpaWFydCIsInRvIjoic2VydmUtc2luayIsImNvbmRpdGlvbnMiOm51bGx9LHsiZnJvbSI6InBsYW5uZXIiLCJ0byI6ImVycm9yLXNpbmsiLCJjb25kaXRpb25zIjp7InRhZ3MiOnsib3BlcmF0b3IiOiJvciIsInZhbHVlcyI6WyJlcnJvciJdfX19XSwibGlmZWN5Y2xlIjp7fSwid2F0ZXJtYXJrIjp7fX0="; #[tokio::test] async fn test_state() { - let msg_graph = MessageGraph::from_pipeline(min_pipeline_spec()).unwrap(); + let pipeline_spec: PipelineDCG = PIPELINE_SPEC_ENCODED.parse().unwrap(); + let msg_graph = MessageGraph::from_pipeline(&pipeline_spec).unwrap(); let store = InMemoryStore::new(); let mut state = State::new(msg_graph, store).await.unwrap(); @@ -277,16 +280,37 @@ mod tests { }, CallbackRequest { id: id.clone(), - vertex: "cat".to_string(), + vertex: "planner".to_string(), cb_time: 12345, from_vertex: "in".to_string(), + tags: Some(vec!["tiger".to_owned(), "asciiart".to_owned()]), + }, + CallbackRequest { + id: id.clone(), + vertex: "tiger".to_string(), + cb_time: 12345, + from_vertex: "planner".to_string(), + tags: None, + }, + CallbackRequest { + id: id.clone(), + vertex: "asciiart".to_string(), + cb_time: 12345, + from_vertex: "planner".to_string(), + tags: None, + }, + CallbackRequest { + id: id.clone(), + vertex: "serve-sink".to_string(), + cb_time: 12345, + from_vertex: "tiger".to_string(), tags: None, }, CallbackRequest { id: id.clone(), - vertex: "out".to_string(), + vertex: "serve-sink".to_string(), cb_time: 12345, - from_vertex: "cat".to_string(), + from_vertex: "asciiart".to_string(), tags: None, }, ]; @@ -300,7 +324,8 @@ mod tests { #[tokio::test] async fn test_retrieve_saved_no_entry() { - let msg_graph = MessageGraph::from_pipeline(min_pipeline_spec()).unwrap(); + let pipeline_spec: PipelineDCG = PIPELINE_SPEC_ENCODED.parse().unwrap(); + let msg_graph = MessageGraph::from_pipeline(&pipeline_spec).unwrap(); let store = InMemoryStore::new(); let mut state = State::new(msg_graph, store).await.unwrap(); @@ -315,7 +340,8 @@ mod tests { #[tokio::test] async fn test_insert_callback_requests_invalid_id() { - let msg_graph = MessageGraph::from_pipeline(min_pipeline_spec()).unwrap(); + let pipeline_spec: PipelineDCG = PIPELINE_SPEC_ENCODED.parse().unwrap(); + let msg_graph = MessageGraph::from_pipeline(&pipeline_spec).unwrap(); let store = InMemoryStore::new(); let mut state = State::new(msg_graph, store).await.unwrap(); @@ -336,7 +362,8 @@ mod tests { #[tokio::test] async fn test_retrieve_subgraph_from_storage_no_entry() { - let msg_graph = MessageGraph::from_pipeline(min_pipeline_spec()).unwrap(); + let pipeline_spec: PipelineDCG = PIPELINE_SPEC_ENCODED.parse().unwrap(); + let msg_graph = MessageGraph::from_pipeline(&pipeline_spec).unwrap(); let store = InMemoryStore::new(); let mut state = State::new(msg_graph, store).await.unwrap(); diff --git a/rust/serving/src/app/callback/store/redisstore.rs b/rust/serving/src/app/callback/store/redisstore.rs index 6e5decf880..4439e7ce8e 100644 --- a/rust/serving/src/app/callback/store/redisstore.rs +++ b/rust/serving/src/app/callback/store/redisstore.rs @@ -8,9 +8,10 @@ use tokio::sync::Semaphore; use super::PayloadToSave; use crate::app::callback::CallbackRequest; +use crate::config::RedisConfig; use crate::consts::SAVED; +use crate::Error; use crate::Error::Connection; -use crate::{config, Error}; const LPUSH: &str = "LPUSH"; const LRANGE: &str = "LRANGE"; @@ -19,14 +20,14 @@ const EXPIRE: &str = "EXPIRE"; // Handle to the Redis actor. #[derive(Clone)] pub(crate) struct RedisConnection { - max_tasks: usize, conn_manager: ConnectionManager, + config: RedisConfig, } impl RedisConnection { /// Creates a new RedisConnection with concurrent operations on Redis set by max_tasks. - pub(crate) async fn new(addr: &str, max_tasks: usize) -> crate::Result { - let client = redis::Client::open(addr) + pub(crate) async fn new(config: RedisConfig) -> crate::Result { + let client = redis::Client::open(config.addr.as_str()) .map_err(|e| Connection(format!("Creating Redis client: {e:?}")))?; let conn = client .get_connection_manager() @@ -34,37 +35,13 @@ impl RedisConnection { .map_err(|e| Connection(format!("Connecting to Redis server: {e:?}")))?; Ok(Self { conn_manager: conn, - max_tasks, + config, }) } - async fn handle_write_requests( - conn_manager: &mut ConnectionManager, - msg: PayloadToSave, - ) -> crate::Result<()> { - match msg { - PayloadToSave::Callback { key, value } => { - // Convert the CallbackRequest to a byte array - let value = serde_json::to_vec(&*value) - .map_err(|e| Error::StoreWrite(format!("Serializing payload - {}", e)))?; - - Self::write_to_redis(conn_manager, &key, &value).await - } - - // Write the byte array to Redis - PayloadToSave::DatumFromPipeline { key, value } => { - // we have to differentiate between the saved responses and the callback requests - // saved responses are stored in "id_SAVED", callback requests are stored in "id" - let key = format!("{}_{}", key, SAVED); - let value: Vec = value.into(); - - Self::write_to_redis(conn_manager, &key, &value).await - } - } - } - async fn execute_redis_cmd( conn_manager: &mut ConnectionManager, + ttl_secs: Option, key: &str, val: &Vec, ) -> Result<(), RedisError> { @@ -72,7 +49,7 @@ impl RedisConnection { pipe.cmd(LPUSH).arg(key).arg(val); // if the ttl is configured, add the EXPIRE command to the pipeline - if let Some(ttl) = config().redis.ttl_secs { + if let Some(ttl) = ttl_secs { pipe.cmd(EXPIRE).arg(key).arg(ttl); } @@ -81,19 +58,21 @@ impl RedisConnection { } // write to Redis with retries - async fn write_to_redis( - conn_manager: &mut ConnectionManager, - key: &str, - value: &Vec, - ) -> crate::Result<()> { - let interval = fixed::Interval::from_millis(config().redis.retries_duration_millis.into()) - .take(config().redis.retries); + async fn write_to_redis(&self, key: &str, value: &Vec) -> crate::Result<()> { + let interval = fixed::Interval::from_millis(self.config.retries_duration_millis.into()) + .take(self.config.retries); Retry::retry( interval, || async { // https://hackmd.io/@compiler-errors/async-closures - Self::execute_redis_cmd(&mut conn_manager.clone(), key, value).await + Self::execute_redis_cmd( + &mut self.conn_manager.clone(), + self.config.ttl_secs, + key, + value, + ) + .await }, |e: &RedisError| !e.is_unrecoverable_error(), ) @@ -102,6 +81,31 @@ impl RedisConnection { } } +async fn handle_write_requests( + redis_conn: RedisConnection, + msg: PayloadToSave, +) -> crate::Result<()> { + match msg { + PayloadToSave::Callback { key, value } => { + // Convert the CallbackRequest to a byte array + let value = serde_json::to_vec(&*value) + .map_err(|e| Error::StoreWrite(format!("Serializing payload - {}", e)))?; + + redis_conn.write_to_redis(&key, &value).await + } + + // Write the byte array to Redis + PayloadToSave::DatumFromPipeline { key, value } => { + // we have to differentiate between the saved responses and the callback requests + // saved responses are stored in "id_SAVED", callback requests are stored in "id" + let key = format!("{}_{}", key, SAVED); + let value: Vec = value.into(); + + redis_conn.write_to_redis(&key, &value).await + } + } +} + // It is possible to move the methods defined here to be methods on the Redis actor and communicate through channels. // With that, all public APIs defined on RedisConnection can be on &self (immutable). impl super::Store for RedisConnection { @@ -110,13 +114,13 @@ impl super::Store for RedisConnection { let mut tasks = vec![]; // This is put in place not to overload Redis and also way some kind of // flow control. - let sem = Arc::new(Semaphore::new(self.max_tasks)); + let sem = Arc::new(Semaphore::new(self.config.max_tasks)); for msg in messages { let permit = Arc::clone(&sem).acquire_owned().await; - let mut _conn_mgr = self.conn_manager.clone(); + let redis_conn = self.clone(); let task = tokio::spawn(async move { let _permit = permit; - Self::handle_write_requests(&mut _conn_mgr, msg).await + handle_write_requests(redis_conn, msg).await }); tasks.push(task); } @@ -205,12 +209,16 @@ mod tests { #[tokio::test] async fn test_redis_store() { - let redis_connection = RedisConnection::new("no_such_redis://127.0.0.1:6379", 10).await; + let redis_config = RedisConfig { + addr: "no_such_redis://127.0.0.1:6379".to_owned(), + max_tasks: 10, + ..Default::default() + }; + let redis_connection = RedisConnection::new(redis_config).await; assert!(redis_connection.is_err()); // Test Redis connection - let redis_connection = - RedisConnection::new(format!("redis://127.0.0.1:{}", "6379").as_str(), 10).await; + let redis_connection = RedisConnection::new(RedisConfig::default()).await; assert!(redis_connection.is_ok()); let key = uuid::Uuid::new_v4().to_string(); @@ -273,7 +281,11 @@ mod tests { #[tokio::test] async fn test_redis_ttl() { - let redis_connection = RedisConnection::new("redis://127.0.0.1:6379", 10) + let redis_config = RedisConfig { + max_tasks: 10, + ..Default::default() + }; + let redis_connection = RedisConnection::new(redis_config) .await .expect("Failed to connect to Redis"); @@ -287,14 +299,12 @@ mod tests { }); // Save with TTL of 1 second + redis_connection + .write_to_redis(&key, &serde_json::to_vec(&*value).unwrap()) + .await + .expect("Failed to write to Redis"); + let mut conn_manager = redis_connection.conn_manager.clone(); - RedisConnection::write_to_redis( - &mut conn_manager, - &key, - &serde_json::to_vec(&*value).unwrap(), - ) - .await - .expect("Failed to write to Redis"); let exists: bool = conn_manager .exists(&key) diff --git a/rust/serving/src/app/direct_proxy.rs b/rust/serving/src/app/direct_proxy.rs index 1f80ff5e7f..9f08321e23 100644 --- a/rust/serving/src/app/direct_proxy.rs +++ b/rust/serving/src/app/direct_proxy.rs @@ -11,7 +11,6 @@ use hyper_util::client::legacy::connect::HttpConnector; use tracing::error; use crate::app::response::ApiError; -use crate::config; pub(crate) type Client = hyper_util::client::legacy::Client; @@ -24,11 +23,15 @@ pub(crate) type Client = hyper_util::client::legacy::Client #[derive(Clone, Debug)] struct ProxyState { client: Client, + upstream_addr: String, } /// Router for direct proxy. -pub(crate) fn direct_proxy(client: Client) -> Router { - let proxy_state = ProxyState { client }; +pub(crate) fn direct_proxy(client: Client, upstream_addr: String) -> Router { + let proxy_state = ProxyState { + client, + upstream_addr, + }; Router::new() // https://docs.rs/axum/latest/axum/struct.Router.html#wildcards @@ -44,7 +47,7 @@ async fn proxy( // This handler is registered with wildcard capture /*upstream. So the path here will never be empty. let path_query = request.uri().path_and_query().unwrap(); - let upstream_uri = format!("http://{}{}", &config().upstream_addr, path_query); + let upstream_uri = format!("http://{}{}", &proxy_state.upstream_addr, path_query); *request.uri_mut() = Uri::try_from(&upstream_uri) .inspect_err(|e| error!(?e, upstream_uri, "Parsing URI for upstream")) .map_err(|e| ApiError::BadRequest(e.to_string()))?; @@ -69,10 +72,8 @@ mod tests { use tower::ServiceExt; use crate::app::direct_proxy::direct_proxy; - use crate::config; - async fn start_server() { - let addr = config().upstream_addr.to_string(); + async fn start_server(addr: String) { let listener = TcpListener::bind(&addr).await.unwrap(); tokio::spawn(async move { loop { @@ -98,11 +99,12 @@ mod tests { #[tokio::test] async fn test_direct_proxy() { - start_server().await; + let upstream_addr = "localhost:4321".to_owned(); + start_server(upstream_addr.clone()).await; let client = hyper_util::client::legacy::Client::<(), ()>::builder(TokioExecutor::new()) .build(HttpConnector::new()); - let app = direct_proxy(client); + let app = direct_proxy(client, upstream_addr.clone()); // Test valid request let res = Request::builder() diff --git a/rust/serving/src/app/jetstream_proxy.rs b/rust/serving/src/app/jetstream_proxy.rs index 8123197857..af7d3917ff 100644 --- a/rust/serving/src/app/jetstream_proxy.rs +++ b/rust/serving/src/app/jetstream_proxy.rs @@ -1,4 +1,4 @@ -use std::borrow::Borrow; +use std::{borrow::Borrow, sync::Arc}; use async_nats::{jetstream::Context, HeaderMap as JSHeaderMap}; use axum::{ @@ -12,10 +12,9 @@ use axum::{ use tracing::error; use uuid::Uuid; -use super::callback::{state::State as CallbackState, store::Store}; +use super::{callback::store::Store, AppState}; use crate::app::callback::state; use crate::app::response::{ApiError, ServeResponse}; -use crate::config; // TODO: // - [ ] better health check @@ -33,34 +32,31 @@ use crate::config; // "from_vertex": "a" // } -const ID_HEADER_KEY: &str = "X-Numaflow-Id"; const CALLBACK_URL_KEY: &str = "X-Numaflow-Callback-Url"; - const NUMAFLOW_RESP_ARRAY_LEN: &str = "Numaflow-Array-Len"; const NUMAFLOW_RESP_ARRAY_IDX_LEN: &str = "Numaflow-Array-Index-Len"; -#[derive(Clone)] struct ProxyState { + tid_header: String, context: Context, callback: state::State, - stream: &'static str, + stream: String, callback_url: String, } pub(crate) async fn jetstream_proxy( - context: Context, - callback_store: CallbackState, + state: AppState, ) -> crate::Result { - let proxy_state = ProxyState { - context, - callback: callback_store, - stream: &config().jetstream.stream, + let proxy_state = Arc::new(ProxyState { + tid_header: state.settings.tid_header.clone(), + context: state.context.clone(), + callback: state.callback_state.clone(), + stream: state.settings.jetstream.stream.clone(), callback_url: format!( "https://{}:{}/v1/process/callback", - config().host_ip, - config().app_listen_port + state.settings.host_ip, state.settings.app_listen_port ), - }; + }); let router = Router::new() .route("/async", post(async_publish)) @@ -71,27 +67,28 @@ pub(crate) async fn jetstream_proxy( } async fn sync_publish_serve( - State(mut proxy_state): State>, + State(proxy_state): State>>, headers: HeaderMap, body: Bytes, ) -> impl IntoResponse { - let id = extract_id_from_headers(&headers); + let id = extract_id_from_headers(&proxy_state.tid_header, &headers); // Register the ID in the callback proxy state - let notify = proxy_state.callback.register(id.clone()); + let notify = proxy_state.callback.clone().register(id.clone()); if let Err(e) = publish_to_jetstream( - proxy_state.stream, + proxy_state.stream.clone(), &proxy_state.callback_url, headers, body, - proxy_state.context, - id.clone(), + proxy_state.context.clone(), + proxy_state.tid_header.as_str(), + id.as_str(), ) .await { // Deregister the ID in the callback proxy state if writing to Jetstream fails - let _ = proxy_state.callback.deregister(&id).await; + let _ = proxy_state.callback.clone().deregister(&id).await; error!(error = ?e, "Publishing message to Jetstream for sync serve request"); return Err(ApiError::BadGateway( "Failed to write message to Jetstream".to_string(), @@ -106,7 +103,7 @@ async fn sync_publish_serve( )); } - let result = match proxy_state.callback.retrieve_saved(&id).await { + let result = match proxy_state.callback.clone().retrieve_saved(&id).await { Ok(result) => result, Err(e) => { error!(error = ?e, "Failed to retrieve from redis"); @@ -140,27 +137,28 @@ async fn sync_publish_serve( } async fn sync_publish( - State(mut proxy_state): State>, + State(proxy_state): State>>, headers: HeaderMap, body: Bytes, ) -> Result, ApiError> { - let id = extract_id_from_headers(&headers); + let id = extract_id_from_headers(&proxy_state.tid_header, &headers); // Register the ID in the callback proxy state - let notify = proxy_state.callback.register(id.clone()); + let notify = proxy_state.callback.clone().register(id.clone()); if let Err(e) = publish_to_jetstream( - proxy_state.stream, + proxy_state.stream.clone(), &proxy_state.callback_url, headers, body, - proxy_state.context, - id.clone(), + proxy_state.context.clone(), + &proxy_state.tid_header, + id.as_str(), ) .await { // Deregister the ID in the callback proxy state if writing to Jetstream fails - let _ = proxy_state.callback.deregister(&id).await; + let _ = proxy_state.callback.clone().deregister(&id).await; error!(error = ?e, "Publishing message to Jetstream for sync request"); return Err(ApiError::BadGateway( "Failed to write message to Jetstream".to_string(), @@ -189,19 +187,19 @@ async fn sync_publish( } async fn async_publish( - State(proxy_state): State>, + State(proxy_state): State>>, headers: HeaderMap, body: Bytes, ) -> Result, ApiError> { - let id = extract_id_from_headers(&headers); - + let id = extract_id_from_headers(&proxy_state.tid_header, &headers); let result = publish_to_jetstream( - proxy_state.stream, + proxy_state.stream.clone(), &proxy_state.callback_url, headers, body, - proxy_state.context, - id.clone(), + proxy_state.context.clone(), + &proxy_state.tid_header, + id.as_str(), ) .await; @@ -222,12 +220,13 @@ async fn async_publish( /// Write to JetStream and return the metadata. It is responsible for getting the ID from the header. async fn publish_to_jetstream( - stream: &'static str, + stream: String, callback_url: &str, headers: HeaderMap, body: Bytes, js_context: Context, - id: String, // Added ID as a parameter + id_header: &str, + id_header_value: &str, ) -> Result<(), async_nats::Error> { let mut js_headers = JSHeaderMap::new(); @@ -236,24 +235,22 @@ async fn publish_to_jetstream( js_headers.append(k.as_ref(), String::from_utf8_lossy(v.as_bytes()).borrow()) } - js_headers.append(ID_HEADER_KEY, id.as_str()); // Use the passed ID + js_headers.append(id_header, id_header_value); // Use the passed ID js_headers.append(CALLBACK_URL_KEY, callback_url); js_context .publish_with_headers(stream, js_headers, body) .await - .inspect_err(|e| error!(stream, error=?e, "Publishing message to stream"))? + .map_err(|e| format!("Publishing message to stream: {e:?}"))? .await - .inspect_err( - |e| error!(stream, error=?e, "Waiting for acknowledgement of published message"), - )?; + .map_err(|e| format!("Waiting for acknowledgement of published message: {e:?}"))?; Ok(()) } // extracts the ID from the headers, if not found, generates a new UUID -fn extract_id_from_headers(headers: &HeaderMap) -> String { - headers.get(&config().tid_header).map_or_else( +fn extract_id_from_headers(tid_header: &str, headers: &HeaderMap) -> String { + headers.get(tid_header).map_or_else( || Uuid::new_v4().to_string(), |v| String::from_utf8_lossy(v.as_bytes()).to_string(), ) @@ -273,12 +270,15 @@ mod tests { use tower::ServiceExt; use super::*; + use crate::app::callback::state::State as CallbackState; use crate::app::callback::store::memstore::InMemoryStore; use crate::app::callback::store::PayloadToSave; use crate::app::callback::CallbackRequest; use crate::app::tracker::MessageGraph; - use crate::pipeline::min_pipeline_spec; - use crate::Error; + use crate::pipeline::PipelineDCG; + use crate::{Error, Settings}; + + const PIPELINE_SPEC_ENCODED: &str = "eyJ2ZXJ0aWNlcyI6W3sibmFtZSI6ImluIiwic291cmNlIjp7InNlcnZpbmciOnsiYXV0aCI6bnVsbCwic2VydmljZSI6dHJ1ZSwibXNnSURIZWFkZXJLZXkiOiJYLU51bWFmbG93LUlkIiwic3RvcmUiOnsidXJsIjoicmVkaXM6Ly9yZWRpczo2Mzc5In19fSwiY29udGFpbmVyVGVtcGxhdGUiOnsicmVzb3VyY2VzIjp7fSwiaW1hZ2VQdWxsUG9saWN5IjoiTmV2ZXIiLCJlbnYiOlt7Im5hbWUiOiJSVVNUX0xPRyIsInZhbHVlIjoiZGVidWcifV19LCJzY2FsZSI6eyJtaW4iOjF9LCJ1cGRhdGVTdHJhdGVneSI6eyJ0eXBlIjoiUm9sbGluZ1VwZGF0ZSIsInJvbGxpbmdVcGRhdGUiOnsibWF4VW5hdmFpbGFibGUiOiIyNSUifX19LHsibmFtZSI6InBsYW5uZXIiLCJ1ZGYiOnsiY29udGFpbmVyIjp7ImltYWdlIjoiYXNjaWk6MC4xIiwiYXJncyI6WyJwbGFubmVyIl0sInJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sImJ1aWx0aW4iOm51bGwsImdyb3VwQnkiOm51bGx9LCJjb250YWluZXJUZW1wbGF0ZSI6eyJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9LCJzY2FsZSI6eyJtaW4iOjF9LCJ1cGRhdGVTdHJhdGVneSI6eyJ0eXBlIjoiUm9sbGluZ1VwZGF0ZSIsInJvbGxpbmdVcGRhdGUiOnsibWF4VW5hdmFpbGFibGUiOiIyNSUifX19LHsibmFtZSI6InRpZ2VyIiwidWRmIjp7ImNvbnRhaW5lciI6eyJpbWFnZSI6ImFzY2lpOjAuMSIsImFyZ3MiOlsidGlnZXIiXSwicmVzb3VyY2VzIjp7fSwiaW1hZ2VQdWxsUG9saWN5IjoiTmV2ZXIifSwiYnVpbHRpbiI6bnVsbCwiZ3JvdXBCeSI6bnVsbH0sImNvbnRhaW5lclRlbXBsYXRlIjp7InJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sInNjYWxlIjp7Im1pbiI6MX0sInVwZGF0ZVN0cmF0ZWd5Ijp7InR5cGUiOiJSb2xsaW5nVXBkYXRlIiwicm9sbGluZ1VwZGF0ZSI6eyJtYXhVbmF2YWlsYWJsZSI6IjI1JSJ9fX0seyJuYW1lIjoiZG9nIiwidWRmIjp7ImNvbnRhaW5lciI6eyJpbWFnZSI6ImFzY2lpOjAuMSIsImFyZ3MiOlsiZG9nIl0sInJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sImJ1aWx0aW4iOm51bGwsImdyb3VwQnkiOm51bGx9LCJjb250YWluZXJUZW1wbGF0ZSI6eyJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9LCJzY2FsZSI6eyJtaW4iOjF9LCJ1cGRhdGVTdHJhdGVneSI6eyJ0eXBlIjoiUm9sbGluZ1VwZGF0ZSIsInJvbGxpbmdVcGRhdGUiOnsibWF4VW5hdmFpbGFibGUiOiIyNSUifX19LHsibmFtZSI6ImVsZXBoYW50IiwidWRmIjp7ImNvbnRhaW5lciI6eyJpbWFnZSI6ImFzY2lpOjAuMSIsImFyZ3MiOlsiZWxlcGhhbnQiXSwicmVzb3VyY2VzIjp7fSwiaW1hZ2VQdWxsUG9saWN5IjoiTmV2ZXIifSwiYnVpbHRpbiI6bnVsbCwiZ3JvdXBCeSI6bnVsbH0sImNvbnRhaW5lclRlbXBsYXRlIjp7InJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sInNjYWxlIjp7Im1pbiI6MX0sInVwZGF0ZVN0cmF0ZWd5Ijp7InR5cGUiOiJSb2xsaW5nVXBkYXRlIiwicm9sbGluZ1VwZGF0ZSI6eyJtYXhVbmF2YWlsYWJsZSI6IjI1JSJ9fX0seyJuYW1lIjoiYXNjaWlhcnQiLCJ1ZGYiOnsiY29udGFpbmVyIjp7ImltYWdlIjoiYXNjaWk6MC4xIiwiYXJncyI6WyJhc2NpaWFydCJdLCJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9LCJidWlsdGluIjpudWxsLCJncm91cEJ5IjpudWxsfSwiY29udGFpbmVyVGVtcGxhdGUiOnsicmVzb3VyY2VzIjp7fSwiaW1hZ2VQdWxsUG9saWN5IjoiTmV2ZXIifSwic2NhbGUiOnsibWluIjoxfSwidXBkYXRlU3RyYXRlZ3kiOnsidHlwZSI6IlJvbGxpbmdVcGRhdGUiLCJyb2xsaW5nVXBkYXRlIjp7Im1heFVuYXZhaWxhYmxlIjoiMjUlIn19fSx7Im5hbWUiOiJzZXJ2ZS1zaW5rIiwic2luayI6eyJ1ZHNpbmsiOnsiY29udGFpbmVyIjp7ImltYWdlIjoic2VydmVzaW5rOjAuMSIsImVudiI6W3sibmFtZSI6Ik5VTUFGTE9XX0NBTExCQUNLX1VSTF9LRVkiLCJ2YWx1ZSI6IlgtTnVtYWZsb3ctQ2FsbGJhY2stVXJsIn0seyJuYW1lIjoiTlVNQUZMT1dfTVNHX0lEX0hFQURFUl9LRVkiLCJ2YWx1ZSI6IlgtTnVtYWZsb3ctSWQifV0sInJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn19LCJyZXRyeVN0cmF0ZWd5Ijp7fX0sImNvbnRhaW5lclRlbXBsYXRlIjp7InJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sInNjYWxlIjp7Im1pbiI6MX0sInVwZGF0ZVN0cmF0ZWd5Ijp7InR5cGUiOiJSb2xsaW5nVXBkYXRlIiwicm9sbGluZ1VwZGF0ZSI6eyJtYXhVbmF2YWlsYWJsZSI6IjI1JSJ9fX0seyJuYW1lIjoiZXJyb3Itc2luayIsInNpbmsiOnsidWRzaW5rIjp7ImNvbnRhaW5lciI6eyJpbWFnZSI6InNlcnZlc2luazowLjEiLCJlbnYiOlt7Im5hbWUiOiJOVU1BRkxPV19DQUxMQkFDS19VUkxfS0VZIiwidmFsdWUiOiJYLU51bWFmbG93LUNhbGxiYWNrLVVybCJ9LHsibmFtZSI6Ik5VTUFGTE9XX01TR19JRF9IRUFERVJfS0VZIiwidmFsdWUiOiJYLU51bWFmbG93LUlkIn1dLCJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9fSwicmV0cnlTdHJhdGVneSI6e319LCJjb250YWluZXJUZW1wbGF0ZSI6eyJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9LCJzY2FsZSI6eyJtaW4iOjF9LCJ1cGRhdGVTdHJhdGVneSI6eyJ0eXBlIjoiUm9sbGluZ1VwZGF0ZSIsInJvbGxpbmdVcGRhdGUiOnsibWF4VW5hdmFpbGFibGUiOiIyNSUifX19XSwiZWRnZXMiOlt7ImZyb20iOiJpbiIsInRvIjoicGxhbm5lciIsImNvbmRpdGlvbnMiOm51bGx9LHsiZnJvbSI6InBsYW5uZXIiLCJ0byI6ImFzY2lpYXJ0IiwiY29uZGl0aW9ucyI6eyJ0YWdzIjp7Im9wZXJhdG9yIjoib3IiLCJ2YWx1ZXMiOlsiYXNjaWlhcnQiXX19fSx7ImZyb20iOiJwbGFubmVyIiwidG8iOiJ0aWdlciIsImNvbmRpdGlvbnMiOnsidGFncyI6eyJvcGVyYXRvciI6Im9yIiwidmFsdWVzIjpbInRpZ2VyIl19fX0seyJmcm9tIjoicGxhbm5lciIsInRvIjoiZG9nIiwiY29uZGl0aW9ucyI6eyJ0YWdzIjp7Im9wZXJhdG9yIjoib3IiLCJ2YWx1ZXMiOlsiZG9nIl19fX0seyJmcm9tIjoicGxhbm5lciIsInRvIjoiZWxlcGhhbnQiLCJjb25kaXRpb25zIjp7InRhZ3MiOnsib3BlcmF0b3IiOiJvciIsInZhbHVlcyI6WyJlbGVwaGFudCJdfX19LHsiZnJvbSI6InRpZ2VyIiwidG8iOiJzZXJ2ZS1zaW5rIiwiY29uZGl0aW9ucyI6bnVsbH0seyJmcm9tIjoiZG9nIiwidG8iOiJzZXJ2ZS1zaW5rIiwiY29uZGl0aW9ucyI6bnVsbH0seyJmcm9tIjoiZWxlcGhhbnQiLCJ0byI6InNlcnZlLXNpbmsiLCJjb25kaXRpb25zIjpudWxsfSx7ImZyb20iOiJhc2NpaWFydCIsInRvIjoic2VydmUtc2luayIsImNvbmRpdGlvbnMiOm51bGx9LHsiZnJvbSI6InBsYW5uZXIiLCJ0byI6ImVycm9yLXNpbmsiLCJjb25kaXRpb25zIjp7InRhZ3MiOnsib3BlcmF0b3IiOiJvciIsInZhbHVlcyI6WyJlcnJvciJdfX19XSwibGlmZWN5Y2xlIjp7fSwid2F0ZXJtYXJrIjp7fX0="; #[derive(Clone)] struct MockStore; @@ -303,7 +303,9 @@ mod tests { #[tokio::test] async fn test_async_publish() -> Result<(), Box> { - let client = async_nats::connect(&config().jetstream.url) + let settings = Settings::default(); + let settings = Arc::new(settings); + let client = async_nats::connect(&settings.jetstream.url) .await .map_err(|e| format!("Connecting to Jetstream: {:?}", e))?; @@ -318,14 +320,20 @@ mod tests { ..Default::default() }) .await - .map_err(|e| format!("creating stream {}: {}", &config().jetstream.url, e))?; + .map_err(|e| format!("creating stream {}: {}", &settings.jetstream.url, e))?; let mock_store = MockStore {}; - let msg_graph = MessageGraph::from_pipeline(min_pipeline_spec()) + let pipeline_spec: PipelineDCG = PIPELINE_SPEC_ENCODED.parse().unwrap(); + let msg_graph = MessageGraph::from_pipeline(&pipeline_spec) .map_err(|e| format!("Failed to create message graph from pipeline spec: {:?}", e))?; let callback_state = CallbackState::new(msg_graph, mock_store).await?; - let app = jetstream_proxy(context, callback_state).await?; + let app_state = AppState { + callback_state, + context, + settings, + }; + let app = jetstream_proxy(app_state).await?; let res = Request::builder() .method("POST") .uri("/async") @@ -384,7 +392,8 @@ mod tests { #[tokio::test] async fn test_sync_publish() { - let client = async_nats::connect(&config().jetstream.url).await.unwrap(); + let settings = Settings::default(); + let client = async_nats::connect(&settings.jetstream.url).await.unwrap(); let context = jetstream::new(client); let id = "foobar"; let stream_name = "sync_pub"; @@ -396,16 +405,21 @@ mod tests { ..Default::default() }) .await - .map_err(|e| format!("creating stream {}: {}", &config().jetstream.url, e)); + .map_err(|e| format!("creating stream {}: {}", &settings.jetstream.url, e)); let mem_store = InMemoryStore::new(); - let msg_graph = MessageGraph::from_pipeline(min_pipeline_spec()).unwrap(); + let pipeline_spec: PipelineDCG = PIPELINE_SPEC_ENCODED.parse().unwrap(); + let msg_graph = MessageGraph::from_pipeline(&pipeline_spec).unwrap(); let mut callback_state = CallbackState::new(msg_graph, mem_store).await.unwrap(); - let app = jetstream_proxy(context, callback_state.clone()) - .await - .unwrap(); + let settings = Arc::new(settings); + let app_state = AppState { + settings, + callback_state: callback_state.clone(), + context, + }; + let app = jetstream_proxy(app_state).await.unwrap(); tokio::spawn(async move { let cbs = create_default_callbacks(id); @@ -448,7 +462,8 @@ mod tests { #[tokio::test] async fn test_sync_publish_serve() { - let client = async_nats::connect(&config().jetstream.url).await.unwrap(); + let settings = Arc::new(Settings::default()); + let client = async_nats::connect(&settings.jetstream.url).await.unwrap(); let context = jetstream::new(client); let id = "foobar"; let stream_name = "sync_serve_pub"; @@ -460,16 +475,21 @@ mod tests { ..Default::default() }) .await - .map_err(|e| format!("creating stream {}: {}", &config().jetstream.url, e)); + .map_err(|e| format!("creating stream {}: {}", &settings.jetstream.url, e)); let mem_store = InMemoryStore::new(); - let msg_graph = MessageGraph::from_pipeline(min_pipeline_spec()).unwrap(); + let pipeline_spec: PipelineDCG = PIPELINE_SPEC_ENCODED.parse().unwrap(); + let msg_graph = MessageGraph::from_pipeline(&pipeline_spec).unwrap(); let mut callback_state = CallbackState::new(msg_graph, mem_store).await.unwrap(); - let app = jetstream_proxy(context, callback_state.clone()) - .await - .unwrap(); + let app_state = AppState { + settings, + callback_state: callback_state.clone(), + context, + }; + + let app = jetstream_proxy(app_state).await.unwrap(); // pipeline is in -> cat -> out, so we will have 3 callback requests let cbs = create_default_callbacks(id); diff --git a/rust/serving/src/app/message_path.rs b/rust/serving/src/app/message_path.rs index 933c58a815..20e5701864 100644 --- a/rust/serving/src/app/message_path.rs +++ b/rust/serving/src/app/message_path.rs @@ -46,12 +46,15 @@ mod tests { use super::*; use crate::app::callback::store::memstore::InMemoryStore; use crate::app::tracker::MessageGraph; - use crate::pipeline::min_pipeline_spec; + use crate::pipeline::PipelineDCG; + + const PIPELINE_SPEC_ENCODED: &str = "eyJ2ZXJ0aWNlcyI6W3sibmFtZSI6ImluIiwic291cmNlIjp7InNlcnZpbmciOnsiYXV0aCI6bnVsbCwic2VydmljZSI6dHJ1ZSwibXNnSURIZWFkZXJLZXkiOiJYLU51bWFmbG93LUlkIiwic3RvcmUiOnsidXJsIjoicmVkaXM6Ly9yZWRpczo2Mzc5In19fSwiY29udGFpbmVyVGVtcGxhdGUiOnsicmVzb3VyY2VzIjp7fSwiaW1hZ2VQdWxsUG9saWN5IjoiTmV2ZXIiLCJlbnYiOlt7Im5hbWUiOiJSVVNUX0xPRyIsInZhbHVlIjoiZGVidWcifV19LCJzY2FsZSI6eyJtaW4iOjF9LCJ1cGRhdGVTdHJhdGVneSI6eyJ0eXBlIjoiUm9sbGluZ1VwZGF0ZSIsInJvbGxpbmdVcGRhdGUiOnsibWF4VW5hdmFpbGFibGUiOiIyNSUifX19LHsibmFtZSI6InBsYW5uZXIiLCJ1ZGYiOnsiY29udGFpbmVyIjp7ImltYWdlIjoiYXNjaWk6MC4xIiwiYXJncyI6WyJwbGFubmVyIl0sInJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sImJ1aWx0aW4iOm51bGwsImdyb3VwQnkiOm51bGx9LCJjb250YWluZXJUZW1wbGF0ZSI6eyJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9LCJzY2FsZSI6eyJtaW4iOjF9LCJ1cGRhdGVTdHJhdGVneSI6eyJ0eXBlIjoiUm9sbGluZ1VwZGF0ZSIsInJvbGxpbmdVcGRhdGUiOnsibWF4VW5hdmFpbGFibGUiOiIyNSUifX19LHsibmFtZSI6InRpZ2VyIiwidWRmIjp7ImNvbnRhaW5lciI6eyJpbWFnZSI6ImFzY2lpOjAuMSIsImFyZ3MiOlsidGlnZXIiXSwicmVzb3VyY2VzIjp7fSwiaW1hZ2VQdWxsUG9saWN5IjoiTmV2ZXIifSwiYnVpbHRpbiI6bnVsbCwiZ3JvdXBCeSI6bnVsbH0sImNvbnRhaW5lclRlbXBsYXRlIjp7InJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sInNjYWxlIjp7Im1pbiI6MX0sInVwZGF0ZVN0cmF0ZWd5Ijp7InR5cGUiOiJSb2xsaW5nVXBkYXRlIiwicm9sbGluZ1VwZGF0ZSI6eyJtYXhVbmF2YWlsYWJsZSI6IjI1JSJ9fX0seyJuYW1lIjoiZG9nIiwidWRmIjp7ImNvbnRhaW5lciI6eyJpbWFnZSI6ImFzY2lpOjAuMSIsImFyZ3MiOlsiZG9nIl0sInJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sImJ1aWx0aW4iOm51bGwsImdyb3VwQnkiOm51bGx9LCJjb250YWluZXJUZW1wbGF0ZSI6eyJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9LCJzY2FsZSI6eyJtaW4iOjF9LCJ1cGRhdGVTdHJhdGVneSI6eyJ0eXBlIjoiUm9sbGluZ1VwZGF0ZSIsInJvbGxpbmdVcGRhdGUiOnsibWF4VW5hdmFpbGFibGUiOiIyNSUifX19LHsibmFtZSI6ImVsZXBoYW50IiwidWRmIjp7ImNvbnRhaW5lciI6eyJpbWFnZSI6ImFzY2lpOjAuMSIsImFyZ3MiOlsiZWxlcGhhbnQiXSwicmVzb3VyY2VzIjp7fSwiaW1hZ2VQdWxsUG9saWN5IjoiTmV2ZXIifSwiYnVpbHRpbiI6bnVsbCwiZ3JvdXBCeSI6bnVsbH0sImNvbnRhaW5lclRlbXBsYXRlIjp7InJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sInNjYWxlIjp7Im1pbiI6MX0sInVwZGF0ZVN0cmF0ZWd5Ijp7InR5cGUiOiJSb2xsaW5nVXBkYXRlIiwicm9sbGluZ1VwZGF0ZSI6eyJtYXhVbmF2YWlsYWJsZSI6IjI1JSJ9fX0seyJuYW1lIjoiYXNjaWlhcnQiLCJ1ZGYiOnsiY29udGFpbmVyIjp7ImltYWdlIjoiYXNjaWk6MC4xIiwiYXJncyI6WyJhc2NpaWFydCJdLCJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9LCJidWlsdGluIjpudWxsLCJncm91cEJ5IjpudWxsfSwiY29udGFpbmVyVGVtcGxhdGUiOnsicmVzb3VyY2VzIjp7fSwiaW1hZ2VQdWxsUG9saWN5IjoiTmV2ZXIifSwic2NhbGUiOnsibWluIjoxfSwidXBkYXRlU3RyYXRlZ3kiOnsidHlwZSI6IlJvbGxpbmdVcGRhdGUiLCJyb2xsaW5nVXBkYXRlIjp7Im1heFVuYXZhaWxhYmxlIjoiMjUlIn19fSx7Im5hbWUiOiJzZXJ2ZS1zaW5rIiwic2luayI6eyJ1ZHNpbmsiOnsiY29udGFpbmVyIjp7ImltYWdlIjoic2VydmVzaW5rOjAuMSIsImVudiI6W3sibmFtZSI6Ik5VTUFGTE9XX0NBTExCQUNLX1VSTF9LRVkiLCJ2YWx1ZSI6IlgtTnVtYWZsb3ctQ2FsbGJhY2stVXJsIn0seyJuYW1lIjoiTlVNQUZMT1dfTVNHX0lEX0hFQURFUl9LRVkiLCJ2YWx1ZSI6IlgtTnVtYWZsb3ctSWQifV0sInJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn19LCJyZXRyeVN0cmF0ZWd5Ijp7fX0sImNvbnRhaW5lclRlbXBsYXRlIjp7InJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sInNjYWxlIjp7Im1pbiI6MX0sInVwZGF0ZVN0cmF0ZWd5Ijp7InR5cGUiOiJSb2xsaW5nVXBkYXRlIiwicm9sbGluZ1VwZGF0ZSI6eyJtYXhVbmF2YWlsYWJsZSI6IjI1JSJ9fX0seyJuYW1lIjoiZXJyb3Itc2luayIsInNpbmsiOnsidWRzaW5rIjp7ImNvbnRhaW5lciI6eyJpbWFnZSI6InNlcnZlc2luazowLjEiLCJlbnYiOlt7Im5hbWUiOiJOVU1BRkxPV19DQUxMQkFDS19VUkxfS0VZIiwidmFsdWUiOiJYLU51bWFmbG93LUNhbGxiYWNrLVVybCJ9LHsibmFtZSI6Ik5VTUFGTE9XX01TR19JRF9IRUFERVJfS0VZIiwidmFsdWUiOiJYLU51bWFmbG93LUlkIn1dLCJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9fSwicmV0cnlTdHJhdGVneSI6e319LCJjb250YWluZXJUZW1wbGF0ZSI6eyJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9LCJzY2FsZSI6eyJtaW4iOjF9LCJ1cGRhdGVTdHJhdGVneSI6eyJ0eXBlIjoiUm9sbGluZ1VwZGF0ZSIsInJvbGxpbmdVcGRhdGUiOnsibWF4VW5hdmFpbGFibGUiOiIyNSUifX19XSwiZWRnZXMiOlt7ImZyb20iOiJpbiIsInRvIjoicGxhbm5lciIsImNvbmRpdGlvbnMiOm51bGx9LHsiZnJvbSI6InBsYW5uZXIiLCJ0byI6ImFzY2lpYXJ0IiwiY29uZGl0aW9ucyI6eyJ0YWdzIjp7Im9wZXJhdG9yIjoib3IiLCJ2YWx1ZXMiOlsiYXNjaWlhcnQiXX19fSx7ImZyb20iOiJwbGFubmVyIiwidG8iOiJ0aWdlciIsImNvbmRpdGlvbnMiOnsidGFncyI6eyJvcGVyYXRvciI6Im9yIiwidmFsdWVzIjpbInRpZ2VyIl19fX0seyJmcm9tIjoicGxhbm5lciIsInRvIjoiZG9nIiwiY29uZGl0aW9ucyI6eyJ0YWdzIjp7Im9wZXJhdG9yIjoib3IiLCJ2YWx1ZXMiOlsiZG9nIl19fX0seyJmcm9tIjoicGxhbm5lciIsInRvIjoiZWxlcGhhbnQiLCJjb25kaXRpb25zIjp7InRhZ3MiOnsib3BlcmF0b3IiOiJvciIsInZhbHVlcyI6WyJlbGVwaGFudCJdfX19LHsiZnJvbSI6InRpZ2VyIiwidG8iOiJzZXJ2ZS1zaW5rIiwiY29uZGl0aW9ucyI6bnVsbH0seyJmcm9tIjoiZG9nIiwidG8iOiJzZXJ2ZS1zaW5rIiwiY29uZGl0aW9ucyI6bnVsbH0seyJmcm9tIjoiZWxlcGhhbnQiLCJ0byI6InNlcnZlLXNpbmsiLCJjb25kaXRpb25zIjpudWxsfSx7ImZyb20iOiJhc2NpaWFydCIsInRvIjoic2VydmUtc2luayIsImNvbmRpdGlvbnMiOm51bGx9LHsiZnJvbSI6InBsYW5uZXIiLCJ0byI6ImVycm9yLXNpbmsiLCJjb25kaXRpb25zIjp7InRhZ3MiOnsib3BlcmF0b3IiOiJvciIsInZhbHVlcyI6WyJlcnJvciJdfX19XSwibGlmZWN5Y2xlIjp7fSwid2F0ZXJtYXJrIjp7fX0="; #[tokio::test] async fn test_message_path_not_present() { let store = InMemoryStore::new(); - let msg_graph = MessageGraph::from_pipeline(min_pipeline_spec()).unwrap(); + let pipeline_spec: PipelineDCG = PIPELINE_SPEC_ENCODED.parse().unwrap(); + let msg_graph = MessageGraph::from_pipeline(&pipeline_spec).unwrap(); let state = CallbackState::new(msg_graph, store).await.unwrap(); let app = get_message_path(state); diff --git a/rust/serving/src/config.rs b/rust/serving/src/config.rs index 82e663c8f5..7ba3778d00 100644 --- a/rust/serving/src/config.rs +++ b/rust/serving/src/config.rs @@ -1,40 +1,23 @@ +use std::collections::HashMap; use std::fmt::Debug; -use std::path::Path; -use std::{env, sync::OnceLock}; use async_nats::rustls; use base64::prelude::BASE64_STANDARD; use base64::Engine; -use config::Config; use rcgen::{generate_simple_self_signed, Certificate, CertifiedKey, KeyPair}; use serde::{Deserialize, Serialize}; -use tracing::info; use crate::Error::ParseConfig; -use crate::{Error, Result}; -const ENV_PREFIX: &str = "NUMAFLOW_SERVING"; const ENV_NUMAFLOW_SERVING_SOURCE_OBJECT: &str = "NUMAFLOW_SERVING_SOURCE_OBJECT"; const ENV_NUMAFLOW_SERVING_JETSTREAM_URL: &str = "NUMAFLOW_ISBSVC_JETSTREAM_URL"; const ENV_NUMAFLOW_SERVING_JETSTREAM_STREAM: &str = "NUMAFLOW_SERVING_JETSTREAM_STREAM"; const ENV_NUMAFLOW_SERVING_STORE_TTL: &str = "NUMAFLOW_SERVING_STORE_TTL"; - -pub fn config() -> &'static Settings { - static CONF: OnceLock = OnceLock::new(); - CONF.get_or_init(|| { - let config_dir = env::var("CONFIG_PATH").unwrap_or_else(|_| { - info!("Config directory is not specified, using default config directory: './config'"); - String::from("config") - }); - - match Settings::load(config_dir) { - Ok(v) => v, - Err(e) => { - panic!("Failed to load configuration: {:?}", e); - } - } - }) -} +const ENV_NUMAFLOW_SERVING_HOST_IP: &str = "NUMAFLOW_SERVING_HOST_IP"; +const ENV_NUMAFLOW_SERVING_APP_PORT: &str = "NUMAFLOW_SERVING_APP_LISTEN_PORT"; +const ENV_NUMAFLOW_SERVING_JETSTREAM_USER: &str = "NUMAFLOW_ISBSVC_JETSTREAM_USER"; +const ENV_NUMAFLOW_SERVING_JETSTREAM_PASSWORD: &str = "NUMAFLOW_ISBSVC_JETSTREAM_PASSWORD"; +const ENV_NUMAFLOW_SERVING_AUTH_TOKEN: &str = "NUMAFLOW_SERVING_AUTH_TOKEN"; pub fn generate_certs() -> std::result::Result<(Certificate, KeyPair), String> { let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); @@ -43,13 +26,47 @@ pub fn generate_certs() -> std::result::Result<(Certificate, KeyPair), String> { Ok((cert, key_pair)) } -#[derive(Debug, Deserialize)] +#[derive(Deserialize, Clone, PartialEq)] +pub struct BasicAuth { + pub username: String, + pub password: String, +} + +impl Debug for BasicAuth { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let passwd_printable = if self.password.len() > 4 { + let passwd: String = self + .password + .chars() + .skip(self.password.len() - 2) + .take(2) + .collect(); + format!("***{}", passwd) + } else { + "*****".to_owned() + }; + write!(f, "{}:{}", self.username, passwd_printable) + } +} + +#[derive(Debug, Deserialize, Clone, PartialEq)] pub struct JetStreamConfig { pub stream: String, pub url: String, + pub auth: Option, +} + +impl Default for JetStreamConfig { + fn default() -> Self { + Self { + stream: "default".to_owned(), + url: "localhost:4222".to_owned(), + auth: None, + } + } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone, PartialEq)] pub struct RedisConfig { pub addr: String, pub max_tasks: usize, @@ -58,7 +75,20 @@ pub struct RedisConfig { pub ttl_secs: Option, } -#[derive(Debug, Deserialize)] +impl Default for RedisConfig { + fn default() -> Self { + Self { + addr: "redis://127.0.0.1:6379".to_owned(), + max_tasks: 50, + retries: 5, + retries_duration_millis: 100, + // TODO: we might need an option type here. Zero value of u32 can be used instead of None + ttl_secs: Some(1), + } + } +} + +#[derive(Debug, Deserialize, Clone, PartialEq)] pub struct Settings { pub tid_header: String, pub app_listen_port: u16, @@ -69,6 +99,23 @@ pub struct Settings { pub redis: RedisConfig, /// The IP address of the numaserve pod. This will be used to construct the value for X-Numaflow-Callback-Url header pub host_ip: String, + pub api_auth_token: Option, +} + +impl Default for Settings { + fn default() -> Self { + Self { + tid_header: "ID".to_owned(), + app_listen_port: 3000, + metrics_server_listen_port: 3001, + upstream_addr: "localhost:8888".to_owned(), + drain_timeout_secs: 10, + jetstream: JetStreamConfig::default(), + redis: RedisConfig::default(), + host_ip: "127.0.0.1".to_owned(), + api_auth_token: None, + } + } } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -84,92 +131,104 @@ pub struct CallbackStorageConfig { pub url: String, } -impl Settings { - fn load>(config_dir: P) -> Result { - let config_dir = config_dir.as_ref(); - if !config_dir.is_dir() { - return Err(Error::Other(format!( - "Path {} is not a directory", - config_dir.to_string_lossy() - ))); +/// This implementation is to load settings from env variables +impl TryFrom> for Settings { + type Error = crate::Error; + fn try_from(env_vars: HashMap) -> std::result::Result { + let host_ip = env_vars + .get(ENV_NUMAFLOW_SERVING_HOST_IP) + .ok_or_else(|| { + ParseConfig(format!( + "Environment variable {ENV_NUMAFLOW_SERVING_HOST_IP} is not set" + )) + })? + .to_owned(); + + let mut settings = Settings { + host_ip, + ..Default::default() + }; + + if let Some(jetstream_url) = env_vars.get(ENV_NUMAFLOW_SERVING_JETSTREAM_URL) { + settings.jetstream.url = jetstream_url.to_owned(); } - let settings = Config::builder() - .add_source(config::File::from(config_dir.join("default.toml"))) - .add_source( - config::Environment::with_prefix(ENV_PREFIX) - .prefix_separator("_") - .separator("."), - ) - .build() - .map_err(|e| ParseConfig(format!("generating runtime configuration: {e:?}")))?; - - let mut settings = settings - .try_deserialize::() - .map_err(|e| ParseConfig(format!("parsing runtime configuration: {e:?}")))?; - - // Update JetStreamConfig from environment variables - if let Ok(url) = env::var(ENV_NUMAFLOW_SERVING_JETSTREAM_URL) { - settings.jetstream.url = url; + if let Some(jetstream_stream) = env_vars.get(ENV_NUMAFLOW_SERVING_JETSTREAM_STREAM) { + settings.jetstream.stream = jetstream_stream.to_owned(); } - if let Ok(stream) = env::var(ENV_NUMAFLOW_SERVING_JETSTREAM_STREAM) { - settings.jetstream.stream = stream; + + if let Some(api_auth_token) = env_vars.get(ENV_NUMAFLOW_SERVING_AUTH_TOKEN) { + settings.api_auth_token = Some(api_auth_token.to_owned()); } - let source_spec_encoded = env::var(ENV_NUMAFLOW_SERVING_SOURCE_OBJECT); - - match source_spec_encoded { - Ok(source_spec_encoded) => { - let source_spec_decoded = BASE64_STANDARD - .decode(source_spec_encoded.as_bytes()) - .map_err(|e| ParseConfig(format!("decoding NUMAFLOW_SERVING_SOURCE: {e:?}")))?; - - let source_spec = serde_json::from_slice::(&source_spec_decoded) - .map_err(|e| ParseConfig(format!("parsing NUMAFLOW_SERVING_SOURCE: {e:?}")))?; - - // Update tid_header from source_spec - if let Some(msg_id_header_key) = source_spec.msg_id_header_key { - settings.tid_header = msg_id_header_key; - } - - // Update redis.addr from source_spec, currently we only support redis as callback storage - settings.redis.addr = source_spec.callback_storage.url; - - // Update redis.ttl_secs from environment variable - settings.redis.ttl_secs = match env::var(ENV_NUMAFLOW_SERVING_STORE_TTL) { - Ok(ttl_secs) => Some(ttl_secs.parse().map_err(|e| { - ParseConfig(format!( - "parsing NUMAFLOW_SERVING_STORE_TTL: expected u32, got {:?}", - e - )) - })?), - Err(_) => None, - }; - - Ok(settings) - } - Err(_) => Ok(settings), + if let Some(app_port) = env_vars.get(ENV_NUMAFLOW_SERVING_APP_PORT) { + settings.app_listen_port = app_port.parse().map_err(|e| { + ParseConfig(format!( + "Parsing {ENV_NUMAFLOW_SERVING_APP_PORT}(set to '{app_port}'): {e:?}" + )) + })?; } + + // If username is set, the password also must be set + if let Some(username) = env_vars.get(ENV_NUMAFLOW_SERVING_JETSTREAM_USER) { + let Some(password) = env_vars.get(ENV_NUMAFLOW_SERVING_JETSTREAM_PASSWORD) else { + return Err(ParseConfig(format!("Env variable {ENV_NUMAFLOW_SERVING_JETSTREAM_USER} is set, but {ENV_NUMAFLOW_SERVING_JETSTREAM_PASSWORD} is not set"))); + }; + settings.jetstream.auth = Some(BasicAuth { + username: username.to_owned(), + password: password.to_owned(), + }); + } + + // Update redis.ttl_secs from environment variable + if let Some(ttl_secs) = env_vars.get(ENV_NUMAFLOW_SERVING_STORE_TTL) { + let ttl_secs: u32 = ttl_secs.parse().map_err(|e| { + ParseConfig(format!("parsing {ENV_NUMAFLOW_SERVING_STORE_TTL}: {e:?}")) + })?; + settings.redis.ttl_secs = Some(ttl_secs); + } + + let Some(source_spec_encoded) = env_vars.get(ENV_NUMAFLOW_SERVING_SOURCE_OBJECT) else { + return Ok(settings); + }; + + let source_spec_decoded = BASE64_STANDARD + .decode(source_spec_encoded.as_bytes()) + .map_err(|e| ParseConfig(format!("decoding NUMAFLOW_SERVING_SOURCE: {e:?}")))?; + + let source_spec = serde_json::from_slice::(&source_spec_decoded) + .map_err(|e| ParseConfig(format!("parsing NUMAFLOW_SERVING_SOURCE: {e:?}")))?; + + // Update tid_header from source_spec + if let Some(msg_id_header_key) = source_spec.msg_id_header_key { + settings.tid_header = msg_id_header_key; + } + + // Update redis.addr from source_spec, currently we only support redis as callback storage + settings.redis.addr = source_spec.callback_storage.url; + + Ok(settings) } } #[cfg(test)] mod tests { - use std::env; - use super::*; #[test] - fn test_config() { - // Set up the environment variable for the config directory - env::set_var("RUN_ENV", "Development"); - env::set_var("APP_HOST_IP", "10.244.0.6"); - env::set_var("CONFIG_PATH", "config"); + fn test_basic_auth_debug_print() { + let auth = BasicAuth { + username: "js-auth-user".into(), + password: "js-auth-password".into(), + }; + let auth_debug = format!("{auth:?}"); + assert_eq!(auth_debug, "js-auth-user:***rd"); + } - // Call the config method - let settings = config(); + #[test] + fn test_default_config() { + let settings = Settings::default(); - // Assert that the settings are as expected assert_eq!(settings.tid_header, "ID"); assert_eq!(settings.app_listen_port, 3000); assert_eq!(settings.metrics_server_listen_port, 3001); @@ -177,9 +236,66 @@ mod tests { assert_eq!(settings.drain_timeout_secs, 10); assert_eq!(settings.jetstream.stream, "default"); assert_eq!(settings.jetstream.url, "localhost:4222"); - assert_eq!(settings.redis.addr, "redis://127.0.0.1/"); + assert_eq!(settings.redis.addr, "redis://127.0.0.1:6379"); assert_eq!(settings.redis.max_tasks, 50); assert_eq!(settings.redis.retries, 5); assert_eq!(settings.redis.retries_duration_millis, 100); } + + #[test] + fn test_config_parse() { + // Set up the environment variables + let env_vars = [ + ( + ENV_NUMAFLOW_SERVING_JETSTREAM_URL, + "nats://isbsvc-default-js-svc.default.svc:4222", + ), + ( + ENV_NUMAFLOW_SERVING_JETSTREAM_STREAM, + "ascii-art-pipeline-in-serving-source", + ), + (ENV_NUMAFLOW_SERVING_JETSTREAM_USER, "js-auth-user"), + (ENV_NUMAFLOW_SERVING_JETSTREAM_PASSWORD, "js-user-password"), + (ENV_NUMAFLOW_SERVING_HOST_IP, "10.2.3.5"), + (ENV_NUMAFLOW_SERVING_AUTH_TOKEN, "api-auth-token"), + (ENV_NUMAFLOW_SERVING_APP_PORT, "8443"), + (ENV_NUMAFLOW_SERVING_STORE_TTL, "86400"), + (ENV_NUMAFLOW_SERVING_SOURCE_OBJECT, "eyJhdXRoIjpudWxsLCJzZXJ2aWNlIjp0cnVlLCJtc2dJREhlYWRlcktleSI6IlgtTnVtYWZsb3ctSWQiLCJzdG9yZSI6eyJ1cmwiOiJyZWRpczovL3JlZGlzOjYzNzkifX0=") + ]; + + // Call the config method + let settings: Settings = env_vars + .into_iter() + .map(|(key, val)| (key.to_owned(), val.to_owned())) + .collect::>() + .try_into() + .unwrap(); + + let expected_config = Settings { + tid_header: "X-Numaflow-Id".into(), + app_listen_port: 8443, + metrics_server_listen_port: 3001, + upstream_addr: "localhost:8888".into(), + drain_timeout_secs: 10, + jetstream: JetStreamConfig { + stream: "ascii-art-pipeline-in-serving-source".into(), + url: "nats://isbsvc-default-js-svc.default.svc:4222".into(), + auth: Some(BasicAuth { + username: "js-auth-user".into(), + password: "js-user-password".into(), + }), + }, + redis: RedisConfig { + addr: "redis://redis:6379".into(), + max_tasks: 50, + retries: 5, + retries_duration_millis: 100, + ttl_secs: Some(86400), + }, + host_ip: "10.2.3.5".into(), + api_auth_token: Some("api-auth-token".into()), + }; + + assert_eq!(settings, expected_config); + } } diff --git a/rust/serving/src/lib.rs b/rust/serving/src/lib.rs index 09e2dfcaa5..796313bdb2 100644 --- a/rust/serving/src/lib.rs +++ b/rust/serving/src/lib.rs @@ -1,42 +1,61 @@ +use std::env; use std::net::SocketAddr; +use std::sync::Arc; use axum_server::tls_rustls::RustlsConfig; use tracing::info; pub use self::error::{Error, Result}; +use self::pipeline::PipelineDCG; use crate::app::start_main_server; -use crate::config::{config, generate_certs}; +use crate::config::generate_certs; use crate::metrics::start_https_metrics_server; -use crate::pipeline::min_pipeline_spec; mod app; + mod config; +pub use config::Settings; + mod consts; mod error; mod metrics; mod pipeline; -pub async fn serve() -> std::result::Result<(), Box> -{ +const ENV_MIN_PIPELINE_SPEC: &str = "NUMAFLOW_SERVING_MIN_PIPELINE_SPEC"; + +pub async fn serve( + settings: Arc, +) -> std::result::Result<(), Box> { let (cert, key) = generate_certs()?; let tls_config = RustlsConfig::from_pem(cert.pem().into(), key.serialize_pem().into()) .await .map_err(|e| format!("Failed to create tls config {:?}", e))?; - info!(config = ?config(), pipeline_spec = ? min_pipeline_spec(), "Starting server with config and pipeline spec"); + // TODO: Move all env variables into one place. Some env variables are loaded when Settings is initialized + let pipeline_spec: PipelineDCG = env::var(ENV_MIN_PIPELINE_SPEC) + .map_err(|_| { + format!("Pipeline spec is not set using environment variable {ENV_MIN_PIPELINE_SPEC}") + })? + .parse() + .map_err(|e| { + format!( + "Parsing pipeline spec: {}: error={e:?}", + env::var(ENV_MIN_PIPELINE_SPEC).unwrap() + ) + })?; + + info!(config = ?settings, ?pipeline_spec, "Starting server with config and pipeline spec"); // Start the metrics server, which serves the prometheus metrics. let metrics_addr: SocketAddr = - format!("0.0.0.0:{}", &config().metrics_server_listen_port).parse()?; + format!("0.0.0.0:{}", &settings.metrics_server_listen_port).parse()?; let metrics_server_handle = tokio::spawn(start_https_metrics_server(metrics_addr, tls_config.clone())); - let app_addr: SocketAddr = format!("0.0.0.0:{}", &config().app_listen_port).parse()?; - // Start the main server, which serves the application. - let app_server_handle = tokio::spawn(start_main_server(app_addr, tls_config)); + let app_server_handle = tokio::spawn(start_main_server(settings, tls_config, pipeline_spec)); // TODO: is try_join the best? we need to short-circuit at the first failure tokio::try_join!(flatten(app_server_handle), flatten(metrics_server_handle))?; diff --git a/rust/serving/src/metrics.rs b/rust/serving/src/metrics.rs index 830a37c0c5..4c64760d4d 100644 --- a/rust/serving/src/metrics.rs +++ b/rust/serving/src/metrics.rs @@ -97,6 +97,7 @@ pub(crate) async fn start_https_metrics_server( ) -> crate::Result<()> { let metrics_app = Router::new().route("/metrics", get(metrics_handler)); + tracing::info!(?addr, "Starting metrics server"); axum_server::bind_rustls(addr, tls_config) .serve(metrics_app.into_make_service()) .await diff --git a/rust/serving/src/pipeline.rs b/rust/serving/src/pipeline.rs index 042e5923b4..d782e3d73a 100644 --- a/rust/serving/src/pipeline.rs +++ b/rust/serving/src/pipeline.rs @@ -1,5 +1,4 @@ -use std::env; -use std::sync::OnceLock; +use std::str::FromStr; use base64::prelude::BASE64_STANDARD; use base64::Engine; @@ -8,16 +7,6 @@ use serde::{Deserialize, Serialize}; use crate::Error::ParseConfig; -const ENV_MIN_PIPELINE_SPEC: &str = "NUMAFLOW_SERVING_MIN_PIPELINE_SPEC"; - -pub fn min_pipeline_spec() -> &'static PipelineDCG { - static PIPELINE: OnceLock = OnceLock::new(); - PIPELINE.get_or_init(|| match PipelineDCG::load() { - Ok(pipeline) => pipeline, - Err(e) => panic!("Failed to load minimum pipeline spec: {:?}", e), - }) -} - // OperatorType is an enum that contains the types of operators // that can be used in the conditions for the edge. #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] @@ -53,6 +42,7 @@ impl From for OperatorType { } // Tag is a struct that contains the information about the tags for the edge +#[cfg_attr(test, derive(PartialEq))] #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Tag { pub operator: Option, @@ -60,6 +50,7 @@ pub struct Tag { } // Conditions is a struct that contains the information about the conditions for the edge +#[cfg_attr(test, derive(PartialEq))] #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Conditions { pub tags: Option, @@ -87,27 +78,17 @@ pub struct Vertex { pub name: String, } -impl PipelineDCG { - pub fn load() -> crate::Result { - let full_pipeline_spec = match env::var(ENV_MIN_PIPELINE_SPEC) { - Ok(env_value) => { - // If the environment variable is set, decode and parse the pipeline - let decoded = BASE64_STANDARD - .decode(env_value.as_bytes()) - .map_err(|e| ParseConfig(format!("decoding pipeline from env: {e:?}")))?; - - serde_json::from_slice::(&decoded) - .map_err(|e| ParseConfig(format!("parsing pipeline from env: {e:?}")))? - } - Err(_) => { - // If the environment variable is not set, read the pipeline from a file - let file_path = "./config/pipeline_spec.json"; - let file_contents = std::fs::read_to_string(file_path) - .map_err(|e| ParseConfig(format!("reading pipeline file: {e:?}")))?; - serde_json::from_str::(&file_contents) - .map_err(|e| ParseConfig(format!("parsing pipeline file: {e:?}")))? - } - }; +impl FromStr for PipelineDCG { + type Err = crate::Error; + + fn from_str(pipeline_spec_encoded: &str) -> Result { + let full_pipeline_spec_decoded = BASE64_STANDARD + .decode(pipeline_spec_encoded) + .map_err(|e| ParseConfig(format!("Decoding pipeline from env: {e:?}")))?; + + let full_pipeline_spec = + serde_json::from_slice::(&full_pipeline_spec_decoded) + .map_err(|e| ParseConfig(format!("parsing pipeline from env: {e:?}")))?; let vertices: Vec = full_pipeline_spec .vertices @@ -148,17 +129,29 @@ mod tests { #[test] fn test_pipeline_load() { - let pipeline = min_pipeline_spec(); - assert_eq!(pipeline.vertices.len(), 3); - assert_eq!(pipeline.edges.len(), 2); + let pipeline: PipelineDCG = "eyJ2ZXJ0aWNlcyI6W3sibmFtZSI6ImluIiwic291cmNlIjp7InNlcnZpbmciOnsiYXV0aCI6bnVsbCwic2VydmljZSI6dHJ1ZSwibXNnSURIZWFkZXJLZXkiOiJYLU51bWFmbG93LUlkIiwic3RvcmUiOnsidXJsIjoicmVkaXM6Ly9yZWRpczo2Mzc5In19fSwiY29udGFpbmVyVGVtcGxhdGUiOnsicmVzb3VyY2VzIjp7fSwiaW1hZ2VQdWxsUG9saWN5IjoiTmV2ZXIiLCJlbnYiOlt7Im5hbWUiOiJSVVNUX0xPRyIsInZhbHVlIjoiZGVidWcifV19LCJzY2FsZSI6eyJtaW4iOjF9LCJ1cGRhdGVTdHJhdGVneSI6eyJ0eXBlIjoiUm9sbGluZ1VwZGF0ZSIsInJvbGxpbmdVcGRhdGUiOnsibWF4VW5hdmFpbGFibGUiOiIyNSUifX19LHsibmFtZSI6InBsYW5uZXIiLCJ1ZGYiOnsiY29udGFpbmVyIjp7ImltYWdlIjoiYXNjaWk6MC4xIiwiYXJncyI6WyJwbGFubmVyIl0sInJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sImJ1aWx0aW4iOm51bGwsImdyb3VwQnkiOm51bGx9LCJjb250YWluZXJUZW1wbGF0ZSI6eyJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9LCJzY2FsZSI6eyJtaW4iOjF9LCJ1cGRhdGVTdHJhdGVneSI6eyJ0eXBlIjoiUm9sbGluZ1VwZGF0ZSIsInJvbGxpbmdVcGRhdGUiOnsibWF4VW5hdmFpbGFibGUiOiIyNSUifX19LHsibmFtZSI6InRpZ2VyIiwidWRmIjp7ImNvbnRhaW5lciI6eyJpbWFnZSI6ImFzY2lpOjAuMSIsImFyZ3MiOlsidGlnZXIiXSwicmVzb3VyY2VzIjp7fSwiaW1hZ2VQdWxsUG9saWN5IjoiTmV2ZXIifSwiYnVpbHRpbiI6bnVsbCwiZ3JvdXBCeSI6bnVsbH0sImNvbnRhaW5lclRlbXBsYXRlIjp7InJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sInNjYWxlIjp7Im1pbiI6MX0sInVwZGF0ZVN0cmF0ZWd5Ijp7InR5cGUiOiJSb2xsaW5nVXBkYXRlIiwicm9sbGluZ1VwZGF0ZSI6eyJtYXhVbmF2YWlsYWJsZSI6IjI1JSJ9fX0seyJuYW1lIjoiZG9nIiwidWRmIjp7ImNvbnRhaW5lciI6eyJpbWFnZSI6ImFzY2lpOjAuMSIsImFyZ3MiOlsiZG9nIl0sInJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sImJ1aWx0aW4iOm51bGwsImdyb3VwQnkiOm51bGx9LCJjb250YWluZXJUZW1wbGF0ZSI6eyJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9LCJzY2FsZSI6eyJtaW4iOjF9LCJ1cGRhdGVTdHJhdGVneSI6eyJ0eXBlIjoiUm9sbGluZ1VwZGF0ZSIsInJvbGxpbmdVcGRhdGUiOnsibWF4VW5hdmFpbGFibGUiOiIyNSUifX19LHsibmFtZSI6ImVsZXBoYW50IiwidWRmIjp7ImNvbnRhaW5lciI6eyJpbWFnZSI6ImFzY2lpOjAuMSIsImFyZ3MiOlsiZWxlcGhhbnQiXSwicmVzb3VyY2VzIjp7fSwiaW1hZ2VQdWxsUG9saWN5IjoiTmV2ZXIifSwiYnVpbHRpbiI6bnVsbCwiZ3JvdXBCeSI6bnVsbH0sImNvbnRhaW5lclRlbXBsYXRlIjp7InJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sInNjYWxlIjp7Im1pbiI6MX0sInVwZGF0ZVN0cmF0ZWd5Ijp7InR5cGUiOiJSb2xsaW5nVXBkYXRlIiwicm9sbGluZ1VwZGF0ZSI6eyJtYXhVbmF2YWlsYWJsZSI6IjI1JSJ9fX0seyJuYW1lIjoiYXNjaWlhcnQiLCJ1ZGYiOnsiY29udGFpbmVyIjp7ImltYWdlIjoiYXNjaWk6MC4xIiwiYXJncyI6WyJhc2NpaWFydCJdLCJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9LCJidWlsdGluIjpudWxsLCJncm91cEJ5IjpudWxsfSwiY29udGFpbmVyVGVtcGxhdGUiOnsicmVzb3VyY2VzIjp7fSwiaW1hZ2VQdWxsUG9saWN5IjoiTmV2ZXIifSwic2NhbGUiOnsibWluIjoxfSwidXBkYXRlU3RyYXRlZ3kiOnsidHlwZSI6IlJvbGxpbmdVcGRhdGUiLCJyb2xsaW5nVXBkYXRlIjp7Im1heFVuYXZhaWxhYmxlIjoiMjUlIn19fSx7Im5hbWUiOiJzZXJ2ZS1zaW5rIiwic2luayI6eyJ1ZHNpbmsiOnsiY29udGFpbmVyIjp7ImltYWdlIjoic2VydmVzaW5rOjAuMSIsImVudiI6W3sibmFtZSI6Ik5VTUFGTE9XX0NBTExCQUNLX1VSTF9LRVkiLCJ2YWx1ZSI6IlgtTnVtYWZsb3ctQ2FsbGJhY2stVXJsIn0seyJuYW1lIjoiTlVNQUZMT1dfTVNHX0lEX0hFQURFUl9LRVkiLCJ2YWx1ZSI6IlgtTnVtYWZsb3ctSWQifV0sInJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn19LCJyZXRyeVN0cmF0ZWd5Ijp7fX0sImNvbnRhaW5lclRlbXBsYXRlIjp7InJlc291cmNlcyI6e30sImltYWdlUHVsbFBvbGljeSI6Ik5ldmVyIn0sInNjYWxlIjp7Im1pbiI6MX0sInVwZGF0ZVN0cmF0ZWd5Ijp7InR5cGUiOiJSb2xsaW5nVXBkYXRlIiwicm9sbGluZ1VwZGF0ZSI6eyJtYXhVbmF2YWlsYWJsZSI6IjI1JSJ9fX0seyJuYW1lIjoiZXJyb3Itc2luayIsInNpbmsiOnsidWRzaW5rIjp7ImNvbnRhaW5lciI6eyJpbWFnZSI6InNlcnZlc2luazowLjEiLCJlbnYiOlt7Im5hbWUiOiJOVU1BRkxPV19DQUxMQkFDS19VUkxfS0VZIiwidmFsdWUiOiJYLU51bWFmbG93LUNhbGxiYWNrLVVybCJ9LHsibmFtZSI6Ik5VTUFGTE9XX01TR19JRF9IRUFERVJfS0VZIiwidmFsdWUiOiJYLU51bWFmbG93LUlkIn1dLCJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9fSwicmV0cnlTdHJhdGVneSI6e319LCJjb250YWluZXJUZW1wbGF0ZSI6eyJyZXNvdXJjZXMiOnt9LCJpbWFnZVB1bGxQb2xpY3kiOiJOZXZlciJ9LCJzY2FsZSI6eyJtaW4iOjF9LCJ1cGRhdGVTdHJhdGVneSI6eyJ0eXBlIjoiUm9sbGluZ1VwZGF0ZSIsInJvbGxpbmdVcGRhdGUiOnsibWF4VW5hdmFpbGFibGUiOiIyNSUifX19XSwiZWRnZXMiOlt7ImZyb20iOiJpbiIsInRvIjoicGxhbm5lciIsImNvbmRpdGlvbnMiOm51bGx9LHsiZnJvbSI6InBsYW5uZXIiLCJ0byI6ImFzY2lpYXJ0IiwiY29uZGl0aW9ucyI6eyJ0YWdzIjp7Im9wZXJhdG9yIjoib3IiLCJ2YWx1ZXMiOlsiYXNjaWlhcnQiXX19fSx7ImZyb20iOiJwbGFubmVyIiwidG8iOiJ0aWdlciIsImNvbmRpdGlvbnMiOnsidGFncyI6eyJvcGVyYXRvciI6Im9yIiwidmFsdWVzIjpbInRpZ2VyIl19fX0seyJmcm9tIjoicGxhbm5lciIsInRvIjoiZG9nIiwiY29uZGl0aW9ucyI6eyJ0YWdzIjp7Im9wZXJhdG9yIjoib3IiLCJ2YWx1ZXMiOlsiZG9nIl19fX0seyJmcm9tIjoicGxhbm5lciIsInRvIjoiZWxlcGhhbnQiLCJjb25kaXRpb25zIjp7InRhZ3MiOnsib3BlcmF0b3IiOiJvciIsInZhbHVlcyI6WyJlbGVwaGFudCJdfX19LHsiZnJvbSI6InRpZ2VyIiwidG8iOiJzZXJ2ZS1zaW5rIiwiY29uZGl0aW9ucyI6bnVsbH0seyJmcm9tIjoiZG9nIiwidG8iOiJzZXJ2ZS1zaW5rIiwiY29uZGl0aW9ucyI6bnVsbH0seyJmcm9tIjoiZWxlcGhhbnQiLCJ0byI6InNlcnZlLXNpbmsiLCJjb25kaXRpb25zIjpudWxsfSx7ImZyb20iOiJhc2NpaWFydCIsInRvIjoic2VydmUtc2luayIsImNvbmRpdGlvbnMiOm51bGx9LHsiZnJvbSI6InBsYW5uZXIiLCJ0byI6ImVycm9yLXNpbmsiLCJjb25kaXRpb25zIjp7InRhZ3MiOnsib3BlcmF0b3IiOiJvciIsInZhbHVlcyI6WyJlcnJvciJdfX19XSwibGlmZWN5Y2xlIjp7fSwid2F0ZXJtYXJrIjp7fX0=".parse().unwrap(); + + assert_eq!(pipeline.vertices.len(), 8); + assert_eq!(pipeline.edges.len(), 10); assert_eq!(pipeline.vertices[0].name, "in"); assert_eq!(pipeline.edges[0].from, "in"); - assert_eq!(pipeline.edges[0].to, "cat"); + assert_eq!(pipeline.edges[0].to, "planner"); assert!(pipeline.edges[0].conditions.is_none()); - assert_eq!(pipeline.vertices[1].name, "cat"); - assert_eq!(pipeline.vertices[2].name, "out"); - assert_eq!(pipeline.edges[1].from, "cat"); - assert_eq!(pipeline.edges[1].to, "out"); + assert_eq!(pipeline.vertices[1].name, "planner"); + assert_eq!(pipeline.edges[1].from, "planner"); + assert_eq!(pipeline.edges[1].to, "asciiart"); + assert_eq!( + pipeline.edges[1].conditions, + Some(Conditions { + tags: Some(Tag { + operator: Some(OperatorType::Or), + values: vec!["asciiart".to_owned()] + }) + }) + ); + + assert_eq!(pipeline.vertices[2].name, "tiger"); + assert_eq!(pipeline.vertices[3].name, "dog"); } }