-
Notifications
You must be signed in to change notification settings - Fork 3.3k
[EventHubs] add amqp switch support #25965
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
Merged
34 commits merged into
Azure:feature/eventhub/pyproto
from
swathipil:swathipil/pyamqp-switch-merge-uamqp
Sep 22, 2022
Merged
Changes from all commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
c72e79b
initial commit for switch support
swathipil eb9d560
fix edbatch test for ci
swathipil e56a266
update default socket timeout time to 0.1
swathipil 0848bc8
update default socket timeout for ssltransport to 0.2
swathipil 4449d96
only add uamqp message headers to outgoing message if values set on a…
swathipil fd94511
support/test legacy batch message
swathipil e9dd09a
update type annotations for both uamqp and pyamqp
swathipil c8e7c2e
remove uamqp dependency
swathipil 834eaa4
bump test timeout
swathipil cca338c
update version
swathipil bdc416e
add back pylint
swathipil 49b828f
remove unused imports
kashifkhan ee5ac84
clean up unused imports
kashifkhan 9f6b7d1
fix whitespace pylint issues
kashifkhan 983aa44
more clean up from pylint
kashifkhan 793a43b
docstrings
kashifkhan a239d6a
fix __init__
kashifkhan dd05e56
protected members
kashifkhan eb0ec0a
more clean up
kashifkhan eecbd7d
more pylint clean up
kashifkhan 7cda10d
add deprecation warning for message
swathipil 3b11441
rename uamqp_BatchMessage for APIView
swathipil 050f04e
Merge branch 'feature/eventhub/pyproto' into swathipil/pyamqp-switch-…
swathipil 8b3da14
fix tests/temporarily ignore pylint
swathipil f08c1a1
Merge branch 'swathipil/pyamqp-switch-merge-uamqp' of https://github.…
swathipil 6015765
add sleeps to bp tests
swathipil 2494ebd
bump sleep times for bp tests
swathipil 98e22e1
bump bp test sleep
swathipil 0f9f576
annas comments
swathipil 04929dc
missed changes
swathipil 7c7dacc
more batch message backcompt checks
swathipil 985eebe
comment
swathipil 63d7b8c
use connect_timeout in transport to settimeout
swathipil c5aa82e
use read timeout instead of connect timeout
swathipil File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
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
13 changes: 13 additions & 0 deletions
13
sdk/eventhub/azure-eventhub/azure/eventhub/_buffered_producer/__init__.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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| # -------------------------------------------------------------------------------------------- | ||
|
swathipil marked this conversation as resolved.
|
||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. See License.txt in the project root for license information. | ||
| # -------------------------------------------------------------------------------------------- | ||
| from ._buffered_producer import BufferedProducer | ||
| from ._partition_resolver import PartitionResolver | ||
| from ._buffered_producer_dispatcher import BufferedProducerDispatcher | ||
|
|
||
| __all__ = [ | ||
| "BufferedProducer", | ||
| "PartitionResolver", | ||
| "BufferedProducerDispatcher", | ||
| ] | ||
218 changes: 218 additions & 0 deletions
218
sdk/eventhub/azure-eventhub/azure/eventhub/_buffered_producer/_buffered_producer.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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,218 @@ | ||
| # -------------------------------------------------------------------------------------------- | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. See License.txt in the project root for license information. | ||
| # -------------------------------------------------------------------------------------------- | ||
| from __future__ import annotations | ||
| import time | ||
| import queue | ||
| import logging | ||
| from threading import RLock | ||
| from concurrent.futures import ThreadPoolExecutor | ||
| from typing import Optional, Callable, TYPE_CHECKING | ||
|
|
||
| from .._producer import EventHubProducer | ||
| from .._common import EventDataBatch | ||
| from ..exceptions import OperationTimeoutError | ||
|
|
||
| if TYPE_CHECKING: | ||
| from .._transport._base import AmqpTransport | ||
| from .._producer_client import SendEventTypes | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class BufferedProducer: | ||
| # pylint: disable=too-many-instance-attributes | ||
| def __init__( | ||
| self, | ||
| producer: EventHubProducer, | ||
| partition_id: str, | ||
| on_success: Callable[["SendEventTypes", Optional[str]], None], | ||
| on_error: Callable[["SendEventTypes", Optional[str], Exception], None], | ||
| max_message_size_on_link: int, | ||
| executor: ThreadPoolExecutor, | ||
| *, | ||
| amqp_transport: AmqpTransport, | ||
|
swathipil marked this conversation as resolved.
|
||
| max_buffer_length: int, | ||
| max_wait_time: float = 1 | ||
| ): | ||
| self._buffered_queue: queue.Queue = queue.Queue() | ||
| self._max_buffer_len = max_buffer_length | ||
| self._cur_buffered_len = 0 | ||
| self._executor: ThreadPoolExecutor = executor | ||
| self._producer: EventHubProducer = producer | ||
| self._lock = RLock() | ||
| self._max_wait_time = max_wait_time | ||
| self._on_success = self.failsafe_callback(on_success) | ||
| self._on_error = self.failsafe_callback(on_error) | ||
| self._last_send_time = None | ||
| self._running = False | ||
| self._cur_batch: Optional[EventDataBatch] = None | ||
| self._max_message_size_on_link = max_message_size_on_link | ||
| self._check_max_wait_time_future = None | ||
| self.partition_id = partition_id | ||
| self._amqp_transport = amqp_transport | ||
|
swathipil marked this conversation as resolved.
|
||
|
|
||
| def start(self): | ||
| with self._lock: | ||
| self._cur_batch = EventDataBatch(self._max_message_size_on_link, amqp_transport=self._amqp_transport) | ||
|
swathipil marked this conversation as resolved.
|
||
| self._running = True | ||
| if self._max_wait_time: | ||
| self._last_send_time = time.time() | ||
| self._check_max_wait_time_future = self._executor.submit( | ||
| self.check_max_wait_time_worker | ||
| ) | ||
|
|
||
| def stop(self, flush=True, timeout_time=None, raise_error=False): | ||
| self._running = False | ||
| if flush: | ||
| with self._lock: | ||
| self.flush(timeout_time=timeout_time, raise_error=raise_error) | ||
| else: | ||
| if self._cur_buffered_len: | ||
| _LOGGER.warning( | ||
| "Shutting down Partition %r. There are still %r events in the buffer which will be lost", | ||
| self.partition_id, | ||
| self._cur_buffered_len, | ||
| ) | ||
| if self._check_max_wait_time_future: | ||
| remain_timeout = timeout_time - time.time() if timeout_time else None | ||
| try: | ||
| self._check_max_wait_time_future.result(remain_timeout) | ||
| except Exception as exc: # pylint: disable=broad-except | ||
| _LOGGER.warning( | ||
| "Partition %r stopped with error %r", self.partition_id, exc | ||
| ) | ||
| self._producer.close() | ||
|
|
||
| def put_events(self, events, timeout_time=None): | ||
| # Put single event or EventDataBatch into the queue. | ||
| # This method would raise OperationTimeout if the queue does not have enough space for the input and | ||
| # flush cannot finish in timeout. | ||
| try: | ||
| new_events_len = len(events) | ||
| except TypeError: | ||
| new_events_len = 1 | ||
| if self._max_buffer_len - self._cur_buffered_len < new_events_len: | ||
| _LOGGER.info( | ||
| "The buffer for partition %r is full. Attempting to flush before adding %r events.", | ||
| self.partition_id, | ||
| new_events_len, | ||
| ) | ||
| # flush the buffer | ||
| self.flush(timeout_time=timeout_time) | ||
| if timeout_time and time.time() > timeout_time: | ||
| raise OperationTimeoutError( | ||
| "Failed to enqueue events into buffer due to timeout." | ||
| ) | ||
| try: | ||
| # add single event into current batch | ||
| self._cur_batch.add(events) | ||
| except AttributeError: # if the input events is a EventDataBatch, put the whole into the buffer | ||
| # if there are events in cur_batch, enqueue cur_batch to the buffer | ||
| with self._lock: | ||
| if self._cur_batch: | ||
| self._buffered_queue.put(self._cur_batch) | ||
| self._buffered_queue.put(events) | ||
| # create a new batch for incoming events | ||
| self._cur_batch = EventDataBatch(self._max_message_size_on_link, amqp_transport=self._amqp_transport) | ||
|
swathipil marked this conversation as resolved.
|
||
| except ValueError: | ||
| # add single event exceeds the cur batch size, create new batch | ||
| with self._lock: | ||
| self._buffered_queue.put(self._cur_batch) | ||
| self._cur_batch = EventDataBatch(self._max_message_size_on_link, amqp_transport=self._amqp_transport) | ||
|
swathipil marked this conversation as resolved.
|
||
| self._cur_batch.add(events) | ||
| with self._lock: | ||
| self._cur_buffered_len += new_events_len | ||
|
|
||
| def failsafe_callback(self, callback): | ||
| def wrapper_callback(*args, **kwargs): | ||
| try: | ||
| callback(*args, **kwargs) | ||
| except Exception as exc: # pylint: disable=broad-except | ||
| _LOGGER.warning( | ||
| "On partition %r, callback %r encountered exception %r", | ||
| callback.__name__, | ||
| exc, | ||
| self.partition_id, | ||
| ) | ||
|
|
||
| return wrapper_callback | ||
|
|
||
| def flush(self, timeout_time=None, raise_error=True): | ||
| # pylint: disable=protected-access | ||
| # try flushing all the buffered batch within given time | ||
| with self._lock: | ||
| _LOGGER.info("Partition: %r started flushing.", self.partition_id) | ||
| if self._cur_batch: # if there is batch, enqueue it to the buffer first | ||
| self._buffered_queue.put(self._cur_batch) | ||
| while self._buffered_queue.qsize() > 0: | ||
| remaining_time = timeout_time - time.time() if timeout_time else None | ||
| if (remaining_time and remaining_time > 0) or remaining_time is None: | ||
| try: | ||
| batch = self._buffered_queue.get(block=False) | ||
| except queue.Empty: | ||
| break | ||
| self._buffered_queue.task_done() | ||
| try: | ||
| _LOGGER.info("Partition %r is sending.", self.partition_id) | ||
| self._producer.send( | ||
| batch, | ||
| timeout=timeout_time - time.time() | ||
| if timeout_time | ||
| else None, | ||
| ) | ||
| _LOGGER.info( | ||
| "Partition %r sending %r events succeeded.", | ||
| self.partition_id, | ||
| len(batch), | ||
| ) | ||
| self._on_success(batch._internal_events, self.partition_id) | ||
| except Exception as exc: # pylint: disable=broad-except | ||
| _LOGGER.info( | ||
| "Partition %r sending %r events failed due to exception: %r ", | ||
| self.partition_id, | ||
| len(batch), | ||
| exc, | ||
| ) | ||
| self._on_error(batch._internal_events, self.partition_id, exc) | ||
| finally: | ||
| self._cur_buffered_len -= len(batch) | ||
| else: | ||
| _LOGGER.info( | ||
| "Partition %r fails to flush due to timeout.", self.partition_id | ||
| ) | ||
| if raise_error: | ||
| raise OperationTimeoutError( | ||
| "Failed to flush {!r} within {}".format( | ||
| self.partition_id, timeout_time | ||
| ) | ||
| ) | ||
| break | ||
| # after finishing flushing, reset cur batch and put it into the buffer | ||
| self._last_send_time = time.time() | ||
| #reset buffered count | ||
| self._cur_buffered_len = 0 | ||
| self._cur_batch = EventDataBatch(self._max_message_size_on_link, amqp_transport=self._amqp_transport) | ||
|
swathipil marked this conversation as resolved.
|
||
| _LOGGER.info("Partition %r finished flushing.", self.partition_id) | ||
|
|
||
| def check_max_wait_time_worker(self): | ||
| while self._running: | ||
| if self._cur_buffered_len > 0: | ||
| now_time = time.time() | ||
| _LOGGER.info( | ||
| "Partition %r worker is checking max_wait_time.", self.partition_id | ||
| ) | ||
| # flush the partition if the producer is running beyond the waiting time | ||
| # or the buffer is at max capacity | ||
| if (now_time - self._last_send_time > self._max_wait_time) or ( | ||
| self._cur_buffered_len >= self._max_buffer_len | ||
| ): | ||
| # in the worker, not raising error for flush, users can not handle this | ||
| with self._lock: | ||
| self.flush(raise_error=False) | ||
| time.sleep(min(self._max_wait_time, 5)) | ||
|
|
||
| @property | ||
| def buffered_event_count(self): | ||
| return self._cur_buffered_len | ||
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.