18
18
)
19
19
from kafka .record import MemoryRecords
20
20
from kafka .serializer import Deserializer
21
- from kafka .structs import TopicPartition , OffsetAndTimestamp
21
+ from kafka .structs import TopicPartition , OffsetAndMetadata , OffsetAndTimestamp
22
22
23
23
log = logging .getLogger (__name__ )
24
24
28
28
READ_COMMITTED = 1
29
29
30
30
ConsumerRecord = collections .namedtuple ("ConsumerRecord" ,
31
- ["topic" , "partition" , "offset" , "timestamp" , "timestamp_type" ,
31
+ ["topic" , "partition" , "leader_epoch" , " offset" , "timestamp" , "timestamp_type" ,
32
32
"key" , "value" , "headers" , "checksum" , "serialized_key_size" , "serialized_value_size" , "serialized_header_size" ])
33
33
34
34
@@ -198,9 +198,6 @@ def get_offsets_by_times(self, timestamps, timeout_ms):
198
198
for tp in timestamps :
199
199
if tp not in offsets :
200
200
offsets [tp ] = None
201
- else :
202
- offset , timestamp = offsets [tp ]
203
- offsets [tp ] = OffsetAndTimestamp (offset , timestamp )
204
201
return offsets
205
202
206
203
def beginning_offsets (self , partitions , timeout_ms ):
@@ -215,7 +212,7 @@ def beginning_or_end_offset(self, partitions, timestamp, timeout_ms):
215
212
timestamps = dict ([(tp , timestamp ) for tp in partitions ])
216
213
offsets = self ._retrieve_offsets (timestamps , timeout_ms )
217
214
for tp in timestamps :
218
- offsets [tp ] = offsets [tp ][ 0 ]
215
+ offsets [tp ] = offsets [tp ]. offset
219
216
return offsets
220
217
221
218
def _reset_offset (self , partition ):
@@ -240,7 +237,7 @@ def _reset_offset(self, partition):
240
237
offsets = self ._retrieve_offsets ({partition : timestamp })
241
238
242
239
if partition in offsets :
243
- offset = offsets [partition ][ 0 ]
240
+ offset = offsets [partition ]. offset
244
241
245
242
# we might lose the assignment while fetching the offset,
246
243
# so check it is still active
@@ -261,8 +258,8 @@ def _retrieve_offsets(self, timestamps, timeout_ms=float("inf")):
261
258
available. Otherwise timestamp is treated as epoch milliseconds.
262
259
263
260
Returns:
264
- {TopicPartition: (int, int) }: Mapping of partition to
265
- retrieved offset and timestamp . If offset does not exist for
261
+ {TopicPartition: OffsetAndTimestamp }: Mapping of partition to
262
+ retrieved offset, timestamp, and leader_epoch . If offset does not exist for
266
263
the provided timestamp, that partition will be missing from
267
264
this mapping.
268
265
"""
@@ -373,28 +370,29 @@ def _append(self, drained, part, max_records, update_offsets):
373
370
log .debug ("Not returning fetched records for assigned partition"
374
371
" %s since it is no longer fetchable" , tp )
375
372
376
- elif fetch_offset == position :
373
+ elif fetch_offset == position . offset :
377
374
# we are ensured to have at least one record since we already checked for emptiness
378
375
part_records = part .take (max_records )
379
376
next_offset = part_records [- 1 ].offset + 1
377
+ leader_epoch = part_records [- 1 ].leader_epoch
380
378
381
379
log .log (0 , "Returning fetched records at offset %d for assigned"
382
- " partition %s and update position to %s" , position ,
383
- tp , next_offset )
380
+ " partition %s and update position to %s (leader epoch %s) " , position . offset ,
381
+ tp , next_offset , leader_epoch )
384
382
385
383
for record in part_records :
386
384
drained [tp ].append (record )
387
385
388
386
if update_offsets :
389
- self ._subscriptions .assignment [tp ].position = next_offset
387
+ self ._subscriptions .assignment [tp ].position = OffsetAndMetadata ( next_offset , b'' , leader_epoch )
390
388
return len (part_records )
391
389
392
390
else :
393
391
# these records aren't next in line based on the last consumed
394
392
# position, ignore them they must be from an obsolete request
395
393
log .debug ("Ignoring fetched records for %s at offset %s since"
396
394
" the current position is %d" , tp , part .fetch_offset ,
397
- position )
395
+ position . offset )
398
396
399
397
part .discard ()
400
398
return 0
@@ -444,13 +442,13 @@ def _message_generator(self):
444
442
break
445
443
446
444
# Compressed messagesets may include earlier messages
447
- elif msg .offset < self ._subscriptions .assignment [tp ].position :
445
+ elif msg .offset < self ._subscriptions .assignment [tp ].position . offset :
448
446
log .debug ("Skipping message offset: %s (expecting %s)" ,
449
447
msg .offset ,
450
- self ._subscriptions .assignment [tp ].position )
448
+ self ._subscriptions .assignment [tp ].position . offset )
451
449
continue
452
450
453
- self ._subscriptions .assignment [tp ].position = msg .offset + 1
451
+ self ._subscriptions .assignment [tp ].position = OffsetAndMetadata ( msg .offset + 1 , b'' , - 1 )
454
452
yield msg
455
453
456
454
self ._next_partition_records = None
@@ -463,8 +461,9 @@ def _unpack_records(self, tp, records):
463
461
# Try DefaultsRecordBatch / message log format v2
464
462
# base_offset, last_offset_delta, and control batches
465
463
try :
466
- self ._subscriptions .assignment [tp ].last_offset_from_record_batch = batch .base_offset + \
467
- batch .last_offset_delta
464
+ batch_offset = batch .base_offset + batch .last_offset_delta
465
+ leader_epoch = batch .leader_epoch
466
+ self ._subscriptions .assignment [tp ].last_offset_from_record_batch = batch_offset
468
467
# Control batches have a single record indicating whether a transaction
469
468
# was aborted or committed.
470
469
# When isolation_level is READ_COMMITTED (currently unsupported)
@@ -475,6 +474,7 @@ def _unpack_records(self, tp, records):
475
474
batch = records .next_batch ()
476
475
continue
477
476
except AttributeError :
477
+ leader_epoch = - 1
478
478
pass
479
479
480
480
for record in batch :
@@ -491,7 +491,7 @@ def _unpack_records(self, tp, records):
491
491
len (h_key .encode ("utf-8" )) + (len (h_val ) if h_val is not None else 0 ) for h_key , h_val in
492
492
headers ) if headers else - 1
493
493
yield ConsumerRecord (
494
- tp .topic , tp .partition , record .offset , record .timestamp ,
494
+ tp .topic , tp .partition , leader_epoch , record .offset , record .timestamp ,
495
495
record .timestamp_type , key , value , headers , record .checksum ,
496
496
key_size , value_size , header_size )
497
497
@@ -577,7 +577,9 @@ def _send_list_offsets_request(self, node_id, timestamps):
577
577
version = self ._client .api_version (ListOffsetsRequest , max_version = 3 )
578
578
by_topic = collections .defaultdict (list )
579
579
for tp , timestamp in six .iteritems (timestamps ):
580
- if version >= 1 :
580
+ if version >= 4 :
581
+ data = (tp .partition , leader_epoch , timestamp )
582
+ elif version >= 1 :
581
583
data = (tp .partition , timestamp )
582
584
else :
583
585
data = (tp .partition , timestamp , 1 )
@@ -630,17 +632,18 @@ def _handle_list_offsets_response(self, future, response):
630
632
offset = UNKNOWN_OFFSET
631
633
else :
632
634
offset = offsets [0 ]
633
- log .debug ("Handling v0 ListOffsetsResponse response for %s. "
634
- "Fetched offset %s" , partition , offset )
635
- if offset != UNKNOWN_OFFSET :
636
- timestamp_offset_map [partition ] = (offset , None )
637
- else :
635
+ timestamp = None
636
+ leader_epoch = - 1
637
+ elif response .API_VERSION <= 3 :
638
638
timestamp , offset = partition_info [2 :]
639
- log .debug ("Handling ListOffsetsResponse response for %s. "
640
- "Fetched offset %s, timestamp %s" ,
641
- partition , offset , timestamp )
642
- if offset != UNKNOWN_OFFSET :
643
- timestamp_offset_map [partition ] = (offset , timestamp )
639
+ leader_epoch = - 1
640
+ else :
641
+ timestamp , offset , leader_epoch = partition_info [2 :]
642
+ log .debug ("Handling ListOffsetsResponse response for %s. "
643
+ "Fetched offset %s, timestamp %s, leader_epoch %s" ,
644
+ partition , offset , timestamp , leader_epoch )
645
+ if offset != UNKNOWN_OFFSET :
646
+ timestamp_offset_map [partition ] = OffsetAndTimestamp (offset , timestamp , leader_epoch )
644
647
elif error_type is Errors .UnsupportedForMessageFormatError :
645
648
# The message format on the broker side is before 0.10.0,
646
649
# we simply put None in the response.
@@ -688,7 +691,7 @@ def _create_fetch_requests(self):
688
691
"""
689
692
# create the fetch info as a dict of lists of partition info tuples
690
693
# which can be passed to FetchRequest() via .items()
691
- version = self ._client .api_version (FetchRequest , max_version = 7 )
694
+ version = self ._client .api_version (FetchRequest , max_version = 10 )
692
695
fetchable = collections .defaultdict (dict )
693
696
694
697
for partition in self ._fetchable_partitions ():
@@ -697,12 +700,12 @@ def _create_fetch_requests(self):
697
700
# advance position for any deleted compacted messages if required
698
701
if self ._subscriptions .assignment [partition ].last_offset_from_record_batch :
699
702
next_offset_from_batch_header = self ._subscriptions .assignment [partition ].last_offset_from_record_batch + 1
700
- if next_offset_from_batch_header > self ._subscriptions .assignment [partition ].position :
703
+ if next_offset_from_batch_header > self ._subscriptions .assignment [partition ].position . offset :
701
704
log .debug (
702
705
"Advance position for partition %s from %s to %s (last record batch location plus one)"
703
706
" to correct for deleted compacted messages and/or transactional control records" ,
704
- partition , self ._subscriptions .assignment [partition ].position , next_offset_from_batch_header )
705
- self ._subscriptions .assignment [partition ].position = next_offset_from_batch_header
707
+ partition , self ._subscriptions .assignment [partition ].position . offset , next_offset_from_batch_header )
708
+ self ._subscriptions .assignment [partition ].position = OffsetAndMetadata ( next_offset_from_batch_header , b'' , - 1 )
706
709
707
710
position = self ._subscriptions .assignment [partition ].position
708
711
@@ -720,19 +723,28 @@ def _create_fetch_requests(self):
720
723
if version < 5 :
721
724
partition_info = (
722
725
partition .partition ,
723
- position ,
726
+ position . offset ,
724
727
self .config ['max_partition_fetch_bytes' ]
725
728
)
729
+ elif version <= 8 :
730
+ partition_info = (
731
+ partition .partition ,
732
+ position .offset ,
733
+ - 1 , # log_start_offset is used internally by brokers / replicas only
734
+ self .config ['max_partition_fetch_bytes' ],
735
+ )
726
736
else :
727
737
partition_info = (
728
738
partition .partition ,
729
- position ,
739
+ position .leader_epoch ,
740
+ position .offset ,
730
741
- 1 , # log_start_offset is used internally by brokers / replicas only
731
742
self .config ['max_partition_fetch_bytes' ],
732
743
)
744
+
733
745
fetchable [node_id ][partition ] = partition_info
734
746
log .debug ("Adding fetch request for partition %s at offset %d" ,
735
- partition , position )
747
+ partition , position . offset )
736
748
737
749
requests = {}
738
750
for node_id , next_partitions in six .iteritems (fetchable ):
@@ -780,7 +792,10 @@ def _create_fetch_requests(self):
780
792
781
793
fetch_offsets = {}
782
794
for tp , partition_data in six .iteritems (next_partitions ):
783
- offset = partition_data [1 ]
795
+ if version <= 8 :
796
+ offset = partition_data [1 ]
797
+ else :
798
+ offset = partition_data [2 ]
784
799
fetch_offsets [tp ] = offset
785
800
786
801
requests [node_id ] = (request , fetch_offsets )
@@ -809,7 +824,7 @@ def _handle_fetch_response(self, node_id, fetch_offsets, send_time, response):
809
824
tp = TopicPartition (topic , partition_data [0 ])
810
825
fetch_offset = fetch_offsets [tp ]
811
826
completed_fetch = CompletedFetch (
812
- tp , fetch_offsets [ tp ] ,
827
+ tp , fetch_offset ,
813
828
response .API_VERSION ,
814
829
partition_data [1 :],
815
830
metric_aggregator
@@ -851,18 +866,18 @@ def _parse_fetched_data(self, completed_fetch):
851
866
# Note that the *response* may return a messageset that starts
852
867
# earlier (e.g., compressed messages) or later (e.g., compacted topic)
853
868
position = self ._subscriptions .assignment [tp ].position
854
- if position is None or position != fetch_offset :
869
+ if position is None or position . offset != fetch_offset :
855
870
log .debug ("Discarding fetch response for partition %s"
856
871
" since its offset %d does not match the"
857
872
" expected offset %d" , tp , fetch_offset ,
858
- position )
873
+ position . offset )
859
874
return None
860
875
861
876
records = MemoryRecords (completed_fetch .partition_data [- 1 ])
862
877
if records .has_next ():
863
878
log .debug ("Adding fetched record for partition %s with"
864
879
" offset %d to buffered record list" , tp ,
865
- position )
880
+ position . offset )
866
881
unpacked = list (self ._unpack_records (tp , records ))
867
882
parsed_records = self .PartitionRecords (fetch_offset , tp , unpacked )
868
883
if unpacked :
@@ -893,10 +908,10 @@ def _parse_fetched_data(self, completed_fetch):
893
908
self ._client .cluster .request_update ()
894
909
elif error_type is Errors .OffsetOutOfRangeError :
895
910
position = self ._subscriptions .assignment [tp ].position
896
- if position is None or position != fetch_offset :
911
+ if position is None or position . offset != fetch_offset :
897
912
log .debug ("Discarding stale fetch response for partition %s"
898
913
" since the fetched offset %d does not match the"
899
- " current offset %d" , tp , fetch_offset , position )
914
+ " current offset %d" , tp , fetch_offset , position . offset )
900
915
elif self ._subscriptions .has_default_offset_reset_policy ():
901
916
log .info ("Fetch offset %s is out of range for topic-partition %s" , fetch_offset , tp )
902
917
self ._subscriptions .need_offset_reset (tp )
0 commit comments