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 )
@@ -628,17 +630,18 @@ def _handle_list_offsets_response(self, future, response):
628
630
offset = UNKNOWN_OFFSET
629
631
else :
630
632
offset = offsets [0 ]
631
- log .debug ("Handling v0 ListOffsetsResponse response for %s. "
632
- "Fetched offset %s" , partition , offset )
633
- if offset != UNKNOWN_OFFSET :
634
- timestamp_offset_map [partition ] = (offset , None )
635
- else :
633
+ timestamp = None
634
+ leader_epoch = - 1
635
+ elif response .API_VERSION <= 3 :
636
636
timestamp , offset = partition_info [2 :]
637
- log .debug ("Handling ListOffsetsResponse response for %s. "
638
- "Fetched offset %s, timestamp %s" ,
639
- partition , offset , timestamp )
640
- if offset != UNKNOWN_OFFSET :
641
- timestamp_offset_map [partition ] = (offset , timestamp )
637
+ leader_epoch = - 1
638
+ else :
639
+ timestamp , offset , leader_epoch = partition_info [2 :]
640
+ log .debug ("Handling ListOffsetsResponse response for %s. "
641
+ "Fetched offset %s, timestamp %s, leader_epoch %s" ,
642
+ partition , offset , timestamp , leader_epoch )
643
+ if offset != UNKNOWN_OFFSET :
644
+ timestamp_offset_map [partition ] = OffsetAndTimestamp (offset , timestamp , leader_epoch )
642
645
elif error_type is Errors .UnsupportedForMessageFormatError :
643
646
# The message format on the broker side is before 0.10.0,
644
647
# we simply put None in the response.
@@ -686,7 +689,7 @@ def _create_fetch_requests(self):
686
689
"""
687
690
# create the fetch info as a dict of lists of partition info tuples
688
691
# which can be passed to FetchRequest() via .items()
689
- version = self ._client .api_version (FetchRequest , max_version = 8 )
692
+ version = self ._client .api_version (FetchRequest , max_version = 10 )
690
693
fetchable = collections .defaultdict (dict )
691
694
692
695
for partition in self ._fetchable_partitions ():
@@ -695,12 +698,12 @@ def _create_fetch_requests(self):
695
698
# advance position for any deleted compacted messages if required
696
699
if self ._subscriptions .assignment [partition ].last_offset_from_record_batch :
697
700
next_offset_from_batch_header = self ._subscriptions .assignment [partition ].last_offset_from_record_batch + 1
698
- if next_offset_from_batch_header > self ._subscriptions .assignment [partition ].position :
701
+ if next_offset_from_batch_header > self ._subscriptions .assignment [partition ].position . offset :
699
702
log .debug (
700
703
"Advance position for partition %s from %s to %s (last record batch location plus one)"
701
704
" to correct for deleted compacted messages and/or transactional control records" ,
702
- partition , self ._subscriptions .assignment [partition ].position , next_offset_from_batch_header )
703
- self ._subscriptions .assignment [partition ].position = next_offset_from_batch_header
705
+ partition , self ._subscriptions .assignment [partition ].position . offset , next_offset_from_batch_header )
706
+ self ._subscriptions .assignment [partition ].position = OffsetAndMetadata ( next_offset_from_batch_header , b'' , - 1 )
704
707
705
708
position = self ._subscriptions .assignment [partition ].position
706
709
@@ -718,19 +721,28 @@ def _create_fetch_requests(self):
718
721
if version < 5 :
719
722
partition_info = (
720
723
partition .partition ,
721
- position ,
724
+ position . offset ,
722
725
self .config ['max_partition_fetch_bytes' ]
723
726
)
727
+ elif version <= 8 :
728
+ partition_info = (
729
+ partition .partition ,
730
+ position .offset ,
731
+ - 1 , # log_start_offset is used internally by brokers / replicas only
732
+ self .config ['max_partition_fetch_bytes' ],
733
+ )
724
734
else :
725
735
partition_info = (
726
736
partition .partition ,
727
- position ,
737
+ position .leader_epoch ,
738
+ position .offset ,
728
739
- 1 , # log_start_offset is used internally by brokers / replicas only
729
740
self .config ['max_partition_fetch_bytes' ],
730
741
)
742
+
731
743
fetchable [node_id ][partition ] = partition_info
732
744
log .debug ("Adding fetch request for partition %s at offset %d" ,
733
- partition , position )
745
+ partition , position . offset )
734
746
735
747
requests = {}
736
748
for node_id , next_partitions in six .iteritems (fetchable ):
@@ -778,7 +790,10 @@ def _create_fetch_requests(self):
778
790
779
791
fetch_offsets = {}
780
792
for tp , partition_data in six .iteritems (next_partitions ):
781
- offset = partition_data [1 ]
793
+ if version <= 8 :
794
+ offset = partition_data [1 ]
795
+ else :
796
+ offset = partition_data [2 ]
782
797
fetch_offsets [tp ] = offset
783
798
784
799
requests [node_id ] = (request , fetch_offsets )
@@ -807,7 +822,7 @@ def _handle_fetch_response(self, node_id, fetch_offsets, send_time, response):
807
822
tp = TopicPartition (topic , partition_data [0 ])
808
823
fetch_offset = fetch_offsets [tp ]
809
824
completed_fetch = CompletedFetch (
810
- tp , fetch_offsets [ tp ] ,
825
+ tp , fetch_offset ,
811
826
response .API_VERSION ,
812
827
partition_data [1 :],
813
828
metric_aggregator
@@ -847,18 +862,18 @@ def _parse_fetched_data(self, completed_fetch):
847
862
# Note that the *response* may return a messageset that starts
848
863
# earlier (e.g., compressed messages) or later (e.g., compacted topic)
849
864
position = self ._subscriptions .assignment [tp ].position
850
- if position is None or position != fetch_offset :
865
+ if position is None or position . offset != fetch_offset :
851
866
log .debug ("Discarding fetch response for partition %s"
852
867
" since its offset %d does not match the"
853
868
" expected offset %d" , tp , fetch_offset ,
854
- position )
869
+ position . offset )
855
870
return None
856
871
857
872
records = MemoryRecords (completed_fetch .partition_data [- 1 ])
858
873
if records .has_next ():
859
874
log .debug ("Adding fetched record for partition %s with"
860
875
" offset %d to buffered record list" , tp ,
861
- position )
876
+ position . offset )
862
877
unpacked = list (self ._unpack_records (tp , records ))
863
878
parsed_records = self .PartitionRecords (fetch_offset , tp , unpacked )
864
879
if unpacked :
@@ -889,10 +904,10 @@ def _parse_fetched_data(self, completed_fetch):
889
904
self ._client .cluster .request_update ()
890
905
elif error_type is Errors .OffsetOutOfRangeError :
891
906
position = self ._subscriptions .assignment [tp ].position
892
- if position is None or position != fetch_offset :
907
+ if position is None or position . offset != fetch_offset :
893
908
log .debug ("Discarding stale fetch response for partition %s"
894
909
" since the fetched offset %d does not match the"
895
- " current offset %d" , tp , fetch_offset , position )
910
+ " current offset %d" , tp , fetch_offset , position . offset )
896
911
elif self ._subscriptions .has_default_offset_reset_policy ():
897
912
log .info ("Fetch offset %s is out of range for topic-partition %s" , fetch_offset , tp )
898
913
self ._subscriptions .need_offset_reset (tp )
0 commit comments