Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 43
feat!: add isolated API instances, remove duped global funcs#591
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base:main
Are you sure you want to change the base?
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
5deba257ad838b556afed0ba95465b46606File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| from __future__ import annotations | ||
| from openfeature._event_support import EventSupport | ||
| from openfeature.client import OpenFeatureClient | ||
| from openfeature.evaluation_context import EvaluationContext | ||
| from openfeature.event import EventHandler, ProviderEvent | ||
| from openfeature.exception import GeneralError | ||
| from openfeature.hook import Hook | ||
| from openfeature.provider import FeatureProvider, ProviderStatus | ||
| from openfeature.provider._registry import ProviderRegistry | ||
| from openfeature.provider.metadata import Metadata | ||
| from openfeature.transaction_context import TransactionContextPropagator | ||
| from openfeature.transaction_context.no_op_transaction_context_propagator import ( | ||
| NoOpTransactionContextPropagator, | ||
| ) | ||
| class OpenFeatureAPI: | ||
| """An independent OpenFeature API instance with its own isolated state. | ||
| Each instance maintains its own providers, evaluation context, hooks, | ||
| event handlers, and transaction context propagator; fully separate from | ||
| the global singleton and from other instances. | ||
| """ | ||
| def __init__(self) -> None: | ||
| self._hooks: list[Hook] = [] | ||
toddbaert marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| self._evaluation_context = EvaluationContext() | ||
| self._transaction_context_propagator: TransactionContextPropagator = ( | ||
| NoOpTransactionContextPropagator() | ||
| ) | ||
| self._event_support = EventSupport() | ||
| self._provider_registry = ProviderRegistry( | ||
| event_support=self._event_support, | ||
| evaluation_context_getter=self.get_evaluation_context, | ||
| ) | ||
| # --- Client creation --- | ||
| def get_client( | ||
| self, domain: str | None = None, version: str | None = None | ||
| ) -> OpenFeatureClient: | ||
| return OpenFeatureClient(domain=domain, version=version, api=self) | ||
| # --- Provider management --- | ||
| def set_provider( | ||
| self, provider: FeatureProvider, domain: str | None = None | ||
| ) -> None: | ||
| if domain is None: | ||
| self._provider_registry.set_default_provider(provider) | ||
| else: | ||
| self._provider_registry.set_provider(domain, provider) | ||
| def set_provider_and_wait( | ||
| self, provider: FeatureProvider, domain: str | None = None | ||
| ) -> None: | ||
| if domain is None: | ||
| self._provider_registry.set_default_provider(provider, wait_for_init=True) | ||
| else: | ||
| self._provider_registry.set_provider(domain, provider, wait_for_init=True) | ||
| def get_provider_metadata(self, domain: str | None = None) -> Metadata: | ||
| return self._provider_registry.get_provider(domain).get_metadata() | ||
| def get_provider(self, domain: str | None = None) -> FeatureProvider: | ||
| return self._provider_registry.get_provider(domain) | ||
| def get_provider_status(self, provider: FeatureProvider) -> ProviderStatus: | ||
| return self._provider_registry.get_provider_status(provider) | ||
| def clear_providers(self) -> None: | ||
| self._provider_registry.clear_providers() | ||
| self._event_support.clear() | ||
| def shutdown(self) -> None: | ||
| # shutdown -> remove providers -> set default provider to NoOp -> remove event handlers | ||
| self.clear_providers() | ||
| # remove hooks | ||
| self.clear_hooks() | ||
| # set evaluation context to default | ||
| self._evaluation_context = EvaluationContext() | ||
| # set propagator to NoOp | ||
| self._transaction_context_propagator = NoOpTransactionContextPropagator() | ||
| # --- Hooks --- | ||
| def add_hooks(self, hooks: list[Hook]) -> None: | ||
| self._hooks = self._hooks + hooks | ||
| def clear_hooks(self) -> None: | ||
| self._hooks = [] | ||
| def get_hooks(self) -> list[Hook]: | ||
| return self._hooks | ||
toddbaert marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| # --- Evaluation context --- | ||
| def get_evaluation_context(self) -> EvaluationContext: | ||
| return self._evaluation_context | ||
| def set_evaluation_context(self, evaluation_context: EvaluationContext) -> None: | ||
| if evaluation_context is None: | ||
| raise GeneralError(error_message="No api level evaluation context") | ||
| self._evaluation_context = evaluation_context | ||
toddbaert marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| def clear_evaluation_context(self) -> None: | ||
| self.set_evaluation_context(EvaluationContext()) | ||
| # --- Transaction context --- | ||
| def set_transaction_context_propagator( | ||
| self, transaction_context_propagator: TransactionContextPropagator | ||
| ) -> None: | ||
| self._transaction_context_propagator = transaction_context_propagator | ||
toddbaert marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| def clear_transaction_context_propagator(self) -> None: | ||
| self.set_transaction_context_propagator(NoOpTransactionContextPropagator()) | ||
| def get_transaction_context(self) -> EvaluationContext: | ||
| return self._transaction_context_propagator.get_transaction_context() | ||
| def set_transaction_context(self, evaluation_context: EvaluationContext) -> None: | ||
| self._transaction_context_propagator.set_transaction_context(evaluation_context) | ||
| # --- Event handlers --- | ||
| def add_handler(self, event: ProviderEvent, handler: EventHandler) -> None: | ||
| self._event_support.add_global_handler(event, handler, self.get_client) | ||
| def remove_handler(self, event: ProviderEvent, handler: EventHandler) -> None: | ||
| self._event_support.remove_global_handler(event, handler) | ||
| _default_api = OpenFeatureAPI() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -4,6 +4,7 @@ | ||
| import threading | ||
| import typing | ||
| from collections import defaultdict | ||
| from collections.abc import Callable | ||
| from concurrent.futures import ThreadPoolExecutor | ||
| from logging import getLogger | ||
toddbaert marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| @@ -23,103 +24,6 @@ | ||
| _event_executor = ThreadPoolExecutor(thread_name_prefix="openfeature-event-handler") | ||
| atexit.register(_event_executor.shutdown, wait=True) | ||
| _global_lock = threading.RLock() | ||
| _global_handlers: dict[ProviderEvent, list[EventHandler]] = defaultdict(list) | ||
| _client_lock = threading.RLock() | ||
| _client_handlers: dict[OpenFeatureClient, dict[ProviderEvent, list[EventHandler]]] = ( | ||
| defaultdict(lambda: defaultdict(list)) | ||
| ) | ||
| def run_client_handlers( | ||
| client: OpenFeatureClient, event: ProviderEvent, details: EventDetails | ||
| ) -> None: | ||
| with _client_lock: | ||
| handlers_by_event = _client_handlers.get(client) | ||
| if handlers_by_event is None: | ||
| return | ||
| handlers = tuple(handlers_by_event.get(event, ())) | ||
| for handler in handlers: | ||
| _submit_handler(handler, details) | ||
| def run_global_handlers(event: ProviderEvent, details: EventDetails) -> None: | ||
| with _global_lock: | ||
| handlers = tuple(_global_handlers.get(event, ())) | ||
| for handler in handlers: | ||
| _submit_handler(handler, details) | ||
| def add_client_handler( | ||
| client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler | ||
| ) -> None: | ||
| with _client_lock: | ||
| handlers = _client_handlers[client][event] | ||
| handlers.append(handler) | ||
| _run_immediate_handler(client, event, handler) | ||
| def remove_client_handler( | ||
| client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler | ||
| ) -> None: | ||
| with _client_lock: | ||
| handlers = _client_handlers[client][event] | ||
| handlers.remove(handler) | ||
| def add_global_handler(event: ProviderEvent, handler: EventHandler) -> None: | ||
| with _global_lock: | ||
| _global_handlers[event].append(handler) | ||
| from openfeature.api import get_client # noqa: PLC0415 | ||
| _run_immediate_handler(get_client(), event, handler) | ||
| def remove_global_handler(event: ProviderEvent, handler: EventHandler) -> None: | ||
| with _global_lock: | ||
| _global_handlers[event].remove(handler) | ||
| def run_handlers_for_provider( | ||
| provider: FeatureProvider, | ||
| event: ProviderEvent, | ||
| provider_details: ProviderEventDetails, | ||
| ) -> None: | ||
| details = EventDetails.from_provider_event_details( | ||
| provider.get_metadata().name, provider_details | ||
| ) | ||
| # run the global handlers | ||
| run_global_handlers(event, details) | ||
| # run the handlers for clients associated to this provider | ||
| with _client_lock: | ||
| clients = tuple( | ||
| client for client in _client_handlers if client.provider == provider | ||
| ) | ||
| for client in clients: | ||
| run_client_handlers(client, event, details) | ||
| def _run_immediate_handler( | ||
| client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler | ||
| ) -> None: | ||
| status_to_event = { | ||
| ProviderStatus.READY: ProviderEvent.PROVIDER_READY, | ||
| ProviderStatus.ERROR: ProviderEvent.PROVIDER_ERROR, | ||
| ProviderStatus.FATAL: ProviderEvent.PROVIDER_ERROR, | ||
| ProviderStatus.STALE: ProviderEvent.PROVIDER_STALE, | ||
| } | ||
| if event == status_to_event.get(client.get_provider_status()): | ||
| _submit_handler( | ||
| handler, EventDetails(provider_name=client.provider.get_metadata().name) | ||
| ) | ||
| def _submit_handler(handler: EventHandler, details: EventDetails) -> None: | ||
| _event_executor.submit(_run_handler, handler, details) | ||
| @@ -132,8 +36,112 @@ def _run_handler(handler: EventHandler, details: EventDetails) -> None: | ||
| logger.exception("Unhandled exception in OpenFeature event handler") | ||
| def clear() -> None: | ||
| with _global_lock: | ||
| _global_handlers.clear() | ||
| with _client_lock: | ||
| _client_handlers.clear() | ||
| class EventSupport: | ||
| """Per-API-instance event handler storage and dispatch.""" | ||
| def __init__(self) -> None: | ||
| self._global_lock = threading.RLock() | ||
| self._global_handlers: dict[ProviderEvent, list[EventHandler]] = defaultdict( | ||
| list | ||
| ) | ||
| self._client_lock = threading.RLock() | ||
| self._client_handlers: dict[ | ||
| OpenFeatureClient, dict[ProviderEvent, list[EventHandler]] | ||
| ] = defaultdict(lambda: defaultdict(list)) | ||
| def run_client_handlers( | ||
| self, client: OpenFeatureClient, event: ProviderEvent, details: EventDetails | ||
| ) -> None: | ||
| with self._client_lock: | ||
| handlers_by_event = self._client_handlers.get(client) | ||
| if handlers_by_event is None: | ||
| return | ||
| handlers = tuple(handlers_by_event.get(event, ())) | ||
| for handler in handlers: | ||
| _submit_handler(handler, details) | ||
| def run_global_handlers(self, event: ProviderEvent, details: EventDetails) -> None: | ||
| with self._global_lock: | ||
| handlers = tuple(self._global_handlers.get(event, ())) | ||
| for handler in handlers: | ||
| _submit_handler(handler, details) | ||
| def add_client_handler( | ||
| self, client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler | ||
| ) -> None: | ||
| with self._client_lock: | ||
| handlers = self._client_handlers[client][event] | ||
| handlers.append(handler) | ||
| self._run_immediate_handler(client, event, handler) | ||
| def remove_client_handler( | ||
| self, client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler | ||
| ) -> None: | ||
| with self._client_lock: | ||
| handlers = self._client_handlers[client][event] | ||
| handlers.remove(handler) | ||
| def add_global_handler( | ||
| self, | ||
| event: ProviderEvent, | ||
| handler: EventHandler, | ||
| get_client: Callable[[], OpenFeatureClient], | ||
| ) -> None: | ||
| with self._global_lock: | ||
| self._global_handlers[event].append(handler) | ||
| self._run_immediate_handler(get_client(), event, handler) | ||
| def remove_global_handler( | ||
| self, event: ProviderEvent, handler: EventHandler | ||
| ) -> None: | ||
| with self._global_lock: | ||
| self._global_handlers[event].remove(handler) | ||
| def run_handlers_for_provider( | ||
| self, | ||
| provider: FeatureProvider, | ||
| event: ProviderEvent, | ||
| provider_details: ProviderEventDetails, | ||
| ) -> None: | ||
| details = EventDetails.from_provider_event_details( | ||
| provider.get_metadata().name, provider_details | ||
| ) | ||
| # run the global handlers | ||
| self.run_global_handlers(event, details) | ||
| # run the handlers for clients associated to this provider | ||
| with self._client_lock: | ||
| clients = tuple( | ||
| client | ||
| for client in self._client_handlers | ||
| if client.provider == provider | ||
| ) | ||
| for client in clients: | ||
| self.run_client_handlers(client, event, details) | ||
| def _run_immediate_handler( | ||
| self, client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler | ||
| ) -> None: | ||
| status_to_event = { | ||
| ProviderStatus.READY: ProviderEvent.PROVIDER_READY, | ||
| ProviderStatus.ERROR: ProviderEvent.PROVIDER_ERROR, | ||
| ProviderStatus.FATAL: ProviderEvent.PROVIDER_ERROR, | ||
| ProviderStatus.STALE: ProviderEvent.PROVIDER_STALE, | ||
| } | ||
| if event == status_to_event.get(client.get_provider_status()): | ||
| _submit_handler( | ||
| handler, | ||
| EventDetails(provider_name=client.provider.get_metadata().name), | ||
| ) | ||
| def clear(self) -> None: | ||
| with self._global_lock: | ||
| self._global_handlers.clear() | ||
| with self._client_lock: | ||
| self._client_handlers.clear() | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.