|
51 | 51 | _RESUME_THRESHOLD = 0.8
|
52 | 52 | """The load threshold below which to resume the incoming message stream."""
|
53 | 53 |
|
54 |
| -_DEFAULT_STREAM_ACK_DEADLINE = 60 |
55 |
| -"""The default message acknowledge deadline in seconds for incoming message stream. |
56 |
| -
|
57 |
| -This default deadline is dynamically modified for the messages that are added |
58 |
| -to the lease management. |
59 |
| -""" |
60 |
| - |
61 | 54 |
|
62 | 55 | def _maybe_wrap_exception(exception):
|
63 | 56 | """Wraps a gRPC exception class, if needed."""
|
@@ -135,9 +128,15 @@ def __init__(
|
135 | 128 | # because the FlowControl limits have been hit.
|
136 | 129 | self._messages_on_hold = queue.Queue()
|
137 | 130 |
|
| 131 | + # the total number of bytes consumed by the messages currently on hold |
| 132 | + self._on_hold_bytes = 0 |
| 133 | + |
138 | 134 | # A lock ensuring that pausing / resuming the consumer are both atomic
|
139 | 135 | # operations that cannot be executed concurrently. Needed for properly
|
140 |
| - # syncing these operations with the current leaser load. |
| 136 | + # syncing these operations with the current leaser load. Additionally, |
| 137 | + # the lock is used to protect modifications of internal data that |
| 138 | + # affects the load computation, i.e. the count and size of the messages |
| 139 | + # currently on hold. |
141 | 140 | self._pause_resume_lock = threading.Lock()
|
142 | 141 |
|
143 | 142 | # The threads created in ``.open()``.
|
@@ -218,10 +217,18 @@ def load(self):
|
218 | 217 | if self._leaser is None:
|
219 | 218 | return 0.0
|
220 | 219 |
|
| 220 | + # Messages that are temporarily put on hold are not being delivered to |
| 221 | + # user's callbacks, thus they should not contribute to the flow control |
| 222 | + # load calculation. |
| 223 | + # However, since these messages must still be lease-managed to avoid |
| 224 | + # unnecessary ACK deadline expirations, their count and total size must |
| 225 | + # be subtracted from the leaser's values. |
221 | 226 | return max(
|
222 | 227 | [
|
223 |
| - self._leaser.message_count / self._flow_control.max_messages, |
224 |
| - self._leaser.bytes / self._flow_control.max_bytes, |
| 228 | + (self._leaser.message_count - self._messages_on_hold.qsize()) |
| 229 | + / self._flow_control.max_messages, |
| 230 | + (self._leaser.bytes - self._on_hold_bytes) |
| 231 | + / self._flow_control.max_bytes, |
225 | 232 | ]
|
226 | 233 | )
|
227 | 234 |
|
@@ -292,13 +299,19 @@ def _maybe_release_messages(self):
|
292 | 299 | except queue.Empty:
|
293 | 300 | break
|
294 | 301 |
|
295 |
| - self.leaser.add( |
296 |
| - [requests.LeaseRequest(ack_id=msg.ack_id, byte_size=msg.size)] |
297 |
| - ) |
| 302 | + self._on_hold_bytes -= msg.size |
| 303 | + |
| 304 | + if self._on_hold_bytes < 0: |
| 305 | + _LOGGER.warning( |
| 306 | + "On hold bytes was unexpectedly negative: %s", self._on_hold_bytes |
| 307 | + ) |
| 308 | + self._on_hold_bytes = 0 |
| 309 | + |
298 | 310 | _LOGGER.debug(
|
299 |
| - "Released held message to leaser, scheduling callback for it, " |
300 |
| - "still on hold %s.", |
| 311 | + "Released held message, scheduling callback for it, " |
| 312 | + "still on hold %s (bytes %s).", |
301 | 313 | self._messages_on_hold.qsize(),
|
| 314 | + self._on_hold_bytes, |
302 | 315 | )
|
303 | 316 | self._scheduler.schedule(self._callback, msg)
|
304 | 317 |
|
@@ -392,17 +405,7 @@ def open(self, callback, on_callback_error):
|
392 | 405 | )
|
393 | 406 |
|
394 | 407 | # Create the RPC
|
395 |
| - |
396 |
| - # We must use a fixed value for the ACK deadline, as we cannot read it |
397 |
| - # from the subscription. The latter would require `pubsub.subscriptions.get` |
398 |
| - # permission, which is not granted to the default subscriber role |
399 |
| - # `roles/pubsub.subscriber`. |
400 |
| - # See also https://github.com/googleapis/google-cloud-python/issues/9339 |
401 |
| - # |
402 |
| - # When dynamic lease management is enabled for the "on hold" messages, |
403 |
| - # the default stream ACK deadline should again be set based on the |
404 |
| - # historic ACK timing data, i.e. `self.ack_histogram.percentile(99)`. |
405 |
| - stream_ack_deadline_seconds = _DEFAULT_STREAM_ACK_DEADLINE |
| 408 | + stream_ack_deadline_seconds = self.ack_histogram.percentile(99) |
406 | 409 |
|
407 | 410 | get_initial_request = functools.partial(
|
408 | 411 | self._get_initial_request, stream_ack_deadline_seconds
|
@@ -540,40 +543,46 @@ def _on_response(self, response):
|
540 | 543 | the callback for each message using the executor.
|
541 | 544 | """
|
542 | 545 | _LOGGER.debug(
|
543 |
| - "Processing %s received message(s), currenty on hold %s.", |
| 546 | + "Processing %s received message(s), currenty on hold %s (bytes %s).", |
544 | 547 | len(response.received_messages),
|
545 | 548 | self._messages_on_hold.qsize(),
|
| 549 | + self._on_hold_bytes, |
546 | 550 | )
|
547 | 551 |
|
| 552 | + # Immediately (i.e. without waiting for the auto lease management) |
| 553 | + # modack the messages we received, as this tells the server that we've |
| 554 | + # received them. |
| 555 | + items = [ |
| 556 | + requests.ModAckRequest(message.ack_id, self._ack_histogram.percentile(99)) |
| 557 | + for message in response.received_messages |
| 558 | + ] |
| 559 | + self._dispatcher.modify_ack_deadline(items) |
| 560 | + |
548 | 561 | invoke_callbacks_for = []
|
549 | 562 |
|
550 | 563 | for received_message in response.received_messages:
|
551 | 564 | message = google.cloud.pubsub_v1.subscriber.message.Message(
|
552 | 565 | received_message.message, received_message.ack_id, self._scheduler.queue
|
553 | 566 | )
|
554 |
| - if self.load < _MAX_LOAD: |
555 |
| - req = requests.LeaseRequest( |
556 |
| - ack_id=message.ack_id, byte_size=message.size |
557 |
| - ) |
558 |
| - self.leaser.add([req]) |
559 |
| - invoke_callbacks_for.append(message) |
560 |
| - self.maybe_pause_consumer() |
561 |
| - else: |
562 |
| - self._messages_on_hold.put(message) |
563 |
| - |
564 |
| - # Immediately (i.e. without waiting for the auto lease management) |
565 |
| - # modack the messages we received and not put on hold, as this tells |
566 |
| - # the server that we've received them. |
567 |
| - items = [ |
568 |
| - requests.ModAckRequest(message.ack_id, self._ack_histogram.percentile(99)) |
569 |
| - for message in invoke_callbacks_for |
570 |
| - ] |
571 |
| - self._dispatcher.modify_ack_deadline(items) |
| 567 | + # Making a decision based on the load, and modifying the data that |
| 568 | + # affects the load -> needs a lock, as that state can be modified |
| 569 | + # by different threads. |
| 570 | + with self._pause_resume_lock: |
| 571 | + if self.load < _MAX_LOAD: |
| 572 | + invoke_callbacks_for.append(message) |
| 573 | + else: |
| 574 | + self._messages_on_hold.put(message) |
| 575 | + self._on_hold_bytes += message.size |
| 576 | + |
| 577 | + req = requests.LeaseRequest(ack_id=message.ack_id, byte_size=message.size) |
| 578 | + self.leaser.add([req]) |
| 579 | + self.maybe_pause_consumer() |
572 | 580 |
|
573 | 581 | _LOGGER.debug(
|
574 |
| - "Scheduling callbacks for %s new messages, new total on hold %s.", |
| 582 | + "Scheduling callbacks for %s new messages, new total on hold %s (bytes %s).", |
575 | 583 | len(invoke_callbacks_for),
|
576 | 584 | self._messages_on_hold.qsize(),
|
| 585 | + self._on_hold_bytes, |
577 | 586 | )
|
578 | 587 | for msg in invoke_callbacks_for:
|
579 | 588 | self._scheduler.schedule(self._callback, msg)
|
|
0 commit comments