12
12
import kafka .errors as Errors
13
13
from kafka .future import Future
14
14
from kafka .metrics .stats import Avg , Count , Max , Rate
15
- from kafka .protocol .fetch import FetchRequest
15
+ from kafka .protocol .fetch import FetchRequest , AbortedTransaction
16
16
from kafka .protocol .list_offsets import (
17
17
ListOffsetsRequest , OffsetResetStrategy , UNKNOWN_OFFSET
18
18
)
28
28
READ_UNCOMMITTED = 0
29
29
READ_COMMITTED = 1
30
30
31
+ ISOLATION_LEVEL_CONFIG = {
32
+ 'read_uncommitted' : READ_UNCOMMITTED ,
33
+ 'read_committed' : READ_COMMITTED ,
34
+ }
35
+
31
36
ConsumerRecord = collections .namedtuple ("ConsumerRecord" ,
32
37
["topic" , "partition" , "leader_epoch" , "offset" , "timestamp" , "timestamp_type" ,
33
38
"key" , "value" , "headers" , "checksum" , "serialized_key_size" , "serialized_value_size" , "serialized_header_size" ])
@@ -60,6 +65,7 @@ class Fetcher(six.Iterator):
60
65
'metric_group_prefix' : 'consumer' ,
61
66
'retry_backoff_ms' : 100 ,
62
67
'enable_incremental_fetch_sessions' : True ,
68
+ 'isolation_level' : 'read_uncommitted' ,
63
69
}
64
70
65
71
def __init__ (self , client , subscriptions , ** configs ):
@@ -100,12 +106,18 @@ def __init__(self, client, subscriptions, **configs):
100
106
consumed. This ensures no on-the-wire or on-disk corruption to
101
107
the messages occurred. This check adds some overhead, so it may
102
108
be disabled in cases seeking extreme performance. Default: True
109
+ isolation_level (str): Configure KIP-98 transactional consumer by
110
+ setting to 'read_committed'. This will cause the consumer to
111
+ skip records from aborted tranactions. Default: 'read_uncommitted'
103
112
"""
104
113
self .config = copy .copy (self .DEFAULT_CONFIG )
105
114
for key in self .config :
106
115
if key in configs :
107
116
self .config [key ] = configs [key ]
108
117
118
+ if self .config ['isolation_level' ] not in ISOLATION_LEVEL_CONFIG :
119
+ raise Errors .KafkaConfigurationError ('Unrecognized isolation_level' )
120
+
109
121
self ._client = client
110
122
self ._subscriptions = subscriptions
111
123
self ._completed_fetches = collections .deque () # Unparsed responses
@@ -116,7 +128,7 @@ def __init__(self, client, subscriptions, **configs):
116
128
self ._sensors = FetchManagerMetrics (self .config ['metrics' ], self .config ['metric_group_prefix' ])
117
129
else :
118
130
self ._sensors = None
119
- self ._isolation_level = READ_UNCOMMITTED
131
+ self ._isolation_level = ISOLATION_LEVEL_CONFIG [ self . config [ 'isolation_level' ]]
120
132
self ._session_handlers = {}
121
133
self ._nodes_with_pending_fetch_requests = set ()
122
134
@@ -244,7 +256,7 @@ def _reset_offset(self, partition, timeout_ms=None):
244
256
else :
245
257
raise NoOffsetForPartitionError (partition )
246
258
247
- log .debug ("Resetting offset for partition %s to %s offset ." ,
259
+ log .debug ("Resetting offset for partition %s to offset %s ." ,
248
260
partition , strategy )
249
261
offsets = self ._retrieve_offsets ({partition : timestamp }, timeout_ms = timeout_ms )
250
262
@@ -765,14 +777,21 @@ def _parse_fetched_data(self, completed_fetch):
765
777
return None
766
778
767
779
records = MemoryRecords (completed_fetch .partition_data [- 1 ])
780
+ aborted_transactions = None
781
+ if completed_fetch .response_version >= 11 :
782
+ aborted_transactions = completed_fetch .partition_data [- 3 ]
783
+ elif completed_fetch .response_version >= 4 :
784
+ aborted_transactions = completed_fetch .partition_data [- 2 ]
768
785
log .debug ("Preparing to read %s bytes of data for partition %s with offset %d" ,
769
786
records .size_in_bytes (), tp , fetch_offset )
770
787
parsed_records = self .PartitionRecords (fetch_offset , tp , records ,
771
- self .config ['key_deserializer' ],
772
- self .config ['value_deserializer' ],
773
- self .config ['check_crcs' ],
774
- completed_fetch .metric_aggregator ,
775
- self ._on_partition_records_drain )
788
+ key_deserializer = self .config ['key_deserializer' ],
789
+ value_deserializer = self .config ['value_deserializer' ],
790
+ check_crcs = self .config ['check_crcs' ],
791
+ isolation_level = self ._isolation_level ,
792
+ aborted_transactions = aborted_transactions ,
793
+ metric_aggregator = completed_fetch .metric_aggregator ,
794
+ on_drain = self ._on_partition_records_drain )
776
795
if not records .has_next () and records .size_in_bytes () > 0 :
777
796
if completed_fetch .response_version < 3 :
778
797
# Implement the pre KIP-74 behavior of throwing a RecordTooLargeException.
@@ -845,13 +864,23 @@ def close(self):
845
864
self ._next_partition_records .drain ()
846
865
847
866
class PartitionRecords (object ):
848
- def __init__ (self , fetch_offset , tp , records , key_deserializer , value_deserializer , check_crcs , metric_aggregator , on_drain ):
867
+ def __init__ (self , fetch_offset , tp , records ,
868
+ key_deserializer = None , value_deserializer = None ,
869
+ check_crcs = True , isolation_level = READ_UNCOMMITTED ,
870
+ aborted_transactions = None , # raw data from response / list of (producer_id, first_offset) tuples
871
+ metric_aggregator = None , on_drain = lambda x : None ):
849
872
self .fetch_offset = fetch_offset
850
873
self .topic_partition = tp
851
874
self .leader_epoch = - 1
852
875
self .next_fetch_offset = fetch_offset
853
876
self .bytes_read = 0
854
877
self .records_read = 0
878
+ self .isolation_level = isolation_level
879
+ self .aborted_producer_ids = set ()
880
+ self .aborted_transactions = collections .deque (
881
+ sorted ([AbortedTransaction (* data ) for data in aborted_transactions ] if aborted_transactions else [],
882
+ key = lambda txn : txn .first_offset )
883
+ )
855
884
self .metric_aggregator = metric_aggregator
856
885
self .check_crcs = check_crcs
857
886
self .record_iterator = itertools .dropwhile (
@@ -900,18 +929,35 @@ def _unpack_records(self, tp, records, key_deserializer, value_deserializer):
900
929
"Record batch for partition %s at offset %s failed crc check" % (
901
930
self .topic_partition , batch .base_offset ))
902
931
932
+
903
933
# Try DefaultsRecordBatch / message log format v2
904
- # base_offset, last_offset_delta, and control batches
934
+ # base_offset, last_offset_delta, aborted transactions, and control batches
905
935
if batch .magic == 2 :
906
936
self .leader_epoch = batch .leader_epoch
937
+ if self .isolation_level == READ_COMMITTED and batch .has_producer_id ():
938
+ # remove from the aborted transaction queue all aborted transactions which have begun
939
+ # before the current batch's last offset and add the associated producerIds to the
940
+ # aborted producer set
941
+ self ._consume_aborted_transactions_up_to (batch .last_offset )
942
+
943
+ producer_id = batch .producer_id
944
+ if self ._contains_abort_marker (batch ):
945
+ try :
946
+ self .aborted_producer_ids .remove (producer_id )
947
+ except KeyError :
948
+ pass
949
+ elif self ._is_batch_aborted (batch ):
950
+ log .debug ("Skipping aborted record batch from partition %s with producer_id %s and"
951
+ " offsets %s to %s" ,
952
+ self .topic_partition , producer_id , batch .base_offset , batch .last_offset )
953
+ self .next_fetch_offset = batch .next_offset
954
+ batch = records .next_batch ()
955
+ continue
956
+
907
957
# Control batches have a single record indicating whether a transaction
908
- # was aborted or committed.
909
- # When isolation_level is READ_COMMITTED (currently unsupported)
910
- # we should also skip all messages from aborted transactions
911
- # For now we only support READ_UNCOMMITTED and so we ignore the
912
- # abort/commit signal.
958
+ # was aborted or committed. These are not returned to the consumer.
913
959
if batch .is_control_batch :
914
- self .next_fetch_offset = next ( batch ). offset + 1
960
+ self .next_fetch_offset = batch . next_offset
915
961
batch = records .next_batch ()
916
962
continue
917
963
@@ -944,7 +990,7 @@ def _unpack_records(self, tp, records, key_deserializer, value_deserializer):
944
990
# unnecessary re-fetching of the same batch (in the worst case, the consumer could get stuck
945
991
# fetching the same batch repeatedly).
946
992
if last_batch and last_batch .magic == 2 :
947
- self .next_fetch_offset = last_batch .base_offset + last_batch . last_offset_delta + 1
993
+ self .next_fetch_offset = last_batch .next_offset
948
994
self .drain ()
949
995
950
996
# If unpacking raises StopIteration, it is erroneously
@@ -961,6 +1007,24 @@ def _deserialize(self, f, topic, bytes_):
961
1007
return f .deserialize (topic , bytes_ )
962
1008
return f (bytes_ )
963
1009
1010
+ def _consume_aborted_transactions_up_to (self , offset ):
1011
+ if not self .aborted_transactions :
1012
+ return
1013
+
1014
+ while self .aborted_transactions and self .aborted_transactions [0 ].first_offset <= offset :
1015
+ self .aborted_producer_ids .add (self .aborted_transactions .popleft ().producer_id )
1016
+
1017
+ def _is_batch_aborted (self , batch ):
1018
+ return batch .is_transactional and batch .producer_id in self .aborted_producer_ids
1019
+
1020
+ def _contains_abort_marker (self , batch ):
1021
+ if not batch .is_control_batch :
1022
+ return False
1023
+ record = next (batch )
1024
+ if not record :
1025
+ return False
1026
+ return record .abort
1027
+
964
1028
965
1029
class FetchSessionHandler (object ):
966
1030
"""
0 commit comments