41 Kafka producer wrapper.
44 def __init__(self, enabled: bool, bootstrap_servers: str):
45 self._enabled = enabled
46 self._bootstrap_servers = bootstrap_servers
48 async def start(self):
50 self.producer = aiokafka.AIOKafkaProducer(
51 bootstrap_servers=self._bootstrap_servers,
52 linger_ms=0, # turn off message buffering
54 await self.producer.start()
61 partition: int | None = None,
62 headers: Headers | None = None,
65 Sends the message (``value``) to ``topic`` by ``key`` and,
66 optionally, to a given ``partition`` and waits until it is delivered.
67 If the call is successfully awaited,
68 message is guaranteed to be delivered.
70 :param topic: topic name.
71 :param key: key. Needed to determine message's partition.
72 :param value: message payload. Must be valid UTF-8.
73 :param partition: Optional message partition.
74 If not passed, determined by internal partitioner
75 depends on key's hash.
78 resp_future = await self.send_async(
79 topic, key, value, partition, headers
88 partition: int | None = None,
89 headers: Headers | None = None,
92 Sends the message (``value``) to ``topic`` by ``key`` and,
93 optionally, to a given ``partition`` and
94 returns the future for message delivery awaiting.
96 :param topic: topic name.
97 :param key: key. Needed to determine message's partition.
98 :param value: message payload. Must be valid UTF-8.
99 :param partition: Optional message partition.
100 If not passed, determined by internal partitioner
101 depends on key's hash.
104 if not self._enabled:
105 raise KafkaDisabledError
107 return await self.producer.send(
109 value=value if isinstance(value, bytes) else value.encode(),
110 key=key if isinstance(key, bytes) else key.encode(),
115 async def _flush(self):
116 logger.info('Flusing produced messages')
117 await self.producer.flush()
119 async def aclose(self):
121 await self.producer.stop()
124class ConsumedMessage:
125 """Wrapper for consumed record."""
134 def __init__(self, record: aiokafka.ConsumerRecord):
135 self.topic = record.topic
136 self.key_raw = record.key
137 self.value_raw = record.value
138 self.partition = record.partition
139 self.offset = record.offset
140 self.headers_raw = record.headers
143 def key(self) -> str:
144 return self.key_raw.decode()
147 def value(self) -> str:
148 return self.value_raw.decode()
151 def headers(self) -> list[Header]:
152 return list(self.headers_raw)
157 Kafka balanced consumer wrapper.
158 All consumers are created with the same group.id,
159 after each test consumer commits offsets for all consumed messages.
160 This is needed to make tests independent.
163 def __init__(self, enabled: bool, bootstrap_servers):
164 self._enabled = enabled
165 self._bootstrap_servers = bootstrap_servers
166 self._subscribed_topics: list[str] = []
168 async def start(self):
170 self.consumer = aiokafka.AIOKafkaConsumer(
171 group_id='Test-group',
172 bootstrap_servers=self._bootstrap_servers,
173 auto_offset_reset='earliest',
174 enable_auto_commit=False,
176 await self.consumer.start()
178 def _subscribe(self, topics: list[str]):
179 if not self._enabled:
180 raise KafkaDisabledError
182 to_subscribe: list[str] = []
184 if topic not in self._subscribed_topics:
185 to_subscribe.append(topic)
188 logger.info('Subscribing to [%s]', ','.join(to_subscribe))
189 self.consumer.subscribe(to_subscribe)
190 self._subscribed_topics.extend(to_subscribe)
192 async def _commit(self):
193 if not self._enabled:
194 raise KafkaDisabledError
196 if self._subscribed_topics:
197 await self.consumer.commit()
199 async def _unsubscribe(self):
202 if self._subscribed_topics:
203 logger.info('Unsubscribing from all topics')
204 self.consumer.unsubscribe()
205 self._subscribed_topics = []
207 async def receive_one(
208 self, topics: list[str], timeout: float = 20.0
209 ) -> ConsumedMessage:
211 Waits until one message are consumed.
213 :param topics: list of topics to read messages from.
214 :param timeout: timeout to stop waiting. Default is 20 seconds.
216 :returns: :py:class:`ConsumedMessage`
218 if not self._enabled:
219 raise KafkaDisabledError
221 self._subscribe(topics)
223 async def _do_receive():
224 record: aiokafka.ConsumerRecord = await self.consumer.getone()
225 return ConsumedMessage(record)
227 return await asyncio.wait_for(_do_receive(), timeout=timeout)
229 async def receive_batch(
232 max_batch_size: int | None,
233 timeout: float = 3.0,
234 ) -> list[ConsumedMessage]:
236 Waits until either ``max_batch_size`` messages are consumed or
239 :param topics: list of topics to read messages from.
240 :max_batch_size: maximum number of consumed messages.
241 :param timeout: timeout to stop waiting. Default is 3 seconds.
243 :returns: :py:class:`List[ConsumedMessage]`
245 if not self._enabled:
246 raise KafkaDisabledError
248 self._subscribe(topics)
251 aiokafka.TopicPartition, list[aiokafka.ConsumerRecord]
252 ] = await self.consumer.getmany(
253 timeout_ms=int(timeout * 1000), max_records=max_batch_size
256 return list(map(ConsumedMessage, sum(records.values(), [])))
258 async def aclose(self):
260 await self._unsubscribe()
261 await self.consumer.stop()