-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: delta api implementation #626
Changes from all commits
433287e
632fcce
e0626b2
dd8faa6
40a2fc4
070d4a8
6ef1ae0
48ae957
358ad4f
4e36715
ca97225
1dc74b8
0e61ea0
47ecc06
23f37d6
e0b78f9
8a625b5
e57a6ed
f2edcbf
b973bf2
efc7add
5e01e91
de9d205
0d93dab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -116,3 +116,6 @@ tracing-test = "0.2.5" | |
|
||
[build-dependencies] | ||
shadow-rs = "0.37.0" | ||
|
||
[features] | ||
delta = [] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Either add #[cfg(feature = "delta")] here, or remove it from the tests part. |
||
async fn handle_client_features_delta_updated( | ||
&self, | ||
refresh_token: &EdgeToken, | ||
delta: ClientFeaturesDelta, | ||
etag: Option<EntityTag>, | ||
) { | ||
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")] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since you're always adding the impl part, I'm not sure if there's much value in using a feature flag just for tests. |
||
#[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<FeatureCache> = Arc::new(FeatureCache::default()); | ||
let engine_cache: Arc<DashMap<String, EngineState>> = 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<String>) -> 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 | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.