-
Notifications
You must be signed in to change notification settings - Fork 378
Pitfalls
work in progress
The usage pattern established in current sample code allows for exceptions to be swallowed and go unnoticed. Using async-hub-scenarios/receive_message.py
, for example:
# define behavior for receiving a message
async def message_listener(device_client):
while True:
message = await device_client.receive_message() # blocking call
# NOT SHOWN: code to handle message
# Schedule task for message listener
asyncio.create_task(message_listener(device_client))
Any exceptions that heppen inside the message_listener
coroutine will be silently ignored. This happens because the code that calls create_task
never checks to make sure the returned Task
is still running (by using await
or by calling the done()
, result()
, or exception()
methods on the Task
.) If this happens, it will appear to the user that messages are not being received. In reality, the message_listener()
task is no longer running.
The fix is to either add a try
/except
block around message_listener
which notifies the developer, or to update the code to watch the task for errors.
This problem is made more difficult in samples that use the asyncio.gather
method because one of the tasks being gathered might raise an exception while the others continue to execute. Detecting and recovering from this condition is left as an exercise to the reader.
The cancel
method on tasks that are returned from the async API is not implemented. It will appear to functoin, but no actual functionality will be impacted. This includes implicit cancel operations, such as the one used inside the asyncio.wait_for
method.
As an example:
send_result = asyncio.wait_for(client.send_message(msg, 10))
If client.send_message
does not complete within 10 seconds, this code will raise an asyncio.TimeoutError
but the sending of the message will not be cancelled. The client object will continue to attempt sending this message until the message is successfully sent or the client shuts down.
This problem is improved by the changes in #576, but there is still the possibility of having multiple clients which fight with each other over resources.
For example:
client = IoTHubDeviceClient.create_from_connection_string(conn_str)
client.send_message(msg)
# some time passes
client = IoTHubDeviceClient.create_from_connection_string(conn_str)
client.send_message(msg2)
If you do this, there is a chance that you will have 2 client instances active at one time. This can lead to the 2 instances "fighting" over the network connection. In the example above, the second client will connect, which will cause IoTHub to forcefully close the first client's connection. The first client will set a reconnect timer and attempt to reconnect. When the first client reconnects, IoTHub will force the second client's connection closed, which will cause the second client to set a reconnect timer, and so on.
There are two workarounds:
- Don't ever create two client objects.
- Call the
disconnect
method when you are done using a client. This will cause the client to stop reconnecting.
Before #576 was committed, the disconect
method wasn't sufficient to prevent the client from reconnecting.