Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 1.8k
fix(pubsub): fix messages delivered multiple times despite a long ACK deadline#9525
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Uh oh!
There was an error while loading. Please reload this page.
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
de45c3e
fix(pubsub): lease-manage all received messages
plamut b65873b
Exclude on hold messages from load calculation
plamut a0cf284
Add warning if internal bytes count is negative
plamut 79adfa4
Use histogram to set default stream ACK deadline
plamut File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
101 changes: 55 additions & 46 deletions
101 pubsub/google/cloud/pubsub_v1/subscriber/_protocol/streaming_pull_manager.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -51,13 +51,6 @@ | ||
| _RESUME_THRESHOLD = 0.8 | ||
| """The load threshold below which to resume the incoming message stream.""" | ||
| _DEFAULT_STREAM_ACK_DEADLINE = 60 | ||
| """The default message acknowledge deadline in seconds for incoming message stream. | ||
| This default deadline is dynamically modified for the messages that are added | ||
| to the lease management. | ||
| """ | ||
| def _maybe_wrap_exception(exception): | ||
| """Wraps a gRPC exception class, if needed.""" | ||
| @@ -135,9 +128,15 @@ def __init__( | ||
| # because the FlowControl limits have been hit. | ||
| self._messages_on_hold = queue.Queue() | ||
| # the total number of bytes consumed by the messages currently on hold | ||
| self._on_hold_bytes = 0 | ||
| # A lock ensuring that pausing / resuming the consumer are both atomic | ||
| # operations that cannot be executed concurrently. Needed for properly | ||
| # syncing these operations with the current leaser load. | ||
| # syncing these operations with the current leaser load. Additionally, | ||
| # the lock is used to protect modifications of internal data that | ||
| # affects the load computation, i.e. the count and size of the messages | ||
| # currently on hold. | ||
| self._pause_resume_lock = threading.Lock() | ||
| # The threads created in ``.open()``. | ||
| @@ -218,10 +217,18 @@ def load(self): | ||
| if self._leaser is None: | ||
| return 0.0 | ||
| # Messages that are temporarily put on hold are not being delivered to | ||
| # user's callbacks, thus they should not contribute to the flow control | ||
| # load calculation. | ||
| # However, since these messages must still be lease-managed to avoid | ||
| # unnecessary ACK deadline expirations, their count and total size must | ||
| # be subtracted from the leaser's values. | ||
| return max( | ||
| [ | ||
| self._leaser.message_count / self._flow_control.max_messages, | ||
| self._leaser.bytes / self._flow_control.max_bytes, | ||
| (self._leaser.message_count - self._messages_on_hold.qsize()) | ||
| / self._flow_control.max_messages, | ||
| (self._leaser.bytes - self._on_hold_bytes) | ||
| / self._flow_control.max_bytes, | ||
| ] | ||
| ) | ||
| @@ -292,13 +299,19 @@ def _maybe_release_messages(self): | ||
| except queue.Empty: | ||
| break | ||
| self.leaser.add( | ||
| [requests.LeaseRequest(ack_id=msg.ack_id, byte_size=msg.size)] | ||
| ) | ||
| self._on_hold_bytes -= msg.size | ||
| if self._on_hold_bytes < 0: | ||
| _LOGGER.warning( | ||
| "On hold bytes was unexpectedly negative: %s", self._on_hold_bytes | ||
| ) | ||
| self._on_hold_bytes = 0 | ||
| _LOGGER.debug( | ||
| "Released held message to leaser, scheduling callback for it, " | ||
| "still on hold %s.", | ||
| "Released held message, scheduling callback for it, " | ||
| "still on hold %s (bytes %s).", | ||
| self._messages_on_hold.qsize(), | ||
| self._on_hold_bytes, | ||
| ) | ||
| self._scheduler.schedule(self._callback, msg) | ||
| @@ -392,17 +405,7 @@ def open(self, callback, on_callback_error): | ||
| ) | ||
| # Create the RPC | ||
| # We must use a fixed value for the ACK deadline, as we cannot read it | ||
| # from the subscription. The latter would require `pubsub.subscriptions.get` | ||
| # permission, which is not granted to the default subscriber role | ||
| # `roles/pubsub.subscriber`. | ||
| # See also https://github.com/googleapis/google-cloud-python/issues/9339 | ||
| # | ||
| # When dynamic lease management is enabled for the "on hold" messages, | ||
| # the default stream ACK deadline should again be set based on the | ||
| # historic ACK timing data, i.e. `self.ack_histogram.percentile(99)`. | ||
| stream_ack_deadline_seconds = _DEFAULT_STREAM_ACK_DEADLINE | ||
| stream_ack_deadline_seconds = self.ack_histogram.percentile(99) | ||
pradn marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| get_initial_request = functools.partial( | ||
| self._get_initial_request, stream_ack_deadline_seconds | ||
| @@ -540,40 +543,46 @@ def _on_response(self, response): | ||
| the callback for each message using the executor. | ||
| """ | ||
| _LOGGER.debug( | ||
| "Processing %s received message(s), currenty on hold %s.", | ||
| "Processing %s received message(s), currenty on hold %s (bytes %s).", | ||
| len(response.received_messages), | ||
| self._messages_on_hold.qsize(), | ||
| self._on_hold_bytes, | ||
| ) | ||
| # Immediately (i.e. without waiting for the auto lease management) | ||
| # modack the messages we received, as this tells the server that we've | ||
| # received them. | ||
| items = [ | ||
| requests.ModAckRequest(message.ack_id, self._ack_histogram.percentile(99)) | ||
| for message in response.received_messages | ||
| ] | ||
| self._dispatcher.modify_ack_deadline(items) | ||
| invoke_callbacks_for = [] | ||
| for received_message in response.received_messages: | ||
| message = google.cloud.pubsub_v1.subscriber.message.Message( | ||
| received_message.message, received_message.ack_id, self._scheduler.queue | ||
| ) | ||
| if self.load < _MAX_LOAD: | ||
| req = requests.LeaseRequest( | ||
| ack_id=message.ack_id, byte_size=message.size | ||
| ) | ||
| self.leaser.add([req]) | ||
| invoke_callbacks_for.append(message) | ||
| self.maybe_pause_consumer() | ||
| else: | ||
| self._messages_on_hold.put(message) | ||
| # Immediately (i.e. without waiting for the auto lease management) | ||
| # modack the messages we received and not put on hold, as this tells | ||
| # the server that we've received them. | ||
| items = [ | ||
| requests.ModAckRequest(message.ack_id, self._ack_histogram.percentile(99)) | ||
| for message in invoke_callbacks_for | ||
| ] | ||
| self._dispatcher.modify_ack_deadline(items) | ||
| # Making a decision based on the load, and modifying the data that | ||
| # affects the load -> needs a lock, as that state can be modified | ||
| # by different threads. | ||
| with self._pause_resume_lock: | ||
| if self.load < _MAX_LOAD: | ||
| invoke_callbacks_for.append(message) | ||
| else: | ||
| self._messages_on_hold.put(message) | ||
| self._on_hold_bytes += message.size | ||
| req = requests.LeaseRequest(ack_id=message.ack_id, byte_size=message.size) | ||
| self.leaser.add([req]) | ||
| self.maybe_pause_consumer() | ||
| _LOGGER.debug( | ||
| "Scheduling callbacks for %s new messages, new total on hold %s.", | ||
| "Scheduling callbacks for %s new messages, new total on hold %s (bytes %s).", | ||
| len(invoke_callbacks_for), | ||
| self._messages_on_hold.qsize(), | ||
| self._on_hold_bytes, | ||
| ) | ||
| for msg in invoke_callbacks_for: | ||
| self._scheduler.schedule(self._callback, msg) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
75 changes: 51 additions & 24 deletions
75 pubsub/tests/unit/pubsub_v1/subscriber/test_streaming_pull_manager.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.