[BUG] Streaming subscribe returns None after a reconnect, making test_streaming_subscribe_receives_published_message flaky
Maintainers usually reply within 3 days
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 45/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- grpc, python, redis
- Domain
- backend, testing-qa
Research direction
The bug is in dapr/clients/grpc/subscription.py and dapr/aio/clients/grpc/subscription.py. Start by reading the next_message method and the reconnect_stream logic. Run the failing test with pytest tests/integration/test_pubsub.py -k streaming. Examine the fake sidecar in unit tests to simulate reconnects. The fix must ensure next_message retries after a reconnect and that acks are not lost.
Written by the indexing model from the issue text.
Description
Expected Behavior
tests/integration/test_pubsub.py::test_streaming_subscribe_receives_published_message passes on every run, and Subscription.next_message() returns the next message even when the stream reconnects while it waits.
Actual Behavior
The test fails intermittently. It failed in 6 of the 25 most recent run-tests runs before #1229, on main pushes, merge-queue runs and unrelated PRs (for example runs 35790024069 on main, 35790699043 and 35791305700 in the merge queue). Latest occurrence: #1229, validate (3.14), runtime 1.18.4. Re-running the job on the same commit passed.
WARNING dapr.clients.grpc.subscription:subscription.py:97 Subscription stream error (StatusCode.UNKNOWN): EOF — reconnecting
WARNING dapr.clients.grpc.subscription:subscription.py:124 Can't send message on inactive stream: 'NoneType' object has no attribute 'id'
E AttributeError: 'NoneType' object has no attribute 'data'
tests/integration/test_pubsub.py:113: AttributeError
Sidecar log for the same test:
Subscribing to pubsub 'pubsub' topic 'TOPIC_STREAM' ConnectionID 1
Unsubscribed from pubsub 'pubsub' topic 'TOPIC_STREAM'
error retrieving pending Redis messages: context canceled
Subscribing to pubsub 'pubsub' topic 'TOPIC_STREAM' ConnectionID 2
encountered a non-retriable error while publishing a subscribed message to topic TOPIC_STREAM, err: stream closed
Unsubscribed from pubsub 'pubsub' topic 'TOPIC_STREAM'
The first stream ends with EOF right after it connects. The SDK reconnects, and the runtime is about to deliver the message on connection 2. But next_message() has already returned None to the caller, so the test fails and its finally: subscription.close() closes connection 2 in the middle of that delivery.
Likely cause (SDK side)
next_message()returnsNoneafter a reconnect instead of reading from the new stream. Indapr/clients/grpc/subscription.py#L89-L102, theUNAVAILABLE/UNKNOWN/INTERNALbranch callsreconnect_stream()and falls through, so the method returnsNone. The docstring saysNonemeans "no message received within the timeout", but there is no timeout. A caller that doessub.next_message().data(), or iterates withfor msg in sub, getsNoneor crashes on a transient reconnect. The async client does the same indapr/aio/clients/grpc/subscription.py#L69-L84. It also does not treatINTERNALas retryable, unlike the sync client.- Suspected, not yet confirmed: acks can be lost across a reconnect. The request generator of the old stream (
subscription.py#L52-L56) blocks onself._send_queue.get()and checks_is_stream_active()only afterget()returns. Afterstart()makes the subscription active again, the old generator can take the next ack off the shared queue and yield it into the dead stream. That message is then never acked and is redelivered.
Why the first stream gets EOF right after it connects is a separate question. It may be on the runtime side.
Related: project coverage changes between runs of the same code
The same timing shows up in unit-test coverage. codecov/project uses threshold: 0%, so it fails a PR whenever these lines happen not to run. On #1229, which does not touch client.py, coverage dropped by 7 lines. They are the error and reconnect branches of subscribe_with_handler's stream_messages loop, dapr/clients/grpc/client.py#L641-L654. They ran on main at 03eebe1 and did not run on the PR head 2a11bb8.
test_subscribe_topic_with_handler (tests/clients/test_dapr_grpc_client.py#L411-L463) calls close_fn() and returns without waiting for the streaming thread. close_fn does not join it either. Whether the thread reaches the StreamInactiveError / reconnect branches before the test run ends depends on timing. A deterministic fix would make close_fn join the thread with a timeout, or have the test wait on it. A test that drives each branch on purpose would also help.
Steps to Reproduce the Problem
Run the integration suite repeatedly against runtime 1.18.4 with Redis pub/sub, e.g. uv run pytest tests/integration/test_pubsub.py -k streaming --count 50 (with pytest-repeat), or re-run validate in CI. The failure is timing-dependent.
Suggested fix
- After a successful reconnect,
next_message()should read again from the new stream instead of returningNone, with a bound on retries so a sidecar that keeps failing still surfaces an error. Same for the async client, includingINTERNAL. - Give each stream its own send queue, or stop the old generator on
close(), so acks cannot go to a dead stream. - Add unit tests for both clients against the fake sidecar: a stream that fails with
UNKNOWNonce, then delivers a message.next_message()returns that message, and the ack reaches the new stream. - Keep the integration test as is, since it is what caught this.
- Make the
subscribe_with_handlerunit test deterministic, so thatcodecov/projectstops changing between runs of the same code.
Release Note
RELEASE NOTE: FIX Streaming subscriptions return the next message after a transient reconnect instead of None.
- Dominant language
- Python
- Stars
- 273
- Forks
- 155
- Avg merge
- 3d 13h
- Merged PRs (30d)
- 11
Getting set up
Starts the project's dev container in your browser, under your own GitHub account.
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from dapr/python-sdk
-
dapr-ext-workflow good first issue kind/enhancement P2
Difficulty 2/5 1-2 days Newbie friendliness 72/100
dapr/python-sdk#853 · 4 comments ·
Maintainers usually reply within 3 days
-
[WORKFLOW SDK FEATURE REQUEST] Retry WaitForInstanceCompletion/Start on a server-sent CANCELLEDOpendapr-ext-workflow kind/enhancement
Difficulty 4/5 3-5 days Newbie friendliness 55/100
dapr/python-sdk#1246 ·
Maintainers usually reply within 3 days
-
kind/bug
Difficulty 4/5 3-5 days Newbie friendliness 65/100
dapr/python-sdk#1233 ·
Maintainers usually reply within 3 days
-
kind/bug
Difficulty 4/5 3-5 days Newbie friendliness 68/100
dapr/python-sdk#1232 ·
Maintainers usually reply within 3 days
-
dapr-ext-workflow kind/enhancement
Difficulty 3/5 1-2 days Newbie friendliness 78/100
dapr/python-sdk#1213 ·
Maintainers usually reply within 3 days
Similar issues
-
customer-reported
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Azure/azure-cli#34150 · 1 comment ·
Maintainers usually reply within 1 day
-
community-request
Difficulty 1/5 Under an hour Newbie friendliness 95/100
NVIDIA-NeMo/Curator#2464 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
WeblateOrg/translation-finder#1099 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
trezor/trezor-firmware#7997 ·
Maintainers usually reply within 2 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
Maintainers usually reply within 1 day