From f118eebc7dbf2c1ba150eaeb8c4b2616eed8d617 Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Wed, 17 Sep 2025 17:10:23 +0200 Subject: [PATCH 01/18] add basic support for sticky assignments --- .../confidence/flags/resolver/v1/api.proto | 45 +++++ confidence-resolver/src/lib.rs | 175 ++++++++++++++---- wasm/proto/resolver/api.proto | 45 +++++ wasm/rust-guest/src/lib.rs | 15 +- 4 files changed, 242 insertions(+), 38 deletions(-) diff --git a/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto b/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto index 65a00b8..e4d0aa8 100644 --- a/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto +++ b/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto @@ -90,6 +90,49 @@ message ResolveFlagsRequest { Sdk sdk = 5 [ (google.api.field_behavior) = OPTIONAL ]; + + // if the resolver should handle sticky assignments + bool process_sticky = 6; + + // Context about the materialization required for the resolve + MaterializationContext materialization_context = 7; +} + +message MaterializationContext { + map unit_materialization_info = 1; +} + +message MaterializationInfo { + bool unit_in_info = 1; + map rule_to_variant = 2; +} + +message ResolveFlagResponseResult { + oneof resolve_result { + ResolveFlagsResponse response = 1; + MissingMaterializations missing_materializations = 2; + } +} + +message MissingMaterializations { + repeated MissingMaterialization items = 1; + repeated MaterializationUpdate updates = 4; +} + +message MissingMaterialization { + string unit = 1; + map materialization_to_rules = 2; + + message MissingRules { + repeated string rules = 1; + } +} + +message MaterializationUpdate { + string unit = 1; + string write_materialization = 2; + string rule = 3; + string variant = 4; } message ResolveFlagsResponse { @@ -103,6 +146,8 @@ message ResolveFlagsResponse { // Unique identifier for this particular resolve request. string resolve_id = 3; + + repeated MaterializationUpdate updates = 4; } message ApplyFlagsRequest { diff --git a/confidence-resolver/src/lib.rs b/confidence-resolver/src/lib.rs index 5309ba5..7cfcddb 100644 --- a/confidence-resolver/src/lib.rs +++ b/confidence-resolver/src/lib.rs @@ -40,6 +40,9 @@ use proto::confidence::iam::v1 as iam; use proto::google::{value::Kind, Struct, Timestamp, Value}; use proto::Message; +use crate::confidence::flags::resolver::v1::resolve_flag_response_result::ResolveResult; +use crate::confidence::flags::resolver::v1::{MaterializationUpdate, ResolveFlagResponseResult}; +use confidence::flags::types::v1 as flags_types; use flags_admin::flag::rule; use flags_admin::flag::{Rule, Variant}; use flags_admin::Flag; @@ -405,10 +408,10 @@ impl<'a, H: Host> AccountResolver<'a, H> { } } - pub fn resolve_flags( + pub fn resolve_flags_sticky( &self, request: &flags_resolver::ResolveFlagsRequest, - ) -> Result { + ) -> Result { let timestamp = H::current_time(); let flag_names = &request.flags; @@ -434,7 +437,7 @@ impl<'a, H: Host> AccountResolver<'a, H> { } } - let resolved_values = flags_to_resolve + let resolve_results = flags_to_resolve .iter() .map(|flag| { self.resolve_flag(flag) @@ -442,13 +445,21 @@ impl<'a, H: Host> AccountResolver<'a, H> { }) .collect::, _>>()?; + let resolved_values: Vec<&ResolvedValue> = + resolve_results.iter().map(|r| &r.resolved_value).collect(); + let resolve_id = H::random_alphanumeric(32); let mut response = flags_resolver::ResolveFlagsResponse { resolve_id: resolve_id.clone(), ..Default::default() }; for resolved_value in &resolved_values { - response.resolved_flags.push(resolved_value.into()); + response.resolved_flags.push((*resolved_value).into()); + } + + // Collect all materialization updates from all resolve results + for resolve_result in &resolve_results { + response.updates.extend(resolve_result.updates.clone()); } if request.apply { @@ -456,7 +467,7 @@ impl<'a, H: Host> AccountResolver<'a, H> { .iter() .filter(|v| v.should_apply) .map(|v| FlagToApply { - assigned_flag: v.into(), + assigned_flag: (*v).into(), skew_adjusted_applied_time: timestamp.clone(), }) .collect(); @@ -476,7 +487,7 @@ impl<'a, H: Host> AccountResolver<'a, H> { ..Default::default() }; for resolved_value in &resolved_values { - let assigned_flag: AssignedFlag = resolved_value.into(); + let assigned_flag: AssignedFlag = (*resolved_value).into(); resolve_token_v1 .assignments .insert(assigned_flag.flag.clone(), assigned_flag); @@ -495,15 +506,47 @@ impl<'a, H: Host> AccountResolver<'a, H> { response.resolve_token = encrypted_token; } + let owned_values: Vec = + resolved_values.iter().map(|v| (*v).clone()).collect(); H::log_resolve( &resolve_id, &self.evaluation_context.context, - resolved_values.as_slice(), + owned_values.as_slice(), self.client, &request.sdk, ); - Ok(response) + Ok(ResolveFlagResponseResult { + resolve_result: Some(ResolveResult::Response(response)), + }) + } + + pub fn resolve_flags( + &self, + request: &flags_resolver::ResolveFlagsRequest, + ) -> Result { + let response = self.resolve_flags_sticky(&flags_resolver::ResolveFlagsRequest { + flags: request.flags.clone(), + sdk: request.sdk.clone(), + evaluation_context: request.evaluation_context.clone(), + client_secret: request.client_secret.clone(), + apply: request.apply.clone(), + materialization_context: None, + process_sticky: false, + }); + + match response { + Ok(v) => match v.resolve_result { + None => Err("failed to resolve flags".to_string()), + Some(r) => match r { + ResolveResult::Response(flags_response) => Ok(flags_response), + ResolveResult::MissingMaterializations(_) => { + Err("Sticky assignment is not supported".to_string()) + } + }, + }, + Err(e) => Err(e), + } } pub fn apply_flags(&self, request: &flags_resolver::ApplyFlagsRequest) -> Result<(), String> { @@ -569,7 +612,7 @@ impl<'a, H: Host> AccountResolver<'a, H> { _ => Err("TargetingKeyError".to_string()), } } - pub fn resolve_flag_name(&'a self, flag_name: &str) -> Result, String> { + pub fn resolve_flag_name(&'a self, flag_name: &str) -> Result, String> { self.state .flags .get(flag_name) @@ -577,11 +620,15 @@ impl<'a, H: Host> AccountResolver<'a, H> { .and_then(|flag| self.resolve_flag(flag)) } - pub fn resolve_flag(&'a self, flag: &'a Flag) -> Result, String> { + pub fn resolve_flag(&'a self, flag: &'a Flag) -> Result, String> { + let mut updates: Vec = Vec::new(); let mut resolved_value = ResolvedValue::new(flag); if flag.state == flags_admin::flag::State::Archived as i32 { - return Ok(resolved_value.error(ResolveReason::FlagArchived)); + return Ok(FlagResolveResult { + resolved_value: resolved_value.error(ResolveReason::FlagArchived), + updates: vec![], + }); } for rule in &flag.rules { @@ -604,7 +651,12 @@ impl<'a, H: Host> AccountResolver<'a, H> { let unit: String = match self.get_targeting_key(targeting_key) { Ok(Some(u)) => u, Ok(None) => continue, - Err(_) => return Ok(resolved_value.error(ResolveReason::TargetingKeyError)), + Err(_) => { + return Ok(FlagResolveResult { + resolved_value: resolved_value.error(ResolveReason::TargetingKeyError), + updates: vec![], + }) + } }; if !self.segment_match(segment, &unit)? { @@ -627,10 +679,34 @@ impl<'a, H: Host> AccountResolver<'a, H> { .any(|range| range.lower <= bucket && bucket < range.upper) }); + let has_write_spec = match &rule.materialization_spec { + Some(materialization_spec) => Some(&materialization_spec.write_materialization), + None => None, + }; + if let Some(assignment) = matched_assignment { let Some(a) = &assignment.assignment else { continue; }; + + // Extract variant name from assignment if it's a variant assignment + let variant_name = match a { + rule::assignment::Assignment::Variant(ref variant_assignment) => { + variant_assignment.variant.clone() + } + _ => "".to_string(), + }; + + // write the materialization info if write spec exists + if let Some(write_spec) = has_write_spec { + updates.push(MaterializationUpdate { + write_materialization: write_spec.to_string(), + unit: unit.to_string(), + rule: rule.clone().name, + variant: variant_name, + }) + } + match a { rule::assignment::Assignment::Fallthrough(_) => { resolved_value.attribute_fallthrough_rule( @@ -641,12 +717,15 @@ impl<'a, H: Host> AccountResolver<'a, H> { continue; } rule::assignment::Assignment::ClientDefault(_) => { - return Ok(resolved_value.with_client_default_match( - rule, - segment, - &assignment.assignment_id, - &unit, - )) + return Ok(FlagResolveResult { + resolved_value: resolved_value.with_client_default_match( + rule, + segment, + &assignment.assignment_id, + &unit, + ), + updates, + }) } rule::assignment::Assignment::Variant( rule::assignment::VariantAssignment { @@ -659,13 +738,16 @@ impl<'a, H: Host> AccountResolver<'a, H> { .find(|variant| variant.name == *variant_name) .or_fail()?; - return Ok(resolved_value.with_variant_match( - rule, - segment, - variant, - &assignment.assignment_id, - &unit, - )); + return Ok(FlagResolveResult { + resolved_value: resolved_value.with_variant_match( + rule, + segment, + variant, + &assignment.assignment_id, + &unit, + ), + updates, + }); } }; } @@ -677,7 +759,10 @@ impl<'a, H: Host> AccountResolver<'a, H> { resolved_value.should_apply = !resolved_value.fallthrough_rules.is_empty(); } - Ok(resolved_value) + Ok(FlagResolveResult { + resolved_value, + updates, + }) } /// Get an attribute value from the [EvaluationContext] struct, adressed by a path specification. @@ -848,7 +933,7 @@ fn list_wrapper(value: &targeting::value::Value) -> targeting::ListValue { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct ResolvedValue<'a> { pub flag: &'a Flag, pub reason: ResolveReason, @@ -857,6 +942,12 @@ pub struct ResolvedValue<'a> { pub should_apply: bool, } +#[derive(Debug)] +pub struct FlagResolveResult<'a> { + pub resolved_value: ResolvedValue<'a>, + pub updates: Vec, +} + impl<'a> ResolvedValue<'a> { fn new(flag: &'a Flag) -> Self { ResolvedValue { @@ -1000,7 +1091,7 @@ impl<'a> From<&ResolvedValue<'a>> for flags_resolver::resolve_token_v1::Assigned } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct AssignmentMatch<'a> { pub rule: &'a Rule, pub segment: &'a Segment, @@ -1017,7 +1108,7 @@ pub struct FallthroughRule<'a> { } // note that the ordinal values are set to match the corresponding protobuf enum -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ResolveReason { // The flag was successfully resolved because one rule matched. Match = 1, @@ -1156,8 +1247,9 @@ mod tests { .get_resolver_with_json_context(SECRET, context_json, &ENCRYPTION_KEY) .unwrap(); let flag = resolver.state.flags.get("flags/tutorial-feature").unwrap(); - let resolved_value = resolver.resolve_flag(flag).unwrap(); - let assignment_match = resolved_value.assignment_match.unwrap(); + let resolve_result = resolver.resolve_flag(flag).unwrap(); + let resolved_value = &resolve_result.resolved_value; + let assignment_match = resolved_value.assignment_match.as_ref().unwrap(); assert_eq!( assignment_match.rule.name, @@ -1179,6 +1271,7 @@ mod tests { let assignment_match = resolver .resolve_flag(flag) .unwrap() + .resolved_value .assignment_match .unwrap(); @@ -1209,6 +1302,8 @@ mod tests { let resolve_flag_req = flags_resolver::ResolveFlagsRequest { evaluation_context: Some(Struct::default()), client_secret: SECRET.to_string(), + process_sticky: false, + materialization_context: None, flags: vec!["flags/tutorial-feature".to_string()], apply: false, sdk: Some(Sdk { @@ -1275,6 +1370,8 @@ mod tests { evaluation_context: Some(Struct::default()), client_secret: SECRET.to_string(), flags: vec!["flags/fallthrough-test-1".to_string()], + process_sticky: false, + materialization_context: None, apply: false, sdk: Some(Sdk { sdk: None, @@ -1331,6 +1428,8 @@ mod tests { evaluation_context: Some(Struct::default()), client_secret: SECRET.to_string(), flags: vec!["flags/fallthrough-test-2".to_string()], + process_sticky: false, + materialization_context: None, apply: false, sdk: Some(Sdk { sdk: None, @@ -1401,6 +1500,8 @@ mod tests { evaluation_context: Some(Struct::default()), client_secret: SECRET.to_string(), flags: vec!["flags/tutorial-feature".to_string()], + process_sticky: false, + materialization_context: None, apply: false, sdk: Some(Sdk { sdk: None, @@ -1495,6 +1596,8 @@ mod tests { evaluation_context: Some(Struct::default()), client_secret: SECRET.to_string(), flags: vec!["flags/tutorial-feature".to_string()], + process_sticky: false, + materialization_context: None, apply: true, sdk: Some(Sdk { sdk: None, @@ -1528,6 +1631,8 @@ mod tests { evaluation_context: Some(Struct::default()), client_secret: SECRET.to_string(), flags: vec!["flags/tutorial-feature".to_string()], + process_sticky: false, + materialization_context: None, apply: true, sdk: Some(Sdk { sdk: None, @@ -1573,10 +1678,11 @@ mod tests { .flags .get("flags/fallthrough-test-2") .unwrap(); - let resolved_value = resolver.resolve_flag(flag).unwrap(); + let resolve_result = resolver.resolve_flag(flag).unwrap(); + let resolved_value = &resolve_result.resolved_value; assert_eq!(resolved_value.reason as i32, ResolveReason::Match as i32); - let assignment_match = resolved_value.assignment_match.unwrap(); + let assignment_match = resolved_value.assignment_match.as_ref().unwrap(); assert_eq!(assignment_match.targeting_key, "26"); } @@ -1599,7 +1705,8 @@ mod tests { .flags .get("flags/fallthrough-test-2") .unwrap(); - let resolved_value = resolver.resolve_flag(flag).unwrap(); + let resolve_result = resolver.resolve_flag(flag).unwrap(); + let resolved_value = &resolve_result.resolved_value; assert_eq!( resolved_value.reason as i32, diff --git a/wasm/proto/resolver/api.proto b/wasm/proto/resolver/api.proto index a84e52d..eb5d7c5 100644 --- a/wasm/proto/resolver/api.proto +++ b/wasm/proto/resolver/api.proto @@ -37,8 +37,16 @@ message ResolveFlagsRequest { // Information about the SDK used to initiate the request. // Sdk sdk = 5; + + // if the resolver should handle sticky assignments + bool process_sticky = 6; + + // Context about the materialization required for the resolve + MaterializationContext materialization_context = 7; } + + message ResolveFlagsResponse { // The list of all flags that could be resolved. Note: if any flag was // archived it will not be included in this list. @@ -50,6 +58,43 @@ message ResolveFlagsResponse { // Unique identifier for this particular resolve request. string resolve_id = 3; + repeated MaterializationUpdate updates = 4; +} + +message MaterializationContext { + map unit_materialization_info = 1; +} + +message MaterializationInfo { + bool unit_in_info = 1; + map rule_to_variant = 2; +} + +message ResolveFlagResponseResult { + oneof result { + ResolveFlagsResponse response = 1; + MissingMaterializations missing_materializations = 2; + } +} + +message MissingMaterializations { + repeated MissingMaterialization items = 1; +} + +message MissingMaterialization { + string unit = 1; + map materialization_to_rules = 2; + + message MissingRules { + repeated string rules = 1; + } +} + +message MaterializationUpdate { + string unit = 1; + string write_materialization = 2; + string rule = 3; + string variant = 4; } diff --git a/wasm/rust-guest/src/lib.rs b/wasm/rust-guest/src/lib.rs index 478b91e..d46532d 100644 --- a/wasm/rust-guest/src/lib.rs +++ b/wasm/rust-guest/src/lib.rs @@ -25,12 +25,12 @@ pub mod proto { include!(concat!(env!("OUT_DIR"), "/rust_guest.rs")); } use crate::proto::SetResolverStateRequest; +use confidence_resolver::confidence::flags::resolver::v1::resolve_flag_response_result; use confidence_resolver::{ proto::{ confidence::flags::admin::v1::ResolverState as ResolverStatePb, confidence::flags::resolver::v1::{ - ResolveFlagsRequest, ResolveFlagsResponse, ResolvedFlag, Sdk, - }, + ResolveFlagResponseResult, ResolveFlagsRequest, ResolveFlagsResponse, ResolvedFlag, Sdk,}, google::{Struct, Timestamp}, }, Client, FlagToApply, Host, ResolveReason, ResolvedValue, ResolverState, @@ -184,6 +184,13 @@ wasm_msg_guest! { Ok(VOID) } + fn resolve_with_sticky(request: ResolveFlagsRequest) -> WasmResult { + let resolver_state = get_resolver_state()?; + let evaluation_context = request.evaluation_context.as_ref().cloned().unwrap_or_default(); + let resolver = resolver_state.get_resolver::(&request.client_secret, evaluation_context, &ENCRYPTION_KEY)?; + resolver.resolve_flags_sticky(&request).into() + } + fn resolve(request: ResolveFlagsRequest) -> WasmResult { let resolver_state = get_resolver_state()?; let evaluation_context = request.evaluation_context.as_ref().cloned().unwrap_or_default(); @@ -194,8 +201,8 @@ wasm_msg_guest! { let resolver_state = get_resolver_state()?; let evaluation_context = request.evaluation_context.as_ref().cloned().unwrap_or_default(); let resolver = resolver_state.get_resolver::(&request.client_secret, evaluation_context, &ENCRYPTION_KEY).unwrap(); - let resolved_value = resolver.resolve_flag_name(&request.name)?; - Ok((&resolved_value).into()) + let resolve_result = resolver.resolve_flag_name(&request.name)?; + Ok((&resolve_result.resolved_value).into()) } fn flush_logs(_request:Void) -> WasmResult { LOGGER.checkpoint().map_err(|e| e.into()) From d9c92138fb16763f4c23a4691c77f404969e3312 Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Thu, 18 Sep 2025 12:05:23 +0200 Subject: [PATCH 02/18] more sticky --- .../confidence/flags/resolver/v1/api.proto | 10 ++-- confidence-resolver/src/lib.rs | 59 ++++++++++++++++--- wasm/proto/resolver/api.proto | 11 ++-- 3 files changed, 61 insertions(+), 19 deletions(-) diff --git a/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto b/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto index e4d0aa8..ea79429 100644 --- a/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto +++ b/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto @@ -115,16 +115,16 @@ message ResolveFlagResponseResult { } message MissingMaterializations { - repeated MissingMaterialization items = 1; + repeated MissingMaterializationsForUnit items = 1; repeated MaterializationUpdate updates = 4; } -message MissingMaterialization { +message MissingMaterializationsForUnit { string unit = 1; - map materialization_to_rules = 2; + map materializations_of_rules = 2; - message MissingRules { - repeated string rules = 1; + message Materializations { + repeated string materializations = 1; } } diff --git a/confidence-resolver/src/lib.rs b/confidence-resolver/src/lib.rs index 7cfcddb..18ee3d7 100644 --- a/confidence-resolver/src/lib.rs +++ b/confidence-resolver/src/lib.rs @@ -41,7 +41,7 @@ use proto::google::{value::Kind, Struct, Timestamp, Value}; use proto::Message; use crate::confidence::flags::resolver::v1::resolve_flag_response_result::ResolveResult; -use crate::confidence::flags::resolver::v1::{MaterializationUpdate, ResolveFlagResponseResult}; +use crate::confidence::flags::resolver::v1::{MaterializationContext, MaterializationUpdate, MissingMaterializations, MissingMaterializationsForUnit, ResolveFlagResponseResult}; use confidence::flags::types::v1 as flags_types; use flags_admin::flag::rule; use flags_admin::flag::{Rule, Variant}; @@ -392,6 +392,40 @@ pub struct AccountResolver<'a, H: Host> { host: PhantomData, } +struct ResolveMaterializationContext { + context: MaterializationContext, + process_sticky: bool, + skip_on_not_missing: bool, +} + +#[derive(Debug)] +pub struct ResolveFlagError { + pub message: String, + pub missing_materializations: Vec +} + +impl ResolveFlagError { + pub fn err(message: &str) -> ResolveFlagError { + ResolveFlagError { + message: message.to_string(), + missing_materializations: vec![], + } + } + + pub fn missing_materializations(items: Vec) -> ResolveFlagError { + ResolveFlagError { + message: "Processing sticky assignments, missing materializations from the store".to_string(), + missing_materializations: items + } + } +} + +impl From for ResolveFlagError { + fn from(value: ErrorCode) -> Self { + ResolveFlagError::err(format!("error code {}", &value.to_string()).as_str()) + } +} + impl<'a, H: Host> AccountResolver<'a, H> { pub fn new( client: &'a Client, @@ -441,7 +475,7 @@ impl<'a, H: Host> AccountResolver<'a, H> { .iter() .map(|flag| { self.resolve_flag(flag) - .map_err(|e| format!("{}: {}", flag.name, e)) + .map_err(|e| format!("{}: {}", flag.name, e.message)) }) .collect::, _>>()?; @@ -524,7 +558,7 @@ impl<'a, H: Host> AccountResolver<'a, H> { pub fn resolve_flags( &self, request: &flags_resolver::ResolveFlagsRequest, - ) -> Result { + ) -> Result { let response = self.resolve_flags_sticky(&flags_resolver::ResolveFlagsRequest { flags: request.flags.clone(), sdk: request.sdk.clone(), @@ -537,15 +571,15 @@ impl<'a, H: Host> AccountResolver<'a, H> { match response { Ok(v) => match v.resolve_result { - None => Err("failed to resolve flags".to_string()), + None => Err(ResolveFlagError::err("failed to resolve flags")), Some(r) => match r { ResolveResult::Response(flags_response) => Ok(flags_response), ResolveResult::MissingMaterializations(_) => { - Err("Sticky assignment is not supported".to_string()) + Err(ResolveFlagError::err("sticky assignments is not supported")) } }, }, - Err(e) => Err(e), + Err(e) => Err(ResolveFlagError::err(e.as_str())), } } @@ -612,16 +646,17 @@ impl<'a, H: Host> AccountResolver<'a, H> { _ => Err("TargetingKeyError".to_string()), } } - pub fn resolve_flag_name(&'a self, flag_name: &str) -> Result, String> { + pub fn resolve_flag_name(&'a self, flag_name: &str) -> Result, ResolveFlagError> { self.state .flags .get(flag_name) - .ok_or("flag not found".to_string()) + .ok_or(ResolveFlagError::err("flag not found")) .and_then(|flag| self.resolve_flag(flag)) } - pub fn resolve_flag(&'a self, flag: &'a Flag) -> Result, String> { + pub fn resolve_flag(&'a self, flag: &'a Flag) -> Result, ResolveFlagError> { let mut updates: Vec = Vec::new(); + let mut missing_materializations: Vec = Vec::new(); let mut resolved_value = ResolvedValue::new(flag); if flag.state == flags_admin::flag::State::Archived as i32 { @@ -659,6 +694,12 @@ impl<'a, H: Host> AccountResolver<'a, H> { } }; + let mut materialization_matched = false; + + if let Some(materialization_spec) = &rule.materialization_spec { + let read_materialization = materialization_spec.read_materialization.clone(); + } + if !self.segment_match(segment, &unit)? { // ResolveReason::SEGMENT_NOT_MATCH continue; diff --git a/wasm/proto/resolver/api.proto b/wasm/proto/resolver/api.proto index eb5d7c5..3cfc382 100644 --- a/wasm/proto/resolver/api.proto +++ b/wasm/proto/resolver/api.proto @@ -78,15 +78,16 @@ message ResolveFlagResponseResult { } message MissingMaterializations { - repeated MissingMaterialization items = 1; + repeated MissingMaterializationsForUnit items = 1; + repeated MaterializationUpdate updates = 4; } -message MissingMaterialization { +message MissingMaterializationsForUnit { string unit = 1; - map materialization_to_rules = 2; + map materializations_of_rules = 2; - message MissingRules { - repeated string rules = 1; + message Materializations { + repeated string materializations = 1; } } From 60cbc3e9d4ac30657c3671629696dcbca3b9bdfb Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Thu, 18 Sep 2025 12:14:07 +0200 Subject: [PATCH 03/18] fixup! more sticky --- confidence-resolver/src/lib.rs | 53 ++++++++++++++++++++++++---------- wasm/rust-guest/src/lib.rs | 8 ++--- 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/confidence-resolver/src/lib.rs b/confidence-resolver/src/lib.rs index 18ee3d7..c7dd343 100644 --- a/confidence-resolver/src/lib.rs +++ b/confidence-resolver/src/lib.rs @@ -40,9 +40,6 @@ use proto::confidence::iam::v1 as iam; use proto::google::{value::Kind, Struct, Timestamp, Value}; use proto::Message; -use crate::confidence::flags::resolver::v1::resolve_flag_response_result::ResolveResult; -use crate::confidence::flags::resolver::v1::{MaterializationContext, MaterializationUpdate, MissingMaterializations, MissingMaterializationsForUnit, ResolveFlagResponseResult}; -use confidence::flags::types::v1 as flags_types; use flags_admin::flag::rule; use flags_admin::flag::{Rule, Variant}; use flags_admin::Flag; @@ -54,6 +51,11 @@ use flags_types::targeting::criterion; use flags_types::targeting::Criterion; use flags_types::Expression; use gzip::decompress_gz; +use proto::confidence::flags::resolver::v1::resolve_flag_response_result::ResolveResult; +use proto::confidence::flags::resolver::v1::{ + MaterializationContext, MaterializationUpdate, MissingMaterializations, + MissingMaterializationsForUnit, ResolveFlagResponseResult, +}; use crate::err::{ErrorCode, OrFailExt}; @@ -401,7 +403,7 @@ struct ResolveMaterializationContext { #[derive(Debug)] pub struct ResolveFlagError { pub message: String, - pub missing_materializations: Vec + pub missing_materializations: Vec, } impl ResolveFlagError { @@ -412,14 +414,23 @@ impl ResolveFlagError { } } - pub fn missing_materializations(items: Vec) -> ResolveFlagError { + pub fn missing_materializations( + items: Vec, + ) -> ResolveFlagError { ResolveFlagError { - message: "Processing sticky assignments, missing materializations from the store".to_string(), - missing_materializations: items + message: "Processing sticky assignments, missing materializations from the store" + .to_string(), + missing_materializations: items, } } } +impl From for String { + fn from(value: ResolveFlagError) -> Self { + value.message.to_string() + } +} + impl From for ResolveFlagError { fn from(value: ErrorCode) -> Self { ResolveFlagError::err(format!("error code {}", &value.to_string()).as_str()) @@ -445,7 +456,7 @@ impl<'a, H: Host> AccountResolver<'a, H> { pub fn resolve_flags_sticky( &self, request: &flags_resolver::ResolveFlagsRequest, - ) -> Result { + ) -> Result { let timestamp = H::current_time(); let flag_names = &request.flags; @@ -459,15 +470,17 @@ impl<'a, H: Host> AccountResolver<'a, H> { .collect::>(); if flags_to_resolve.len() > MAX_NO_OF_FLAGS_TO_BATCH_RESOLVE { - return Err(format!( + return Err(ResolveFlagError::err(format!( "max {} flags allowed in a single resolve request, this request would return {} flags.", MAX_NO_OF_FLAGS_TO_BATCH_RESOLVE, - flags_to_resolve.len())); + flags_to_resolve.len()).as_str())); } if let Ok(Some(unit)) = self.get_targeting_key(TARGETING_KEY) { if unit.len() > 100 { - return Err("Targeting key is too larger, max 100 characters.".to_string()); + return Err(ResolveFlagError::err( + "Targeting key is too larger, max 100 characters.", + )); } } @@ -477,7 +490,8 @@ impl<'a, H: Host> AccountResolver<'a, H> { self.resolve_flag(flag) .map_err(|e| format!("{}: {}", flag.name, e.message)) }) - .collect::, _>>()?; + .collect::, _>>() + .or_fail()?; let resolved_values: Vec<&ResolvedValue> = resolve_results.iter().map(|r| &r.resolved_value).collect(); @@ -535,7 +549,8 @@ impl<'a, H: Host> AccountResolver<'a, H> { let encrypted_token = self .encrypt_resolve_token(&resolve_token) - .map_err(|_| "Failed to encrypt resolve token".to_string())?; + .map_err(|_| "Failed to encrypt resolve token".to_string()) + .or_fail()?; response.resolve_token = encrypted_token; } @@ -579,7 +594,7 @@ impl<'a, H: Host> AccountResolver<'a, H> { } }, }, - Err(e) => Err(ResolveFlagError::err(e.as_str())), + Err(e) => Err(e), } } @@ -646,7 +661,10 @@ impl<'a, H: Host> AccountResolver<'a, H> { _ => Err("TargetingKeyError".to_string()), } } - pub fn resolve_flag_name(&'a self, flag_name: &str) -> Result, ResolveFlagError> { + pub fn resolve_flag_name( + &'a self, + flag_name: &str, + ) -> Result, ResolveFlagError> { self.state .flags .get(flag_name) @@ -654,7 +672,10 @@ impl<'a, H: Host> AccountResolver<'a, H> { .and_then(|flag| self.resolve_flag(flag)) } - pub fn resolve_flag(&'a self, flag: &'a Flag) -> Result, ResolveFlagError> { + pub fn resolve_flag( + &'a self, + flag: &'a Flag, + ) -> Result, ResolveFlagError> { let mut updates: Vec = Vec::new(); let mut missing_materializations: Vec = Vec::new(); let mut resolved_value = ResolvedValue::new(flag); diff --git a/wasm/rust-guest/src/lib.rs b/wasm/rust-guest/src/lib.rs index d46532d..c9f827c 100644 --- a/wasm/rust-guest/src/lib.rs +++ b/wasm/rust-guest/src/lib.rs @@ -25,12 +25,12 @@ pub mod proto { include!(concat!(env!("OUT_DIR"), "/rust_guest.rs")); } use crate::proto::SetResolverStateRequest; -use confidence_resolver::confidence::flags::resolver::v1::resolve_flag_response_result; use confidence_resolver::{ proto::{ confidence::flags::admin::v1::ResolverState as ResolverStatePb, confidence::flags::resolver::v1::{ - ResolveFlagResponseResult, ResolveFlagsRequest, ResolveFlagsResponse, ResolvedFlag, Sdk,}, + ResolveFlagResponseResult, ResolveFlagsRequest, ResolveFlagsResponse, ResolvedFlag, Sdk, + }, google::{Struct, Timestamp}, }, Client, FlagToApply, Host, ResolveReason, ResolvedValue, ResolverState, @@ -188,14 +188,14 @@ wasm_msg_guest! { let resolver_state = get_resolver_state()?; let evaluation_context = request.evaluation_context.as_ref().cloned().unwrap_or_default(); let resolver = resolver_state.get_resolver::(&request.client_secret, evaluation_context, &ENCRYPTION_KEY)?; - resolver.resolve_flags_sticky(&request).into() + resolver.resolve_flags_sticky(&request).map_err(|e| e.message).into() } fn resolve(request: ResolveFlagsRequest) -> WasmResult { let resolver_state = get_resolver_state()?; let evaluation_context = request.evaluation_context.as_ref().cloned().unwrap_or_default(); let resolver = resolver_state.get_resolver::(&request.client_secret, evaluation_context, &ENCRYPTION_KEY)?; - resolver.resolve_flags(&request).into() + resolver.resolve_flags(&request).map_err(|e| e.message).into() } fn resolve_simple(request: ResolveSimpleRequest) -> WasmResult { let resolver_state = get_resolver_state()?; From 329ab213305c6d34e5d211b102a69bad24e4cc92 Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Thu, 18 Sep 2025 15:59:40 +0200 Subject: [PATCH 04/18] cleanup --- .../confidence/flags/resolver/v1/api.proto | 11 +- confidence-resolver/src/lib.rs | 135 +++++++++++++----- wasm/proto/resolver/api.proto | 11 +- 3 files changed, 111 insertions(+), 46 deletions(-) diff --git a/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto b/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto index ea79429..d8e5d28 100644 --- a/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto +++ b/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto @@ -115,17 +115,14 @@ message ResolveFlagResponseResult { } message MissingMaterializations { - repeated MissingMaterializationsForUnit items = 1; + repeated MissingMaterializationItem items = 1; repeated MaterializationUpdate updates = 4; } -message MissingMaterializationsForUnit { +message MissingMaterializationItem { string unit = 1; - map materializations_of_rules = 2; - - message Materializations { - repeated string materializations = 1; - } + string rule = 2; + string read_materialization = 3; } message MaterializationUpdate { diff --git a/confidence-resolver/src/lib.rs b/confidence-resolver/src/lib.rs index c7dd343..b88358f 100644 --- a/confidence-resolver/src/lib.rs +++ b/confidence-resolver/src/lib.rs @@ -53,11 +53,14 @@ use flags_types::Expression; use gzip::decompress_gz; use proto::confidence::flags::resolver::v1::resolve_flag_response_result::ResolveResult; use proto::confidence::flags::resolver::v1::{ - MaterializationContext, MaterializationUpdate, MissingMaterializations, - MissingMaterializationsForUnit, ResolveFlagResponseResult, + MaterializationContext, MaterializationUpdate, ResolveFlagResponseResult, }; use crate::err::{ErrorCode, OrFailExt}; +use crate::proto::confidence::flags::resolver::v1::{ + MissingMaterializationItem, MissingMaterializations, +}; +use crate::ResolveWithStickyContext::NoStickyContext; impl TryFrom> for ResolverStatePb { type Error = ErrorCode; @@ -212,7 +215,7 @@ pub struct EvaluationContext { pub context: Struct, } pub struct FlagToApply { - pub assigned_flag: flags_resolver::resolve_token_v1::AssignedFlag, + pub assigned_flag: AssignedFlag, pub skew_adjusted_applied_time: Timestamp, } @@ -394,16 +397,20 @@ pub struct AccountResolver<'a, H: Host> { host: PhantomData, } -struct ResolveMaterializationContext { +pub enum ResolveWithStickyContext { + WithStickyContext(StickyResolveContext), + NoStickyContext, +} + +pub struct StickyResolveContext { context: MaterializationContext, - process_sticky: bool, skip_on_not_missing: bool, } #[derive(Debug)] pub struct ResolveFlagError { pub message: String, - pub missing_materializations: Vec, + pub missing_materializations: Vec, } impl ResolveFlagError { @@ -414,9 +421,7 @@ impl ResolveFlagError { } } - pub fn missing_materializations( - items: Vec, - ) -> ResolveFlagError { + pub fn missing_materializations(items: Vec) -> ResolveFlagError { ResolveFlagError { message: "Processing sticky assignments, missing materializations from the store" .to_string(), @@ -456,7 +461,7 @@ impl<'a, H: Host> AccountResolver<'a, H> { pub fn resolve_flags_sticky( &self, request: &flags_resolver::ResolveFlagsRequest, - ) -> Result { + ) -> Result { let timestamp = H::current_time(); let flag_names = &request.flags; @@ -470,28 +475,46 @@ impl<'a, H: Host> AccountResolver<'a, H> { .collect::>(); if flags_to_resolve.len() > MAX_NO_OF_FLAGS_TO_BATCH_RESOLVE { - return Err(ResolveFlagError::err(format!( + return Err(format!( "max {} flags allowed in a single resolve request, this request would return {} flags.", MAX_NO_OF_FLAGS_TO_BATCH_RESOLVE, - flags_to_resolve.len()).as_str())); + flags_to_resolve.len())); } if let Ok(Some(unit)) = self.get_targeting_key(TARGETING_KEY) { if unit.len() > 100 { - return Err(ResolveFlagError::err( - "Targeting key is too larger, max 100 characters.", - )); + return Err("Targeting key is too larger, max 100 characters.".to_string()); } } - let resolve_results = flags_to_resolve - .iter() - .map(|flag| { - self.resolve_flag(flag) - .map_err(|e| format!("{}: {}", flag.name, e.message)) - }) - .collect::, _>>() - .or_fail()?; + let mut resolve_results = Vec::with_capacity(flags_to_resolve.len()); + let mut missing_materialization_items: Vec = vec![]; + + for flag in flags_to_resolve { + let resolve_result = self.resolve_flag(flag, NoStickyContext); + match resolve_result { + Ok(resolve_result) => resolve_results.push(resolve_result), + + Err(err) => { + if err.missing_materializations.is_empty() { + return Err(err.message.to_string()); + } else { + missing_materialization_items.extend(err.missing_materializations); + } + } + } + } + + if !missing_materialization_items.is_empty() { + return Ok(ResolveFlagResponseResult { + resolve_result: Some(ResolveResult::MissingMaterializations( + MissingMaterializations { + items: missing_materialization_items, + updates: vec![], + }, + )), + }); + } let resolved_values: Vec<&ResolvedValue> = resolve_results.iter().map(|r| &r.resolved_value).collect(); @@ -573,7 +596,7 @@ impl<'a, H: Host> AccountResolver<'a, H> { pub fn resolve_flags( &self, request: &flags_resolver::ResolveFlagsRequest, - ) -> Result { + ) -> Result { let response = self.resolve_flags_sticky(&flags_resolver::ResolveFlagsRequest { flags: request.flags.clone(), sdk: request.sdk.clone(), @@ -586,11 +609,11 @@ impl<'a, H: Host> AccountResolver<'a, H> { match response { Ok(v) => match v.resolve_result { - None => Err(ResolveFlagError::err("failed to resolve flags")), + None => Err("failed to resolve flags".to_string()), Some(r) => match r { ResolveResult::Response(flags_response) => Ok(flags_response), ResolveResult::MissingMaterializations(_) => { - Err(ResolveFlagError::err("sticky assignments is not supported")) + Err("sticky assignments is not supported".to_string()) } }, }, @@ -669,15 +692,16 @@ impl<'a, H: Host> AccountResolver<'a, H> { .flags .get(flag_name) .ok_or(ResolveFlagError::err("flag not found")) - .and_then(|flag| self.resolve_flag(flag)) + .and_then(|flag| self.resolve_flag(flag, NoStickyContext)) } pub fn resolve_flag( &'a self, flag: &'a Flag, + sticky_context: ResolveWithStickyContext, ) -> Result, ResolveFlagError> { let mut updates: Vec = Vec::new(); - let mut missing_materializations: Vec = Vec::new(); + let mut missing_materializations: Vec = Vec::new(); let mut resolved_value = ResolvedValue::new(flag); if flag.state == flags_admin::flag::State::Archived as i32 { @@ -687,6 +711,8 @@ impl<'a, H: Host> AccountResolver<'a, H> { }); } + let mut skip_evaluation = false; + for rule in &flag.rules { if !rule.enabled { continue; @@ -718,7 +744,41 @@ impl<'a, H: Host> AccountResolver<'a, H> { let mut materialization_matched = false; if let Some(materialization_spec) = &rule.materialization_spec { + let rule_name = &rule.name; let read_materialization = materialization_spec.read_materialization.clone(); + if !read_materialization.is_empty() { + // check if the materialization for the unit exists + match &sticky_context { + ResolveWithStickyContext::WithStickyContext(sticky_context) => { + skip_evaluation = sticky_context.skip_on_not_missing; + + if !sticky_context + .context + .unit_materialization_info + .contains_key(&unit) + { + missing_materializations.push(MissingMaterializationItem { + unit: unit.clone(), + rule: rule_name.clone(), + read_materialization, + }); + // check the other rule + continue; + } + } + NoStickyContext => { + materialization_matched = false; + } + } + } + } + + if !missing_materializations.is_empty() || skip_evaluation { + /*** + don't want to evaluate the rules when we have missing dependencies + or when we are in discovery mode (finding missing dependencies for rules) + */ + continue; } if !self.segment_match(segment, &unit)? { @@ -815,6 +875,12 @@ impl<'a, H: Host> AccountResolver<'a, H> { } } + if !missing_materializations.is_empty() { + return Err(ResolveFlagError::missing_materializations( + missing_materializations, + )); + } + if resolved_value.reason == ResolveReason::Match { resolved_value.should_apply = true; } else { @@ -1198,6 +1264,7 @@ pub fn bucket(hash: u128, buckets: u64) -> usize { mod tests { use super::*; use crate::proto::confidence::flags::resolver::v1::{ResolveFlagsResponse, Sdk}; + use crate::ResolveWithStickyContext::NoStickyContext; const EXAMPLE_STATE: &[u8] = include_bytes!("../test-payloads/resolver_state.pb"); const SECRET: &str = "mkjJruAATQWjeY7foFIWfVAcBWnci2YF"; @@ -1309,7 +1376,9 @@ mod tests { .get_resolver_with_json_context(SECRET, context_json, &ENCRYPTION_KEY) .unwrap(); let flag = resolver.state.flags.get("flags/tutorial-feature").unwrap(); - let resolve_result = resolver.resolve_flag(flag).unwrap(); + let resolve_result = resolver + .resolve_flag(flag, ResolveWithStickyContext::NoStickyContext) + .unwrap(); let resolved_value = &resolve_result.resolved_value; let assignment_match = resolved_value.assignment_match.as_ref().unwrap(); @@ -1331,7 +1400,7 @@ mod tests { .unwrap(); let flag = resolver.state.flags.get("flags/tutorial-feature").unwrap(); let assignment_match = resolver - .resolve_flag(flag) + .resolve_flag(flag, ResolveWithStickyContext::NoStickyContext) .unwrap() .resolved_value .assignment_match @@ -1740,7 +1809,9 @@ mod tests { .flags .get("flags/fallthrough-test-2") .unwrap(); - let resolve_result = resolver.resolve_flag(flag).unwrap(); + let resolve_result = resolver + .resolve_flag(flag, ResolveWithStickyContext::NoStickyContext) + .unwrap(); let resolved_value = &resolve_result.resolved_value; assert_eq!(resolved_value.reason as i32, ResolveReason::Match as i32); @@ -1767,7 +1838,7 @@ mod tests { .flags .get("flags/fallthrough-test-2") .unwrap(); - let resolve_result = resolver.resolve_flag(flag).unwrap(); + let resolve_result = resolver.resolve_flag(flag, NoStickyContext).unwrap(); let resolved_value = &resolve_result.resolved_value; assert_eq!( diff --git a/wasm/proto/resolver/api.proto b/wasm/proto/resolver/api.proto index 3cfc382..4d43ca1 100644 --- a/wasm/proto/resolver/api.proto +++ b/wasm/proto/resolver/api.proto @@ -78,17 +78,14 @@ message ResolveFlagResponseResult { } message MissingMaterializations { - repeated MissingMaterializationsForUnit items = 1; + repeated MissingMaterializationItem items = 1; repeated MaterializationUpdate updates = 4; } -message MissingMaterializationsForUnit { +message MissingMaterializationItem { string unit = 1; - map materializations_of_rules = 2; - - message Materializations { - repeated string materializations = 1; - } + string rule = 2; + string read_materialization = 3; } message MaterializationUpdate { From 1a818bc42090e10d8ad5d75389305328ce97f2e2 Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Thu, 18 Sep 2025 16:05:35 +0200 Subject: [PATCH 05/18] fixup! cleanup --- .../confidence/flags/resolver/v1/api.proto | 1 - confidence-resolver/src/lib.rs | 62 ++++++++++--------- wasm/rust-guest/src/lib.rs | 4 +- 3 files changed, 36 insertions(+), 31 deletions(-) diff --git a/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto b/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto index d8e5d28..dbb33d4 100644 --- a/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto +++ b/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto @@ -116,7 +116,6 @@ message ResolveFlagResponseResult { message MissingMaterializations { repeated MissingMaterializationItem items = 1; - repeated MaterializationUpdate updates = 4; } message MissingMaterializationItem { diff --git a/confidence-resolver/src/lib.rs b/confidence-resolver/src/lib.rs index b88358f..aa8bc5c 100644 --- a/confidence-resolver/src/lib.rs +++ b/confidence-resolver/src/lib.rs @@ -491,7 +491,16 @@ impl<'a, H: Host> AccountResolver<'a, H> { let mut missing_materialization_items: Vec = vec![]; for flag in flags_to_resolve { - let resolve_result = self.resolve_flag(flag, NoStickyContext); + let sticky_context = if let Some(context) = &request.materialization_context { + ResolveWithStickyContext::WithStickyContext(StickyResolveContext { + // don't evaluate the flag when already have some dependency missing + skip_on_not_missing: !missing_materialization_items.is_empty(), + context: context.clone(), + }) + } else { + NoStickyContext + }; + let resolve_result = self.resolve_flag(flag, sticky_context); match resolve_result { Ok(resolve_result) => resolve_results.push(resolve_result), @@ -510,7 +519,6 @@ impl<'a, H: Host> AccountResolver<'a, H> { resolve_result: Some(ResolveResult::MissingMaterializations( MissingMaterializations { items: missing_materialization_items, - updates: vec![], }, )), }); @@ -711,7 +719,16 @@ impl<'a, H: Host> AccountResolver<'a, H> { }); } - let mut skip_evaluation = false; + let materialization_context = match &sticky_context { + ResolveWithStickyContext::WithStickyContext(ctx) => Some(ctx), + NoStickyContext => None, + }; + + let skip_evaluation = if let Some(ctx) = &materialization_context { + ctx.skip_on_not_missing + } else { + false + }; for rule in &flag.rules { if !rule.enabled { @@ -741,33 +758,20 @@ impl<'a, H: Host> AccountResolver<'a, H> { } }; - let mut materialization_matched = false; - if let Some(materialization_spec) = &rule.materialization_spec { let rule_name = &rule.name; let read_materialization = materialization_spec.read_materialization.clone(); if !read_materialization.is_empty() { // check if the materialization for the unit exists - match &sticky_context { - ResolveWithStickyContext::WithStickyContext(sticky_context) => { - skip_evaluation = sticky_context.skip_on_not_missing; - - if !sticky_context - .context - .unit_materialization_info - .contains_key(&unit) - { - missing_materializations.push(MissingMaterializationItem { - unit: unit.clone(), - rule: rule_name.clone(), - read_materialization, - }); - // check the other rule - continue; - } - } - NoStickyContext => { - materialization_matched = false; + if let Some(ctx) = materialization_context { + if !ctx.context.unit_materialization_info.contains_key(&unit) { + missing_materializations.push(MissingMaterializationItem { + unit: unit.clone(), + rule: rule_name.clone(), + read_materialization, + }); + // check the other rule + continue; } } } @@ -781,6 +785,10 @@ impl<'a, H: Host> AccountResolver<'a, H> { continue; } + if let Some(ctx) = materialization_context { + // now we have all the dependencies required for evaluating the sticky assignments + } + if !self.segment_match(segment, &unit)? { // ResolveReason::SEGMENT_NOT_MATCH continue; @@ -1809,9 +1817,7 @@ mod tests { .flags .get("flags/fallthrough-test-2") .unwrap(); - let resolve_result = resolver - .resolve_flag(flag, ResolveWithStickyContext::NoStickyContext) - .unwrap(); + let resolve_result = resolver.resolve_flag(flag, NoStickyContext).unwrap(); let resolved_value = &resolve_result.resolved_value; assert_eq!(resolved_value.reason as i32, ResolveReason::Match as i32); diff --git a/wasm/rust-guest/src/lib.rs b/wasm/rust-guest/src/lib.rs index c9f827c..03dc4ce 100644 --- a/wasm/rust-guest/src/lib.rs +++ b/wasm/rust-guest/src/lib.rs @@ -188,14 +188,14 @@ wasm_msg_guest! { let resolver_state = get_resolver_state()?; let evaluation_context = request.evaluation_context.as_ref().cloned().unwrap_or_default(); let resolver = resolver_state.get_resolver::(&request.client_secret, evaluation_context, &ENCRYPTION_KEY)?; - resolver.resolve_flags_sticky(&request).map_err(|e| e.message).into() + resolver.resolve_flags_sticky(&request).into() } fn resolve(request: ResolveFlagsRequest) -> WasmResult { let resolver_state = get_resolver_state()?; let evaluation_context = request.evaluation_context.as_ref().cloned().unwrap_or_default(); let resolver = resolver_state.get_resolver::(&request.client_secret, evaluation_context, &ENCRYPTION_KEY)?; - resolver.resolve_flags(&request).map_err(|e| e.message).into() + resolver.resolve_flags(&request).into() } fn resolve_simple(request: ResolveSimpleRequest) -> WasmResult { let resolver_state = get_resolver_state()?; From b49a7e08c70b02b39dbf55339352d8d18432dbb6 Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Fri, 19 Sep 2025 11:35:48 +0200 Subject: [PATCH 06/18] add resolving sticky --- confidence-resolver/src/lib.rs | 78 +++++++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 5 deletions(-) diff --git a/confidence-resolver/src/lib.rs b/confidence-resolver/src/lib.rs index aa8bc5c..4f71c39 100644 --- a/confidence-resolver/src/lib.rs +++ b/confidence-resolver/src/lib.rs @@ -785,18 +785,86 @@ impl<'a, H: Host> AccountResolver<'a, H> { continue; } + let Some(spec) = &rule.assignment_spec else { + continue; + }; + + let mut materialization_matched = false; + if let Some(ctx) = materialization_context { // now we have all the dependencies required for evaluating the sticky assignments + if let Some(materialization_spec) = &rule.materialization_spec { + let read_materialization = &materialization_spec.read_materialization; + if !read_materialization.is_empty() { + let materialization_info = ctx.context.unit_materialization_info.get(&unit); + if let Some(materialization_info) = materialization_info { + materialization_matched = if !materialization_info.unit_in_info { + if materialization_spec + .mode + .as_ref() + .map(|mode| mode.materialization_must_match) + .unwrap_or(false) + { + // Materialization must match but unit is not in materialization + continue; + } + false + } else { + if materialization_spec + .mode + .as_ref() + .map(|mode| mode.segment_targeting_can_be_ignored) + .unwrap_or(false) + { + true + } else { + self.segment_match(segment, &unit)? + } + }; + + if materialization_matched { + if let Some(variant_name) = + materialization_info.rule_to_variant.get(&rule.name) + { + if let Some(assignment) = + spec.assignments.iter().find(|assignment| { + if let Some(rule::assignment::Assignment::Variant( + ref variant_assignment, + )) = &assignment.assignment + { + variant_assignment.variant == *variant_name + } else { + false + } + }) + { + let variant = self.state.flags[flag.name.as_str()] + .variants + .iter() + .find(|v| v.name == *variant_name) + .or_fail()?; + return Ok(FlagResolveResult { + resolved_value: resolved_value.with_variant_match( + rule, + segment, + variant, + &assignment.assignment_id, + &unit, + ), + updates: vec![], + }); + } + } + } + } + } + } } - if !self.segment_match(segment, &unit)? { + if !materialization_matched && !self.segment_match(segment, &unit)? { // ResolveReason::SEGMENT_NOT_MATCH continue; } - - let Some(spec) = &rule.assignment_spec else { - continue; - }; let bucket_count = spec.bucket_count; let variant_salt = segment_name.split("/").nth(1).or_fail()?; let key = format!("{}|{}", variant_salt, unit); From 9998135e0c4ad5b6693c33a9fb21cd113a69e1d0 Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Fri, 19 Sep 2025 11:45:05 +0200 Subject: [PATCH 07/18] fail fast on the first missing materialization info --- .../confidence/flags/resolver/v1/api.proto | 5 +++- confidence-resolver/src/lib.rs | 27 ++++++++++++++++++- wasm/proto/resolver/api.proto | 4 ++- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto b/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto index dbb33d4..6e7328b 100644 --- a/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto +++ b/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto @@ -94,8 +94,11 @@ message ResolveFlagsRequest { // if the resolver should handle sticky assignments bool process_sticky = 6; - // Context about the materialization required for the resolve + // Context about the materialization required for the resolve MaterializationContext materialization_context = 7; + + // if a materialization info is missing, we want tor return to the caller immediately + bool fail_fast_on_sticky = 8; } message MaterializationContext { diff --git a/confidence-resolver/src/lib.rs b/confidence-resolver/src/lib.rs index 4f71c39..236d1f7 100644 --- a/confidence-resolver/src/lib.rs +++ b/confidence-resolver/src/lib.rs @@ -509,6 +509,15 @@ impl<'a, H: Host> AccountResolver<'a, H> { return Err(err.message.to_string()); } else { missing_materialization_items.extend(err.missing_materializations); + if request.fail_fast_on_sticky { + return Ok(ResolveFlagResponseResult { + resolve_result: Some(ResolveResult::MissingMaterializations( + MissingMaterializations { + items: missing_materialization_items, + }, + )), + }); + } } } } @@ -612,6 +621,7 @@ impl<'a, H: Host> AccountResolver<'a, H> { client_secret: request.client_secret.clone(), apply: request.apply.clone(), materialization_context: None, + fail_fast_on_sticky: false, process_sticky: false, }); @@ -838,7 +848,11 @@ impl<'a, H: Host> AccountResolver<'a, H> { } }) { - let variant = self.state.flags[flag.name.as_str()] + let variant = self + .state + .flags + .get(flag.name.as_str()) + .unwrap() .variants .iter() .find(|v| v.name == *variant_name) @@ -1509,6 +1523,7 @@ mod tests { let resolve_flag_req = flags_resolver::ResolveFlagsRequest { evaluation_context: Some(Struct::default()), client_secret: SECRET.to_string(), + fail_fast_on_sticky: false, process_sticky: false, materialization_context: None, flags: vec!["flags/tutorial-feature".to_string()], @@ -1577,7 +1592,9 @@ mod tests { evaluation_context: Some(Struct::default()), client_secret: SECRET.to_string(), flags: vec!["flags/fallthrough-test-1".to_string()], + fail_fast_on_sticky: false, process_sticky: false, + materialization_context: None, apply: false, sdk: Some(Sdk { @@ -1635,7 +1652,9 @@ mod tests { evaluation_context: Some(Struct::default()), client_secret: SECRET.to_string(), flags: vec!["flags/fallthrough-test-2".to_string()], + fail_fast_on_sticky: false, process_sticky: false, + materialization_context: None, apply: false, sdk: Some(Sdk { @@ -1707,7 +1726,9 @@ mod tests { evaluation_context: Some(Struct::default()), client_secret: SECRET.to_string(), flags: vec!["flags/tutorial-feature".to_string()], + fail_fast_on_sticky: false, process_sticky: false, + materialization_context: None, apply: false, sdk: Some(Sdk { @@ -1803,7 +1824,9 @@ mod tests { evaluation_context: Some(Struct::default()), client_secret: SECRET.to_string(), flags: vec!["flags/tutorial-feature".to_string()], + fail_fast_on_sticky: false, process_sticky: false, + materialization_context: None, apply: true, sdk: Some(Sdk { @@ -1838,7 +1861,9 @@ mod tests { evaluation_context: Some(Struct::default()), client_secret: SECRET.to_string(), flags: vec!["flags/tutorial-feature".to_string()], + fail_fast_on_sticky: false, process_sticky: false, + materialization_context: None, apply: true, sdk: Some(Sdk { diff --git a/wasm/proto/resolver/api.proto b/wasm/proto/resolver/api.proto index 4d43ca1..073881b 100644 --- a/wasm/proto/resolver/api.proto +++ b/wasm/proto/resolver/api.proto @@ -40,9 +40,11 @@ message ResolveFlagsRequest { // if the resolver should handle sticky assignments bool process_sticky = 6; - // Context about the materialization required for the resolve MaterializationContext materialization_context = 7; + + // if a materialization info is missing, we want tor return to the caller immediately + bool fail_fast_on_sticky = 8; } From 6073069b2c725a193d54ef2bfe455938a0d01db2 Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Fri, 19 Sep 2025 12:03:19 +0200 Subject: [PATCH 08/18] add documentation about how sticky assignments are resolved and what is the algorithm to discover missing materializations and resolve --- STICKY_ASSIGNMENTS.md | 271 +++++++++++++++++++++++++++++++++ confidence-resolver/src/lib.rs | 6 +- 2 files changed, 272 insertions(+), 5 deletions(-) create mode 100644 STICKY_ASSIGNMENTS.md diff --git a/STICKY_ASSIGNMENTS.md b/STICKY_ASSIGNMENTS.md new file mode 100644 index 0000000..2392912 --- /dev/null +++ b/STICKY_ASSIGNMENTS.md @@ -0,0 +1,271 @@ +# Sticky Assignments in Confidence Flag Resolver + +## Overview + +Sticky assignments are a feature in the Confidence Flag Resolver that allows flag assignments to persist across multiple resolve requests. This ensures consistent user experiences and enables advanced experimentation workflows by maintaining assignment state over time. + +## What are Sticky Assignments? + +Sticky assignments work by storing flag assignment information (materializations) that can be referenced in future resolve requests. Instead of randomly assigning users to variants each time a flag is resolved, the system can "stick" to previous assignments when certain conditions are met. + +### Key Concepts + +- **Materialization**: The persisted record of a flag assignment for a specific unit (user/entity) +- **Unit**: The entity being assigned (typically a user ID or targeting key) +- **Materialization Context**: Information about previous assignments passed to the resolver +- **Read/Write Materialization**: Rules specify whether to read from or write to materializations + +## How It Works + +### 1. Materialization Specification + +Each flag rule can include a `MaterializationSpec` that defines: + +```protobuf +message MaterializationSpec { + // Where to read previous assignments from + string read_materialization = 2; + + // Where to write new assignments to + string write_materialization = 1; + + // How materialization reads should be treated + MaterializationReadMode mode = 3; +} +``` + +### 2. Materialization Read Mode + +The `MaterializationReadMode` controls how materializations interact with normal targeting: + +```protobuf +message MaterializationReadMode { + // If true, only units in the materialization will be considered + // If false, units match if they're in materialization OR match segment + bool materialization_must_match = 1; + + // If true, segment targeting is ignored for units in materialization + // If false, both materialization and segment must match + bool segment_targeting_can_be_ignored = 2; +} +``` + +### 3. Resolution Process + +When resolving a flag with sticky assignments enabled: + +1. **Check Dependencies**: Verify all required materializations are available +2. **Read Materialization**: Check if the unit has a previous assignment for this rule +3. **Apply Logic**: Based on `MaterializationReadMode`, determine if the stored assignment should be used +4. **Write Materialization**: If a new assignment is made and a write materialization is specified, store it + +### 4. Materialization Context + +The resolver accepts a `MaterializationContext` containing previous assignments: + +```protobuf +message MaterializationContext { + map unit_materialization_info = 1; +} + +message MaterializationInfo { + bool unit_in_info = 1; + map rule_to_variant = 2; +} +``` + +## Usage Patterns + +### Basic Sticky Assignment + +A rule with both read and write materialization will: +1. Check if the unit was previously assigned +2. Use the previous assignment if available +3. Store new assignments for future use + +### Paused Intake + +Setting `materialization_must_match = true` creates "paused intake": +- Only units already in the materialization will match the rule +- New units will skip this rule entirely +- Useful for controlled rollout scenarios + +### Override Targeting + +Setting `segment_targeting_can_be_ignored = true` allows: +- Units in materialization match the rule regardless of segment targeting +- Segment allocation proportions are ignored for these units +- Useful for maintaining assignments when targeting rules change + +## API Integration + +### Enable Sticky Assignments + +Set `process_sticky = true` in the resolve request: + +```protobuf +message ResolveFlagsRequest { + // ... other fields ... + + // if the resolver should handle sticky assignments + bool process_sticky = 6; + + // Context about the materialization required for the resolve + MaterializationContext materialization_context = 7; + + // if a materialization info is missing, return immediately + bool fail_fast_on_sticky = 8; +} +``` + +### Handling Missing Materializations + +The resolver may return `MissingMaterializations` when required materialization data is unavailable: + +```protobuf +message ResolveFlagResponseResult { + oneof resolve_result { + ResolveFlagsResponse response = 1; + MissingMaterializations missing_materializations = 2; + } +} + +message MissingMaterializationItem { + string unit = 1; + string rule = 2; + string read_materialization = 3; +} +``` + +## Use Cases + +### 1. Consistent User Experience + +Ensure users see the same variant across app sessions and devices by storing their assignments in a shared materialization store. + +### 2. Experiment Analysis + +Maintain assignment consistency during long-running experiments, even when targeting rules or traffic allocation changes. + +### 3. Migration Scenarios + +Gradually migrate users from one variant to another by updating materializations over time. + +### 4. Controlled Rollout + +Use "paused intake" mode to limit new user assignments while maintaining existing ones. + +## Implementation Details + +### Materialization Updates + +When assignments are made, the resolver returns `MaterializationUpdate` objects: + +```protobuf +message MaterializationUpdate { + string unit = 1; + string write_materialization = 2; + string rule = 3; + string variant = 4; +} +``` + +These should be persisted by the client for use in future resolve requests. + +### Error Handling + +- **Missing Materializations**: When required materialization data is unavailable +- **Fail Fast**: `fail_fast_on_sticky` controls whether to return immediately or continue processing +- **Dependency Checking**: The resolver validates all materialization dependencies before evaluation + +## Advanced Optimizations + +### Fail Fast on First Missing Materialization + +The `fail_fast_on_sticky` parameter provides a performance optimization for handling missing materializations: + +**Behavior:** +- When `fail_fast_on_sticky = true`: As soon as any flag encounters a missing materialization dependency, the resolver immediately returns all accumulated missing materializations without processing remaining flags +- When `fail_fast_on_sticky = false`: The resolver continues processing all flags and collects all missing materializations before returning + +**Use Cases:** +- **Discovery Mode**: Set to `false` when you want to collect all missing materializations across all flags in a single request +- **Production Mode**: Set to `true` when you want immediate feedback about missing dependencies to avoid unnecessary processing + +**Example Flow:** +``` +Flag A: ✅ Has materialization → Process normally +Flag B: ❌ Missing materialization + fail_fast=true → Return immediately with [Flag B missing item] +Flag C: (Not processed due to fail_fast) +``` + +### Rule Evaluation Skipping Optimization + +The resolver implements a sophisticated optimization to avoid unnecessary rule evaluation when materialization dependencies are missing: + +**The `skip_on_not_missing` Mechanism:** + +1. **Dependency Discovery Phase**: When processing multiple flags, if any previous flag had missing materializations, subsequent flags enter "discovery mode" + +2. **Two-Pass Evaluation**: + - **Pass 1**: Check for missing materializations only (skip rule evaluation) + - **Pass 2**: If all materializations are available, re-evaluate with full rule processing + +3. **Optimization Logic**: + ```rust + skip_on_not_missing: !missing_materialization_items.is_empty() + ``` + +**How It Works:** + +``` +Processing Flag 1: +├── Rule 1: Missing materialization X → Collect missing item +├── Rule 2: Skip evaluation (skip_on_not_missing=true) +├── Result: Flag 1 has missing materializations + +Processing Flag 2: +├── skip_on_not_missing = true (because Flag 1 had missing deps) +├── All rules: Only check for missing materializations, don't evaluate +├── Result: Collect any additional missing items for Flag 2 +``` + +**Benefits:** +- **Performance**: Avoids expensive rule evaluation (segment matching, bucket calculation) when dependencies are missing +- **Consistency**: Ensures all missing materializations are discovered before any rule evaluation begins +- **Atomicity**: Either all flags resolve successfully with their materializations, or all missing dependencies are returned + +**Complete Resolution Flow:** + +1. **First Pass**: Process all flags in discovery mode to find all missing materializations +2. **Early Return**: If `fail_fast_on_sticky=true` and missing deps found, return immediately +3. **Second Pass**: If all materializations available, re-process all flags with full evaluation +4. **Success**: Return resolved flags with materialization updates + +This optimization ensures efficient handling of complex dependency graphs while maintaining correctness and performance. + +### Performance Considerations + +- **Materialization lookups happen before rule evaluation**: Dependencies are checked first to avoid expensive operations +- **Failed materialization dependencies skip rule evaluation**: No segment matching or bucket calculation when deps missing +- **Two-phase resolution**: Discovery phase finds all missing deps, evaluation phase only runs when all deps available +- **Batch processing**: Multiple flags can share materialization context for efficient processing + +## Best Practices + +1. **Consistent Storage**: Use reliable storage for materialization data to ensure assignment consistency +2. **Version Management**: Consider materialization versioning for complex migration scenarios +3. **Monitoring**: Track materialization hit rates and assignment consistency +4. **Testing**: Verify sticky behavior with different materialization states +5. **Cleanup**: Implement materialization cleanup for archived flags or expired experiments + +## Example Workflow + +1. User requests flag resolution without materialization context +2. Resolver assigns variants and returns `MaterializationUpdate`s +3. Client stores materialization data +4. Subsequent requests include `MaterializationContext` +5. Resolver uses stored assignments when available, creating new ones as needed +6. Process continues with updated materialization context + +This approach ensures assignment consistency while allowing new users to be assigned according to current targeting rules. diff --git a/confidence-resolver/src/lib.rs b/confidence-resolver/src/lib.rs index 236d1f7..196ed3f 100644 --- a/confidence-resolver/src/lib.rs +++ b/confidence-resolver/src/lib.rs @@ -848,11 +848,7 @@ impl<'a, H: Host> AccountResolver<'a, H> { } }) { - let variant = self - .state - .flags - .get(flag.name.as_str()) - .unwrap() + let variant = flag .variants .iter() .find(|v| v.name == *variant_name) From 242ac57df38f5d409940952eec6d981507304058 Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Tue, 23 Sep 2025 15:43:45 +0200 Subject: [PATCH 09/18] wrap the resolve request in resolve with sticky req --- .../confidence/flags/resolver/v1/api.proto | 17 ++- confidence-resolver/src/lib.rs | 115 +++++++++--------- wasm/proto/resolver/api.proto | 43 ------- wasm/rust-guest/src/lib.rs | 11 +- 4 files changed, 76 insertions(+), 110 deletions(-) diff --git a/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto b/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto index 6e7328b..cdf70c5 100644 --- a/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto +++ b/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto @@ -90,15 +90,19 @@ message ResolveFlagsRequest { Sdk sdk = 5 [ (google.api.field_behavior) = OPTIONAL ]; +} + +message ResolveWithStickyRequest { + ResolveFlagsRequest resolve_request = 1; - // if the resolver should handle sticky assignments + // if the resolver should handle sticky assignments bool process_sticky = 6; // Context about the materialization required for the resolve MaterializationContext materialization_context = 7; // if a materialization info is missing, we want tor return to the caller immediately - bool fail_fast_on_sticky = 8; + bool fail_fast_on_sticky = 8; } message MaterializationContext { @@ -112,11 +116,16 @@ message MaterializationInfo { message ResolveFlagResponseResult { oneof resolve_result { - ResolveFlagsResponse response = 1; + ResolveWithStickySuccess success = 1; MissingMaterializations missing_materializations = 2; } } +message ResolveWithStickySuccess { + ResolveFlagsResponse response = 1; + repeated MaterializationUpdate updates = 2; +} + message MissingMaterializations { repeated MissingMaterializationItem items = 1; } @@ -145,8 +154,6 @@ message ResolveFlagsResponse { // Unique identifier for this particular resolve request. string resolve_id = 3; - - repeated MaterializationUpdate updates = 4; } message ApplyFlagsRequest { diff --git a/confidence-resolver/src/lib.rs b/confidence-resolver/src/lib.rs index 196ed3f..2510a6f 100644 --- a/confidence-resolver/src/lib.rs +++ b/confidence-resolver/src/lib.rs @@ -58,7 +58,8 @@ use proto::confidence::flags::resolver::v1::{ use crate::err::{ErrorCode, OrFailExt}; use crate::proto::confidence::flags::resolver::v1::{ - MissingMaterializationItem, MissingMaterializations, + MissingMaterializationItem, MissingMaterializations, ResolveFlagsRequest, ResolveFlagsResponse, + ResolveWithStickyRequest, ResolveWithStickySuccess, }; use crate::ResolveWithStickyContext::NoStickyContext; @@ -442,6 +443,36 @@ impl From for ResolveFlagError { } } +impl ResolveFlagResponseResult { + fn with_success(response: ResolveFlagsResponse, updates: Vec) -> Self { + ResolveFlagResponseResult { + resolve_result: Some(ResolveResult::Success(ResolveWithStickySuccess { + response: Some(response), + updates, + })), + } + } + + fn with_missing_materializations(items: Vec) -> Self { + ResolveFlagResponseResult { + resolve_result: Some(ResolveResult::MissingMaterializations( + MissingMaterializations { items }, + )), + } + } +} + +impl ResolveWithStickyRequest { + fn without_sticky(resolve_request: ResolveFlagsRequest) -> ResolveWithStickyRequest { + ResolveWithStickyRequest { + resolve_request: Some(resolve_request), + process_sticky: false, + fail_fast_on_sticky: false, + materialization_context: None, + } + } +} + impl<'a, H: Host> AccountResolver<'a, H> { pub fn new( client: &'a Client, @@ -460,11 +491,12 @@ impl<'a, H: Host> AccountResolver<'a, H> { pub fn resolve_flags_sticky( &self, - request: &flags_resolver::ResolveFlagsRequest, + request: &flags_resolver::ResolveWithStickyRequest, ) -> Result { let timestamp = H::current_time(); - let flag_names = &request.flags; + let resolve_request = request.resolve_request.as_ref().unwrap(); + let flag_names = resolve_request.flags.clone(); let flags_to_resolve = self .state .flags @@ -510,13 +542,9 @@ impl<'a, H: Host> AccountResolver<'a, H> { } else { missing_materialization_items.extend(err.missing_materializations); if request.fail_fast_on_sticky { - return Ok(ResolveFlagResponseResult { - resolve_result: Some(ResolveResult::MissingMaterializations( - MissingMaterializations { - items: missing_materialization_items, - }, - )), - }); + return Ok(ResolveFlagResponseResult::with_missing_materializations( + missing_materialization_items, + )); } } } @@ -524,13 +552,9 @@ impl<'a, H: Host> AccountResolver<'a, H> { } if !missing_materialization_items.is_empty() { - return Ok(ResolveFlagResponseResult { - resolve_result: Some(ResolveResult::MissingMaterializations( - MissingMaterializations { - items: missing_materialization_items, - }, - )), - }); + return Ok(ResolveFlagResponseResult::with_missing_materializations( + missing_materialization_items, + )); } let resolved_values: Vec<&ResolvedValue> = @@ -541,16 +565,17 @@ impl<'a, H: Host> AccountResolver<'a, H> { resolve_id: resolve_id.clone(), ..Default::default() }; + let mut updates: Vec = vec![]; for resolved_value in &resolved_values { response.resolved_flags.push((*resolved_value).into()); } // Collect all materialization updates from all resolve results for resolve_result in &resolve_results { - response.updates.extend(resolve_result.updates.clone()); + updates.extend(resolve_result.updates.clone()); } - if request.apply { + if resolve_request.apply { let flags_to_apply: Vec = resolved_values .iter() .filter(|v| v.should_apply) @@ -565,7 +590,7 @@ impl<'a, H: Host> AccountResolver<'a, H> { &self.evaluation_context.context, flags_to_apply.as_slice(), self.client, - &request.sdk, + &resolve_request.sdk.clone(), ); } else { // create resolve token @@ -602,34 +627,31 @@ impl<'a, H: Host> AccountResolver<'a, H> { &self.evaluation_context.context, owned_values.as_slice(), self.client, - &request.sdk, + &resolve_request.sdk.clone(), ); - Ok(ResolveFlagResponseResult { - resolve_result: Some(ResolveResult::Response(response)), - }) + Ok(ResolveFlagResponseResult::with_success(response, updates)) } pub fn resolve_flags( &self, request: &flags_resolver::ResolveFlagsRequest, ) -> Result { - let response = self.resolve_flags_sticky(&flags_resolver::ResolveFlagsRequest { - flags: request.flags.clone(), - sdk: request.sdk.clone(), - evaluation_context: request.evaluation_context.clone(), - client_secret: request.client_secret.clone(), - apply: request.apply.clone(), - materialization_context: None, - fail_fast_on_sticky: false, - process_sticky: false, - }); + let response = self.resolve_flags_sticky(&ResolveWithStickyRequest::without_sticky( + flags_resolver::ResolveFlagsRequest { + flags: request.flags.clone(), + sdk: request.sdk.clone(), + evaluation_context: request.evaluation_context.clone(), + client_secret: request.client_secret.clone(), + apply: request.apply.clone(), + }, + )); match response { Ok(v) => match v.resolve_result { None => Err("failed to resolve flags".to_string()), Some(r) => match r { - ResolveResult::Response(flags_response) => Ok(flags_response), + ResolveResult::Success(flags_response) => Ok(flags_response.response.unwrap()), ResolveResult::MissingMaterializations(_) => { Err("sticky assignments is not supported".to_string()) } @@ -1519,9 +1541,6 @@ mod tests { let resolve_flag_req = flags_resolver::ResolveFlagsRequest { evaluation_context: Some(Struct::default()), client_secret: SECRET.to_string(), - fail_fast_on_sticky: false, - process_sticky: false, - materialization_context: None, flags: vec!["flags/tutorial-feature".to_string()], apply: false, sdk: Some(Sdk { @@ -1588,10 +1607,6 @@ mod tests { evaluation_context: Some(Struct::default()), client_secret: SECRET.to_string(), flags: vec!["flags/fallthrough-test-1".to_string()], - fail_fast_on_sticky: false, - process_sticky: false, - - materialization_context: None, apply: false, sdk: Some(Sdk { sdk: None, @@ -1648,10 +1663,6 @@ mod tests { evaluation_context: Some(Struct::default()), client_secret: SECRET.to_string(), flags: vec!["flags/fallthrough-test-2".to_string()], - fail_fast_on_sticky: false, - process_sticky: false, - - materialization_context: None, apply: false, sdk: Some(Sdk { sdk: None, @@ -1722,10 +1733,6 @@ mod tests { evaluation_context: Some(Struct::default()), client_secret: SECRET.to_string(), flags: vec!["flags/tutorial-feature".to_string()], - fail_fast_on_sticky: false, - process_sticky: false, - - materialization_context: None, apply: false, sdk: Some(Sdk { sdk: None, @@ -1820,10 +1827,6 @@ mod tests { evaluation_context: Some(Struct::default()), client_secret: SECRET.to_string(), flags: vec!["flags/tutorial-feature".to_string()], - fail_fast_on_sticky: false, - process_sticky: false, - - materialization_context: None, apply: true, sdk: Some(Sdk { sdk: None, @@ -1857,10 +1860,6 @@ mod tests { evaluation_context: Some(Struct::default()), client_secret: SECRET.to_string(), flags: vec!["flags/tutorial-feature".to_string()], - fail_fast_on_sticky: false, - process_sticky: false, - - materialization_context: None, apply: true, sdk: Some(Sdk { sdk: None, diff --git a/wasm/proto/resolver/api.proto b/wasm/proto/resolver/api.proto index 073881b..a71fd4f 100644 --- a/wasm/proto/resolver/api.proto +++ b/wasm/proto/resolver/api.proto @@ -37,14 +37,6 @@ message ResolveFlagsRequest { // Information about the SDK used to initiate the request. // Sdk sdk = 5; - - // if the resolver should handle sticky assignments - bool process_sticky = 6; - // Context about the materialization required for the resolve - MaterializationContext materialization_context = 7; - - // if a materialization info is missing, we want tor return to the caller immediately - bool fail_fast_on_sticky = 8; } @@ -60,41 +52,6 @@ message ResolveFlagsResponse { // Unique identifier for this particular resolve request. string resolve_id = 3; - repeated MaterializationUpdate updates = 4; -} - -message MaterializationContext { - map unit_materialization_info = 1; -} - -message MaterializationInfo { - bool unit_in_info = 1; - map rule_to_variant = 2; -} - -message ResolveFlagResponseResult { - oneof result { - ResolveFlagsResponse response = 1; - MissingMaterializations missing_materializations = 2; - } -} - -message MissingMaterializations { - repeated MissingMaterializationItem items = 1; - repeated MaterializationUpdate updates = 4; -} - -message MissingMaterializationItem { - string unit = 1; - string rule = 2; - string read_materialization = 3; -} - -message MaterializationUpdate { - string unit = 1; - string write_materialization = 2; - string rule = 3; - string variant = 4; } diff --git a/wasm/rust-guest/src/lib.rs b/wasm/rust-guest/src/lib.rs index 03dc4ce..48b2bb7 100644 --- a/wasm/rust-guest/src/lib.rs +++ b/wasm/rust-guest/src/lib.rs @@ -9,7 +9,9 @@ use prost::Message; #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; -use confidence_resolver::proto::confidence::flags::resolver::v1::WriteFlagLogsRequest; +use confidence_resolver::proto::confidence::flags::resolver::v1::{ + ResolveWithStickyRequest, WriteFlagLogsRequest, +}; use confidence_resolver::resolve_logger::ResolveLogger; use rand::distr::Alphanumeric; use rand::distr::SampleString; @@ -184,10 +186,11 @@ wasm_msg_guest! { Ok(VOID) } - fn resolve_with_sticky(request: ResolveFlagsRequest) -> WasmResult { + fn resolve_with_sticky(request: ResolveWithStickyRequest) -> WasmResult { let resolver_state = get_resolver_state()?; - let evaluation_context = request.evaluation_context.as_ref().cloned().unwrap_or_default(); - let resolver = resolver_state.get_resolver::(&request.client_secret, evaluation_context, &ENCRYPTION_KEY)?; + let resolve_request = &request.resolve_request.clone().unwrap(); + let evaluation_context = resolve_request.evaluation_context.clone().unwrap(); + let resolver = resolver_state.get_resolver::(resolve_request.client_secret.as_str(), evaluation_context, &ENCRYPTION_KEY)?; resolver.resolve_flags_sticky(&request).into() } From b1bd52fb9ffe83c7a5eef10ff25c0b8132848631 Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Tue, 23 Sep 2025 15:52:51 +0200 Subject: [PATCH 10/18] fixup! wrap the resolve request in resolve with sticky req --- .../confidence/flags/resolver/v1/api.proto | 3 --- confidence-resolver/src/lib.rs | 16 +++++++++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto b/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto index cdf70c5..b777a60 100644 --- a/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto +++ b/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto @@ -95,9 +95,6 @@ message ResolveFlagsRequest { message ResolveWithStickyRequest { ResolveFlagsRequest resolve_request = 1; - // if the resolver should handle sticky assignments - bool process_sticky = 6; - // Context about the materialization required for the resolve MaterializationContext materialization_context = 7; diff --git a/confidence-resolver/src/lib.rs b/confidence-resolver/src/lib.rs index 2510a6f..56895de 100644 --- a/confidence-resolver/src/lib.rs +++ b/confidence-resolver/src/lib.rs @@ -466,7 +466,6 @@ impl ResolveWithStickyRequest { fn without_sticky(resolve_request: ResolveFlagsRequest) -> ResolveWithStickyRequest { ResolveWithStickyRequest { resolve_request: Some(resolve_request), - process_sticky: false, fail_fast_on_sticky: false, materialization_context: None, } @@ -495,7 +494,11 @@ impl<'a, H: Host> AccountResolver<'a, H> { ) -> Result { let timestamp = H::current_time(); - let resolve_request = request.resolve_request.as_ref().unwrap(); + let resolve_request = if let Some(req) = &request.resolve_request { + req + } else { + return Err("resolve_request is None".into()); + }; let flag_names = resolve_request.flags.clone(); let flags_to_resolve = self .state @@ -651,7 +654,10 @@ impl<'a, H: Host> AccountResolver<'a, H> { Ok(v) => match v.resolve_result { None => Err("failed to resolve flags".to_string()), Some(r) => match r { - ResolveResult::Success(flags_response) => Ok(flags_response.response.unwrap()), + ResolveResult::Success(flags_response) => match flags_response.response { + Some(flags_response) => Ok(flags_response), + None => Err("failed to resolve flags".to_string()), + }, ResolveResult::MissingMaterializations(_) => { Err("sticky assignments is not supported".to_string()) } @@ -1263,7 +1269,7 @@ impl<'a> From<&ResolvedValue<'a>> for flags_resolver::ResolvedFlag { fn from(value: &ResolvedValue<'a>) -> Self { let mut resolved_flag = flags_resolver::ResolvedFlag { flag: value.flag.name.clone(), - reason: value.reason.clone() as i32, + reason: value.reason as i32, should_apply: value.should_apply, ..Default::default() }; @@ -1292,7 +1298,7 @@ impl<'a> From<&ResolvedValue<'a>> for flags_resolver::resolve_token_v1::Assigned fn from(value: &ResolvedValue<'a>) -> Self { let mut assigned_flag = flags_resolver::resolve_token_v1::AssignedFlag { flag: value.flag.name.clone(), - reason: value.reason.clone() as i32, + reason: value.reason as i32, fallthrough_assignments: value .fallthrough_rules .iter() From d5dde2fa74d41589e2e50e8537b4874377aa61f6 Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Wed, 24 Sep 2025 12:08:41 +0200 Subject: [PATCH 11/18] fix clippy --- confidence-cloudflare-resolver/src/lib.rs | 6 ++-- confidence-resolver/src/lib.rs | 42 +++++++++++++++-------- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/confidence-cloudflare-resolver/src/lib.rs b/confidence-cloudflare-resolver/src/lib.rs index 4e29654..169c606 100644 --- a/confidence-cloudflare-resolver/src/lib.rs +++ b/confidence-cloudflare-resolver/src/lib.rs @@ -33,7 +33,7 @@ static FLAGS_LOGS_QUEUE: OnceLock = OnceLock::new(); static CONFIDENCE_CLIENT_ID: OnceLock = OnceLock::new(); static CONFIDENCE_CLIENT_SECRET: OnceLock = OnceLock::new(); -static FLAG_LOGGER: Lazy = Lazy::new(|| Logger::new()); +static FLAG_LOGGER: Lazy = Lazy::new(Logger::new); static RESOLVER_STATE: Lazy = Lazy::new(|| { ResolverState::from_proto(STATE_JSON.to_owned().try_into().unwrap(), ACCOUNT_ID).unwrap() @@ -192,9 +192,7 @@ pub async fn main(req: Request, env: Env, _ctx: Context) -> Result { &Bytes::from(STANDARD.decode(ENCRYPTION_KEY_BASE64).unwrap()), ) { Ok(resolver) => match resolver.apply_flags(&apply_flag_req) { - Ok(()) => { - return Response::from_json(&ApplyFlagsResponse::default()); - } + Ok(()) => Response::from_json(&ApplyFlagsResponse::default()), Err(msg) => { Response::error(msg, 500)?.with_cors_headers(&allowed_origin) } diff --git a/confidence-resolver/src/lib.rs b/confidence-resolver/src/lib.rs index 56895de..f3e3d3a 100644 --- a/confidence-resolver/src/lib.rs +++ b/confidence-resolver/src/lib.rs @@ -57,10 +57,12 @@ use proto::confidence::flags::resolver::v1::{ }; use crate::err::{ErrorCode, OrFailExt}; +use crate::flag_logger::Logger; use crate::proto::confidence::flags::resolver::v1::{ MissingMaterializationItem, MissingMaterializations, ResolveFlagsRequest, ResolveFlagsResponse, ResolveWithStickyRequest, ResolveWithStickySuccess, }; +use crate::resolve_logger::ResolveLogger; use crate::ResolveWithStickyContext::NoStickyContext; impl TryFrom> for ResolverStatePb { @@ -472,6 +474,18 @@ impl ResolveWithStickyRequest { } } +impl Default for ResolveLogger { + fn default() -> Self { + Self::new() + } +} + +impl Default for Logger { + fn default() -> Self { + Self::new() + } +} + impl<'a, H: Host> AccountResolver<'a, H> { pub fn new( client: &'a Client, @@ -646,7 +660,7 @@ impl<'a, H: Host> AccountResolver<'a, H> { sdk: request.sdk.clone(), evaluation_context: request.evaluation_context.clone(), client_secret: request.client_secret.clone(), - apply: request.apply.clone(), + apply: request.apply, }, )); @@ -847,17 +861,15 @@ impl<'a, H: Host> AccountResolver<'a, H> { continue; } false + } else if materialization_spec + .mode + .as_ref() + .map(|mode| mode.segment_targeting_can_be_ignored) + .unwrap_or(false) + { + true } else { - if materialization_spec - .mode - .as_ref() - .map(|mode| mode.segment_targeting_can_be_ignored) - .unwrap_or(false) - { - true - } else { - self.segment_match(segment, &unit)? - } + self.segment_match(segment, &unit)? }; if materialization_matched { @@ -915,10 +927,10 @@ impl<'a, H: Host> AccountResolver<'a, H> { .any(|range| range.lower <= bucket && bucket < range.upper) }); - let has_write_spec = match &rule.materialization_spec { - Some(materialization_spec) => Some(&materialization_spec.write_materialization), - None => None, - }; + let has_write_spec = rule + .materialization_spec + .as_ref() + .map(|materialization_spec| &materialization_spec.write_materialization); if let Some(assignment) = matched_assignment { let Some(a) = &assignment.assignment else { From 577812bbe05ae10af8a64f6b74b8f961ad39633d Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Wed, 24 Sep 2025 12:10:43 +0200 Subject: [PATCH 12/18] fixup! fix clippy --- confidence-resolver/src/lib.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/confidence-resolver/src/lib.rs b/confidence-resolver/src/lib.rs index f3e3d3a..21f0703 100644 --- a/confidence-resolver/src/lib.rs +++ b/confidence-resolver/src/lib.rs @@ -811,16 +811,16 @@ impl<'a, H: Host> AccountResolver<'a, H> { }; if let Some(materialization_spec) = &rule.materialization_spec { - let rule_name = &rule.name; - let read_materialization = materialization_spec.read_materialization.clone(); + let rule_name = &rule.name.as_str(); + let read_materialization = materialization_spec.read_materialization.as_str(); if !read_materialization.is_empty() { // check if the materialization for the unit exists if let Some(ctx) = materialization_context { if !ctx.context.unit_materialization_info.contains_key(&unit) { missing_materializations.push(MissingMaterializationItem { - unit: unit.clone(), - rule: rule_name.clone(), - read_materialization, + unit, + rule: rule_name.to_string(), + read_materialization: read_materialization.to_string(), }); // check the other rule continue; From e9da866b7a4d925533be0f41235014665bb0b469 Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Wed, 24 Sep 2025 13:05:42 +0200 Subject: [PATCH 13/18] fix clippy --- .../protos/confidence/flags/resolver/v1/wasm_api.proto | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 confidence-resolver/protos/confidence/flags/resolver/v1/wasm_api.proto diff --git a/confidence-resolver/protos/confidence/flags/resolver/v1/wasm_api.proto b/confidence-resolver/protos/confidence/flags/resolver/v1/wasm_api.proto new file mode 100644 index 0000000..e69de29 From 43cc4e0af14ae02542888a1e1da93edc738649d1 Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Wed, 24 Sep 2025 13:06:11 +0200 Subject: [PATCH 14/18] move to wasm_api --- confidence-resolver/build.rs | 1 + .../confidence/flags/resolver/v1/api.proto | 48 ------------- .../flags/resolver/v1/wasm_api.proto | 67 +++++++++++++++++++ confidence-resolver/src/lib.rs | 8 +-- wasm/proto/resolver/api.proto | 2 - 5 files changed, 71 insertions(+), 55 deletions(-) diff --git a/confidence-resolver/build.rs b/confidence-resolver/build.rs index 8769daa..297e5c5 100644 --- a/confidence-resolver/build.rs +++ b/confidence-resolver/build.rs @@ -9,6 +9,7 @@ fn main() -> Result<()> { root.join("confidence/flags/admin/v1/resolver.proto"), root.join("confidence/flags/resolver/v1/api.proto"), root.join("confidence/flags/resolver/v1/internal_api.proto"), + root.join("confidence/flags/resolver/v1/wasm_api.proto"), root.join("confidence/flags/resolver/v1/events/events.proto"), ]; diff --git a/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto b/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto index b777a60..65a00b8 100644 --- a/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto +++ b/confidence-resolver/protos/confidence/flags/resolver/v1/api.proto @@ -92,54 +92,6 @@ message ResolveFlagsRequest { ]; } -message ResolveWithStickyRequest { - ResolveFlagsRequest resolve_request = 1; - - // Context about the materialization required for the resolve - MaterializationContext materialization_context = 7; - - // if a materialization info is missing, we want tor return to the caller immediately - bool fail_fast_on_sticky = 8; -} - -message MaterializationContext { - map unit_materialization_info = 1; -} - -message MaterializationInfo { - bool unit_in_info = 1; - map rule_to_variant = 2; -} - -message ResolveFlagResponseResult { - oneof resolve_result { - ResolveWithStickySuccess success = 1; - MissingMaterializations missing_materializations = 2; - } -} - -message ResolveWithStickySuccess { - ResolveFlagsResponse response = 1; - repeated MaterializationUpdate updates = 2; -} - -message MissingMaterializations { - repeated MissingMaterializationItem items = 1; -} - -message MissingMaterializationItem { - string unit = 1; - string rule = 2; - string read_materialization = 3; -} - -message MaterializationUpdate { - string unit = 1; - string write_materialization = 2; - string rule = 3; - string variant = 4; -} - message ResolveFlagsResponse { // The list of all flags that could be resolved. Note: if any flag was // archived it will not be included in this list. diff --git a/confidence-resolver/protos/confidence/flags/resolver/v1/wasm_api.proto b/confidence-resolver/protos/confidence/flags/resolver/v1/wasm_api.proto index e69de29..9d6beae 100644 --- a/confidence-resolver/protos/confidence/flags/resolver/v1/wasm_api.proto +++ b/confidence-resolver/protos/confidence/flags/resolver/v1/wasm_api.proto @@ -0,0 +1,67 @@ +syntax = "proto3"; + +package confidence.flags.resolver.v1; + +import "google/api/resource.proto"; +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; + +import "confidence/api/annotations.proto"; +import "confidence/flags/types/v1/types.proto"; +import "confidence/flags/resolver/v1/types.proto"; +import "confidence/flags/resolver/v1/api.proto"; + +option java_package = "com.spotify.confidence.flags.resolver.v1"; +option java_multiple_files = true; +option java_outer_classname = "WasmApiProto"; + + +message ResolveWithStickyRequest { + ResolveFlagsRequest resolve_request = 1; + + // Context about the materialization required for the resolve + MaterializationContext materialization_context = 7; + + // if a materialization info is missing, we want tor return to the caller immediately + bool fail_fast_on_sticky = 8; +} + +message MaterializationContext { + map unit_materialization_info = 1; +} + +message MaterializationInfo { + bool unit_in_info = 1; + map rule_to_variant = 2; +} + +message ResolveFlagResponseResult { + oneof resolve_result { + ResolveWithStickySuccess success = 1; + MissingMaterializations missing_materializations = 2; + } +} + +message ResolveWithStickySuccess { + ResolveFlagsResponse response = 1; + repeated MaterializationUpdate updates = 2; +} + +message MissingMaterializations { + repeated MissingMaterializationItem items = 1; +} + +message MissingMaterializationItem { + string unit = 1; + string rule = 2; + string read_materialization = 3; +} + +message MaterializationUpdate { + string unit = 1; + string write_materialization = 2; + string rule = 3; + string variant = 4; +} \ No newline at end of file diff --git a/confidence-resolver/src/lib.rs b/confidence-resolver/src/lib.rs index 21f0703..0853775 100644 --- a/confidence-resolver/src/lib.rs +++ b/confidence-resolver/src/lib.rs @@ -51,15 +51,13 @@ use flags_types::targeting::criterion; use flags_types::targeting::Criterion; use flags_types::Expression; use gzip::decompress_gz; -use proto::confidence::flags::resolver::v1::resolve_flag_response_result::ResolveResult; -use proto::confidence::flags::resolver::v1::{ - MaterializationContext, MaterializationUpdate, ResolveFlagResponseResult, -}; use crate::err::{ErrorCode, OrFailExt}; use crate::flag_logger::Logger; +use crate::proto::confidence::flags::resolver::v1::resolve_flag_response_result::ResolveResult; use crate::proto::confidence::flags::resolver::v1::{ - MissingMaterializationItem, MissingMaterializations, ResolveFlagsRequest, ResolveFlagsResponse, + MaterializationContext, MaterializationUpdate, MissingMaterializationItem, + MissingMaterializations, ResolveFlagResponseResult, ResolveFlagsRequest, ResolveFlagsResponse, ResolveWithStickyRequest, ResolveWithStickySuccess, }; use crate::resolve_logger::ResolveLogger; diff --git a/wasm/proto/resolver/api.proto b/wasm/proto/resolver/api.proto index a71fd4f..a84e52d 100644 --- a/wasm/proto/resolver/api.proto +++ b/wasm/proto/resolver/api.proto @@ -39,8 +39,6 @@ message ResolveFlagsRequest { // Sdk sdk = 5; } - - message ResolveFlagsResponse { // The list of all flags that could be resolved. Note: if any flag was // archived it will not be included in this list. From 8ee81ff343eb7bf952269cdddaee14170b95197b Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Thu, 25 Sep 2025 11:12:31 +0200 Subject: [PATCH 15/18] move collect deps out of resolve flag --- confidence-resolver/src/lib.rs | 197 ++++++++++++++++----------------- 1 file changed, 97 insertions(+), 100 deletions(-) diff --git a/confidence-resolver/src/lib.rs b/confidence-resolver/src/lib.rs index 0853775..25b5d4f 100644 --- a/confidence-resolver/src/lib.rs +++ b/confidence-resolver/src/lib.rs @@ -61,7 +61,7 @@ use crate::proto::confidence::flags::resolver::v1::{ ResolveWithStickyRequest, ResolveWithStickySuccess, }; use crate::resolve_logger::ResolveLogger; -use crate::ResolveWithStickyContext::NoStickyContext; +use crate::ResolveWithStickyContext::EmptyStickyContext; impl TryFrom> for ResolverStatePb { type Error = ErrorCode; @@ -400,40 +400,41 @@ pub struct AccountResolver<'a, H: Host> { pub enum ResolveWithStickyContext { WithStickyContext(StickyResolveContext), - NoStickyContext, + EmptyStickyContext, } pub struct StickyResolveContext { context: MaterializationContext, - skip_on_not_missing: bool, } #[derive(Debug)] -pub struct ResolveFlagError { - pub message: String, - pub missing_materializations: Vec, +pub enum ResolveFlagError { + Message(String), + MissingMaterializations(), } impl ResolveFlagError { - pub fn err(message: &str) -> ResolveFlagError { - ResolveFlagError { - message: message.to_string(), - missing_materializations: vec![], + fn message(&self) -> String { + match self { + ResolveFlagError::Message(msg) => msg.clone(), + ResolveFlagError::MissingMaterializations() => "Missing materializations".to_string(), } } +} - pub fn missing_materializations(items: Vec) -> ResolveFlagError { - ResolveFlagError { - message: "Processing sticky assignments, missing materializations from the store" - .to_string(), - missing_materializations: items, - } +impl ResolveFlagError { + pub fn err(message: &str) -> ResolveFlagError { + ResolveFlagError::Message(message.to_string()) + } + + pub fn missing_materializations() -> ResolveFlagError { + ResolveFlagError::MissingMaterializations() } } impl From for String { fn from(value: ResolveFlagError) -> Self { - value.message.to_string() + value.message().to_string() } } @@ -535,43 +536,44 @@ impl<'a, H: Host> AccountResolver<'a, H> { } let mut resolve_results = Vec::with_capacity(flags_to_resolve.len()); - let mut missing_materialization_items: Vec = vec![]; for flag in flags_to_resolve { let sticky_context = if let Some(context) = &request.materialization_context { ResolveWithStickyContext::WithStickyContext(StickyResolveContext { - // don't evaluate the flag when already have some dependency missing - skip_on_not_missing: !missing_materialization_items.is_empty(), context: context.clone(), }) } else { - NoStickyContext + EmptyStickyContext }; let resolve_result = self.resolve_flag(flag, sticky_context); match resolve_result { Ok(resolve_result) => resolve_results.push(resolve_result), - Err(err) => { - if err.missing_materializations.is_empty() { - return Err(err.message.to_string()); - } else { - missing_materialization_items.extend(err.missing_materializations); - if request.fail_fast_on_sticky { - return Ok(ResolveFlagResponseResult::with_missing_materializations( - missing_materialization_items, - )); + return match err { + ResolveFlagError::Message(msg) => Err(msg.to_string()), + ResolveFlagError::MissingMaterializations() => { + // we want to fallback on online resolver, return early + if request.fail_fast_on_sticky { + Ok(ResolveFlagResponseResult::with_missing_materializations( + vec![], + )) + } else { + let deps = self.collect_missing_materializations(&flag); + match deps { + Ok(deps) => Ok( + ResolveFlagResponseResult::with_missing_materializations( + deps, + ), + ), + Err(err) => Err(err), + } + } } - } + }; } } } - if !missing_materialization_items.is_empty() { - return Ok(ResolveFlagResponseResult::with_missing_materializations( - missing_materialization_items, - )); - } - let resolved_values: Vec<&ResolvedValue> = resolve_results.iter().map(|r| &r.resolved_value).collect(); @@ -750,7 +752,50 @@ impl<'a, H: Host> AccountResolver<'a, H> { .flags .get(flag_name) .ok_or(ResolveFlagError::err("flag not found")) - .and_then(|flag| self.resolve_flag(flag, NoStickyContext)) + .and_then(|flag| self.resolve_flag(flag, EmptyStickyContext)) + } + + pub fn collect_missing_materializations( + &'a self, + flag: &'a Flag, + ) -> Result, String> { + let mut missing_materializations: Vec = Vec::new(); + + if flag.state == flags_admin::flag::State::Archived as i32 { + return Ok(vec![]); + } + + for rule in &flag.rules { + if !rule.enabled { + continue; + } + + let targeting_key = if !rule.targeting_key_selector.is_empty() { + rule.targeting_key_selector.as_str() + } else { + TARGETING_KEY + }; + let unit: String = match self.get_targeting_key(targeting_key) { + Ok(Some(u)) => u, + Ok(None) => continue, + Err(_) => return Err("Targeting key error".to_string()), + }; + + if let Some(materialization_spec) = &rule.materialization_spec { + let rule_name = &rule.name.as_str(); + let read_materialization = materialization_spec.read_materialization.as_str(); + if !read_materialization.is_empty() { + missing_materializations.push(MissingMaterializationItem { + unit, + rule: rule_name.to_string(), + read_materialization: read_materialization.to_string(), + }); + continue; + } + } + } + + Ok(missing_materializations) } pub fn resolve_flag( @@ -759,7 +804,6 @@ impl<'a, H: Host> AccountResolver<'a, H> { sticky_context: ResolveWithStickyContext, ) -> Result, ResolveFlagError> { let mut updates: Vec = Vec::new(); - let mut missing_materializations: Vec = Vec::new(); let mut resolved_value = ResolvedValue::new(flag); if flag.state == flags_admin::flag::State::Archived as i32 { @@ -769,17 +813,6 @@ impl<'a, H: Host> AccountResolver<'a, H> { }); } - let materialization_context = match &sticky_context { - ResolveWithStickyContext::WithStickyContext(ctx) => Some(ctx), - NoStickyContext => None, - }; - - let skip_evaluation = if let Some(ctx) = &materialization_context { - ctx.skip_on_not_missing - } else { - false - }; - for rule in &flag.rules { if !rule.enabled { continue; @@ -808,47 +841,17 @@ impl<'a, H: Host> AccountResolver<'a, H> { } }; - if let Some(materialization_spec) = &rule.materialization_spec { - let rule_name = &rule.name.as_str(); - let read_materialization = materialization_spec.read_materialization.as_str(); - if !read_materialization.is_empty() { - // check if the materialization for the unit exists - if let Some(ctx) = materialization_context { - if !ctx.context.unit_materialization_info.contains_key(&unit) { - missing_materializations.push(MissingMaterializationItem { - unit, - rule: rule_name.to_string(), - read_materialization: read_materialization.to_string(), - }); - // check the other rule - continue; - } - } - } - } - - if !missing_materializations.is_empty() || skip_evaluation { - /*** - don't want to evaluate the rules when we have missing dependencies - or when we are in discovery mode (finding missing dependencies for rules) - */ - continue; - } - let Some(spec) = &rule.assignment_spec else { continue; }; let mut materialization_matched = false; - - if let Some(ctx) = materialization_context { - // now we have all the dependencies required for evaluating the sticky assignments - if let Some(materialization_spec) = &rule.materialization_spec { + if let Some(materialization_spec) = &rule.materialization_spec { + if let ResolveWithStickyContext::WithStickyContext(ctx) = &sticky_context { let read_materialization = &materialization_spec.read_materialization; if !read_materialization.is_empty() { - let materialization_info = ctx.context.unit_materialization_info.get(&unit); - if let Some(materialization_info) = materialization_info { - materialization_matched = if !materialization_info.unit_in_info { + if let Some(info) = ctx.context.unit_materialization_info.get(&unit) { + materialization_matched = if !info.unit_in_info { if materialization_spec .mode .as_ref() @@ -869,11 +872,8 @@ impl<'a, H: Host> AccountResolver<'a, H> { } else { self.segment_match(segment, &unit)? }; - if materialization_matched { - if let Some(variant_name) = - materialization_info.rule_to_variant.get(&rule.name) - { + if let Some(variant_name) = info.rule_to_variant.get(&rule.name) { if let Some(assignment) = spec.assignments.iter().find(|assignment| { if let Some(rule::assignment::Assignment::Variant( @@ -904,8 +904,12 @@ impl<'a, H: Host> AccountResolver<'a, H> { } } } - } + } else { + materialization_matched = false; + }; } + } else { + return Err(ResolveFlagError::missing_materializations()); } } @@ -999,12 +1003,6 @@ impl<'a, H: Host> AccountResolver<'a, H> { } } - if !missing_materializations.is_empty() { - return Err(ResolveFlagError::missing_materializations( - missing_materializations, - )); - } - if resolved_value.reason == ResolveReason::Match { resolved_value.should_apply = true; } else { @@ -1388,7 +1386,6 @@ pub fn bucket(hash: u128, buckets: u64) -> usize { mod tests { use super::*; use crate::proto::confidence::flags::resolver::v1::{ResolveFlagsResponse, Sdk}; - use crate::ResolveWithStickyContext::NoStickyContext; const EXAMPLE_STATE: &[u8] = include_bytes!("../test-payloads/resolver_state.pb"); const SECRET: &str = "mkjJruAATQWjeY7foFIWfVAcBWnci2YF"; @@ -1501,7 +1498,7 @@ mod tests { .unwrap(); let flag = resolver.state.flags.get("flags/tutorial-feature").unwrap(); let resolve_result = resolver - .resolve_flag(flag, ResolveWithStickyContext::NoStickyContext) + .resolve_flag(flag, ResolveWithStickyContext::EmptyStickyContext) .unwrap(); let resolved_value = &resolve_result.resolved_value; let assignment_match = resolved_value.assignment_match.as_ref().unwrap(); @@ -1524,7 +1521,7 @@ mod tests { .unwrap(); let flag = resolver.state.flags.get("flags/tutorial-feature").unwrap(); let assignment_match = resolver - .resolve_flag(flag, ResolveWithStickyContext::NoStickyContext) + .resolve_flag(flag, ResolveWithStickyContext::EmptyStickyContext) .unwrap() .resolved_value .assignment_match @@ -1921,7 +1918,7 @@ mod tests { .flags .get("flags/fallthrough-test-2") .unwrap(); - let resolve_result = resolver.resolve_flag(flag, NoStickyContext).unwrap(); + let resolve_result = resolver.resolve_flag(flag, EmptyStickyContext).unwrap(); let resolved_value = &resolve_result.resolved_value; assert_eq!(resolved_value.reason as i32, ResolveReason::Match as i32); @@ -1948,7 +1945,7 @@ mod tests { .flags .get("flags/fallthrough-test-2") .unwrap(); - let resolve_result = resolver.resolve_flag(flag, NoStickyContext).unwrap(); + let resolve_result = resolver.resolve_flag(flag, EmptyStickyContext).unwrap(); let resolved_value = &resolve_result.resolved_value; assert_eq!( From 691e0ff491a5704f18314711a689ee107592ee66 Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Thu, 25 Sep 2025 11:17:19 +0200 Subject: [PATCH 16/18] fixup! move collect deps out of resolve flag --- confidence-resolver/src/lib.rs | 37 +++++++++------------------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/confidence-resolver/src/lib.rs b/confidence-resolver/src/lib.rs index 25b5d4f..ebfacad 100644 --- a/confidence-resolver/src/lib.rs +++ b/confidence-resolver/src/lib.rs @@ -61,7 +61,6 @@ use crate::proto::confidence::flags::resolver::v1::{ ResolveWithStickyRequest, ResolveWithStickySuccess, }; use crate::resolve_logger::ResolveLogger; -use crate::ResolveWithStickyContext::EmptyStickyContext; impl TryFrom> for ResolverStatePb { type Error = ErrorCode; @@ -398,15 +397,6 @@ pub struct AccountResolver<'a, H: Host> { host: PhantomData, } -pub enum ResolveWithStickyContext { - WithStickyContext(StickyResolveContext), - EmptyStickyContext, -} - -pub struct StickyResolveContext { - context: MaterializationContext, -} - #[derive(Debug)] pub enum ResolveFlagError { Message(String), @@ -538,14 +528,7 @@ impl<'a, H: Host> AccountResolver<'a, H> { let mut resolve_results = Vec::with_capacity(flags_to_resolve.len()); for flag in flags_to_resolve { - let sticky_context = if let Some(context) = &request.materialization_context { - ResolveWithStickyContext::WithStickyContext(StickyResolveContext { - context: context.clone(), - }) - } else { - EmptyStickyContext - }; - let resolve_result = self.resolve_flag(flag, sticky_context); + let resolve_result = self.resolve_flag(flag, request.materialization_context.clone()); match resolve_result { Ok(resolve_result) => resolve_results.push(resolve_result), Err(err) => { @@ -752,7 +735,7 @@ impl<'a, H: Host> AccountResolver<'a, H> { .flags .get(flag_name) .ok_or(ResolveFlagError::err("flag not found")) - .and_then(|flag| self.resolve_flag(flag, EmptyStickyContext)) + .and_then(|flag| self.resolve_flag(flag, None)) } pub fn collect_missing_materializations( @@ -801,7 +784,7 @@ impl<'a, H: Host> AccountResolver<'a, H> { pub fn resolve_flag( &'a self, flag: &'a Flag, - sticky_context: ResolveWithStickyContext, + sticky_context: Option, ) -> Result, ResolveFlagError> { let mut updates: Vec = Vec::new(); let mut resolved_value = ResolvedValue::new(flag); @@ -847,10 +830,10 @@ impl<'a, H: Host> AccountResolver<'a, H> { let mut materialization_matched = false; if let Some(materialization_spec) = &rule.materialization_spec { - if let ResolveWithStickyContext::WithStickyContext(ctx) = &sticky_context { + if let Some(context) = &sticky_context { let read_materialization = &materialization_spec.read_materialization; if !read_materialization.is_empty() { - if let Some(info) = ctx.context.unit_materialization_info.get(&unit) { + if let Some(info) = context.unit_materialization_info.get(&unit) { materialization_matched = if !info.unit_in_info { if materialization_spec .mode @@ -1497,9 +1480,7 @@ mod tests { .get_resolver_with_json_context(SECRET, context_json, &ENCRYPTION_KEY) .unwrap(); let flag = resolver.state.flags.get("flags/tutorial-feature").unwrap(); - let resolve_result = resolver - .resolve_flag(flag, ResolveWithStickyContext::EmptyStickyContext) - .unwrap(); + let resolve_result = resolver.resolve_flag(flag, None).unwrap(); let resolved_value = &resolve_result.resolved_value; let assignment_match = resolved_value.assignment_match.as_ref().unwrap(); @@ -1521,7 +1502,7 @@ mod tests { .unwrap(); let flag = resolver.state.flags.get("flags/tutorial-feature").unwrap(); let assignment_match = resolver - .resolve_flag(flag, ResolveWithStickyContext::EmptyStickyContext) + .resolve_flag(flag, None) .unwrap() .resolved_value .assignment_match @@ -1918,7 +1899,7 @@ mod tests { .flags .get("flags/fallthrough-test-2") .unwrap(); - let resolve_result = resolver.resolve_flag(flag, EmptyStickyContext).unwrap(); + let resolve_result = resolver.resolve_flag(flag, None).unwrap(); let resolved_value = &resolve_result.resolved_value; assert_eq!(resolved_value.reason as i32, ResolveReason::Match as i32); @@ -1945,7 +1926,7 @@ mod tests { .flags .get("flags/fallthrough-test-2") .unwrap(); - let resolve_result = resolver.resolve_flag(flag, EmptyStickyContext).unwrap(); + let resolve_result = resolver.resolve_flag(flag, None).unwrap(); let resolved_value = &resolve_result.resolved_value; assert_eq!( From fd118554ce42a5a46a883902406e0cc80d6465ce Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Fri, 26 Sep 2025 13:29:06 +0200 Subject: [PATCH 17/18] fix comments --- .../flags/resolver/v1/wasm_api.proto | 2 +- confidence-resolver/src/lib.rs | 76 +++++++------------ wasm/rust-guest/src/lib.rs | 4 +- 3 files changed, 32 insertions(+), 50 deletions(-) diff --git a/confidence-resolver/protos/confidence/flags/resolver/v1/wasm_api.proto b/confidence-resolver/protos/confidence/flags/resolver/v1/wasm_api.proto index 9d6beae..d83c7c9 100644 --- a/confidence-resolver/protos/confidence/flags/resolver/v1/wasm_api.proto +++ b/confidence-resolver/protos/confidence/flags/resolver/v1/wasm_api.proto @@ -37,7 +37,7 @@ message MaterializationInfo { map rule_to_variant = 2; } -message ResolveFlagResponseResult { +message ResolveFlagStickyResponse { oneof resolve_result { ResolveWithStickySuccess success = 1; MissingMaterializations missing_materializations = 2; diff --git a/confidence-resolver/src/lib.rs b/confidence-resolver/src/lib.rs index ebfacad..153182e 100644 --- a/confidence-resolver/src/lib.rs +++ b/confidence-resolver/src/lib.rs @@ -53,11 +53,10 @@ use flags_types::Expression; use gzip::decompress_gz; use crate::err::{ErrorCode, OrFailExt}; -use crate::flag_logger::Logger; -use crate::proto::confidence::flags::resolver::v1::resolve_flag_response_result::ResolveResult; +use crate::proto::confidence::flags::resolver::v1::resolve_flag_sticky_response::ResolveResult; use crate::proto::confidence::flags::resolver::v1::{ MaterializationContext, MaterializationUpdate, MissingMaterializationItem, - MissingMaterializations, ResolveFlagResponseResult, ResolveFlagsRequest, ResolveFlagsResponse, + MissingMaterializations, ResolveFlagStickyResponse, ResolveFlagsRequest, ResolveFlagsResponse, ResolveWithStickyRequest, ResolveWithStickySuccess, }; use crate::resolve_logger::ResolveLogger; @@ -410,9 +409,7 @@ impl ResolveFlagError { ResolveFlagError::MissingMaterializations() => "Missing materializations".to_string(), } } -} -impl ResolveFlagError { pub fn err(message: &str) -> ResolveFlagError { ResolveFlagError::Message(message.to_string()) } @@ -434,9 +431,9 @@ impl From for ResolveFlagError { } } -impl ResolveFlagResponseResult { +impl ResolveFlagStickyResponse { fn with_success(response: ResolveFlagsResponse, updates: Vec) -> Self { - ResolveFlagResponseResult { + ResolveFlagStickyResponse { resolve_result: Some(ResolveResult::Success(ResolveWithStickySuccess { response: Some(response), updates, @@ -445,7 +442,7 @@ impl ResolveFlagResponseResult { } fn with_missing_materializations(items: Vec) -> Self { - ResolveFlagResponseResult { + ResolveFlagStickyResponse { resolve_result: Some(ResolveResult::MissingMaterializations( MissingMaterializations { items }, )), @@ -458,23 +455,11 @@ impl ResolveWithStickyRequest { ResolveWithStickyRequest { resolve_request: Some(resolve_request), fail_fast_on_sticky: false, - materialization_context: None, + materialization_context: Some(MaterializationContext::default()), } } } -impl Default for ResolveLogger { - fn default() -> Self { - Self::new() - } -} - -impl Default for Logger { - fn default() -> Self { - Self::new() - } -} - impl<'a, H: Host> AccountResolver<'a, H> { pub fn new( client: &'a Client, @@ -494,14 +479,10 @@ impl<'a, H: Host> AccountResolver<'a, H> { pub fn resolve_flags_sticky( &self, request: &flags_resolver::ResolveWithStickyRequest, - ) -> Result { + ) -> Result { let timestamp = H::current_time(); - let resolve_request = if let Some(req) = &request.resolve_request { - req - } else { - return Err("resolve_request is None".into()); - }; + let resolve_request = &request.resolve_request.clone().or_fail()?; let flag_names = resolve_request.flags.clone(); let flags_to_resolve = self .state @@ -537,14 +518,14 @@ impl<'a, H: Host> AccountResolver<'a, H> { ResolveFlagError::MissingMaterializations() => { // we want to fallback on online resolver, return early if request.fail_fast_on_sticky { - Ok(ResolveFlagResponseResult::with_missing_materializations( + Ok(ResolveFlagStickyResponse::with_missing_materializations( vec![], )) } else { let deps = self.collect_missing_materializations(&flag); match deps { Ok(deps) => Ok( - ResolveFlagResponseResult::with_missing_materializations( + ResolveFlagStickyResponse::with_missing_materializations( deps, ), ), @@ -557,8 +538,10 @@ impl<'a, H: Host> AccountResolver<'a, H> { } } - let resolved_values: Vec<&ResolvedValue> = - resolve_results.iter().map(|r| &r.resolved_value).collect(); + let resolved_values: Vec = resolve_results + .iter() + .map(|r| r.resolved_value.clone()) + .collect(); let resolve_id = H::random_alphanumeric(32); let mut response = flags_resolver::ResolveFlagsResponse { @@ -567,7 +550,7 @@ impl<'a, H: Host> AccountResolver<'a, H> { }; let mut updates: Vec = vec![]; for resolved_value in &resolved_values { - response.resolved_flags.push((*resolved_value).into()); + response.resolved_flags.push(resolved_value.into()); } // Collect all materialization updates from all resolve results @@ -580,7 +563,7 @@ impl<'a, H: Host> AccountResolver<'a, H> { .iter() .filter(|v| v.should_apply) .map(|v| FlagToApply { - assigned_flag: (*v).into(), + assigned_flag: v.into(), skew_adjusted_applied_time: timestamp.clone(), }) .collect(); @@ -600,7 +583,7 @@ impl<'a, H: Host> AccountResolver<'a, H> { ..Default::default() }; for resolved_value in &resolved_values { - let assigned_flag: AssignedFlag = (*resolved_value).into(); + let assigned_flag: AssignedFlag = resolved_value.into(); resolve_token_v1 .assignments .insert(assigned_flag.flag.clone(), assigned_flag); @@ -630,7 +613,7 @@ impl<'a, H: Host> AccountResolver<'a, H> { &resolve_request.sdk.clone(), ); - Ok(ResolveFlagResponseResult::with_success(response, updates)) + Ok(ResolveFlagStickyResponse::with_success(response, updates)) } pub fn resolve_flags( @@ -753,21 +736,20 @@ impl<'a, H: Host> AccountResolver<'a, H> { continue; } - let targeting_key = if !rule.targeting_key_selector.is_empty() { - rule.targeting_key_selector.as_str() - } else { - TARGETING_KEY - }; - let unit: String = match self.get_targeting_key(targeting_key) { - Ok(Some(u)) => u, - Ok(None) => continue, - Err(_) => return Err("Targeting key error".to_string()), - }; - if let Some(materialization_spec) = &rule.materialization_spec { let rule_name = &rule.name.as_str(); let read_materialization = materialization_spec.read_materialization.as_str(); if !read_materialization.is_empty() { + let targeting_key = if !rule.targeting_key_selector.is_empty() { + rule.targeting_key_selector.as_str() + } else { + TARGETING_KEY + }; + let unit: String = match self.get_targeting_key(targeting_key) { + Ok(Some(u)) => u, + Ok(None) => continue, + Err(_) => return Err("Targeting key error".to_string()), + }; missing_materializations.push(MissingMaterializationItem { unit, rule: rule_name.to_string(), @@ -998,7 +980,7 @@ impl<'a, H: Host> AccountResolver<'a, H> { }) } - /// Get an attribute value from the [EvaluationContext] struct, adressed by a path specification. + /// Get an attribute value from the [EvaluationContext] struct, addressed by a path specification. /// If the struct is `{user:{name:"roug",id:42}}`, then getting the `"user.name"` field will return /// the value `"roug"`. pub fn get_attribute_value(&self, field_path: &str) -> &Value { diff --git a/wasm/rust-guest/src/lib.rs b/wasm/rust-guest/src/lib.rs index 48b2bb7..25121db 100644 --- a/wasm/rust-guest/src/lib.rs +++ b/wasm/rust-guest/src/lib.rs @@ -31,7 +31,7 @@ use confidence_resolver::{ proto::{ confidence::flags::admin::v1::ResolverState as ResolverStatePb, confidence::flags::resolver::v1::{ - ResolveFlagResponseResult, ResolveFlagsRequest, ResolveFlagsResponse, ResolvedFlag, Sdk, + ResolveFlagStickyResponse, ResolveFlagsRequest, ResolveFlagsResponse, ResolvedFlag, Sdk, }, google::{Struct, Timestamp}, }, @@ -186,7 +186,7 @@ wasm_msg_guest! { Ok(VOID) } - fn resolve_with_sticky(request: ResolveWithStickyRequest) -> WasmResult { + fn resolve_with_sticky(request: ResolveWithStickyRequest) -> WasmResult { let resolver_state = get_resolver_state()?; let resolve_request = &request.resolve_request.clone().unwrap(); let evaluation_context = resolve_request.evaluation_context.clone().unwrap(); From 9e3276e6c430401ef9d2926f1ee611f01f48975d Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Tue, 30 Sep 2025 11:32:06 +0200 Subject: [PATCH 18/18] move the materialization types to the response --- .../flags/resolver/v1/wasm_api.proto | 41 ++++++------- confidence-resolver/src/lib.rs | 58 +++++++++++-------- wasm/rust-guest/src/lib.rs | 4 +- 3 files changed, 56 insertions(+), 47 deletions(-) diff --git a/confidence-resolver/protos/confidence/flags/resolver/v1/wasm_api.proto b/confidence-resolver/protos/confidence/flags/resolver/v1/wasm_api.proto index d83c7c9..73c6cd3 100644 --- a/confidence-resolver/protos/confidence/flags/resolver/v1/wasm_api.proto +++ b/confidence-resolver/protos/confidence/flags/resolver/v1/wasm_api.proto @@ -37,31 +37,32 @@ message MaterializationInfo { map rule_to_variant = 2; } -message ResolveFlagStickyResponse { +message ResolveWithStickyResponse { oneof resolve_result { - ResolveWithStickySuccess success = 1; + Success success = 1; MissingMaterializations missing_materializations = 2; } -} -message ResolveWithStickySuccess { - ResolveFlagsResponse response = 1; - repeated MaterializationUpdate updates = 2; -} + message Success { + ResolveFlagsResponse response = 1; + repeated MaterializationUpdate updates = 2; + } -message MissingMaterializations { - repeated MissingMaterializationItem items = 1; -} + message MissingMaterializations { + repeated MissingMaterializationItem items = 1; + } -message MissingMaterializationItem { - string unit = 1; - string rule = 2; - string read_materialization = 3; + message MissingMaterializationItem { + string unit = 1; + string rule = 2; + string read_materialization = 3; + } + + message MaterializationUpdate { + string unit = 1; + string write_materialization = 2; + string rule = 3; + string variant = 4; + } } -message MaterializationUpdate { - string unit = 1; - string write_materialization = 2; - string rule = 3; - string variant = 4; -} \ No newline at end of file diff --git a/confidence-resolver/src/lib.rs b/confidence-resolver/src/lib.rs index 153182e..3f3d03b 100644 --- a/confidence-resolver/src/lib.rs +++ b/confidence-resolver/src/lib.rs @@ -53,13 +53,13 @@ use flags_types::Expression; use gzip::decompress_gz; use crate::err::{ErrorCode, OrFailExt}; -use crate::proto::confidence::flags::resolver::v1::resolve_flag_sticky_response::ResolveResult; +use crate::proto::confidence::flags::resolver::v1::resolve_with_sticky_response::{ + MaterializationUpdate, ResolveResult, +}; use crate::proto::confidence::flags::resolver::v1::{ - MaterializationContext, MaterializationUpdate, MissingMaterializationItem, - MissingMaterializations, ResolveFlagStickyResponse, ResolveFlagsRequest, ResolveFlagsResponse, - ResolveWithStickyRequest, ResolveWithStickySuccess, + resolve_with_sticky_response, MaterializationContext, ResolveFlagsRequest, + ResolveFlagsResponse, ResolveWithStickyRequest, ResolveWithStickyResponse, }; -use crate::resolve_logger::ResolveLogger; impl TryFrom> for ResolverStatePb { type Error = ErrorCode; @@ -431,20 +431,24 @@ impl From for ResolveFlagError { } } -impl ResolveFlagStickyResponse { +impl ResolveWithStickyResponse { fn with_success(response: ResolveFlagsResponse, updates: Vec) -> Self { - ResolveFlagStickyResponse { - resolve_result: Some(ResolveResult::Success(ResolveWithStickySuccess { - response: Some(response), - updates, - })), + ResolveWithStickyResponse { + resolve_result: Some(ResolveResult::Success( + resolve_with_sticky_response::Success { + response: Some(response), + updates, + }, + )), } } - fn with_missing_materializations(items: Vec) -> Self { - ResolveFlagStickyResponse { + fn with_missing_materializations( + items: Vec, + ) -> Self { + ResolveWithStickyResponse { resolve_result: Some(ResolveResult::MissingMaterializations( - MissingMaterializations { items }, + resolve_with_sticky_response::MissingMaterializations { items }, )), } } @@ -479,7 +483,7 @@ impl<'a, H: Host> AccountResolver<'a, H> { pub fn resolve_flags_sticky( &self, request: &flags_resolver::ResolveWithStickyRequest, - ) -> Result { + ) -> Result { let timestamp = H::current_time(); let resolve_request = &request.resolve_request.clone().or_fail()?; @@ -518,14 +522,14 @@ impl<'a, H: Host> AccountResolver<'a, H> { ResolveFlagError::MissingMaterializations() => { // we want to fallback on online resolver, return early if request.fail_fast_on_sticky { - Ok(ResolveFlagStickyResponse::with_missing_materializations( + Ok(ResolveWithStickyResponse::with_missing_materializations( vec![], )) } else { let deps = self.collect_missing_materializations(&flag); match deps { Ok(deps) => Ok( - ResolveFlagStickyResponse::with_missing_materializations( + ResolveWithStickyResponse::with_missing_materializations( deps, ), ), @@ -613,7 +617,7 @@ impl<'a, H: Host> AccountResolver<'a, H> { &resolve_request.sdk.clone(), ); - Ok(ResolveFlagStickyResponse::with_success(response, updates)) + Ok(ResolveWithStickyResponse::with_success(response, updates)) } pub fn resolve_flags( @@ -724,8 +728,10 @@ impl<'a, H: Host> AccountResolver<'a, H> { pub fn collect_missing_materializations( &'a self, flag: &'a Flag, - ) -> Result, String> { - let mut missing_materializations: Vec = Vec::new(); + ) -> Result, String> { + let mut missing_materializations: Vec< + resolve_with_sticky_response::MissingMaterializationItem, + > = Vec::new(); if flag.state == flags_admin::flag::State::Archived as i32 { return Ok(vec![]); @@ -750,11 +756,13 @@ impl<'a, H: Host> AccountResolver<'a, H> { Ok(None) => continue, Err(_) => return Err("Targeting key error".to_string()), }; - missing_materializations.push(MissingMaterializationItem { - unit, - rule: rule_name.to_string(), - read_materialization: read_materialization.to_string(), - }); + missing_materializations.push( + resolve_with_sticky_response::MissingMaterializationItem { + unit, + rule: rule_name.to_string(), + read_materialization: read_materialization.to_string(), + }, + ); continue; } } diff --git a/wasm/rust-guest/src/lib.rs b/wasm/rust-guest/src/lib.rs index 25121db..c55c042 100644 --- a/wasm/rust-guest/src/lib.rs +++ b/wasm/rust-guest/src/lib.rs @@ -31,7 +31,7 @@ use confidence_resolver::{ proto::{ confidence::flags::admin::v1::ResolverState as ResolverStatePb, confidence::flags::resolver::v1::{ - ResolveFlagStickyResponse, ResolveFlagsRequest, ResolveFlagsResponse, ResolvedFlag, Sdk, + ResolveFlagsRequest, ResolveFlagsResponse, ResolveWithStickyResponse, ResolvedFlag, Sdk, }, google::{Struct, Timestamp}, }, @@ -186,7 +186,7 @@ wasm_msg_guest! { Ok(VOID) } - fn resolve_with_sticky(request: ResolveWithStickyRequest) -> WasmResult { + fn resolve_with_sticky(request: ResolveWithStickyRequest) -> WasmResult { let resolver_state = get_resolver_state()?; let resolve_request = &request.resolve_request.clone().unwrap(); let evaluation_context = resolve_request.evaluation_context.clone().unwrap();