Skip to content

fix(pubsub): nack buffered messages when the stream closes - #7413

Open
shanielh wants to merge 4 commits into
googleapis:mainfrom
shanielh:fix/pubsub-nack-buffered-on-close
Open

shanielh wants to merge 4 commits into
googleapis:mainfrom
shanielh:fix/pubsub-nack-buffered-on-close

Conversation

@shanielh

@shanielh shanielh commented Nov 8, 2026

Copy link
Copy Markdown

When a MessageStream shuts down or is dropped, it drops the gRPC stream. Any responses the server already sent but the client had not read yet are discarded with it. Their messages never reach lease management, so they are neither leased nor nacked, and the service only redelivers them once their ack deadline expires. With the default 60 second deadline, a message published around shutdown can stall for a minute. We observed this in an application's end-to-end tests: a message published while a subscriber shut down was never yielded by MessageStream::next() in any process, and was redelivered exactly one ack deadline later.

With this change, before the stream is dropped (on shutdown, on a permanent error, or when the MessageStream is dropped), the client reads the responses already buffered on the stream, without waiting on the network, and nacks their messages with ModifyAckDeadline(0). The drain stops at the first response that is not immediately available, at the end of the stream, at an error, or after 100 responses, so it never blocks. The nacks go directly through the stub, because lease management never knew about these messages and may already have shut down. The nack RPCs run in a spawned task, so ShutdownToken::shutdown() does not wait for them. This applies to both ShutdownBehavior values.

How this compares to other Pub/Sub clients: Go and Java do not drain. Go cancels the pull streams first on shutdown and nacks only the messages it has already read (nackInventory). Java cancels the call in StreamingSubscriberConnection.doStop() with closeSendWithError(CANCELLED). Both lose already-buffered responses the same way this crate did. Rust does drain because the cost is small and bounded: it only reads data the client already holds, it never waits, and it turns a full ack-deadline stall into an immediate redelivery for those messages.

Limits: messages that are still in flight on the network when the stream closes are not covered, and are still redelivered after their ack deadline. If the application signals shutdown but neither calls next() again nor drops the MessageStream, the drain happens only when it does one of those.

Tests:

  • stream.rs: drain_buffered returns buffered responses and stops when the next one is pending, at the response limit, at the end of the stream, and at an error.
  • message_stream.rs (against the mock gRPC server): a response buffered at shutdown is nacked, not dropped, under NackImmediately and WaitForProcessing, both through next() after shutdown and through dropping the stream. These tests time out without the fix. They wait 200 ms of real time for the responses to reach the client's buffers.

Fixes #7412

🤖 Generated with Claude Code

@shanielh
shanielh requested a review from a team as a code owner November 8, 2026 12:53
@product-auto-label product-auto-label Bot added the api: pubsub Issues related to the Pub/Sub API. label Nov 8, 2026
@google-cla

google-cla Bot commented Nov 8, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a mechanism to drain and nack buffered responses when a message stream is closed or dropped, preventing messages already sent by the server but not yet read by the application from being delayed until their ack deadline expires. This is achieved by introducing a drain_buffered method on the underlying stream and calling it within drain_and_nack during stream closure and drop. Feedback on the changes suggests optimizing the drain_and_nack task to avoid unnecessary string clones when batching ack_ids by draining the vector directly instead of using chunks() and to_vec().

@dbolduc

dbolduc commented Nov 9, 2026

Copy link
Copy Markdown
Member

/gcbrun

@shanielh
shanielh force-pushed the fix/pubsub-nack-buffered-on-close branch from e7bab4b to 072b276 Compare November 9, 2026 06:07
When a `MessageStream` shuts down or is dropped, it drops the gRPC
stream. Any responses the server already sent but the client had not
read yet were discarded with it. Their messages never reached lease
management, so they were neither leased nor nacked, and the service
only redelivered them once their ack deadline expired. With the default
60 second deadline, a message published around shutdown could stall
for a minute.

Now, before the stream is dropped (on shutdown, on a permanent error,
or when the `MessageStream` is dropped), the client reads the responses
already buffered on the stream, without waiting on the network, and
nacks their messages with `ModifyAckDeadline(0)`. The drain stops at
the first response that is not immediately available, at the end of the
stream, at an error, or after 100 responses. The nacks go directly
through the stub, because lease management never knew about these
messages and may already have shut down. This applies to both
`ShutdownBehavior` values.

Messages that are still in flight on the network when the stream closes
are not covered. The service still redelivers them after their ack
deadline.

Fixes googleapis#7412
Move the ack IDs into each nack request instead of copying them out of
`chunks()`.
@shanielh
shanielh force-pushed the fix/pubsub-nack-buffered-on-close branch from 072b276 to 4b8d9ec Compare November 9, 2026 06:09
@shanielh

shanielh commented Nov 9, 2026

Copy link
Copy Markdown
Author

@dbolduc Sorry, I re-pushed with my own email. probably another /gcbrun needed 😄

@dbolduc

dbolduc commented Nov 9, 2026

Copy link
Copy Markdown
Member

/gcbrun

@codecov

codecov Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.52830% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 98.65%. Comparing base (e0e7bfa) to head (8f674f5).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
src/pubsub/src/subscriber/message_stream.rs 99.33% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7413      +/-   ##
==========================================
- Coverage   98.65%   98.65%   -0.01%     
==========================================
  Files         334      334              
  Lines      145392   145604     +212     
==========================================
+ Hits       143433   143642     +209     
- Misses       1959     1962       +3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Move the nack loop for drained messages into `nack_ack_ids()` and test
that it splits the ack IDs into batches of at most 1000. Also test that
dropping a `MessageStream` outside of a Tokio runtime does not panic.
@shanielh

shanielh commented Nov 9, 2026

Copy link
Copy Markdown
Author

@dbolduc Added the missing coverage, note that the Google Cloud Build / gcb-pr-protojson-conformance (rust-sdk-testing) failed, which I'm not sure why.

@dbolduc

dbolduc commented Nov 9, 2026

Copy link
Copy Markdown
Member

/gcbrun

@dbolduc

dbolduc commented Nov 9, 2026 •

Copy link
Copy Markdown
Member

@dbolduc Added the missing coverage, note that the Google Cloud Build / gcb-pr-protojson-conformance (rust-sdk-testing) failed, which I'm not sure why.

I think GCB was having networking issues GitHub Releases was down, so the failures are unrelated to the code.

@shanielh

shanielh commented Nov 9, 2026 •

Copy link
Copy Markdown
Author

@dbolduc Yay! All builds are passing now 🎉

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api: pubsub Issues related to the Pub/Sub API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pubsub: messages buffered on the stream are lost until their ack deadline when a MessageStream closes

2 participants