Skip to content

Latest commit

 

History

History
208 lines (151 loc) · 10.3 KB

File metadata and controls

208 lines (151 loc) · 10.3 KB

Architecture

flagd-evaluator is a Rust-based feature flag evaluation engine designed to replace per-language JSON Logic implementations with a single core — one implementation to maintain, one test suite, consistent behavior across all languages. Thin wrapper libraries keep language-specific code minimal. It ships as:

  • WASM module (~2.4MB) — for Java (Chicory), Go (wazero), JavaScript, .NET, and other WASM runtimes
  • Native bindings — Python (PyO3), with more planned
  • Rust library — direct API for Rust consumers

The best integration strategy is chosen per language based on benchmarks. For example, Python benchmarks showed PyO3 native bindings outperform WASM via wasmtime-py, while Go (wazero) and Java (Chicory) perform well with embedded WASM. Each wrapper must meet or exceed the performance of the language-native approach it replaces. Cross-language benchmarks validate this.

Design Principles

  1. WASM-First — Compiled to WebAssembly for cross-language portability
  2. No External Dependencies — Single WASM file, no JNI, no JavaScript bindings
  3. Chicory Compatible — Works with pure Java WASM runtimes (no native code)
  4. Memory Safe — Explicit alloc/dealloc, no panics, all errors returned as JSON
  5. Size Optimized — Aggressive compilation flags (opt-level = "z", LTO, panic = "abort")

Module Organization

src/
├── lib.rs              # Main entry point, WASM exports (update_state, evaluate)
├── evaluation.rs       # Core flag evaluation logic, context enrichment ($flagd properties)
├── memory.rs           # WASM memory management (alloc/dealloc, pointer packing)
├── storage/            # Thread-local flag state storage
├── operators/          # Custom JSON Logic operators (registered via datalogic_rs::Operator)
│   ├── fractional.rs   # MurmurHash3-based consistent bucketing for A/B testing
│   └── sem_ver.rs      # Semantic version comparison (=, !=, <, <=, >, >=, ^, ~)
├── model/              # Flag configuration data structures
└── validation.rs       # JSON Schema validation against flagd schemas

WASM Exports

All WASM export functions return a packed u64: upper 32 bits = pointer, lower 32 bits = length.

Export Signature Description
evaluate_logic (rule_ptr, rule_len, data_ptr, data_len) -> u64 Direct JSON Logic evaluation
update_state (config_ptr, config_len) -> u64 Store flag configuration, returns changed flags
evaluate (flag_key_ptr, flag_key_len, context_ptr, context_len) -> u64 Evaluate a stored flag
alloc (len) -> *mut u8 Allocate WASM memory
dealloc (ptr, len) Free WASM memory
set_validation_mode (mode) -> u64 Set strict (0) or permissive (1) validation

Memory Model

Caller allocates input buffers, callee allocates result buffers. Caller must free all allocations. UTF-8 JSON strings for all inputs/outputs.

Memory Safety Rules

  1. Never panic in WASM exports — All errors must be returned as JSON error responses
  2. Always validate UTF-8 — Use string_from_memory() which returns Result
  3. Pointer lifetime — WASM memory is stable within a single function call but may be reallocated between calls
  4. Safety comments required — All unsafe blocks must have // SAFETY: comments

Resource Limits

The evaluator enforces strict resource limits at the WASM boundary and within the Rust API to prevent resource exhaustion from adversarial or accidentally oversized inputs. All limits are defined as constants in src/limits.rs.

Constant Value Real-world meaning
MAX_CONFIG_BYTES 100 MB ~125,000 complex flags at ~800 bytes each — covers the largest conceivable local-file deployment
MAX_CONTEXT_BYTES 1 MB ~26,000 context fields at ~40 bytes each — far exceeds any realistic evaluation context
MAX_REF_DEPTH 64 Maximum $ref hops during evaluator resolution — more than any real-world chain needs
MAX_JSON_DEPTH 128 Maximum JSON nesting depth checked before parsing — prevents stack overflows

Where limits are enforced

  • Config size (MAX_CONFIG_BYTES): Checked in update_state_internal (WASM boundary) before reading memory. A host passing an oversized config_len gets a JSON error without any memory access.
  • Context size (MAX_CONTEXT_BYTES): Checked in evaluate_internal and evaluate_by_index_internal before reading context bytes.
  • JSON depth (MAX_JSON_DEPTH): Pre-parse byte scan in FlagEvaluator::update_state (see limits::check_json_depth). Fires before serde_json recursion, preventing stack overflows from adversarial nesting. Also covers YAML configs, since update_state_from_yaml converts to JSON first.
  • $ref depth (MAX_REF_DEPTH): Depth counter in resolve_refs (see feature_flag.rs). Counts only $ref hops — JSON structural traversal does not consume the budget.

