Skip to content

Commit d4c1d2e

Browse files
committed
Add system test for PubSub max_messages setting
1 parent a9a329a commit d4c1d2e

1 file changed

Lines changed: 101 additions & 0 deletions

File tree

pubsub/tests/system.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from __future__ import absolute_import
1616

1717
import datetime
18+
import itertools
1819
import threading
1920
import time
2021

@@ -23,6 +24,7 @@
2324

2425
import google.auth
2526
from google.cloud import pubsub_v1
27+
from google.cloud.pubsub_v1 import types
2628

2729

2830
from test_utils.system import unique_resource_id
@@ -178,6 +180,82 @@ def test_subscribe_to_messages_async_callbacks(
178180
future.cancel()
179181

180182

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+
181259
class AckCallback(object):
182260
def __init__(self):
183261
self.calls = 0
@@ -207,3 +285,26 @@ def __call__(self, message):
207285
# ``calls`` is incremented to do it.
208286
self.call_times.append(now)
209287
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

Comments
 (0)