Azure / Azure/azure-sdk-for-python
[servicebus] Async ServiceBusSender leaks AttributeError ('NoneType' has no attribute 'client_ready_async') after idle timeout, instead of ServiceBusConnectionError
- Dominant language
- Python
- Stars
- 5.6k
- Forks
- 3.4k
- Avg merge
- 1d 21h
- Merged PRs (30d)
- 193
Description
### Related issues
Same root cause as #35618 and #36334 (internal `_handler` reset to `None`),
but a **different trigger**: those reproduce under concurrent sends and were
closed `not_planned` with an `asyncio.Lock()` workaround. This report is a
**single-threaded, non-concurrent** reproduction driven by Azure's ~10-minute
idle timeout — the lock workaround does not apply.
### Describe the bug
A long-lived (singleton) async `ServiceBusSender` that is reused across sends
works fine until the connection sits idle past Azure's ~10-minute idle timeout.
The next `send_messages()` raises:
AttributeError: 'NoneType' object has no attribute 'client_ready_async'
Expected: a documented, catchable `ServiceBusConnectionError` (or transparent
implicit reconnect), so callers can distinguish "stale link, safe to recreate
and retry" from a genuine error (auth/quota/payload).
Because the leaked exception is a bare `AttributeError`, callers cannot reliably
tell it apart from an ordinary programming bug without string-matching on the
internal attribute name `client_ready_async`, which is fragile across releases.
### Steps to reproduce
1. Create an async `ServiceBusClient`, then one `ServiceBusSender`, and keep both.
2. `await sender.send_messages(...)` once — succeeds.
3. Leave the sender idle for > 10 minutes (Azure idle timeout drops the AMQP link).
4. `await sender.send_messages(...)` again.
Result: `AttributeError: 'NoneType' object has no attribute 'client_ready_async'`
rather than a `ServiceBusConnectionError`.
Minimal sketch:
```python
import asyncio
from azure.servicebus.aio import ServiceBusClient
from azure.servicebus import ServiceBusMessage
async def main():
client = ServiceBusClient.from_connection_string(CONN_STR)
sender = client.get_queue_sender(QUEUE)
await sender.send_messages(ServiceBusMessage("first")) # ok
await asyncio.sleep(11 * 60) # exceed idle timeout
await sender.send_messages(ServiceBusMessage("second")) # AttributeError
asyncio.run(main())
```
Expected behavior
The idle-dropped link should surface as ServiceBusConnectionError (the SDK's
documented connection-loss exception), or the sender should reconnect implicitly.
Either way, no internal AttributeError should escape.
Environment
azure-servicebus: 7.14.3 (latest stable)
azure-core: 1.38.2
Python: 3.11.14
OS: Windows 11 (also observed in Linux containers)
Transport: async (azure.servicebus.aio), pure-Python AMQP
Impact / current workaround
We run one persistent sender per worker and must detect this failure to recreate
the sender and retry. Since no typed exception is raised, we string-match:
isinstance(exc, AttributeError) and "client_ready_async" in str(exc)
This is brittle — it depends on an internal attribute name. A typed exception
(or implicit reconnect) would let us delete the string match. Please consider
fixing the idle-timeout path specifically, independently of the concurrency
scenarios in
[Azure/azure-sdk-for-python#35618](https://github.com/Azure/azure-sdk-for-python/issues/35618) /
[Azure/azure-sdk-for-python#36334](https://github.com/Azure/azure-sdk-for-python/issues/36334).
Contributor guide
Research direction
Start at the async ServiceBusSender send_messages path and trace how an idle-disconnected link leaves the internal _handler as None. Add a regression test for a single-threaded send after the idle-timeout condition, then verify that it raises ServiceBusConnectionError rather than AttributeError or reconnects transparently.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- azure, python
- Domain
- cloud, distributed-systems
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100