Skip to content
This repository was archived by the owner on Oct 12, 2023. It is now read-only.

Commit dd38016

Browse files
authored
Merge pull request #44 from annatisch/eh_scenarios
Stability improvements
2 parents 91b630c + f02c954 commit dd38016

31 files changed

+825
-257
lines changed

HISTORY.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,19 @@
33
Release History
44
===============
55

6+
0.2.0rc2 (2018-07-29)
7+
+++++++++++++++++++++
8+
9+
- **Breaking change** `EventData.offset` will now return an object of type `~uamqp.common.Offset` rather than str.
10+
The original string value can be retrieved from `~uamqp.common.Offset.value`.
11+
- Each sender/receiver will now run in its own independent connection.
12+
- Updated uAMQP dependency to 0.2.0
13+
- Fixed issue with IoTHub clients not being able to retrieve partition information.
14+
- Added support for HTTP proxy settings to both EventHubClient and EPH.
15+
- Added error handling policy to automatically reconnect on retryable error.
16+
- Added keep-alive thread for maintaining an unused connection.
17+
18+
619
0.2.0rc1 (2018-07-06)
720
+++++++++++++++++++++
821

azure/eventhub/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Licensed under the MIT License. See License.txt in the project root for license information.
44
# --------------------------------------------------------------------------------------------
55

6-
__version__ = "0.2.0rc1"
6+
__version__ = "0.2.0rc2"
77

88
from azure.eventhub.common import EventData, EventHubError, Offset
99
from azure.eventhub.client import EventHubClient

azure/eventhub/_async/__init__.py

Lines changed: 26 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@
77
import asyncio
88
import time
99
import datetime
10+
try:
11+
from urllib import urlparse, unquote_plus, urlencode, quote_plus
12+
except ImportError:
13+
from urllib.parse import urlparse, unquote_plus, urlencode, quote_plus
1014

