From 56735bca5c8c26b36fa37662dd5809a2bf8ce716 Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Fri, 3 Oct 2025 13:29:19 +0200 Subject: [PATCH] update sticky --- STICKY_ASSIGNMENTS.md | 485 ++++++++++++++---- .../flags/resolver/v1/wasm_api.proto | 13 +- confidence-resolver/src/lib.rs | 120 +++-- wasm/rust-guest/src/lib.rs | 4 +- 4 files changed, 472 insertions(+), 150 deletions(-) diff --git a/STICKY_ASSIGNMENTS.md b/STICKY_ASSIGNMENTS.md index 2392912..79a8bee 100644 --- a/STICKY_ASSIGNMENTS.md +++ b/STICKY_ASSIGNMENTS.md @@ -59,20 +59,173 @@ When resolving a flag with sticky assignments enabled: 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 +#### Detailed Resolution Flowchart -The resolver accepts a `MaterializationContext` containing previous assignments: +The following flowchart illustrates the complete sticky assignment resolution logic for a single rule: -```protobuf -message MaterializationContext { - map unit_materialization_info = 1; -} - -message MaterializationInfo { - bool unit_in_info = 1; - map rule_to_variant = 2; -} ``` +┌─────────────────────────────────────────────┐ +│ Start: Processing Rule with Materialization │ +└──────────────────┬──────────────────────────┘ + │ + ▼ + ┌─────────────────────┐ + │ Read Materialization │ + │ Spec Defined? │ + └──────┬──────────────┘ + │ + ┌──────┴──────┐ + │ │ + NO YES + │ │ + │ ▼ + │ ┌─────────────────────────┐ + │ │ Get MaterializationInfo │ + │ │ from Context │ + │ └──────┬──────────────────┘ + │ │ + │ ┌──────┴──────┐ + │ │ │ + │ FOUND NOT FOUND + │ │ │ + │ │ ▼ + │ │ ┌────────────────────┐ + │ │ │ Return: Missing │ + │ │ │ Materialization │ + │ │ │ Error │ + │ │ └────────────────────┘ + │ │ + │ ▼ + │ ┌────────────────────────────┐ + │ │ Check: unit_in_info flag │ + │ └──────┬─────────────────────┘ + │ │ + │ ┌──────┴──────┐ + │ │ │ + │ FALSE TRUE + │ │ │ + │ │ Unit NOT │ Unit IS + │ │ in mat. │ in mat. + │ │ │ + │ ▼ ▼ + │ ┌──────────┐ ┌──────────────────────┐ + │ │materialization│ │segment_targeting_ │ + │ │_must_match? │ │can_be_ignored? │ + │ └──┬───────┘ └──┬────────────────────┘ + │ │ │ + │ ┌──┴──┐ ┌──┴──┐ + │ │ │ │ │ + │TRUE FALSE TRUE FALSE + │ │ │ │ │ + │ ▼ │ │ ▼ + │┌────────┐ │ ┌──────────────┐ + ││Skip │ │ │Check Segment │ + ││Rule │ │ │Match │ + │└────────┘ │ └──────┬───────┘ + │ │ │ │ + │ │ │ ┌────┴────┐ + │ │ │ │ │ + │ │ │ MATCH NO MATCH + │ │ │ │ │ + │ ▼ ▼ ▼ │ + │ ┌──────────┐ │ + │ │mat_matched│ │ + │ │= false │ │ + │ └─────┬─────┘ │ + │ │ │ + │ │ ┌────────┘ + │ │ │mat_matched + │ │ │= segment result + │ │ │ + │ │ ▼ + │ │ ┌──────────┐ + │ │ │mat_matched│ + │ │ │= true │ + │ │ └─────┬─────┘ + │ │ │ + └─────────────┴────────┴────────┐ + │ + ▼ + ┌─────────────────┐ + │ mat_matched? │ + └────┬────────────┘ + │ + ┌─────┴─────┐ + │ │ + YES NO + │ │ + ▼ ▼ + ┌──────────────────┐ ┌────────────────┐ + │ Check if variant │ │ Check Normal │ + │ exists in │ │ Segment Match? │ + │ rule_to_variant │ └────┬───────────┘ + └────┬─────────────┘ │ + │ ┌────┴────┐ + ┌────┴────┐ │ │ + │ │ YES NO + FOUND NOT FOUND │ │ + │ │ │ ▼ + ▼ │ │ ┌──────────┐ + ┌──────────────┐ │ │ │Skip Rule │ + │Return Sticky │ │ │ └──────────┘ + │Assignment │ │ │ + │(no updates) │ │ ▼ + └──────────────┘ │ ┌────────────────┐ + │ │ Calculate │ + │ │ Bucket & Find │ + └────┤ Assignment │ + └────┬───────────┘ + │ + ┌────┴────┐ + │ │ + FOUND NOT FOUND + │ │ + ▼ ▼ + ┌──────────────┐ ┌──────────┐ + │Has write_mat?│ │Skip Rule │ + └────┬─────────┘ └──────────┘ + │ + ┌────┴────┐ + │ │ + YES NO + │ │ + ▼ │ + ┌──────────────┐ │ + │Create Update │ │ + │for write_mat │ │ + └────┬─────────┘ │ + │ │ + └─────┬───────┘ + │ + ▼ + ┌──────────────────┐ + │ Return Assignment│ + │ with Updates │ + └──────────────────┘ +``` + +#### Key Decision Points + +1. **Unit Not in Materialization (`unit_in_info = false`)**: + - If `materialization_must_match = true`: **Skip rule** (paused intake - only existing users) + - Otherwise: Continue with normal segment evaluation + +2. **Unit in Materialization (`unit_in_info = true`)**: + - If `segment_targeting_can_be_ignored = true`: **Match immediately** (sticky users bypass targeting) + - Otherwise: Still check segment match (sticky users must meet targeting criteria) + +3. **Materialization Matched**: + - Look up previously assigned variant from `rule_to_variant` map + - Return sticky assignment (no new updates needed) + +4. **Normal Assignment**: + - Calculate bucket using hash + - Find matching assignment in bucket ranges + - If `write_materialization` specified: Create update for persistence + +### 4. Materialization Context + +The resolver accepts materialization context via the `materializations_per_unit` map in `ResolveWithStickyRequest`. This map goes from unit (targeting key) to `MaterializationMap`, which contains all the materialization information for that unit. ## Usage Patterns @@ -86,35 +239,47 @@ A rule with both read and write materialization will: ### 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 +- Only units already in the materialization (where `unit_in_info = true`) can proceed with rule evaluation +- New units (where `unit_in_info = false`) will skip this rule entirely +- Useful for controlled rollout scenarios where you want to stop accepting new users into an experiment while maintaining existing assignments ### 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 +- Units already in materialization (where `unit_in_info = true`) match the rule regardless of segment targeting +- Previously assigned variants are returned even if segment criteria no longer match +- Useful for maintaining assignments when targeting rules change or evolve over time ## API Integration ### Enable Sticky Assignments -Set `process_sticky = true` in the resolve request: +Use the `ResolveWithStickyRequest` message for sticky assignment support: ```protobuf -message ResolveFlagsRequest { - // ... other fields ... - - // if the resolver should handle sticky assignments - bool process_sticky = 6; +message ResolveWithStickyRequest { + ResolveFlagsRequest resolve_request = 1; // Context about the materialization required for the resolve - MaterializationContext materialization_context = 7; + // Map from unit (targeting key) to materialization data + map materializations_per_unit = 2; // if a materialization info is missing, return immediately - bool fail_fast_on_sticky = 8; + bool fail_fast_on_sticky = 3; +} + +message MaterializationMap { + // materialization name to info + map info_map = 1; +} + +message MaterializationInfo { + // true = unit IS in the materialization (has been assigned) + // false = unit is NOT in the materialization (new user) + bool unit_in_info = 1; + + // Map of rule names to assigned variant names for this unit + map rule_to_variant = 2; } ``` @@ -123,17 +288,33 @@ message ResolveFlagsRequest { The resolver may return `MissingMaterializations` when required materialization data is unavailable: ```protobuf -message ResolveFlagResponseResult { +message ResolveWithStickyResponse { oneof resolve_result { - ResolveFlagsResponse response = 1; + Success success = 1; MissingMaterializations missing_materializations = 2; } -} -message MissingMaterializationItem { - string unit = 1; - string rule = 2; - string read_materialization = 3; + message Success { + 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; + } } ``` @@ -159,7 +340,7 @@ Use "paused intake" mode to limit new user assignments while maintaining existin ### Materialization Updates -When assignments are made, the resolver returns `MaterializationUpdate` objects: +When assignments are made, the resolver returns `MaterializationUpdate` objects in the `Success` response: ```protobuf message MaterializationUpdate { @@ -170,7 +351,7 @@ message MaterializationUpdate { } ``` -These should be persisted by the client for use in future resolve requests. +These updates should be persisted by the client and included in the `materializations_per_unit` map for future resolve requests. ### Error Handling @@ -178,6 +359,110 @@ These should be persisted by the client for use in future resolve requests. - **Fail Fast**: `fail_fast_on_sticky` controls whether to return immediately or continue processing - **Dependency Checking**: The resolver validates all materialization dependencies before evaluation +## Multi-Flag Resolution Flow + +When resolving multiple flags with sticky assignments, the resolver uses a sophisticated flow to handle missing materializations efficiently: + +``` +┌──────────────────────────────────────┐ +│ Start: resolve_flags_sticky() │ +│ Input: Multiple flags + context │ +└────────────┬─────────────────────────┘ + │ + ▼ + ┌─────────────────────┐ + │ For each flag: │ + │ Process flag │ + └──────┬──────────────┘ + │ + ▼ + ┌───────────────────────────┐ + │ Try to resolve flag │ + └──────┬────────────────────┘ + │ + ┌─────┴─────┐ + │ │ + Success Error + │ │ + │ ┌────┴──────┐ + │ │ │ + │ Missing Other + │ Mat. Error + │ │ │ + │ ▼ ▼ + │ ┌──────────────────┐ + │ │fail_fast_on_ │ + │ │sticky = true? │ + │ └────┬────────┬────┘ + │ │ │ + │ YES NO + │ │ │ + │ ▼ ▼ + │ ┌────────┐ ┌───────────────┐ + │ │Return │ │Set has_missing│ + │ │Missing │ │= true; break │ + │ │Mat. │ │loop │ + │ │(empty) │ └────┬──────────┘ + │ └────────┘ │ + │ │ + ▼ ▼ + ┌───────────────────────┐ + │ All flags processed? │ + └────┬──────────────────┘ + │ + ┌────┴────┐ + │ │ +YES NO + │ └──────┐ + │ │ + ▼ │ +┌──────────────┐ │ +│has_missing │ │ +│= true? │ │ +└──┬───────────┘ │ + │ │ +┌──┴──┐ │ +│ │ │ +YES NO │ +│ │ │ +▼ │ │ +┌──────────────┐ │ +│Collect all │ │ +│missing mat. │ │ +│dependencies │ │ +└──┬───────────┘ │ + │ │ + ▼ │ +┌──────────────┐ │ +│Return │ │ +│Missing │ │ +│Mat. List │ │ +└──────────────┘ │ + │ + ┌──────────┘ + │ + ▼ +┌────────────────┐ +│Return Success │ +│with Resolved │ +│Flags + Updates │ +└────────────────┘ +``` + +### Fail Fast vs. Discovery Mode + +**Fail Fast Mode (`fail_fast_on_sticky = true`)**: +- Immediately returns when first missing materialization is detected +- Returns empty missing materialization list (signals caller to handle it) +- Stops processing remaining flags +- Best for production: fast failure for immediate remediation + +**Discovery Mode (`fail_fast_on_sticky = false`)**: +- Continues processing all flags even when missing materializations are found +- Collects ALL missing materializations across all flags +- Calls `collect_missing_materializations()` to gather complete dependency list +- Best for initialization: discover all required materializations in one pass + ## Advanced Optimizations ### Fail Fast on First Missing Materialization @@ -199,73 +484,101 @@ Flag B: ❌ Missing materialization + fail_fast=true → Return immediately with Flag C: (Not processed due to fail_fast) ``` -### Rule Evaluation Skipping Optimization +### Performance Considerations -The resolver implements a sophisticated optimization to avoid unnecessary rule evaluation when materialization dependencies are missing: +- **Early dependency validation**: When a rule requires a read_materialization, the resolver checks for it in the context before processing the rule logic +- **Fail fast on missing dependencies**: When a required MaterializationInfo is not found in the context, the resolver immediately returns an error without attempting rule evaluation +- **Selective dependency collection**: In discovery mode (`fail_fast_on_sticky = false`), after detecting any missing materialization, the resolver uses `collect_missing_materializations()` to efficiently gather all missing dependencies across all flags without full rule evaluation +- **Shared context efficiency**: Multiple flags can reference the same materialization context, avoiding redundant lookups -**The `skip_on_not_missing` Mechanism:** +## Best Practices -1. **Dependency Discovery Phase**: When processing multiple flags, if any previous flag had missing materializations, subsequent flags enter "discovery mode" +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 -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 +## Example Workflow -3. **Optimization Logic**: - ```rust - skip_on_not_missing: !missing_materialization_items.is_empty() - ``` +1. User requests flag resolution with empty or minimal `materializations_per_unit` map +2. Resolver assigns variants and returns `MaterializationUpdate`s in the success response +3. Client stores materialization data (variant assignments per unit/rule/materialization) +4. Subsequent requests include the stored data in the `materializations_per_unit` map +5. Resolver uses stored assignments when available, creating new ones as needed +6. Process continues with updated materialization context from new updates -**How It Works:** +This approach ensures assignment consistency while allowing new users to be assigned according to current targeting rules. -``` -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 -``` +## Quick Reference -**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 +### Key Flag Values -**Complete Resolution Flow:** +| Field | Value | Meaning | +|-------|-------|---------| +| `unit_in_info` | `true` | Unit **IS** in materialization (already assigned) | +| `unit_in_info` | `false` | Unit **is NOT** in materialization (new user) | +| `materialization_must_match` | `true` | Only accept units already in materialization (paused intake) | +| `materialization_must_match` | `false` | Accept both existing and new units | +| `segment_targeting_can_be_ignored` | `true` | Units in materialization bypass segment checks | +| `segment_targeting_can_be_ignored` | `false` | Units in materialization still need segment match | +| `fail_fast_on_sticky` | `true` | Return immediately on first missing materialization | +| `fail_fast_on_sticky` | `false` | Collect all missing materializations before returning | -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 +### Behavior Matrix -This optimization ensures efficient handling of complex dependency graphs while maintaining correctness and performance. +| unit_in_info | materialization_must_match | Result | +|--------------|---------------------------|--------| +| `false` (new) | `true` | **Skip rule** (paused intake) | +| `false` (new) | `false` | Normal segment evaluation | +| `true` (existing) | `true` | Continue processing | +| `true` (existing) | `false` | Continue processing | -### Performance Considerations +| unit_in_info | segment_targeting_can_be_ignored | Result | +|--------------|----------------------------------|--------| +| `true` (existing) | `true` | **Match immediately** (bypass targeting) | +| `true` (existing) | `false` | Check segment match required | +| `false` (new) | any | Not applicable (handled by must_match) | -- **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 +### Common Configuration Patterns -## Best Practices +**Standard Sticky Assignment:** +``` +read_materialization: "experiment_v1" +write_materialization: "experiment_v1" +mode: + materialization_must_match: false + segment_targeting_can_be_ignored: false +``` +*Behavior:* Sticky for existing users, new users get assigned normally -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 +**Paused Intake:** +``` +read_materialization: "experiment_v1" +write_materialization: "" # No new assignments +mode: + materialization_must_match: true + segment_targeting_can_be_ignored: false +``` +*Behavior:* Only existing users proceed, new users skip this rule -## Example Workflow +**Sticky Override (Ignore Targeting Changes):** +``` +read_materialization: "experiment_v1" +write_materialization: "experiment_v1" +mode: + materialization_must_match: false + segment_targeting_can_be_ignored: true +``` +*Behavior:* Existing users keep assignment regardless of targeting changes -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 +**Full Lockdown:** +``` +read_materialization: "experiment_v1" +write_materialization: "" +mode: + materialization_must_match: true + segment_targeting_can_be_ignored: true +``` +*Behavior:* Only existing users, no new assignments, bypass targeting checks -This approach ensures assignment consistency while allowing new users to be assigned according to current targeting rules. 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 fce98e8..d693be6 100644 --- a/confidence-resolver/protos/confidence/flags/resolver/v1/wasm_api.proto +++ b/confidence-resolver/protos/confidence/flags/resolver/v1/wasm_api.proto @@ -22,19 +22,20 @@ message ResolveWithStickyRequest { ResolveFlagsRequest resolve_request = 1; // Context about the materialization required for the resolve - MaterializationContext materialization_context = 7; + map materializations_per_unit = 2; // 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 = 3; } -message MaterializationContext { - map unit_materialization_info = 1; +message MaterializationMap { + // materialization name to info + map info_map = 1; } message MaterializationInfo { - bool unit_in_info = 1; - map rule_to_variant = 2; + bool unit_in_info = 1; + map rule_to_variant = 2; } message LogMessage { diff --git a/confidence-resolver/src/lib.rs b/confidence-resolver/src/lib.rs index 4e7a771..6333a8f 100644 --- a/confidence-resolver/src/lib.rs +++ b/confidence-resolver/src/lib.rs @@ -9,7 +9,7 @@ use bitvec::prelude as bv; use core::marker::PhantomData; use fastmurmur3::murmur3_x64_128; -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::fmt::format; use bytes::Bytes; @@ -57,8 +57,8 @@ use crate::proto::confidence::flags::resolver::v1::resolve_with_sticky_response: MaterializationUpdate, ResolveResult, }; use crate::proto::confidence::flags::resolver::v1::{ - resolve_with_sticky_response, MaterializationContext, ResolveFlagsRequest, - ResolveFlagsResponse, ResolveWithStickyRequest, ResolveWithStickyResponse, + resolve_with_sticky_response, MaterializationMap, ResolveFlagsRequest, ResolveFlagsResponse, + ResolveWithStickyRequest, ResolveWithStickyResponse, }; impl TryFrom> for ResolverStatePb { @@ -463,7 +463,7 @@ impl ResolveWithStickyRequest { ResolveWithStickyRequest { resolve_request: Some(resolve_request), fail_fast_on_sticky: false, - materialization_context: Some(MaterializationContext::default()), + materializations_per_unit: BTreeMap::new(), } } } @@ -519,7 +519,7 @@ impl<'a, H: Host> AccountResolver<'a, H> { let mut has_missing_materializations = false; for flag in flags_to_resolve.clone() { - let resolve_result = self.resolve_flag(flag, request.materialization_context.clone()); + let resolve_result = self.resolve_flag(flag, request.materializations_per_unit.clone()); match resolve_result { Ok(resolve_result) => resolve_results.push(resolve_result), Err(err) => { @@ -730,7 +730,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, None)) + .and_then(|flag| self.resolve_flag(flag, BTreeMap::new())) } pub fn collect_missing_materializations( @@ -802,7 +802,7 @@ impl<'a, H: Host> AccountResolver<'a, H> { pub fn resolve_flag( &'a self, flag: &'a Flag, - sticky_context: Option, + sticky_context: BTreeMap, ) -> Result, ResolveFlagError> { let mut updates: Vec = Vec::new(); let mut resolved_value = ResolvedValue::new(flag); @@ -848,11 +848,13 @@ impl<'a, H: Host> AccountResolver<'a, H> { let mut materialization_matched = false; if let Some(materialization_spec) = &rule.materialization_spec { - if let Some(context) = &sticky_context { - let read_materialization = &materialization_spec.read_materialization; - if !read_materialization.is_empty() { - if let Some(info) = context.unit_materialization_info.get(&unit) { - materialization_matched = if !info.unit_in_info { + let read_materialization = &materialization_spec.read_materialization; + if !read_materialization.is_empty() { + if let Some(info) = sticky_context.get(&unit) { + let info_from_context = info.info_map.get(read_materialization).clone(); + + if let Some(ref info_data) = info_from_context { + if !info_data.unit_in_info { if materialization_spec .mode .as_ref() @@ -862,55 +864,59 @@ impl<'a, H: Host> AccountResolver<'a, H> { // Materialization must match but unit is not in materialization continue; } - false + materialization_matched = false; } else if materialization_spec .mode .as_ref() .map(|mode| mode.segment_targeting_can_be_ignored) .unwrap_or(false) { - true + materialization_matched = true; } else { - self.segment_match(segment, &unit)? - }; - if materialization_matched { - 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( - ref variant_assignment, - )) = &assignment.assignment - { - variant_assignment.variant == *variant_name - } else { - false - } - }) - { - let variant = flag - .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![], - }); - } - } + materialization_matched = self.segment_match(segment, &unit)?; } } else { - materialization_matched = false; - }; - } - } else { - return Err(ResolveFlagError::missing_materializations()); + return Err(ResolveFlagError::missing_materializations()); + } + + if materialization_matched { + if let Some(variant_name) = info_from_context + .as_ref() + .and_then(|info| 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 = flag + .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![], + }); + } + } + } + } else { + return Err(ResolveFlagError::missing_materializations()); + }; } } @@ -1069,7 +1075,7 @@ impl<'a, H: Host> AccountResolver<'a, H> { // check bitset let Some(bitset) = self.state.bitsets.get(&segment.name) else { return Ok(true); - }; // todo: whould this match or not? + }; // todo: would this match or not? let salted_unit = self.client.account.salt_unit(unit)?; let unit_hash = bucket(hash(&salted_unit), BUCKETS); Ok(bitset[unit_hash]) @@ -1498,7 +1504,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, None).unwrap(); + let resolve_result = resolver.resolve_flag(flag, BTreeMap::new()).unwrap(); let resolved_value = &resolve_result.resolved_value; let assignment_match = resolved_value.assignment_match.as_ref().unwrap(); @@ -1520,7 +1526,7 @@ mod tests { .unwrap(); let flag = resolver.state.flags.get("flags/tutorial-feature").unwrap(); let assignment_match = resolver - .resolve_flag(flag, None) + .resolve_flag(flag, BTreeMap::new()) .unwrap() .resolved_value .assignment_match @@ -1917,7 +1923,7 @@ mod tests { .flags .get("flags/fallthrough-test-2") .unwrap(); - let resolve_result = resolver.resolve_flag(flag, None).unwrap(); + let resolve_result = resolver.resolve_flag(flag, BTreeMap::new()).unwrap(); let resolved_value = &resolve_result.resolved_value; assert_eq!(resolved_value.reason as i32, ResolveReason::Match as i32); @@ -1944,7 +1950,7 @@ mod tests { .flags .get("flags/fallthrough-test-2") .unwrap(); - let resolve_result = resolver.resolve_flag(flag, None).unwrap(); + let resolve_result = resolver.resolve_flag(flag, BTreeMap::new()).unwrap(); let resolved_value = &resolve_result.resolved_value; assert_eq!( diff --git a/wasm/rust-guest/src/lib.rs b/wasm/rust-guest/src/lib.rs index 65328e3..6fdaaed 100644 --- a/wasm/rust-guest/src/lib.rs +++ b/wasm/rust-guest/src/lib.rs @@ -1,4 +1,5 @@ use std::cell::RefCell; +use std::fmt::format; use std::sync::Arc; use std::sync::LazyLock; @@ -212,7 +213,8 @@ wasm_msg_guest! { Ok((&resolve_result.resolved_value).into()) } fn flush_logs(_request:Void) -> WasmResult { - Ok(LOGGER.checkpoint()) + let response = LOGGER.checkpoint(); + Ok(response) } }