Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions crates/flagd/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ rpc = ["dep:tonic", "dep:tonic-prost", "dep:prost", "dep:prost-types", "dep:hype
# REST evaluation mode - uses HTTP/OFREP to connect to flagd service
rest = ["dep:reqwest"]
# In-process evaluation mode - local evaluation with gRPC sync or file-based configuration
in-process = ["dep:tonic", "dep:tonic-prost", "dep:prost", "dep:prost-types", "dep:datalogic-rs", "dep:murmurhash3", "dep:semver"]
in-process = ["dep:tonic", "dep:tonic-prost", "dep:prost", "dep:prost-types", "dep:flagd-evaluator"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

Expand Down Expand Up @@ -72,7 +72,5 @@ tower = { version = "0.5", optional = true }
# REST-specific dependencies
reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "rustls-tls"], optional = true }

# In-Process-specific dependencies (targeting engine)
datalogic-rs = { version = "4.0.4", optional = true }
murmurhash3 = { version = "0.0.5", optional = true }
semver = { version = "1.0.27", optional = true }
# In-Process-specific dependencies (evaluation engine)
flagd-evaluator = { path = "../../../flagd-evaluator", default-features = false, optional = true }
84 changes: 84 additions & 0 deletions crates/flagd/benches/evaluation_benchmark.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion, BenchmarkId};
use open_feature_flagd::in_process::*;
use serde_json::json;

fn create_test_flags() -> serde_json::Value {
json!({
"flags": {
"simple-bool": {
"state": "ENABLED",
"variants": {
"on": true,
"off": false
},
"defaultVariant": "on"
},
"targeted-string": {
"state": "ENABLED",
"variants": {
"variant-a": "A",
"variant-b": "B"
},
"defaultVariant": "variant-a",
"targeting": {
"if": [
{"==": [{"var": "email"}, "user@example.com"]},
"variant-b",
null
]
}
},
"fractional-rollout": {
"state": "ENABLED",
"variants": {
"red": "red",
"blue": "blue",
"green": "green"
},
"defaultVariant": "red",
"targeting": {
"fractional": [
{"var": "$flagd.flagKey"},
[["red", 25], ["blue", 25], ["green", 50]]
]
}
}
}
})
}

fn benchmark_evaluations(c: &mut Criterion) {
let mut group = c.benchmark_group("flag_evaluation");

// Benchmark simple boolean flag
group.bench_function("simple_bool", |b| {
let flags = create_test_flags();
// Initialize your resolver here with the flags
b.iter(|| {
// Evaluate the flag
black_box(/* your evaluation call */);
});
});

// Benchmark targeted evaluation
group.bench_function("targeted_with_context", |b| {
let flags = create_test_flags();
let context = json!({"email": "user@example.com"});
b.iter(|| {
black_box(/* evaluation with context */);
});
});

// Benchmark fractional evaluation
group.bench_function("fractional_rollout", |b| {
let flags = create_test_flags();
b.iter(|| {
black_box(/* fractional evaluation */);
});
});

group.finish();
}

criterion_group!(benches, benchmark_evaluations);
criterion_main!(benches);
2 changes: 0 additions & 2 deletions crates/flagd/src/resolver/in_process/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
pub mod model;
pub mod resolver;
pub mod storage;
pub mod targeting;
28 changes: 0 additions & 28 deletions crates/flagd/src/resolver/in_process/model/feature_flag.rs

This file was deleted.

42 changes: 0 additions & 42 deletions crates/flagd/src/resolver/in_process/model/flag_parser.rs

This file was deleted.

3 changes: 0 additions & 3 deletions crates/flagd/src/resolver/in_process/model/mod.rs

This file was deleted.

36 changes: 0 additions & 36 deletions crates/flagd/src/resolver/in_process/model/value_converter.rs

This file was deleted.

158 changes: 158 additions & 0 deletions crates/flagd/src/resolver/in_process/resolver/common.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
use flagd_evaluator::evaluation::{
ErrorCode as EvaluatorErrorCode, EvaluationResult, ResolutionReason as EvaluatorReason,
};
use open_feature::{
EvaluationContext, EvaluationError, EvaluationErrorCode, EvaluationReason, FlagMetadata,
FlagMetadataValue, StructValue, Value,
};
use serde_json::Value as JsonValue;

