Skip to content

feat!: make set_provider non-blocking, add set_provider_and_wait#595

Draft
jonathannorris wants to merge 3 commits into
mainfrom
feat/non-blocking-set-provider
Draft

feat!: make set_provider non-blocking, add set_provider_and_wait#595
jonathannorris wants to merge 3 commits into
mainfrom
feat/non-blocking-set-provider

Conversation

@jonathannorris
Copy link
Copy Markdown
Member

Summary

  • set_provider() now returns immediately; provider initialization runs in a background thread (daemon)
  • Adds set_provider_and_wait() that blocks until initialization completes or re-raises on failure
  • Updates existing tests to use set_provider_and_wait where init completion is required; adds 8 new tests covering non-blocking semantics explicitly

Motivation

Closes #594. Spec Requirement 1.1.2.4 implies set_provider() should be non-blocking, with a separate variant for callers who need to wait. Python was the only SDK where set_provider() blocked by default. The gunicorn DoS scenario in the issue is real — a provider that hangs during init causes gunicorn to kill the worker with a cryptic WORKER TIMEOUT.

All other SDKs (Go, Java, JS/TS, Ruby) have the same split: a fire-and-forget default and a *AndWait variant. This implementation follows Ruby's approach since it's also synchronous — a wait_for_init flag in the registry routes to either Thread(daemon=True).start() or an inline call.

Behavior change

set_provider() — non-blocking (default):

  • Returns before initialize() completes
  • Flag evaluations during the init window return the default value with PROVIDER_NOT_READY
  • Initialization errors fire a PROVIDER_ERROR event; they are not propagated to the caller

set_provider_and_wait() — blocking:

  • Blocks until initialize() completes or raises
  • Provider is READY (or in error state) before the call returns
  • Re-raises the original exception on failure

Notes

This is a breaking behavioral change for callers who relied on set_provider() blocking until ready. Tagging as feat! so release-please bumps the minor version (pre-1.0, so 0.9.x → 0.10.0 per bump-minor-pre-major). Getting this in before 1.0 is the right call to avoid carrying it as a breaking change post-stable.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 20, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.41%. Comparing base (5ff1cf0) to head (862beb2).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #595      +/-   ##
==========================================
+ Coverage   98.35%   98.41%   +0.06%     
==========================================
  Files          45       45              
  Lines        2183     2276      +93     
==========================================
+ Hits         2147     2240      +93     
  Misses         36       36              
Flag Coverage Δ
unittests 98.41% <100.00%> (+0.06%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces asynchronous provider initialization to the OpenFeature Python SDK. The set_provider method is now non-blocking by default, delegating initialization to a background thread, while a new set_provider_and_wait method has been added for cases requiring blocking behavior. The documentation and existing tests have been updated to reflect these changes. Feedback from the review highlights critical thread-safety concerns, specifically a race condition in the provider registry that could lead to redundant initializations and the need for synchronization when updating shared state from background threads.

Comment thread openfeature/provider/_registry.py
Comment thread openfeature/provider/_registry.py
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the provider registration API to make set_provider() non-blocking by default (initialization runs asynchronously), and introduces set_provider_and_wait() for callers/tests that need to block until provider initialization completes or fails. This aligns the Python SDK behavior with the OpenFeature spec guidance and other SDKs.

Changes:

  • Make provider initialization asynchronous by default via a background daemon thread.
  • Add set_provider_and_wait() (blocking) and update tests/BDD steps to use it where readiness is required.
  • Update README examples to document the new semantics and correct a few API usage examples.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
openfeature/provider/_registry.py Adds async initialization path (wait_for_init flag) and refactors initialization into _run_initialize.
openfeature/api.py Exposes set_provider_and_wait() and adds it to __all__.
tests/test_api.py Updates existing tests to block where needed; adds new tests for non-blocking semantics.
tests/provider/test_registry.py Updates registry tests for the new wait_for_init behavior; adds new async-init tests.
tests/conftest.py Switches fixture initialization to set_provider_and_wait to ensure readiness.
tests/features/steps/steps.py Updates behave steps to use set_provider_and_wait.
tests/features/steps/metadata_steps.py Updates behave steps to use set_provider_and_wait.
README.md Documents non-blocking set_provider() and adds example for set_provider_and_wait(); adjusts several snippets for API correctness.
Comments suppressed due to low confidence (2)

openfeature/provider/_registry.py:130

  • With async initialization, _shutdown_provider can run before _run_initialize has ever set an entry in _provider_status. del self._provider_status[provider] will then raise KeyError, which is caught and reported as a shutdown failure (and also leaves status cleanup inconsistent). Consider using pop(..., None) and/or inserting an initial NOT_READY status when initialization starts so shutdown/clear is safe while init is in-flight.
    def _shutdown_provider(self, provider: FeatureProvider) -> None:
        try:
            if hasattr(provider, "shutdown"):
                provider.shutdown()
            del self._provider_status[provider]
        except Exception as err:

openfeature/provider/_registry.py:108

  • _initialize_provider now spawns a background thread which always calls self.dispatch_event(...PROVIDER_READY/ERROR...) after initialize() finishes. If the provider is replaced/cleared/shutdown while init is still running, this thread can still update _provider_status and run global/client handlers after the provider has been detached, causing late/incorrect events and state resurrection. Consider tracking init threads/futures and canceling/ignoring completion for providers no longer registered (e.g., generation token or per-provider state guarded by a lock).
    def _initialize_provider(
        self, provider: FeatureProvider, wait_for_init: bool = False
    ) -> None:
        provider.attach(self.dispatch_event)
        if wait_for_init:
            self._run_initialize(provider, raise_on_error=True)
        else:
            thread = threading.Thread(
                target=self._run_initialize,
                args=(provider,),
                kwargs={"raise_on_error": False},
                daemon=True,
            )
            thread.start()

    def _run_initialize(
        self, provider: FeatureProvider, raise_on_error: bool = False
    ) -> None:
        try:
            if hasattr(provider, "initialize"):
                provider.initialize(self._get_evaluation_context())
            self.dispatch_event(
                provider, ProviderEvent.PROVIDER_READY, ProviderEventDetails()
            )
        except Exception as err:

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/test_api.py Outdated
Comment thread README.md
@jonathannorris
Copy link
Copy Markdown
Member Author

There's a related PR (#567) worth flagging. It takes a purely additive approach — set_provider() stays blocking, and set_provider_and_wait() is added as a second blocking method. That doesn't fix the DoS scenario from #594; a hanging provider still blocks the caller.

This PR makes set_provider() actually non-blocking (background thread), matching Go, Java, JS/TS, and Ruby. It's a breaking behavioral change, which is exactly why the 1.0-release label matters.

@jonathannorris jonathannorris force-pushed the feat/non-blocking-set-provider branch from cd994d6 to 86f8b63 Compare May 20, 2026 18:10
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
@jonathannorris jonathannorris force-pushed the feat/non-blocking-set-provider branch from 5f4f5fd to 3dd16c1 Compare May 20, 2026 18:14
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
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.

set_provider() should be non-waiting

2 participants