Repository navigation
feat: feature flags on OpenFeature — v26.10.01 - #180
Merged
Merged
Conversation
…adata keys and evaluator names
…ff the loop and name the file in syntax errors
FlagdCore refuses an empty metadata key with 'key must not be empty' and would refuse the whole composed document, and JSON carries neither NaN nor infinity, which YAML reads from .nan and .inf. Both are now validation errors with the contract's messages: 'metadata keys must not be empty' for a flag's metadata or the document metadata, 'numbers must be finite' for a flag (anywhere in its definition), an evaluator (key $evaluators, the reason names it) or the document metadata.
FlagdCore substitutes $refs textually with the rule's JSON as an re.sub
template, so a backslash in an evaluator was re-escaped ("C:\\temp"
became a tab, "a\\d" refused the whole document), and its single pass
never resolved a reference an evaluator brings in.
The provider now expands every flag's targeting on the parsed document,
transitively and whatever the names sort as, and hands FlagdCore a
document without $evaluators. A missing name or a cycle stays an
unresolved {"$ref": ...}, which FlagdCore evaluates as PARSE_ERROR. A
flag whose targeting would expand to more than 10 000 JSON values is
counted with a bounded budget, never expanded, and made to reference an
evaluator that does not exist, so it too is a PARSE_ERROR. provider.document
keeps the document as pushed, $evaluators included.
…ct delta A byte-identical copy of the shared conformance folder (firefly-vectors.json sha256 96ff5ccd...). It adds the validate vector 'flag metadata keys must not be empty' and eight evaluate vectors: backslashes kept inside an evaluator, transitive references whatever the names sort as, a reference cycle, and a fan-out under and over the 10 000-value expansion limit. Counts: normalize 5, validate 20, compose 4, expiry 1, evaluate 46, bucket 5.
…rden refusals - A boot composition the provider refuses now fails FlagRegistry.start() with FeatureFlagsError naming the provider's error (spec 4.5); nothing is published and the provider keeps its empty document. - A test-override change the provider refuses is rolled back (overrides and the attribution flag) and raised as FeatureFlagsError to the test. - A source whose document was refused keeps the refusal as its reason: a later failed load followed by "unchanged" reports the refusal again instead of the timeout text. - A refusal blamed on a source also logs feature_flag_source_failed at WARNING with the source name, as load failures do (spec 4.9). - One change signal: FireflyFlagProvider.update takes changed_keys and PROVIDER_CONFIGURATION_CHANGED carries exactly those keys; the registry passes the keys FeatureFlagsChanged carries. The return value is still FlagdCore's own diff. - The registry's evaluator dependencies follow $refs in targeting only, as the provider now expands them. - DocumentSource moves to tests/feature_flags/support.py, and tests now cover the test-overrides event drained by stop(), removed keys, refresh_all, has_source, unknown sources and the last_refresh rollback.
Contract rule (spec 4.1): a flag whose expanded targeting nests deeper than
128 levels is a PARSE_ERROR for that flag only. The targeting object is
level 1, each object or array inside a container adds one, and a resolved
$ref takes the place of its {"$ref": ...} object without adding a level.
The 10 000-value budget and the depth limit are decided in one bounded
pass. A reference to a reference is followed in a loop and recursion stops
at a container past level 128, so a long chain of evaluators can no longer
exhaust the stack: a 400-evaluator chain used to make update() raise
RecursionError, which refused the whole document and left the source
STALE. Only a flag within both limits is expanded, and the expansion
recurses at most 128 levels.
The conformance folder is re-vendored (firefly-vectors.json 6199394...,
evaluate 48: deep-chain-short and deep-chain-long).
…refuse over-deep definitions
- Every $ref resolved while expanding a flag's targeting now costs one unit
of the 10 000-value budget (R-ref-budget), and the counting pass follows
a chain of references only as far as the budget goes. A chain of
references to references used to cost nothing, so each of up to 10 000
occurrences could walk an arbitrarily long chain: the review's 78 KB
probe (2 000 chained evaluators, a 14-level fan-out over the chain end,
20 flags) blocked update() for 14 s; it now returns in about 30 ms.
- A definition nested deeper than Python's stack lets parse_document copy
it is refused with FlagDefinitionError(<flag key>, "definition nests too
deeply"), "$evaluators" naming the evaluator, instead of a
RecursionError; the finite-number walk uses a stack of its own.
- What a reference is (one key, $ref, a text value) lives in the private
module pyfly.feature_flags._references, shared by the provider and the
registry, so the registry no longer counts {"$ref": x, "other": 1} as a
dependency.
…nsaction context filter
…incipal tenant win, and pin the transaction-context restore
…er a failed start or a cancelled stop, and evaluate typed getters with their own type
…lists, null variants, non-object documents)
…rough client hooks
MetricsHook counts feature_flag_evaluations_total{flag, variant, reason} (variant none without one, an error as reason ERROR) and ExposureEventHook publishes FeatureFlagEvaluated on the running loop, on the application's loop from a worker thread, or through a short-lived loop. Both skip the management preview, never change an evaluation (failures are logged at DEBUG) and the exposure hook keeps its scheduled publishes for drain().
… counter creation, publish no variant for an error, and make the drain tests able to fail The hooks and their tests carried a 12-line Apache header without the closing line. MetricsHook now creates its counter under a lock (the registry's counter() is check-then-create), ExposureEventHook publishes variant None when the evaluation failed (as the metric counts it), and the two dedicated drain tests give the drain task real chances to run, so a no-op drain fails them.
…ator vectors) Byte-identical copy of the shared conformance folder (firefly-vectors.json sha256 721e54ad...). The vectors are one case per line now and gain three validate cases: a non-object evaluator rule reported under $evaluators, a 301-level targeting rejected as nesting too deeply, and a 201-level one that is valid. Two of them fail until the validation change that follows.
…uator rules under $evaluators A definition nesting more than 256 levels anywhere (the flag object is level 1, each object or array inside adds one; an evaluator rule's own object is level 1) is invalid with `definition nests too deeply`. The check is one iterative walk with an explicit stack that ends at the first container past the limit and reports depth before non-finite numbers, so a 5,000-level or self-containing (YAML anchor) document is refused cheaply instead of hitting a RecursionError. parse_document no longer recurses at all: the YAML date and int-key conversion is iterative and builds the copy that is stored (no deepcopy, no RecursionError guard). A valid definition of up to 256 levels needs about 520 frames in the copies made downstream (composition, provider), well inside Python's default. A non-object evaluator rule is now reported under key `$evaluators` with `targeting must be an object (evaluator '<name>')`, as the shared vectors and LaraFly do.
… pyfly.feature-flags.enabled FeatureFlagsProperties binds pyfly.feature-flags.* (every default lives in pyfly-defaults.yaml) and refuses an invalid setting at startup with a ValueError naming the full key. An enabled polling source (file, http, store) refuses a refresh-interval that is not greater than 0: the registry polls only a positive interval, so 0 would silently mean "never refresh". FeatureFlagsAutoConfiguration wires the Firefly provider (or none, when the application declares its own OpenFeature provider), the registry of sources, the ambient evaluation context (trust-tenant-header defaults to false), the transaction context filter, the framework's client, the facade and the binding. The metrics hook is always attached and finds the MetricsRegistry at evaluation time; the exposure hook is attached only with events.evaluations. Several provider beans, a source that must load and cannot, and a refused boot composition each fail the context's start. pyfly.feature_flags exports its public names lazily (PEP 562), FeatureFlagsError included, so the package imports without the feature-flags extra.
…ext is never a placeholder
Config.bind resolves the ${...} placeholders of the whole section it binds, which
included the verbatim flags/evaluators maps: a valid string variant such as
"Hello ${name}" failed startup with "Cannot resolve placeholder", without naming
the flag, and there is no escape syntax. FeatureFlagsProperties now binds from a
copy of the configuration whose pyfly.feature-flags section has no flags or
evaluators; the settings still resolve their own placeholders and take their
PYFLY_FEATURE_FLAGS_* overrides, and the config source keeps reading the maps
verbatim.
The tests also pin that the facade receives the registry, that an application
provider still gets the gating slot and the Firefly hooks, and that the tenant
test is immune to PYFLY_PROFILES_ACTIVE; the auto-configuration docstring states
the actual bean creation order.
…flag auto-configuration An application booted the canonical way (create_app, then the context started in the lifespan) uses flags from configuration and a watched file. A request authenticated by a header reaches the facade and a plain OpenFeature client with the same ambient context (roles, and naive and offset-aware date-time attributes evaluated as epoch milliseconds), a flag variant whose text looks like a placeholder is served verbatim, the file source propagates a change as one FeatureFlagsChanged, the exposure events and the evaluation metric count only the framework client's evaluations, and stopping the context restores the SDK and the gating slot.
…nd empty-default vectors) A byte-identical copy of the shared conformance folder (firefly-vectors.json sha256 8986ef44...5aa5f92: validate 27, evaluate 53). New vectors: an empty defaultVariant naming a "" variant is valid and evaluates as no default (the caller's default, reason DEFAULT), and two authored cases pin that a float request and an integer request on a boolean flag are TYPE_MISMATCH. The float case fails until the provider refuses a boolean for a float request.
# Conflicts: # src/pyfly/feature_flags/auto_configuration.py
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.
Adds optional OpenFeature feature flags with one shared evaluation contract for PyFly and LaraFly, including deterministic targeting and percentage rollouts. Flags default to disabled and support configuration, watched JSON/YAML, HTTP synchronization, and audited relational writes with optimistic concurrency.
The facade, decorators, routes, templates, actuator, dashboard, and CLI use the same effective definitions. Management preview suppresses evaluation telemetry; accepted writes that are not yet locally visible return a pending receipt. Shared conformance covers evaluation, context, overrides, HTTP conditions, management, and transaction behavior.
Includes a complete guide and public contract, tested Lumen examples, and updated English and Spanish books. The release workflow builds PDF/EPUB assets from the tag and attaches a source-commit/hash manifest. Package version is 26.10.1; release tag is v26.10.01. Distribution remains GitHub Releases.
Validation and review
Compatibility
Implementation adaptations
The public contract captures the final shared interpretations: enabled-source ordering, UTC management timestamps, unknown-key errors, explicit-context telemetry-free preview, change origins, caller-default gating, trusted actor provenance, and strict object-shaped CLI/HTTP context input. Historical planning text is superseded by that published contract and the shipped guide.