@@ -98,6 +98,26 @@ def _get_leader_for_partition(self, topic, partition):
98
98
# Otherwise return the BrokerMetadata
99
99
return self .brokers [meta .leader ]
100
100
101
+ def _get_coordinator_for_group (self , group ):
102
+ """
103
+ Returns the coordinator broker for a consumer group.
104
+
105
+ ConsumerCoordinatorNotAvailableCode will be raised if the coordinator
106
+ does not currently exist for the group.
107
+
108
+ OffsetsLoadInProgressCode is raised if the coordinator is available
109
+ but is still loading offsets from the internal topic
110
+ """
111
+
112
+ resp = self .send_consumer_metadata_request (group )
113
+
114
+ # If there's a problem with finding the coordinator, raise the
115
+ # provided error
116
+ kafka .common .check_error (resp )
117
+
118
+ # Otherwise return the BrokerMetadata
119
+ return BrokerMetadata (resp .nodeId , resp .host , resp .port )
120
+
101
121
def _next_id (self ):
102
122
"""Generate a new correlation id"""
103
123
# modulo to keep w/i int32
@@ -254,6 +274,96 @@ def _send_broker_aware_request(self, payloads, encoder_fn, decoder_fn):
254
274
# Return responses in the same order as provided
255
275
return [responses [tp ] for tp in original_ordering ]
256
276
277
+ def _send_consumer_aware_request (self , group , payloads , encoder_fn , decoder_fn ):
278
+ """
279
+ Send a list of requests to the consumer coordinator for the group
280
+ specified using the supplied encode/decode functions. As the payloads
281
+ that use consumer-aware requests do not contain the group (e.g.
282
+ OffsetFetchRequest), all payloads must be for a single group.
283
+
284
+ Arguments:
285
+
286
+ group: the name of the consumer group (str) the payloads are for
287
+ payloads: list of object-like entities with topic (str) and
288
+ partition (int) attributes; payloads with duplicate
289
+ topic+partition are not supported.
290
+
291
+ encode_fn: a method to encode the list of payloads to a request body,
292
+ must accept client_id, correlation_id, and payloads as
293
+ keyword arguments
294
+
295
+ decode_fn: a method to decode a response body into response objects.
296
+ The response objects must be object-like and have topic
297
+ and partition attributes
298
+
299
+ Returns:
300
+
301
+ List of response objects in the same order as the supplied payloads
302
+ """
303
+ # encoders / decoders do not maintain ordering currently
304
+ # so we need to keep this so we can rebuild order before returning
305
+ original_ordering = [(p .topic , p .partition ) for p in payloads ]
306
+
307
+ broker = self ._get_coordinator_for_group (group )
308
+
309
+ # Send the list of request payloads and collect the responses and
310
+ # errors
311
+ responses = {}
312
+ requestId = self ._next_id ()
313
+ log .debug ('Request %s to %s: %s' , requestId , broker , payloads )
314
+ request = encoder_fn (client_id = self .client_id ,
315
+ correlation_id = requestId , payloads = payloads )
316
+
317
+ # Send the request, recv the response
318
+ try :
319
+ conn = self ._get_conn (broker .host .decode ('utf-8' ), broker .port )
320
+ conn .send (requestId , request )
321
+
322
+ except ConnectionError as e :
323
+ log .warning ('ConnectionError attempting to send request %s '
324
+ 'to server %s: %s' , requestId , broker , e )
325
+
326
+ for payload in payloads :
327
+ topic_partition = (payload .topic , payload .partition )
328
+ responses [topic_partition ] = FailedPayloadsError (payload )
329
+
330
+ # No exception, try to get response
331
+ else :
332
+
333
+ # decoder_fn=None signal that the server is expected to not
334
+ # send a response. This probably only applies to
335
+ # ProduceRequest w/ acks = 0
336
+ if decoder_fn is None :
337
+ log .debug ('Request %s does not expect a response '
338
+ '(skipping conn.recv)' , requestId )
339
+ for payload in payloads :
340
+ topic_partition = (payload .topic , payload .partition )
341
+ responses [topic_partition ] = None
342
+ return []
343
+
344
+ try :
345
+ response = conn .recv (requestId )
346
+ except ConnectionError as e :
347
+ log .warning ('ConnectionError attempting to receive a '
348
+ 'response to request %s from server %s: %s' ,
349
+ requestId , broker , e )
350
+
351
+ for payload in payloads :
352
+ topic_partition = (payload .topic , payload .partition )
353
+ responses [topic_partition ] = FailedPayloadsError (payload )
354
+
355
+ else :
356
+ _resps = []
357
+ for payload_response in decoder_fn (response ):
358
+ topic_partition = (payload_response .topic ,
359
+ payload_response .partition )
360
+ responses [topic_partition ] = payload_response
361
+ _resps .append (payload_response )
362
+ log .debug ('Response %s: %s' , requestId , _resps )
363
+
364
+ # Return responses in the same order as provided
365
+ return [responses [tp ] for tp in original_ordering ]
366
+
257
367
def __repr__ (self ):
258
368
return '<KafkaClient client_id=%s>' % (self .client_id )
259
369
@@ -446,6 +556,13 @@ def send_metadata_request(self, payloads=[], fail_on_error=True,
446
556
447
557
return self ._send_broker_unaware_request (payloads , encoder , decoder )
448
558
559
+ def send_consumer_metadata_request (self , payloads = [], fail_on_error = True ,
560
+ callback = None ):
561
+ encoder = KafkaProtocol .encode_consumer_metadata_request
562
+ decoder = KafkaProtocol .decode_consumer_metadata_response
563
+
564
+ return self ._send_broker_unaware_request (payloads , encoder , decoder )
565
+
449
566
def send_produce_request (self , payloads = [], acks = 1 , timeout = 1000 ,
450
567
fail_on_error = True , callback = None ):
451
568
"""
@@ -550,3 +667,14 @@ def send_offset_fetch_request(self, group, payloads=[],
550
667
551
668
return [resp if not callback else callback (resp ) for resp in resps
552
669
if not fail_on_error or not self ._raise_on_response_error (resp )]
670
+
671
+ def send_offset_fetch_request_kafka (self , group , payloads = [],
672
+ fail_on_error = True , callback = None ):
673
+
674
+ encoder = functools .partial (KafkaProtocol .encode_offset_fetch_request ,
675
+ group = group , from_kafka = True )
676
+ decoder = KafkaProtocol .decode_offset_fetch_response
677
+ resps = self ._send_consumer_aware_request (group , payloads , encoder , decoder )
678
+
679
+ return [resp if not callback else callback (resp ) for resp in resps
680
+ if not fail_on_error or not self ._raise_on_response_error (resp )]
0 commit comments