1115
from uamqp import authentication, constants, types, errors
1216
from uamqp import (
1317
Message,
14-
Source,
1518
ConnectionAsync,
1619
AMQPClientAsync,
1720
SendClientAsync,
@@ -37,7 +40,7 @@ class EventHubClientAsync(EventHubClient):
3740
sending events to and receiving events from the Azure Event Hubs service.
3841
"""
3942

40-
def _create_auth(self, auth_uri, username, password): # pylint: disable=no-self-use
43+
def _create_auth(self, username=None, password=None): # pylint: disable=no-self-use
4144
"""
4245
Create an ~uamqp.authentication.cbs_auth_async.SASTokenAuthAsync instance to authenticate
4346
the session.
@@ -49,32 +52,13 @@ def _create_auth(self, auth_uri, username, password): # pylint: disable=no-self
4952
:param password: The shared access key.
5053
:type password: str
5154
"""
55+
username = username or self._auth_config['username']
56+
password = password or self._auth_config['password']
5257
if "@sas.root" in username:
53-
return authentication.SASLPlain(self.address.hostname, username, password)
54-
return authentication.SASTokenAsync.from_shared_access_key(auth_uri, username, password)
55-
56-
def _create_connection_async(self):
57-
"""
58-
Create a new ~uamqp._async.connection_async.ConnectionAsync instance that will be shared between all
59-
AsyncSender/AsyncReceiver clients.
60-
"""
61-
if not self.connection:
62-
log.info("{}: Creating connection with address={}".format(
63-
self.container_id, self.address.geturl()))
64-
self.connection = ConnectionAsync(
65-
self.address.hostname,
66-
self.auth,
67-
container_id=self.container_id,
68-
properties=self._create_properties(),
69-
debug=self.debug)
70-
71-
async def _close_connection_async(self):
72-
"""
73-
Close and destroy the connection async.
74-
"""
75-
if self.connection:
76-
await self.connection.destroy_async()
77-
self.connection = None
58+
return authentication.SASLPlain(
59+
self.address.hostname, username, password, http_proxy=self.http_proxy)
60+
return authentication.SASTokenAsync.from_shared_access_key(
61+
self.auth_uri, username, password, timeout=60, http_proxy=self.http_proxy)
7862

7963
async def _close_clients_async(self):
8064
"""
@@ -85,17 +69,13 @@ async def _close_clients_async(self):
8569
async def _wait_for_client(self, client):
8670
try:
8771
while client.get_handler_state().value == 2:
88-
await self.connection.work_async()
72+
await client._handler._connection.work_async() # pylint: disable=protected-access
8973
except Exception as exp: # pylint: disable=broad-except
9074
await client.close_async(exception=exp)
9175

9276
async def _start_client_async(self, client):
9377
try:
94-
await client.open_async(self.connection)
95-
started = await client.has_started()
96-
while not started:
97-
await self.connection.work_async()
98-
started = await client.has_started()
78+
await client.open_async()
9979
except Exception as exp: # pylint: disable=broad-except
10080
await client.close_async(exception=exp)
10181

@@ -108,9 +88,8 @@ async def _handle_redirect(self, redirects):
10888
redirects = [c.redirected for c in self.clients if c.redirected]
10989
if not all(r.hostname == redirects[0].hostname for r in redirects):
11090
raise EventHubError("Multiple clients attempting to redirect to different hosts.")
111-
self.auth = self._create_auth(redirects[0].address.decode('utf-8'), **self._auth_config)
112-
await self.connection.redirect_async(redirects[0], self.auth)
113-
await asyncio.gather(*[c.open_async(self.connection) for c in self.clients])
91+
self._process_redirect_uri(redirects[0])
92+
await asyncio.gather(*[c.open_async() for c in self.clients])
11493

11594
async def run_async(self):
11695
"""
@@ -125,7 +104,6 @@ async def run_async(self):
125104
:rtype: list[~azure.eventhub.common.EventHubError]
126105
"""
127106
log.info("{}: Starting {} clients".format(self.container_id, len(self.clients)))
128-
self._create_connection_async()
129107
tasks = [self._start_client_async(c) for c in self.clients]
130108
try:
131109
await asyncio.gather(*tasks)
@@ -153,18 +131,21 @@ async def stop_async(self):
153131
log.info("{}: Stopping {} clients".format(self.container_id, len(self.clients)))
154132
self.stopped = True
155133
await self._close_clients_async()
156-
await self._close_connection_async()
157134

158135
async def get_eventhub_info_async(self):
159136
"""
160137
Get details on the specified EventHub async.
161138
162139
:rtype: dict
163140
"""
164-
eh_name = self.address.path.lstrip('/')
165-
target = "amqps://{}/{}".format(self.address.hostname, eh_name)
166-
async with AMQPClientAsync(target, auth=self.auth, debug=self.debug) as mgmt_client:
167-
mgmt_msg = Message(application_properties={'name': eh_name})
141+
alt_creds = {
142+
"username": self._auth_config.get("iot_username"),
143+
"password":self._auth_config.get("iot_password")}
144+
try:
145+
mgmt_auth = self._create_auth(**alt_creds)
146+
mgmt_client = AMQPClientAsync(self.mgmt_target, auth=mgmt_auth, debug=self.debug)
147+
await mgmt_client.open_async()
148+
mgmt_msg = Message(application_properties={'name': self.eh_name})
168149
response = await mgmt_client.mgmt_request_async(
169150
mgmt_msg,
170151
constants.READ_OPERATION,
@@ -180,6 +161,8 @@ async def get_eventhub_info_async(self):
180161
output['partition_count'] = eh_info[b'partition_count']
181162
output['partition_ids'] = [p.decode('utf-8') for p in eh_info[b'partition_ids']]
182163
return output
164+
finally:
165+
await mgmt_client.close_async()
183166

184167
def add_async_receiver(self, consumer_group, partition, offset=None, prefetch=300, operation=None, loop=None):
185168
"""
@@ -201,10 +184,7 @@ def add_async_receiver(self, consumer_group, partition, offset=None, prefetch=30
201184
path = self.address.path + operation if operation else self.address.path
202185
source_url = "amqps://{}{}/ConsumerGroups/{}/Partitions/{}".format(
203186
self.address.hostname, path, consumer_group, partition)
204-
source = Source(source_url)
205-
if offset is not None:
206-
source.set_filter(offset.selector())
207-
handler = AsyncReceiver(self, source, prefetch=prefetch, loop=loop)
187+
handler = AsyncReceiver(self, source_url, offset=offset, prefetch=prefetch, loop=loop)
208188
self.clients.append(handler)
209189
return handler
210190

azure/eventhub/_async/receiver_async.py

Lines changed: 74 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,19 @@
66
import asyncio
77

88
from uamqp import errors, types
9-
from uamqp import ReceiveClientAsync
9+
from uamqp import ReceiveClientAsync, Source
1010

1111
from azure.eventhub import EventHubError, EventData
1212
from azure.eventhub.receiver import Receiver
13+
from azure.eventhub.common import _error_handler
1314

1415

1516
class AsyncReceiver(Receiver):
1617
"""
1718
Implements the async API of a Receiver.
1819
"""
1920

20-
def __init__(self, client, source, prefetch=300, epoch=None, loop=None): # pylint: disable=super-init-not-called
21+
def __init__(self, client, source, offset=None, prefetch=300, epoch=None, loop=None): # pylint: disable=super-init-not-called
2122
"""
2223
Instantiate an async receiver.
2324
@@ -33,25 +34,32 @@ def __init__(self, client, source, prefetch=300, epoch=None, loop=None): # pyli
3334
:param loop: An event loop.
3435
"""
3536
self.loop = loop or asyncio.get_event_loop()
37+
self.client = client
38+
self.source = source
39+
self.offset = offset
40+
self.prefetch = prefetch
41+
self.epoch = epoch
42+
self.retry_policy = errors.ErrorPolicy(max_retries=3, on_error=_error_handler)
3643
self.redirected = None
3744
self.error = None
38-
self.debug = client.debug
39-
self.offset = None
40-
self.prefetch = prefetch
4145
self.properties = None
42-
self.epoch = epoch
46+
source = Source(self.source)
47+
if self.offset is not None:
48+
source.set_filter(self.offset.selector())
4349
if epoch:
4450
self.properties = {types.AMQPSymbol(self._epoch): types.AMQPLong(int(epoch))}
4551
self._handler = ReceiveClientAsync(
4652
source,
47-
auth=client.auth,
48-
debug=self.debug,
53+
auth=self.client.get_auth(),
54+
debug=self.client.debug,
4955
prefetch=self.prefetch,
5056
link_properties=self.properties,
5157
timeout=self.timeout,
58+
error_policy=self.retry_policy,
59+
keep_alive_interval=30,
5260
loop=self.loop)
5361

54-
async def open_async(self, connection):
62+
async def open_async(self):
5563
"""
5664
Open the Receiver using the supplied conneciton.
5765
If the handler has previously been redirected, the redirect
@@ -60,16 +68,54 @@ async def open_async(self, connection):
6068
:param connection: The underlying client shared connection.
6169
:type: connection: ~uamqp._async.connection_async.ConnectionAsync
6270
"""
71+
# pylint: disable=protected-access
6372
if self.redirected:
73+
self.source = self.redirected.address
74+
source = Source(self.source)
75+
if self.offset is not None:
76+
source.set_filter(self.offset.selector())
77+
alt_creds = {
78+
"username": self.client._auth_config.get("iot_username"),
79+
"password":self.client._auth_config.get("iot_password")}
6480
self._handler = ReceiveClientAsync(
65-
self.redirected.address,
66-
auth=None,
67-
debug=self.debug,
81+
source,
82+
auth=self.client.get_auth(**alt_creds),
83+
debug=self.client.debug,
6884
prefetch=self.prefetch,
6985
link_properties=self.properties,
7086
timeout=self.timeout,
87+
error_policy=self.retry_policy,
88+
keep_alive_interval=30,
7189
loop=self.loop)
72-
await self._handler.open_async(connection=connection)
90+
await self._handler.open_async()
91+
while not await self.has_started():
92+
await self._handler._connection.work_async()
93+
94+
async def reconnect_async(self):
95+
"""If the Receiver was disconnected from the service with
96+
a retryable error - attempt to reconnect."""
97+
# pylint: disable=protected-access
98+
alt_creds = {
99+
"username": self.client._auth_config.get("iot_username"),
100+
"password":self.client._auth_config.get("iot_password")}
101+
await self._handler.close_async()
102+
source = Source(self.source)
103+
if self.offset is not None:
104+
source.set_filter(self.offset.selector())
105+
self._handler = ReceiveClientAsync(
106+
source,
107+
auth=self.client.get_auth(**alt_creds),
108+
debug=self.client.debug,
109+
prefetch=self.prefetch,
110+
link_properties=self.properties,
111+
timeout=self.timeout,
112+
error_policy=self.retry_policy,
113+
keep_alive_interval=30,
114+
properties=self.client.create_properties(),
115+
loop=self.loop)
116+
await self._handler.open_async()
117+
while not await self.has_started():
118+
await self._handler._connection.work_async()
73119

74120
async def has_started(self):
75121
"""
@@ -88,7 +134,7 @@ async def has_started(self):
88134
raise EventHubError("Authorization timeout.")
89135
elif auth_in_progress:
90136
return False
91-
elif not await self._handler._client_ready():
137+
elif not await self._handler._client_ready_async():
92138
return False
93139
else:
94140
return True
@@ -109,6 +155,8 @@ async def close_async(self, exception=None):
109155
self.redirected = exception
110156
elif isinstance(exception, EventHubError):
111157
self.error = exception
158+
elif isinstance(exception, (errors.LinkDetach, errors.ConnectionClose)):
159+
self.error = EventHubError(str(exception), exception)
112160
elif exception:
113161
self.error = EventHubError(str(exception))
114162
else:
@@ -129,21 +177,28 @@ async def receive(self, max_batch_size=None, timeout=None):
129177
"""
130178
if self.error:
131179
raise self.error
180+
data_batch = []
132181
try:
133182
timeout_ms = 1000 * timeout if timeout else 0
134183
message_batch = await self._handler.receive_message_batch_async(
135184
max_batch_size=max_batch_size,
136185
timeout=timeout_ms)
137-
data_batch = []
138186
for message in message_batch:
139187
event_data = EventData(message=message)
140188
self.offset = event_data.offset
141189
data_batch.append(event_data)
142190
return data_batch
143-
except errors.LinkDetach as detach:
144-
error = EventHubError(str(detach))
145-
await self.close_async(exception=error)
146-
raise error
191+
except (errors.LinkDetach, errors.ConnectionClose) as shutdown:
192+
if shutdown.action.retry:
193+
await self.reconnect_async()
194+
return data_batch
195+
else:
196+
error = EventHubError(str(shutdown), shutdown)
197+
await self.close_async(exception=error)
198+
raise error
199+
except errors.MessageHandlerError:
200+
await self.reconnect_async()
201+
return data_batch
147202
except Exception as e:
148203
error = EventHubError("Receive failed: {}".format(e))
149204
await self.close_async(exception=error)

0 commit comments

Comments
 (0)