|
15 | 15 | from __future__ import absolute_import |
16 | 16 |
|
17 | 17 | import datetime |
| 18 | +import itertools |
18 | 19 | import threading |
19 | 20 | import time |
20 | 21 |
|
|
24 | 25 |
|
25 | 26 | import google.auth |
26 | 27 | from google.cloud import pubsub_v1 |
| 28 | +from google.cloud.pubsub_v1 import types |
27 | 29 |
|
28 | 30 |
|
29 | 31 | from test_utils.system import unique_resource_id |
@@ -206,6 +208,80 @@ class CallbackError(Exception): |
206 | 208 | with pytest.raises(CallbackError): |
207 | 209 | future.result(timeout=30) |
208 | 210 |
|
| 211 | + def test_streaming_pull_max_messages( |
| 212 | + self, publisher, topic_path, subscriber, subscription_path, cleanup |
| 213 | + ): |
| 214 | + # Make sure the topic and subscription get deleted. |
| 215 | + cleanup.append((publisher.delete_topic, topic_path)) |
| 216 | + cleanup.append((subscriber.delete_subscription, subscription_path)) |
| 217 | + |
| 218 | + # create a topic and subscribe to it |
| 219 | + publisher.create_topic(topic_path) |
| 220 | + subscriber.create_subscription(subscription_path, topic_path) |
| 221 | + |
| 222 | + batch_sizes = (7, 4, 8, 2, 10, 1, 3, 8, 6, 1) # total: 50 |
| 223 | + self._publish_messages(publisher, topic_path, batch_sizes=batch_sizes) |
| 224 | + |
| 225 | + # now subscribe and do the main part, check for max pending messages |
| 226 | + total_messages = sum(batch_sizes) |
| 227 | + flow_control = types.FlowControl(max_messages=5) |
| 228 | + callback = StreamingPullCallback(processing_time=1) |
| 229 | + |
| 230 | + future = subscriber.subscribe( |
| 231 | + subscription_path, callback, flow_control=flow_control |
| 232 | + ) |
| 233 | + |
| 234 | + # Expected time to process all messages in ideal case: |
| 235 | + # total_messages / FlowControl.max_messages * processing_time |
| 236 | + # |
| 237 | + # With total=50, max messages=5, and processing_time=1 this amounts to |
| 238 | + # 10 seconds (+ overhead), thus a full minute should be more than enough |
| 239 | + # for the processing to complete. If not, fail the test with a timeout. |
| 240 | + for _ in six.moves.range(60): |
| 241 | + time.sleep(1) |
| 242 | + if callback.completed_calls >= total_messages: |
| 243 | + break |
| 244 | + else: |
| 245 | + pytest.fail( |
| 246 | + "Timeout: receiving/processing streamed messages took too long." |
| 247 | + ) |
| 248 | + |
| 249 | + try: |
| 250 | + # All messages should have been processed exactly once, and no more |
| 251 | + # than max_messages simultaneously at any time. |
| 252 | + assert callback.completed_calls == total_messages |
| 253 | + assert sorted(callback.seen_message_ids) == list( |
| 254 | + range(1, total_messages + 1) |
| 255 | + ) |
| 256 | + assert callback.max_pending_ack <= flow_control.max_messages |
| 257 | + finally: |
| 258 | + future.cancel() |
| 259 | + |
| 260 | + def _publish_messages(self, publisher, topic_path, batch_sizes): |
| 261 | + """Publish ``count`` messages in batches and wait until completion.""" |
| 262 | + publish_futures = [] |
| 263 | + msg_counter = itertools.count(start=1) |
| 264 | + |
| 265 | + for batch_size in batch_sizes: |
| 266 | + msg_batch = self._make_messages(count=batch_size) |
| 267 | + for msg in msg_batch: |
| 268 | + future = publisher.publish( |
| 269 | + topic_path, msg, seq_num=str(next(msg_counter)) |
| 270 | + ) |
| 271 | + publish_futures.append(future) |
| 272 | + time.sleep(0.1) |
| 273 | + |
| 274 | + # wait untill all messages have been successfully published |
| 275 | + for future in publish_futures: |
| 276 | + future.result(timeout=30) |
| 277 | + |
| 278 | + def _make_messages(self, count): |
| 279 | + messages = [ |
| 280 | + u"message {}/{}".format(i, count).encode("utf-8") |
| 281 | + for i in range(1, count + 1) |
| 282 | + ] |
| 283 | + return messages |
| 284 | + |
209 | 285 |
|
210 | 286 | class AckCallback(object): |
211 | 287 | def __init__(self): |
@@ -236,3 +312,26 @@ def __call__(self, message): |
236 | 312 | # ``calls`` is incremented to do it. |
237 | 313 | self.call_times.append(now) |
238 | 314 | self.calls += 1 |
| 315 | + |
| 316 | + |
| 317 | +class StreamingPullCallback(object): |
| 318 | + def __init__(self, processing_time): |
| 319 | + self._lock = threading.Lock() |
| 320 | + self._processing_time = processing_time |
| 321 | + self._pending_ack = 0 |
| 322 | + self.max_pending_ack = 0 |
| 323 | + self.completed_calls = 0 |
| 324 | + self.seen_message_ids = [] |
| 325 | + |
| 326 | + def __call__(self, message): |
| 327 | + with self._lock: |
| 328 | + self._pending_ack += 1 |
| 329 | + self.max_pending_ack = max(self.max_pending_ack, self._pending_ack) |
| 330 | + self.seen_message_ids.append(int(message.attributes["seq_num"])) |
| 331 | + |
| 332 | + time.sleep(self._processing_time) |
| 333 | + |
| 334 | + with self._lock: |
| 335 | + self._pending_ack -= 1 |
| 336 | + message.ack() |
| 337 | + self.completed_calls += 1 |
0 commit comments