Skip to content

Commit 4a2bb2d

Browse files
committed
fixes
1 parent 7ce5d90 commit 4a2bb2d

File tree

3 files changed

+59
-29
lines changed

3 files changed

+59
-29
lines changed

src/sentry/conf/types/kafka_definition.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ class ConsumerDefinition(TypedDict, total=False):
8686
dlq_max_invalid_ratio: float | None
8787
dlq_max_consecutive_count: int | None
8888

89+
stale_topic: Topic
90+
8991

9092
def validate_consumer_definition(consumer_definition: ConsumerDefinition) -> None:
9193
if "dlq_topic" not in consumer_definition and (

src/sentry/consumers/__init__.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -569,19 +569,20 @@ def build_consumer_config(group_id: str):
569569
else:
570570
stale_topic = None
571571

572+
dlq_policy = None
573+
572574
if enable_dlq or stale_threshold_sec:
573575
dlq_producer = build_dlq_producer(dlq_topic=dlq_topic, stale_topic=stale_topic)
574576

575-
dlq_policy = DlqPolicy(
576-
dlq_producer,
577-
DlqLimit(
578-
max_invalid_ratio=consumer_definition.get("dlq_max_invalid_ratio"),
579-
max_consecutive_count=consumer_definition.get("dlq_max_consecutive_count"),
580-
),
581-
None,
582-
)
583-
else:
584-
dlq_policy = None
577+
if dlq_producer:
578+
dlq_policy = DlqPolicy(
579+
dlq_producer,
580+
DlqLimit(
581+
max_invalid_ratio=consumer_definition.get("dlq_max_invalid_ratio"),
582+
max_consecutive_count=consumer_definition.get("dlq_max_consecutive_count"),
583+
),
584+
None,
585+
)
585586

586587
return StreamProcessor(
587588
consumer=consumer,

src/sentry/consumers/dlq.py

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
1+
import logging
12
import time
2-
from collections.abc import Callable, Mapping, MutableMapping
3+
from collections.abc import Mapping, MutableMapping
34
from concurrent.futures import Future
45
from datetime import datetime, timedelta, timezone
56
from enum import Enum
67

78
from arroyo.backends.kafka import KafkaPayload, KafkaProducer
89
from arroyo.dlq import InvalidMessage, KafkaDlqProducer
910
from arroyo.processing.strategies.abstract import ProcessingStrategy, ProcessingStrategyFactory
10-
from arroyo.types import FILTERED_PAYLOAD, BrokerValue, Message, Partition
11+
from arroyo.types import FILTERED_PAYLOAD, BrokerValue, Commit, FilteredPayload, Message, Partition
1112
from arroyo.types import Topic as ArroyoTopic
1213
from arroyo.types import Value
1314

1415
from sentry.conf.types.kafka_definition import Topic
1516
from sentry.utils.kafka_config import get_kafka_producer_cluster_options, get_topic_definition
1617

18+
logger = logging.getLogger(__name__)
19+
1720

1821
class RejectReason(Enum):
1922
STALE = "stale"
@@ -27,16 +30,30 @@ class MultipleDestinationDlqProducer(KafkaDlqProducer):
2730

2831
def __init__(
2932
self,
30-
producers: Mapping[RejectReason, KafkaDlqProducer],
31-
topic_selector: Callable[[BrokerValue[KafkaPayload], str], RejectReason],
33+
producers: Mapping[RejectReason, KafkaDlqProducer | None],
3234
) -> None:
3335
self.producers = producers
34-
self.topic_selector = topic_selector
3536

3637
def produce(
37-
self, value: BrokerValue[KafkaPayload], reason: str
38+
self,
39+
value: BrokerValue[KafkaPayload],
40+
reason: str | None = None,
3841
) -> Future[BrokerValue[KafkaPayload]]:
39-
return self.producers[self.topic_selector(value, reason)].produce(value)
42+
try:
43+
reject_reason = RejectReason(reason)
44+
producer = self.producers.get(reject_reason)
45+
except ValueError:
46+
producer = None
47+
48+
if producer:
49+
return producer.produce(value)
50+
else:
51+
# No DLQ producer configured for the reason.
52+
logger.error("No DLQ producer configured for reason %s", reason)
53+
future: Future[BrokerValue[KafkaPayload]] = Future()
54+
future.set_running_or_notify_cancel()
55+
future.set_result(value)
56+
return future
4057

4158

4259
def _get_dlq_producer(topic: Topic | None) -> KafkaDlqProducer | None:
@@ -50,7 +67,8 @@ def _get_dlq_producer(topic: Topic | None) -> KafkaDlqProducer | None:
5067

5168

5269
def build_dlq_producer(
53-
dlq_topic: Topic | None, stale_topic: Topic | None
70+
dlq_topic: Topic | None,
71+
stale_topic: Topic | None,
5472
) -> MultipleDestinationDlqProducer | None:
5573
if dlq_topic is None and stale_topic is None:
5674
return None
@@ -63,9 +81,11 @@ def build_dlq_producer(
6381
return MultipleDestinationDlqProducer(producers)
6482

6583

66-
class DlqStaleMessages(ProcessingStrategy):
84+
class DlqStaleMessages(ProcessingStrategy[KafkaPayload]):
6785
def __init__(
68-
self, stale_threshold_sec: int, next_step: ProcessingStrategy[KafkaPayload]
86+
self,
87+
stale_threshold_sec: int,
88+
next_step: ProcessingStrategy[KafkaPayload | FilteredPayload],
6989
) -> None:
7090
self.stale_threshold_sec = stale_threshold_sec
7191
self.next_step = next_step
@@ -80,17 +100,18 @@ def submit(self, message: Message[KafkaPayload]) -> None:
80100
)
81101

82102
if isinstance(message.value, BrokerValue):
83-
message_timestamp = message.timestamp.astimezone(timezone.utc)
84-
if message_timestamp < min_accepted_timestamp:
85-
self.offsets_to_forward[message.value.partition, message.value.next_offset]
103+
if message.value.timestamp < min_accepted_timestamp:
104+
self.offsets_to_forward[message.value.partition] = message.value.next_offset
86105
raise InvalidMessage(
87-
message.value.partition, message.value.offset, RejectReason.STALE.value
106+
message.value.partition, message.value.offset, reason=RejectReason.STALE.value
88107
)
89108

90109
if self.offsets_to_forward and time.time() > self.last_forwarded_offsets + 1:
91-
message = Message(Value(FILTERED_PAYLOAD), self.offsets_to_forward)
110+
filtered_message = Message(Value(FILTERED_PAYLOAD, self.offsets_to_forward))
92111
self.offsets_to_forward = {}
93-
self.next_step.submit(message)
112+
self.next_step.submit(filtered_message)
113+
114+
self.next_step.submit(message)
94115

95116
def poll(self) -> None:
96117
self.next_step.poll()
@@ -105,17 +126,23 @@ def terminate(self) -> None:
105126
self.next_step.terminate()
106127

107128

108-
class DlqStaleMessagesStrategyFactoryWrapper(ProcessingStrategyFactory):
129+
class DlqStaleMessagesStrategyFactoryWrapper(ProcessingStrategyFactory[KafkaPayload]):
109130
"""
110131
Wrapper used to dlq a message with a stale timestamp before it is passed to
111132
the rest of the pipeline. The InvalidMessage is raised with a
112133
"stale" reason so it can be routed to a separate stale topic.
113134
"""
114135

115-
def __init__(self, stale_threshold_sec: int, inner: ProcessingStrategyFactory) -> None:
136+
def __init__(
137+
self,
138+
stale_threshold_sec: int,
139+
inner: ProcessingStrategyFactory[KafkaPayload | FilteredPayload],
140+
) -> None:
116141
self.stale_threshold_sec = stale_threshold_sec
117142
self.inner = inner
118143

119-
def create_with_partitions(self, commit, partitions) -> ProcessingStrategy:
144+
def create_with_partitions(
145+
self, commit: Commit, partitions: Mapping[Partition, int]
146+
) -> ProcessingStrategy[KafkaPayload]:
120147
rv = self.inner.create_with_partitions(commit, partitions)
121148
return DlqStaleMessages(self.stale_threshold_sec, rv)

0 commit comments

Comments
 (0)