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.
@@ -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.warn(f"Legacy commit message found: {commit}")
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
- self.__consumer.assign(
455
- [
456
- ConfluentTopicPartition(partition.topic.name, partition.index, offset)
457
- for partition, offset in offsets.items()
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 reset(self) -> None:
318
+ def remove(self, partition: Partition) -> None:
319
319
  """
320
- Reset the buffer.
320
+ Remove a revoked partition from the buffer.
321
321
  """
322
- self.__buffered_messages = defaultdict(deque)
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.reset_offsets({})
346
+ self.reset_dlq_limits({})
347
347
 
348
- def reset_offsets(self, assignment: Mapping[Partition, int]) -> None:
348
+ def reset_dlq_limits(self, assignment: Mapping[Partition, int]) -> None:
349
349
  """
350
350
  Called on consumer assignment
351
351
  """
@@ -238,16 +238,23 @@ class StreamProcessor(Generic[TStrategyPayload]):
238
238
  "arroyo.consumer.partitions_assigned.count", len(partitions)
239
239
  )
240
240
 
241
- self.__buffered_messages.reset()
241
+ current_partitions = dict(self.__consumer.tell())
242
+ current_partitions.update(partitions)
243
+
242
244
  if self.__dlq_policy:
243
- self.__dlq_policy.reset_offsets(partitions)
244
- if partitions:
245
+ self.__dlq_policy.reset_dlq_limits(current_partitions)
246
+ if current_partitions:
245
247
  if self.__processing_strategy is not None:
246
- logger.exception(
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(partitions)
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
@@ -28,7 +28,7 @@ class SystemClock(Clock):
28
28
  time.sleep(duration)
29
29
 
30
30
 
31
- class TestingClock(Clock):
31
+ class MockedClock(Clock):
32
32
  """
33
33
  A clock implementation that uses a stable time for testing. To advance
34
34
  the time, use the ``sleep`` method.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sentry-arroyo
3
- Version: 2.19.4
3
+ Version: 2.19.6
4
4
  Summary: Arroyo is a Python library for working with streaming data.
5
5
  Home-page: https://github.com/getsentry/arroyo
6
6
  Author: Sentry
@@ -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=2_IlVfG_bizSSPOyGXLaI6yJCDoAWMkWx42ah0JTJr0,15260
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=JV1C8hbTUTWyDIU1qcaQkUzQdc1j-APXIjYUJJRpb1o,3056
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=LUUp-jffNfgW8Qerw8Yv8IhVabw-oONrkIFrjDVcauM,27304
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=kIb_IXoRyy8j85H1--KheQYxeJzBTTZ6V9yc59LkpYw,18475
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=xZE-amURj5wiuQXRpdoakB_osmEVf3llNpMLV0lVX8g,945
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=I8oDRWgUhQEo6P6v1rYqrwiqcaigxSaJIMSHd7bntj4,17228
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=BP1OBWmciG-4yWWqdcrAV3406azFeHPmnICqUSLD3YI,11151
53
- tests/backends/test_local.py,sha256=JbWvB5ssNp9fggHDQRT53O_qpuQAN0GgwTLFXWLzIT4,3365
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=5KT9I7rcI5fIERVFdUZQJ8R8B7c4ndHOblfiTbylDlE,20428
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=YIExrxIhhNtekj3tr9KjSoX7EGFnxUQyhYrwQvlzq-k,2174
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=qSXalR9TvjvbPpMAinhmJvpmoVo8Ybv07c5VgQZZeI8,3081
74
- sentry_arroyo-2.19.4.dist-info/LICENSE,sha256=0Ng3MFdEcnz0sVD1XvGBBzbavvNp_7OAM5yVObB46jU,10829
75
- sentry_arroyo-2.19.4.dist-info/METADATA,sha256=jDFHiPwdPjQtmegxMa78Xgq7QiMVASqgmkIE3S9vpdU,1989
76
- sentry_arroyo-2.19.4.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
77
- sentry_arroyo-2.19.4.dist-info/top_level.txt,sha256=DVdMZKysL_iIxm5aY0sYgZtP5ZXMg9YBaBmGQHVmDXA,22
78
- sentry_arroyo-2.19.4.dist-info/RECORD,,
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 range(2)
418
+ for i in [0, 1]
412
419
  ]
413
420
 
414
- consumer_a.subscribe([topic])
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([topic])
432
- for i in range(10):
433
- assert consumer_a.poll(0) is None # attempt to force session timeout
434
- if consumer_b.poll(1.0) is not None:
435
- break
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
- # The first consumer should have had its offsets rolled back, as
440
- # well as should have had it's partition resumed during
441
- # rebalancing.
442
- assert consumer_a.paused() == []
443
- assert consumer_a.poll(10.0) is not None
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
+ ]
@@ -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 build_kafka_configuration
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
- configuration = build_kafka_configuration(
76
- {"bootstrap.servers": os.environ.get("DEFAULT_BROKERS", "localhost:9092")}
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(self.get_consumer(max_poll_interval_ms=poll_interval)) as consumer:
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
@@ -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 TestingClock
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, TestingClock())
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 TestingClock
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 = TestingClock()
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 TestingClock
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, TestingClock())
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()
@@ -3,7 +3,7 @@ from unittest import mock
3
3
 
4
4
  import pytest
5
5
 
6
- from arroyo.utils.clock import TestingClock
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 = TestingClock()
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 = TestingClock()
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 = TestingClock()
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 = TestingClock()
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 = TestingClock()
112
+ clock = MockedClock()
113
113
  policy = BasicRetryPolicy(
114
114
  3, constant_delay(1), suppression_test=suppression_test, clock=clock
115
115
  )