19
19
from kafka .producer .future import FutureRecordMetadata , FutureProduceResult
20
20
from kafka .producer .record_accumulator import AtomicInteger , RecordAccumulator
21
21
from kafka .producer .sender import Sender
22
- from kafka .producer .transaction_state import TransactionState
22
+ from kafka .producer .transaction_manager import TransactionManager
23
23
from kafka .record .default_records import DefaultRecordBatchBuilder
24
24
from kafka .record .legacy_records import LegacyRecordBatchBuilder
25
25
from kafka .serializer import Serializer
@@ -318,6 +318,8 @@ class KafkaProducer(object):
318
318
'key_serializer' : None ,
319
319
'value_serializer' : None ,
320
320
'enable_idempotence' : False ,
321
+ 'transactional_id' : None ,
322
+ 'transaction_timeout_ms' : 60000 ,
321
323
'acks' : 1 ,
322
324
'bootstrap_topics_filter' : set (),
323
325
'compression_type' : None ,
@@ -444,9 +446,30 @@ def __init__(self, **configs):
444
446
assert checker (), "Libraries for {} compression codec not found" .format (ct )
445
447
self .config ['compression_attrs' ] = compression_attrs
446
448
447
- self ._transaction_state = None
449
+ self ._metadata = client .cluster
450
+ self ._transaction_manager = None
451
+ self ._init_transactions_result = None
452
+ if 'enable_idempotence' in user_provided_configs and not self .config ['enable_idempotence' ] and self .config ['transactional_id' ]:
453
+ raise Errors .KafkaConfigurationError ("Cannot set transactional_id without enable_idempotence." )
454
+
455
+ if self .config ['transactional_id' ]:
456
+ self .config ['enable_idempotence' ] = True
457
+
448
458
if self .config ['enable_idempotence' ]:
449
- self ._transaction_state = TransactionState ()
459
+ assert self .config ['api_version' ] >= (0 , 11 ), "Transactional/Idempotent producer requires >= Kafka 0.11 Brokers"
460
+
461
+ self ._transaction_manager = TransactionManager (
462
+ transactional_id = self .config ['transactional_id' ],
463
+ transaction_timeout_ms = self .config ['transaction_timeout_ms' ],
464
+ retry_backoff_ms = self .config ['retry_backoff_ms' ],
465
+ api_version = self .config ['api_version' ],
466
+ metadata = self ._metadata ,
467
+ )
468
+ if self ._transaction_manager .is_transactional ():
469
+ log .info ("Instantiated a transactional producer." )
470
+ else :
471
+ log .info ("Instantiated an idempotent producer." )
472
+
450
473
if 'retries' not in user_provided_configs :
451
474
log .info ("Overriding the default 'retries' config to 3 since the idempotent producer is enabled." )
452
475
self .config ['retries' ] = 3
@@ -470,15 +493,14 @@ def __init__(self, **configs):
470
493
471
494
message_version = self .max_usable_produce_magic (self .config ['api_version' ])
472
495
self ._accumulator = RecordAccumulator (
473
- transaction_state = self ._transaction_state ,
496
+ transaction_manager = self ._transaction_manager ,
474
497
message_version = message_version ,
475
498
** self .config )
476
- self ._metadata = client .cluster
477
499
guarantee_message_order = bool (self .config ['max_in_flight_requests_per_connection' ] == 1 )
478
500
self ._sender = Sender (client , self ._metadata ,
479
501
self ._accumulator ,
480
502
metrics = self ._metrics ,
481
- transaction_state = self ._transaction_state ,
503
+ transaction_manager = self ._transaction_manager ,
482
504
guarantee_message_order = guarantee_message_order ,
483
505
** self .config )
484
506
self ._sender .daemon = True
@@ -610,6 +632,84 @@ def _estimate_size_in_bytes(self, key, value, headers=[]):
610
632
return LegacyRecordBatchBuilder .estimate_size_in_bytes (
611
633
magic , self .config ['compression_type' ], key , value )
612
634
635
+ def init_transactions (self ):
636
+ """
637
+ Needs to be called before any other methods when the transactional.id is set in the configuration.
638
+
639
+ This method does the following:
640
+ 1. Ensures any transactions initiated by previous instances of the producer with the same
641
+ transactional_id are completed. If the previous instance had failed with a transaction in
642
+ progress, it will be aborted. If the last transaction had begun completion,
643
+ but not yet finished, this method awaits its completion.
644
+ 2. Gets the internal producer id and epoch, used in all future transactional
645
+ messages issued by the producer.
646
+
647
+ Note that this method will raise KafkaTimeoutError if the transactional state cannot
648
+ be initialized before expiration of `max_block_ms`.
649
+
650
+ Retrying after a KafkaTimeoutError will continue to wait for the prior request to succeed or fail.
651
+ Retrying after any other exception will start a new initialization attempt.
652
+ Retrying after a successful initialization will do nothing.
653
+
654
+ Raises:
655
+ IllegalStateError: if no transactional_id has been configured
656
+ AuthorizationError: fatal error indicating that the configured
657
+ transactional_id is not authorized.
658
+ KafkaError: if the producer has encountered a previous fatal error or for any other unexpected error
659
+ KafkaTimeoutError: if the time taken for initialize the transaction has surpassed `max.block.ms`.
660
+ """
661
+ if not self ._transaction_manager :
662
+ raise Errors .IllegalStateError ("Cannot call init_transactions without setting a transactional_id." )
663
+ if self ._init_transactions_result is None :
664
+ self ._init_transactions_result = self ._transaction_manager .initialize_transactions ()
665
+ self ._sender .wakeup ()
666
+
667
+ try :
668
+ if not self ._init_transactions_result .wait (timeout_ms = self .config ['max_block_ms' ]):
669
+ raise Errors .KafkaTimeoutError ("Timeout expired while initializing transactional state in %s ms." % (self .config ['max_block_ms' ],))
670
+ finally :
671
+ if self ._init_transactions_result .failed :
672
+ self ._init_transactions_result = None
673
+
674
+ def begin_transaction (self ):
675
+ """ Should be called before the start of each new transaction.
676
+
677
+ Note that prior to the first invocation of this method,
678
+ you must invoke `init_transactions()` exactly one time.
679
+
680
+ Raises:
681
+ ProducerFencedError if another producer is with the same
682
+ transactional_id is active.
683
+ """
684
+ # Set the transactional bit in the producer.
685
+ if not self ._transaction_manager :
686
+ raise Errors .IllegalStateError ("Cannot use transactional methods without enabling transactions" )
687
+ self ._transaction_manager .begin_transaction ()
688
+
689
+ def commit_transaction (self ):
690
+ """ Commits the ongoing transaction.
691
+
692
+ Raises: ProducerFencedError if another producer with the same
693
+ transactional_id is active.
694
+ """
695
+ if not self ._transaction_manager :
696
+ raise Errors .IllegalStateError ("Cannot commit transaction since transactions are not enabled" )
697
+ result = self ._transaction_manager .begin_commit ()
698
+ self ._sender .wakeup ()
699
+ result .wait ()
700
+
701
+ def abort_transaction (self ):
702
+ """ Aborts the ongoing transaction.
703
+
704
+ Raises: ProducerFencedError if another producer with the same
705
+ transactional_id is active.
706
+ """
707
+ if not self ._transaction_manager :
708
+ raise Errors .IllegalStateError ("Cannot abort transaction since transactions are not enabled." )
709
+ result = self ._transaction_manager .begin_abort ()
710
+ self ._sender .wakeup ()
711
+ result .wait ()
712
+
613
713
def send (self , topic , value = None , key = None , headers = None , partition = None , timestamp_ms = None ):
614
714
"""Publish a message to a topic.
615
715
@@ -687,6 +787,10 @@ def send(self, topic, value=None, key=None, headers=None, partition=None, timest
687
787
688
788
tp = TopicPartition (topic , partition )
689
789
log .debug ("Sending (key=%r value=%r headers=%r) to %s" , key , value , headers , tp )
790
+
791
+ if self ._transaction_manager and self ._transaction_manager .is_transactional ():
792
+ self ._transaction_manager .maybe_add_partition_to_transaction (tp )
793
+
690
794
result = self ._accumulator .append (tp , timestamp_ms ,
691
795
key_bytes , value_bytes , headers )
692
796
future , batch_is_full , new_batch_created = result
0 commit comments