Behavior when limits are exceeded

All limit violations return deterministic errors — no panics, no crashes:

  • Config/context size violations → {"success": false, "error": "... exceeds maximum ..."}
  • JSON depth violation → {"success": false, "error": "JSON nesting depth exceeds ..."}
  • $ref depth violation → {"success": false, "error": "$ref resolution depth limit of 64 exceeded ..."}
  • Evaluation with oversized context → {"reason": "ERROR", "errorCode": "PARSE_ERROR", ...}

WASM Build Flags

From Cargo.toml release profile:

[profile.release]
opt-level = "z"      # Optimize for size
lto = true           # Link-time optimization
codegen-units = 1    # Single codegen unit for better optimization
strip = true         # Strip symbols
panic = "abort"      # Remove panic unwinding infrastructure

Always build WASM with --no-default-features to exclude unnecessary dependencies.

Context Enrichment

The evaluator automatically injects standard $flagd properties into the evaluation context (see flagd provider spec):

Property Description
$flagd.flagKey The flag being evaluated
$flagd.timestamp Unix timestamp (seconds) at evaluation time
targetingKey Defaults to empty string if not provided

Custom Operators

Two custom operators are implemented in src/operators/ and registered via datalogic_rs::Operator in src/operators/mod.rs. See the flagd custom operations spec for full details.

The starts_with and ends_with string-matching operators are built into datalogic-rs and require no custom implementation in this repository.

Validation

Uses the boon crate to validate flag configs against flagd-schemas:

  • Strict (default): Reject invalid configs
  • Permissive: Accept with warnings (for legacy compatibility)

Flag State Management

Thread-local storage for flag configurations (src/storage/mod.rs). update_state detects and reports changed flags (added, removed, or mutated).

Error Handling

JSON Logic evaluation (lib.rs):

match logic.evaluate_json(&rule_str, &data_str) {
    Ok(result) => EvaluationResponse::success(result),
    Err(e) => EvaluationResponse::error(format!("{}", e)),
}

Flag evaluation returns EvaluationResult with standardized error codes:

Error Code Meaning
FLAG_NOT_FOUND Flag key not in configuration
PARSE_ERROR JSON parsing or rule evaluation error
TYPE_MISMATCH Resolved value doesn't match expected type
GENERAL Other errors

Resolution reasons: STATIC, DEFAULT, TARGETING_MATCH, DISABLED, ERROR, FLAG_NOT_FOUND

Common Workflows

Adding a New Custom Operator

  1. Create new file in src/operators/ (e.g., my_operator.rs)
  2. Implement datalogic_rs::Operator trait
  3. Register in src/operators/mod.rs via create_evaluator()
  4. Add tests in both unit tests and tests/integration_tests.rs
  5. Document in README.md under "Custom Operators"

Modifying Flag Evaluation Logic

  1. Primary logic is in src/evaluation.rs
  2. Context enrichment happens in evaluate_flag() function
  3. State retrieval uses thread-local storage via get_flag_state()
  4. Always maintain backward compatibility with the flagd provider specification
  5. Test with targeting rules, disabled flags, and missing flags

Memory Management Changes

  1. All WASM-facing functions must use packed u64 returns
  2. Use string_to_memory() to allocate and pack results
  3. Use string_from_memory() to read inputs (handles UTF-8 validation)
  4. Document caller responsibilities in function doc comments
  5. Test with the Java example in examples/java/

Cross-Language Integration

This WASM module is embedded in multiple language providers. The general integration pattern:

  1. Load WASM module
  2. Get function exports (alloc, dealloc, evaluate_logic, update_state, evaluate)
  3. For each call:
    • Allocate memory for inputs using alloc()
    • Write UTF-8 encoded JSON strings to WASM memory
    • Call evaluation function with pointers and lengths
    • Unpack returned u64 (ptr = upper 32 bits, len = lower 32 bits)
    • Read result JSON from WASM memory
    • Free all allocations using dealloc()

Memory lifecycle: Host application owns all memory allocation/deallocation decisions. WASM module only allocates result memory internally.

See examples/java/FlagdEvaluatorExample.java for a complete Java (Chicory) integration example. See python/README.md for native Python bindings via PyO3.

Dependencies

Production

Crate Version Purpose
datalogic-rs 4.0 JSON Logic implementation
serde, serde_json JSON serialization (no_std compatible with alloc)
boon 0.6 JSON Schema validation
murmurhash3 Hash function for fractional operator
ahash Hash table implementation (SIMD-disabled for Chicory)
getrandom Random number generation for WASM

Dev

Crate Purpose
cucumber Gherkin/BDD testing
tokio Async runtime for tests