|
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 |
|
|
23 | 24 |
|
24 | 25 | import google.auth |
25 | 26 | from google.cloud import pubsub_v1 |
| 27 | +from google.cloud.pubsub_v1 import types |
26 | 28 |
|
27 | 29 |
|
28 | 30 | from test_utils.system import unique_resource_id |
@@ -178,6 +180,82 @@ def test_subscribe_to_messages_async_callbacks( |
178 | 180 | future.cancel() |
179 | 181 |
|
180 | 182 |
|
| 183 | +class TestStreamingPull(object): |
| 184 | + def test_streaming_pull_max_messages( |
| 185 | + self, publisher, topic_path, subscriber, subscription_path, cleanup |
| 186 | + ): |
| 187 | + # Make sure the topic and subscription get deleted. |
| 188 | + cleanup.append((publisher.delete_topic, topic_path)) |
| 189 | + cleanup.append((subscriber.delete_subscription, subscription_path)) |
| 190 | + |
| 191 | + # create a topic and subscribe to it |
| 192 | + publisher.create_topic(topic_path) |
| 193 | + subscriber.create_subscription(subscription_path, topic_path) |
| 194 | + |
| 195 | + batch_sizes = (7, 4, 8, 2, 10, 1, 3, 8, 6, 1) # total: 50 |
| 196 | + self._publish_messages(publisher, topic_path, batch_sizes=batch_sizes) |
| 197 | + |
| 198 | + # now subscribe and do the main part, check for max pending messages |
| 199 | + total_messages = sum(batch_sizes) |
| 200 | + flow_control = types.FlowControl(max_messages=5) |
| 201 | + callback = StreamingPullCallback(processing_time=1) |
| 202 | + |
| 203 | + future = subscriber.subscribe( |
| 204 | + subscription_path, callback, flow_control=flow_control |
| 205 | + ) |
| 206 | + |
| 207 | + # Expected time to process all messages in ideal case: |
| 208 | + # total_messages / FlowControl.max_messages * processing_time |
| 209 | + # |
| 210 | + # With total=50, max messages=5, and processing_time=1 this amounts to |
| 211 | + # 10 seconds (+ overhead), thus a full minute should be more than enough |
| 212 | + # for the processing to complete. If not, fail the test with a timeout. |
| 213 | + for _ in six.moves.range(60): |
| 214 | + time.sleep(1) |
| 215 | + if callback.completed_calls >= total_messages: |
| 216 | + break |
| 217 | + else: |
| 218 | + pytest.fail( |
| 219 | + "Timeout: receiving/processing streamed messages took too long." |
| 220 | + ) |
| 221 | + |
| 222 | + try: |
| 223 | + # All messages should have been processed exactly once, and no more |
| 224 | + # than max_messages simultaneously at any time. |
| 225 | + assert callback.completed_calls == total_messages |
| 226 | + assert sorted(callback.seen_message_ids) == list( |
| 227 | + range(1, total_messages + 1) |
| 228 | + ) |
| 229 | + assert callback.max_pending_ack <= flow_control.max_messages |
| 230 | + finally: |
| 231 | + future.cancel() |
| 232 | + |
| 233 | + def _publish_messages(self, publisher, topic_path, batch_sizes): |
| 234 | + """Publish ``count`` messages in batches and wait until completion.""" |
| 235 | + publish_futures = [] |
| 236 | + msg_counter = itertools.count(start=1) |
| 237 | + |
| 238 | + for batch_size in batch_sizes: |
| 239 | + msg_batch = self._make_messages(count=batch_size) |
| 240 | + for msg in msg_batch: |
| 241 | + future = publisher.publish( |
| 242 | + topic_path, msg, seq_num=str(next(msg_counter)) |
| 243 | + ) |
| 244 | + publish_futures.append(future) |
| 245 | + time.sleep(0.1) |
| 246 | + |
| 247 | + # wait untill all messages have been successfully published |
| 248 | + for future in publish_futures: |
| 249 | + future.result(timeout=30) |
| 250 | + |
| 251 | + def _make_messages(self, count): |
| 252 | + messages = [ |
| 253 | + u"message {}/{}".format(i, count).encode("utf-8") |
| 254 | + for i in range(1, count + 1) |
| 255 | + ] |
| 256 | + return messages |
| 257 | + |
| 258 | + |
181 | 259 | class AckCallback(object): |
182 | 260 | def __init__(self): |
183 | 261 | self.calls = 0 |
@@ -207,3 +285,26 @@ def __call__(self, message): |
207 | 285 | # ``calls`` is incremented to do it. |
208 | 286 | self.call_times.append(now) |
209 | 287 | self.calls += 1 |
| 288 | + |
| 289 | + |
| 290 | +class StreamingPullCallback(object): |
| 291 | + def __init__(self, processing_time): |
| 292 | + self._lock = threading.Lock() |
| 293 | + self._processing_time = processing_time |
| 294 | + self._pending_ack = 0 |
| 295 | + self.max_pending_ack = 0 |
| 296 | + self.completed_calls = 0 |
| 297 | + self.seen_message_ids = [] |
| 298 | + |
| 299 | + def __call__(self, message): |
| 300 | + with self._lock: |
| 301 | + self._pending_ack += 1 |
| 302 | + self.max_pending_ack = max(self.max_pending_ack, self._pending_ack) |
| 303 | + self.seen_message_ids.append(int(message.attributes["seq_num"])) |
| 304 | + |
| 305 | + time.sleep(self._processing_time) |
| 306 | + |
| 307 | + with self._lock: |
| 308 | + self._pending_ack -= 1 |
| 309 | + message.ack() |
| 310 | + self.completed_calls += 1 |
0 commit comments