Skip to content

Commit

Permalink
fix: include strategy variant stickiness (#403)
Browse files Browse the repository at this point in the history
  • Loading branch information
sighphyre authored Jan 24, 2024
1 parent fab4871 commit 706990d
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 23 deletions.
36 changes: 32 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ tokio = { version = "1.35.1", features = [
tracing = { version = "0.1.40", features = ["log"] }
tracing-subscriber = { version = "0.3.18", features = ["json", "env-filter"] }
ulid = "1.1.0"
unleash-types = { version = "0.10", features = ["openapi", "hashes"] }
unleash-yggdrasil = { version = "0.8.0" }
unleash-types = { version = "0.11", features = ["openapi", "hashes"] }
unleash-yggdrasil = { version = "0.9.0" }
utoipa = { version = "4.2.0", features = ["actix_extras", "chrono"] }
utoipa-swagger-ui = { version = "6", features = ["actix-web"] }
[dev-dependencies]
Expand Down
10 changes: 8 additions & 2 deletions server/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,14 @@ async fn hydrate_from_persistent_storage(
tracing::debug!("Hydrating features for {key:?}");
features_cache.insert(key.clone(), features.clone());
let mut engine_state = EngineState::default();
engine_state.take_state(features);
engine_cache.insert(key, engine_state);

if engine_state.take_state(features).is_err() {
tracing::warn!(
"Loaded an invalid state from persistent storage, bootstrapping has failed"
);
} else {
engine_cache.insert(key, engine_state);
}
}

for target in refresh_targets {
Expand Down
57 changes: 55 additions & 2 deletions server/src/client_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,9 @@ mod tests {
use maplit::hashmap;
use reqwest::StatusCode;
use ulid::Ulid;
use unleash_types::client_features::{ClientFeature, Constraint, Operator, Strategy};
use unleash_types::client_features::{
ClientFeature, Constraint, Operator, Strategy, StrategyVariant,
};
use unleash_types::client_metrics::{
ClientMetricsEnv, ConnectViaBuilder, MetricBucket, ToggleStats,
};
Expand Down Expand Up @@ -431,7 +433,12 @@ mod tests {
project: Some("default".into()),
strategies: Some(vec![
Strategy {
variants: None,
variants: Some(vec![StrategyVariant {
name: "test".into(),
payload: None,
weight: 7,
stickiness: Some("sticky-on-something".into()),
}]),
name: "standard".into(),
sort_order: Some(500),
segments: None,
Expand Down Expand Up @@ -507,6 +514,52 @@ mod tests {
}
}

#[tokio::test]
async fn response_includes_variant_stickiness_for_strategy_variants() {
let features_cache: Arc<DashMap<String, ClientFeatures>> = Arc::new(DashMap::default());
let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
let app = test::init_service(
App::new()
.app_data(Data::from(features_cache.clone()))
.app_data(Data::from(token_cache.clone()))
.service(web::scope("/api/client").service(get_features)),
)
.await;

features_cache.insert("production".into(), cached_client_features());
let mut production_token = EdgeToken::try_from(
"*:production.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7".to_string(),
)
.unwrap();
production_token.token_type = Some(TokenType::Client);
production_token.status = TokenValidationStatus::Validated;
token_cache.insert(production_token.token.clone(), production_token.clone());
let req = make_features_request_with_token(production_token.clone()).await;
let res: ClientFeatures = test::call_and_read_body_json(&app, req).await;

assert_eq!(res.features.len(), cached_client_features().features.len());
let strategy_variant_stickiness = res
.features
.iter()
.find(|f| f.name == "feature_one")
.unwrap()
.strategies
.clone()
.unwrap()
.iter()
.find(|s| s.name == "standard")
.unwrap()
.variants
.clone()
.unwrap()
.iter()
.find(|v| v.name == "test")
.unwrap()
.stickiness
.clone();
assert!(strategy_variant_stickiness.is_some());
}

#[tokio::test]
async fn register_endpoint_correctly_aggregates_applications() {
let metrics_cache = Arc::new(MetricsCache::default());
Expand Down
25 changes: 15 additions & 10 deletions server/src/http/feature_refresher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use actix_web::http::header::EntityTag;
use chrono::Utc;
use dashmap::DashMap;
use reqwest::StatusCode;
use tracing::{debug, info};
use tracing::{debug, error, info};
use unleash_types::client_features::Segment;
use unleash_types::client_metrics::ClientApplication;
use unleash_types::{
Expand Down Expand Up @@ -353,13 +353,18 @@ impl FeatureRefresher {
.and_modify(|engine| {
if let Some(f) = self.features_cache.get(&key) {
let mut new_state = EngineState::default();
new_state.take_state(f.clone());
*engine = new_state;
if new_state.take_state(f.clone()).is_err() {
error!("Received a response from Unleash that cannot be compiled, this is likely a bug in Edge. Features will not be updated")
} else {
*engine = new_state;
}
}
})
.or_insert_with(|| {
let mut new_state = EngineState::default();
new_state.take_state(features);
if new_state.take_state(features).is_err() {
error!("Received a response from Unleash that cannot be compiled, this is likely a bug in Edge. Features will not be updated")
}
new_state
});
}
Expand Down Expand Up @@ -912,7 +917,7 @@ mod tests {
let example_features = features_from_disk("../examples/features.json");
let cache_key = cache_key(&token);
let mut engine_state = EngineState::default();
engine_state.take_state(example_features.clone());
engine_state.take_state(example_features.clone()).unwrap();
upstream_features_cache.insert(cache_key.clone(), example_features.clone());
upstream_engine_cache.insert(cache_key.clone(), engine_state);
let mut server = client_api_test_server(
Expand Down Expand Up @@ -968,7 +973,7 @@ mod tests {
let example_features = features_from_disk("../examples/features.json");
let cache_key = cache_key(&valid_token);
let mut engine_state = EngineState::default();
engine_state.take_state(example_features.clone());
engine_state.take_state(example_features.clone()).unwrap();
upstream_features_cache.insert(cache_key.clone(), example_features.clone());
upstream_engine_cache.insert(cache_key.clone(), engine_state);
let server = client_api_test_server(
Expand Down Expand Up @@ -1013,7 +1018,7 @@ mod tests {
let example_features = features_from_disk("../examples/hostedexample.json");
let cache_key = cache_key(&dx_token);
let mut engine_state = EngineState::default();
engine_state.take_state(example_features.clone());
engine_state.take_state(example_features.clone()).unwrap();
upstream_features_cache.insert(cache_key.clone(), example_features.clone());
upstream_engine_cache.insert(cache_key.clone(), engine_state);
let server = client_api_test_server(
Expand Down Expand Up @@ -1062,7 +1067,7 @@ mod tests {
let cache_key = cache_key(&dx_token);
upstream_features_cache.insert(cache_key.clone(), example_features.clone());
let mut engine_state = EngineState::default();
engine_state.take_state(example_features.clone());
engine_state.take_state(example_features.clone()).unwrap();
upstream_engine_cache.insert(cache_key, engine_state);
let server = client_api_test_server(
upstream_token_cache,
Expand Down Expand Up @@ -1123,7 +1128,7 @@ mod tests {
let cache_key = cache_key(&dx_token);
upstream_features_cache.insert(cache_key.clone(), example_features.clone());
let mut engine_state = EngineState::default();
engine_state.take_state(example_features.clone());
engine_state.take_state(example_features.clone()).unwrap();
upstream_engine_cache.insert(cache_key, engine_state);
let server = client_api_test_server(
upstream_token_cache,
Expand Down Expand Up @@ -1230,7 +1235,7 @@ mod tests {
let cache_key = cache_key(&eg_token);
upstream_features_cache.insert(cache_key.clone(), example_features.clone());
let mut engine_state = EngineState::default();
engine_state.take_state(example_features.clone());
engine_state.take_state(example_features.clone()).unwrap();
upstream_engine_cache.insert(cache_key.clone(), engine_state);
let server = client_api_test_server(
upstream_token_cache,
Expand Down
2 changes: 1 addition & 1 deletion server/src/middleware/client_token_from_frontend_token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ mod tests {
upstream_features_cache.insert(client_token.environment.clone().unwrap(), features.clone());
let upstream_engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
let mut engine = EngineState::default();
engine.take_state(features.clone());
engine.take_state(features.clone()).unwrap();
upstream_engine_cache.insert(client_token.token.clone(), engine);
let upstream_server = upstream_server(
upstream_token_cache.clone(),
Expand Down
7 changes: 5 additions & 2 deletions server/src/offline/offline_hotload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,11 @@ pub(crate) fn load_offline_engine_cache(
client_features.clone(),
);
let mut engine_state = EngineState::default();
engine_state.take_state(client_features);
engine_cache.insert(crate::tokens::cache_key(edge_token), engine_state);
if engine_state.take_state(client_features).is_err() {
tracing::warn!("Loaded an invalid feature set from file, likely due to a manual edit, reverting to previous state");
} else {
engine_cache.insert(crate::tokens::cache_key(edge_token), engine_state);
}
}

#[derive(Deserialize)]
Expand Down

0 comments on commit 706990d

Please sign in to comment.