sentry-arroyo 2.19.4__py3-none-any.whl → 2.19.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- arroyo/backends/kafka/commit.py +1 -1
- arroyo/backends/kafka/consumer.py +16 -6
- arroyo/dlq.py +5 -5
- arroyo/processing/processor.py +15 -5
- arroyo/utils/clock.py +1 -1
- {sentry_arroyo-2.19.4.dist-info → sentry_arroyo-2.19.6.dist-info}/METADATA +1 -1
- {sentry_arroyo-2.19.4.dist-info → sentry_arroyo-2.19.6.dist-info}/RECORD +16 -16
- tests/backends/mixins.py +84 -15
- tests/backends/test_kafka.py +37 -6
- tests/backends/test_local.py +2 -2
- tests/processing/strategies/test_produce.py +2 -2
- tests/processing/test_processor.py +13 -3
- tests/utils/test_retries.py +6 -6
- {sentry_arroyo-2.19.4.dist-info → sentry_arroyo-2.19.6.dist-info}/LICENSE +0 -0
- {sentry_arroyo-2.19.4.dist-info → sentry_arroyo-2.19.6.dist-info}/WHEEL +0 -0
- {sentry_arroyo-2.19.4.dist-info → sentry_arroyo-2.19.6.dist-info}/top_level.txt +0 -0
arroyo/backends/kafka/commit.py
CHANGED
|
@@ -97,6 +97,6 @@ class CommitCodec(Codec[KafkaPayload, Commit]):
|
|
|
97
97
|
|
|
98
98
|
if max_times_to_log_legacy_message > 0:
|
|
99
99
|
max_times_to_log_legacy_message -= 1
|
|
100
|
-
logger.
|
|
100
|
+
logger.warning("Legacy commit message found: %s", commit)
|
|
101
101
|
|
|
102
102
|
return commit
|
|
@@ -161,6 +161,10 @@ class KafkaConsumer(Consumer[KafkaPayload]):
|
|
|
161
161
|
)
|
|
162
162
|
|
|
163
163
|
configuration = dict(configuration)
|
|
164
|
+
self.__is_incremental = (
|
|
165
|
+
configuration.get("partition.assignment.strategy") == "cooperative-sticky"
|
|
166
|
+
or configuration.get("group.protocol") == "consumer"
|
|
167
|
+
)
|
|
164
168
|
auto_offset_reset = configuration.get("auto.offset.reset", "largest")
|
|
165
169
|
|
|
166
170
|
# This is a special flag that controls the auto offset behavior for
|
|
@@ -269,6 +273,10 @@ class KafkaConsumer(Consumer[KafkaPayload]):
|
|
|
269
273
|
def assignment_callback(
|
|
270
274
|
consumer: ConfluentConsumer, partitions: Sequence[ConfluentTopicPartition]
|
|
271
275
|
) -> None:
|
|
276
|
+
if not partitions:
|
|
277
|
+
logger.info("skipping empty assignment")
|
|
278
|
+
return
|
|
279
|
+
|
|
272
280
|
self.__state = KafkaConsumerState.ASSIGNING
|
|
273
281
|
|
|
274
282
|
try:
|
|
@@ -451,12 +459,14 @@ class KafkaConsumer(Consumer[KafkaPayload]):
|
|
|
451
459
|
|
|
452
460
|
def __assign(self, offsets: Mapping[Partition, int]) -> None:
|
|
453
461
|
self.__validate_offsets(offsets)
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
462
|
+
partitions = [
|
|
463
|
+
ConfluentTopicPartition(partition.topic.name, partition.index, offset)
|
|
464
|
+
for partition, offset in offsets.items()
|
|
465
|
+
]
|
|
466
|
+
if self.__is_incremental:
|
|
467
|
+
self.__consumer.incremental_assign(partitions)
|
|
468
|
+
else:
|
|
469
|
+
self.__consumer.assign(partitions)
|
|
460
470
|
self.__offsets.update(offsets)
|
|
461
471
|
|
|
462
472
|
def seek(self, offsets: Mapping[Partition, int]) -> None:
|
arroyo/dlq.py
CHANGED
|
@@ -315,11 +315,11 @@ class BufferedMessages(Generic[TStrategyPayload]):
|
|
|
315
315
|
|
|
316
316
|
return None
|
|
317
317
|
|
|
318
|
-
def
|
|
318
|
+
def remove(self, partition: Partition) -> None:
|
|
319
319
|
"""
|
|
320
|
-
|
|
320
|
+
Remove a revoked partition from the buffer.
|
|
321
321
|
"""
|
|
322
|
-
self.__buffered_messages
|
|
322
|
+
self.__buffered_messages.pop(partition, None)
|
|
323
323
|
|
|
324
324
|
|
|
325
325
|
class DlqPolicyWrapper(Generic[TStrategyPayload]):
|
|
@@ -343,9 +343,9 @@ class DlqPolicyWrapper(Generic[TStrategyPayload]):
|
|
|
343
343
|
]
|
|
344
344
|
],
|
|
345
345
|
] = defaultdict(deque)
|
|
346
|
-
self.
|
|
346
|
+
self.reset_dlq_limits({})
|
|
347
347
|
|
|
348
|
-
def
|
|
348
|
+
def reset_dlq_limits(self, assignment: Mapping[Partition, int]) -> None:
|
|
349
349
|
"""
|
|
350
350
|
Called on consumer assignment
|
|
351
351
|
"""
|
arroyo/processing/processor.py
CHANGED
|
@@ -238,16 +238,23 @@ class StreamProcessor(Generic[TStrategyPayload]):
|
|
|
238
238
|
"arroyo.consumer.partitions_assigned.count", len(partitions)
|
|
239
239
|
)
|
|
240
240
|
|
|
241
|
-
self.
|
|
241
|
+
current_partitions = dict(self.__consumer.tell())
|
|
242
|
+
current_partitions.update(partitions)
|
|
243
|
+
|
|
242
244
|
if self.__dlq_policy:
|
|
243
|
-
self.__dlq_policy.
|
|
244
|
-
if
|
|
245
|
+
self.__dlq_policy.reset_dlq_limits(current_partitions)
|
|
246
|
+
if current_partitions:
|
|
245
247
|
if self.__processing_strategy is not None:
|
|
246
|
-
|
|
248
|
+
# TODO: for cooperative-sticky rebalancing this can happen
|
|
249
|
+
# quite often. we should port the changes to
|
|
250
|
+
# ProcessingStrategyFactory that we made in Rust: Remove
|
|
251
|
+
# create_with_partitions, replace with create +
|
|
252
|
+
# update_partitions
|
|
253
|
+
logger.warning(
|
|
247
254
|
"Partition assignment while processing strategy active"
|
|
248
255
|
)
|
|
249
256
|
_close_strategy()
|
|
250
|
-
_create_strategy(
|
|
257
|
+
_create_strategy(current_partitions)
|
|
251
258
|
|
|
252
259
|
@_rdkafka_callback(metrics=self.__metrics_buffer)
|
|
253
260
|
def on_partitions_revoked(partitions: Sequence[Partition]) -> None:
|
|
@@ -278,6 +285,9 @@ class StreamProcessor(Generic[TStrategyPayload]):
|
|
|
278
285
|
except RuntimeError:
|
|
279
286
|
pass
|
|
280
287
|
|
|
288
|
+
for partition in partitions:
|
|
289
|
+
self.__buffered_messages.remove(partition)
|
|
290
|
+
|
|
281
291
|
# Partition revocation can happen anytime during the consumer lifecycle and happen
|
|
282
292
|
# multiple times. What we want to know is that the consumer is not stuck somewhere.
|
|
283
293
|
# The presence of this message as the last message of a consumer
|
arroyo/utils/clock.py
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
arroyo/__init__.py,sha256=fcpHZd2P3MxWl6PJJ8n__fM_NRIfiUE8tKN-orv6lb0,187
|
|
2
2
|
arroyo/commit.py,sha256=oFihWUW8fLsjomWh0o085qIHe9vwVNgoOJC6JQdFM7M,2235
|
|
3
|
-
arroyo/dlq.py,sha256=
|
|
3
|
+
arroyo/dlq.py,sha256=LkWbgI6jQBaiGw8mPzfG-vZ62at5IBsFivRcCDRDNK0,15315
|
|
4
4
|
arroyo/errors.py,sha256=IbtoIbz_m5QrxNRBLOxiy-hOfJQTEwNPCyq6yqedJYk,1059
|
|
5
5
|
arroyo/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
arroyo/types.py,sha256=sLY0x030np4UmbaW5C1KH1se7Z2pjQiPvAe5x2sXf7A,5684
|
|
7
7
|
arroyo/backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
8
|
arroyo/backends/abstract.py,sha256=PicUWWsgH-41XpHDcuq86P4PpPx7GWgLWdwnw4B0obo,7582
|
|
9
9
|
arroyo/backends/kafka/__init__.py,sha256=TZ0omd3LFXcZUaN_soFTuTgbjNEQYF1mF_i6_KIfCNo,306
|
|
10
|
-
arroyo/backends/kafka/commit.py,sha256=
|
|
10
|
+
arroyo/backends/kafka/commit.py,sha256=LPsjvX5PPXR62DT6sa5GuSF78qk9F_L--Fz4kw7-m-s,3060
|
|
11
11
|
arroyo/backends/kafka/configuration.py,sha256=D9zre9H2xagUsk7qBA-bm38V3_4Mg_X5hpKsKx2BkM8,3048
|
|
12
|
-
arroyo/backends/kafka/consumer.py,sha256=
|
|
12
|
+
arroyo/backends/kafka/consumer.py,sha256=sWqmn8jDf-wfZOXRc7vrotEepfeRbtdpzoP46VDf49A,27721
|
|
13
13
|
arroyo/backends/local/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
14
|
arroyo/backends/local/backend.py,sha256=pQeZRUo9KDWlOT1pICWM0MvS39QuJqHnN5ryrmcxnSc,13878
|
|
15
15
|
arroyo/backends/local/storages/__init__.py,sha256=AGYujdAAcn3osoj9jq84IzTywYbkIDv9wRg2rLhLXeg,104
|
|
16
16
|
arroyo/backends/local/storages/abstract.py,sha256=1qVQp6roxHkK6XT2aklZyZk1qq7RzcPN6Db_CA5--kg,2901
|
|
17
17
|
arroyo/backends/local/storages/memory.py,sha256=AoKDsVZzBXkOJyWArKWp3vfGfU9xLlKFXE9gsJiMIzQ,2613
|
|
18
18
|
arroyo/processing/__init__.py,sha256=vZVg0wJvJfoVzlzGvnL59bT6YNIRJNQ5t7oU045Qbk4,87
|
|
19
|
-
arroyo/processing/processor.py,sha256=
|
|
19
|
+
arroyo/processing/processor.py,sha256=rbguYCg51rX8PVmhxUbf-IXZZulX2pVJUVVHJgU76bw,18998
|
|
20
20
|
arroyo/processing/strategies/__init__.py,sha256=EU_JMb54eOxMxaC5mIFpI-sAF-X2ZScbE8czBZ7bQkY,1106
|
|
21
21
|
arroyo/processing/strategies/abstract.py,sha256=nu7juEz_aQmQIH35Z8u--FBuLjkK8_LQ1hIG2xpw9AA,4808
|
|
22
22
|
arroyo/processing/strategies/batching.py,sha256=s89xC6lQpBseEaApu1iNTipXGKeO95OMwinj2VBKn9s,4778
|
|
@@ -33,7 +33,7 @@ arroyo/processing/strategies/run_task_in_threads.py,sha256=f1sb2AG-BLz11X78jfhtE
|
|
|
33
33
|
arroyo/processing/strategies/run_task_with_multiprocessing.py,sha256=XhLRiMXFhZTRlxCTTmeI6SSTxy9SwiDHHiqq6mow-Z0,34409
|
|
34
34
|
arroyo/processing/strategies/unfold.py,sha256=wZDNdMo1Ln27P1tUvLO2svhL-JDs2ZCyIFF2cq0DxVQ,3400
|
|
35
35
|
arroyo/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
|
-
arroyo/utils/clock.py,sha256=
|
|
36
|
+
arroyo/utils/clock.py,sha256=r2EMO4nL5qIb1xnAd1sTAk2yK1UltyUi04lk5BqWKIc,944
|
|
37
37
|
arroyo/utils/codecs.py,sha256=x-8SJK0GLTOH4c_k24K97JPjBckxyQJcSpgoEViGUy0,541
|
|
38
38
|
arroyo/utils/concurrent.py,sha256=dbdPinjqmxCQ7izUGFNbGjB3OxfSIO01bnCSTANaVOE,1187
|
|
39
39
|
arroyo/utils/logging.py,sha256=Y1PnhYcI9XNNEK0H13Ct2xKLr2Niuw0dxayc6sWnui8,606
|
|
@@ -47,12 +47,12 @@ examples/transform_and_produce/batched.py,sha256=st2R6qTneAtV0JFbKP30Ti3sJDYj8Jk
|
|
|
47
47
|
examples/transform_and_produce/script.py,sha256=8kSMIjQNqGYEVyE0PvrfJh-a_UYCrJSstTp_De7kyyg,2306
|
|
48
48
|
examples/transform_and_produce/simple.py,sha256=H7xqxItjl4tx34wVW5dy6mB9G39QucAtxkJSBzVmjgA,1637
|
|
49
49
|
tests/backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
50
|
-
tests/backends/mixins.py,sha256=
|
|
50
|
+
tests/backends/mixins.py,sha256=31t7B4a8cJ65XeUzv4ZoG9aPU1lTpDZXgkQrEyKhyqE,20131
|
|
51
51
|
tests/backends/test_commit.py,sha256=iTHfK1qsBxim0XwxgMvNNSMqDUMEHoYkYBDcgxGBFbs,831
|
|
52
|
-
tests/backends/test_kafka.py,sha256=
|
|
53
|
-
tests/backends/test_local.py,sha256=
|
|
52
|
+
tests/backends/test_kafka.py,sha256=cB_GIY1yWT9w2VB1PWrvTr0lCa46r5uDfxisDFrcSmg,12089
|
|
53
|
+
tests/backends/test_local.py,sha256=Mfd4DFuWVSVtl1GomQ6TIoWuJNcAliKqKU0BShPlEMY,3363
|
|
54
54
|
tests/processing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
55
|
-
tests/processing/test_processor.py,sha256=
|
|
55
|
+
tests/processing/test_processor.py,sha256=PDCrmhWAt_wZKwYlBzDuEGsd8PnjKM-p4ySaPvVd11k,20781
|
|
56
56
|
tests/processing/strategies/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
57
57
|
tests/processing/strategies/test_all.py,sha256=ahAF-nbdmqVkYGNCg0OFCD6fzNTA-XxYrW8NQHajCDU,10167
|
|
58
58
|
tests/processing/strategies/test_batching.py,sha256=nyyX0y6qYHX7jT4gCgsUjT5RzBMDrBp790SCmOizQ0Q,11787
|
|
@@ -61,7 +61,7 @@ tests/processing/strategies/test_commit.py,sha256=ruGcBa2qBf0BoxdGZh2hE4Fa_XnOX7
|
|
|
61
61
|
tests/processing/strategies/test_filter.py,sha256=hbuWDWF1Eie9ditJa6dZ3VgsJ-ODU7EADawD-gHXwa8,8411
|
|
62
62
|
tests/processing/strategies/test_guard.py,sha256=fMx2wMlbVDDX4qqXuNRqzcLf5H4seTWw2kk4tBYK9d8,1361
|
|
63
63
|
tests/processing/strategies/test_noop.py,sha256=a5noKxkWtY7SR_gcfu4oSNk2zvJA8Zmv7PncAdWNryk,512
|
|
64
|
-
tests/processing/strategies/test_produce.py,sha256=
|
|
64
|
+
tests/processing/strategies/test_produce.py,sha256=UQ03quIAvfnsg8Og7US6D4ERs-J8nCT12bHVnjHqxRw,2172
|
|
65
65
|
tests/processing/strategies/test_reduce.py,sha256=crPFtGp7cyD8QOsmfVsyYh8KLOTzb8ryI7XtYg0vQSQ,1101
|
|
66
66
|
tests/processing/strategies/test_run_task.py,sha256=bWIy4U6QyOBtqdiJdGLMAadlEME-W2aE_ZzDbU_BsGo,2805
|
|
67
67
|
tests/processing/strategies/test_run_task_in_threads.py,sha256=5nwzF1iV6MTK1xETzWvMEOwAcZWrMOQaIPSWbiAjKFo,1457
|
|
@@ -70,9 +70,9 @@ tests/processing/strategies/test_unfold.py,sha256=Qic2Y2Un9EYBiW6E84YRvpyNctDNbf
|
|
|
70
70
|
tests/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
71
71
|
tests/utils/test_concurrent.py,sha256=Gwdzym2UZ1HO3rhOSGmzxImWcLFygY8P7MXHT3Q0xTE,455
|
|
72
72
|
tests/utils/test_metrics.py,sha256=bI0EtGgPokMQyEqX58i0-8zvLfxRP2nWaWr2wLMaJ_o,917
|
|
73
|
-
tests/utils/test_retries.py,sha256=
|
|
74
|
-
sentry_arroyo-2.19.
|
|
75
|
-
sentry_arroyo-2.19.
|
|
76
|
-
sentry_arroyo-2.19.
|
|
77
|
-
sentry_arroyo-2.19.
|
|
78
|
-
sentry_arroyo-2.19.
|
|
73
|
+
tests/utils/test_retries.py,sha256=AxJLkXWeL9AjHv_p1n0pe8CXXJp24ZQIuYBHfNcmiz4,3075
|
|
74
|
+
sentry_arroyo-2.19.6.dist-info/LICENSE,sha256=0Ng3MFdEcnz0sVD1XvGBBzbavvNp_7OAM5yVObB46jU,10829
|
|
75
|
+
sentry_arroyo-2.19.6.dist-info/METADATA,sha256=smHYlB9nbo3gVdL5EodtxEihujwnzo4GYsl8VGao52Y,1989
|
|
76
|
+
sentry_arroyo-2.19.6.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
77
|
+
sentry_arroyo-2.19.6.dist-info/top_level.txt,sha256=DVdMZKysL_iIxm5aY0sYgZtP5ZXMg9YBaBmGQHVmDXA,22
|
|
78
|
+
sentry_arroyo-2.19.6.dist-info/RECORD,,
|
tests/backends/mixins.py
CHANGED
|
@@ -2,7 +2,7 @@ import time
|
|
|
2
2
|
import uuid
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
4
|
from contextlib import closing
|
|
5
|
-
from typing import ContextManager, Generic, Iterator, Mapping, Optional, Sequence
|
|
5
|
+
from typing import Any, ContextManager, Generic, Iterator, Mapping, Optional, Sequence
|
|
6
6
|
from unittest import mock
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
@@ -14,6 +14,8 @@ from tests.assertions import assert_changes, assert_does_not_change
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class StreamsTestMixin(ABC, Generic[TStrategyPayload]):
|
|
17
|
+
cooperative_sticky = False
|
|
18
|
+
|
|
17
19
|
@abstractmethod
|
|
18
20
|
def get_topic(self, partitions: int = 1) -> ContextManager[Topic]:
|
|
19
21
|
raise NotImplementedError
|
|
@@ -397,6 +399,11 @@ class StreamsTestMixin(ABC, Generic[TStrategyPayload]):
|
|
|
397
399
|
def test_pause_resume_rebalancing(self) -> None:
|
|
398
400
|
payloads = self.get_payloads()
|
|
399
401
|
|
|
402
|
+
consumer_a_on_assign = mock.Mock()
|
|
403
|
+
consumer_a_on_revoke = mock.Mock()
|
|
404
|
+
consumer_b_on_assign = mock.Mock()
|
|
405
|
+
consumer_b_on_revoke = mock.Mock()
|
|
406
|
+
|
|
400
407
|
with self.get_topic(2) as topic, closing(
|
|
401
408
|
self.get_producer()
|
|
402
409
|
) as producer, closing(
|
|
@@ -408,10 +415,22 @@ class StreamsTestMixin(ABC, Generic[TStrategyPayload]):
|
|
|
408
415
|
producer.produce(Partition(topic, i), next(payloads)).result(
|
|
409
416
|
timeout=5.0
|
|
410
417
|
)
|
|
411
|
-
for i in
|
|
418
|
+
for i in [0, 1]
|
|
412
419
|
]
|
|
413
420
|
|
|
414
|
-
|
|
421
|
+
def wait_until_rebalancing(
|
|
422
|
+
from_consumer: Consumer[Any], to_consumer: Consumer[Any]
|
|
423
|
+
) -> None:
|
|
424
|
+
for _ in range(10):
|
|
425
|
+
assert from_consumer.poll(0) is None
|
|
426
|
+
if to_consumer.poll(1.0) is not None:
|
|
427
|
+
return
|
|
428
|
+
|
|
429
|
+
raise RuntimeError("no rebalancing happened")
|
|
430
|
+
|
|
431
|
+
consumer_a.subscribe(
|
|
432
|
+
[topic], on_assign=consumer_a_on_assign, on_revoke=consumer_a_on_revoke
|
|
433
|
+
)
|
|
415
434
|
|
|
416
435
|
# It doesn't really matter which message is fetched first -- we
|
|
417
436
|
# just want to know the assignment occurred.
|
|
@@ -428,19 +447,69 @@ class StreamsTestMixin(ABC, Generic[TStrategyPayload]):
|
|
|
428
447
|
[Partition(topic, 0), Partition(topic, 1)]
|
|
429
448
|
)
|
|
430
449
|
|
|
431
|
-
consumer_b.subscribe(
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
else:
|
|
437
|
-
assert False, "rebalance did not occur"
|
|
450
|
+
consumer_b.subscribe(
|
|
451
|
+
[topic], on_assign=consumer_b_on_assign, on_revoke=consumer_b_on_revoke
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
wait_until_rebalancing(consumer_a, consumer_b)
|
|
438
455
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
456
|
+
if self.cooperative_sticky:
|
|
457
|
+
# within incremental rebalancing, only one partition should have been reassigned to the consumer_b, and consumer_a should remain paused
|
|
458
|
+
assert consumer_a.paused() == [Partition(topic, 1)]
|
|
459
|
+
assert consumer_a.poll(10.0) is None
|
|
460
|
+
else:
|
|
461
|
+
# The first consumer should have had its offsets rolled back, as
|
|
462
|
+
# well as should have had it's partition resumed during
|
|
463
|
+
# rebalancing.
|
|
464
|
+
assert consumer_a.paused() == []
|
|
465
|
+
assert consumer_a.poll(10.0) is not None
|
|
444
466
|
|
|
445
467
|
assert len(consumer_a.tell()) == 1
|
|
446
468
|
assert len(consumer_b.tell()) == 1
|
|
469
|
+
|
|
470
|
+
(consumer_a_partition,) = consumer_a.tell()
|
|
471
|
+
(consumer_b_partition,) = consumer_b.tell()
|
|
472
|
+
|
|
473
|
+
# Pause consumer_a again.
|
|
474
|
+
consumer_a.pause(list(consumer_a.tell()))
|
|
475
|
+
# if we close consumer_a, consumer_b should get all partitions
|
|
476
|
+
producer.produce(next(iter(consumer_a.tell())), next(payloads)).result(
|
|
477
|
+
timeout=5.0
|
|
478
|
+
)
|
|
479
|
+
consumer_a.unsubscribe()
|
|
480
|
+
wait_until_rebalancing(consumer_a, consumer_b)
|
|
481
|
+
|
|
482
|
+
assert len(consumer_b.tell()) == 2
|
|
483
|
+
|
|
484
|
+
if self.cooperative_sticky:
|
|
485
|
+
|
|
486
|
+
assert consumer_a_on_assign.mock_calls == [
|
|
487
|
+
mock.call({Partition(topic, 0): 0, Partition(topic, 1): 0}),
|
|
488
|
+
]
|
|
489
|
+
assert consumer_a_on_revoke.mock_calls == [
|
|
490
|
+
mock.call([Partition(topic, 0)]),
|
|
491
|
+
mock.call([Partition(topic, 1)]),
|
|
492
|
+
]
|
|
493
|
+
|
|
494
|
+
assert consumer_b_on_assign.mock_calls == [
|
|
495
|
+
mock.call({Partition(topic, 0): 0}),
|
|
496
|
+
mock.call({Partition(topic, 1): 0}),
|
|
497
|
+
]
|
|
498
|
+
assert consumer_b_on_revoke.mock_calls == []
|
|
499
|
+
else:
|
|
500
|
+
assert consumer_a_on_assign.mock_calls == [
|
|
501
|
+
mock.call({Partition(topic, 0): 0, Partition(topic, 1): 0}),
|
|
502
|
+
mock.call({consumer_a_partition: 0}),
|
|
503
|
+
]
|
|
504
|
+
assert consumer_a_on_revoke.mock_calls == [
|
|
505
|
+
mock.call([Partition(topic, 0), Partition(topic, 1)]),
|
|
506
|
+
mock.call([consumer_a_partition]),
|
|
507
|
+
]
|
|
508
|
+
|
|
509
|
+
assert consumer_b_on_assign.mock_calls == [
|
|
510
|
+
mock.call({consumer_b_partition: 0}),
|
|
511
|
+
mock.call({Partition(topic, 0): 0, Partition(topic, 1): 0}),
|
|
512
|
+
]
|
|
513
|
+
assert consumer_b_on_revoke.mock_calls == [
|
|
514
|
+
mock.call([consumer_b_partition])
|
|
515
|
+
]
|
tests/backends/test_kafka.py
CHANGED
|
@@ -14,7 +14,10 @@ from confluent_kafka.admin import AdminClient, NewTopic
|
|
|
14
14
|
|
|
15
15
|
from arroyo.backends.kafka import KafkaConsumer, KafkaPayload, KafkaProducer
|
|
16
16
|
from arroyo.backends.kafka.commit import CommitCodec
|
|
17
|
-
from arroyo.backends.kafka.configuration import
|
|
17
|
+
from arroyo.backends.kafka.configuration import (
|
|
18
|
+
KafkaBrokerConfig,
|
|
19
|
+
build_kafka_configuration,
|
|
20
|
+
)
|
|
18
21
|
from arroyo.backends.kafka.consumer import as_kafka_configuration_bool
|
|
19
22
|
from arroyo.commit import IMMEDIATE, Commit
|
|
20
23
|
from arroyo.errors import ConsumerError, EndOfPartition
|
|
@@ -56,6 +59,7 @@ def get_topic(
|
|
|
56
59
|
configuration: Mapping[str, Any], partitions_count: int
|
|
57
60
|
) -> Iterator[Topic]:
|
|
58
61
|
name = f"test-{uuid.uuid1().hex}"
|
|
62
|
+
configuration = dict(configuration)
|
|
59
63
|
client = AdminClient(configuration)
|
|
60
64
|
[[key, future]] = client.create_topics(
|
|
61
65
|
[NewTopic(name, num_partitions=partitions_count, replication_factor=1)]
|
|
@@ -71,10 +75,15 @@ def get_topic(
|
|
|
71
75
|
|
|
72
76
|
|
|
73
77
|
class TestKafkaStreams(StreamsTestMixin[KafkaPayload]):
|
|
78
|
+
kip_848 = False
|
|
74
79
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
@property
|
|
81
|
+
def configuration(self) -> KafkaBrokerConfig:
|
|
82
|
+
config = {
|
|
83
|
+
"bootstrap.servers": os.environ.get("DEFAULT_BROKERS", "localhost:9092"),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return build_kafka_configuration(config)
|
|
78
87
|
|
|
79
88
|
@contextlib.contextmanager
|
|
80
89
|
def get_topic(self, partitions: int = 1) -> Iterator[Topic]:
|
|
@@ -90,7 +99,7 @@ class TestKafkaStreams(StreamsTestMixin[KafkaPayload]):
|
|
|
90
99
|
enable_end_of_partition: bool = True,
|
|
91
100
|
auto_offset_reset: str = "earliest",
|
|
92
101
|
strict_offset_reset: Optional[bool] = None,
|
|
93
|
-
max_poll_interval_ms: Optional[int] = None
|
|
102
|
+
max_poll_interval_ms: Optional[int] = None,
|
|
94
103
|
) -> KafkaConsumer:
|
|
95
104
|
configuration = {
|
|
96
105
|
**self.configuration,
|
|
@@ -110,6 +119,16 @@ class TestKafkaStreams(StreamsTestMixin[KafkaPayload]):
|
|
|
110
119
|
if max_poll_interval_ms < 45000:
|
|
111
120
|
configuration["session.timeout.ms"] = max_poll_interval_ms
|
|
112
121
|
|
|
122
|
+
if self.cooperative_sticky:
|
|
123
|
+
configuration["partition.assignment.strategy"] = "cooperative-sticky"
|
|
124
|
+
|
|
125
|
+
if self.kip_848:
|
|
126
|
+
configuration["group.protocol"] = "consumer"
|
|
127
|
+
configuration.pop("session.timeout.ms")
|
|
128
|
+
configuration.pop("max.poll.interval.ms", None)
|
|
129
|
+
assert "group.protocol.type" not in configuration
|
|
130
|
+
assert "heartbeat.interval.ms" not in configuration
|
|
131
|
+
|
|
113
132
|
return KafkaConsumer(configuration)
|
|
114
133
|
|
|
115
134
|
def get_producer(self) -> KafkaProducer:
|
|
@@ -210,7 +229,9 @@ class TestKafkaStreams(StreamsTestMixin[KafkaPayload]):
|
|
|
210
229
|
poll_interval = 6000
|
|
211
230
|
|
|
212
231
|
with self.get_topic() as topic:
|
|
213
|
-
with closing(self.get_producer()) as producer, closing(
|
|
232
|
+
with closing(self.get_producer()) as producer, closing(
|
|
233
|
+
self.get_consumer(max_poll_interval_ms=poll_interval)
|
|
234
|
+
) as consumer:
|
|
214
235
|
producer.produce(topic, next(self.get_payloads())).result(5.0)
|
|
215
236
|
|
|
216
237
|
processor = StreamProcessor(consumer, topic, factory, IMMEDIATE)
|
|
@@ -245,6 +266,16 @@ class TestKafkaStreams(StreamsTestMixin[KafkaPayload]):
|
|
|
245
266
|
assert consumer.paused() == []
|
|
246
267
|
|
|
247
268
|
|
|
269
|
+
class TestKafkaStreamsIncrementalRebalancing(TestKafkaStreams):
|
|
270
|
+
# re-test the kafka consumer with cooperative-sticky rebalancing
|
|
271
|
+
cooperative_sticky = True
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@pytest.mark.skip("kip-848 not functional yet")
|
|
275
|
+
class TestKafkaStreamsKip848(TestKafkaStreams):
|
|
276
|
+
kip_848 = True
|
|
277
|
+
|
|
278
|
+
|
|
248
279
|
def test_commit_codec() -> None:
|
|
249
280
|
commit = Commit(
|
|
250
281
|
"group", Partition(Topic("topic"), 0), 0, time.time(), time.time() - 5
|
tests/backends/test_local.py
CHANGED
|
@@ -18,14 +18,14 @@ from arroyo.backends.local.storages.abstract import (
|
|
|
18
18
|
)
|
|
19
19
|
from arroyo.backends.local.storages.memory import MemoryMessageStorage
|
|
20
20
|
from arroyo.types import Partition, Topic
|
|
21
|
-
from arroyo.utils.clock import
|
|
21
|
+
from arroyo.utils.clock import MockedClock
|
|
22
22
|
from tests.backends.mixins import StreamsTestMixin
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
class LocalStreamsTestMixin(StreamsTestMixin[int]):
|
|
26
26
|
def setUp(self) -> None:
|
|
27
27
|
self.storage = self.get_message_storage()
|
|
28
|
-
self.broker = LocalBroker(self.storage,
|
|
28
|
+
self.broker = LocalBroker(self.storage, MockedClock())
|
|
29
29
|
|
|
30
30
|
@abstractmethod
|
|
31
31
|
def get_message_storage(self) -> MessageStorage[int]:
|
|
@@ -9,13 +9,13 @@ from arroyo.backends.local.storages.memory import MemoryMessageStorage
|
|
|
9
9
|
from arroyo.processing.strategies.abstract import MessageRejected
|
|
10
10
|
from arroyo.processing.strategies.produce import Produce
|
|
11
11
|
from arroyo.types import Message, Partition, Topic, Value
|
|
12
|
-
from arroyo.utils.clock import
|
|
12
|
+
from arroyo.utils.clock import MockedClock
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def test_produce() -> None:
|
|
16
16
|
orig_topic = Topic("orig-topic")
|
|
17
17
|
result_topic = Topic("result-topic")
|
|
18
|
-
clock =
|
|
18
|
+
clock = MockedClock()
|
|
19
19
|
broker_storage: MemoryMessageStorage[KafkaPayload] = MemoryMessageStorage()
|
|
20
20
|
broker: LocalBroker[KafkaPayload] = LocalBroker(broker_storage, clock)
|
|
21
21
|
broker.create_topic(result_topic, partitions=1)
|
|
@@ -19,7 +19,7 @@ from arroyo.processing.strategies.abstract import (
|
|
|
19
19
|
ProcessingStrategyFactory,
|
|
20
20
|
)
|
|
21
21
|
from arroyo.types import BrokerValue, Commit, Message, Partition, Topic
|
|
22
|
-
from arroyo.utils.clock import
|
|
22
|
+
from arroyo.utils.clock import MockedClock
|
|
23
23
|
from tests.assertions import assert_changes, assert_does_not_change
|
|
24
24
|
from tests.metrics import Increment, TestingMetricsBackend, Timing
|
|
25
25
|
|
|
@@ -41,6 +41,7 @@ def test_stream_processor_lifecycle() -> None:
|
|
|
41
41
|
|
|
42
42
|
# The processor should accept heartbeat messages without an assignment or
|
|
43
43
|
# active processor.
|
|
44
|
+
consumer.tell.return_value = {}
|
|
44
45
|
consumer.poll.return_value = None
|
|
45
46
|
processor._run_once()
|
|
46
47
|
|
|
@@ -166,6 +167,7 @@ def test_stream_processor_termination_on_error() -> None:
|
|
|
166
167
|
offset = 0
|
|
167
168
|
now = datetime.now()
|
|
168
169
|
|
|
170
|
+
consumer.tell.return_value = {}
|
|
169
171
|
consumer.poll.return_value = BrokerValue(0, partition, offset, now)
|
|
170
172
|
|
|
171
173
|
exception = NotImplementedError("error")
|
|
@@ -199,6 +201,7 @@ def test_stream_processor_invalid_message_from_poll() -> None:
|
|
|
199
201
|
offset = 1
|
|
200
202
|
now = datetime.now()
|
|
201
203
|
|
|
204
|
+
consumer.tell.return_value = {}
|
|
202
205
|
consumer.poll.side_effect = [BrokerValue(0, partition, offset, now)]
|
|
203
206
|
|
|
204
207
|
strategy = mock.Mock()
|
|
@@ -236,6 +239,7 @@ def test_stream_processor_invalid_message_from_submit() -> None:
|
|
|
236
239
|
offset = 1
|
|
237
240
|
now = datetime.now()
|
|
238
241
|
|
|
242
|
+
consumer.tell.return_value = {}
|
|
239
243
|
consumer.poll.side_effect = [
|
|
240
244
|
BrokerValue(0, partition, offset, now),
|
|
241
245
|
BrokerValue(1, partition, offset + 1, now),
|
|
@@ -283,6 +287,7 @@ def test_stream_processor_create_with_partitions() -> None:
|
|
|
283
287
|
topic = Topic("topic")
|
|
284
288
|
|
|
285
289
|
consumer = mock.Mock()
|
|
290
|
+
consumer.tell.return_value = {}
|
|
286
291
|
strategy = mock.Mock()
|
|
287
292
|
factory = mock.Mock()
|
|
288
293
|
factory.create_with_partitions.return_value = strategy
|
|
@@ -306,13 +311,15 @@ def test_stream_processor_create_with_partitions() -> None:
|
|
|
306
311
|
assert factory.create_with_partitions.call_count == 1
|
|
307
312
|
assert create_args[1] == offsets_p0
|
|
308
313
|
|
|
314
|
+
consumer.tell.return_value = {**offsets_p0}
|
|
315
|
+
|
|
309
316
|
# Second partition assigned
|
|
310
317
|
offsets_p1 = {Partition(topic, 1): 0}
|
|
311
318
|
assignment_callback(offsets_p1)
|
|
312
319
|
|
|
313
320
|
create_args, _ = factory.create_with_partitions.call_args
|
|
314
321
|
assert factory.create_with_partitions.call_count == 2
|
|
315
|
-
assert create_args[1] == offsets_p1
|
|
322
|
+
assert create_args[1] == {**offsets_p1, **offsets_p0}
|
|
316
323
|
|
|
317
324
|
processor._run_once()
|
|
318
325
|
|
|
@@ -376,6 +383,7 @@ def run_commit_policy_test(
|
|
|
376
383
|
) -> Sequence[int]:
|
|
377
384
|
commit = mock.Mock()
|
|
378
385
|
consumer = mock.Mock()
|
|
386
|
+
consumer.tell.return_value = {}
|
|
379
387
|
consumer.commit_offsets = commit
|
|
380
388
|
|
|
381
389
|
factory = CommitOffsetsFactory()
|
|
@@ -523,7 +531,7 @@ def test_commit_policy_bench(
|
|
|
523
531
|
storage: MessageStorage[int] = MemoryMessageStorage()
|
|
524
532
|
storage.create_topic(topic, num_partitions)
|
|
525
533
|
|
|
526
|
-
broker = LocalBroker(storage,
|
|
534
|
+
broker = LocalBroker(storage, MockedClock())
|
|
527
535
|
|
|
528
536
|
consumer = broker.get_consumer("test-group", enable_end_of_partition=True)
|
|
529
537
|
|
|
@@ -551,6 +559,7 @@ def test_dlq() -> None:
|
|
|
551
559
|
partition = Partition(topic, 0)
|
|
552
560
|
consumer = mock.Mock()
|
|
553
561
|
consumer.poll.return_value = BrokerValue(0, partition, 1, datetime.now())
|
|
562
|
+
consumer.tell.return_value = {}
|
|
554
563
|
strategy = mock.Mock()
|
|
555
564
|
strategy.submit.side_effect = InvalidMessage(partition, 1)
|
|
556
565
|
factory = mock.Mock()
|
|
@@ -585,6 +594,7 @@ def test_healthcheck(tmpdir: py.path.local) -> None:
|
|
|
585
594
|
consumer = mock.Mock()
|
|
586
595
|
now = datetime.now()
|
|
587
596
|
consumer.poll.return_value = BrokerValue(0, partition, 1, now)
|
|
597
|
+
consumer.tell.return_value = {}
|
|
588
598
|
strategy = mock.Mock()
|
|
589
599
|
strategy.submit.side_effect = InvalidMessage(partition, 1)
|
|
590
600
|
factory = mock.Mock()
|
tests/utils/test_retries.py
CHANGED
|
@@ -3,7 +3,7 @@ from unittest import mock
|
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
5
5
|
|
|
6
|
-
from arroyo.utils.clock import
|
|
6
|
+
from arroyo.utils.clock import MockedClock
|
|
7
7
|
from arroyo.utils.retries import BasicRetryPolicy, RetryException, constant_delay
|
|
8
8
|
|
|
9
9
|
value = object()
|
|
@@ -45,7 +45,7 @@ def setup_function() -> None:
|
|
|
45
45
|
|
|
46
46
|
def test_basic_retry_policy_no_delay() -> None:
|
|
47
47
|
|
|
48
|
-
clock =
|
|
48
|
+
clock = MockedClock()
|
|
49
49
|
|
|
50
50
|
policy = BasicRetryPolicy(3, clock=clock)
|
|
51
51
|
|
|
@@ -68,19 +68,19 @@ def test_basic_retry_policy_no_delay() -> None:
|
|
|
68
68
|
|
|
69
69
|
@pytest.mark.parametrize("delay", [1, constant_delay(1)])
|
|
70
70
|
def test_basic_retry_policy_with_delay(delay: int) -> None:
|
|
71
|
-
clock =
|
|
71
|
+
clock = MockedClock()
|
|
72
72
|
policy = BasicRetryPolicy(3, delay, clock=clock)
|
|
73
73
|
assert policy.call(good_function) is value
|
|
74
74
|
assert good_function.call_count == 1
|
|
75
75
|
assert clock.time() == 0
|
|
76
76
|
|
|
77
|
-
clock =
|
|
77
|
+
clock = MockedClock()
|
|
78
78
|
policy = BasicRetryPolicy(3, delay, clock=clock)
|
|
79
79
|
assert policy.call(flaky_function) is value
|
|
80
80
|
assert flaky_function.call_count == 2
|
|
81
81
|
assert clock.time() == 1 # one retry
|
|
82
82
|
|
|
83
|
-
clock =
|
|
83
|
+
clock = MockedClock()
|
|
84
84
|
policy = BasicRetryPolicy(3, delay, clock=clock)
|
|
85
85
|
try:
|
|
86
86
|
policy.call(bad_function)
|
|
@@ -109,7 +109,7 @@ def test_basic_retry_policy_with_supression() -> None:
|
|
|
109
109
|
def suppression_test(exception: Exception) -> bool:
|
|
110
110
|
return isinstance(exception, ExpectedError)
|
|
111
111
|
|
|
112
|
-
clock =
|
|
112
|
+
clock = MockedClock()
|
|
113
113
|
policy = BasicRetryPolicy(
|
|
114
114
|
3, constant_delay(1), suppression_test=suppression_test, clock=clock
|
|
115
115
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|