diff --git a/server/Cargo.toml b/server/Cargo.toml index 7c5d1d8a..1515c283 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -116,3 +116,6 @@ tracing-test = "0.2.5" [build-dependencies] shadow-rs = "0.37.0" + +[features] +delta = [] diff --git a/server/src/auth/token_validator.rs b/server/src/auth/token_validator.rs index 58c01437..f566d4bb 100644 --- a/server/src/auth/token_validator.rs +++ b/server/src/auth/token_validator.rs @@ -4,7 +4,7 @@ use dashmap::DashMap; use tracing::trace; use unleash_types::Upsert; -use crate::http::feature_refresher::FeatureRefresher; +use crate::http::refresher::feature_refresher::FeatureRefresher; use crate::http::unleash_client::UnleashClient; use crate::persistence::EdgePersistence; use crate::types::{ diff --git a/server/src/builder.rs b/server/src/builder.rs index 220f2c88..f6130ec5 100644 --- a/server/src/builder.rs +++ b/server/src/builder.rs @@ -12,7 +12,7 @@ use unleash_yggdrasil::EngineState; use crate::cli::RedisMode; use crate::feature_cache::FeatureCache; -use crate::http::feature_refresher::{FeatureRefreshConfig, FeatureRefresherMode}; +use crate::http::refresher::feature_refresher::{FeatureRefreshConfig, FeatureRefresherMode}; use crate::http::unleash_client::{new_reqwest_client, ClientMetaInformation}; use crate::offline::offline_hotload::{load_bootstrap, load_offline_engine_cache}; use crate::persistence::file::FilePersister; @@ -23,7 +23,7 @@ use crate::{ auth::token_validator::TokenValidator, cli::{CliArgs, EdgeArgs, EdgeMode, OfflineArgs}, error::EdgeError, - http::{feature_refresher::FeatureRefresher, unleash_client::UnleashClient}, + http::{refresher::feature_refresher::FeatureRefresher, unleash_client::UnleashClient}, types::{EdgeResult, EdgeToken, TokenType}, }; @@ -270,6 +270,7 @@ async fn build_edge( Duration::seconds(args.features_refresh_interval_seconds as i64), refresher_mode, client_meta_information, + args.delta, ); let feature_refresher = Arc::new(FeatureRefresher::new( unleash_client, @@ -385,6 +386,7 @@ mod tests { prometheus_password: None, prometheus_username: None, streaming: false, + delta: false, }; let result = build_edge( diff --git a/server/src/cli.rs b/server/src/cli.rs index ee9aeb40..d46674c0 100644 --- a/server/src/cli.rs +++ b/server/src/cli.rs @@ -217,6 +217,10 @@ pub struct EdgeArgs { #[clap(long, env, default_value_t = false, requires = "strict")] pub streaming: bool, + /// If set to true. Edge connects to upstream using delta polling instead of normal polling. This is experimental feature and might and change. + #[clap(long, env, default_value_t = false, conflicts_with = "streaming")] + pub delta: bool, + /// Sets a remote write url for prometheus metrics, if this is set, prometheus metrics will be written upstream #[clap(long, env)] pub prometheus_remote_write_url: Option, diff --git a/server/src/client_api.rs b/server/src/client_api.rs index daa24013..3d8ef9d0 100644 --- a/server/src/client_api.rs +++ b/server/src/client_api.rs @@ -4,7 +4,7 @@ use crate::filters::{ filter_client_features, name_match_filter, name_prefix_filter, project_filter, FeatureFilterSet, }; use crate::http::broadcaster::Broadcaster; -use crate::http::feature_refresher::FeatureRefresher; +use crate::http::refresher::feature_refresher::FeatureRefresher; use crate::metrics::client_metrics::MetricsCache; use crate::tokens::cache_key; use crate::types::{ @@ -1012,6 +1012,7 @@ mod tests { strict: false, streaming: false, client_meta_information: ClientMetaInformation::test_config(), + delta: false, }); let token_validator = Arc::new(TokenValidator { unleash_client: unleash_client.clone(), diff --git a/server/src/feature_cache.rs b/server/src/feature_cache.rs index d2891569..3e1021e3 100644 --- a/server/src/feature_cache.rs +++ b/server/src/feature_cache.rs @@ -4,7 +4,7 @@ use unleash_types::{ client_features::{ClientFeature, ClientFeatures, Segment}, Deduplicate, }; - +use unleash_types::client_features::ClientFeaturesDelta; use crate::types::EdgeToken; #[derive(Debug, Clone)] @@ -67,6 +67,23 @@ impl FeatureCache { self.send_full_update(key); } + pub fn apply_delta(&self, key: String, delta: &ClientFeaturesDelta) { + let client_features = ClientFeatures { + version : 2, + features : delta.updated.clone(), + segments: delta.segments.clone(), + query: None, + meta: None, + }; + self.features + .entry(key.clone()) + .and_modify(|existing_features| { + existing_features.modify_in_place(delta); + }) + .or_insert(client_features); + self.send_full_update(key); + } + pub fn is_empty(&self) -> bool { self.features.is_empty() } diff --git a/server/src/http/background_send_metrics.rs b/server/src/http/background_send_metrics.rs index dd6001d0..a0d9aba4 100644 --- a/server/src/http/background_send_metrics.rs +++ b/server/src/http/background_send_metrics.rs @@ -14,7 +14,7 @@ use crate::{ metrics::client_metrics::{size_of_batch, MetricsCache}, }; -use super::feature_refresher::FeatureRefresher; +use super::refresher::feature_refresher::FeatureRefresher; lazy_static! { pub static ref METRICS_UPSTREAM_HTTP_ERRORS: IntGaugeVec = register_int_gauge_vec!( diff --git a/server/src/http/mod.rs b/server/src/http/mod.rs index f7251942..c223d055 100644 --- a/server/src/http/mod.rs +++ b/server/src/http/mod.rs @@ -1,6 +1,6 @@ #[cfg(not(tarpaulin_include))] pub mod background_send_metrics; pub mod broadcaster; -pub mod feature_refresher; pub(crate) mod headers; pub mod unleash_client; +pub mod refresher; diff --git a/server/src/http/refresher/delta_refresher.rs b/server/src/http/refresher/delta_refresher.rs new file mode 100644 index 00000000..312274c7 --- /dev/null +++ b/server/src/http/refresher/delta_refresher.rs @@ -0,0 +1,271 @@ +use actix_web::http::header::EntityTag; +use reqwest::StatusCode; +use tracing::{debug, info, warn}; +use unleash_types::client_features::{ClientFeaturesDelta}; +use unleash_yggdrasil::EngineState; + +use crate::error::{EdgeError, FeatureError}; +use crate::types::{ClientFeaturesDeltaResponse, ClientFeaturesRequest, EdgeToken, TokenRefresh}; +use crate::http::refresher::feature_refresher::FeatureRefresher; +use crate::tokens::cache_key; + +impl FeatureRefresher { + async fn handle_client_features_delta_updated( + &self, + refresh_token: &EdgeToken, + delta: ClientFeaturesDelta, + etag: Option, + ) { + debug!("Got updated client features delta. Updating features with {etag:?}"); + let key = cache_key(refresh_token); + self.features_cache.apply_delta(key.clone(), &delta); + self.update_last_refresh( + refresh_token, + etag, + self.features_cache.get(&key).unwrap().features.len(), + ); + self.engine_cache + .entry(key.clone()) + .and_modify(|engine| { + engine.take_delta(&delta); + }) + .or_insert_with(|| { + let mut new_state = EngineState::default(); + + let warnings = new_state.take_delta(&delta); + if let Some(warnings) = warnings { + warn!("The following toggle failed to compile and will be defaulted to off: {warnings:?}"); + }; + new_state + }); + } + + pub async fn refresh_single_delta(&self, refresh: TokenRefresh) { + let features_result = self + .unleash_client + .get_client_features_delta(ClientFeaturesRequest { + api_key: refresh.token.token.clone(), + etag: refresh.etag, + }) + .await; + match features_result { + Ok(delta_response) => match delta_response { + ClientFeaturesDeltaResponse::NoUpdate(tag) => { + debug!("No update needed. Will update last check time with {tag}"); + self.update_last_check(&refresh.token.clone()); + } + ClientFeaturesDeltaResponse::Updated(features, etag) => { + self.handle_client_features_delta_updated(&refresh.token, features, etag) + .await + } + }, + Err(e) => { + match e { + EdgeError::ClientFeaturesFetchError(fe) => { + match fe { + FeatureError::Retriable(status_code) => match status_code { + StatusCode::INTERNAL_SERVER_ERROR + | StatusCode::BAD_GATEWAY + | StatusCode::SERVICE_UNAVAILABLE + | StatusCode::GATEWAY_TIMEOUT => { + info!("Upstream is having some problems, increasing my waiting period"); + self.backoff(&refresh.token); + } + StatusCode::TOO_MANY_REQUESTS => { + info!("Got told that upstream is receiving too many requests"); + self.backoff(&refresh.token); + } + _ => { + info!("Couldn't refresh features, but will retry next go") + } + }, + FeatureError::AccessDenied => { + info!("Token used to fetch features was Forbidden, will remove from list of refresh tasks"); + self.tokens_to_refresh.remove(&refresh.token.token); + if !self.tokens_to_refresh.iter().any(|e| { + e.value().token.environment == refresh.token.environment + }) { + let cache_key = cache_key(&refresh.token); + // No tokens left that access the environment of our current refresh. Deleting client features and engine cache + self.features_cache.remove(&cache_key); + self.engine_cache.remove(&cache_key); + } + } + FeatureError::NotFound => { + info!("Had a bad URL when trying to fetch features. Increasing waiting period for the token before trying again"); + self.backoff(&refresh.token); + } + } + } + EdgeError::ClientCacheError => { + info!("Couldn't refresh features, but will retry next go") + } + _ => info!("Couldn't refresh features: {e:?}. Will retry next pass"), + } + } + } + } +} + + +#[cfg(feature = "delta")] +#[cfg(test)] +mod tests { + use actix_http::header::IF_NONE_MATCH; + use actix_http::HttpService; + use actix_http_test::{test_server, TestServer}; + use actix_service::map_config; + use actix_web::dev::AppConfig; + use actix_web::http::header::{ETag, EntityTag}; + use actix_web::{web, App, HttpRequest, HttpResponse}; + use chrono::Duration; + use dashmap::DashMap; + use std::sync::Arc; + use crate::feature_cache::FeatureCache; + use crate::http::refresher::feature_refresher::FeatureRefresher; + use crate::http::unleash_client::{ClientMetaInformation, UnleashClient}; + use crate::types::EdgeToken; + use unleash_types::client_features::{ + ClientFeature, ClientFeatures, ClientFeaturesDelta, Constraint, Operator, Segment, + }; + use unleash_yggdrasil::EngineState; + + #[actix_web::test] + #[tracing_test::traced_test] + async fn test_delta() { + let srv = test_features_server().await; + let unleash_client = Arc::new(UnleashClient::new(srv.url("/").as_str(), None).unwrap()); + let features_cache: Arc = Arc::new(FeatureCache::default()); + let engine_cache: Arc> = Arc::new(DashMap::default()); + + let feature_refresher = Arc::new(FeatureRefresher { + unleash_client: unleash_client.clone(), + tokens_to_refresh: Arc::new(Default::default()), + features_cache: features_cache.clone(), + engine_cache: engine_cache.clone(), + refresh_interval: Duration::seconds(6000), + persistence: None, + strict: false, + streaming: false, + delta: true, + client_meta_information:ClientMetaInformation::test_config(), + }); + let features = ClientFeatures { + version: 2, + features: vec![], + segments: None, + query: None, + meta: None, + }; + let initial_features = features.modify_and_copy(&revision(1)); + let final_features = initial_features.modify_and_copy(&revision(2)); + let token = + EdgeToken::try_from("*:development.abcdefghijklmnopqrstuvwxyz".to_string()).unwrap(); + feature_refresher + .register_token_for_refresh(token.clone(), None) + .await; + feature_refresher.refresh_features().await; + let refreshed_features = features_cache + .get(&cache_key(&token)) + .unwrap() + .value() + .clone(); + assert_eq!(refreshed_features, initial_features); + + let token_refresh = feature_refresher + .tokens_to_refresh + .get(&token.token) + .unwrap() + .clone(); + feature_refresher.refresh_single_delta(token_refresh).await; + let refreshed_features = features_cache + .get(&cache_key(&token)) + .unwrap() + .value() + .clone(); + assert_eq!(refreshed_features, final_features); + } + + fn cache_key(token: &EdgeToken) -> String { + token + .environment + .clone() + .unwrap_or_else(|| token.token.clone()) + } + + fn revision(revision_id: u32) -> ClientFeaturesDelta { + match revision_id { + 1 => ClientFeaturesDelta { + updated: vec![ + ClientFeature { + name: "test1".into(), + feature_type: Some("release".into()), + ..Default::default() + }, + ClientFeature { + name: "test2".into(), + feature_type: Some("release".into()), + ..Default::default() + }, + ], + removed: vec![], + segments: Some(vec![Segment { + id: 1, + constraints: vec![Constraint { + context_name: "userId".into(), + operator: Operator::In, + case_insensitive: false, + inverted: false, + values: Some(vec!["7".into()]), + value: None, + }], + }]), + revision_id: 1, + }, + _ => ClientFeaturesDelta { + updated: vec![ClientFeature { + name: "test1".into(), + feature_type: Some("release".into()), + ..Default::default() + }], + removed: vec!["test2".to_string()], + segments: None, + revision_id: 2, + }, + } + } + + async fn return_client_features_delta(etag_header: Option) -> HttpResponse { + match etag_header { + Some(value) => match value.as_str() { + "\"1\"" => HttpResponse::Ok() + .insert_header(ETag(EntityTag::new_strong("2".to_string()))) + .json(revision(2)), + "\"2\"" => HttpResponse::NotModified().finish(), + _ => HttpResponse::NotModified().finish(), + }, + None => HttpResponse::Ok() + .insert_header(ETag(EntityTag::new_strong("1".to_string()))) + .json(revision(1)), + } + } + + async fn test_features_server() -> TestServer { + test_server(move || { + HttpService::new(map_config( + App::new().service(web::resource("/api/client/delta").route(web::get().to( + |req: HttpRequest| { + let etag_header = req + .headers() + .get(IF_NONE_MATCH) + .and_then(|h| h.to_str().ok()); + return_client_features_delta(etag_header.map(|s| s.to_string())) + }, + ))), + |_| AppConfig::default(), + )) + .tcp() + }) + .await + } +} diff --git a/server/src/http/feature_refresher.rs b/server/src/http/refresher/feature_refresher.rs similarity index 98% rename from server/src/http/feature_refresher.rs rename to server/src/http/refresher/feature_refresher.rs index 5aba48f1..3f647937 100644 --- a/server/src/http/feature_refresher.rs +++ b/server/src/http/refresher/feature_refresher.rs @@ -8,7 +8,7 @@ use eventsource_client::Client; use futures::TryStreamExt; use reqwest::StatusCode; use tracing::{debug, info, warn}; -use unleash_types::client_features::ClientFeatures; +use unleash_types::client_features::{ClientFeatures}; use unleash_types::client_metrics::{ClientApplication, MetricsMetadata}; use unleash_yggdrasil::EngineState; @@ -18,14 +18,16 @@ use crate::filters::{filter_client_features, FeatureFilterSet}; use crate::http::headers::{ UNLEASH_APPNAME_HEADER, UNLEASH_CLIENT_SPEC_HEADER, UNLEASH_INSTANCE_ID_HEADER, }; -use crate::types::{build, EdgeResult, TokenType, TokenValidationStatus}; +use crate::types::{ + build, EdgeResult, TokenType, TokenValidationStatus, +}; use crate::{ persistence::EdgePersistence, tokens::{cache_key, simplify}, types::{ClientFeaturesRequest, ClientFeaturesResponse, EdgeToken, TokenRefresh}, }; -use super::unleash_client::{ClientMetaInformation, UnleashClient}; +use crate::http::unleash_client::{ClientMetaInformation, UnleashClient}; fn frontend_token_is_covered_by_tokens( frontend_token: &EdgeToken, @@ -49,6 +51,7 @@ pub struct FeatureRefresher { pub strict: bool, pub streaming: bool, pub client_meta_information: ClientMetaInformation, + pub delta: bool, } impl Default for FeatureRefresher { @@ -63,6 +66,7 @@ impl Default for FeatureRefresher { strict: true, streaming: false, client_meta_information: Default::default(), + delta: false, } } } @@ -100,6 +104,7 @@ pub struct FeatureRefreshConfig { features_refresh_interval: chrono::Duration, mode: FeatureRefresherMode, client_meta_information: ClientMetaInformation, + delta: bool, } impl FeatureRefreshConfig { @@ -107,11 +112,13 @@ impl FeatureRefreshConfig { features_refresh_interval: chrono::Duration, mode: FeatureRefresherMode, client_meta_information: ClientMetaInformation, + delta: bool, ) -> Self { Self { features_refresh_interval, mode, client_meta_information, + delta, } } } @@ -134,6 +141,7 @@ impl FeatureRefresher { strict: config.mode != FeatureRefresherMode::Dynamic, streaming: config.mode == FeatureRefresherMode::Streaming, client_meta_information: config.client_meta_information, + delta: config.delta, } } @@ -404,13 +412,22 @@ impl FeatureRefresher { pub async fn hydrate_new_tokens(&self) { let hydrations = self.get_tokens_never_refreshed(); for hydration in hydrations { - self.refresh_single(hydration).await; + if cfg!(feature = "delta") && self.delta { + self.refresh_single_delta(hydration).await; + } else { + self.refresh_single(hydration).await; + } } } pub async fn refresh_features(&self) { let refreshes = self.get_tokens_due_for_refresh(); for refresh in refreshes { - self.refresh_single(refresh).await; + if cfg!(feature = "delta") && self.delta { + self.refresh_single_delta(refresh).await; + } else { + self.refresh_single(refresh).await; + } + } } @@ -448,7 +465,6 @@ impl FeatureRefresher { new_state }); } - pub async fn refresh_single(&self, refresh: TokenRefresh) { let features_result = self .unleash_client @@ -1581,7 +1597,7 @@ mod tests { token_type: Some(TokenType::Client), environment: Some("dev".into()), projects: vec![String::from("testproject"), String::from("someother")], - status: TokenValidationStatus::Validated, + status: Validated, }; let updated = update_projects_from_feature_update( &token_with_access_to_both_empty_and_full_project, diff --git a/server/src/http/refresher/mod.rs b/server/src/http/refresher/mod.rs new file mode 100644 index 00000000..e826dcf7 --- /dev/null +++ b/server/src/http/refresher/mod.rs @@ -0,0 +1,2 @@ +pub mod feature_refresher; +pub mod delta_refresher; diff --git a/server/src/http/unleash_client.rs b/server/src/http/unleash_client.rs index f0e533b0..e1829e34 100644 --- a/server/src/http/unleash_client.rs +++ b/server/src/http/unleash_client.rs @@ -14,7 +14,7 @@ use reqwest::{ClientBuilder, Identity, RequestBuilder, StatusCode, Url}; use serde::{Deserialize, Serialize}; use tracing::error; use tracing::{info, trace, warn}; -use unleash_types::client_features::ClientFeatures; +use unleash_types::client_features::{ClientFeatures, ClientFeaturesDelta}; use unleash_types::client_metrics::ClientApplication; use crate::cli::ClientIdentity; @@ -26,7 +26,8 @@ use crate::http::headers::{ use crate::metrics::client_metrics::MetricsBatch; use crate::tls::build_upstream_certificate; use crate::types::{ - ClientFeaturesResponse, EdgeResult, EdgeToken, TokenValidationStatus, ValidateTokensRequest, + ClientFeaturesDeltaResponse, ClientFeaturesResponse, EdgeResult, EdgeToken, + TokenValidationStatus, ValidateTokensRequest, }; use crate::urls::UnleashUrls; use crate::{error::EdgeError, types::ClientFeaturesRequest}; @@ -47,6 +48,13 @@ lazy_static! { vec![1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 100.0, 200.0, 500.0, 1000.0, 5000.0] ) .unwrap(); + pub static ref CLIENT_FEATURE_DELTA_FETCH: HistogramVec = register_histogram_vec!( + "client_feature_delta_fetch", + "Timings for fetching feature deltas in milliseconds", + &["status_code"], + vec![1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 100.0, 200.0, 500.0, 1000.0, 5000.0] + ) + .unwrap(); pub static ref CLIENT_FEATURE_FETCH_FAILURES: IntGaugeVec = register_int_gauge_vec!( Opts::new( "client_feature_fetch_failures", @@ -210,7 +218,6 @@ impl UnleashClient { } } - #[cfg(test)] pub fn new(server_url: &str, instance_id_opt: Option) -> Result { use ulid::Ulid; @@ -265,6 +272,18 @@ impl UnleashClient { } } + fn client_features_delta_req(&self, req: ClientFeaturesRequest) -> RequestBuilder { + let client_req = self + .backing_client + .get(self.urls.client_features_delta_url.to_string()) + .headers(self.header_map(Some(req.api_key))); + if let Some(tag) = req.etag { + client_req.header(header::IF_NONE_MATCH, tag.to_string()) + } else { + client_req + } + } + fn header_map(&self, api_key: Option) -> HeaderMap { let mut header_map = HeaderMap::new(); let token_header: HeaderName = HeaderName::from_str(self.token_header.as_str()).unwrap(); @@ -390,6 +409,83 @@ impl UnleashClient { } } + pub async fn get_client_features_delta( + &self, + request: ClientFeaturesRequest, + ) -> EdgeResult { + let start_time = Utc::now(); + let response = self + .client_features_delta_req(request.clone()) + .send() + .await + .map_err(|e| { + warn!("Failed to fetch. Due to [{e:?}] - Will retry"); + match e.status() { + Some(s) => EdgeError::ClientFeaturesFetchError(FeatureError::Retriable(s)), + None => EdgeError::ClientFeaturesFetchError(FeatureError::NotFound), + } + })?; + let stop_time = Utc::now(); + CLIENT_FEATURE_DELTA_FETCH + .with_label_values(&[&response.status().as_u16().to_string()]) + .observe( + stop_time + .signed_duration_since(start_time) + .num_milliseconds() as f64, + ); + if response.status() == StatusCode::NOT_MODIFIED { + Ok(ClientFeaturesDeltaResponse::NoUpdate( + request.etag.expect("Got NOT_MODIFIED without an ETag"), + )) + } else if response.status().is_success() { + let etag = response + .headers() + .get("ETag") + .or_else(|| response.headers().get("etag")) + .and_then(|etag| EntityTag::from_str(etag.to_str().unwrap()).ok()); + let features = response.json::().await.map_err(|e| { + warn!("Could not parse features response to internal representation"); + EdgeError::ClientFeaturesParseError(e.to_string()) + })?; + Ok(ClientFeaturesDeltaResponse::Updated(features, etag)) + } else if response.status() == StatusCode::FORBIDDEN { + CLIENT_FEATURE_FETCH_FAILURES + .with_label_values(&[response.status().as_str()]) + .inc(); + Err(EdgeError::ClientFeaturesFetchError( + FeatureError::AccessDenied, + )) + } else if response.status() == StatusCode::UNAUTHORIZED { + CLIENT_FEATURE_FETCH_FAILURES + .with_label_values(&[response.status().as_str()]) + .inc(); + warn!( + "Failed to get features. Url: [{}]. Status code: [401]", + self.urls.client_features_delta_url.to_string() + ); + Err(EdgeError::ClientFeaturesFetchError( + FeatureError::AccessDenied, + )) + } else if response.status() == StatusCode::NOT_FOUND { + CLIENT_FEATURE_FETCH_FAILURES + .with_label_values(&[response.status().as_str()]) + .inc(); + warn!( + "Failed to get features. Url: [{}]. Status code: [{}]", + self.urls.client_features_delta_url.to_string(), + response.status().as_str() + ); + Err(EdgeError::ClientFeaturesFetchError(FeatureError::NotFound)) + } else { + CLIENT_FEATURE_FETCH_FAILURES + .with_label_values(&[response.status().as_str()]) + .inc(); + Err(EdgeError::ClientFeaturesFetchError( + FeatureError::Retriable(response.status()), + )) + } + } + pub async fn send_batch_metrics(&self, request: MetricsBatch) -> EdgeResult<()> { trace!("Sending metrics to old /edge/metrics endpoint"); let result = self @@ -524,7 +620,6 @@ mod tests { }; use chrono::Duration; use unleash_types::client_features::{ClientFeature, ClientFeatures}; - use crate::cli::ClientIdentity; use crate::http::unleash_client::new_reqwest_client; use crate::{ diff --git a/server/src/internal_backstage.rs b/server/src/internal_backstage.rs index 70468fbb..ed55f703 100644 --- a/server/src/internal_backstage.rs +++ b/server/src/internal_backstage.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; use unleash_types::client_features::ClientFeatures; use unleash_types::client_metrics::ClientApplication; -use crate::http::feature_refresher::FeatureRefresher; +use crate::http::refresher::feature_refresher::FeatureRefresher; use crate::metrics::actix_web_metrics::PrometheusMetricsHandler; use crate::metrics::client_metrics::MetricsCache; use crate::types::{BuildInfo, EdgeJsonResult, EdgeToken, TokenInfo, TokenRefresh}; @@ -182,7 +182,7 @@ mod tests { use crate::auth::token_validator::TokenValidator; use crate::feature_cache::FeatureCache; - use crate::http::feature_refresher::FeatureRefresher; + use crate::http::refresher::feature_refresher::FeatureRefresher; use crate::http::unleash_client::UnleashClient; use crate::internal_backstage::EdgeStatus; use crate::middleware; diff --git a/server/src/main.rs b/server/src/main.rs index 641f8e0f..14b0f7d5 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -17,7 +17,7 @@ use unleash_edge::builder::build_caches_and_refreshers; use unleash_edge::cli::{CliArgs, EdgeMode}; use unleash_edge::feature_cache::FeatureCache; use unleash_edge::http::background_send_metrics::send_metrics_one_shot; -use unleash_edge::http::feature_refresher::FeatureRefresher; +use unleash_edge::http::refresher::feature_refresher::FeatureRefresher; use unleash_edge::metrics::client_metrics::MetricsCache; use unleash_edge::offline::offline_hotload; use unleash_edge::persistence::{persist_data, EdgePersistence}; diff --git a/server/src/middleware/client_token_from_frontend_token.rs b/server/src/middleware/client_token_from_frontend_token.rs index f3f2a9c9..ee85fd25 100644 --- a/server/src/middleware/client_token_from_frontend_token.rs +++ b/server/src/middleware/client_token_from_frontend_token.rs @@ -7,7 +7,7 @@ use dashmap::DashMap; use tracing::debug; use crate::{ - http::feature_refresher::FeatureRefresher, + http::refresher::feature_refresher::FeatureRefresher, tokens, types::{EdgeResult, EdgeToken, TokenValidationStatus}, }; @@ -64,7 +64,7 @@ mod tests { use crate::auth::token_validator::TokenValidator; use crate::feature_cache::FeatureCache; - use crate::http::feature_refresher::FeatureRefresher; + use crate::http::refresher::feature_refresher::FeatureRefresher; use crate::http::unleash_client::{new_reqwest_client, UnleashClient}; use crate::tests::upstream_server; use crate::types::{EdgeToken, TokenType, TokenValidationStatus}; diff --git a/server/src/types.rs b/server/src/types.rs index bb05b600..6b0ee23e 100644 --- a/server/src/types.rs +++ b/server/src/types.rs @@ -15,7 +15,7 @@ use chrono::{DateTime, Duration, Utc}; use dashmap::DashMap; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use shadow_rs::shadow; -use unleash_types::client_features::ClientFeatures; +use unleash_types::client_features::{ClientFeatures, ClientFeaturesDelta}; use unleash_types::client_features::Context; use unleash_types::client_metrics::{ClientApplication, ClientMetricsEnv}; use unleash_yggdrasil::EngineState; @@ -96,6 +96,12 @@ pub enum ClientFeaturesResponse { Updated(ClientFeatures, Option), } +#[derive(Clone, Debug)] +pub enum ClientFeaturesDeltaResponse { + NoUpdate(EntityTag), + Updated(ClientFeaturesDelta, Option), +} + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Default, Deserialize, utoipa::ToSchema)] pub enum TokenValidationStatus { Invalid, diff --git a/server/src/urls.rs b/server/src/urls.rs index 7315a051..12336c4c 100644 --- a/server/src/urls.rs +++ b/server/src/urls.rs @@ -10,6 +10,7 @@ pub struct UnleashUrls { pub api_url: Url, pub client_api_url: Url, pub client_features_url: Url, + pub client_features_delta_url: Url, pub client_register_app_url: Url, pub client_metrics_url: Url, pub client_bulk_metrics_url: Url, @@ -50,6 +51,11 @@ impl UnleashUrls { .path_segments_mut() .unwrap() .push("features"); + let mut client_features_delta_url = client_api_url.clone(); + client_features_delta_url + .path_segments_mut() + .unwrap() + .push("delta"); let mut client_features_stream_url = client_api_url.clone(); client_features_stream_url .path_segments_mut() @@ -99,6 +105,7 @@ impl UnleashUrls { api_url, client_api_url, client_features_url, + client_features_delta_url, client_register_app_url, client_bulk_metrics_url, client_metrics_url, @@ -116,19 +123,21 @@ mod tests { use super::*; use test_case::test_case; - #[test_case("https://app.unleash-hosted.com/demo", "https://app.unleash-hosted.com/demo/api", "https://app.unleash-hosted.com/demo/api/client", "https://app.unleash-hosted.com/demo/api/client/features" ; "No trailing slash, https protocol")] - #[test_case("https://app.unleash-hosted.com/demo/", "https://app.unleash-hosted.com/demo/api", "https://app.unleash-hosted.com/demo/api/client", "https://app.unleash-hosted.com/demo/api/client/features" ; "One trailing slash, https protocol")] - #[test_case("http://app.unleash-hosted.com/demo/", "http://app.unleash-hosted.com/demo/api", "http://app.unleash-hosted.com/demo/api/client", "http://app.unleash-hosted.com/demo/api/client/features" ; "One trailing slash, http protocol")] - #[test_case("http://app.unleash-hosted.com/", "http://app.unleash-hosted.com/api", "http://app.unleash-hosted.com/api/client", "http://app.unleash-hosted.com/api/client/features" ; "One trailing slash, no subpath, http protocol")] + #[test_case("https://app.unleash-hosted.com/demo", "https://app.unleash-hosted.com/demo/api", "https://app.unleash-hosted.com/demo/api/client", "https://app.unleash-hosted.com/demo/api/client/features", "https://app.unleash-hosted.com/demo/api/client/delta" ; "No trailing slash, https protocol")] + #[test_case("https://app.unleash-hosted.com/demo/", "https://app.unleash-hosted.com/demo/api", "https://app.unleash-hosted.com/demo/api/client", "https://app.unleash-hosted.com/demo/api/client/features", "https://app.unleash-hosted.com/demo/api/client/delta" ; "One trailing slash, https protocol")] + #[test_case("http://app.unleash-hosted.com/demo/", "http://app.unleash-hosted.com/demo/api", "http://app.unleash-hosted.com/demo/api/client", "http://app.unleash-hosted.com/demo/api/client/features", "http://app.unleash-hosted.com/demo/api/client/delta" ; "One trailing slash, http protocol")] + #[test_case("http://app.unleash-hosted.com/", "http://app.unleash-hosted.com/api", "http://app.unleash-hosted.com/api/client", "http://app.unleash-hosted.com/api/client/features", "http://app.unleash-hosted.com/api/client/delta" ; "One trailing slash, no subpath, http protocol")] pub fn can_handle_base_urls( base_url: &str, api_url: &str, client_url: &str, client_features_url: &str, + client_features_delta_url: &str, ) { let urls = UnleashUrls::from_str(base_url).unwrap(); assert_eq!(urls.api_url.to_string(), api_url); assert_eq!(urls.client_api_url.to_string(), client_url); assert_eq!(urls.client_features_url.to_string(), client_features_url); + assert_eq!(urls.client_features_delta_url.to_string(), client_features_delta_url); } }