-
Notifications
You must be signed in to change notification settings - Fork 43
fix: dispatch event handlers asynchronously #598
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,6 +3,8 @@ | |||||||||||||||||||||||
| import threading | ||||||||||||||||||||||||
| import typing | ||||||||||||||||||||||||
| from collections import defaultdict | ||||||||||||||||||||||||
| from concurrent.futures import ThreadPoolExecutor | ||||||||||||||||||||||||
| from logging import getLogger | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| from openfeature.event import ( | ||||||||||||||||||||||||
| EventDetails, | ||||||||||||||||||||||||
|
|
@@ -16,6 +18,8 @@ | |||||||||||||||||||||||
| from openfeature.client import OpenFeatureClient | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| _logger = getLogger(__name__) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| _global_lock = threading.RLock() | ||||||||||||||||||||||||
| _global_handlers: dict[ProviderEvent, list[EventHandler]] = defaultdict(list) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
@@ -24,19 +28,39 @@ | |||||||||||||||||||||||
| defaultdict(lambda: defaultdict(list)) | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| _executor_lock = threading.RLock() | ||||||||||||||||||||||||
| _handler_executor = ThreadPoolExecutor(thread_name_prefix="openfeature-event-handler") | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def _run_handler(handler: EventHandler, details: EventDetails) -> None: | ||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||
| handler(details) | ||||||||||||||||||||||||
| except Exception: | ||||||||||||||||||||||||
| _logger.exception("OpenFeature event handler raised an exception") | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def _submit_handler(handler: EventHandler, details: EventDetails) -> None: | ||||||||||||||||||||||||
| with _executor_lock: | ||||||||||||||||||||||||
| _handler_executor.submit(_run_handler, handler, details) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def _run_handlers(handlers: list[EventHandler], details: EventDetails) -> None: | ||||||||||||||||||||||||
| for handler in handlers: | ||||||||||||||||||||||||
| _submit_handler(handler, details) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def run_client_handlers( | ||||||||||||||||||||||||
| client: OpenFeatureClient, event: ProviderEvent, details: EventDetails | ||||||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||||||
| with _client_lock: | ||||||||||||||||||||||||
| for handler in _client_handlers[client][event]: | ||||||||||||||||||||||||
| handler(details) | ||||||||||||||||||||||||
| handlers = list(_client_handlers[client][event]) | ||||||||||||||||||||||||
| _run_handlers(handlers, details) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def run_global_handlers(event: ProviderEvent, details: EventDetails) -> None: | ||||||||||||||||||||||||
| with _global_lock: | ||||||||||||||||||||||||
| for handler in _global_handlers[event]: | ||||||||||||||||||||||||
| handler(details) | ||||||||||||||||||||||||
| handlers = list(_global_handlers[event]) | ||||||||||||||||||||||||
| _run_handlers(handlers, details) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def add_client_handler( | ||||||||||||||||||||||||
|
|
@@ -83,9 +107,17 @@ def run_handlers_for_provider( | |||||||||||||||||||||||
| run_global_handlers(event, details) | ||||||||||||||||||||||||
| # run the handlers for clients associated to this provider | ||||||||||||||||||||||||
| with _client_lock: | ||||||||||||||||||||||||
| for client in _client_handlers: | ||||||||||||||||||||||||
| if client.provider == provider: | ||||||||||||||||||||||||
| run_client_handlers(client, event, details) | ||||||||||||||||||||||||
| client_handlers_snapshot = [ | ||||||||||||||||||||||||
| (client, list(event_handlers[event])) | ||||||||||||||||||||||||
| for client, event_handlers in _client_handlers.items() | ||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||
| handlers = [ | ||||||||||||||||||||||||
| handler | ||||||||||||||||||||||||
| for client, event_list in client_handlers_snapshot | ||||||||||||||||||||||||
| if client.provider == provider | ||||||||||||||||||||||||
| for handler in event_list | ||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||
| _run_handlers(handlers, details) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def _run_immediate_handler( | ||||||||||||||||||||||||
|
|
@@ -98,11 +130,21 @@ def _run_immediate_handler( | |||||||||||||||||||||||
| ProviderStatus.STALE: ProviderEvent.PROVIDER_STALE, | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| if event == status_to_event.get(client.get_provider_status()): | ||||||||||||||||||||||||
| handler(EventDetails(provider_name=client.provider.get_metadata().name)) | ||||||||||||||||||||||||
| _submit_handler( | ||||||||||||||||||||||||
| handler, | ||||||||||||||||||||||||
| EventDetails(provider_name=client.provider.get_metadata().name), | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def clear() -> None: | ||||||||||||||||||||||||
| global _handler_executor | ||||||||||||||||||||||||
| with _global_lock: | ||||||||||||||||||||||||
| _global_handlers.clear() | ||||||||||||||||||||||||
| with _client_lock: | ||||||||||||||||||||||||
| _client_handlers.clear() | ||||||||||||||||||||||||
| with _executor_lock: | ||||||||||||||||||||||||
| old_executor = _handler_executor | ||||||||||||||||||||||||
| _handler_executor = ThreadPoolExecutor( | ||||||||||||||||||||||||
| thread_name_prefix="openfeature-event-handler" | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
Comment on lines
+145
to
+149
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Calling
Suggested change
|
||||||||||||||||||||||||
| old_executor.shutdown(wait=True, cancel_futures=False) | ||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accessing
client.provider(which queries the provider registry and may acquire registry-level locks) while holding_client_lockintroduces a risk of lock inversion and potential deadlocks if another thread holding the registry lock attempts to acquire_client_lock. To prevent this, copy the client handlers list under_client_lockfirst, and then perform the provider filtering outside of the lock.