1212import kafka .errors as Errors
1313from kafka .future import Future
1414from kafka .metrics .stats import Avg , Count , Max , Rate
15- from kafka .protocol .fetch import FetchRequest
15+ from kafka .protocol .fetch import FetchRequest , AbortedTransaction
1616from kafka .protocol .list_offsets import (
1717 ListOffsetsRequest , OffsetResetStrategy , UNKNOWN_OFFSET
1818)
2828READ_UNCOMMITTED = 0
2929READ_COMMITTED = 1
3030
31+ ISOLATION_LEVEL_CONFIG = {
32+ 'read_uncommitted' : READ_UNCOMMITTED ,
33+ 'read_committed' : READ_COMMITTED ,
34+ }
35+
3136ConsumerRecord = collections .namedtuple ("ConsumerRecord" ,
3237 ["topic" , "partition" , "leader_epoch" , "offset" , "timestamp" , "timestamp_type" ,
3338 "key" , "value" , "headers" , "checksum" , "serialized_key_size" , "serialized_value_size" , "serialized_header_size" ])
@@ -60,6 +65,7 @@ class Fetcher(six.Iterator):
6065 'metric_group_prefix' : 'consumer' ,
6166 'retry_backoff_ms' : 100 ,
6267 'enable_incremental_fetch_sessions' : True ,
68+ 'isolation_level' : 'read_uncommitted' ,
6369 }
6470
6571 def __init__ (self , client , subscriptions , ** configs ):
@@ -100,12 +106,18 @@ def __init__(self, client, subscriptions, **configs):
100106 consumed. This ensures no on-the-wire or on-disk corruption to
101107 the messages occurred. This check adds some overhead, so it may
102108 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'
103112 """
104113 self .config = copy .copy (self .DEFAULT_CONFIG )
105114 for key in self .config :
106115 if key in configs :
107116 self .config [key ] = configs [key ]
108117
118+ if self .config ['isolation_level' ] not in ISOLATION_LEVEL_CONFIG :
119+ raise Errors .KafkaConfigurationError ('Unrecognized isolation_level' )
120+
109121 self ._client = client
110122 self ._subscriptions = subscriptions
111123 self ._completed_fetches = collections .deque () # Unparsed responses
@@ -116,7 +128,7 @@ def __init__(self, client, subscriptions, **configs):
116128 self ._sensors = FetchManagerMetrics (self .config ['metrics' ], self .config ['metric_group_prefix' ])
117129 else :
118130 self ._sensors = None
119- self ._isolation_level = READ_UNCOMMITTED
131+ self ._isolation_level = ISOLATION_LEVEL_CONFIG [ self . config [ 'isolation_level' ]]
120132 self ._session_handlers = {}
121133 self ._nodes_with_pending_fetch_requests = set ()
122134
@@ -244,7 +256,7 @@ def _reset_offset(self, partition, timeout_ms=None):
244256 else :
245257 raise NoOffsetForPartitionError (partition )
246258
247- log .debug ("Resetting offset for partition %s to %s offset ." ,
259+ log .debug ("Resetting offset for partition %s to offset %s ." ,
248260 partition , strategy )
249261 offsets = self ._retrieve_offsets ({partition : timestamp }, timeout_ms = timeout_ms )
250262
@@ -765,14 +777,21 @@ def _parse_fetched_data(self, completed_fetch):
765777 return None
766778
767779 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 ]
768785 log .debug ("Preparing to read %s bytes of data for partition %s with offset %d" ,
769786 records .size_in_bytes (), tp , fetch_offset )
770787 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 )
776795 if not records .has_next () and records .size_in_bytes () > 0 :
777796 if completed_fetch .response_version < 3 :
778797 # Implement the pre KIP-74 behavior of throwing a RecordTooLargeException.
@@ -845,13 +864,23 @@ def close(self):
845864 self ._next_partition_records .drain ()
846865
847866 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 ):
849872 self .fetch_offset = fetch_offset
850873 self .topic_partition = tp
851874 self .leader_epoch = - 1
852875 self .next_fetch_offset = fetch_offset
853876 self .bytes_read = 0
854877 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+ )
855884 self .metric_aggregator = metric_aggregator
856885 self .check_crcs = check_crcs
857886 self .record_iterator = itertools .dropwhile (
@@ -900,18 +929,35 @@ def _unpack_records(self, tp, records, key_deserializer, value_deserializer):
900929 "Record batch for partition %s at offset %s failed crc check" % (
901930 self .topic_partition , batch .base_offset ))
902931
932+
903933 # 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
905935 if batch .magic == 2 :
906936 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+
907957 # 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.
913959 if batch .is_control_batch :
914- self .next_fetch_offset = next ( batch ). offset + 1
960+ self .next_fetch_offset = batch . next_offset
915961 batch = records .next_batch ()
916962 continue
917963
@@ -944,7 +990,7 @@ def _unpack_records(self, tp, records, key_deserializer, value_deserializer):
944990 # unnecessary re-fetching of the same batch (in the worst case, the consumer could get stuck
945991 # fetching the same batch repeatedly).
946992 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
948994 self .drain ()
949995
950996 # If unpacking raises StopIteration, it is erroneously
@@ -961,6 +1007,24 @@ def _deserialize(self, f, topic, bytes_):
9611007 return f .deserialize (topic , bytes_ )
9621008 return f (bytes_ )
9631009
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+
9641028
9651029class FetchSessionHandler (object ):
9661030 """
0 commit comments