Skip to content

Commit 1d5f4f8

Browse files
aepfliclaude
andcommitted
refactor(resolver): use instance-based FlagEvaluator for in-process resolvers
Replace global thread-local state with per-instance FlagEvaluator for both InProcessResolver and FileResolver, eliminating global state issues and enabling multiple independent resolver instances with different configurations. - Update InProcessResolver to use Arc<tokio::sync::RwLock<FlagEvaluator>> - Update FileResolver to use Arc<tokio::sync::RwLock<FlagEvaluator>> - Each resolver instance creates its own FlagEvaluator with Permissive mode - Remove unused get_flag_and_metadata() and empty_flag() from common.rs - Use async RwLock for Send-safe access across await points - Remove calls to thread-local update_flag_state_with_mode() - Simplify evaluation by using evaluator instance methods directly This fixes issues where multiple resolver instances could interfere with each other via global state, and aligns the Rust implementation with Python's instance-based architecture. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent e78f9f1 commit 1d5f4f8

3 files changed

Lines changed: 52 additions & 115 deletions

File tree

crates/flagd/src/resolver/in_process/resolver/common.rs

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,11 @@
11
use flagd_evaluator::evaluation::{
22
ErrorCode as EvaluatorErrorCode, EvaluationResult, ResolutionReason as EvaluatorReason,
33
};
4-
use flagd_evaluator::model::FeatureFlag;
54
use open_feature::{
65
EvaluationContext, EvaluationError, EvaluationErrorCode, EvaluationReason, FlagMetadata,
76
FlagMetadataValue, StructValue, Value,
87
};
98
use serde_json::Value as JsonValue;
10-
use std::collections::HashMap;
11-
12-
/// Helper to create an empty FeatureFlag for a given key when one doesn't exist
13-
pub fn empty_flag(key: &str) -> FeatureFlag {
14-
FeatureFlag {
15-
key: Some(key.to_string()),
16-
state: "DISABLED".to_string(),
17-
default_variant: None,
18-
variants: Default::default(),
19-
targeting: None,
20-
metadata: Default::default(),
21-
}
22-
}
239

2410
/// Convert EvaluationContextFieldValue to JsonValue recursively
2511
fn context_field_to_json(value: &open_feature::EvaluationContextFieldValue) -> JsonValue {
@@ -170,20 +156,3 @@ pub fn json_to_metadata_value(v: &JsonValue) -> Option<FlagMetadataValue> {
170156
}
171157
}
172158

173-
/// Get flag and metadata from evaluator storage
174-
pub fn get_flag_and_metadata(
175-
flag_key: &str,
176-
) -> (FeatureFlag, HashMap<String, serde_json::Value>) {
177-
let state = flagd_evaluator::storage::get_flag_state();
178-
let flag = state
179-
.as_ref()
180-
.and_then(|s| s.flags.get(flag_key))
181-
.cloned()
182-
.unwrap_or_else(|| empty_flag(flag_key));
183-
let metadata = state
184-
.as_ref()
185-
.map(|s| &s.flag_set_metadata)
186-
.cloned()
187-
.unwrap_or_default();
188-
(flag, metadata)
189-
}

crates/flagd/src/resolver/in_process/resolver/file.rs