/// Convert EvaluationContextFieldValue to JsonValue recursively
fn context_field_to_json(value: &open_feature::EvaluationContextFieldValue) -> JsonValue {
use open_feature::EvaluationContextFieldValue;
match value {
EvaluationContextFieldValue::String(s) => JsonValue::String(s.clone()),
EvaluationContextFieldValue::Bool(b) => JsonValue::Bool(*b),
EvaluationContextFieldValue::Int(i) => JsonValue::Number((*i).into()),
EvaluationContextFieldValue::Float(f) => {
JsonValue::Number(serde_json::Number::from_f64(*f).unwrap_or_else(|| {
serde_json::Number::from_f64(0.0).unwrap()
}))
}
EvaluationContextFieldValue::DateTime(dt) => JsonValue::String(dt.to_string()),
EvaluationContextFieldValue::Struct(_) => {
// NOTE: The OpenFeature Rust SDK stores structs as Arc<dyn Any> which cannot be
// introspected or serialized. This is a known limitation - see the TODO comment in
// the SDK source. Until this is fixed, we return an empty object to avoid breaking
// targeting rules that expect an object type. This means nested struct fields in
// evaluation context cannot be accessed by targeting rules.
// See: https://github.com/open-feature/rust-sdk/blob/main/open-feature/src/evaluation/context_field_value.rs
JsonValue::Object(serde_json::Map::new())
}
}
}

/// Build context JSON for evaluator from OpenFeature context
pub fn build_context_json(context: &EvaluationContext) -> JsonValue {
let mut root = serde_json::Map::new();

// Add targeting key if present
if let Some(targeting_key) = &context.targeting_key {
root.insert(
"targetingKey".to_string(),
JsonValue::String(targeting_key.clone()),
);
}

// Add custom fields
for (key, value) in &context.custom_fields {
root.insert(key.clone(), context_field_to_json(value));
}

JsonValue::Object(root)
}

/// Map evaluator reason to OpenFeature reason
pub fn map_reason(reason: &EvaluatorReason) -> Option<EvaluationReason> {
match reason {
EvaluatorReason::Static => Some(EvaluationReason::Static),
EvaluatorReason::Default => Some(EvaluationReason::Default),
EvaluatorReason::TargetingMatch => Some(EvaluationReason::TargetingMatch),
EvaluatorReason::Disabled => Some(EvaluationReason::Disabled),
EvaluatorReason::Error
| EvaluatorReason::FlagNotFound
| EvaluatorReason::Fallback => Some(EvaluationReason::Error),
}
}

/// Map evaluator error code to OpenFeature error code
pub fn map_error_code(code: &EvaluatorErrorCode) -> EvaluationErrorCode {
match code {
EvaluatorErrorCode::FlagNotFound => EvaluationErrorCode::FlagNotFound,
EvaluatorErrorCode::ParseError => EvaluationErrorCode::ParseError,
EvaluatorErrorCode::TypeMismatch => EvaluationErrorCode::TypeMismatch,
EvaluatorErrorCode::General => {
EvaluationErrorCode::General("Evaluation error".to_string())
}
}
}

/// Convert evaluation result to resolution details
pub fn result_to_details<T>(
result: &EvaluationResult,
value_extractor: impl Fn(&JsonValue) -> Option<T>,
) -> Result<open_feature::provider::ResolutionDetails<T>, EvaluationError> {
use open_feature::provider::ResolutionDetails;

// Check for errors
if let Some(error_code) = &result.error_code {
return Err(EvaluationError::builder()
.code(map_error_code(error_code))
.message(result.error_message.clone().unwrap_or_default())
.build());
}

// Extract value
let value = value_extractor(&result.value).ok_or_else(|| {
EvaluationError::builder()
.code(EvaluationErrorCode::TypeMismatch)
.message("Value type mismatch".to_string())
.build()
})?;

Ok(ResolutionDetails {
value,
variant: result.variant.clone(),
reason: map_reason(&result.reason),
flag_metadata: result.flag_metadata.as_ref().map(|metadata| {
let mut flag_metadata = FlagMetadata::default();
for (key, value) in metadata {
if let Some(metadata_value) = json_to_metadata_value(value) {
flag_metadata = flag_metadata.with_value(key.clone(), metadata_value);
}
}
flag_metadata
}),
})
}

/// Convert JsonValue to OpenFeature Value
pub fn json_to_value(v: &JsonValue) -> Value {
match v {
JsonValue::String(s) => Value::String(s.clone()),
JsonValue::Number(n) => {
if let Some(i) = n.as_i64() {
Value::Int(i)
} else {
Value::Float(n.as_f64().unwrap())
}
}
JsonValue::Bool(b) => Value::Bool(*b),
JsonValue::Object(obj) => {
let fields = obj
.iter()
.map(|(k, v)| (k.clone(), json_to_value(v)))
.collect();
Value::Struct(StructValue { fields })
}
JsonValue::Array(arr) => Value::Array(arr.iter().map(json_to_value).collect()),
JsonValue::Null => Value::String(String::new()), // Default for null
}
}

/// Convert JsonValue to FlagMetadataValue
pub fn json_to_metadata_value(v: &JsonValue) -> Option<FlagMetadataValue> {
match v {
JsonValue::String(s) => Some(FlagMetadataValue::String(s.clone())),
JsonValue::Number(n) => {
if let Some(i) = n.as_i64() {
Some(FlagMetadataValue::Int(i))
} else {
n.as_f64().map(FlagMetadataValue::Float)
}
}
JsonValue::Bool(b) => Some(FlagMetadataValue::Bool(*b)),
_ => None, // FlagMetadata only supports primitives
}
}

Loading