feat: Lazy identity-flag evaluation in local-eval mode#200
Open
feat: Lazy identity-flag evaluation in local-eval mode#200
Conversation
``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
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
``update_environment`` now sends a HEAD first and compares the ``x-flagsmith-document-updated-at`` response header against the value stored from the last successful fetch. When they match, the GET, the JSON parse, ``map_environment_document_to_context``, and the overrides-index rebuild are all skipped — the cached evaluation context is reused as-is. On the customer's QA env this eliminates the ~5ms p99 GIL stall the polling thread imposes every ``environment_refresh_interval_seconds`` (default 60s) — which is the largest remaining contributor to identity-flag-eval p99 once lazy is enabled. Standard 60s polling against a stable env now does HEAD-only round trips between actual changes. HEAD failures (e.g. proxy that doesn't permit it) silently fall through to the existing GET path, so no environment regresses to a worse-than-current behaviour if the optimisation can't apply. beep boop
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two complementary changes targeting the local-eval latency reported in flagsmith-python-client#198 and the customer thread that motivated it.
1. Lazy identity-flag evaluation
Flagsmith.get_identity_flagsreturns aFlagsthat holds the evaluation context plus a precomputed segment-overrides reverse index, and resolves each feature on first access via the engine's already-publicis_context_in_segment/get_flag_result_from_contextprimitives, instead of running a full bulk evaluation up-front.The reverse index is rebuilt inside the
_evaluation_contextsetter, so it stays in sync with environment refreshes with zero hot-path cost.New
lazy_identity_evaluation: bool = Trueconstructor kwarg acts as a rollback switch.2. Skip env-doc re-parse on no-op refresh
update_environmentnow sends a HEAD first and compares thex-flagsmith-document-updated-atresponse header against the value stored from the last successful fetch. When they match, the GET, JSON parse,map_environment_document_to_context, and overrides-index rebuild are all skipped — the cached evaluation context is reused. HEAD failures fall through to the existing GET path so no environment regresses if a proxy doesn't permit HEAD.This eliminates the ~5 ms p99 GIL stall the polling thread otherwise imposes every
environment_refresh_interval_seconds(default 60s) when re-parsing a 400+ feature env doc.Bench against real customer env (Sevenrooms QA, 434 features, 64 segments, 305 rules)
Hot loop of
get_identity_flags(...).is_feature_enabled(name), 20,000 iterations.lazy_identity_evaluation=False)100–200× across every percentile vs any released version, on the customer's actual environment shape.
Back-compat
Flagspublic surface unchanged (is_feature_enabled,get_feature_value,get_flag,all_flags).FlagResultproduced via the same engine helper as the bulk path — identical output shape.lazy_identity_evaluation=Falserestores the old eager-eval timing byte-for-byte.engine.get_evaluation_resultare pinned tolazy_identity_evaluation=False— they exercise the eager-path call shape, which still ships as the rollback.Engine contract
Untouched. SDK only imports already-public symbols from
flag_engine.segments.evaluator.Tests
test_models.py(override match/no-match, per-flag caching,all_flagsmaterialisation, fallthrough to default handler, reverse-index correctness).test_flagsmith.py: lazy-by-default, rollback kwarg, HEAD-skip on unchanged docs, HEAD-failure fall-through.