Lines changed: 26 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,9 @@ use crate::resolver::in_process::storage::connector::{Connector, QueuePayloadTyp
44
use crate::{CacheService, CacheSettings};
55
use anyhow::Result;
66
use async_trait::async_trait;
7-
use flagd_evaluator::evaluation::{
8-
evaluate_bool_flag, evaluate_float_flag, evaluate_flag, evaluate_int_flag,
9-
evaluate_string_flag, EvaluationResult,
10-
};
7+
use flagd_evaluator::evaluation::EvaluationResult;
118
use flagd_evaluator::model::ParsingResult;
12-
use flagd_evaluator::storage::{update_flag_state, ValidationMode};
9+
use flagd_evaluator::storage::ValidationMode;
1310
use open_feature::provider::{FeatureProvider, ProviderMetadata, ResolutionDetails};
1411
use open_feature::{EvaluationContext, EvaluationError, Value};
1512
use serde_json::Value as JsonValue;
@@ -20,24 +17,22 @@ use tracing::debug;
2017
pub struct FileResolver {
2118
/// Connector for watching file changes
2219
connector: Arc<FileConnector>,
20+
/// Instance-based flag evaluator with its own state and validation mode
21+
evaluator: Arc<tokio::sync::RwLock<flagd_evaluator::FlagEvaluator>>,
2322
metadata: ProviderMetadata,
2423
cache: Option<Arc<CacheService<Value>>>,
2524
}
2625

2726
impl FileResolver {
2827
pub async fn new(source_path: String, cache_settings: Option<CacheSettings>) -> Result<Self> {
29-
// Set validation mode to permissive to match other providers
30-
// NOTE: This sets a thread-local global state. If multiple resolver instances
31-
// are created in the same thread with different validation requirements, this
32-
// could cause issues. Currently both InProcessResolver and FileResolver use
33-
// Permissive mode, so this is not a problem in practice. A future improvement
34-
// would be to make validation mode configurable per CacheSettings and pass it
35-
// to evaluator functions, or for flagd-evaluator to support per-instance config.
36-
flagd_evaluator::storage::set_validation_mode(ValidationMode::Permissive);
37-
3828
let connector = Arc::new(FileConnector::new(source_path));
3929
let cache = cache_settings.map(|settings| Arc::new(CacheService::new(settings)));
4030

31+
// Create evaluator instance with permissive validation mode
32+
let evaluator = Arc::new(tokio::sync::RwLock::new(
33+
flagd_evaluator::FlagEvaluator::new(ValidationMode::Permissive)
34+
));
35+
4136
// Initialize the connector to start watching the file
4237
connector.init().await?;
4338

@@ -53,7 +48,8 @@ impl FileResolver {
5348
debug!("Received initial flag configuration from file");
5449
match ParsingResult::parse(&payload.flag_data) {
5550
Ok(_) => {
56-
if let Err(e) = update_flag_state(&payload.flag_data) {
51+
let mut eval = evaluator.write().await;
52+
if let Err(e) = eval.update_state(&payload.flag_data) {
5753
return Err(anyhow::anyhow!("Failed to update flag state: {}", e));
5854
}
5955
}
@@ -78,6 +74,7 @@ impl FileResolver {
7874
// Spawn task to handle subsequent config updates
7975
let stream_clone = stream.clone();
8076
let cache_clone = cache.clone();
77+
let evaluator_clone = evaluator.clone();
8178
tokio::spawn(async move {
8279
let mut receiver_opt = stream_clone.lock().await;
8380
if let Some(receiver) = receiver_opt.as_mut() {
@@ -88,14 +85,16 @@ impl FileResolver {
8885
// Parse and update state in evaluator
8986
match ParsingResult::parse(&payload.flag_data) {
9087
Ok(_) => {
91-
if let Err(e) = update_flag_state(&payload.flag_data) {
88+
let mut eval = evaluator_clone.write().await;
89+
if let Err(e) = eval.update_state(&payload.flag_data) {
9290
tracing::error!("Failed to update flag state: {}", e);
9391
} else {
9492
// Clear cache when flags update
9593
if let Some(cache) = &cache_clone {
9694
cache.purge().await;
9795
}
9896
}
97+
drop(eval); // Explicitly drop lock before continuing
9998
}
10099
Err(e) => {
101100
tracing::error!("Failed to parse flag configuration: {}", e);
@@ -108,6 +107,7 @@ impl FileResolver {
108107

109108
Ok(Self {
110109
connector,
110+
evaluator,
111111
metadata: ProviderMetadata::new("flagd"),
112112
cache,
113113
})
@@ -131,7 +131,7 @@ impl FileResolver {
131131
&self,
132132
flag_key: &str,
133133
context: &EvaluationContext,
134-
evaluator_fn: impl Fn(&serde_json::Map<String, JsonValue>) -> EvaluationResult,
134+
evaluator_fn: impl Fn(&flagd_evaluator::FlagEvaluator, &JsonValue) -> EvaluationResult,
135135
value_extractor: impl Fn(&JsonValue) -> Option<T>,
136136
cache_value_fn: impl Fn(T) -> Value,
137137
) -> Result<ResolutionDetails<T>, EvaluationError>
@@ -156,12 +156,11 @@ impl FileResolver {
156156

157157
// Build context for evaluator
158158
let ctx_json = common::build_context_json(context);
159-
let ctx_map = ctx_json.as_object().unwrap_or_else(|| {
160-
panic!("build_context_json should always return an object")
161-
});
162159

163-
// Call evaluator (no clone needed)
164-
let result = evaluator_fn(ctx_map);
160+
// Get read lock on evaluator and call evaluation function
161+
let eval = self.evaluator.read().await;
162+
let result = evaluator_fn(&*eval, &ctx_json);
163+
drop(eval); // Release lock before awaiting
165164

166165
// Convert result to details
167166
let details = common::result_to_details(&result, value_extractor)?;
@@ -191,10 +190,7 @@ impl FeatureProvider for FileResolver {
191190
self.resolve_value(
192191
flag_key,
193192
context,
194-
|ctx| {
195-
let (flag, metadata) = common::get_flag_and_metadata(flag_key);
196-
evaluate_bool_flag(&flag, &JsonValue::Object(ctx.clone()), &metadata)
197-
},
193+
|eval, ctx| eval.evaluate_bool(flag_key, ctx),
198194
|v| v.as_bool(),
199195
Value::Bool,
200196
)
@@ -209,10 +205,7 @@ impl FeatureProvider for FileResolver {
209205
self.resolve_value(
210206
flag_key,
211207
context,
212-
|ctx| {
213-
let (flag, metadata) = common::get_flag_and_metadata(flag_key);
214-
evaluate_string_flag(&flag, &JsonValue::Object(ctx.clone()), &metadata)
215-
},
208+
|eval, ctx| eval.evaluate_string(flag_key, ctx),
216209
|v| v.as_str().map(String::from),
217210
Value::String,
218211
)
@@ -227,10 +220,7 @@ impl FeatureProvider for FileResolver {
227220
self.resolve_value(
228221
flag_key,
229222
context,
230-
|ctx| {
231-
let (flag, metadata) = common::get_flag_and_metadata(flag_key);
232-
evaluate_int_flag(&flag, &JsonValue::Object(ctx.clone()), &metadata)
233-
},
223+
|eval, ctx| eval.evaluate_int(flag_key, ctx),
234224
|v| v.as_i64(),
235225
Value::Int,
236226
)
@@ -245,10 +235,7 @@ impl FeatureProvider for FileResolver {
245235
self.resolve_value(
246236
flag_key,
247237
context,
248-
|ctx| {
249-
let (flag, metadata) = common::get_flag_and_metadata(flag_key);
250-
evaluate_float_flag(&flag, &JsonValue::Object(ctx.clone()), &metadata)
251-
},
238+
|eval, ctx| eval.evaluate_float(flag_key, ctx),
252239
|v| v.as_f64(),
253240
Value::Float,
254241
)
@@ -263,10 +250,7 @@ impl FeatureProvider for FileResolver {
263250
self.resolve_value(
264251
flag_key,
265252
context,
266-
|ctx| {
267-
let (flag, metadata) = common::get_flag_and_metadata(flag_key);
268-
evaluate_flag(&flag, &JsonValue::Object(ctx.clone()), &metadata)
269-
},
253+
|eval, ctx| eval.evaluate_flag(flag_key, ctx),
270254
|v| {
271255
v.as_object().map(|obj| {
272256
let fields = obj

0 commit comments

Comments
 (0)