Skip to content

feat: Lazy identity-flag evaluation in local-eval mode#200

Open
khvn26 wants to merge 2 commits intomainfrom
feat/lazy-identity-flags
Open

feat: Lazy identity-flag evaluation in local-eval mode#200
khvn26 wants to merge 2 commits intomainfrom
feat/lazy-identity-flags

Conversation

@khvn26
Copy link
Copy Markdown
Member

@khvn26 khvn26 commented Apr 24, 2026

Contributes to #198.

  • Adds an optional lazy path to Flagsmith.get_identity_flags for local evaluation. The returned Flags now holds the evaluation context plus a precomputed segment-overrides reverse index and resolves each feature on first access via the engine's already-public is_context_in_segment / get_flag_result_from_context primitives, instead of running a full bulk evaluation up-front.
  • Reverse index is rebuilt inside the _evaluation_context setter, so it stays in sync with environment refreshes with zero hot-path cost.
  • New lazy_identity_evaluation: bool = True constructor kwarg acts as a rollback switch; set to False to opt back into the legacy eager path with the exact previous behaviour.

Why

From customer report: 420-feature local-eval env, hot loop reading one boolean flag, 400+ µs per call. Every call was re-evaluating the whole environment document even though the caller only reads a single flag.

Bench (off main, poetry run python -m benchmarks.bench)

scenario eager lazy single lazy rotating lazy all_flags()
customer-shape (0 overrides) 432 µs 1.85 µs (234×) 1.97 µs (219×) 268 µs (1.6×)
flag targeted by 10 overrides 431 µs 44 µs (10×) 2.07 µs (200×) 311 µs (1.4×)
segmenting-heavy (200 segs) 1204 µs 1.96 µs (610×) 2.13 µs 267 µs (4.5×)

.all_flags() is faster too because each lazy per-flag resolution walks only the segments that target that flag (via the reverse index), rather than every segment across every feature.

Back-compat

  • Flags public surface unchanged (is_feature_enabled, get_feature_value, get_flag, all_flags, dataclass shape).
  • FlagResult construction reuses the same engine helper as the bulk path — identical output shape, no reason-string drift, no hidden branching.
  • Evaluation-error timing shifts from get_identity_flags to the first flag read; wrap-in-try keeps semantics for any caller that already defends against it. lazy_identity_evaluation=False restores the old timing byte-for-byte if needed.

Tests

  • 8 new unit tests in tests/test_models.py (match / no-match / per-flag caching / all_flags materialisation / default-handler fallthrough / reverse-index correctness).
  • 2 new integration tests in tests/test_flagsmith.py: lazy-by-default (no bulk engine call) and rollback kwarg (eager bulk call).
  • 95 passing (was 87), mypy strict clean, black clean, isort clean.

``get_identity_flags`` now returns a ``Flags`` that holds the
evaluation context plus a precomputed segment-overrides reverse index,
and resolves each feature on first access via the engine primitives
(``is_context_in_segment`` + ``get_flag_result_from_context``) rather
than running a full bulk evaluation up-front.

In environments shaped like the Slack-report customer (420 features,
30 CSV-IN segments, hot loop reading one boolean flag) this takes
``get_identity_flags().is_feature_enabled(name)`` from ~430 µs to
~1.85 µs per call; 200-segment envs go from ~1200 µs to ~2 µs. The
``.all_flags()`` materialisation path is never slower than the
eager baseline in the bench matrix.

Back-compat:
  * ``Flags`` public API unchanged (``is_feature_enabled``,
    ``get_feature_value``, ``get_flag``, ``all_flags``).
  * ``FlagResult`` construction reuses the same engine helper as the
    bulk path — identical output shape.
  * New ``lazy_identity_evaluation`` constructor kwarg, default
    ``True``, lets operators flip back to the eager path if they hit
    an unexpected regression.

Engine contract is untouched: the SDK consumes only already-public
``flag_engine.segments.evaluator`` symbols.

beep boop
@khvn26 khvn26 requested a review from a team as a code owner April 24, 2026 20:10
@khvn26 khvn26 requested review from gagantrivedi and removed request for a team April 24, 2026 20:10
Picks up the IN segment-condition evaluation speedup
(Flagsmith/flagsmith-engine#295), which cuts per-IN-condition latency
on segment walks by roughly 30%. Complementary to the lazy identity
evaluation added in this PR — most customer envs will benefit from both.

beep boop
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant