class TxAdminTest(RedpandaTest): topics = (TopicSpec(name="tx_test", partition_count=3, replication_factor=3), ) def __init__(self, test_context): super(TxAdminTest, self).__init__(test_context=test_context, num_brokers=3, extra_rp_conf={ "enable_idempotence": True, "enable_transactions": True, "tx_timeout_delay_ms": 10000000, "abort_timed_out_transactions_interval_ms": 10000000, 'enable_leader_balancer': False }) self.admin = Admin(self.redpanda) def extract_pid(self, tx): return (tx["producer_id"]["id"], tx["producer_id"]["epoch"]) @cluster(num_nodes=3) def test_simple_get_transaction(self): producer1 = ck.Producer({ 'bootstrap.servers': self.redpanda.brokers(), 'transactional.id': '0', }) producer2 = ck.Producer({ 'bootstrap.servers': self.redpanda.brokers(), 'transactional.id': '1', }) producer1.init_transactions() producer2.init_transactions() producer1.begin_transaction() producer2.begin_transaction() for topic in self.topics: for partition in range(topic.partition_count): producer1.produce(topic.name, '0', '0', partition) producer2.produce(topic.name, '0', '1', partition) producer1.flush() producer2.flush() expected_pids = None for topic in self.topics: for partition in range(topic.partition_count): txs_info = self.admin.get_transactions(topic.name, partition, "kafka") assert ('expired_transactions' not in txs_info) if expected_pids == None: expected_pids = set( map(self.extract_pid, txs_info['active_transactions'])) assert (len(expected_pids) == 2) assert (len(expected_pids) == len( txs_info['active_transactions'])) for tx in txs_info['active_transactions']: assert (self.extract_pid(tx) in expected_pids) assert (tx['status'] == 'ongoing') assert (tx['timeout_ms'] == 60000) @cluster(num_nodes=3) def test_expired_transaction(self): ''' Problem: rm_stm contains timer to run try_abort_old_txs. This timer rearm on begin_tx to smallest transaction deadline, and after try_abort_old_txs to one of settings abort_timed_out_transactions_interval_ms or tx_timeout_delay_ms. If we do sleep to transaction timeout and try to get expired transaction, timer can be signal before our request and after clean expire transactions. How to solve: 0) Run transaction 1) Change leader for parititons (New leader did not get requests for begin_txs, so his timer is armed on one of settings) 2) Get expired transactions ''' assert (len(self.redpanda.nodes) >= 2) producer1 = ck.Producer({ 'bootstrap.servers': self.redpanda.brokers(), 'transactional.id': '0', 'transaction.timeout.ms': '30000' }) producer1.init_transactions() producer1.begin_transaction() for topic in self.topics: for partition in range(topic.partition_count): producer1.produce(topic.name, '0', '0', partition) producer1.flush() # We need to change leader for all partition to another node for topic in self.topics: for partition in range(topic.partition_count): old_leader = self.admin.get_partition_leader( namespace="kafka", topic=self.topic, partition=partition) self.admin.transfer_leadership_to(namespace="kafka", topic=self.topic, partition=partition, target=None) def leader_is_changed(): return self.admin.get_partition_leader( namespace="kafka", topic=self.topic, partition=partition) != old_leader wait_until(leader_is_changed, timeout_sec=30, backoff_sec=2, err_msg="Failed to establish current leader") expected_pids = None for topic in self.topics: for partition in range(topic.partition_count): txs_info = self.admin.get_transactions(topic.name, partition, "kafka") assert ('active_transactions' not in txs_info) if expected_pids == None: expected_pids = set( map(self.extract_pid, txs_info['expired_transactions'])) assert (len(expected_pids) == 1) assert (len(expected_pids) == len( txs_info['expired_transactions'])) for tx in txs_info['expired_transactions']: assert (self.extract_pid(tx) in expected_pids) assert (tx['status'] == 'ongoing') assert (tx['timeout_ms'] == -1) @cluster(num_nodes=3) def test_mark_transaction_expired(self): producer1 = ck.Producer({ 'bootstrap.servers': self.redpanda.brokers(), 'transactional.id': '0', }) producer2 = ck.Producer({ 'bootstrap.servers': self.redpanda.brokers(), 'transactional.id': '1', }) producer1.init_transactions() producer2.init_transactions() producer1.begin_transaction() producer2.begin_transaction() for topic in self.topics: for partition in range(topic.partition_count): producer1.produce(topic.name, '0', '0', partition) producer2.produce(topic.name, '0', '1', partition) producer1.flush() producer2.flush() expected_pids = None txs_info = self.admin.get_transactions(topic.name, partition, "kafka") expected_pids = set( map(self.extract_pid, txs_info['active_transactions'])) assert (len(expected_pids) == 2) abort_tx = list(expected_pids)[0] expected_pids.discard(abort_tx) for topic in self.topics: for partition in range(topic.partition_count): self.admin.mark_transaction_expired(topic.name, partition, { "id": abort_tx[0], "epoch": abort_tx[1] }, "kafka") txs_info = self.admin.get_transactions(topic.name, partition, "kafka") assert ('expired_transactions' not in txs_info) assert (len(expected_pids) == len( txs_info['active_transactions'])) for tx in txs_info['active_transactions']: assert (self.extract_pid(tx) in expected_pids) assert (tx['status'] == 'ongoing') assert (tx['timeout_ms'] == 60000)
class TxAdminTest(RedpandaTest): topics = (TopicSpec(partition_count=3, replication_factor=3), TopicSpec(partition_count=3, replication_factor=3)) def __init__(self, test_context): super(TxAdminTest, self).__init__(test_context=test_context, num_brokers=3, extra_rp_conf={ "enable_idempotence": True, "enable_transactions": True, "tx_timeout_delay_ms": 10000000, "abort_timed_out_transactions_interval_ms": 10000000, 'enable_leader_balancer': False }) self.admin = Admin(self.redpanda) def extract_pid(self, tx): return (tx["producer_id"]["id"], tx["producer_id"]["epoch"]) @cluster(num_nodes=3) def test_simple_get_transaction(self): producer1 = ck.Producer({ 'bootstrap.servers': self.redpanda.brokers(), 'transactional.id': '0', }) producer2 = ck.Producer({ 'bootstrap.servers': self.redpanda.brokers(), 'transactional.id': '1', }) producer1.init_transactions() producer2.init_transactions() producer1.begin_transaction() producer2.begin_transaction() for topic in self.topics: for partition in range(topic.partition_count): producer1.produce(topic.name, '0', '0', partition) producer2.produce(topic.name, '0', '1', partition) producer1.flush() producer2.flush() expected_pids = None for topic in self.topics: for partition in range(topic.partition_count): txs_info = self.admin.get_transactions(topic.name, partition, "kafka") assert ('expired_transactions' not in txs_info) if expected_pids == None: expected_pids = set( map(self.extract_pid, txs_info['active_transactions'])) assert (len(expected_pids) == 2) assert (len(expected_pids) == len( txs_info['active_transactions'])) for tx in txs_info['active_transactions']: assert (self.extract_pid(tx) in expected_pids) assert (tx['status'] == 'ongoing') assert (tx['timeout_ms'] == 60000) @cluster(num_nodes=3) def test_expired_transaction(self): ''' Problem: rm_stm contains timer to run try_abort_old_txs. This timer rearm on begin_tx to smallest transaction deadline, and after try_abort_old_txs to one of settings abort_timed_out_transactions_interval_ms or tx_timeout_delay_ms. If we do sleep to transaction timeout and try to get expired transaction, timer can be signal before our request and after clean expire transactions. How to solve: 0) Run transaction 1) Change leader for parititons (New leader did not get requests for begin_txs, so his timer is armed on one of settings) 2) Get expired transactions ''' assert (len(self.redpanda.nodes) >= 2) producer1 = ck.Producer({ 'bootstrap.servers': self.redpanda.brokers(), 'transactional.id': '0', 'transaction.timeout.ms': '900000' }) producer1.init_transactions() producer1.begin_transaction() for topic in self.topics: for partition in range(topic.partition_count): producer1.produce(topic.name, '0', '0', partition) producer1.flush() # We need to change leader for all partition to another node for topic in self.topics: for partition in range(topic.partition_count): old_leader = self.admin.get_partition_leader( namespace="kafka", topic=topic, partition=partition) self.admin.transfer_leadership_to(namespace="kafka", topic=topic, partition=partition, target=None) def leader_is_changed(): new_leader = self.admin.get_partition_leader( namespace="kafka", topic=topic, partition=partition) return (new_leader != -1) and (new_leader != old_leader) wait_until(leader_is_changed, timeout_sec=30, backoff_sec=2, err_msg="Failed to establish current leader") expected_pids = None for topic in self.topics: for partition in range(topic.partition_count): txs_info = self.admin.get_transactions(topic.name, partition, "kafka") assert ('active_transactions' not in txs_info) if expected_pids == None: expected_pids = set( map(self.extract_pid, txs_info['expired_transactions'])) assert (len(expected_pids) == 1) assert (len(expected_pids) == len( txs_info['expired_transactions'])) for tx in txs_info['expired_transactions']: assert (self.extract_pid(tx) in expected_pids) assert (tx['status'] == 'ongoing') assert (tx['timeout_ms'] == -1) @cluster(num_nodes=3) def test_mark_transaction_expired(self): producer1 = ck.Producer({ 'bootstrap.servers': self.redpanda.brokers(), 'transactional.id': '0', }) producer2 = ck.Producer({ 'bootstrap.servers': self.redpanda.brokers(), 'transactional.id': '1', }) producer1.init_transactions() producer2.init_transactions() producer1.begin_transaction() producer2.begin_transaction() for topic in self.topics: for partition in range(topic.partition_count): producer1.produce(topic.name, '0', '0', partition) producer2.produce(topic.name, '0', '1', partition) producer1.flush() producer2.flush() expected_pids = None txs_info = self.admin.get_transactions(topic.name, partition, "kafka") expected_pids = set( map(self.extract_pid, txs_info['active_transactions'])) assert (len(expected_pids) == 2) abort_tx = list(expected_pids)[0] expected_pids.discard(abort_tx) for topic in self.topics: for partition in range(topic.partition_count): self.admin.mark_transaction_expired(topic.name, partition, { "id": abort_tx[0], "epoch": abort_tx[1] }, "kafka") txs_info = self.admin.get_transactions(topic.name, partition, "kafka") assert ('expired_transactions' not in txs_info) assert (len(expected_pids) == len( txs_info['active_transactions'])) for tx in txs_info['active_transactions']: assert (self.extract_pid(tx) in expected_pids) assert (tx['status'] == 'ongoing') assert (tx['timeout_ms'] == 60000) @cluster(num_nodes=3) def test_all_transactions(self): tx_id = "0" producer = ck.Producer({ 'bootstrap.servers': self.redpanda.brokers(), 'transactional.id': tx_id, }) producer.init_transactions() producer.begin_transaction() for topic in self.topics: for partition in range(topic.partition_count): producer.produce(topic.name, '0', '0', partition) producer.flush() txs_info = self.admin.get_all_transactions() assert len(txs_info) == 1 expected_partitions = dict() tx = txs_info[0] assert tx["transactional_id"] == tx_id assert tx["timeout_ms"] == 60000 for partition in tx["partitions"]: assert partition["ns"] == "kafka" if partition["topic"] not in expected_partitions: expected_partitions[partition["topic"]] = set() expected_partitions[partition["topic"]].add( partition["partition_id"]) for topic in self.topics: assert len( expected_partitions[topic.name]) == topic.partition_count for partition in range(topic.partition_count): assert partition in expected_partitions[topic.name] @cluster(num_nodes=3) def test_delete_topic_from_ongoin_tx(self): tx_id = "0" producer = ck.Producer({ 'bootstrap.servers': self.redpanda.brokers(), 'transactional.id': tx_id, }) producer.init_transactions() producer.begin_transaction() for topic in self.topics: for partition in range(topic.partition_count): producer.produce(topic.name, '0', '0', partition) producer.flush() txs_info = self.admin.get_all_transactions() assert len( txs_info) == 1, "Should be only one transaction in current time" rpk = RpkTool(self.redpanda) topic_name = self.topics[0].name rpk.delete_topic(topic_name) tx = txs_info[0] assert tx[ "transactional_id"] == tx_id, f"Expected transactional_id: {tx_id}, but got {tx['transactional_id']}" for partition in tx["partitions"]: assert (partition["ns"] == "kafka") if partition["topic"] == topic_name: self.admin.delete_partition_from_transaction( tx["transactional_id"], partition["ns"], partition["topic"], partition["partition_id"], partition["etag"]) producer.commit_transaction() producer = ck.Producer({ 'bootstrap.servers': self.redpanda.brokers(), 'transactional.id': tx_id, }) producer.init_transactions() producer.begin_transaction() for topic in self.topics: if topic.name is not topic_name: for partition in range(topic.partition_count): producer.produce(topic.name, '0', '0', partition) producer.commit_transaction() @cluster(num_nodes=3) def test_delete_topic_from_prepared_tx(self): tx_id = "0" producer = ck.Producer({ 'bootstrap.servers': self.redpanda.brokers(), 'transactional.id': tx_id, }) producer.init_transactions() producer.begin_transaction() for topic in self.topics: for partition in range(topic.partition_count): producer.produce(topic.name, '0', '0', partition) producer.flush() rpk = RpkTool(self.redpanda) topic_name = self.topics[0].name rpk.delete_topic(topic_name) try: producer.commit_transaction() raise Exception("commit_transaction should fail") except ck.cimpl.KafkaException as e: kafka_error = e.args[0] assert kafka_error.code() == ck.cimpl.KafkaError.UNKNOWN producer = ck.Producer({ 'bootstrap.servers': self.redpanda.brokers(), 'transactional.id': tx_id, }) try: producer.init_transactions() raise Exception("init_transaction should fail") except ck.cimpl.KafkaException as e: kafka_error = e.args[0] assert kafka_error.code( ) == ck.cimpl.KafkaError.BROKER_NOT_AVAILABLE txs_info = self.admin.get_all_transactions() assert len( txs_info) == 1, "Should be only one transaction in current time" tx = txs_info[0] for partition in tx["partitions"]: assert (partition["ns"] == "kafka") if partition["topic"] == topic_name: self.admin.delete_partition_from_transaction( tx["transactional_id"], partition["ns"], partition["topic"], partition["partition_id"], partition["etag"]) producer = ck.Producer({ 'bootstrap.servers': self.redpanda.brokers(), 'transactional.id': tx_id, }) producer.init_transactions() producer.begin_transaction() for topic in self.topics: if topic.name is not topic_name: for partition in range(topic.partition_count): producer.produce(topic.name, '0', '0', partition) producer.commit_transaction() @cluster(num_nodes=3) def test_delete_non_existent_topic(self): tx_id = "0" producer = ck.Producer({ 'bootstrap.servers': self.redpanda.brokers(), 'transactional.id': tx_id, }) producer.init_transactions() producer.begin_transaction() error_topic_name = "error_topic" for topic in self.topics: assert error_topic_name is not topic.name for partition in range(topic.partition_count): producer.produce(topic.name, '0', '0', partition) producer.flush() try: self.admin.delete_partition_from_transaction( tx_id, "kafka", error_topic_name, 0, 0) except requests.exceptions.HTTPError as e: assert e.response.text == '{"message": "Can not find partition({kafka/error_topic/0}) in transaction for delete", "code": 400}' producer.commit_transaction() @cluster(num_nodes=3) def test_delete_non_existent_tid(self): tx_id = "0" producer = ck.Producer({ 'bootstrap.servers': self.redpanda.brokers(), 'transactional.id': tx_id, }) producer.init_transactions() producer.begin_transaction() for topic in self.topics: for partition in range(topic.partition_count): producer.produce(topic.name, '0', '0', partition) producer.flush() txs_info = self.admin.get_all_transactions() tx = txs_info[0] topic_name = self.topics[0].name error_tx_id = "1" for partition in tx["partitions"]: if partition["topic"] == topic_name: try: self.admin.delete_partition_from_transaction( error_tx_id, partition["ns"], partition["topic"], partition["partition_id"], partition["etag"]) except requests.exceptions.HTTPError as e: assert e.response.text == '{"message": "Unexpected tx_error error: Unknown server error", "code": 500}' producer.commit_transaction() @cluster(num_nodes=3) def test_delete_non_existent_etag(self): tx_id = "0" producer = ck.Producer({ 'bootstrap.servers': self.redpanda.brokers(), 'transactional.id': tx_id, }) producer.init_transactions() producer.begin_transaction() for topic in self.topics: for partition in range(topic.partition_count): producer.produce(topic.name, '0', '0', partition) producer.flush() txs_info = self.admin.get_all_transactions() tx = txs_info[0] topic_name = self.topics[0].name for partition in tx["partitions"]: if partition["topic"] == topic_name: try: self.admin.delete_partition_from_transaction( tx_id, partition["ns"], partition["topic"], partition["partition_id"], partition["etag"] + 100) except requests.exceptions.HTTPError as e: e.response.text == '{{"message": "Can not find partition({{{}/{}/{}}}) in transaction for delete", "code": 400}}'.format( partition["ns"], partition["topic"], partition["partition_id"]) producer.commit_transaction()