Repository: ydb-platform/ydb
Pull request #17141: [Kafka API] Add kafka transaction actor (part of kafka transactions epic)
Author: login: a-serebryanskiy
Labels: name: not-for-changelog
Created: 2025-04-12T15:53:34Z | Merged: 2025-04-22T15:53:29Z

### Changelog entry <!-- a user-readable short description of the changes that goes to CHANGELOG.md and Release Notes -->

Add kafka transaction actor (part of kafka transactions epic). This PR adds a new actor in kafka api - TKafkaTransactionActor. This new actor is responsible for handling of 4 requests:
- AddPartitionsToTxn
- AddOffsetToTxn
- TxnOffsetCommit
- EndTxn

### Changelog category <!-- remove all except one -->

* Not for changelog (changelog entry is not required)

### Description for reviewers <!-- (optional) description for those who read this PR -->

...



Diff:
diff --git a/ydb/core/kafka_proxy/actors/kafka_init_producer_id_actor.cpp b/ydb/core/kafka_proxy/actors/kafka_init_producer_id_actor.cpp
index 412b49d1257b..72c46ff3a18f 100644
--- a/ydb/core/kafka_proxy/actors/kafka_init_producer_id_actor.cpp
+++ b/ydb/core/kafka_proxy/actors/kafka_init_producer_id_actor.cpp
@@ -284,8 +284,10 @@ namespace NKafka {
 
         Send(NKafka::MakeKafkaTransactionsServiceID(), new TEvKafka::TEvSaveTxnProducerRequest(
             producerState.TransactionalId,
-            producerState.ProducerId,
-            producerState.ProducerEpoch
+            {
+                producerState.ProducerId, 
+                producerState.ProducerEpoch
+            }
         ));
     }
 
diff --git a/ydb/core/kafka_proxy/actors/kafka_transaction_actor.cpp b/ydb/core/kafka_proxy/actors/kafka_transaction_actor.cpp
new file mode 100644
index 000000000000..758874c63d97
--- /dev/null
+++ b/ydb/core/kafka_proxy/actors/kafka_transaction_actor.cpp
@@ -0,0 +1,434 @@
+#include "kafka_transaction_actor.h"
+#include "kafka_transaction_actor_sql.h"
+#include "txn_actor_response_builder.h"
+#include <ydb/core/kafka_proxy/kafka_transactions_coordinator.h>
+#include <ydb/core/kafka_proxy/kafka_transactional_producers_initializers.h>
+#include <ydb/core/kafka_proxy/kafka_consumer_groups_metadata_initializers.h>
+#include <ydb/core/kqp/common/simple/services.h>
+#include <util/generic/cast.h>
+#include <regex>
+
+#define VALIDATE_PRODUCER_IN_REQUEST(ErrorResponseType) \
+if (!ProducerInRequestIsValid(ev->Get()->Request)) { \
+    auto& kafkaRequest = ev->Get()->Request; \
+    TString message = TStringBuilder() << "Recieved invalid request. Got: " \
+            << "transactionalId=" << kafkaRequest->TransactionalId->c_str() \
+            << ", producerId=" << kafkaRequest->ProducerId \
+            << ", producerEpoch=" << kafkaRequest->ProducerEpoch; \
+    SendFailResponse<ErrorResponseType>(ev, EKafkaErrors::UNKNOWN_SERVER_ERROR, message); \
+    throw yexception() << message; \
+} \
+
+namespace NKafka {
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddPartitionsToTxnRequest::TPtr& ev, const TActorContext&){
+        KAFKA_LOG_D("Receieved ADD_PARTITIONS_TO_TXN request");
+        VALIDATE_PRODUCER_IN_REQUEST(TAddPartitionsToTxnResponseData);
+
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                PartitionsInTxn.emplace(GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest));
+            }
+        }
+        SendOkResponse<TAddPartitionsToTxnResponseData>(ev);
+    }
+
+    // Method does nothing. In Kafka it will add __consumer_offsets topic to transaction, but 
+    // in YDB Topics we store offsets in table and do not need this extra action.
+    // Thus we can just ignore this request.
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddOffsetsToTxnRequest::TPtr& ev, const TActorContext&) {
+        KAFKA_LOG_D("Receieved ADD_OFFSETS_TO_TXN request");
+        VALIDATE_PRODUCER_IN_REQUEST(TAddOffsetsToTxnResponseData);
+        SendOkResponse<TAddOffsetsToTxnResponseData>(ev);
+    }
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvTxnOffsetCommitRequest::TPtr& ev, const TActorContext&) {
+        KAFKA_LOG_D("Receieved TXN_OFFSET_COMMIT request");
+        VALIDATE_PRODUCER_IN_REQUEST(TTxnOffsetCommitResponseData);
+
+        // save offsets for future use
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                TTopicPartition topicPartition = {GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest.PartitionIndex)};
+                TPartitionCommit newCommit{
+                    .Partition = partitionInRequest.PartitionIndex,
+                    .Offset = partitionInRequest.CommittedOffset,
+                    .ConsumerName = ev->Get()->Request->GroupId->c_str(),
+                    .ConsumerGeneration = ev->Get()->Request->GenerationId
+                };
+                auto it = OffsetsToCommit.find(topicPartition);
+                if (it == OffsetsToCommit.end()) {
+                    OffsetsToCommit.emplace(std::make_pair(topicPartition, newCommit));
+                } else {
+                    it->second = newCommit;
+                }
+            }
+        }
+        SendOkResponse<TTxnOffsetCommitResponseData>(ev);
+    }
+
+    /* 
+    If abort, will send back ok.
+    If commit, will do following sequence of calls and event handlers:
+    1. Open KQP Session
+    2. Handle(NKqp::TEvKqp::TEvCreateSessionResponse) - KQP session created
+    3. Send Select request for current producer and consumers state, begin transaction
+    4. Handle(NKqp::TEvKqp::TEvQueryResponse): Validate returned producer and consumers state. Return PRODUCER_FENCED if check failed
+    5. Send Commit request with properly created KafkaApiOperations
+    6. Handle(NKqp::TEvKqp::TEvQueryResponse): If everything committed successfully, return OK to the client
+    7. Close KQP session, send to coordinator TEvTransactionActorDied, die.
+    */
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvEndTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved END_TXN request");
+        VALIDATE_PRODUCER_IN_REQUEST(TEndTxnResponseData);
+
+        bool txnAborted = !ev->Get()->Request->Committed;
+        if (CommitStarted) {
+            return; // we just ignore second and subsequent requests
+        } else if (txnAborted) {
+            SendOkResponse<TEndTxnResponseData>(ev);
+        } else {
+            CommitStarted = true;
+            EndTxnRequestPtr = std::move(ev);
+            StartKqpSession(ctx);
+        }
+    }
+
+    void TKafkaTransactionActor::Handle(TEvents::TEvPoison::TPtr&, const TActorContext& ctx) {
+        Die(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvCreateSessionResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "KQP session created");
+        if (!Kqp->HandleCreateSessionResponse(ev, ctx)) {
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, "Failed to create KQP session");
+            Die(ctx);
+            return;
+        }
+
+        SendToKqpValidationRequests(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Receieved query response from KQP for " << GetAsStr(LastSentToKqpRequest) << " request");
+        if (auto error = GetErrorFromYdbResponse(ev)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error->data());
+            Die(ctx);
+            return;
+        }
+
+        switch (LastSentToKqpRequest) {
+            case EKafkaTxnKqpRequests::SELECT:
+                HandleSelectResponse(*ev->Get(), ctx);
+                break;
+            case EKafkaTxnKqpRequests::COMMIT:
+                HandleCommitResponse(ctx);
+                break;
+            default:
+                Y_FAIL("Unexpected KQP request");
+        }
+    }
+
+    void TKafkaTransactionActor::StartKqpSession(const TActorContext& ctx) {
+        Kqp = std::make_unique<TKqpTxHelper>(DatabasePath);
+        KAFKA_LOG_D(TStringBuilder() << "Sending create session request to KQP for database " << DatabasePath);
+        Kqp->SendCreateSessionRequest(ctx, KqpActorId);
+    }
+
+    void TKafkaTransactionActor::SendToKqpValidationRequests(const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Sending select request to KQP for database " << DatabasePath);
+        Kqp->SendYqlRequest(
+            GetYqlWithTablesNames(NKafkaTransactionSql::SELECT_FOR_VALIDATION), 
+            BuildSelectParams(), 
+            ++KqpCookie, 
+            ctx,
+            false,
+            KqpActorId
+        );
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::SELECT;
+    }
+
+    void TKafkaTransactionActor::SendCommitTxnRequest(const TString& kqpTransactionId) {
+        auto request = BuildCommitTxnRequestToKqp(kqpTransactionId);
+
+        Send(KqpActorId, request.Release(), 0, ++KqpCookie);
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::COMMIT;
+    }
+    
+    // Response senders
+    template<class ErrorResponseType, class EventType>
+    void TKafkaTransactionActor::SendFailResponse(TAutoPtr<TEventHandle<EventType>>& evHandle, EKafkaErrors errorCode, const TString& errorMessage) {
+        if (errorMessage) {
+            KAFKA_LOG_W(TStringBuilder() << "Sending fail response with error code: " << errorCode << ". Reason:  " << errorMessage);
+        } else {
+            KAFKA_LOG_W(TStringBuilder() << "Sending fail response with error code: " << errorCode);
+        }
+
+        auto response = NKafkaTransactions::BuildResponse<ErrorResponseType>(evHandle->Get()->Request, errorCode);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, errorCode));
+    }
+
+    template<class ResponseType, class EventType>
+    void TKafkaTransactionActor::SendOkResponse(TAutoPtr<TEventHandle<EventType>>& evHandle) {
+        auto& kafkaRequest = evHandle->Get()->Request;
+        KAFKA_LOG_D(TStringBuilder() << "Sending OK response to " << evHandle->Get()->ConnectionId << " with correlationId " << evHandle->Get()->CorrelationId << " and transactionalId " << TransactionalId);
+        std::shared_ptr<ResponseType> response = NKafkaTransactions::BuildResponse<ResponseType>(kafkaRequest, EKafkaErrors::NONE_ERROR);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, EKafkaErrors::NONE_ERROR));
+    }
+
+    // helper methods
+    void TKafkaTransactionActor::Die(const TActorContext &ctx) {
+        KAFKA_LOG_D("Dying.");
+        if (Kqp) {
+            Kqp->CloseKqpSession(ctx);
+        }
+        Send(TxnCoordinatorActorId, new TEvKafka::TEvTransactionActorDied(TransactionalId, ProducerInstanceId));
+        TBase::Die(ctx);
+    }
+
+    template<class EventType>
+    bool TKafkaTransactionActor::ProducerInRequestIsValid(TMessagePtr<EventType> kafkaRequest) {
+        return kafkaRequest->TransactionalId->c_str() == TransactionalId 
+            && kafkaRequest->ProducerId == ProducerInstanceId.Id 
+            && kafkaRequest->ProducerEpoch == ProducerInstanceId.Epoch;
+    }
+
+    TString TKafkaTransactionActor::GetFullTopicPath(const TString& topicName) {
+        return NPersQueue::GetFullTopicPath(DatabasePath, topicName);
+    }
+
+    TString TKafkaTransactionActor::GetYqlWithTablesNames(const TString& templateStr) {
+        TString templateWithProducerStateTable = std::regex_replace(
+            templateStr.c_str(),
+            std::regex("<producer_state_table_name>"), 
+            NKikimr::NGRpcProxy::V1::TTransactionalProducersInitManager::GetInstant()->GetStorageTablePath().c_str()
+        );
+        TString templateWithConsumerStateTable = std::regex_replace(
+            templateWithProducerStateTable.c_str(),
+            std::regex("<consumer_state_table_name>"), 
+            NKikimr::NGRpcProxy::V1::TKafkaConsumerGroupsMetaInitManager::GetInstant()->GetStorageTablePath().c_str()
+        );
+
+        return templateWithConsumerStateTable;
+    }
+
+    NYdb::TParams TKafkaTransactionActor::BuildSelectParams() {
+        NYdb::TParamsBuilder params;
+        params.AddParam("$Database").Utf8(DatabasePath).Build();
+        params.AddParam("$TransactionalId").Utf8(TransactionalId).Build();
+        
+        // select unique consumer group names
+        std::unordered_set<TString> uniqueConsumerGroups;
+        for (auto& [partition, commitDetails] : OffsetsToCommit) {
+            uniqueConsumerGroups.emplace(commitDetails.ConsumerName);
+        }
+
+        // add unique consumer group names to request as params
+        auto& consumerGroupsParamBuilder = params.AddParam("$ConsumerGroups").BeginList();
+        for (auto& consumerGroupName : uniqueConsumerGroups) {
+            consumerGroupsParamBuilder.AddListItem().Utf8(consumerGroupName);
+        }
+        consumerGroupsParamBuilder.EndList().Build();
+
+        return params.Build();
+    }
+
+    THolder<NKikimr::NKqp::TEvKqp::TEvQueryRequest> TKafkaTransactionActor::BuildCommitTxnRequestToKqp(const TString& kqpTransactionId) {
+        auto ev = MakeHolder<TEvKqp::TEvQueryRequest>();
+
+        ev->Record.MutableRequest()->SetType(NKikimrKqp::QUERY_TYPE_UNDEFINED);
+        ev->Record.MutableRequest()->SetAction(NKikimrKqp::QUERY_ACTION_TOPIC);
+        ev->Record.MutableRequest()->SetDatabase(DatabasePath);
+        ev->Record.MutableRequest()->SetSessionId(KqpSessionId);
+
+        ev->Record.MutableRequest()->MutableTxControl()->set_commit_tx(true);
+        ev->Record.MutableRequest()->MutableTxControl()->set_tx_id(kqpTransactionId);
+
+        ev->Record.MutableRequest()->SetUsePublicResponseDataFormat(true);
+
+        auto* kafkaApiOperations = ev->Record.MutableRequest()->MutableKafkaApiOperations();
+        kafkaApiOperations->set_transactionalid(TransactionalId);
+        kafkaApiOperations->set_producerid(ProducerInstanceId.Id);
+        kafkaApiOperations->set_producerepoch(ProducerInstanceId.Epoch);
+
+        for (auto& partition : PartitionsInTxn) {
+            auto* partitionInRequest = kafkaApiOperations->AddPartitionsInTxn();
+            partitionInRequest->set_topicpath(partition.TopicPath);
+            partitionInRequest->set_partitionid(partition.PartitionId);
+        }
+
+        for (auto& [partition, offsetDetails] : OffsetsToCommit) {
+            auto* offsetInRequest = kafkaApiOperations->AddOffsetsInTxn();
+            offsetInRequest->set_topicpath(partition.TopicPath);
+            offsetInRequest->set_partitionid(partition.PartitionId);
+            offsetInRequest->set_consumername(offsetDetails.ConsumerName);
+            offsetInRequest->set_consumergeneration(offsetDetails.ConsumerGeneration);
+            offsetInRequest->set_offset(offsetDetails.Offset);
+        }
+        
+        return ev;
+    }
+
+    void TKafkaTransactionActor::HandleSelectResponse(const NKqp::TEvKqp::TEvQueryResponse& response, const TActorContext& ctx) {
+        // YDB should return exactly two result sets for two queries: to producer and consumer state tables
+        if (response.Record.GetResponse().GetYdbResults().size() != 2) {
+            TString error = TStringBuilder() << "KQP returned wrong number of result sets on SELECT query. Expected 2, got " << response.Record.GetResponse().GetYdbResults().size() << ".";
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error);
+            Die(ctx);
+            return;
+        }
+
+        // parse and validate producer state
+        TMaybe<TProducerState> producerState;
+        try {
+            producerState = ParseProducerState(response);
+        } catch (const yexception& y) {
+            TString error = TStringBuilder() << "Error parsing producer state response from KQP. Reason: " << y.what();
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error);
+            Die(ctx);
+            return;
+        }
+        if (auto error = GetErrorInProducerState(producerState)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::PRODUCER_FENCED, error->data());
+            Die(ctx);
+            return;
+        }
+
+        // parse and validate consumers
+        std::unordered_map<TString, i32> consumerGenerationsByName = ParseConsumersGenerations(response);
+        if (auto error = GetErrorInConsumersStates(consumerGenerationsByName)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::PRODUCER_FENCED, error->data());
+            Die(ctx);
+            return;
+        }
+
+        KAFKA_LOG_D(TStringBuilder() << "Validated producer and consumers states. Everything is alright, sending commit");
+        auto kqpTxnId = response.Record.GetResponse().GetTxMeta().id();
+        // finally everything is valid and we can attempt to commit
+        SendCommitTxnRequest(kqpTxnId);
+    }
+
+    void TKafkaTransactionActor::HandleCommitResponse(const TActorContext& ctx) {
+        KAFKA_LOG_D("Successfully committed transaction. Sending ok and dying.");
+        SendOkResponse<TEndTxnResponseData>(EndTxnRequestPtr);
+        Die(ctx);
+    }
+
+    TMaybe<TString> TKafkaTransactionActor::GetErrorFromYdbResponse(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev) {
+        TStringBuilder builder = TStringBuilder() << "Recieved error on request to KQP. Last sent request: " << GetAsStr(LastSentToKqpRequest) << ". Reason: ";
+        if (ev->Cookie != KqpCookie) {
+            return builder << "Unexpected cookie in TEvQueryResponse. Expected KQP Cookie: " << KqpCookie << ", Actual: " << ev->Cookie << ".";
+        } else if (ev->Get()->Record.GetYdbStatus() != Ydb::StatusIds::SUCCESS) {
+            return builder << "Unexpected YDB status in TEvQueryResponse. Expected YDB SUCCESS status, Actual: " << ev->Get()->Record.GetYdbStatus() << ".";
+        } else {
+            return {};
+        }
+    }
+
+    TMaybe<TProducerState> TKafkaTransactionActor::ParseProducerState(const NKqp::TEvKqp::TEvQueryResponse& response) {
+        auto& resp = response.Record.GetResponse();
+
+        NYdb::TResultSetParser parser(resp.GetYdbResults(NKafkaTransactionSql::PRODUCER_STATE_REQUEST_INDEX));
+
+        // for this transactional id there is no rows
+        if (parser.RowsCount() == 0) {
+            return {};
+        } 
+        // there are multiple rows for this transactional id. This is unexpected and should not happen
+        else if (parser.RowsCount() > 1) {
+            throw yexception() << "Request returned more than one row: " << resp.GetYdbResults().size();
+        } else {
+            parser.TryNextRow();
+
+            TProducerState result; 
+
+            result.TransactionalId = parser.ColumnParser("transactional_id").GetUtf8();
+            result.ProducerId = parser.ColumnParser("producer_id").GetInt64();
+            result.ProducerEpoch = parser.ColumnParser("producer_epoch").GetInt16();
+
+            return result;
+        }
+    }
+
+    TMaybe<TString> TKafkaTransactionActor::GetErrorInProducerState(const TMaybe<TProducerState>& producerState) {
+        if (!producerState) {
+            return "No producer state found. May be it has expired";
+        } else if (producerState->TransactionalId != TransactionalId || producerState->ProducerId != ProducerInstanceId.Id || producerState->ProducerEpoch != ProducerInstanceId.Epoch) {
+            return TStringBuilder() << "Producer state in producers_state table is different from the one in this actor. Producer state in table: "
+            << "transactionalId=" << producerState->TransactionalId 
+            << ", producerId=" << producerState->ProducerId
+            << ", producerEpoch=" << producerState->ProducerEpoch;
+        } else {
+            return {};
+        }
+    }
+
+        /**
+        * Parses the response to extract consumer group generations.
+        *
+        * @param response The response object containing the result set from the YDB query.
+        * @return A map where keys are consumer group names and values are their corresponding generations.
+        */
+    std::unordered_map<TString, i32> TKafkaTransactionActor::ParseConsumersGenerations(const NKqp::TEvKqp::TEvQueryResponse& response) {
+        std::unordered_map<TString, i32> generationByConsumerName;
+    
+        NYdb::TResultSetParser parser(response.Record.GetResponse().GetYdbResults(NKafkaTransactionSql::CONSUMER_STATES_REQUEST_INDEX));
+        while (parser.TryNextRow()) {
+            TString consumerName = parser.ColumnParser("consumer_group").GetUtf8().c_str();
+            i32 generation = IntegerCast<i32>(parser.ColumnParser("generation").GetUint64());
+            generationByConsumerName.emplace(consumerName, generation);
+        }
+    
+        return generationByConsumerName;
+    }
+
+    TMaybe<TString> TKafkaTransactionActor::GetErrorInConsumersStates(const std::unordered_map<TString, i32>& consumerGenerationByName) {
+        TStringBuilder builder;
+        bool foundError = false;
+        for (auto& [topicPartition, offsetCommit] : OffsetsToCommit) {
+            auto it = consumerGenerationByName.find(offsetCommit.ConsumerName);
+            if (it != consumerGenerationByName.end()) {
+                i32 consumerGenerationInTable = it->second;
+                if (consumerGenerationInTable != offsetCommit.ConsumerGeneration) {
+                    if (foundError) {
+                        builder << ", ";
+                    } else {
+                        foundError = true;
+                    }
+                    builder << "Consumer " << offsetCommit.ConsumerName << " has different generation in table than requested in transaction. Generation in table: " << consumerGenerationInTable << ", Generation requested in txn: " << offsetCommit.ConsumerGeneration << ".";
+                }
+            } else {
+                if (foundError) {
+                    builder << ", ";
+                } else {
+                    foundError = true;
+                }
+                builder << "There is no consumer state in table for group " << offsetCommit.ConsumerName;
+            }
+        }
+
+        if (foundError) {
+            return builder;
+        } else {
+            return {};
+        }
+    }
+
+    TString TKafkaTransactionActor::GetAsStr(EKafkaTxnKqpRequests request) {
+        switch (request) {
+        case SELECT:
+            return "SELECT";
+        case COMMIT:
+            return "SELECT";
+        case NO_REQUEST:
+            return "NO_REQUEST";
+        }
+    }
+} // namespace NKafka
\ No newline at end of file
diff --git a/ydb/core/kafka_proxy/actors/kafka_transaction_actor.h b/ydb/core/kafka_proxy/actors/kafka_transaction_actor.h
index fc1b11647efa..1a53d83c1410 100644
--- a/ydb/core/kafka_proxy/actors/kafka_transaction_actor.h
+++ b/ydb/core/kafka_proxy/actors/kafka_transaction_actor.h
@@ -1,6 +1,9 @@
 #pragma once
 
+#include "kafka_init_producer_id_actor.h"
+#include <ydb/core/kafka_proxy/kafka_events.h>
 #include <ydb/library/actors/core/actor_bootstrapped.h>
+#include <ydb/core/kafka_proxy/kqp_helper.h>
 
 namespace NKafka {
     /* 
@@ -8,25 +11,134 @@ namespace NKafka {
 
     It accumulates transaction state (partitions in tx, offsets) and on commit submits transaction to KQP
     */
-    class TKafkaTransactionActor : public NActors::TActorBootstrapped<TKafkaTransactionActor> {
+    class TKafkaTransactionActor : public NActors::TActor<TKafkaTransactionActor> {
 
-        using TBase = NActors::TActorBootstrapped<TKafkaTransactionActor>;
+        using TBase = NActors::TActor<TKafkaTransactionActor>;
         
         public:
-            void Bootstrap(const NActors::TActorContext&) {
-                TBase::Become(&TKafkaTransactionActor::StateWork);
-            }
+            struct TTopicPartition {
+                TString TopicPath;
+                ui32 PartitionId;
+
+                bool operator==(const TTopicPartition &other) const = default;
+            };
+
+            struct TopicPartitionHashFn {
+                size_t operator()(const TTopicPartition& partition) const {
+                    return std::hash<TString>()(partition.TopicPath) ^ std::hash<int64_t>()(partition.PartitionId);
+                }
+            };
+
+            struct TPartitionCommit {
+                i32 Partition; 
+                i64 Offset;
+                TString ConsumerName;
+                i64 ConsumerGeneration;
+            };
+
+            enum EKafkaTxnKqpRequests : ui8 {
+                NO_REQUEST = 0,
+                
+                // This request selects up-to-date producer and consumers states from relevant tables
+                // After this request a check will happen, that no transaction details has expired.
+                SELECT,
+                // This request sends to KQP a command to commit transaction
+                // Both these requests happen in same transaction
+                COMMIT 
+            };
+
+            // we need to exlplicitly specify kqpActorId and txnCoordinatorActorId for unit tests
+            TKafkaTransactionActor(const TString& transactionalId, i64 producerId, i16 producerEpoch, const TString& DatabasePath, const TActorId& kqpActorId, const TActorId& txnCoordinatorActorId) : 
+                TActor<TKafkaTransactionActor>(&TKafkaTransactionActor::StateFunc),
+                TransactionalId(transactionalId),
+                ProducerInstanceId({producerId, producerEpoch}),
+                DatabasePath(DatabasePath),
+                TxnCoordinatorActorId(txnCoordinatorActorId),
+                KqpActorId(kqpActorId) {};
 
             TStringBuilder LogPrefix() const {
-                return TStringBuilder() << "KafkaTransactionActor";
+                return TStringBuilder() << "KafkaTransactionActor{TransactionalId=" << TransactionalId << "; ProducerId=" << ProducerInstanceId.Id << "; ProducerEpoch=" << ProducerInstanceId.Epoch << "}: ";
             }
         
         private:
-            STFUNC(StateWork) {
-                switch (ev->GetTypeRewrite()) {
-                    // will be eimplemented in a future PR
-                    // ToDo: add poison pill handler
+            STFUNC(StateFunc) {
+                try {
+                    switch (ev->GetTypeRewrite()) {
+                        HFunc(TEvKafka::TEvAddPartitionsToTxnRequest, Handle);
+                        HFunc(TEvKafka::TEvAddOffsetsToTxnRequest, Handle);
+                        HFunc(TEvKafka::TEvTxnOffsetCommitRequest, Handle);
+                        HFunc(TEvKafka::TEvEndTxnRequest, Handle);
+                        HFunc(NKqp::TEvKqp::TEvCreateSessionResponse, Handle);
+                        HFunc(NKqp::TEvKqp::TEvQueryResponse, Handle);
+                        HFunc(TEvents::TEvPoison, Handle);
+                    }
+                } catch (const yexception& y) {
+                    KAFKA_LOG_CRIT(TStringBuilder() << "Critical error happened. Reason: " << y.what());
+                    Die(ActorContext());
                 }
             }
+
+            // Kafka API events
+            void Handle(TEvKafka::TEvAddPartitionsToTxnRequest::TPtr& ev, const TActorContext& ctx);
+            void Handle(TEvKafka::TEvAddOffsetsToTxnRequest::TPtr& ev, const TActorContext& ctx);
+            void Handle(TEvKafka::TEvTxnOffsetCommitRequest::TPtr& ev, const TActorContext& ctx);
+            void Handle(TEvKafka::TEvEndTxnRequest::TPtr& ev, const TActorContext& ctx);
+            // KQP events
+            void Handle(NKqp::TEvKqp::TEvCreateSessionResponse::TPtr& ev, const TActorContext& ctx);
+            void Handle(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev, const TActorContext& ctx);
+            
+            // Poison pill
+            void Handle(TEvents::TEvPoison::TPtr& ev, const TActorContext& ctx);
+
+            // Transaction commit logic
+            void StartKqpSession(const TActorContext& ctx);
+            void SendToKqpValidationRequests(const TActorContext& ctx);
+            void SendCommitTxnRequest(const TString& kqpTransactionId);
+
+            // Response senders
+            template<class ErrorResponseType, class EventType>
+            void SendFailResponse(TAutoPtr<TEventHandle<EventType>>& evHandle, EKafkaErrors errorCode, const TString& errorMessage = {});
+            template<class ResponseType, class EventType>
+            void SendOkResponse(TAutoPtr<TEventHandle<EventType>>& evHandle);
+
+            // helper methods
+            void Die(const TActorContext &ctx);
+            template<class EventType>
+            bool ProducerInRequestIsValid(TMessagePtr<EventType> kafkaRequest);
+            TString GetFullTopicPath(const TString& topicName);
+            TString GetYqlWithTablesNames(const TString& templateStr);
+            NYdb::TParams BuildSelectParams();
+            THolder<NKikimr::NKqp::TEvKqp::TEvQueryRequest> BuildCommitTxnRequestToKqp(const TString& kqpTransactionId);
+            void HandleSelectResponse(const NKqp::TEvKqp::TEvQueryResponse& response, const TActorContext& ctx);
+            void HandleCommitResponse(const TActorContext& ctx);
+            TMaybe<TString> GetErrorFromYdbResponse(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev);
+            TMaybe<TProducerState> ParseProducerState(const NKqp::TEvKqp::TEvQueryResponse& response);
+            TMaybe<TString> GetErrorInProducerState(const TMaybe<TProducerState>& producerState);
+            std::unordered_map<TString, i32> ParseConsumersGenerations(const NKqp::TEvKqp::TEvQueryResponse& response);
+            TMaybe<TString> GetErrorInConsumersStates(const std::unordered_map<TString, i32>& consumerGenerationByName);
+            TString GetAsStr(EKafkaTxnKqpRequests request);
+
+            // data from fields below will be sent to KQP on END_TXN request
+            std::unordered_map<TTopicPartition, TPartitionCommit, TopicPartitionHashFn> OffsetsToCommit = {};
+            std::unordered_set<TTopicPartition, TopicPartitionHashFn> PartitionsInTxn = {};
+            const TString TransactionalId;
+            const TEvKafka::TProducerInstanceId ProducerInstanceId;
+            // const i64 ProducerId;
+            // const i16 ProducerEpoch;
+
+            // helper fields
+            const TString DatabasePath;
+            // This field need to preserve request details between several requests to KQP
+            // In case something goes off road, we can always send error back to client
+            TAutoPtr<TEventHandle<TEvKafka::TEvEndTxnRequest>> EndTxnRequestPtr;
+            bool CommitStarted = false;
+            const TActorId TxnCoordinatorActorId;
+
+            // communication with KQP
+            std::unique_ptr<TKqpTxHelper> Kqp;
+            TActorId KqpActorId;
+            TString KqpSessionId;
+            ui64 KqpCookie = 0;
+            EKafkaTxnKqpRequests LastSentToKqpRequest;
     };
 } // namespace NKafka
\ No newline at end of file
diff --git a/ydb/core/kafka_proxy/actors/kafka_transaction_actor_sql.cpp b/ydb/core/kafka_proxy/actors/kafka_transaction_actor_sql.cpp
new file mode 100644
index 000000000000..e4f18f3be3aa
--- /dev/null
+++ b/ydb/core/kafka_proxy/actors/kafka_transaction_actor_sql.cpp
@@ -0,0 +1,22 @@
+#include <util/generic/string.h>
+
+namespace NKafka::NKafkaTransactionSql {
+
+    TString SELECT_FOR_VALIDATION = R"sql(
+        --!syntax_v1
+        DECLARE $Database AS Utf8;
+        DECLARE $TransactionalId AS Utf8;
+        DECLARE $ConsumerGroups AS List<Utf8>;
+
+        SELECT * FROM `<producer_state_table_name>`
+        WHERE database = $Database 
+        AND transactional_id = $TransactionalId;
+
+        SELECT consumer_group, MAX(generation) FROM `<consumer_state_table_name>`
+        VIEW PRIMARY KEY
+        WHERE database = $Database
+        AND consumer_group IN COMPACT ($ConsumerGroups)
+        GROUP BY consumer_group;
+    )sql";
+
+} // namespace NKafka::NKafkaTransactionSql
diff --git a/ydb/core/kafka_proxy/actors/kafka_transaction_actor_sql.h b/ydb/core/kafka_proxy/actors/kafka_transaction_actor_sql.h
new file mode 100644
index 000000000000..8a116a90ce05
--- /dev/null
+++ b/ydb/core/kafka_proxy/actors/kafka_transaction_actor_sql.h
@@ -0,0 +1,12 @@
+#pragma once
+
+#include <util/generic/fwd.h>
+
+namespace NKafka::NKafkaTransactionSql {
+
+    constexpr ui32 PRODUCER_STATE_REQUEST_INDEX = 0;
+    constexpr ui32 CONSUMER_STATES_REQUEST_INDEX = 1;
+
+    extern const TString SELECT_FOR_VALIDATION;
+
+} // namespace NKafka::NKafkaTransactionSql
diff --git a/ydb/core/kafka_proxy/actors/txn_actor_response_builder.cpp b/ydb/core/kafka_proxy/actors/txn_actor_response_builder.cpp
new file mode 100644
index 000000000000..f6e77e43ffc8
--- /dev/null
+++ b/ydb/core/kafka_proxy/actors/txn_actor_response_builder.cpp
@@ -0,0 +1,57 @@
+#include "txn_actor_response_builder.h"
+
+namespace NKafka::NKafkaTransactions {
+    template<class ResponseType, class RequestType>
+    std::shared_ptr<ResponseType> BuildResponse(TMessagePtr<RequestType> request, EKafkaErrors errorCode) {
+        Y_UNUSED(request); // used in other template functions
+        auto response = std::make_shared<ResponseType>();
+        response->ErrorCode = errorCode;
+        return response;
+    };
+    
+    template std::shared_ptr<TAddOffsetsToTxnResponseData> BuildResponse<TAddOffsetsToTxnResponseData, TAddOffsetsToTxnRequestData>(TMessagePtr<TAddOffsetsToTxnRequestData> request, EKafkaErrors errorCode);
+    
+    template std::shared_ptr<TEndTxnResponseData> BuildResponse<TEndTxnResponseData, TEndTxnRequestData>(TMessagePtr<TEndTxnRequestData> request, EKafkaErrors errorCode);
+
+    template<>
+    std::shared_ptr<TAddPartitionsToTxnResponseData> BuildResponse<TAddPartitionsToTxnResponseData, TAddPartitionsToTxnRequestData>(TMessagePtr<TAddPartitionsToTxnRequestData> request, EKafkaErrors errorCode) {
+        auto response = std::make_shared<TAddPartitionsToTxnResponseData>();
+        std::vector<TAddPartitionsToTxnResponseData::TAddPartitionsToTxnTopicResult> topicsResponse;
+        topicsResponse.reserve(request->Topics.size());
+        for (const auto& requestTopic : request->Topics) {
+            TAddPartitionsToTxnResponseData::TAddPartitionsToTxnTopicResult topicInResponse;
+            topicInResponse.Name = requestTopic.Name;
+            topicInResponse.Results.reserve(requestTopic.Partitions.size());
+            for (const auto& requestPartition : requestTopic.Partitions) {
+                TAddPartitionsToTxnResponseData::TAddPartitionsToTxnTopicResult::TAddPartitionsToTxnPartitionResult partitionInResponse;
+                partitionInResponse.PartitionIndex = requestPartition;
+                partitionInResponse.ErrorCode = errorCode;
+                topicInResponse.Results.push_back(partitionInResponse);
+            }
+            topicsResponse.push_back(topicInResponse);
+        }
+        response->Results = std::move(topicsResponse);
+        return response;
+    };
+
+    template<>
+    std::shared_ptr<TTxnOffsetCommitResponseData> BuildResponse<TTxnOffsetCommitResponseData, TTxnOffsetCommitRequestData>(TMessagePtr<TTxnOffsetCommitRequestData> request, EKafkaErrors errorCode) {
+        auto response = std::make_shared<TTxnOffsetCommitResponseData>();
+        std::vector<TTxnOffsetCommitResponseData::TTxnOffsetCommitResponseTopic> topicsResponse;
+        topicsResponse.reserve(request->Topics.size());
+        for (const auto& requestTopic : request->Topics) {
+            TTxnOffsetCommitResponseData::TTxnOffsetCommitResponseTopic topicInResponse;
+            topicInResponse.Name = requestTopic.Name;
+            topicInResponse.Partitions.reserve(requestTopic.Partitions.size());
+            for (const auto& requestPartition : requestTopic.Partitions) {
+                TTxnOffsetCommitResponseData::TTxnOffsetCommitResponseTopic::TTxnOffsetCommitResponsePartition partitionInResponse;
+                partitionInResponse.PartitionIndex = requestPartition.PartitionIndex;
+                partitionInResponse.ErrorCode = errorCode;
+                topicInResponse.Partitions.push_back(partitionInResponse);
+            }
+            topicsResponse.push_back(topicInResponse);
+        }
+        response->Topics = std::move(topicsResponse);;
+        return response;
+    };
+} // namespace NKafka::NKafkaTransactions
\ No newline at end of file
diff --git a/ydb/core/kafka_proxy/actors/txn_actor_response_builder.h b/ydb/core/kafka_proxy/actors/txn_actor_response_builder.h
new file mode 100644
index 000000000000..9a96d1fbe59a
--- /dev/null
+++ b/ydb/core/kafka_proxy/actors/txn_actor_response_builder.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include <ydb/core/kafka_proxy/actors/actors.h>
+#include <ydb/core/kafka_proxy/kafka.h>
+#include <ydb/core/kafka_proxy/kafka_messages.h>
+
+namespace NKafka::NKafkaTransactions {
+    template<class ResponseType, class RequestType>
+    std::shared_ptr<ResponseType> BuildResponse(TMessagePtr<RequestType> request, EKafkaErrors errorCode);
+} // namespace NKafka::NKafkaTransactions
\ No newline at end of file
diff --git a/ydb/core/kafka_proxy/kafka_events.h b/ydb/core/kafka_proxy/kafka_events.h
index fb8a304cedca..594fc3aa210b 100644
--- a/ydb/core/kafka_proxy/kafka_events.h
+++ b/ydb/core/kafka_proxy/kafka_events.h
@@ -37,6 +37,7 @@ struct TEvKafka {
         EvAddOffsetsToTxnRequest,
         EvTxnOffsetCommitRequest,
         EvEndTxnRequest,
+        EvTransactionActorDied,
         EvResponse = EvRequest + 256,
         EvInternalEvents = EvResponse + 256,
         EvEnd
@@ -255,15 +256,17 @@ struct TEvTopicModificationResponse : public NActors::TEventLocal<TEvTopicModifi
 };
 
 struct TEvAddPartitionsToTxnRequest : public TEventLocal<TEvAddPartitionsToTxnRequest, EvAddPartitionsToTxnRequest> {
-    TEvAddPartitionsToTxnRequest(const ui64 correlationId, const TMessagePtr<TAddPartitionsToTxnRequestData>& request, const TActorId connectionId)
+    TEvAddPartitionsToTxnRequest(const ui64 correlationId, const TMessagePtr<TAddPartitionsToTxnRequestData>& request, const TActorId connectionId, const TString& databasePath)
     : CorrelationId(correlationId)
     , Request(request)
     , ConnectionId(connectionId)
+    , DatabasePath(databasePath)
     {}
 
     ui64 CorrelationId;
     const TMessagePtr<TAddPartitionsToTxnRequestData> Request;
     TActorId ConnectionId;
+    TString DatabasePath;
 };
 
 struct TEvTopicDescribeResponse : public NActors::TEventLocal<TEvTopicDescribeResponse, EvDescribeTopicsResponse>
@@ -286,54 +289,64 @@ struct TEvTopicDescribeResponse : public NActors::TEventLocal<TEvTopicDescribeRe
 };
 
 struct TEvAddOffsetsToTxnRequest : public TEventLocal<TEvAddOffsetsToTxnRequest, EvAddOffsetsToTxnRequest> {
-    TEvAddOffsetsToTxnRequest(const ui64 correlationId, const TMessagePtr<TAddOffsetsToTxnRequestData>& request, const TActorId connectionId)
+    TEvAddOffsetsToTxnRequest(const ui64 correlationId, const TMessagePtr<TAddOffsetsToTxnRequestData>& request, const TActorId connectionId, const TString& databasePath)
     : CorrelationId(correlationId)
     , Request(request)
     , ConnectionId(connectionId)
+    , DatabasePath(databasePath)
     {}
 
     ui64 CorrelationId;
     const TMessagePtr<TAddOffsetsToTxnRequestData> Request;
     TActorId ConnectionId;
+    TString DatabasePath;
 };
 
 struct TEvTxnOffsetCommitRequest : public TEventLocal<TEvTxnOffsetCommitRequest, EvTxnOffsetCommitRequest> {
-    TEvTxnOffsetCommitRequest(const ui64 correlationId, const TMessagePtr<TTxnOffsetCommitRequestData>& request, const TActorId connectionId)
+    TEvTxnOffsetCommitRequest(const ui64 correlationId, const TMessagePtr<TTxnOffsetCommitRequestData>& request, const TActorId connectionId, const TString& databasePath)
     : CorrelationId(correlationId)
     , Request(request)
     , ConnectionId(connectionId)
+    , DatabasePath(databasePath)
     {}
 
     ui64 CorrelationId;
     const TMessagePtr<TTxnOffsetCommitRequestData> Request;
     TActorId ConnectionId;
+    TString DatabasePath;
 };
 
 struct TEvEndTxnRequest : public TEventLocal<TEvEndTxnRequest, EvEndTxnRequest> {
-    TEvEndTxnRequest(const ui64 correlationId, const TMessagePtr<TEndTxnRequestData>& request, const TActorId connectionId)
+    TEvEndTxnRequest(const ui64 correlationId, const TMessagePtr<TEndTxnRequestData>& request, const TActorId connectionId, const TString& databasePath)
     : CorrelationId(correlationId)
     , Request(request)
     , ConnectionId(connectionId)
+    , DatabasePath(databasePath)
     {}
 
     ui64 CorrelationId;
     const TMessagePtr<TEndTxnRequestData> Request;
     TActorId ConnectionId;
+    TString DatabasePath;
+};
+struct TProducerInstanceId {
+    i64 Id;
+    i32 Epoch;
+
+    auto operator<=>(TProducerInstanceId const&) const = default;
 };
 
 /* 
 Event sent from TIintProducerActor to TKafkaTransactionRouter to notify that producer id will be obtained by client
  */
 struct TEvSaveTxnProducerRequest : public NActors::TEventLocal<TEvSaveTxnProducerRequest, EvSaveTxnProducerRequest> {
-    TEvSaveTxnProducerRequest(const TString& transactionalId, const i64 producerId, const i16 producerEpoch) :
-        TransactionalId(std::move(transactionalId)),
-        ProducerId(producerId),
-        ProducerEpoch(producerEpoch)
+    TEvSaveTxnProducerRequest(const TString& transactionalId, const TProducerInstanceId& producerState) :
+        TransactionalId(transactionalId),
+        ProducerState(producerState)
     {}
 
     const TString TransactionalId;
-    const i64 ProducerId;
-    const i16 ProducerEpoch;
+    const TProducerInstanceId ProducerState;
 };
 
 /* 
@@ -357,6 +370,16 @@ struct TEvSaveTxnProducerResponse : public NActors::TEventLocal<TEvSaveTxnProduc
     EStatus Status;
     TString Message;
 };
+
+struct TEvTransactionActorDied : public NActors::TEventLocal<TEvTransactionActorDied, EvTransactionActorDied> {
+    TEvTransactionActorDied(const TString& transactionalId, const TProducerInstanceId& producerState) :
+        TransactionalId(transactionalId),
+        ProducerState(producerState)
+    {}
+
+    const TString TransactionalId;
+    const TProducerInstanceId ProducerState;
+};
 }; // struct TEvKafka
 
 } // namespace NKafka
diff --git a/ydb/core/kafka_proxy/kafka_transactions_coordinator.cpp b/ydb/core/kafka_proxy/kafka_transactions_coordinator.cpp
index 001ca00d4d43..03783e6f5dad 100644
--- a/ydb/core/kafka_proxy/kafka_transactions_coordinator.cpp
+++ b/ydb/core/kafka_proxy/kafka_transactions_coordinator.cpp
@@ -1,5 +1,7 @@
 #include "kafka_transactions_coordinator.h"
 #include "actors/kafka_transaction_actor.h"
+#include "actors/txn_actor_response_builder.h"
+#include <ydb/core/kqp/common/simple/services.h>
 
 namespace NKafka {
     // Handles new transactional_id+producer_id+producer_epoch: 
@@ -8,9 +10,10 @@ namespace NKafka {
     void TKafkaTransactionsCoordinator::Handle(TEvKafka::TEvSaveTxnProducerRequest::TPtr& ev, const TActorContext& ctx){
         TEvKafka::TEvSaveTxnProducerRequest* request = ev->Get();
 
-        if (ProducersByTransactionalId.contains(request->TransactionalId)) {
-            TProducerState& currentProducerState = ProducersByTransactionalId[request->TransactionalId];
-            TProducerState newProducerState = TProducerState(request->ProducerId, request->ProducerEpoch);
+        auto it = ProducersByTransactionalId.find(request->TransactionalId);
+        if (it != ProducersByTransactionalId.end()) {
+            TEvKafka::TProducerInstanceId& currentProducerState = it->second;
+            const TEvKafka::TProducerInstanceId& newProducerState = request->ProducerState;
 
             if (NewProducerStateIsOutdated(currentProducerState, newProducerState)) {
                 TString message = GetProducerIsOutdatedError(request->TransactionalId, currentProducerState, newProducerState);
@@ -18,10 +21,9 @@ namespace NKafka {
                 return;
             } 
 
-            currentProducerState.Id = request->ProducerId;
-            currentProducerState.Epoch = request->ProducerEpoch;
+            currentProducerState = std::move(newProducerState);
         } else {
-            ProducersByTransactionalId[request->TransactionalId] = TProducerState(request->ProducerId, request->ProducerEpoch);
+            ProducersByTransactionalId.emplace(request->TransactionalId, request->ProducerState);
         }
         
         ctx.Send(ev->Sender, new TEvKafka::TEvSaveTxnProducerResponse(TEvKafka::TEvSaveTxnProducerResponse::EStatus::OK, ""));
@@ -43,6 +45,23 @@ namespace NKafka {
         HandleTransactionalRequest<TEndTxnResponseData>(ev, ctx);
     };
 
+    void TKafkaTransactionsCoordinator::Handle(TEvKafka::TEvTransactionActorDied::TPtr& ev, const TActorContext&) {
+        auto it = ProducersByTransactionalId.find(ev->Get()->TransactionalId);
+        const TEvKafka::TProducerInstanceId& deadActorProducerState = ev->Get()->ProducerState;
+
+        if (it == ProducersByTransactionalId.end() || it->second != deadActorProducerState) {
+            // new actor was already registered, we can just ignore this event
+            KAFKA_LOG_D(TStringBuilder() << "Received TEvTransactionActorDied for transactionalId " << ev->Get()->TransactionalId << " but producer has already been reinitialized with new epoch or deleted. Ignoring this event");
+        } else {
+            KAFKA_LOG_D(TStringBuilder() << "Received TEvTransactionActorDied for transactionalId " << ev->Get()->TransactionalId 
+                << " and producerId " << ev->Get()->ProducerState.Id 
+                << " and producerEpoch " << ev->Get()->ProducerState.Epoch
+                << ". Erasing info about this actor.");
+            // erase info about actor to prevent future zombie requests from this producer
+            TxnActorByTransactionalId.erase(ev->Get()->TransactionalId);
+        }
+    };
+
     void TKafkaTransactionsCoordinator::Handle(TEvents::TEvPoison::TPtr&, const TActorContext& ctx) {
         KAFKA_LOG_D("Got poison pill, killing all transaction actors");
         for (auto& [transactionalId, txnActorId]: TxnActorByTransactionalId) {
@@ -68,7 +87,7 @@ namespace NKafka {
         // create helper struct to simplify methods interaction
         auto txnRequest = TTransactionalRequest(
             ev->Request->TransactionalId->c_str(),
-            TProducerState(ev->Request->ProducerId, ev->Request->ProducerEpoch),
+            TEvKafka::TProducerInstanceId(ev->Request->ProducerId, ev->Request->ProducerEpoch),
             ev->CorrelationId,
             ev->ConnectionId
         );
@@ -82,7 +101,7 @@ namespace NKafka {
     template<class ErrorResponseType, class RequestType>
     void TKafkaTransactionsCoordinator::SendProducerFencedResponse(TMessagePtr<RequestType> kafkaRequest, const TString& error, const TTransactionalRequest& txnRequestDetails) {
         KAFKA_LOG_W(error);
-        std::shared_ptr<ErrorResponseType> response = BuildProducerFencedResponse<ErrorResponseType>(kafkaRequest);
+        std::shared_ptr<ErrorResponseType> response = NKafkaTransactions::BuildResponse<ErrorResponseType>(kafkaRequest, EKafkaErrors::PRODUCER_FENCED);
         Send(txnRequestDetails.ConnectionId, new TEvKafka::TEvResponse(txnRequestDetails.CorrelationId, response, EKafkaErrors::PRODUCER_FENCED));
     };
 
@@ -94,7 +113,7 @@ namespace NKafka {
         if (TxnActorByTransactionalId.contains(ev->Request->TransactionalId->c_str())) {
             txnActorId = TxnActorByTransactionalId[ev->Request->TransactionalId->c_str()];
         } else {
-            txnActorId = ctx.Register(new TKafkaTransactionActor());
+            txnActorId = ctx.Register(new TKafkaTransactionActor(ev->Request->TransactionalId->c_str(), ev->Request->ProducerId, ev->Request->ProducerEpoch, ev->DatabasePath, NKikimr::NKqp::MakeKqpProxyID(ctx.SelfID.NodeId()), ctx.SelfID));
             TxnActorByTransactionalId[ev->Request->TransactionalId->c_str()] = txnActorId;
             KAFKA_LOG_D(TStringBuilder() << "Registered TKafkaTransactionActor with id " << txnActorId << " for transactionalId " << ev->Request->TransactionalId->c_str() << " and ApiKey " << ev->Request->ApiKey());
         }
@@ -103,74 +122,23 @@ namespace NKafka {
         KAFKA_LOG_D(TStringBuilder() << "Forwarded message to TKafkaTransactionActor with id " << txnActorId << " for transactionalId " << ev->Request->TransactionalId->c_str() << " and ApiKey " << ev->Request->ApiKey());
     };
 
-    template<class ResponseType, class RequestType>
-    std::shared_ptr<ResponseType> TKafkaTransactionsCoordinator::BuildProducerFencedResponse(TMessagePtr<RequestType> request) {
-        Y_UNUSED(request); // used in other template functions
-        auto response = std::make_shared<ResponseType>();
-        response->ErrorCode = EKafkaErrors::PRODUCER_FENCED;
-        return response;
-    };
-
-    template<>
-    std::shared_ptr<TAddPartitionsToTxnResponseData> TKafkaTransactionsCoordinator::BuildProducerFencedResponse<TAddPartitionsToTxnResponseData, TAddPartitionsToTxnRequestData>(TMessagePtr<TAddPartitionsToTxnRequestData> request) {
-        auto response = std::make_shared<TAddPartitionsToTxnResponseData>();
-        std::vector<TAddPartitionsToTxnResponseData::TAddPartitionsToTxnTopicResult> topicsResponse;
-        topicsResponse.reserve(request->Topics.size());
-        for (const auto& requestTopic : request->Topics) {
-            TAddPartitionsToTxnResponseData::TAddPartitionsToTxnTopicResult topicInResponse;
-            topicInResponse.Name = requestTopic.Name;
-            topicInResponse.Results.reserve(requestTopic.Partitions.size());
-            for (const auto& requestPartition : requestTopic.Partitions) {
-                TAddPartitionsToTxnResponseData::TAddPartitionsToTxnTopicResult::TAddPartitionsToTxnPartitionResult partitionInResponse;
-                partitionInResponse.PartitionIndex = requestPartition;
-                partitionInResponse.ErrorCode = EKafkaErrors::PRODUCER_FENCED;
-                topicInResponse.Results.push_back(partitionInResponse);
-            }
-            topicsResponse.push_back(topicInResponse);
-        }
-        response->Results = std::move(topicsResponse);
-        return response;
-    };
-
-    template<>
-    std::shared_ptr<TTxnOffsetCommitResponseData> TKafkaTransactionsCoordinator::BuildProducerFencedResponse<TTxnOffsetCommitResponseData, TTxnOffsetCommitRequestData>(TMessagePtr<TTxnOffsetCommitRequestData> request) {
-        auto response = std::make_shared<TTxnOffsetCommitResponseData>();
-        std::vector<TTxnOffsetCommitResponseData::TTxnOffsetCommitResponseTopic> topicsResponse;
-        topicsResponse.reserve(request->Topics.size());
-        for (const auto& requestTopic : request->Topics) {
-            TTxnOffsetCommitResponseData::TTxnOffsetCommitResponseTopic topicInResponse;
-            topicInResponse.Name = requestTopic.Name;
-            topicInResponse.Partitions.reserve(requestTopic.Partitions.size());
-            for (const auto& requestPartition : requestTopic.Partitions) {
-                TTxnOffsetCommitResponseData::TTxnOffsetCommitResponseTopic::TTxnOffsetCommitResponsePartition partitionInResponse;
-                partitionInResponse.PartitionIndex = requestPartition.PartitionIndex;
-                partitionInResponse.ErrorCode = EKafkaErrors::PRODUCER_FENCED;
-                topicInResponse.Partitions.push_back(partitionInResponse);
-            }
-            topicsResponse.push_back(topicInResponse);
-        }
-        response->Topics = std::move(topicsResponse);;
-        return response;
-    };
-
-    bool TKafkaTransactionsCoordinator::NewProducerStateIsOutdated(const TProducerState& currentProducerState, const TProducerState& newProducerState) {
-        bool producerIdAlreadyGreater = currentProducerState.Id > newProducerState.Id;
-        bool producerIdsAreEqual = currentProducerState.Id == newProducerState.Id;
-        bool epochAlreadyGreater = currentProducerState.Epoch > newProducerState.Epoch;
-        return producerIdAlreadyGreater || (producerIdsAreEqual && epochAlreadyGreater);
+    bool TKafkaTransactionsCoordinator::NewProducerStateIsOutdated(const TEvKafka::TProducerInstanceId& currentProducerState, const TEvKafka::TProducerInstanceId& newProducerState) {
+        return currentProducerState > newProducerState;
     };
 
     TMaybe<TString> TKafkaTransactionsCoordinator::GetTxnRequestError(const TTransactionalRequest& request) {
-        if (!ProducersByTransactionalId.contains(request.TransactionalId)) {
+        auto it = ProducersByTransactionalId.find(request.TransactionalId);
+
+        if (it == ProducersByTransactionalId.end()) {
             return TStringBuilder() << "Producer with transactional id " << request.TransactionalId << " was not yet initailized.";
-        } else if (NewProducerStateIsOutdated(ProducersByTransactionalId[request.TransactionalId], request.ProducerState)) {
-            return GetProducerIsOutdatedError(request.TransactionalId, ProducersByTransactionalId[request.TransactionalId], request.ProducerState);
+        } else if (NewProducerStateIsOutdated(it->second, request.ProducerState)) {
+            return GetProducerIsOutdatedError(request.TransactionalId, it->second, request.ProducerState);
         } else {
             return {};
         }
     };
 
-    TString TKafkaTransactionsCoordinator::GetProducerIsOutdatedError(const TString& transactionalId, const TProducerState& currentProducerState, const TProducerState& newProducerState) {
+    TString TKafkaTransactionsCoordinator::GetProducerIsOutdatedError(const TString& transactionalId, const TEvKafka::TProducerInstanceId& currentProducerState, const TEvKafka::TProducerInstanceId& newProducerState) {
         return TStringBuilder() << "Producer with transactional id " << transactionalId <<
                     "is outdated. Current producer id is " << currentProducerState.Id << 
                     " and producer epoch is " << currentProducerState.Epoch << ". Requested producer id is " << newProducerState.Id << 
diff --git a/ydb/core/kafka_proxy/kafka_transactions_coordinator.h b/ydb/core/kafka_proxy/kafka_transactions_coordinator.h
index fd0551bbd2f1..532a123556e1 100644
--- a/ydb/core/kafka_proxy/kafka_transactions_coordinator.h
+++ b/ydb/core/kafka_proxy/kafka_transactions_coordinator.h
@@ -16,15 +16,10 @@ namespace NKafka {
     class TKafkaTransactionsCoordinator : public NActors::TActorBootstrapped<TKafkaTransactionsCoordinator> {
 
         using TBase = NActors::TActorBootstrapped<TKafkaTransactionsCoordinator>;
-    
-        struct TProducerState {
-            i64 Id;
-            i32 Epoch;
-        };
 
         struct TTransactionalRequest {
             TString TransactionalId;
-            TProducerState ProducerState;
+            TEvKafka::TProducerInstanceId ProducerState;
             ui64 CorrelationId;
             TActorId ConnectionId;
         };
@@ -47,6 +42,7 @@ namespace NKafka {
                     HFunc(TEvKafka::TEvAddOffsetsToTxnRequest, Handle);
                     HFunc(TEvKafka::TEvTxnOffsetCommitRequest, Handle);
                     HFunc(TEvKafka::TEvEndTxnRequest, Handle);
+                    HFunc(TEvKafka::TEvTransactionActorDied, Handle);
                     HFunc(TEvents::TEvPoison, Handle);
                 }
             }
@@ -59,6 +55,9 @@ namespace NKafka {
             void Handle(TEvKafka::TEvAddOffsetsToTxnRequest::TPtr& ev, const TActorContext& ctx);
             void Handle(TEvKafka::TEvTxnOffsetCommitRequest::TPtr& ev, const TActorContext& ctx);
             void Handle(TEvKafka::TEvEndTxnRequest::TPtr& ev, const TActorContext& ctx);
+            
+            // remove transaction actor id from TxnActorByTransactionalId
+            void Handle(TEvKafka::TEvTransactionActorDied::TPtr& ev, const TActorContext& ctx);
             // Will kill all txn actors
             void Handle(TEvents::TEvPoison::TPtr& ev, const TActorContext& ctx);
 
@@ -69,14 +68,11 @@ namespace NKafka {
             template<class EventType> 
             void ForwardToTransactionActor(TAutoPtr<TEventHandle<EventType>>& evHandle, const TActorContext& ctx);
 
-            template<class ResponseType, class RequestType>
-            std::shared_ptr<ResponseType> BuildProducerFencedResponse(TMessagePtr<RequestType> request);
-
-            bool NewProducerStateIsOutdated(const TProducerState& currentProducerState, const TProducerState& newProducerState);
+            bool NewProducerStateIsOutdated(const TEvKafka::TProducerInstanceId& currentProducerState, const TEvKafka::TProducerInstanceId& newProducerState);
             TMaybe<TString> GetTxnRequestError(const TTransactionalRequest& request);
-            TString GetProducerIsOutdatedError(const TString& transactionalId, const TProducerState& currentProducerState, const TProducerState& newProducerState);
+            TString GetProducerIsOutdatedError(const TString& transactionalId, const TEvKafka::TProducerInstanceId& currentProducerState, const TEvKafka::TProducerInstanceId& newProducerState);
 
-            std::unordered_map<TString, TProducerState> ProducersByTransactionalId;
+            std::unordered_map<TString, TEvKafka::TProducerInstanceId> ProducersByTransactionalId;
             std::unordered_map<TString, TActorId> TxnActorByTransactionalId;
     };
 
diff --git a/ydb/core/kafka_proxy/kqp_helper.cpp b/ydb/core/kafka_proxy/kqp_helper.cpp
index f59641a3506b..0ca2e3c1ec68 100644
--- a/ydb/core/kafka_proxy/kqp_helper.cpp
+++ b/ydb/core/kafka_proxy/kqp_helper.cpp
@@ -9,9 +9,15 @@ TKqpTxHelper::TKqpTxHelper(TString database)
     : DataBase(database)
 {}
 
-void TKqpTxHelper::SendCreateSessionRequest(const TActorContext& ctx) {
+void TKqpTxHelper::SendCreateSessionRequest(const TActorContext& ctx, const TMaybe<TActorId>& kqpActorId) {
     auto ev = MakeCreateSessionRequest();
-    ctx.Send(MakeKqpProxyID(ctx.SelfID.NodeId()), ev.Release(), 0, 0);
+    TActorId actorId;
+    if (!kqpActorId) {
+        actorId = MakeKqpProxyID(ctx.SelfID.NodeId());
+    } else {
+        actorId = *kqpActorId;
+    }
+    ctx.Send(actorId, ev.Release(), 0, 0);
 }
 
 void TKqpTxHelper::BeginTransaction(ui64 cookie, const TActorContext& ctx) {
@@ -62,7 +68,7 @@ void TKqpTxHelper::SendRequest(THolder<TEvKqp::TEvQueryRequest> request, ui64 co
     ctx.Send(MakeKqpProxyID(ctx.SelfID.NodeId()), request.Release(), 0, cookie);
 }
 
-void TKqpTxHelper::SendYqlRequest(const TString& yqlRequest, NYdb::TParams sqlParams, ui64 cookie, const TActorContext& ctx, bool commit) {
+void TKqpTxHelper::SendYqlRequest(const TString& yqlRequest, NYdb::TParams sqlParams, ui64 cookie, const TActorContext& ctx, bool commit, const TMaybe<TActorId>& kqpActorId) {
     auto ev = MakeHolder<TEvKqp::TEvQueryRequest>();
 
     ev->Record.MutableRequest()->SetAction(NKikimrKqp::QUERY_ACTION_EXECUTE);
@@ -80,7 +86,14 @@ void TKqpTxHelper::SendYqlRequest(const TString& yqlRequest, NYdb::TParams sqlPa
     ev->Record.MutableRequest()->MutableQueryCachePolicy()->set_keep_in_cache(true);
 
     ev->Record.MutableRequest()->MutableYdbParameters()->swap(*(NYdb::TProtoAccessor::GetProtoMapPtr(sqlParams)));
-    ctx.Send(MakeKqpProxyID(ctx.SelfID.NodeId()), ev.Release(), 0, cookie);
+
+    TActorId actorId;
+    if (!kqpActorId) {
+        actorId = MakeKqpProxyID(ctx.SelfID.NodeId());
+    } else {
+        actorId = *kqpActorId;
+    }
+    ctx.Send(actorId, ev.Release(), 0, cookie);
 }
 
 void TKqpTxHelper::CommitTx(ui64 cookie, const TActorContext& ctx) {
diff --git a/ydb/core/kafka_proxy/kqp_helper.h b/ydb/core/kafka_proxy/kqp_helper.h
index 37908184b5d3..ea22719b8e15 100644
--- a/ydb/core/kafka_proxy/kqp_helper.h
+++ b/ydb/core/kafka_proxy/kqp_helper.h
@@ -1,11 +1,9 @@
 #pragma once
 
-#include <ydb/core/client/server/msgbus_server_pq_metacache.h>
-#include <ydb/core/grpc_services/rpc_deferrable.h>
 #include <ydb/core/kqp/common/events/events.h>
 #include <ydb/services/metadata/abstract/initialization.h>
-#include <ydb/core/persqueue/events/global.h>
 #include <ydb/services/metadata/abstract/kqp_common.h>
+#include <ydb/public/sdk/cpp/include/ydb-cpp-sdk/client/proto/accessor.h>
 
 namespace NKafka {
 
@@ -16,13 +14,14 @@ using namespace NActors;
 class TKqpTxHelper {
 public:
     TKqpTxHelper(TString database);
-    void SendCreateSessionRequest(const TActorContext& ctx);
+    // kqp actor id can be changed for testing purposes
+    void SendCreateSessionRequest(const TActorContext& ctx, const TMaybe<TActorId>& kqpActorId = {});
     void BeginTransaction(ui64 cookie, const TActorContext& ctx);
     bool HandleCreateSessionResponse(TEvKqp::TEvCreateSessionResponse::TPtr& ev, const TActorContext& ctx);
     void CloseKqpSession(const TActorContext& ctx);
     void SendRequest(THolder<TEvKqp::TEvQueryRequest> request, ui64 cookie, const TActorContext& ctx);
     void CommitTx(ui64 cookie, const TActorContext& ctx);
-    void SendYqlRequest(const TString& yqlRequest, NYdb::TParams sqlParams, ui64 cookie, const TActorContext& ctx, bool commit = false);
+    void SendYqlRequest(const TString& yqlRequest, NYdb::TParams sqlParams, ui64 cookie, const TActorContext& ctx, bool commit = false, const TMaybe<TActorId>& kqpActorId = {});
     void SendInitTableRequest(const TActorContext& ctx, std::shared_ptr<NKikimr::NMetadata::IClassBehaviour> prepareManager);
     void SetTxId(const TString& txId);
     void ResetTxId();
diff --git a/ydb/core/kafka_proxy/ut/ut_transaction_actor.cpp b/ydb/core/kafka_proxy/ut/ut_transaction_actor.cpp
new file mode 100644
index 000000000000..64666a3af73a
--- /dev/null
+++ b/ydb/core/kafka_proxy/ut/ut_transaction_actor.cpp
@@ -0,0 +1,665 @@
+#include <ydb/core/kafka_proxy/actors/kafka_transaction_actor.h>
+#include <ydb/core/kafka_proxy/kafka_events.h>
+
+#include <library/cpp/testing/unittest/registar.h>
+#include <util/generic/fwd.h>
+#include <ydb/core/kqp/common/simple/services.h>
+#include <ydb/core/persqueue/ut/common/pq_ut_common.h>
+#include <ydb/core/kqp/common/events/events.h>
+#include <ydb/public/sdk/cpp/include/ydb-cpp-sdk/type_switcher.h>
+
+namespace {
+    using namespace NKafka;
+    class TDummyKqpActor : public TActor<TDummyKqpActor> {
+        public:
+            TDummyKqpActor() : TActor<TDummyKqpActor>(&TDummyKqpActor::StateFunc) {}
+
+            void SetValidationResponse(const TString& transactionalId, ui64 producerId, ui64 producerEpoch, const std::unordered_map<TString, i32>& consumerGenerations = {}) {
+                TransactionalIdToReturn = transactionalId;
+                ProducerIdToReturn = producerId;
+                ProducerEpochToReturn = producerEpoch;
+                ConsumerGenerationsToReturn = consumerGenerations;
+            }
+
+            void SetCommitResponse(bool success) {
+                ReturnSuccessOnCommit = success;
+            }
+
+        private:
+            STFUNC(StateFunc) {
+                switch (ev->GetTypeRewrite()) {
+                    HFunc(NKqp::TEvKqp::TEvCreateSessionRequest, Handle);
+                    HFunc(NKqp::TEvKqp::TEvQueryRequest, Handle);
+                }
+            }
+
+            void Handle(NKqp::TEvKqp::TEvCreateSessionRequest::TPtr& ev, const TActorContext& ctx) {
+                auto response = MakeHolder<TEvKqp::TEvCreateSessionResponse>();
+                response->Record.SetYdbStatus(Ydb::StatusIds::SUCCESS);
+                response->Record.MutableResponse()->SetSessionId("123");
+                Send(new IEventHandle(ev->Sender, ctx.SelfID, response.Release(), 0, ev->Cookie));
+            }
+
+            void Handle(NKqp::TEvKqp::TEvQueryRequest::TPtr& ev, const TActorContext& ctx) {
+                Cout << "Handling query request" << Endl;
+                THolder<NKqp::TEvKqp::TEvQueryResponse> response;
+                if (ev->Get()->Record.GetRequest().GetTxControl().commit_tx()) {
+                    Cout << "Sending response on commit from dummy kqp" << Endl;
+                    response = MakeResponseOnCommit();
+                } else {
+                    Cout << "Sending response on select from dummy kqp" << Endl;
+                    response = MakeResponseOnSelectFromKqp();
+                }
+                Send(new IEventHandle(
+                    ev->Sender, 
+                    ctx.SelfID, 
+                    response.Release(),
+                    0, 
+                    ev->Cookie
+                ));
+            }
+
+            THolder<NKqp::TEvKqp::TEvQueryResponse> MakeResponseOnCommit() {
+                auto response = MakeHolder<NKqp::TEvKqp::TEvQueryResponse>();
+                NKikimrKqp::TEvQueryResponse record;
+                if (ReturnSuccessOnCommit) {
+                    record.SetYdbStatus(Ydb::StatusIds::SUCCESS);
+                } else {
+                    record.SetYdbStatus(Ydb::StatusIds::ABORTED);
+                }
+                return response;
+            }
+
+            THolder<NKqp::TEvKqp::TEvQueryResponse> MakeResponseOnSelectFromKqp() {
+                auto response = MakeHolder<NKqp::TEvKqp::TEvQueryResponse>();
+                NKikimrKqp::TEvQueryResponse record;
+
+                record.SetYdbStatus(Ydb::StatusIds::SUCCESS);
+                auto* producerState = record.MutableResponse()->AddYdbResults();
+                *producerState = CreateProducerStateResultsSet();
+                auto* consumersState = record.MutableResponse()->AddYdbResults();
+                *consumersState = CreateConsumersStatesResultSet();
+                
+                response->Record = record;
+                return response;
+            }
+
+            Ydb::ResultSet CreateProducerStateResultsSet() {
+                const std::string resultSetString = TStringBuilder() <<
+                    "columns {\n"
+                    "  name: \"transactional_id\"\n"
+                    "  type {\n"
+                    "    type_id: UTF8\n" 
+                    "  }\n"
+                    "}\n"
+                    "columns {\n"
+                    "  name: \"producer_id\"\n"
+                    "  type {\n"
+                    "    type_id: INT64\n"
+                    "  }\n"
+                    "}\n"
+                    "columns {\n"
+                    "  name: \"producer_epoch\"\n"
+                    "  type {\n"
+                    "    type_id: INT16\n"
+                    "  }\n"
+                    "}\n"
+                    "rows {\n"
+                    "  items {\n"
+                    "    text_value: \"" << TransactionalIdToReturn << "\"\n"
+                    "  }\n"
+                    "  items {\n"
+                    "    int64_value: " << ProducerIdToReturn << "\n"
+                    "  }\n"
+                    "  items {\n"
+                    "    int32_value: " << ProducerEpochToReturn << "\n"
+                    "  }\n"
+                    "}\n";
+                Ydb::ResultSet rsProto;
+                google::protobuf::TextFormat::ParseFromString(NYdb::TStringType{resultSetString}, &rsProto);
+                return rsProto;
+            }
+
+            Ydb::ResultSet CreateConsumersStatesResultSet() {
+                TStringBuilder builder;
+                builder << 
+                    "columns {\n"
+                    "  name: \"consumer_group\"\n"
+                    "  type {\n"
+                    "    type_id: UTF8\n"
+                    "  }\n"
+                    "}\n"
+                    "columns {\n"
+                    "  name: \"generation\"\n"
+                    "  type {\n"
+                    "    type_id: UINT64\n"
+                    "  }\n"
+                    "}\n";
+
+                for (auto& [consumerName, generation] : ConsumerGenerationsToReturn) {
+                    builder <<
+                        "rows {\n"
+                        "  items {\n"
+                        "    text_value: \"" << consumerName << "\"\n"
+                        "  }\n"
+                        "  items {\n"
+                        "    uint64_value: " << generation << "\n"
+                        "  }\n"
+                        "}\n";
+                }
+                Ydb::ResultSet rsProto;
+                google::protobuf::TextFormat::ParseFromString(NYdb::TStringType{builder}, &rsProto);
+                return rsProto;
+            }
+
+            TString TransactionalIdToReturn;
+            ui64 ProducerIdToReturn;
+            ui16 ProducerEpochToReturn;
+            std::unordered_map<TString, i32> ConsumerGenerationsToReturn;
+            bool ReturnSuccessOnCommit;
+        };
+
+    class TTransactionActorFixture : public NUnitTest::TBaseFixture {
+
+        public:
+            struct TTopicPartitions {
+                TString Topic;
+                TVector<ui32> Partitions;
+            };
+
+            struct TConsumerCommitMatcher {
+                TString ConsumerName;
+                i32 GenerationId;
+                // map, where key - topicName, value - vector with pairs, 
+                // where left - partition index, right - consumer offset for this partition
+                std::unordered_map<TString, std::vector<std::pair<ui32, ui64>>> PartitionOffsetsByTopic;
+            };
+
+            struct TQueryRequestMatcher {
+                bool CommitTx;
+                TVector<TTopicPartitions> TopicPartitions;
+                TVector<TConsumerCommitMatcher> ConsumerCommitMatchers;
+            };
+
+            struct TCommitRequest {
+                TString ConsumerName;
+                i32 GenerationId;
+                // map, where key - topicName, value - vector with pairs, 
+                // where left - partition index, right - consumer offset for this partition
+                std::unordered_map<TString, std::vector<std::pair<ui32, ui64>>> PartitionOffsetsToCommitByTopic;
+            };
+
+            using KafkaApiOffsetInTxn = NKikimrKqp::TKafkaApiOperationsRequest::KafkaApiOffsetInTxn;
+
+            TMaybe<NKikimr::NPQ::TTestContext> Ctx;
+            TActorId ActorId;
+            TDummyKqpActor* DummyKqpActor = nullptr;
+            TActorId KqpActorId;
+            const TString Database = "/Root/PQ";
+            const TString TransactionalId = "123"; // transactional id from kafka SDK
+            const ui64 ProducerId = 1;
+            const ui16 ProducerEpoch = 1;
+            ui32 QueryRequestsCounter = 0;
+            
+            void SetUp(NUnitTest::TTestContext&) override {
+                Ctx.ConstructInPlace();
+                
+                Ctx->Prepare();
+                Ctx->Runtime->SetScheduledLimit(5'000);
+                Ctx->Runtime->DisableBreakOnStopCondition();
+                Ctx->Runtime->SetLogPriority(NKikimrServices::KAFKA_PROXY, NLog::PRI_DEBUG);
+                DummyKqpActor = new TDummyKqpActor();
+                KqpActorId = Ctx->Runtime->Register(DummyKqpActor);
+                ActorId = Ctx->Runtime->Register(new TKafkaTransactionActor(
+                    TransactionalId,
+                    ProducerId,
+                    ProducerEpoch,
+                    Database,
+                    KqpActorId,
+                    Ctx->Edge
+                ));
+            }
+
+            void TearDown(NUnitTest::TTestContext&) override  {
+                QueryRequestsCounter = 0;
+                Ctx->Finalize();
+            }
+
+            THolder<NKafka::TEvKafka::TEvResponse> SendAddPartitionsToTxnRequest(std::vector<TTopicPartitions> topicPartitions, ui64 correlationId = 0) {
+                auto message = std::make_shared<NKafka::TAddPartitionsToTxnRequestData>();
+                message->TransactionalId = TransactionalId;
+                message->ProducerId = ProducerId;
+                message->ProducerEpoch = ProducerEpoch;
+                for (const auto& tp : topicPartitions) {
+                    NKafka::TAddPartitionsToTxnRequestData::TAddPartitionsToTxnTopic topic;
+                    topic.Name = tp.Topic;
+                    for (auto partitionIndex : tp.Partitions) {
+                        topic.Partitions.push_back(partitionIndex);
+                    }
+                    message->Topics.push_back(topic);
+                }
+                auto event = MakeHolder<NKafka::TEvKafka::TEvAddPartitionsToTxnRequest>(correlationId, NKafka::TMessagePtr<NKafka::TAddPartitionsToTxnRequestData>({}, message), Ctx->Edge, Database);
+
+                Ctx->Runtime->SingleSys()->Send(new IEventHandle(ActorId, Ctx->Edge, event.Release()));
+
+                return Ctx->Runtime->GrabEdgeEvent<NKafka::TEvKafka::TEvResponse>();
+            }
+
+            THolder<NKafka::TEvKafka::TEvResponse> SendTxnOffsetCommitRequest(const TCommitRequest& commitRequest, ui64 correlationId = 0) {
+                auto message = std::make_shared<NKafka::TTxnOffsetCommitRequestData>();
+                message->TransactionalId = TransactionalId;
+                message->ProducerId = ProducerId;
+                message->ProducerEpoch = ProducerEpoch;
+                message->GroupId = commitRequest.ConsumerName;
+                message->GenerationId = commitRequest.GenerationId;
+                for (auto& [topicName, commits]: commitRequest.PartitionOffsetsToCommitByTopic) {
+                    NKafka::TTxnOffsetCommitRequestData::TTxnOffsetCommitRequestTopic topic;
+                    topic.Name = topicName;
+                    for (const auto& commit : commits) {
+                        NKafka::TTxnOffsetCommitRequestData::TTxnOffsetCommitRequestTopic::TTxnOffsetCommitRequestPartition partition;
+                        partition.PartitionIndex = commit.first;
+                        partition.CommittedOffset = commit.second;
+                        topic.Partitions.push_back(partition);
+                    }
+                    message->Topics.push_back(topic);
+                }
+                auto event = MakeHolder<NKafka::TEvKafka::TEvTxnOffsetCommitRequest>(correlationId, NKafka::TMessagePtr<NKafka::TTxnOffsetCommitRequestData>({}, message), Ctx->Edge, Database);
+
+                Ctx->Runtime->SingleSys()->Send(new IEventHandle(ActorId, Ctx->Edge, event.Release()));
+
+                return Ctx->Runtime->GrabEdgeEvent<NKafka::TEvKafka::TEvResponse>();
+            }
+
+            THolder<NKafka::TEvKafka::TEvResponse> SendEndTxnRequest(bool commit = false, ui64 correlationId = 0) {
+                auto message = std::make_shared<NKafka::TEndTxnRequestData>();
+                message->TransactionalId = TransactionalId;
+                message->ProducerId = ProducerId;
+                message->ProducerEpoch = ProducerEpoch;
+                message->Committed = commit;
+                auto event = MakeHolder<NKafka::TEvKafka::TEvEndTxnRequest>(correlationId, NKafka::TMessagePtr<NKafka::TEndTxnRequestData>({}, message), Ctx->Edge, Database);
+                
+                Ctx->Runtime->SingleSys()->Send(new IEventHandle(ActorId, Ctx->Edge, event.Release()));
+
+                return Ctx->Runtime->GrabEdgeEvent<NKafka::TEvKafka::TEvResponse>();
+            }
+
+            void SendPoisonPill() {
+                Ctx->Runtime->SingleSys()->Send(new IEventHandle(ActorId, Ctx->Edge, new NActors::TEvents::TEvPoison()));
+            }
+
+            // Arguments:
+            // 1. callback - function, that will be called on recieving the response from KQP om commit request
+            // 2. consumerGenerationsToReturnInValidationRequest - map of consumer name to its generation to ensure proper validation of consumer state by actor
+            void AddObserverForCommitRequestToKqp(std::function<void(const TEvKqp::TEvQueryRequest*)> callback, std::unordered_map<TString, i32> consumerGenerationsToReturnInValidationRequest = {}) {
+                DummyKqpActor->SetValidationResponse(TransactionalId, ProducerId, ProducerEpoch, consumerGenerationsToReturnInValidationRequest);
+
+                auto observer = [callback = std::move(callback), this](TAutoPtr<IEventHandle>& input) {
+                    // handle query request
+                    if (auto* event = input->CastAsLocal<TEvKqp::TEvQueryRequest>()) {
+                        // first request is a validation request with select statements
+                        // second request should be a commit request
+                        if (QueryRequestsCounter == 0) {
+                            QueryRequestsCounter++;
+                        } else {
+                            callback(event);
+                        }
+                    }
+
+                    return TTestActorRuntimeBase::EEventAction::PROCESS;
+                };
+
+                Ctx->Runtime->SetObserverFunc(observer);
+            }
+
+            void MatchQueryRequest(const NKikimr::NKqp::TEvKqp::TEvQueryRequest* request, const TQueryRequestMatcher& matcher) {
+                UNIT_ASSERT_VALUES_EQUAL(request->Record.GetRequest().GetTxControl().begin_tx().has_serializable_read_write(), false);
+                UNIT_ASSERT_VALUES_EQUAL(request->Record.GetRequest().GetTxControl().commit_tx(), matcher.CommitTx);
+                UNIT_ASSERT_VALUES_EQUAL(request->Record.GetRequest().GetKafkaApiOperations().GetTransactionalId(), TransactionalId);
+                UNIT_ASSERT_VALUES_EQUAL(request->Record.GetRequest().GetKafkaApiOperations().GetProducerId(), ProducerId);
+                UNIT_ASSERT_VALUES_EQUAL(request->Record.GetRequest().GetKafkaApiOperations().GetProducerEpoch(), ProducerEpoch);
+                
+                MatchPartitionsInTxn(request, matcher);
+                MatchOffsetsInTxn(request, matcher);
+            }
+
+        private:
+            void MatchPartitionsInTxn(const NKikimr::NKqp::TEvKqp::TEvQueryRequest* request, const TQueryRequestMatcher& matcher) {
+                auto& partitionsInRequest = request->Record.GetRequest().GetKafkaApiOperations().GetPartitionsInTxn();
+                std::unordered_set<TKafkaTransactionActor::TTopicPartition, TKafkaTransactionActor::TopicPartitionHashFn> paritionsInRequestSet;
+                paritionsInRequestSet.reserve(partitionsInRequest.size());
+                for (auto& partition : partitionsInRequest) {
+                    paritionsInRequestSet.emplace(partition.GetTopicPath(), partition.GetPartitionId());
+                };
+
+                ui32 totalExpectedPartitions = 0;
+                for (ui32 i = 0; i < matcher.TopicPartitions.size(); i++) {
+                    auto& topicPartition = matcher.TopicPartitions[i];
+                    TString expectedTopicPath = GetExpectedTopicPath(topicPartition.Topic);
+                    for (auto partition : topicPartition.Partitions) {
+                        totalExpectedPartitions++;
+                        UNIT_ASSERT(paritionsInRequestSet.contains({expectedTopicPath, partition}));
+                    }
+                }
+                UNIT_ASSERT_VALUES_EQUAL(request->Record.GetRequest().GetKafkaApiOperations().PartitionsInTxnSize(), totalExpectedPartitions);
+            }
+
+            void MatchOffsetsInTxn(const NKikimr::NKqp::TEvKqp::TEvQueryRequest* request, const TQueryRequestMatcher& matcher) {
+                auto& offsetCommitsInRequest = request->Record.GetRequest().GetKafkaApiOperations().GetOffsetsInTxn();
+
+                for (auto& consumerCommitMatcher : matcher.ConsumerCommitMatchers) {
+                    for (auto& [topicName, partitionOffsets] : consumerCommitMatcher.PartitionOffsetsByTopic) {
+                        for (auto& partitionOffset : partitionOffsets) {
+                            auto it = std::find_if(offsetCommitsInRequest.begin(), offsetCommitsInRequest.end(), [&](const KafkaApiOffsetInTxn& offsetCommit) {
+                                return offsetCommit.GetTopicPath() == GetExpectedTopicPath(topicName) 
+                                    && offsetCommit.GetPartitionId() == partitionOffset.first
+                                    && offsetCommit.GetConsumerName() == consumerCommitMatcher.ConsumerName;
+                            });
+    
+                            UNIT_ASSERT_C(it != offsetCommitsInRequest.end(), TStringBuilder() << 
+                                "Consumer commit for consuemr group " << consumerCommitMatcher.ConsumerName <<
+                                " and topic partition " << topicName << "-" << partitionOffset.first <<
+                                " not found in txn");
+                            UNIT_ASSERT_VALUES_EQUAL(it->GetTopicPath(), GetExpectedTopicPath(topicName));
+                            UNIT_ASSERT_VALUES_EQUAL(it->GetPartitionId(), partitionOffset.first);
+                            UNIT_ASSERT_VALUES_EQUAL(it->GetOffset(), partitionOffset.second);
+                            UNIT_ASSERT_VALUES_EQUAL(it->GetConsumerName(), consumerCommitMatcher.ConsumerName);
+                            UNIT_ASSERT_VALUES_EQUAL(it->GetConsumerGeneration(), consumerCommitMatcher.GenerationId);
+                        }
+                    }
+                }
+            }
+
+            TString GetExpectedTopicPath(const TString& topicName) {
+                return TStringBuilder() << Database << "/" << topicName;
+            }
+    };
+
+    Y_UNIT_TEST_SUITE_F(KafkaTransactionActor, TTransactionActorFixture) {
+        Y_UNIT_TEST(OnAddPartitionsAndEndTxn_shouldSendTxnToKqpWithSpecifiedPartitions) {
+            TVector<TTopicPartitions> topics = {{"topic1", {0, 1}}, {"topic2", {0}}};
+            bool seenEvent = false;
+            AddObserverForCommitRequestToKqp([&](const NKikimr::NKqp::TEvKqp::TEvQueryRequest* request) {
+                MatchQueryRequest(request, {true, topics, {}});
+                seenEvent = true;
+            });
+
+            SendAddPartitionsToTxnRequest(topics);
+            SendEndTxnRequest(true);
+
+            TDispatchOptions options;
+            options.CustomFinalCondition = [&seenEvent]() {
+                return seenEvent;
+            };
+            UNIT_ASSERT(Ctx->Runtime->DispatchEvents(options));
+            UNIT_ASSERT(seenEvent);
+        }
+
+        Y_UNIT_TEST(OnAddPartitionsToTxn_shouldReturnOkToSDK) {
+            TVector<TTopicPartitions> topics = {{"topic1", {0, 1}}, {"topic2", {0}}};
+            ui64 correlationId = 123;
+
+            auto response = SendAddPartitionsToTxnRequest(topics, correlationId);
+
+            UNIT_ASSERT(response != nullptr);
+            UNIT_ASSERT_VALUES_EQUAL(response->ErrorCode, NKafka::EKafkaErrors::NONE_ERROR);
+            UNIT_ASSERT_EQUAL(response->Response->ApiKey(), NKafka::EApiKey::ADD_PARTITIONS_TO_TXN);
+            const auto& message = static_cast<const NKafka::TAddPartitionsToTxnResponseData&>(*response->Response);
+            UNIT_ASSERT_VALUES_EQUAL(response->CorrelationId, correlationId);
+            UNIT_ASSERT_VALUES_EQUAL(message.Results.size(), 2);
+            UNIT_ASSERT_VALUES_EQUAL(message.Results[0].Name, "topic1");
+            UNIT_ASSERT_VALUES_EQUAL(message.Results[0].Results.size(), 2);
+            UNIT_ASSERT_VALUES_EQUAL(message.Results[0].Results[0].PartitionIndex, 0);
+            UNIT_ASSERT_EQUAL(message.Results[0].Results[0].ErrorCode, NKafka::EKafkaErrors::NONE_ERROR);
+            UNIT_ASSERT_VALUES_EQUAL(message.Results[0].Results[1].PartitionIndex, 1);
+            UNIT_ASSERT_EQUAL(message.Results[0].Results[1].ErrorCode, NKafka::EKafkaErrors::NONE_ERROR);
+            UNIT_ASSERT_VALUES_EQUAL(message.Results[1].Name, "topic2");
+            UNIT_ASSERT_VALUES_EQUAL(message.Results[1].Results.size(), 1);
+            UNIT_ASSERT_VALUES_EQUAL(message.Results[1].Results[0].PartitionIndex, 0);
+            UNIT_ASSERT_EQUAL(message.Results[1].Results[0].ErrorCode, NKafka::EKafkaErrors::NONE_ERROR);
+        }
+
+        Y_UNIT_TEST(OnDoubleAddPartitionsWithSamePartitionsAndEndTxn_shouldSendTxnToKqpWithOnceSpecifiedPartitions) {
+            TVector<TTopicPartitions> topics = {{"topic1", {0}}};
+            bool seenEvent = false;
+            AddObserverForCommitRequestToKqp([&](const NKikimr::NKqp::TEvKqp::TEvQueryRequest* request) {
+                MatchQueryRequest(request, {true, topics, {}});
+                seenEvent = true;
+            });
+
+            SendAddPartitionsToTxnRequest(topics);
+            SendAddPartitionsToTxnRequest(topics);
+            SendEndTxnRequest(true);
+
+            TDispatchOptions options;
+            options.CustomFinalCondition = [&seenEvent]() {
+                return seenEvent;
+            };
+            UNIT_ASSERT(Ctx->Runtime->DispatchEvents(options));
+            UNIT_ASSERT(seenEvent);
+        }
+
+        Y_UNIT_TEST(OnDoubleAddPartitionsWithDifferentPartitionsAndEndTxn_shouldSendTxnToKqpWithAllSpecifiedPartitions) {
+            TVector<TTopicPartitions> topics1 = {{"topic1", {0}}};
+            TVector<TTopicPartitions> topics2 = {{"topic2", {0}}};
+            bool seenEvent = false;
+            AddObserverForCommitRequestToKqp([&](const NKikimr::NKqp::TEvKqp::TEvQueryRequest* request) {
+                MatchQueryRequest(request, {true, {{"topic1", {0}}, {"topic2", {0}}}, {}});
+                seenEvent = true;
+            });
+
+            SendAddPartitionsToTxnRequest(topics1);
+            SendAddPartitionsToTxnRequest(topics2);
+            SendEndTxnRequest(true);
+
+            TDispatchOptions options;
+            options.CustomFinalCondition = [&seenEvent]() {
+                return seenEvent;
+            };
+            UNIT_ASSERT(Ctx->Runtime->DispatchEvents(options));
+            UNIT_ASSERT(seenEvent);
+        }
+
+        Y_UNIT_TEST(OnTxnOffsetCommit_shouldSendTxnToKqpWithSpecifiedOffsets) {
+            TString consumerName = "my-consumer";
+            i32 consumerGeneration = 0;
+            std::unordered_map<TString, std::vector<std::pair<ui32, ui64>>> partitionOffsetsToCommitByTopic;
+            partitionOffsetsToCommitByTopic["topic1"] = {{0, 0}};
+            partitionOffsetsToCommitByTopic["topic2"] = {{0, 10}, {1, 5}};
+            std::unordered_map<TString, i32> consumerGenerationByNameToReturnFromKqp;
+            consumerGenerationByNameToReturnFromKqp[consumerName] = consumerGeneration;
+            bool seenEvent = false;
+            AddObserverForCommitRequestToKqp([&](const NKikimr::NKqp::TEvKqp::TEvQueryRequest* request) {
+                MatchQueryRequest(request, {true, {}, {{consumerName, consumerGeneration, partitionOffsetsToCommitByTopic}}});
+                seenEvent = true;
+            }, consumerGenerationByNameToReturnFromKqp);
+
+            SendTxnOffsetCommitRequest({consumerName, consumerGeneration, partitionOffsetsToCommitByTopic});
+            SendEndTxnRequest(true);
+
+            TDispatchOptions options;
+            options.CustomFinalCondition = [&seenEvent]() {
+                return seenEvent;
+            };
+            UNIT_ASSERT(Ctx->Runtime->DispatchEvents(options));
+            UNIT_ASSERT(seenEvent);
+        }
+
+        Y_UNIT_TEST(OnDoubleTxnOffsetCommitRequest_shouldSendToKqpLatestGeneration) {
+            TString consumerName = "my-consumer";
+            i32 consumerGenerationFromTable = 1;
+            std::unordered_map<TString, std::vector<std::pair<ui32, ui64>>> partitionOffsetsToCommitByTopic;
+            partitionOffsetsToCommitByTopic["topic1"] = {{0, 0}};
+            std::unordered_map<TString, i32> consumerGenerationByNameToReturnFromKqp;
+            consumerGenerationByNameToReturnFromKqp[consumerName] = consumerGenerationFromTable;
+            bool seenEvent = false;
+            AddObserverForCommitRequestToKqp([&](const NKikimr::NKqp::TEvKqp::TEvQueryRequest* request) {
+                MatchQueryRequest(request, {true, {}, {{consumerName, consumerGenerationFromTable, partitionOffsetsToCommitByTopic}}});
+                seenEvent = true;
+            }, consumerGenerationByNameToReturnFromKqp);
+
+            // first request with old generation
+            SendTxnOffsetCommitRequest({consumerName, consumerGenerationFromTable - 1, partitionOffsetsToCommitByTopic});
+            // second with actual generation
+            SendTxnOffsetCommitRequest({consumerName, consumerGenerationFromTable, partitionOffsetsToCommitByTopic});
+            SendEndTxnRequest(true);
+
+            TDispatchOptions options;
+            options.CustomFinalCondition = [&seenEvent]() {
+                return seenEvent;
+            };
+            UNIT_ASSERT(Ctx->Runtime->DispatchEvents(options));
+            UNIT_ASSERT(seenEvent);
+        }
+
+        Y_UNIT_TEST(OnDoubleTxnOffsetCommitRequest_shouldSendToKqpLatestSpecifiedOffsets) {
+            TString consumerName = "my-consumer";
+            i32 consumerGeneration = 0;
+            std::unordered_map<TString, std::vector<std::pair<ui32, ui64>>> firstRequestOffsetsByTopic;
+            firstRequestOffsetsByTopic["topic1"] = {{0, 0}};
+            std::unordered_map<TString, std::vector<std::pair<ui32, ui64>>> secondRequestOffsetsByTopic;
+            secondRequestOffsetsByTopic["topic1"] = {{0, 1}};
+            std::unordered_map<TString, i32> consumerGenerationByNameToReturnFromKqp;
+            consumerGenerationByNameToReturnFromKqp[consumerName] = consumerGeneration;
+            bool seenEvent = false;
+            AddObserverForCommitRequestToKqp([&](const NKikimr::NKqp::TEvKqp::TEvQueryRequest* request) {
+                // we validate that to KQP are sent offsets from second request
+                MatchQueryRequest(request, {true, {}, {{consumerName, consumerGeneration, secondRequestOffsetsByTopic}}});
+                seenEvent = true;
+            }, consumerGenerationByNameToReturnFromKqp);
+
+            SendTxnOffsetCommitRequest({consumerName, consumerGeneration, firstRequestOffsetsByTopic});
+            SendTxnOffsetCommitRequest({consumerName, consumerGeneration, secondRequestOffsetsByTopic});
+            SendEndTxnRequest(true);
+
+            TDispatchOptions options;
+            options.CustomFinalCondition = [&seenEvent]() {
+                return seenEvent;
+            };
+            UNIT_ASSERT(Ctx->Runtime->DispatchEvents(options));
+            UNIT_ASSERT(seenEvent);
+        }
+
+        Y_UNIT_TEST(OnTxnOffsetCommit_shouldReturnOkToSDK) {
+            TString consumerName = "my-consumer";
+            i32 consumerGeneration = 0;
+            std::unordered_map<TString, std::vector<std::pair<ui32, ui64>>> partitionOffsetsToCommitByTopic;
+            partitionOffsetsToCommitByTopic["topic1"] = {{0, 0}};
+            partitionOffsetsToCommitByTopic["topic2"] = {{0, 10}, {1, 5}};
+            ui64 correlationId = 123;
+
+            auto response = SendTxnOffsetCommitRequest({consumerName, consumerGeneration, partitionOffsetsToCommitByTopic}, correlationId);
+
+            UNIT_ASSERT(response != nullptr);
+            UNIT_ASSERT_VALUES_EQUAL(response->ErrorCode, NKafka::EKafkaErrors::NONE_ERROR);
+            UNIT_ASSERT_EQUAL(response->Response->ApiKey(), NKafka::EApiKey::TXN_OFFSET_COMMIT);
+            const auto& message = static_cast<const NKafka::TTxnOffsetCommitResponseData&>(*response->Response);
+            UNIT_ASSERT_VALUES_EQUAL(response->CorrelationId, correlationId);
+            UNIT_ASSERT_VALUES_EQUAL(message.Topics.size(), 2);
+            UNIT_ASSERT_VALUES_EQUAL(message.Topics[0].Name, "topic1");
+            UNIT_ASSERT_VALUES_EQUAL(message.Topics[0].Partitions.size(), 1);
+            UNIT_ASSERT_VALUES_EQUAL(message.Topics[0].Partitions[0].PartitionIndex, 0);
+            UNIT_ASSERT_EQUAL(message.Topics[0].Partitions[0].ErrorCode, NKafka::EKafkaErrors::NONE_ERROR);
+            UNIT_ASSERT_VALUES_EQUAL(message.Topics[1].Name, "topic2");
+            UNIT_ASSERT_VALUES_EQUAL(message.Topics[1].Partitions.size(), 2);
+            UNIT_ASSERT_VALUES_EQUAL(message.Topics[1].Partitions[0].PartitionIndex, 0);
+            UNIT_ASSERT_EQUAL(message.Topics[1].Partitions[0].ErrorCode, NKafka::EKafkaErrors::NONE_ERROR);
+            UNIT_ASSERT_VALUES_EQUAL(message.Topics[1].Partitions.size(), 2);
+            UNIT_ASSERT_VALUES_EQUAL(message.Topics[1].Partitions[1].PartitionIndex, 1);
+            UNIT_ASSERT_EQUAL(message.Topics[1].Partitions[1].ErrorCode, NKafka::EKafkaErrors::NONE_ERROR);
+        }
+
+        Y_UNIT_TEST(OnAddOffsetsToTxnAndAddPartitionsAndEndTxn_shouldSendToKqpCorrectTxn) {
+            TVector<TTopicPartitions> topics = {{"topic3", {0}}};
+            TString consumerName = "my-consumer";
+            i32 consumerGeneration = 0;
+            std::unordered_map<TString, std::vector<std::pair<ui32, ui64>>> partitionOffsetsToCommitByTopic;
+            partitionOffsetsToCommitByTopic["topic1"] = {{0, 0}};
+            partitionOffsetsToCommitByTopic["topic2"] = {{0, 10}, {1, 5}};
+            std::unordered_map<TString, i32> consumerGenerationByNameToReturnFromKqp;
+            consumerGenerationByNameToReturnFromKqp[consumerName] = consumerGeneration;
+            bool seenEvent = false;
+            AddObserverForCommitRequestToKqp([&](const NKikimr::NKqp::TEvKqp::TEvQueryRequest* request) {
+                MatchQueryRequest(request, {true, topics, {{consumerName, consumerGeneration, partitionOffsetsToCommitByTopic}}});
+                seenEvent = true;
+            }, consumerGenerationByNameToReturnFromKqp);
+
+            SendAddPartitionsToTxnRequest(topics);
+            SendTxnOffsetCommitRequest({consumerName, consumerGeneration, partitionOffsetsToCommitByTopic});
+            SendEndTxnRequest(true);
+
+            TDispatchOptions options;
+            options.CustomFinalCondition = [&seenEvent]() {
+                return seenEvent;
+            };
+            UNIT_ASSERT(Ctx->Runtime->DispatchEvents(options));
+            UNIT_ASSERT(seenEvent);
+        }
+
+        Y_UNIT_TEST(OnEndTxnWithAbort_shouldSendOkAndDie) {
+            ui64 correlationId = 123;
+
+            auto response = SendEndTxnRequest(false, correlationId);
+
+            UNIT_ASSERT(response != nullptr);
+            UNIT_ASSERT_VALUES_EQUAL(response->ErrorCode, NKafka::EKafkaErrors::NONE_ERROR);
+            UNIT_ASSERT_EQUAL(response->Response->ApiKey(), NKafka::EApiKey::END_TXN);
+            const auto& result = static_cast<const NKafka::TEndTxnResponseData&>(*response->Response);
+            UNIT_ASSERT_VALUES_EQUAL(response->CorrelationId, correlationId);
+            UNIT_ASSERT_VALUES_EQUAL(result.ErrorCode, NKafka::EKafkaErrors::NONE_ERROR);
+        }
+
+        Y_UNIT_TEST(OnEndTxnWithCommitAndAbortFromTxn_shouldReturnPRODUCER_FENCED) {
+            ui64 correlationId = 987;
+            DummyKqpActor->SetCommitResponse(false);
+
+            auto response = SendEndTxnRequest(true, correlationId);
+
+            UNIT_ASSERT(response != nullptr);
+            UNIT_ASSERT_VALUES_EQUAL(response->ErrorCode, NKafka::EKafkaErrors::PRODUCER_FENCED);
+            UNIT_ASSERT_EQUAL(response->Response->ApiKey(), NKafka::EApiKey::END_TXN);
+            const auto& result = static_cast<const NKafka::TEndTxnResponseData&>(*response->Response);
+            UNIT_ASSERT_VALUES_EQUAL(response->CorrelationId, correlationId);
+            UNIT_ASSERT_VALUES_EQUAL(result.ErrorCode, NKafka::EKafkaErrors::PRODUCER_FENCED);
+        }
+
+        Y_UNIT_TEST(OnEndTxnWithCommitAndNoConsumerStateFound_shouldReturnINVALID_TXN_STATE) {
+            std::unordered_map<TString, std::vector<std::pair<ui32, ui64>>> partitionOffsetsToCommitByTopic;
+            partitionOffsetsToCommitByTopic["topic1"] = {{0, 0}};
+            std::unordered_map<TString, i32> consumerGenerationByNameToReturnFromKqp; // empty
+            DummyKqpActor->SetValidationResponse(TransactionalId, ProducerId, ProducerEpoch, consumerGenerationByNameToReturnFromKqp);
+
+            SendTxnOffsetCommitRequest({"my-consumer", 0, partitionOffsetsToCommitByTopic});
+            auto response = SendEndTxnRequest(true);
+
+            UNIT_ASSERT(response != nullptr);
+            UNIT_ASSERT_VALUES_EQUAL(response->ErrorCode, NKafka::EKafkaErrors::PRODUCER_FENCED);
+            UNIT_ASSERT_EQUAL(response->Response->ApiKey(), NKafka::EApiKey::END_TXN);
+            const auto& result = static_cast<const NKafka::TEndTxnResponseData&>(*response->Response);
+            UNIT_ASSERT_VALUES_EQUAL(result.ErrorCode, NKafka::EKafkaErrors::PRODUCER_FENCED);
+        }
+
+        Y_UNIT_TEST(OnEndTxnWithCommitAndExpiredProducerState_shouldReturnProducerFenced) {
+            DummyKqpActor->SetValidationResponse(TransactionalId, ProducerId, ProducerEpoch + 1);
+
+            SendAddPartitionsToTxnRequest({{"topic1", {0}}});
+            auto response = SendEndTxnRequest(true);
+
+            UNIT_ASSERT(response != nullptr);
+            UNIT_ASSERT_VALUES_EQUAL(response->ErrorCode, NKafka::EKafkaErrors::PRODUCER_FENCED);
+            UNIT_ASSERT_EQUAL(response->Response->ApiKey(), NKafka::EApiKey::END_TXN);
+            const auto& result = static_cast<const NKafka::TEndTxnResponseData&>(*response->Response);
+            UNIT_ASSERT_VALUES_EQUAL(result.ErrorCode, NKafka::EKafkaErrors::PRODUCER_FENCED);
+        }
+
+        Y_UNIT_TEST(OnDie_ShouldSendRemoveTransactionActorRequestToTxnCoordinator) {
+            SendPoisonPill();
+
+            auto response = Ctx->Runtime->GrabEdgeEvent<NKafka::TEvKafka::TEvTransactionActorDied>();
+
+            UNIT_ASSERT(response != nullptr);
+            UNIT_ASSERT_VALUES_EQUAL(response->TransactionalId, TransactionalId);
+            UNIT_ASSERT_VALUES_EQUAL(response->ProducerState.Id, ProducerId);
+            UNIT_ASSERT_VALUES_EQUAL(response->ProducerState.Epoch, ProducerEpoch);
+        }
+    }
+} // namespace
\ No newline at end of file
diff --git a/ydb/core/kafka_proxy/ut/ut_transaction_coordinator.cpp b/ydb/core/kafka_proxy/ut/ut_transaction_coordinator.cpp
index 3dbe77d737ce..5262312e3c15 100644
--- a/ydb/core/kafka_proxy/ut/ut_transaction_coordinator.cpp
+++ b/ydb/core/kafka_proxy/ut/ut_transaction_coordinator.cpp
@@ -15,6 +15,7 @@ namespace {
         public:
             TMaybe<NKikimr::NPQ::TTestContext> Ctx;
             TActorId ActorId;
+            const TString Database = "/Root/PQ";
             
             void SetUp(NUnitTest::TTestContext&) override {
                 Ctx.ConstructInPlace();
@@ -30,7 +31,7 @@ namespace {
             }
 
             THolder<NKafka::TEvKafka::TEvSaveTxnProducerResponse> SaveTxnProducer(const TString& txnId, i64 producerId, i16 producerEpoch) {
-                auto request = MakeHolder<NKafka::TEvKafka::TEvSaveTxnProducerRequest>(txnId, producerId, producerEpoch);
+                auto request = MakeHolder<NKafka::TEvKafka::TEvSaveTxnProducerRequest>(txnId, NKafka::TEvKafka::TProducerInstanceId{producerId, producerEpoch});
                 Ctx->Runtime->SingleSys()->Send(new IEventHandle(ActorId, Ctx->Edge, request.Release()));
                 auto response = Ctx->Runtime->GrabEdgeEvent<NKafka::TEvKafka::TEvSaveTxnProducerResponse>();
                 UNIT_ASSERT(response != nullptr);
@@ -50,7 +51,7 @@ namespace {
                     }
                     message->Topics.push_back(topic);
                 }
-                auto event = MakeHolder<NKafka::TEvKafka::TEvAddPartitionsToTxnRequest>(correlationId, NKafka::TMessagePtr<NKafka::TAddPartitionsToTxnRequestData>({}, message), Ctx->Edge);
+                auto event = MakeHolder<NKafka::TEvKafka::TEvAddPartitionsToTxnRequest>(correlationId, NKafka::TMessagePtr<NKafka::TAddPartitionsToTxnRequestData>({}, message), Ctx->Edge, Database);
                 Ctx->Runtime->SingleSys()->Send(new IEventHandle(ActorId, Ctx->Edge, event.Release()));
             }
 
@@ -69,7 +70,7 @@ namespace {
                     }
                     message->Topics.push_back(topic);
                 }
-                auto event = MakeHolder<NKafka::TEvKafka::TEvTxnOffsetCommitRequest>(correlationId, NKafka::TMessagePtr<NKafka::TTxnOffsetCommitRequestData>({}, message), Ctx->Edge);
+                auto event = MakeHolder<NKafka::TEvKafka::TEvTxnOffsetCommitRequest>(correlationId, NKafka::TMessagePtr<NKafka::TTxnOffsetCommitRequestData>({}, message), Ctx->Edge, Database);
                 Ctx->Runtime->SingleSys()->Send(new IEventHandle(ActorId, Ctx->Edge, event.Release()));
             }
 
@@ -78,7 +79,7 @@ namespace {
                 message->TransactionalId = txnId;
                 message->ProducerId = producerId;
                 message->ProducerEpoch = producerEpoch;
-                auto event = MakeHolder<NKafka::TEvKafka::TEvEndTxnRequest>(correlationId, NKafka::TMessagePtr<NKafka::TEndTxnRequestData>({}, message), Ctx->Edge);
+                auto event = MakeHolder<NKafka::TEvKafka::TEvEndTxnRequest>(correlationId, NKafka::TMessagePtr<NKafka::TEndTxnRequestData>({}, message), Ctx->Edge, Database);
                 Ctx->Runtime->SingleSys()->Send(new IEventHandle(ActorId, Ctx->Edge, event.Release()));
             }
     };
@@ -306,5 +307,4 @@ namespace {
             UNIT_ASSERT_EQUAL(firstResponse->ErrorCode, NKafka::EKafkaErrors::PRODUCER_FENCED);
         }
     }
-
-} // namespace
\ No newline at end of file
+} // namespace
diff --git a/ydb/core/kafka_proxy/ut/ya.make b/ydb/core/kafka_proxy/ut/ya.make
index 799550cfe59f..99c2ca373030 100644
--- a/ydb/core/kafka_proxy/ut/ya.make
+++ b/ydb/core/kafka_proxy/ut/ya.make
@@ -13,6 +13,7 @@ SRCS(
     ut_serialization.cpp
     metarequest_ut.cpp
     ut_transaction_coordinator.cpp
+    ut_transaction_actor.cpp
     actors_ut.cpp
 )
 
diff --git a/ydb/core/kafka_proxy/ya.make b/ydb/core/kafka_proxy/ya.make
index a7dee4ff78a0..98da7f8badb1 100644
--- a/ydb/core/kafka_proxy/ya.make
+++ b/ydb/core/kafka_proxy/ya.make
@@ -21,6 +21,9 @@ SRCS(
     actors/kafka_describe_configs_actor.cpp
     actors/kafka_balance_actor_sql.cpp
     actors/kafka_balancer_actor.cpp
+    actors/txn_actor_response_builder.cpp
+    actors/kafka_transaction_actor_sql.cpp
+    actors/kafka_transaction_actor.cpp
     kafka_connection.cpp
     kafka_connection.h
     kafka_constants.h
diff --git a/ydb/core/protos/kqp.proto b/ydb/core/protos/kqp.proto
index 11fa34060a3c..3fed3c2fd4a9 100644
--- a/ydb/core/protos/kqp.proto
+++ b/ydb/core/protos/kqp.proto
@@ -99,7 +99,26 @@ message TTopicOperationsRequest {
     }
 }
 
+message TKafkaApiOperationsRequest {
+    optional string TransactionalId = 1;
+    optional int64 ProducerId = 2;
+    optional int32 ProducerEpoch = 3;
+    repeated TKafkaApiOperationsRequest.KafkaApiPartitionInTxn PartitionsInTxn = 4;
+    repeated TKafkaApiOperationsRequest.KafkaApiOffsetInTxn OffsetsInTxn = 5;
+
+    message KafkaApiPartitionInTxn {
+        optional string TopicPath = 1;
+        optional uint32 PartitionId = 2;
+    }
 
+    message KafkaApiOffsetInTxn {
+        optional string TopicPath = 1;
+        optional uint32 PartitionId = 2;
+        optional string ConsumerName = 3;
+        optional int64 ConsumerGeneration = 4;
+        optional int64 Offset = 5;
+    }
+}
 
 message TTopicOperationsResponse {
     message TWriteId {
@@ -148,6 +167,7 @@ message TQueryRequest {
     optional uint64 OutputChunkMaxSize = 34;
     optional string PoolId = 35;
     optional string DatabaseId = 36;
+    optional TKafkaApiOperationsRequest KafkaApiOperations = 37;
 }
 
 message TKqpPathIdProto {


Reviews:
user: login: Alek5andr-Kotov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T09:54:44Z
user: login: Alek5andr-Kotov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T10:08:25Z
user: login: Alek5andr-Kotov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T10:12:27Z
user: login: Alek5andr-Kotov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T10:14:17Z
user: login: Alek5andr-Kotov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T10:17:43Z
user: login: Alek5andr-Kotov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T10:18:36Z
user: login: Alek5andr-Kotov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T10:26:47Z
user: login: Alek5andr-Kotov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T10:44:50Z
user: login: Alek5andr-Kotov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T10:46:38Z
user: login: Alek5andr-Kotov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T10:47:24Z
user: login: a-serebryanskiy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-20T10:56:36Z
user: login: qyryq
state: DISMISSED
author_association: NONE
submitted_at: 2025-04-21T07:01:31Z
user: login: Alek5andr-Kotov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T07:01:34Z
user: login: Alek5andr-Kotov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T07:02:45Z
user: login: a-serebryanskiy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T07:11:12Z
user: login: Alek5andr-Kotov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T07:16:20Z
user: login: Alek5andr-Kotov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T07:18:44Z
user: login: Alek5andr-Kotov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T07:20:15Z
user: login: Alek5andr-Kotov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T07:21:26Z
user: login: a-serebryanskiy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T07:22:53Z
user: login: a-serebryanskiy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T07:23:30Z
user: login: a-serebryanskiy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T07:24:18Z
user: login: Alek5andr-Kotov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T07:24:41Z
user: login: a-serebryanskiy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T07:25:39Z
user: login: Alek5andr-Kotov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T07:31:57Z
user: login: Alek5andr-Kotov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T07:36:04Z
user: login: Alek5andr-Kotov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T07:38:19Z
user: login: Alek5andr-Kotov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T07:38:52Z
user: login: Alek5andr-Kotov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T07:39:46Z
user: login: Alek5andr-Kotov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T07:48:45Z
user: login: Alek5andr-Kotov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T07:51:30Z
user: login: Alek5andr-Kotov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T08:00:34Z
user: login: a-serebryanskiy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T14:44:29Z
user: login: a-serebryanskiy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T14:48:33Z
user: login: a-serebryanskiy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T14:59:46Z
user: login: a-serebryanskiy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-22T10:07:00Z
user: login: Alek5andr-Kotov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-22T11:50:22Z
user: login: Alek5andr-Kotov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-22T12:09:21Z
user: login: Alek5andr-Kotov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-22T12:13:15Z
user: login: Alek5andr-Kotov
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-22T14:03:37Z

Review comments:
diff_hunk: @@ -13,20 +17,133 @@ namespace NKafka {
         using TBase = NActors::TActorBootstrapped<TKafkaTransactionActor>;
         
         public:
+            struct TTopicPartition {
+                TString TopicPath;
+                ui32 PartitionId;
+
+                bool operator==(const TTopicPartition &other) const
+                { return (TopicPath == other.TopicPath
+                        && PartitionId == other.PartitionId);
+                }
+            };
+
+            struct TopicPartitionHashFn {
+                size_t operator()(const TTopicPartition& partition) const {
+                    return std::hash<TString>()(partition.TopicPath) ^ std::hash<int64_t>()(partition.PartitionId);
+                }
+            };
+
+            struct TPartitionCommit {
+                i32 Partition; 
+                i64 Offset;
+                TString ConsumerName;
+                i64 ConsumerGeneration;
+            };
+
+            enum EKafkaTxnKqpRequests : ui8 {
+                NO_REQUEST = 0,
+                
+                // This request selects up-to-date producer and consumers states from relevant tables
+                // After this request a check will happen, that no transaction details has expired.
+                SELECT,
+                // This request sends to KQP a command to commit transaction
+                // Both these requests happen in same transaction
+                COMMIT 
+            };
+
+            // we need to exlplicitly specify kqpActorId and txnCoordinatorActorId for unit tests
+            TKafkaTransactionActor(const TString& transactionalId, i64 producerId, i16 producerEpoch, const TString& DatabasePath, const TActorId& kqpActorId, const TActorId& txnCoordinatorActorId) : 
+                TransactionalId(transactionalId),
+                ProducerId(producerId),
+                ProducerEpoch(producerEpoch),
+                DatabasePath(DatabasePath),
+                TxnCoordinatorActorId(txnCoordinatorActorId),
+                KqpActorId(kqpActorId) {};
+
             void Bootstrap(const NActors::TActorContext&) {
                 TBase::Become(&TKafkaTransactionActor::StateWork);
             }
 
             TStringBuilder LogPrefix() const {
-                return TStringBuilder() << "KafkaTransactionActor";
+                return TStringBuilder() << "KafkaTransactionActor{TransactionalId=" << TransactionalId << "; ProducerId=" << ProducerId << "; ProducerEpoch=" << ProducerEpoch << "}: ";
             }
         
         private:
             STFUNC(StateWork) {
                 switch (ev->GetTypeRewrite()) {
-                    // will be eimplemented in a future PR
-                    // ToDo: add poison pill handler
+                    HFunc(TEvKafka::TEvAddPartitionsToTxnRequest, Handle);
+                    HFunc(TEvKafka::TEvAddOffsetsToTxnRequest, Handle);
+                    HFunc(TEvKafka::TEvTxnOffsetCommitRequest, Handle);
+                    HFunc(TEvKafka::TEvEndTxnRequest, Handle);
+                    HFunc(NKqp::TEvKqp::TEvCreateSessionResponse, Handle);
+                    HFunc(NKqp::TEvKqp::TEvQueryResponse, Handle);
+                    HFunc(TEvents::TEvPoison, Handle);
                 }
             }
+
+            // Kafka API events
+            void Handle(TEvKafka::TEvAddPartitionsToTxnRequest::TPtr& ev, const TActorContext& ctx);
+            void Handle(TEvKafka::TEvAddOffsetsToTxnRequest::TPtr& ev, const TActorContext& ctx);
+            void Handle(TEvKafka::TEvTxnOffsetCommitRequest::TPtr& ev, const TActorContext& ctx);
+            void Handle(TEvKafka::TEvEndTxnRequest::TPtr& ev, const TActorContext& ctx);
+            // KQP events
+            void Handle(NKqp::TEvKqp::TEvCreateSessionResponse::TPtr& ev, const TActorContext& ctx);
+            void Handle(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev, const TActorContext& ctx);
+            
+            // Poison pill
+            void Handle(TEvents::TEvPoison::TPtr& ev, const TActorContext& ctx);
+
+            // Transaction commit logic
+            void StartKqpSession(const TActorContext& ctx);
+            void SendToKqpValidationRequests(const TActorContext& ctx);
+            void SendCommitTxnRequest(const TString& kqpTransactionId);
+
+            // Response senders
+            template<class ErrorResponseType, class EventType>
+            void SendInvalidTransactionActorStateResponse(TAutoPtr<TEventHandle<EventType>>& evHandle);
+            template<class ErrorResponseType, class EventType>
+            void SendResponseFail(TAutoPtr<TEventHandle<EventType>>& evHandle, EKafkaErrors errorCode, const TString& errorMessage = {});
+            template<class ResponseType, class EventType>
+            void SendOkResponse(TAutoPtr<TEventHandle<EventType>>& evHandle);
path: ydb/core/kafka_proxy/actors/kafka_transaction_actor.h
user: login: Alek5andr-Kotov
body: Будьте последовательными в своих заблуждениях :D
Либо `Send*Response`, либо `SendResponse*`
created_at: 2025-04-14T09:54:44Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,59 @@
+#include "txn_actor_response_builder.h"
+
+namespace NKafka {
+    namespace NKafkaTransactions {
path: ydb/core/kafka_proxy/actors/txn_actor_response_builder.cpp
user: login: Alek5andr-Kotov
body: Меньше отступов, хороших и разных :)
Лучше `namespace NKafka::NKafkaTransaction {`
created_at: 2025-04-14T10:08:25Z
author_association: COLLABORATOR
diff_hunk: @@ -13,20 +17,133 @@ namespace NKafka {
         using TBase = NActors::TActorBootstrapped<TKafkaTransactionActor>;
         
         public:
+            struct TTopicPartition {
+                TString TopicPath;
+                ui32 PartitionId;
+
+                bool operator==(const TTopicPartition &other) const
+                { return (TopicPath == other.TopicPath
+                        && PartitionId == other.PartitionId);
+                }
+            };
+
+            struct TopicPartitionHashFn {
+                size_t operator()(const TTopicPartition& partition) const {
+                    return std::hash<TString>()(partition.TopicPath) ^ std::hash<int64_t>()(partition.PartitionId);
+                }
+            };
+
+            struct TPartitionCommit {
+                i32 Partition; 
+                i64 Offset;
+                TString ConsumerName;
+                i64 ConsumerGeneration;
+            };
+
+            enum EKafkaTxnKqpRequests : ui8 {
+                NO_REQUEST = 0,
+                
+                // This request selects up-to-date producer and consumers states from relevant tables
+                // After this request a check will happen, that no transaction details has expired.
+                SELECT,
+                // This request sends to KQP a command to commit transaction
+                // Both these requests happen in same transaction
+                COMMIT 
+            };
+
+            // we need to exlplicitly specify kqpActorId and txnCoordinatorActorId for unit tests
+            TKafkaTransactionActor(const TString& transactionalId, i64 producerId, i16 producerEpoch, const TString& DatabasePath, const TActorId& kqpActorId, const TActorId& txnCoordinatorActorId) : 
+                TransactionalId(transactionalId),
+                ProducerId(producerId),
+                ProducerEpoch(producerEpoch),
+                DatabasePath(DatabasePath),
+                TxnCoordinatorActorId(txnCoordinatorActorId),
+                KqpActorId(kqpActorId) {};
+
             void Bootstrap(const NActors::TActorContext&) {
                 TBase::Become(&TKafkaTransactionActor::StateWork);
             }
 
             TStringBuilder LogPrefix() const {
-                return TStringBuilder() << "KafkaTransactionActor";
+                return TStringBuilder() << "KafkaTransactionActor{TransactionalId=" << TransactionalId << "; ProducerId=" << ProducerId << "; ProducerEpoch=" << ProducerEpoch << "}: ";
             }
         
         private:
             STFUNC(StateWork) {
                 switch (ev->GetTypeRewrite()) {
-                    // will be eimplemented in a future PR
-                    // ToDo: add poison pill handler
+                    HFunc(TEvKafka::TEvAddPartitionsToTxnRequest, Handle);
+                    HFunc(TEvKafka::TEvAddOffsetsToTxnRequest, Handle);
+                    HFunc(TEvKafka::TEvTxnOffsetCommitRequest, Handle);
+                    HFunc(TEvKafka::TEvEndTxnRequest, Handle);
+                    HFunc(NKqp::TEvKqp::TEvCreateSessionResponse, Handle);
+                    HFunc(NKqp::TEvKqp::TEvQueryResponse, Handle);
+                    HFunc(TEvents::TEvPoison, Handle);
                 }
             }
+
+            // Kafka API events
+            void Handle(TEvKafka::TEvAddPartitionsToTxnRequest::TPtr& ev, const TActorContext& ctx);
+            void Handle(TEvKafka::TEvAddOffsetsToTxnRequest::TPtr& ev, const TActorContext& ctx);
+            void Handle(TEvKafka::TEvTxnOffsetCommitRequest::TPtr& ev, const TActorContext& ctx);
+            void Handle(TEvKafka::TEvEndTxnRequest::TPtr& ev, const TActorContext& ctx);
+            // KQP events
+            void Handle(NKqp::TEvKqp::TEvCreateSessionResponse::TPtr& ev, const TActorContext& ctx);
+            void Handle(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev, const TActorContext& ctx);
+            
+            // Poison pill
+            void Handle(TEvents::TEvPoison::TPtr& ev, const TActorContext& ctx);
+
+            // Transaction commit logic
+            void StartKqpSession(const TActorContext& ctx);
+            void SendToKqpValidationRequests(const TActorContext& ctx);
+            void SendCommitTxnRequest(const TString& kqpTransactionId);
+
+            // Response senders
+            template<class ErrorResponseType, class EventType>
+            void SendInvalidTransactionActorStateResponse(TAutoPtr<TEventHandle<EventType>>& evHandle);
+            template<class ErrorResponseType, class EventType>
+            void SendResponseFail(TAutoPtr<TEventHandle<EventType>>& evHandle, EKafkaErrors errorCode, const TString& errorMessage = {});
+            template<class ResponseType, class EventType>
+            void SendOkResponse(TAutoPtr<TEventHandle<EventType>>& evHandle);
+
+            // helper methods
+            void Die(const TActorContext &ctx);
+            template<class EventType>
+            bool ProducerInRequestIsValid(TMessagePtr<EventType> kafkaRequest);
+            TString GetFullTopicPath(const TString& topicName);
+            TString GetYqlWithTablesNames(const TString& templateStr);
+            NYdb::TParams BuildSelectParams();
+            THolder<NKikimr::NKqp::TEvKqp::TEvQueryRequest> BuildCommitTxnRequestToKqp(const TString& kqpTransactionId);
+            void HandleSelectResponse(const NKikimrKqp::TEvQueryResponse& response, const TActorContext& ctx);
+            void HandleCommitResponse(const TActorContext& ctx);
+            TMaybe<TString> GetErrorFromYdbResponse(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev);
+            TMaybe<TProducerState> ParseProducerState(const NKikimrKqp::TEvQueryResponse& response);
path: ydb/core/kafka_proxy/actors/kafka_transaction_actor.h
user: login: Alek5andr-Kotov
body: Здесь точно 3 разных типов `TEvQueryResponse`?
created_at: 2025-04-14T10:12:26Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,15 @@
+#pragma once
+
+#include <ydb/core/kafka_proxy/actors/actors.h>
+#include <ydb/core/kafka_proxy/kafka.h>
+#include <ydb/core/kafka_proxy/kafka_messages.h>
+
+namespace NKafka {
+    namespace NKafkaTransactions {
+        class TResponseBuilder {
+            public:
+                template<class ResponseType, class RequestType>
+                std::shared_ptr<ResponseType> Build(TMessagePtr<RequestType> request, EKafkaErrors errorCode);
path: ydb/core/kafka_proxy/actors/txn_actor_response_builder.h
user: login: Alek5andr-Kotov
body: Почему `TResponseBuilder::Build` вместо `BuildResponse`?
created_at: 2025-04-14T10:14:17Z
author_association: COLLABORATOR
diff_hunk: @@ -337,6 +346,18 @@ struct TEvSaveTxnProducerResponse : public NActors::TEventLocal<TEvSaveTxnProduc
     EStatus Status;
     TString Message;
 };
+
+struct TEvTransactionActorDied : public NActors::TEventLocal<TEvTransactionActorDied, EvTransactionActorDied> {
+    TEvTransactionActorDied(const TString& transactionalId, const i64 producerId, const i16 producerEpoch) :
+        TransactionalId(std::move(transactionalId)),
+        ProducerId(producerId),
+        ProducerEpoch(producerEpoch)
+    {}
+
+    const TString TransactionalId;
+    const i64 ProducerId;
+    const i16 ProducerEpoch;
path: ydb/core/kafka_proxy/kafka_events.h
user: login: Alek5andr-Kotov
body: Это локальное событие. Можно заменить пару `ProdurerId, ProducerEpoch` на `TProducerState`?
created_at: 2025-04-14T10:17:43Z
author_association: COLLABORATOR
diff_hunk: @@ -43,6 +44,23 @@ namespace NKafka {
         HandleTransactionalRequest<TEndTxnResponseData>(ev, ctx);
     };
 
+    void TKafkaTransactionsCoordinator::Handle(TEvKafka::TEvTransactionActorDied::TPtr& ev, const TActorContext&) {
+        TProducerState deadActorState = {ev->Get()->ProducerId, ev->Get()->ProducerEpoch};
+        TProducerState currentProducerState = ProducersByTransactionalId[ev->Get()->TransactionalId];
path: ydb/core/kafka_proxy/kafka_transactions_coordinator.cpp
user: login: Alek5andr-Kotov
body: А если у нас в `ProducersByTransactionalId` нет такой транзакции?
created_at: 2025-04-14T10:18:36Z
author_association: COLLABORATOR
diff_hunk: @@ -20,6 +21,9 @@ namespace NKafka {
         struct TProducerState {
             i64 Id;
             i32 Epoch;
+
+            bool operator==(TProducerState const&) const = default;
+            bool operator!=(TProducerState const&) const = default;
path: ydb/core/kafka_proxy/kafka_transactions_coordinator.h
user: login: Alek5andr-Kotov
body: А как это работает?
Не знал о такой возможности. Положил себе в копилку. Спасибо :)
created_at: 2025-04-14T10:26:47Z
author_association: COLLABORATOR
diff_hunk: @@ -13,20 +17,133 @@ namespace NKafka {
         using TBase = NActors::TActorBootstrapped<TKafkaTransactionActor>;
         
         public:
+            struct TTopicPartition {
+                TString TopicPath;
+                ui32 PartitionId;
+
+                bool operator==(const TTopicPartition &other) const
+                { return (TopicPath == other.TopicPath
+                        && PartitionId == other.PartitionId);
+                }
path: ydb/core/kafka_proxy/actors/kafka_transaction_actor.h
user: login: Alek5andr-Kotov
body: Может здесь сделать так? 
```
bool operator==(const TTopicPartition& other) const = default;
```
created_at: 2025-04-14T10:44:50Z
author_association: COLLABORATOR
diff_hunk: @@ -69,15 +77,13 @@ namespace NKafka {
             template<class EventType> 
             void ForwardToTransactionActor(TAutoPtr<TEventHandle<EventType>>& evHandle, const TActorContext& ctx);
 
-            template<class ResponseType, class RequestType>
-            std::shared_ptr<ResponseType> BuildProducerFencedResponse(TMessagePtr<RequestType> request);
-
             bool NewProducerStateIsOutdated(const TProducerState& currentProducerState, const TProducerState& newProducerState);
             TMaybe<TString> GetTxnRequestError(const TTransactionalRequest& request);
             TString GetProducerIsOutdatedError(const TString& transactionalId, const TProducerState& currentProducerState, const TProducerState& newProducerState);
 
             std::unordered_map<TString, TProducerState> ProducersByTransactionalId;
             std::unordered_map<TString, TActorId> TxnActorByTransactionalId;
+            NKafkaTransactions::TResponseBuilder ResponseBuilder = NKafkaTransactions::TResponseBuilder();
path: ydb/core/kafka_proxy/kafka_transactions_coordinator.h
user: login: Alek5andr-Kotov
body: Зачем здесь явно конструируется объект? Конструктора по умолчанию недостаточно?
created_at: 2025-04-14T10:46:37Z
author_association: COLLABORATOR
diff_hunk: @@ -9,9 +9,15 @@ TKqpTxHelper::TKqpTxHelper(TString database)
     : DataBase(database)
 {}
 
-void TKqpTxHelper::SendCreateSessionRequest(const TActorContext& ctx) {
+void TKqpTxHelper::SendCreateSessionRequest(const TActorContext& ctx, const TMaybe<TActorId>& kqpActorId) {
     auto ev = MakeCreateSessionRequest();
-    ctx.Send(MakeKqpProxyID(ctx.SelfID.NodeId()), ev.Release(), 0, 0);
+    TActorId actorId;
+    if (!kqpActorId) {
+        actorId = MakeKqpProxyID(ctx.SelfID.NodeId());
+    } else {
+        actorId = *kqpActorId.Get();
path: ydb/core/kafka_proxy/kqp_helper.cpp
user: login: Alek5andr-Kotov
body: Достаточно `*kqpActorId`
created_at: 2025-04-14T10:47:23Z
author_association: COLLABORATOR
diff_hunk: @@ -337,6 +346,18 @@ struct TEvSaveTxnProducerResponse : public NActors::TEventLocal<TEvSaveTxnProduc
     EStatus Status;
     TString Message;
 };
+
+struct TEvTransactionActorDied : public NActors::TEventLocal<TEvTransactionActorDied, EvTransactionActorDied> {
+    TEvTransactionActorDied(const TString& transactionalId, const i64 producerId, const i16 producerEpoch) :
+        TransactionalId(std::move(transactionalId)),
+        ProducerId(producerId),
+        ProducerEpoch(producerEpoch)
+    {}
+
+    const TString TransactionalId;
+    const i64 ProducerId;
+    const i16 ProducerEpoch;
path: ydb/core/kafka_proxy/kafka_events.h
user: login: a-serebryanskiy
body: Можно, но зачем? Сделает ли это код проще?
created_at: 2025-04-20T10:56:35Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,448 @@
+#include "kafka_transaction_actor.h"
+#include "kafka_transaction_actor_sql.cpp"
+#include "txn_actor_response_builder.h"
+#include <ydb/core/kafka_proxy/kafka_transactions_coordinator.h>
+#include <ydb/core/kafka_proxy/kafka_transactional_producers_initializers.h>
+#include <ydb/core/kafka_proxy/kafka_consumer_groups_metadata_initializers.h>
+#include <ydb/core/kqp/common/simple/services.h>
+#include <regex>
+
+namespace NKafka {
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddPartitionsToTxnRequest::TPtr& ev, const TActorContext& ctx){
+        KAFKA_LOG_D("Receieved ADD_PARTITIONS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddPartitionsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
path: ydb/core/kafka_proxy/actors/kafka_transaction_actor.cpp
user: login: Alek5andr-Kotov
body: Дублирование кода в этих пяти строках вижу я...
created_at: 2025-04-21T07:01:34Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,448 @@
+#include "kafka_transaction_actor.h"
+#include "kafka_transaction_actor_sql.cpp"
+#include "txn_actor_response_builder.h"
+#include <ydb/core/kafka_proxy/kafka_transactions_coordinator.h>
+#include <ydb/core/kafka_proxy/kafka_transactional_producers_initializers.h>
+#include <ydb/core/kafka_proxy/kafka_consumer_groups_metadata_initializers.h>
+#include <ydb/core/kqp/common/simple/services.h>
+#include <regex>
+
+namespace NKafka {
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddPartitionsToTxnRequest::TPtr& ev, const TActorContext& ctx){
+        KAFKA_LOG_D("Receieved ADD_PARTITIONS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddPartitionsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                PartitionsInTxn.emplace(GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest));
+            }
+        }
+        SendOkResponse<TAddPartitionsToTxnResponseData>(ev);
+    }
+
+    // Method does nothing. In Kafka it will add __consumer_offsets topic to transaction, but 
+    // in YDB Topics we store offsets in table and do not need this extra action.
+    // Thus we can just ignore this request.
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddOffsetsToTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved ADD_OFFSETS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddOffsetsToTxnResponseData>(ev);
+            Die(ctx);
path: ydb/core/kafka_proxy/actors/kafka_transaction_actor.cpp
user: login: Alek5andr-Kotov
body: Актор умирает. Его ActorId не остаётся в какой-нибудь коллекции?
created_at: 2025-04-21T07:02:45Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,448 @@
+#include "kafka_transaction_actor.h"
+#include "kafka_transaction_actor_sql.cpp"
+#include "txn_actor_response_builder.h"
+#include <ydb/core/kafka_proxy/kafka_transactions_coordinator.h>
+#include <ydb/core/kafka_proxy/kafka_transactional_producers_initializers.h>
+#include <ydb/core/kafka_proxy/kafka_consumer_groups_metadata_initializers.h>
+#include <ydb/core/kqp/common/simple/services.h>
+#include <regex>
+
+namespace NKafka {
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddPartitionsToTxnRequest::TPtr& ev, const TActorContext& ctx){
+        KAFKA_LOG_D("Receieved ADD_PARTITIONS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddPartitionsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
path: ydb/core/kafka_proxy/actors/kafka_transaction_actor.cpp
user: login: a-serebryanskiy
body: Тут переделать на DEFINE
created_at: 2025-04-21T07:11:12Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,448 @@
+#include "kafka_transaction_actor.h"
+#include "kafka_transaction_actor_sql.cpp"
+#include "txn_actor_response_builder.h"
+#include <ydb/core/kafka_proxy/kafka_transactions_coordinator.h>
+#include <ydb/core/kafka_proxy/kafka_transactional_producers_initializers.h>
+#include <ydb/core/kafka_proxy/kafka_consumer_groups_metadata_initializers.h>
+#include <ydb/core/kqp/common/simple/services.h>
+#include <regex>
+
+namespace NKafka {
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddPartitionsToTxnRequest::TPtr& ev, const TActorContext& ctx){
+        KAFKA_LOG_D("Receieved ADD_PARTITIONS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddPartitionsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                PartitionsInTxn.emplace(GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest));
+            }
+        }
+        SendOkResponse<TAddPartitionsToTxnResponseData>(ev);
+    }
+
+    // Method does nothing. In Kafka it will add __consumer_offsets topic to transaction, but 
+    // in YDB Topics we store offsets in table and do not need this extra action.
+    // Thus we can just ignore this request.
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddOffsetsToTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved ADD_OFFSETS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddOffsetsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        SendOkResponse<TAddOffsetsToTxnResponseData>(ev);
+    }
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvTxnOffsetCommitRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved TXN_OFFSET_COMMIT request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TTxnOffsetCommitResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        // save offsets for future use
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                TTopicPartition topicPartition = {GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest.PartitionIndex)};
+                TPartitionCommit newCommit{
+                    .Partition = partitionInRequest.PartitionIndex,
+                    .Offset = partitionInRequest.CommittedOffset,
+                    .ConsumerName = ev->Get()->Request->GroupId->c_str(),
+                    .ConsumerGeneration = ev->Get()->Request->GenerationId
+                };
+                auto it = OffsetsToCommit.find(topicPartition);
+                if (it == OffsetsToCommit.end()) {
+                    OffsetsToCommit.emplace(std::make_pair(topicPartition, newCommit));
+                } else {
+                    it->second = newCommit;
+                }
+            }
+        }
+        SendOkResponse<TTxnOffsetCommitResponseData>(ev);
+    }
+
+    /* 
+    If abort, will send back ok.
+    If commit, will do following sequence of calls and event handlers:
+    1. Open KQP Session
+    2. Handle(NKqp::TEvKqp::TEvCreateSessionResponse) - KQP session created
+    3. Send Select request for current producer and consumers state, begin transaction
+    4. Handle(NKqp::TEvKqp::TEvQueryResponse): Validate returned producer and consumers state. Return PRODUCER_FENCED if check failed
+    5. Send Commit request with properly created KafkaApiOperations
+    6. Handle(NKqp::TEvKqp::TEvQueryResponse): If everything committed successfully, return OK to the client
+    7. Close KQP session, send to coordinator TEvTransactionActorDied, die.
+    */
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvEndTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved END_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TEndTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+
+        bool txnAborted = !ev->Get()->Request->Committed;
+        if (CommitStarted) {
+            return; // we just ignore second and subsequent requests
+        } else if (txnAborted) {
+            SendOkResponse<TEndTxnResponseData>(ev);
+        } else {
+            CommitStarted = true;
+            EndTxnRequestPtr = std::move(ev);
+            StartKqpSession(ctx);
+        }
+    }
+
+    void TKafkaTransactionActor::Handle(TEvents::TEvPoison::TPtr&, const TActorContext& ctx) {
+        Die(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvCreateSessionResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "KQP session created");
+        if (!Kqp->HandleCreateSessionResponse(ev, ctx)) {
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, "Failed to create KQP session");
+            Die(ctx);
+            return;
+        }
+
+        SendToKqpValidationRequests(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Receieved query response from KQP for " << GetAsStr(LastSentToKqpRequest) << " request");
+        if (auto error = GetErrorFromYdbResponse(ev)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error->data());
+            Die(ctx);
+            return;
+        }
+
+        switch (LastSentToKqpRequest) {
+            case EKafkaTxnKqpRequests::SELECT:
+                HandleSelectResponse(*ev->Get(), ctx);
+                break;
+            case EKafkaTxnKqpRequests::COMMIT:
+                HandleCommitResponse(ctx);
+                break;
+            default:
+                Y_FAIL("Unexpected KQP request");
+        }
+    }
+
+    void TKafkaTransactionActor::StartKqpSession(const TActorContext& ctx) {
+        Kqp = std::make_unique<TKqpTxHelper>(DatabasePath);
+        KAFKA_LOG_D(TStringBuilder() << "Sending create session request to KQP for database " << DatabasePath);
+        Kqp->SendCreateSessionRequest(ctx, KqpActorId);
path: ydb/core/kafka_proxy/actors/kafka_transaction_actor.cpp
user: login: Alek5andr-Kotov
body: Я правильно понимаю, что KqpActorId это всегда `MakeKqpProxyID(...)`?
created_at: 2025-04-21T07:16:20Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,448 @@
+#include "kafka_transaction_actor.h"
+#include "kafka_transaction_actor_sql.cpp"
+#include "txn_actor_response_builder.h"
+#include <ydb/core/kafka_proxy/kafka_transactions_coordinator.h>
+#include <ydb/core/kafka_proxy/kafka_transactional_producers_initializers.h>
+#include <ydb/core/kafka_proxy/kafka_consumer_groups_metadata_initializers.h>
+#include <ydb/core/kqp/common/simple/services.h>
+#include <regex>
+
+namespace NKafka {
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddPartitionsToTxnRequest::TPtr& ev, const TActorContext& ctx){
+        KAFKA_LOG_D("Receieved ADD_PARTITIONS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddPartitionsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                PartitionsInTxn.emplace(GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest));
+            }
+        }
+        SendOkResponse<TAddPartitionsToTxnResponseData>(ev);
+    }
+
+    // Method does nothing. In Kafka it will add __consumer_offsets topic to transaction, but 
+    // in YDB Topics we store offsets in table and do not need this extra action.
+    // Thus we can just ignore this request.
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddOffsetsToTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved ADD_OFFSETS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddOffsetsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        SendOkResponse<TAddOffsetsToTxnResponseData>(ev);
+    }
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvTxnOffsetCommitRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved TXN_OFFSET_COMMIT request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TTxnOffsetCommitResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        // save offsets for future use
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                TTopicPartition topicPartition = {GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest.PartitionIndex)};
+                TPartitionCommit newCommit{
+                    .Partition = partitionInRequest.PartitionIndex,
+                    .Offset = partitionInRequest.CommittedOffset,
+                    .ConsumerName = ev->Get()->Request->GroupId->c_str(),
+                    .ConsumerGeneration = ev->Get()->Request->GenerationId
+                };
+                auto it = OffsetsToCommit.find(topicPartition);
+                if (it == OffsetsToCommit.end()) {
+                    OffsetsToCommit.emplace(std::make_pair(topicPartition, newCommit));
+                } else {
+                    it->second = newCommit;
+                }
+            }
+        }
+        SendOkResponse<TTxnOffsetCommitResponseData>(ev);
+    }
+
+    /* 
+    If abort, will send back ok.
+    If commit, will do following sequence of calls and event handlers:
+    1. Open KQP Session
+    2. Handle(NKqp::TEvKqp::TEvCreateSessionResponse) - KQP session created
+    3. Send Select request for current producer and consumers state, begin transaction
+    4. Handle(NKqp::TEvKqp::TEvQueryResponse): Validate returned producer and consumers state. Return PRODUCER_FENCED if check failed
+    5. Send Commit request with properly created KafkaApiOperations
+    6. Handle(NKqp::TEvKqp::TEvQueryResponse): If everything committed successfully, return OK to the client
+    7. Close KQP session, send to coordinator TEvTransactionActorDied, die.
+    */
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvEndTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved END_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TEndTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+
+        bool txnAborted = !ev->Get()->Request->Committed;
+        if (CommitStarted) {
+            return; // we just ignore second and subsequent requests
+        } else if (txnAborted) {
+            SendOkResponse<TEndTxnResponseData>(ev);
+        } else {
+            CommitStarted = true;
+            EndTxnRequestPtr = std::move(ev);
+            StartKqpSession(ctx);
+        }
+    }
+
+    void TKafkaTransactionActor::Handle(TEvents::TEvPoison::TPtr&, const TActorContext& ctx) {
+        Die(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvCreateSessionResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "KQP session created");
+        if (!Kqp->HandleCreateSessionResponse(ev, ctx)) {
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, "Failed to create KQP session");
+            Die(ctx);
+            return;
+        }
+
+        SendToKqpValidationRequests(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Receieved query response from KQP for " << GetAsStr(LastSentToKqpRequest) << " request");
+        if (auto error = GetErrorFromYdbResponse(ev)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error->data());
+            Die(ctx);
+            return;
+        }
+
+        switch (LastSentToKqpRequest) {
+            case EKafkaTxnKqpRequests::SELECT:
+                HandleSelectResponse(*ev->Get(), ctx);
+                break;
+            case EKafkaTxnKqpRequests::COMMIT:
+                HandleCommitResponse(ctx);
+                break;
+            default:
+                Y_FAIL("Unexpected KQP request");
+        }
+    }
+
+    void TKafkaTransactionActor::StartKqpSession(const TActorContext& ctx) {
+        Kqp = std::make_unique<TKqpTxHelper>(DatabasePath);
+        KAFKA_LOG_D(TStringBuilder() << "Sending create session request to KQP for database " << DatabasePath);
+        Kqp->SendCreateSessionRequest(ctx, KqpActorId);
+    }
+
+    void TKafkaTransactionActor::SendToKqpValidationRequests(const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Sending select request to KQP for database " << DatabasePath);
+        Kqp->SendYqlRequest(
+            GetYqlWithTablesNames(NKafkaTransactionSql::SELECT_FOR_VALIDATION), 
+            BuildSelectParams(), 
+            ++KqpCookie, 
+            ctx,
+            false,
+            KqpActorId
+        );
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::SELECT;
+    }
+
+    void TKafkaTransactionActor::SendCommitTxnRequest(const TString& kqpTransactionId) {
+        auto request = BuildCommitTxnRequestToKqp(kqpTransactionId);
+
+        Send(KqpActorId, request.Release(), 0, ++KqpCookie);
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::COMMIT;
+    }
+    
+    // Response senders
+    template<class ErrorResponseType, class EventType>
+    void TKafkaTransactionActor::SendInvalidTransactionActorStateResponse(TAutoPtr<TEventHandle<EventType>>& evHandle) {
+        auto kafkaRequest = evHandle->Get()->Request;
+        KAFKA_LOG_CRIT(TStringBuilder() << "Recieved invalid request. Got: "
+            << "transactionalId=" << kafkaRequest->TransactionalId->c_str() 
+            << ", producerId=" << kafkaRequest->ProducerId 
+            << ", producerEpoch=" << kafkaRequest->ProducerEpoch
+        );
+        std::shared_ptr<ErrorResponseType> response = NKafkaTransactions::BuildResponse<ErrorResponseType>(kafkaRequest, EKafkaErrors::UNKNOWN_SERVER_ERROR);
path: ydb/core/kafka_proxy/actors/kafka_transaction_actor.cpp
user: login: Alek5andr-Kotov
body: Вместо `std::shared_ptr<...>` лучше `auto`. Код будет короче и остаётся понятным
created_at: 2025-04-21T07:18:44Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,448 @@
+#include "kafka_transaction_actor.h"
+#include "kafka_transaction_actor_sql.cpp"
+#include "txn_actor_response_builder.h"
+#include <ydb/core/kafka_proxy/kafka_transactions_coordinator.h>
+#include <ydb/core/kafka_proxy/kafka_transactional_producers_initializers.h>
+#include <ydb/core/kafka_proxy/kafka_consumer_groups_metadata_initializers.h>
+#include <ydb/core/kqp/common/simple/services.h>
+#include <regex>
+
+namespace NKafka {
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddPartitionsToTxnRequest::TPtr& ev, const TActorContext& ctx){
+        KAFKA_LOG_D("Receieved ADD_PARTITIONS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddPartitionsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                PartitionsInTxn.emplace(GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest));
+            }
+        }
+        SendOkResponse<TAddPartitionsToTxnResponseData>(ev);
+    }
+
+    // Method does nothing. In Kafka it will add __consumer_offsets topic to transaction, but 
+    // in YDB Topics we store offsets in table and do not need this extra action.
+    // Thus we can just ignore this request.
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddOffsetsToTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved ADD_OFFSETS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddOffsetsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        SendOkResponse<TAddOffsetsToTxnResponseData>(ev);
+    }
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvTxnOffsetCommitRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved TXN_OFFSET_COMMIT request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TTxnOffsetCommitResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        // save offsets for future use
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                TTopicPartition topicPartition = {GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest.PartitionIndex)};
+                TPartitionCommit newCommit{
+                    .Partition = partitionInRequest.PartitionIndex,
+                    .Offset = partitionInRequest.CommittedOffset,
+                    .ConsumerName = ev->Get()->Request->GroupId->c_str(),
+                    .ConsumerGeneration = ev->Get()->Request->GenerationId
+                };
+                auto it = OffsetsToCommit.find(topicPartition);
+                if (it == OffsetsToCommit.end()) {
+                    OffsetsToCommit.emplace(std::make_pair(topicPartition, newCommit));
+                } else {
+                    it->second = newCommit;
+                }
+            }
+        }
+        SendOkResponse<TTxnOffsetCommitResponseData>(ev);
+    }
+
+    /* 
+    If abort, will send back ok.
+    If commit, will do following sequence of calls and event handlers:
+    1. Open KQP Session
+    2. Handle(NKqp::TEvKqp::TEvCreateSessionResponse) - KQP session created
+    3. Send Select request for current producer and consumers state, begin transaction
+    4. Handle(NKqp::TEvKqp::TEvQueryResponse): Validate returned producer and consumers state. Return PRODUCER_FENCED if check failed
+    5. Send Commit request with properly created KafkaApiOperations
+    6. Handle(NKqp::TEvKqp::TEvQueryResponse): If everything committed successfully, return OK to the client
+    7. Close KQP session, send to coordinator TEvTransactionActorDied, die.
+    */
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvEndTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved END_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TEndTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+
+        bool txnAborted = !ev->Get()->Request->Committed;
+        if (CommitStarted) {
+            return; // we just ignore second and subsequent requests
+        } else if (txnAborted) {
+            SendOkResponse<TEndTxnResponseData>(ev);
+        } else {
+            CommitStarted = true;
+            EndTxnRequestPtr = std::move(ev);
+            StartKqpSession(ctx);
+        }
+    }
+
+    void TKafkaTransactionActor::Handle(TEvents::TEvPoison::TPtr&, const TActorContext& ctx) {
+        Die(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvCreateSessionResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "KQP session created");
+        if (!Kqp->HandleCreateSessionResponse(ev, ctx)) {
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, "Failed to create KQP session");
+            Die(ctx);
+            return;
+        }
+
+        SendToKqpValidationRequests(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Receieved query response from KQP for " << GetAsStr(LastSentToKqpRequest) << " request");
+        if (auto error = GetErrorFromYdbResponse(ev)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error->data());
+            Die(ctx);
+            return;
+        }
+
+        switch (LastSentToKqpRequest) {
+            case EKafkaTxnKqpRequests::SELECT:
+                HandleSelectResponse(*ev->Get(), ctx);
+                break;
+            case EKafkaTxnKqpRequests::COMMIT:
+                HandleCommitResponse(ctx);
+                break;
+            default:
+                Y_FAIL("Unexpected KQP request");
+        }
+    }
+
+    void TKafkaTransactionActor::StartKqpSession(const TActorContext& ctx) {
+        Kqp = std::make_unique<TKqpTxHelper>(DatabasePath);
+        KAFKA_LOG_D(TStringBuilder() << "Sending create session request to KQP for database " << DatabasePath);
+        Kqp->SendCreateSessionRequest(ctx, KqpActorId);
+    }
+
+    void TKafkaTransactionActor::SendToKqpValidationRequests(const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Sending select request to KQP for database " << DatabasePath);
+        Kqp->SendYqlRequest(
+            GetYqlWithTablesNames(NKafkaTransactionSql::SELECT_FOR_VALIDATION), 
+            BuildSelectParams(), 
+            ++KqpCookie, 
+            ctx,
+            false,
+            KqpActorId
+        );
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::SELECT;
+    }
+
+    void TKafkaTransactionActor::SendCommitTxnRequest(const TString& kqpTransactionId) {
+        auto request = BuildCommitTxnRequestToKqp(kqpTransactionId);
+
+        Send(KqpActorId, request.Release(), 0, ++KqpCookie);
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::COMMIT;
+    }
+    
+    // Response senders
+    template<class ErrorResponseType, class EventType>
+    void TKafkaTransactionActor::SendInvalidTransactionActorStateResponse(TAutoPtr<TEventHandle<EventType>>& evHandle) {
+        auto kafkaRequest = evHandle->Get()->Request;
+        KAFKA_LOG_CRIT(TStringBuilder() << "Recieved invalid request. Got: "
+            << "transactionalId=" << kafkaRequest->TransactionalId->c_str() 
+            << ", producerId=" << kafkaRequest->ProducerId 
+            << ", producerEpoch=" << kafkaRequest->ProducerEpoch
+        );
+        std::shared_ptr<ErrorResponseType> response = NKafkaTransactions::BuildResponse<ErrorResponseType>(kafkaRequest, EKafkaErrors::UNKNOWN_SERVER_ERROR);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, EKafkaErrors::UNKNOWN_SERVER_ERROR));
path: ydb/core/kafka_proxy/actors/kafka_transaction_actor.cpp
user: login: Alek5andr-Kotov
body: Код ошибки `EKafkaErrors::UNKNOWN_SERVER_ERROR` здесь и выше всегда равны? Здесь можно использовать вызов `SendFailResponse`?
created_at: 2025-04-21T07:20:14Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,448 @@
+#include "kafka_transaction_actor.h"
+#include "kafka_transaction_actor_sql.cpp"
+#include "txn_actor_response_builder.h"
+#include <ydb/core/kafka_proxy/kafka_transactions_coordinator.h>
+#include <ydb/core/kafka_proxy/kafka_transactional_producers_initializers.h>
+#include <ydb/core/kafka_proxy/kafka_consumer_groups_metadata_initializers.h>
+#include <ydb/core/kqp/common/simple/services.h>
+#include <regex>
+
+namespace NKafka {
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddPartitionsToTxnRequest::TPtr& ev, const TActorContext& ctx){
+        KAFKA_LOG_D("Receieved ADD_PARTITIONS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddPartitionsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                PartitionsInTxn.emplace(GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest));
+            }
+        }
+        SendOkResponse<TAddPartitionsToTxnResponseData>(ev);
+    }
+
+    // Method does nothing. In Kafka it will add __consumer_offsets topic to transaction, but 
+    // in YDB Topics we store offsets in table and do not need this extra action.
+    // Thus we can just ignore this request.
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddOffsetsToTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved ADD_OFFSETS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddOffsetsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        SendOkResponse<TAddOffsetsToTxnResponseData>(ev);
+    }
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvTxnOffsetCommitRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved TXN_OFFSET_COMMIT request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TTxnOffsetCommitResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        // save offsets for future use
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                TTopicPartition topicPartition = {GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest.PartitionIndex)};
+                TPartitionCommit newCommit{
+                    .Partition = partitionInRequest.PartitionIndex,
+                    .Offset = partitionInRequest.CommittedOffset,
+                    .ConsumerName = ev->Get()->Request->GroupId->c_str(),
+                    .ConsumerGeneration = ev->Get()->Request->GenerationId
+                };
+                auto it = OffsetsToCommit.find(topicPartition);
+                if (it == OffsetsToCommit.end()) {
+                    OffsetsToCommit.emplace(std::make_pair(topicPartition, newCommit));
+                } else {
+                    it->second = newCommit;
+                }
+            }
+        }
+        SendOkResponse<TTxnOffsetCommitResponseData>(ev);
+    }
+
+    /* 
+    If abort, will send back ok.
+    If commit, will do following sequence of calls and event handlers:
+    1. Open KQP Session
+    2. Handle(NKqp::TEvKqp::TEvCreateSessionResponse) - KQP session created
+    3. Send Select request for current producer and consumers state, begin transaction
+    4. Handle(NKqp::TEvKqp::TEvQueryResponse): Validate returned producer and consumers state. Return PRODUCER_FENCED if check failed
+    5. Send Commit request with properly created KafkaApiOperations
+    6. Handle(NKqp::TEvKqp::TEvQueryResponse): If everything committed successfully, return OK to the client
+    7. Close KQP session, send to coordinator TEvTransactionActorDied, die.
+    */
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvEndTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved END_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TEndTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+
+        bool txnAborted = !ev->Get()->Request->Committed;
+        if (CommitStarted) {
+            return; // we just ignore second and subsequent requests
+        } else if (txnAborted) {
+            SendOkResponse<TEndTxnResponseData>(ev);
+        } else {
+            CommitStarted = true;
+            EndTxnRequestPtr = std::move(ev);
+            StartKqpSession(ctx);
+        }
+    }
+
+    void TKafkaTransactionActor::Handle(TEvents::TEvPoison::TPtr&, const TActorContext& ctx) {
+        Die(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvCreateSessionResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "KQP session created");
+        if (!Kqp->HandleCreateSessionResponse(ev, ctx)) {
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, "Failed to create KQP session");
+            Die(ctx);
+            return;
+        }
+
+        SendToKqpValidationRequests(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Receieved query response from KQP for " << GetAsStr(LastSentToKqpRequest) << " request");
+        if (auto error = GetErrorFromYdbResponse(ev)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error->data());
+            Die(ctx);
+            return;
+        }
+
+        switch (LastSentToKqpRequest) {
+            case EKafkaTxnKqpRequests::SELECT:
+                HandleSelectResponse(*ev->Get(), ctx);
+                break;
+            case EKafkaTxnKqpRequests::COMMIT:
+                HandleCommitResponse(ctx);
+                break;
+            default:
+                Y_FAIL("Unexpected KQP request");
+        }
+    }
+
+    void TKafkaTransactionActor::StartKqpSession(const TActorContext& ctx) {
+        Kqp = std::make_unique<TKqpTxHelper>(DatabasePath);
+        KAFKA_LOG_D(TStringBuilder() << "Sending create session request to KQP for database " << DatabasePath);
+        Kqp->SendCreateSessionRequest(ctx, KqpActorId);
+    }
+
+    void TKafkaTransactionActor::SendToKqpValidationRequests(const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Sending select request to KQP for database " << DatabasePath);
+        Kqp->SendYqlRequest(
+            GetYqlWithTablesNames(NKafkaTransactionSql::SELECT_FOR_VALIDATION), 
+            BuildSelectParams(), 
+            ++KqpCookie, 
+            ctx,
+            false,
+            KqpActorId
+        );
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::SELECT;
+    }
+
+    void TKafkaTransactionActor::SendCommitTxnRequest(const TString& kqpTransactionId) {
+        auto request = BuildCommitTxnRequestToKqp(kqpTransactionId);
+
+        Send(KqpActorId, request.Release(), 0, ++KqpCookie);
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::COMMIT;
+    }
+    
+    // Response senders
+    template<class ErrorResponseType, class EventType>
+    void TKafkaTransactionActor::SendInvalidTransactionActorStateResponse(TAutoPtr<TEventHandle<EventType>>& evHandle) {
+        auto kafkaRequest = evHandle->Get()->Request;
path: ydb/core/kafka_proxy/actors/kafka_transaction_actor.cpp
user: login: Alek5andr-Kotov
body: Здесь точно нужно копировать содержимое `Request`?
created_at: 2025-04-21T07:21:26Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,448 @@
+#include "kafka_transaction_actor.h"
+#include "kafka_transaction_actor_sql.cpp"
+#include "txn_actor_response_builder.h"
+#include <ydb/core/kafka_proxy/kafka_transactions_coordinator.h>
+#include <ydb/core/kafka_proxy/kafka_transactional_producers_initializers.h>
+#include <ydb/core/kafka_proxy/kafka_consumer_groups_metadata_initializers.h>
+#include <ydb/core/kqp/common/simple/services.h>
+#include <regex>
+
+namespace NKafka {
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddPartitionsToTxnRequest::TPtr& ev, const TActorContext& ctx){
+        KAFKA_LOG_D("Receieved ADD_PARTITIONS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddPartitionsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                PartitionsInTxn.emplace(GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest));
+            }
+        }
+        SendOkResponse<TAddPartitionsToTxnResponseData>(ev);
+    }
+
+    // Method does nothing. In Kafka it will add __consumer_offsets topic to transaction, but 
+    // in YDB Topics we store offsets in table and do not need this extra action.
+    // Thus we can just ignore this request.
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddOffsetsToTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved ADD_OFFSETS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddOffsetsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        SendOkResponse<TAddOffsetsToTxnResponseData>(ev);
+    }
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvTxnOffsetCommitRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved TXN_OFFSET_COMMIT request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TTxnOffsetCommitResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        // save offsets for future use
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                TTopicPartition topicPartition = {GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest.PartitionIndex)};
+                TPartitionCommit newCommit{
+                    .Partition = partitionInRequest.PartitionIndex,
+                    .Offset = partitionInRequest.CommittedOffset,
+                    .ConsumerName = ev->Get()->Request->GroupId->c_str(),
+                    .ConsumerGeneration = ev->Get()->Request->GenerationId
+                };
+                auto it = OffsetsToCommit.find(topicPartition);
+                if (it == OffsetsToCommit.end()) {
+                    OffsetsToCommit.emplace(std::make_pair(topicPartition, newCommit));
+                } else {
+                    it->second = newCommit;
+                }
+            }
+        }
+        SendOkResponse<TTxnOffsetCommitResponseData>(ev);
+    }
+
+    /* 
+    If abort, will send back ok.
+    If commit, will do following sequence of calls and event handlers:
+    1. Open KQP Session
+    2. Handle(NKqp::TEvKqp::TEvCreateSessionResponse) - KQP session created
+    3. Send Select request for current producer and consumers state, begin transaction
+    4. Handle(NKqp::TEvKqp::TEvQueryResponse): Validate returned producer and consumers state. Return PRODUCER_FENCED if check failed
+    5. Send Commit request with properly created KafkaApiOperations
+    6. Handle(NKqp::TEvKqp::TEvQueryResponse): If everything committed successfully, return OK to the client
+    7. Close KQP session, send to coordinator TEvTransactionActorDied, die.
+    */
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvEndTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved END_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TEndTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+
+        bool txnAborted = !ev->Get()->Request->Committed;
+        if (CommitStarted) {
+            return; // we just ignore second and subsequent requests
+        } else if (txnAborted) {
+            SendOkResponse<TEndTxnResponseData>(ev);
+        } else {
+            CommitStarted = true;
+            EndTxnRequestPtr = std::move(ev);
+            StartKqpSession(ctx);
+        }
+    }
+
+    void TKafkaTransactionActor::Handle(TEvents::TEvPoison::TPtr&, const TActorContext& ctx) {
+        Die(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvCreateSessionResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "KQP session created");
+        if (!Kqp->HandleCreateSessionResponse(ev, ctx)) {
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, "Failed to create KQP session");
+            Die(ctx);
+            return;
+        }
+
+        SendToKqpValidationRequests(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Receieved query response from KQP for " << GetAsStr(LastSentToKqpRequest) << " request");
+        if (auto error = GetErrorFromYdbResponse(ev)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error->data());
+            Die(ctx);
+            return;
+        }
+
+        switch (LastSentToKqpRequest) {
+            case EKafkaTxnKqpRequests::SELECT:
+                HandleSelectResponse(*ev->Get(), ctx);
+                break;
+            case EKafkaTxnKqpRequests::COMMIT:
+                HandleCommitResponse(ctx);
+                break;
+            default:
+                Y_FAIL("Unexpected KQP request");
+        }
+    }
+
+    void TKafkaTransactionActor::StartKqpSession(const TActorContext& ctx) {
+        Kqp = std::make_unique<TKqpTxHelper>(DatabasePath);
+        KAFKA_LOG_D(TStringBuilder() << "Sending create session request to KQP for database " << DatabasePath);
+        Kqp->SendCreateSessionRequest(ctx, KqpActorId);
path: ydb/core/kafka_proxy/actors/kafka_transaction_actor.cpp
user: login: a-serebryanskiy
body: Нет, в тестах он другой - id dummy kqp actor-а
created_at: 2025-04-21T07:22:53Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,448 @@
+#include "kafka_transaction_actor.h"
+#include "kafka_transaction_actor_sql.cpp"
+#include "txn_actor_response_builder.h"
+#include <ydb/core/kafka_proxy/kafka_transactions_coordinator.h>
+#include <ydb/core/kafka_proxy/kafka_transactional_producers_initializers.h>
+#include <ydb/core/kafka_proxy/kafka_consumer_groups_metadata_initializers.h>
+#include <ydb/core/kqp/common/simple/services.h>
+#include <regex>
+
+namespace NKafka {
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddPartitionsToTxnRequest::TPtr& ev, const TActorContext& ctx){
+        KAFKA_LOG_D("Receieved ADD_PARTITIONS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddPartitionsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                PartitionsInTxn.emplace(GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest));
+            }
+        }
+        SendOkResponse<TAddPartitionsToTxnResponseData>(ev);
+    }
+
+    // Method does nothing. In Kafka it will add __consumer_offsets topic to transaction, but 
+    // in YDB Topics we store offsets in table and do not need this extra action.
+    // Thus we can just ignore this request.
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddOffsetsToTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved ADD_OFFSETS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddOffsetsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        SendOkResponse<TAddOffsetsToTxnResponseData>(ev);
+    }
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvTxnOffsetCommitRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved TXN_OFFSET_COMMIT request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TTxnOffsetCommitResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        // save offsets for future use
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                TTopicPartition topicPartition = {GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest.PartitionIndex)};
+                TPartitionCommit newCommit{
+                    .Partition = partitionInRequest.PartitionIndex,
+                    .Offset = partitionInRequest.CommittedOffset,
+                    .ConsumerName = ev->Get()->Request->GroupId->c_str(),
+                    .ConsumerGeneration = ev->Get()->Request->GenerationId
+                };
+                auto it = OffsetsToCommit.find(topicPartition);
+                if (it == OffsetsToCommit.end()) {
+                    OffsetsToCommit.emplace(std::make_pair(topicPartition, newCommit));
+                } else {
+                    it->second = newCommit;
+                }
+            }
+        }
+        SendOkResponse<TTxnOffsetCommitResponseData>(ev);
+    }
+
+    /* 
+    If abort, will send back ok.
+    If commit, will do following sequence of calls and event handlers:
+    1. Open KQP Session
+    2. Handle(NKqp::TEvKqp::TEvCreateSessionResponse) - KQP session created
+    3. Send Select request for current producer and consumers state, begin transaction
+    4. Handle(NKqp::TEvKqp::TEvQueryResponse): Validate returned producer and consumers state. Return PRODUCER_FENCED if check failed
+    5. Send Commit request with properly created KafkaApiOperations
+    6. Handle(NKqp::TEvKqp::TEvQueryResponse): If everything committed successfully, return OK to the client
+    7. Close KQP session, send to coordinator TEvTransactionActorDied, die.
+    */
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvEndTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved END_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TEndTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+
+        bool txnAborted = !ev->Get()->Request->Committed;
+        if (CommitStarted) {
+            return; // we just ignore second and subsequent requests
+        } else if (txnAborted) {
+            SendOkResponse<TEndTxnResponseData>(ev);
+        } else {
+            CommitStarted = true;
+            EndTxnRequestPtr = std::move(ev);
+            StartKqpSession(ctx);
+        }
+    }
+
+    void TKafkaTransactionActor::Handle(TEvents::TEvPoison::TPtr&, const TActorContext& ctx) {
+        Die(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvCreateSessionResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "KQP session created");
+        if (!Kqp->HandleCreateSessionResponse(ev, ctx)) {
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, "Failed to create KQP session");
+            Die(ctx);
+            return;
+        }
+
+        SendToKqpValidationRequests(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Receieved query response from KQP for " << GetAsStr(LastSentToKqpRequest) << " request");
+        if (auto error = GetErrorFromYdbResponse(ev)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error->data());
+            Die(ctx);
+            return;
+        }
+
+        switch (LastSentToKqpRequest) {
+            case EKafkaTxnKqpRequests::SELECT:
+                HandleSelectResponse(*ev->Get(), ctx);
+                break;
+            case EKafkaTxnKqpRequests::COMMIT:
+                HandleCommitResponse(ctx);
+                break;
+            default:
+                Y_FAIL("Unexpected KQP request");
+        }
+    }
+
+    void TKafkaTransactionActor::StartKqpSession(const TActorContext& ctx) {
+        Kqp = std::make_unique<TKqpTxHelper>(DatabasePath);
+        KAFKA_LOG_D(TStringBuilder() << "Sending create session request to KQP for database " << DatabasePath);
+        Kqp->SendCreateSessionRequest(ctx, KqpActorId);
+    }
+
+    void TKafkaTransactionActor::SendToKqpValidationRequests(const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Sending select request to KQP for database " << DatabasePath);
+        Kqp->SendYqlRequest(
+            GetYqlWithTablesNames(NKafkaTransactionSql::SELECT_FOR_VALIDATION), 
+            BuildSelectParams(), 
+            ++KqpCookie, 
+            ctx,
+            false,
+            KqpActorId
+        );
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::SELECT;
+    }
+
+    void TKafkaTransactionActor::SendCommitTxnRequest(const TString& kqpTransactionId) {
+        auto request = BuildCommitTxnRequestToKqp(kqpTransactionId);
+
+        Send(KqpActorId, request.Release(), 0, ++KqpCookie);
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::COMMIT;
+    }
+    
+    // Response senders
+    template<class ErrorResponseType, class EventType>
+    void TKafkaTransactionActor::SendInvalidTransactionActorStateResponse(TAutoPtr<TEventHandle<EventType>>& evHandle) {
+        auto kafkaRequest = evHandle->Get()->Request;
+        KAFKA_LOG_CRIT(TStringBuilder() << "Recieved invalid request. Got: "
+            << "transactionalId=" << kafkaRequest->TransactionalId->c_str() 
+            << ", producerId=" << kafkaRequest->ProducerId 
+            << ", producerEpoch=" << kafkaRequest->ProducerEpoch
+        );
+        std::shared_ptr<ErrorResponseType> response = NKafkaTransactions::BuildResponse<ErrorResponseType>(kafkaRequest, EKafkaErrors::UNKNOWN_SERVER_ERROR);
path: ydb/core/kafka_proxy/actors/kafka_transaction_actor.cpp
user: login: a-serebryanskiy
body: А я наоборот хотел эксплицитнее, по твоей рекомендации избегать auto) Поправлю
created_at: 2025-04-21T07:23:30Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,448 @@
+#include "kafka_transaction_actor.h"
+#include "kafka_transaction_actor_sql.cpp"
+#include "txn_actor_response_builder.h"
+#include <ydb/core/kafka_proxy/kafka_transactions_coordinator.h>
+#include <ydb/core/kafka_proxy/kafka_transactional_producers_initializers.h>
+#include <ydb/core/kafka_proxy/kafka_consumer_groups_metadata_initializers.h>
+#include <ydb/core/kqp/common/simple/services.h>
+#include <regex>
+
+namespace NKafka {
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddPartitionsToTxnRequest::TPtr& ev, const TActorContext& ctx){
+        KAFKA_LOG_D("Receieved ADD_PARTITIONS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddPartitionsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                PartitionsInTxn.emplace(GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest));
+            }
+        }
+        SendOkResponse<TAddPartitionsToTxnResponseData>(ev);
+    }
+
+    // Method does nothing. In Kafka it will add __consumer_offsets topic to transaction, but 
+    // in YDB Topics we store offsets in table and do not need this extra action.
+    // Thus we can just ignore this request.
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddOffsetsToTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved ADD_OFFSETS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddOffsetsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        SendOkResponse<TAddOffsetsToTxnResponseData>(ev);
+    }
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvTxnOffsetCommitRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved TXN_OFFSET_COMMIT request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TTxnOffsetCommitResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        // save offsets for future use
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                TTopicPartition topicPartition = {GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest.PartitionIndex)};
+                TPartitionCommit newCommit{
+                    .Partition = partitionInRequest.PartitionIndex,
+                    .Offset = partitionInRequest.CommittedOffset,
+                    .ConsumerName = ev->Get()->Request->GroupId->c_str(),
+                    .ConsumerGeneration = ev->Get()->Request->GenerationId
+                };
+                auto it = OffsetsToCommit.find(topicPartition);
+                if (it == OffsetsToCommit.end()) {
+                    OffsetsToCommit.emplace(std::make_pair(topicPartition, newCommit));
+                } else {
+                    it->second = newCommit;
+                }
+            }
+        }
+        SendOkResponse<TTxnOffsetCommitResponseData>(ev);
+    }
+
+    /* 
+    If abort, will send back ok.
+    If commit, will do following sequence of calls and event handlers:
+    1. Open KQP Session
+    2. Handle(NKqp::TEvKqp::TEvCreateSessionResponse) - KQP session created
+    3. Send Select request for current producer and consumers state, begin transaction
+    4. Handle(NKqp::TEvKqp::TEvQueryResponse): Validate returned producer and consumers state. Return PRODUCER_FENCED if check failed
+    5. Send Commit request with properly created KafkaApiOperations
+    6. Handle(NKqp::TEvKqp::TEvQueryResponse): If everything committed successfully, return OK to the client
+    7. Close KQP session, send to coordinator TEvTransactionActorDied, die.
+    */
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvEndTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved END_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TEndTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+
+        bool txnAborted = !ev->Get()->Request->Committed;
+        if (CommitStarted) {
+            return; // we just ignore second and subsequent requests
+        } else if (txnAborted) {
+            SendOkResponse<TEndTxnResponseData>(ev);
+        } else {
+            CommitStarted = true;
+            EndTxnRequestPtr = std::move(ev);
+            StartKqpSession(ctx);
+        }
+    }
+
+    void TKafkaTransactionActor::Handle(TEvents::TEvPoison::TPtr&, const TActorContext& ctx) {
+        Die(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvCreateSessionResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "KQP session created");
+        if (!Kqp->HandleCreateSessionResponse(ev, ctx)) {
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, "Failed to create KQP session");
+            Die(ctx);
+            return;
+        }
+
+        SendToKqpValidationRequests(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Receieved query response from KQP for " << GetAsStr(LastSentToKqpRequest) << " request");
+        if (auto error = GetErrorFromYdbResponse(ev)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error->data());
+            Die(ctx);
+            return;
+        }
+
+        switch (LastSentToKqpRequest) {
+            case EKafkaTxnKqpRequests::SELECT:
+                HandleSelectResponse(*ev->Get(), ctx);
+                break;
+            case EKafkaTxnKqpRequests::COMMIT:
+                HandleCommitResponse(ctx);
+                break;
+            default:
+                Y_FAIL("Unexpected KQP request");
+        }
+    }
+
+    void TKafkaTransactionActor::StartKqpSession(const TActorContext& ctx) {
+        Kqp = std::make_unique<TKqpTxHelper>(DatabasePath);
+        KAFKA_LOG_D(TStringBuilder() << "Sending create session request to KQP for database " << DatabasePath);
+        Kqp->SendCreateSessionRequest(ctx, KqpActorId);
+    }
+
+    void TKafkaTransactionActor::SendToKqpValidationRequests(const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Sending select request to KQP for database " << DatabasePath);
+        Kqp->SendYqlRequest(
+            GetYqlWithTablesNames(NKafkaTransactionSql::SELECT_FOR_VALIDATION), 
+            BuildSelectParams(), 
+            ++KqpCookie, 
+            ctx,
+            false,
+            KqpActorId
+        );
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::SELECT;
+    }
+
+    void TKafkaTransactionActor::SendCommitTxnRequest(const TString& kqpTransactionId) {
+        auto request = BuildCommitTxnRequestToKqp(kqpTransactionId);
+
+        Send(KqpActorId, request.Release(), 0, ++KqpCookie);
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::COMMIT;
+    }
+    
+    // Response senders
+    template<class ErrorResponseType, class EventType>
+    void TKafkaTransactionActor::SendInvalidTransactionActorStateResponse(TAutoPtr<TEventHandle<EventType>>& evHandle) {
+        auto kafkaRequest = evHandle->Get()->Request;
path: ydb/core/kafka_proxy/actors/kafka_transaction_actor.cpp
user: login: a-serebryanskiy
body: Нет! нужна только ссылка. Поправлю
created_at: 2025-04-21T07:24:18Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,448 @@
+#include "kafka_transaction_actor.h"
+#include "kafka_transaction_actor_sql.cpp"
+#include "txn_actor_response_builder.h"
+#include <ydb/core/kafka_proxy/kafka_transactions_coordinator.h>
+#include <ydb/core/kafka_proxy/kafka_transactional_producers_initializers.h>
+#include <ydb/core/kafka_proxy/kafka_consumer_groups_metadata_initializers.h>
+#include <ydb/core/kqp/common/simple/services.h>
+#include <regex>
+
+namespace NKafka {
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddPartitionsToTxnRequest::TPtr& ev, const TActorContext& ctx){
+        KAFKA_LOG_D("Receieved ADD_PARTITIONS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddPartitionsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                PartitionsInTxn.emplace(GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest));
+            }
+        }
+        SendOkResponse<TAddPartitionsToTxnResponseData>(ev);
+    }
+
+    // Method does nothing. In Kafka it will add __consumer_offsets topic to transaction, but 
+    // in YDB Topics we store offsets in table and do not need this extra action.
+    // Thus we can just ignore this request.
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddOffsetsToTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved ADD_OFFSETS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddOffsetsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        SendOkResponse<TAddOffsetsToTxnResponseData>(ev);
+    }
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvTxnOffsetCommitRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved TXN_OFFSET_COMMIT request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TTxnOffsetCommitResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        // save offsets for future use
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                TTopicPartition topicPartition = {GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest.PartitionIndex)};
+                TPartitionCommit newCommit{
+                    .Partition = partitionInRequest.PartitionIndex,
+                    .Offset = partitionInRequest.CommittedOffset,
+                    .ConsumerName = ev->Get()->Request->GroupId->c_str(),
+                    .ConsumerGeneration = ev->Get()->Request->GenerationId
+                };
+                auto it = OffsetsToCommit.find(topicPartition);
+                if (it == OffsetsToCommit.end()) {
+                    OffsetsToCommit.emplace(std::make_pair(topicPartition, newCommit));
+                } else {
+                    it->second = newCommit;
+                }
+            }
+        }
+        SendOkResponse<TTxnOffsetCommitResponseData>(ev);
+    }
+
+    /* 
+    If abort, will send back ok.
+    If commit, will do following sequence of calls and event handlers:
+    1. Open KQP Session
+    2. Handle(NKqp::TEvKqp::TEvCreateSessionResponse) - KQP session created
+    3. Send Select request for current producer and consumers state, begin transaction
+    4. Handle(NKqp::TEvKqp::TEvQueryResponse): Validate returned producer and consumers state. Return PRODUCER_FENCED if check failed
+    5. Send Commit request with properly created KafkaApiOperations
+    6. Handle(NKqp::TEvKqp::TEvQueryResponse): If everything committed successfully, return OK to the client
+    7. Close KQP session, send to coordinator TEvTransactionActorDied, die.
+    */
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvEndTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved END_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TEndTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+
+        bool txnAborted = !ev->Get()->Request->Committed;
+        if (CommitStarted) {
+            return; // we just ignore second and subsequent requests
+        } else if (txnAborted) {
+            SendOkResponse<TEndTxnResponseData>(ev);
+        } else {
+            CommitStarted = true;
+            EndTxnRequestPtr = std::move(ev);
+            StartKqpSession(ctx);
+        }
+    }
+
+    void TKafkaTransactionActor::Handle(TEvents::TEvPoison::TPtr&, const TActorContext& ctx) {
+        Die(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvCreateSessionResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "KQP session created");
+        if (!Kqp->HandleCreateSessionResponse(ev, ctx)) {
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, "Failed to create KQP session");
+            Die(ctx);
+            return;
+        }
+
+        SendToKqpValidationRequests(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Receieved query response from KQP for " << GetAsStr(LastSentToKqpRequest) << " request");
+        if (auto error = GetErrorFromYdbResponse(ev)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error->data());
+            Die(ctx);
+            return;
+        }
+
+        switch (LastSentToKqpRequest) {
+            case EKafkaTxnKqpRequests::SELECT:
+                HandleSelectResponse(*ev->Get(), ctx);
+                break;
+            case EKafkaTxnKqpRequests::COMMIT:
+                HandleCommitResponse(ctx);
+                break;
+            default:
+                Y_FAIL("Unexpected KQP request");
+        }
+    }
+
+    void TKafkaTransactionActor::StartKqpSession(const TActorContext& ctx) {
+        Kqp = std::make_unique<TKqpTxHelper>(DatabasePath);
+        KAFKA_LOG_D(TStringBuilder() << "Sending create session request to KQP for database " << DatabasePath);
+        Kqp->SendCreateSessionRequest(ctx, KqpActorId);
+    }
+
+    void TKafkaTransactionActor::SendToKqpValidationRequests(const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Sending select request to KQP for database " << DatabasePath);
+        Kqp->SendYqlRequest(
+            GetYqlWithTablesNames(NKafkaTransactionSql::SELECT_FOR_VALIDATION), 
+            BuildSelectParams(), 
+            ++KqpCookie, 
+            ctx,
+            false,
+            KqpActorId
+        );
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::SELECT;
+    }
+
+    void TKafkaTransactionActor::SendCommitTxnRequest(const TString& kqpTransactionId) {
+        auto request = BuildCommitTxnRequestToKqp(kqpTransactionId);
+
+        Send(KqpActorId, request.Release(), 0, ++KqpCookie);
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::COMMIT;
+    }
+    
+    // Response senders
+    template<class ErrorResponseType, class EventType>
+    void TKafkaTransactionActor::SendInvalidTransactionActorStateResponse(TAutoPtr<TEventHandle<EventType>>& evHandle) {
+        auto kafkaRequest = evHandle->Get()->Request;
+        KAFKA_LOG_CRIT(TStringBuilder() << "Recieved invalid request. Got: "
+            << "transactionalId=" << kafkaRequest->TransactionalId->c_str() 
+            << ", producerId=" << kafkaRequest->ProducerId 
+            << ", producerEpoch=" << kafkaRequest->ProducerEpoch
+        );
+        std::shared_ptr<ErrorResponseType> response = NKafkaTransactions::BuildResponse<ErrorResponseType>(kafkaRequest, EKafkaErrors::UNKNOWN_SERVER_ERROR);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, EKafkaErrors::UNKNOWN_SERVER_ERROR));
+    }
+
+    template<class ErrorResponseType, class EventType>
+    void TKafkaTransactionActor::SendFailResponse(TAutoPtr<TEventHandle<EventType>>& evHandle, EKafkaErrors errorCode, const TString& errorMessage) {
+        if (errorMessage) {
+            KAFKA_LOG_W(TStringBuilder() << "Sending fail response with error code: " << errorCode << ". Reason:  " << errorMessage);
+        } else {
+            KAFKA_LOG_W(TStringBuilder() << "Sending fail response with error code: " << errorCode);
+        }
+
+        std::shared_ptr<ErrorResponseType> response = NKafkaTransactions::BuildResponse<ErrorResponseType>(evHandle->Get()->Request, errorCode);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, errorCode));
+    }
+
+    template<class ResponseType, class EventType>
+    void TKafkaTransactionActor::SendOkResponse(TAutoPtr<TEventHandle<EventType>>& evHandle) {
+        auto kafkaRequest = evHandle->Get()->Request;
+        KAFKA_LOG_D(TStringBuilder() << "Sending OK response to " << evHandle->Get()->ConnectionId << " with correlationId " << evHandle->Get()->CorrelationId << " and transactionalId " << TransactionalId);
+        std::shared_ptr<ResponseType> response = NKafkaTransactions::BuildResponse<ResponseType>(kafkaRequest, EKafkaErrors::NONE_ERROR);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, EKafkaErrors::NONE_ERROR));
+    }
+
+    // helper methods
+    void TKafkaTransactionActor::Die(const TActorContext &ctx) {
+        KAFKA_LOG_D("Dying.");
+        if (Kqp) {
+            Kqp->CloseKqpSession(ctx);
+        }
+        Send(TxnCoordinatorActorId, new TEvKafka::TEvTransactionActorDied(TransactionalId, ProducerId, ProducerEpoch));
+        TBase::Die(ctx);
+    }
+
+    template<class EventType>
+    bool TKafkaTransactionActor::ProducerInRequestIsValid(TMessagePtr<EventType> kafkaRequest) {
+        return kafkaRequest->TransactionalId->c_str() == TransactionalId 
+            && kafkaRequest->ProducerId == ProducerId 
+            && kafkaRequest->ProducerEpoch == ProducerEpoch;
path: ydb/core/kafka_proxy/actors/kafka_transaction_actor.cpp
user: login: Alek5andr-Kotov
body: А вот если использовать `TProducerState`, то можно написать одно сравнение... ;)
created_at: 2025-04-21T07:24:41Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,448 @@
+#include "kafka_transaction_actor.h"
+#include "kafka_transaction_actor_sql.cpp"
+#include "txn_actor_response_builder.h"
+#include <ydb/core/kafka_proxy/kafka_transactions_coordinator.h>
+#include <ydb/core/kafka_proxy/kafka_transactional_producers_initializers.h>
+#include <ydb/core/kafka_proxy/kafka_consumer_groups_metadata_initializers.h>
+#include <ydb/core/kqp/common/simple/services.h>
+#include <regex>
+
+namespace NKafka {
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddPartitionsToTxnRequest::TPtr& ev, const TActorContext& ctx){
+        KAFKA_LOG_D("Receieved ADD_PARTITIONS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddPartitionsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                PartitionsInTxn.emplace(GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest));
+            }
+        }
+        SendOkResponse<TAddPartitionsToTxnResponseData>(ev);
+    }
+
+    // Method does nothing. In Kafka it will add __consumer_offsets topic to transaction, but 
+    // in YDB Topics we store offsets in table and do not need this extra action.
+    // Thus we can just ignore this request.
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddOffsetsToTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved ADD_OFFSETS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddOffsetsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        SendOkResponse<TAddOffsetsToTxnResponseData>(ev);
+    }
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvTxnOffsetCommitRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved TXN_OFFSET_COMMIT request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TTxnOffsetCommitResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        // save offsets for future use
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                TTopicPartition topicPartition = {GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest.PartitionIndex)};
+                TPartitionCommit newCommit{
+                    .Partition = partitionInRequest.PartitionIndex,
+                    .Offset = partitionInRequest.CommittedOffset,
+                    .ConsumerName = ev->Get()->Request->GroupId->c_str(),
+                    .ConsumerGeneration = ev->Get()->Request->GenerationId
+                };
+                auto it = OffsetsToCommit.find(topicPartition);
+                if (it == OffsetsToCommit.end()) {
+                    OffsetsToCommit.emplace(std::make_pair(topicPartition, newCommit));
+                } else {
+                    it->second = newCommit;
+                }
+            }
+        }
+        SendOkResponse<TTxnOffsetCommitResponseData>(ev);
+    }
+
+    /* 
+    If abort, will send back ok.
+    If commit, will do following sequence of calls and event handlers:
+    1. Open KQP Session
+    2. Handle(NKqp::TEvKqp::TEvCreateSessionResponse) - KQP session created
+    3. Send Select request for current producer and consumers state, begin transaction
+    4. Handle(NKqp::TEvKqp::TEvQueryResponse): Validate returned producer and consumers state. Return PRODUCER_FENCED if check failed
+    5. Send Commit request with properly created KafkaApiOperations
+    6. Handle(NKqp::TEvKqp::TEvQueryResponse): If everything committed successfully, return OK to the client
+    7. Close KQP session, send to coordinator TEvTransactionActorDied, die.
+    */
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvEndTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved END_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TEndTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+
+        bool txnAborted = !ev->Get()->Request->Committed;
+        if (CommitStarted) {
+            return; // we just ignore second and subsequent requests
+        } else if (txnAborted) {
+            SendOkResponse<TEndTxnResponseData>(ev);
+        } else {
+            CommitStarted = true;
+            EndTxnRequestPtr = std::move(ev);
+            StartKqpSession(ctx);
+        }
+    }
+
+    void TKafkaTransactionActor::Handle(TEvents::TEvPoison::TPtr&, const TActorContext& ctx) {
+        Die(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvCreateSessionResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "KQP session created");
+        if (!Kqp->HandleCreateSessionResponse(ev, ctx)) {
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, "Failed to create KQP session");
+            Die(ctx);
+            return;
+        }
+
+        SendToKqpValidationRequests(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Receieved query response from KQP for " << GetAsStr(LastSentToKqpRequest) << " request");
+        if (auto error = GetErrorFromYdbResponse(ev)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error->data());
+            Die(ctx);
+            return;
+        }
+
+        switch (LastSentToKqpRequest) {
+            case EKafkaTxnKqpRequests::SELECT:
+                HandleSelectResponse(*ev->Get(), ctx);
+                break;
+            case EKafkaTxnKqpRequests::COMMIT:
+                HandleCommitResponse(ctx);
+                break;
+            default:
+                Y_FAIL("Unexpected KQP request");
+        }
+    }
+
+    void TKafkaTransactionActor::StartKqpSession(const TActorContext& ctx) {
+        Kqp = std::make_unique<TKqpTxHelper>(DatabasePath);
+        KAFKA_LOG_D(TStringBuilder() << "Sending create session request to KQP for database " << DatabasePath);
+        Kqp->SendCreateSessionRequest(ctx, KqpActorId);
+    }
+
+    void TKafkaTransactionActor::SendToKqpValidationRequests(const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Sending select request to KQP for database " << DatabasePath);
+        Kqp->SendYqlRequest(
+            GetYqlWithTablesNames(NKafkaTransactionSql::SELECT_FOR_VALIDATION), 
+            BuildSelectParams(), 
+            ++KqpCookie, 
+            ctx,
+            false,
+            KqpActorId
+        );
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::SELECT;
+    }
+
+    void TKafkaTransactionActor::SendCommitTxnRequest(const TString& kqpTransactionId) {
+        auto request = BuildCommitTxnRequestToKqp(kqpTransactionId);
+
+        Send(KqpActorId, request.Release(), 0, ++KqpCookie);
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::COMMIT;
+    }
+    
+    // Response senders
+    template<class ErrorResponseType, class EventType>
+    void TKafkaTransactionActor::SendInvalidTransactionActorStateResponse(TAutoPtr<TEventHandle<EventType>>& evHandle) {
+        auto kafkaRequest = evHandle->Get()->Request;
+        KAFKA_LOG_CRIT(TStringBuilder() << "Recieved invalid request. Got: "
+            << "transactionalId=" << kafkaRequest->TransactionalId->c_str() 
+            << ", producerId=" << kafkaRequest->ProducerId 
+            << ", producerEpoch=" << kafkaRequest->ProducerEpoch
+        );
+        std::shared_ptr<ErrorResponseType> response = NKafkaTransactions::BuildResponse<ErrorResponseType>(kafkaRequest, EKafkaErrors::UNKNOWN_SERVER_ERROR);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, EKafkaErrors::UNKNOWN_SERVER_ERROR));
path: ydb/core/kafka_proxy/actors/kafka_transaction_actor.cpp
user: login: a-serebryanskiy
body: Сделал отдельный метод, так как выше (в 167 строке) уровень логирования - CRIT. Если мы зашли в этот метод - значит актор оказался в состоянии, в котором никогда не должен был оказываться. Возможно имеет смысл здесь VERIFY добавить, но они меня пугают тем, что кладут весь процесс
created_at: 2025-04-21T07:25:39Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,448 @@
+#include "kafka_transaction_actor.h"
+#include "kafka_transaction_actor_sql.cpp"
+#include "txn_actor_response_builder.h"
+#include <ydb/core/kafka_proxy/kafka_transactions_coordinator.h>
+#include <ydb/core/kafka_proxy/kafka_transactional_producers_initializers.h>
+#include <ydb/core/kafka_proxy/kafka_consumer_groups_metadata_initializers.h>
+#include <ydb/core/kqp/common/simple/services.h>
+#include <regex>
+
+namespace NKafka {
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddPartitionsToTxnRequest::TPtr& ev, const TActorContext& ctx){
+        KAFKA_LOG_D("Receieved ADD_PARTITIONS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddPartitionsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                PartitionsInTxn.emplace(GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest));
+            }
+        }
+        SendOkResponse<TAddPartitionsToTxnResponseData>(ev);
+    }
+
+    // Method does nothing. In Kafka it will add __consumer_offsets topic to transaction, but 
+    // in YDB Topics we store offsets in table and do not need this extra action.
+    // Thus we can just ignore this request.
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddOffsetsToTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved ADD_OFFSETS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddOffsetsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        SendOkResponse<TAddOffsetsToTxnResponseData>(ev);
+    }
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvTxnOffsetCommitRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved TXN_OFFSET_COMMIT request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TTxnOffsetCommitResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        // save offsets for future use
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                TTopicPartition topicPartition = {GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest.PartitionIndex)};
+                TPartitionCommit newCommit{
+                    .Partition = partitionInRequest.PartitionIndex,
+                    .Offset = partitionInRequest.CommittedOffset,
+                    .ConsumerName = ev->Get()->Request->GroupId->c_str(),
+                    .ConsumerGeneration = ev->Get()->Request->GenerationId
+                };
+                auto it = OffsetsToCommit.find(topicPartition);
+                if (it == OffsetsToCommit.end()) {
+                    OffsetsToCommit.emplace(std::make_pair(topicPartition, newCommit));
+                } else {
+                    it->second = newCommit;
+                }
+            }
+        }
+        SendOkResponse<TTxnOffsetCommitResponseData>(ev);
+    }
+
+    /* 
+    If abort, will send back ok.
+    If commit, will do following sequence of calls and event handlers:
+    1. Open KQP Session
+    2. Handle(NKqp::TEvKqp::TEvCreateSessionResponse) - KQP session created
+    3. Send Select request for current producer and consumers state, begin transaction
+    4. Handle(NKqp::TEvKqp::TEvQueryResponse): Validate returned producer and consumers state. Return PRODUCER_FENCED if check failed
+    5. Send Commit request with properly created KafkaApiOperations
+    6. Handle(NKqp::TEvKqp::TEvQueryResponse): If everything committed successfully, return OK to the client
+    7. Close KQP session, send to coordinator TEvTransactionActorDied, die.
+    */
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvEndTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved END_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TEndTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+
+        bool txnAborted = !ev->Get()->Request->Committed;
+        if (CommitStarted) {
+            return; // we just ignore second and subsequent requests
+        } else if (txnAborted) {
+            SendOkResponse<TEndTxnResponseData>(ev);
+        } else {
+            CommitStarted = true;
+            EndTxnRequestPtr = std::move(ev);
+            StartKqpSession(ctx);
+        }
+    }
+
+    void TKafkaTransactionActor::Handle(TEvents::TEvPoison::TPtr&, const TActorContext& ctx) {
+        Die(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvCreateSessionResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "KQP session created");
+        if (!Kqp->HandleCreateSessionResponse(ev, ctx)) {
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, "Failed to create KQP session");
+            Die(ctx);
+            return;
+        }
+
+        SendToKqpValidationRequests(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Receieved query response from KQP for " << GetAsStr(LastSentToKqpRequest) << " request");
+        if (auto error = GetErrorFromYdbResponse(ev)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error->data());
+            Die(ctx);
+            return;
+        }
+
+        switch (LastSentToKqpRequest) {
+            case EKafkaTxnKqpRequests::SELECT:
+                HandleSelectResponse(*ev->Get(), ctx);
+                break;
+            case EKafkaTxnKqpRequests::COMMIT:
+                HandleCommitResponse(ctx);
+                break;
+            default:
+                Y_FAIL("Unexpected KQP request");
+        }
+    }
+
+    void TKafkaTransactionActor::StartKqpSession(const TActorContext& ctx) {
+        Kqp = std::make_unique<TKqpTxHelper>(DatabasePath);
+        KAFKA_LOG_D(TStringBuilder() << "Sending create session request to KQP for database " << DatabasePath);
+        Kqp->SendCreateSessionRequest(ctx, KqpActorId);
+    }
+
+    void TKafkaTransactionActor::SendToKqpValidationRequests(const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Sending select request to KQP for database " << DatabasePath);
+        Kqp->SendYqlRequest(
+            GetYqlWithTablesNames(NKafkaTransactionSql::SELECT_FOR_VALIDATION), 
+            BuildSelectParams(), 
+            ++KqpCookie, 
+            ctx,
+            false,
+            KqpActorId
+        );
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::SELECT;
+    }
+
+    void TKafkaTransactionActor::SendCommitTxnRequest(const TString& kqpTransactionId) {
+        auto request = BuildCommitTxnRequestToKqp(kqpTransactionId);
+
+        Send(KqpActorId, request.Release(), 0, ++KqpCookie);
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::COMMIT;
+    }
+    
+    // Response senders
+    template<class ErrorResponseType, class EventType>
+    void TKafkaTransactionActor::SendInvalidTransactionActorStateResponse(TAutoPtr<TEventHandle<EventType>>& evHandle) {
+        auto kafkaRequest = evHandle->Get()->Request;
+        KAFKA_LOG_CRIT(TStringBuilder() << "Recieved invalid request. Got: "
+            << "transactionalId=" << kafkaRequest->TransactionalId->c_str() 
+            << ", producerId=" << kafkaRequest->ProducerId 
+            << ", producerEpoch=" << kafkaRequest->ProducerEpoch
+        );
+        std::shared_ptr<ErrorResponseType> response = NKafkaTransactions::BuildResponse<ErrorResponseType>(kafkaRequest, EKafkaErrors::UNKNOWN_SERVER_ERROR);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, EKafkaErrors::UNKNOWN_SERVER_ERROR));
+    }
+
+    template<class ErrorResponseType, class EventType>
+    void TKafkaTransactionActor::SendFailResponse(TAutoPtr<TEventHandle<EventType>>& evHandle, EKafkaErrors errorCode, const TString& errorMessage) {
+        if (errorMessage) {
+            KAFKA_LOG_W(TStringBuilder() << "Sending fail response with error code: " << errorCode << ". Reason:  " << errorMessage);
+        } else {
+            KAFKA_LOG_W(TStringBuilder() << "Sending fail response with error code: " << errorCode);
+        }
+
+        std::shared_ptr<ErrorResponseType> response = NKafkaTransactions::BuildResponse<ErrorResponseType>(evHandle->Get()->Request, errorCode);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, errorCode));
+    }
+
+    template<class ResponseType, class EventType>
+    void TKafkaTransactionActor::SendOkResponse(TAutoPtr<TEventHandle<EventType>>& evHandle) {
+        auto kafkaRequest = evHandle->Get()->Request;
+        KAFKA_LOG_D(TStringBuilder() << "Sending OK response to " << evHandle->Get()->ConnectionId << " with correlationId " << evHandle->Get()->CorrelationId << " and transactionalId " << TransactionalId);
+        std::shared_ptr<ResponseType> response = NKafkaTransactions::BuildResponse<ResponseType>(kafkaRequest, EKafkaErrors::NONE_ERROR);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, EKafkaErrors::NONE_ERROR));
+    }
+
+    // helper methods
+    void TKafkaTransactionActor::Die(const TActorContext &ctx) {
+        KAFKA_LOG_D("Dying.");
+        if (Kqp) {
+            Kqp->CloseKqpSession(ctx);
+        }
+        Send(TxnCoordinatorActorId, new TEvKafka::TEvTransactionActorDied(TransactionalId, ProducerId, ProducerEpoch));
+        TBase::Die(ctx);
+    }
+
+    template<class EventType>
+    bool TKafkaTransactionActor::ProducerInRequestIsValid(TMessagePtr<EventType> kafkaRequest) {
+        return kafkaRequest->TransactionalId->c_str() == TransactionalId 
+            && kafkaRequest->ProducerId == ProducerId 
+            && kafkaRequest->ProducerEpoch == ProducerEpoch;
+    }
+
+    TString TKafkaTransactionActor::GetFullTopicPath(const TString& topicName) {
+        return NPersQueue::GetFullTopicPath(DatabasePath, topicName);
+    }
+
+    TString TKafkaTransactionActor::GetYqlWithTablesNames(const TString& templateStr) {
+        TString templateWithProducerStateTable = std::regex_replace(
+            templateStr.c_str(),
+            std::regex("<producer_state_table_name>"), 
+            NKikimr::NGRpcProxy::V1::TTransactionalProducersInitManager::GetInstant()->GetStorageTablePath().c_str()
+        );
+        TString templateWithConsumerStateTable = std::regex_replace(
+            templateWithProducerStateTable.c_str(),
+            std::regex("<consumer_state_table_name>"), 
+            NKikimr::NGRpcProxy::V1::TKafkaConsumerGroupsMetaInitManager::GetInstant()->GetStorageTablePath().c_str()
+        );
+
+        return templateWithConsumerStateTable;
+    }
+
+    NYdb::TParams TKafkaTransactionActor::BuildSelectParams() {
+        NYdb::TParamsBuilder params;
+        params.AddParam("$Database").Utf8(DatabasePath).Build();
+        params.AddParam("$TransactionalId").Utf8(TransactionalId).Build();
+        
+        // select unique consumer group names
+        std::unordered_set<TString> uniqueConsumerGroups;
+        for (auto& [partition, commitDetails] : OffsetsToCommit) {
+            uniqueConsumerGroups.emplace(commitDetails.ConsumerName);
+        }
+
+        // add unique consumer group names to request as params
+        auto& consumerGroupsParamBuilder = params.AddParam("$ConsumerGroups").BeginList();
+        for (auto& consumerGroupName : uniqueConsumerGroups) {
+            consumerGroupsParamBuilder.AddListItem().Utf8(consumerGroupName);
+        }
+        consumerGroupsParamBuilder.EndList().Build();
+
+        return params.Build();
+    }
+
+    THolder<NKikimr::NKqp::TEvKqp::TEvQueryRequest> TKafkaTransactionActor::BuildCommitTxnRequestToKqp(const TString& kqpTransactionId) {
+        auto ev = MakeHolder<TEvKqp::TEvQueryRequest>();
+
+        ev->Record.MutableRequest()->SetType(NKikimrKqp::QUERY_TYPE_UNDEFINED);
+        ev->Record.MutableRequest()->SetAction(NKikimrKqp::QUERY_ACTION_TOPIC);
+        ev->Record.MutableRequest()->SetDatabase(DatabasePath);
+        ev->Record.MutableRequest()->SetSessionId(KqpSessionId);
+
+        ev->Record.MutableRequest()->MutableTxControl()->set_commit_tx(true);
+        ev->Record.MutableRequest()->MutableTxControl()->set_tx_id(kqpTransactionId);
+
+        ev->Record.MutableRequest()->SetUsePublicResponseDataFormat(true);
+
+        auto* kafkaApiOperations = ev->Record.MutableRequest()->MutableKafkaApiOperations();
+        kafkaApiOperations->set_transactionalid(TransactionalId);
+        kafkaApiOperations->set_producerid(ProducerId);
+        kafkaApiOperations->set_producerepoch(ProducerEpoch);
+
+        for (auto& partition : PartitionsInTxn) {
+            auto* partitionInRequest = kafkaApiOperations->AddPartitionsInTxn();
+            partitionInRequest->set_topicpath(partition.TopicPath);
+            partitionInRequest->set_partitionid(partition.PartitionId);
+        }
+
+        for (auto& [partition, offsetDetails] : OffsetsToCommit) {
+            auto* offsetInRequest = kafkaApiOperations->AddOffsetsInTxn();
+            offsetInRequest->set_topicpath(partition.TopicPath);
+            offsetInRequest->set_partitionid(partition.PartitionId);
+            offsetInRequest->set_consumername(offsetDetails.ConsumerName);
+            offsetInRequest->set_consumergeneration(offsetDetails.ConsumerGeneration);
+            offsetInRequest->set_offset(offsetDetails.Offset);
+        }
+        
+        return ev;
+    }
+
+    void TKafkaTransactionActor::HandleSelectResponse(const NKqp::TEvKqp::TEvQueryResponse& response, const TActorContext& ctx) {
+        // YDB should return exactly two result sets for two queries: to producer and consumer state tables
+        if (response.Record.GetResponse().GetYdbResults().size() != 2) {
+            TString error = TStringBuilder() << "KQP returned wrong number of result sets on SELECT query. Expected 2, got " << response.Record.GetResponse().GetYdbResults().size() << ".";
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error);
+            Die(ctx);
+            return;
+        }
+
+        // parse and validate producer state
+        TMaybe<TProducerState> producerState;
+        try {
+            producerState = ParseProducerState(response);
+        } catch (const yexception& y) {
+            TString error = TStringBuilder() << "Error parsing producer state response from KQP. Reason: " << y.what();
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error);
+            Die(ctx);
+            return;
+        }
+        if (auto error = GetErrorInProducerState(producerState)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::PRODUCER_FENCED, error->data());
+            Die(ctx);
+            return;
+        }
+
+        // parse and validate consumers
+        std::unordered_map<TString, i32> consumerGenerationsByName = ParseConsumersGenerations(response);
+        if (auto error = GetErrorInConsumersStates(consumerGenerationsByName)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::PRODUCER_FENCED, error->data());
+            Die(ctx);
+            return;
+        }
+
+        KAFKA_LOG_D(TStringBuilder() << "Validated producer and consumers states. Everything is alright, sending commit");
+        auto kqpTxnId = response.Record.GetResponse().GetTxMeta().id();
+        // finally everything is valid and we can attempt to commit
+        SendCommitTxnRequest(kqpTxnId);
+    }
+
+    void TKafkaTransactionActor::HandleCommitResponse(const TActorContext& ctx) {
+        KAFKA_LOG_D("Successfully committed transaction. Sending ok and dying.");
+        SendOkResponse<TEndTxnResponseData>(EndTxnRequestPtr);
+        Die(ctx);
+    }
+
+    TMaybe<TString> TKafkaTransactionActor::GetErrorFromYdbResponse(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev) {
+        TStringBuilder builder = TStringBuilder() << "Recieved error on request to KQP. Last sent request: " << GetAsStr(LastSentToKqpRequest) << ". Reason: ";
+        if (ev->Cookie != KqpCookie) {
+            return builder << "Unexpected cookie in TEvQueryResponse. Expected KQP Cookie: " << KqpCookie << ", Actual: " << ev->Cookie << ".";
+        } else if (ev->Get()->Record.GetYdbStatus() != Ydb::StatusIds::SUCCESS) {
+            return builder << "Unexpected YDB status in TEvQueryResponse. Expected YDB SUCCESS status, Actual: " << ev->Get()->Record.GetYdbStatus() << ".";
path: ydb/core/kafka_proxy/actors/kafka_transaction_actor.cpp
user: login: Alek5andr-Kotov
body: Здесь есть возможность вывести описание ошибки из ответа? Будет проще разбираться с ошибками пользователей.
created_at: 2025-04-21T07:31:57Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,448 @@
+#include "kafka_transaction_actor.h"
+#include "kafka_transaction_actor_sql.cpp"
+#include "txn_actor_response_builder.h"
+#include <ydb/core/kafka_proxy/kafka_transactions_coordinator.h>
+#include <ydb/core/kafka_proxy/kafka_transactional_producers_initializers.h>
+#include <ydb/core/kafka_proxy/kafka_consumer_groups_metadata_initializers.h>
+#include <ydb/core/kqp/common/simple/services.h>
+#include <regex>
+
+namespace NKafka {
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddPartitionsToTxnRequest::TPtr& ev, const TActorContext& ctx){
+        KAFKA_LOG_D("Receieved ADD_PARTITIONS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddPartitionsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                PartitionsInTxn.emplace(GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest));
+            }
+        }
+        SendOkResponse<TAddPartitionsToTxnResponseData>(ev);
+    }
+
+    // Method does nothing. In Kafka it will add __consumer_offsets topic to transaction, but 
+    // in YDB Topics we store offsets in table and do not need this extra action.
+    // Thus we can just ignore this request.
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddOffsetsToTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved ADD_OFFSETS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddOffsetsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        SendOkResponse<TAddOffsetsToTxnResponseData>(ev);
+    }
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvTxnOffsetCommitRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved TXN_OFFSET_COMMIT request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TTxnOffsetCommitResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        // save offsets for future use
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                TTopicPartition topicPartition = {GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest.PartitionIndex)};
+                TPartitionCommit newCommit{
+                    .Partition = partitionInRequest.PartitionIndex,
+                    .Offset = partitionInRequest.CommittedOffset,
+                    .ConsumerName = ev->Get()->Request->GroupId->c_str(),
+                    .ConsumerGeneration = ev->Get()->Request->GenerationId
+                };
+                auto it = OffsetsToCommit.find(topicPartition);
+                if (it == OffsetsToCommit.end()) {
+                    OffsetsToCommit.emplace(std::make_pair(topicPartition, newCommit));
+                } else {
+                    it->second = newCommit;
+                }
+            }
+        }
+        SendOkResponse<TTxnOffsetCommitResponseData>(ev);
+    }
+
+    /* 
+    If abort, will send back ok.
+    If commit, will do following sequence of calls and event handlers:
+    1. Open KQP Session
+    2. Handle(NKqp::TEvKqp::TEvCreateSessionResponse) - KQP session created
+    3. Send Select request for current producer and consumers state, begin transaction
+    4. Handle(NKqp::TEvKqp::TEvQueryResponse): Validate returned producer and consumers state. Return PRODUCER_FENCED if check failed
+    5. Send Commit request with properly created KafkaApiOperations
+    6. Handle(NKqp::TEvKqp::TEvQueryResponse): If everything committed successfully, return OK to the client
+    7. Close KQP session, send to coordinator TEvTransactionActorDied, die.
+    */
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvEndTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved END_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TEndTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+
+        bool txnAborted = !ev->Get()->Request->Committed;
+        if (CommitStarted) {
+            return; // we just ignore second and subsequent requests
+        } else if (txnAborted) {
+            SendOkResponse<TEndTxnResponseData>(ev);
+        } else {
+            CommitStarted = true;
+            EndTxnRequestPtr = std::move(ev);
+            StartKqpSession(ctx);
+        }
+    }
+
+    void TKafkaTransactionActor::Handle(TEvents::TEvPoison::TPtr&, const TActorContext& ctx) {
+        Die(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvCreateSessionResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "KQP session created");
+        if (!Kqp->HandleCreateSessionResponse(ev, ctx)) {
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, "Failed to create KQP session");
+            Die(ctx);
+            return;
+        }
+
+        SendToKqpValidationRequests(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Receieved query response from KQP for " << GetAsStr(LastSentToKqpRequest) << " request");
+        if (auto error = GetErrorFromYdbResponse(ev)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error->data());
+            Die(ctx);
+            return;
+        }
+
+        switch (LastSentToKqpRequest) {
+            case EKafkaTxnKqpRequests::SELECT:
+                HandleSelectResponse(*ev->Get(), ctx);
+                break;
+            case EKafkaTxnKqpRequests::COMMIT:
+                HandleCommitResponse(ctx);
+                break;
+            default:
+                Y_FAIL("Unexpected KQP request");
+        }
+    }
+
+    void TKafkaTransactionActor::StartKqpSession(const TActorContext& ctx) {
+        Kqp = std::make_unique<TKqpTxHelper>(DatabasePath);
+        KAFKA_LOG_D(TStringBuilder() << "Sending create session request to KQP for database " << DatabasePath);
+        Kqp->SendCreateSessionRequest(ctx, KqpActorId);
+    }
+
+    void TKafkaTransactionActor::SendToKqpValidationRequests(const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Sending select request to KQP for database " << DatabasePath);
+        Kqp->SendYqlRequest(
+            GetYqlWithTablesNames(NKafkaTransactionSql::SELECT_FOR_VALIDATION), 
+            BuildSelectParams(), 
+            ++KqpCookie, 
+            ctx,
+            false,
+            KqpActorId
+        );
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::SELECT;
+    }
+
+    void TKafkaTransactionActor::SendCommitTxnRequest(const TString& kqpTransactionId) {
+        auto request = BuildCommitTxnRequestToKqp(kqpTransactionId);
+
+        Send(KqpActorId, request.Release(), 0, ++KqpCookie);
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::COMMIT;
+    }
+    
+    // Response senders
+    template<class ErrorResponseType, class EventType>
+    void TKafkaTransactionActor::SendInvalidTransactionActorStateResponse(TAutoPtr<TEventHandle<EventType>>& evHandle) {
+        auto kafkaRequest = evHandle->Get()->Request;
+        KAFKA_LOG_CRIT(TStringBuilder() << "Recieved invalid request. Got: "
+            << "transactionalId=" << kafkaRequest->TransactionalId->c_str() 
+            << ", producerId=" << kafkaRequest->ProducerId 
+            << ", producerEpoch=" << kafkaRequest->ProducerEpoch
+        );
+        std::shared_ptr<ErrorResponseType> response = NKafkaTransactions::BuildResponse<ErrorResponseType>(kafkaRequest, EKafkaErrors::UNKNOWN_SERVER_ERROR);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, EKafkaErrors::UNKNOWN_SERVER_ERROR));
+    }
+
+    template<class ErrorResponseType, class EventType>
+    void TKafkaTransactionActor::SendFailResponse(TAutoPtr<TEventHandle<EventType>>& evHandle, EKafkaErrors errorCode, const TString& errorMessage) {
+        if (errorMessage) {
+            KAFKA_LOG_W(TStringBuilder() << "Sending fail response with error code: " << errorCode << ". Reason:  " << errorMessage);
+        } else {
+            KAFKA_LOG_W(TStringBuilder() << "Sending fail response with error code: " << errorCode);
+        }
+
+        std::shared_ptr<ErrorResponseType> response = NKafkaTransactions::BuildResponse<ErrorResponseType>(evHandle->Get()->Request, errorCode);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, errorCode));
+    }
+
+    template<class ResponseType, class EventType>
+    void TKafkaTransactionActor::SendOkResponse(TAutoPtr<TEventHandle<EventType>>& evHandle) {
+        auto kafkaRequest = evHandle->Get()->Request;
+        KAFKA_LOG_D(TStringBuilder() << "Sending OK response to " << evHandle->Get()->ConnectionId << " with correlationId " << evHandle->Get()->CorrelationId << " and transactionalId " << TransactionalId);
+        std::shared_ptr<ResponseType> response = NKafkaTransactions::BuildResponse<ResponseType>(kafkaRequest, EKafkaErrors::NONE_ERROR);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, EKafkaErrors::NONE_ERROR));
+    }
+
+    // helper methods
+    void TKafkaTransactionActor::Die(const TActorContext &ctx) {
+        KAFKA_LOG_D("Dying.");
+        if (Kqp) {
+            Kqp->CloseKqpSession(ctx);
+        }
+        Send(TxnCoordinatorActorId, new TEvKafka::TEvTransactionActorDied(TransactionalId, ProducerId, ProducerEpoch));
+        TBase::Die(ctx);
+    }
+
+    template<class EventType>
+    bool TKafkaTransactionActor::ProducerInRequestIsValid(TMessagePtr<EventType> kafkaRequest) {
+        return kafkaRequest->TransactionalId->c_str() == TransactionalId 
+            && kafkaRequest->ProducerId == ProducerId 
+            && kafkaRequest->ProducerEpoch == ProducerEpoch;
+    }
+
+    TString TKafkaTransactionActor::GetFullTopicPath(const TString& topicName) {
+        return NPersQueue::GetFullTopicPath(DatabasePath, topicName);
+    }
+
+    TString TKafkaTransactionActor::GetYqlWithTablesNames(const TString& templateStr) {
+        TString templateWithProducerStateTable = std::regex_replace(
+            templateStr.c_str(),
+            std::regex("<producer_state_table_name>"), 
+            NKikimr::NGRpcProxy::V1::TTransactionalProducersInitManager::GetInstant()->GetStorageTablePath().c_str()
+        );
+        TString templateWithConsumerStateTable = std::regex_replace(
+            templateWithProducerStateTable.c_str(),
+            std::regex("<consumer_state_table_name>"), 
+            NKikimr::NGRpcProxy::V1::TKafkaConsumerGroupsMetaInitManager::GetInstant()->GetStorageTablePath().c_str()
+        );
+
+        return templateWithConsumerStateTable;
+    }
+
+    NYdb::TParams TKafkaTransactionActor::BuildSelectParams() {
+        NYdb::TParamsBuilder params;
+        params.AddParam("$Database").Utf8(DatabasePath).Build();
+        params.AddParam("$TransactionalId").Utf8(TransactionalId).Build();
+        
+        // select unique consumer group names
+        std::unordered_set<TString> uniqueConsumerGroups;
+        for (auto& [partition, commitDetails] : OffsetsToCommit) {
+            uniqueConsumerGroups.emplace(commitDetails.ConsumerName);
+        }
+
+        // add unique consumer group names to request as params
+        auto& consumerGroupsParamBuilder = params.AddParam("$ConsumerGroups").BeginList();
+        for (auto& consumerGroupName : uniqueConsumerGroups) {
+            consumerGroupsParamBuilder.AddListItem().Utf8(consumerGroupName);
+        }
+        consumerGroupsParamBuilder.EndList().Build();
+
+        return params.Build();
+    }
+
+    THolder<NKikimr::NKqp::TEvKqp::TEvQueryRequest> TKafkaTransactionActor::BuildCommitTxnRequestToKqp(const TString& kqpTransactionId) {
+        auto ev = MakeHolder<TEvKqp::TEvQueryRequest>();
+
+        ev->Record.MutableRequest()->SetType(NKikimrKqp::QUERY_TYPE_UNDEFINED);
+        ev->Record.MutableRequest()->SetAction(NKikimrKqp::QUERY_ACTION_TOPIC);
+        ev->Record.MutableRequest()->SetDatabase(DatabasePath);
+        ev->Record.MutableRequest()->SetSessionId(KqpSessionId);
+
+        ev->Record.MutableRequest()->MutableTxControl()->set_commit_tx(true);
+        ev->Record.MutableRequest()->MutableTxControl()->set_tx_id(kqpTransactionId);
+
+        ev->Record.MutableRequest()->SetUsePublicResponseDataFormat(true);
+
+        auto* kafkaApiOperations = ev->Record.MutableRequest()->MutableKafkaApiOperations();
+        kafkaApiOperations->set_transactionalid(TransactionalId);
+        kafkaApiOperations->set_producerid(ProducerId);
+        kafkaApiOperations->set_producerepoch(ProducerEpoch);
+
+        for (auto& partition : PartitionsInTxn) {
+            auto* partitionInRequest = kafkaApiOperations->AddPartitionsInTxn();
+            partitionInRequest->set_topicpath(partition.TopicPath);
+            partitionInRequest->set_partitionid(partition.PartitionId);
+        }
+
+        for (auto& [partition, offsetDetails] : OffsetsToCommit) {
+            auto* offsetInRequest = kafkaApiOperations->AddOffsetsInTxn();
+            offsetInRequest->set_topicpath(partition.TopicPath);
+            offsetInRequest->set_partitionid(partition.PartitionId);
+            offsetInRequest->set_consumername(offsetDetails.ConsumerName);
+            offsetInRequest->set_consumergeneration(offsetDetails.ConsumerGeneration);
+            offsetInRequest->set_offset(offsetDetails.Offset);
+        }
+        
+        return ev;
+    }
+
+    void TKafkaTransactionActor::HandleSelectResponse(const NKqp::TEvKqp::TEvQueryResponse& response, const TActorContext& ctx) {
+        // YDB should return exactly two result sets for two queries: to producer and consumer state tables
+        if (response.Record.GetResponse().GetYdbResults().size() != 2) {
+            TString error = TStringBuilder() << "KQP returned wrong number of result sets on SELECT query. Expected 2, got " << response.Record.GetResponse().GetYdbResults().size() << ".";
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error);
+            Die(ctx);
+            return;
+        }
+
+        // parse and validate producer state
+        TMaybe<TProducerState> producerState;
+        try {
+            producerState = ParseProducerState(response);
+        } catch (const yexception& y) {
+            TString error = TStringBuilder() << "Error parsing producer state response from KQP. Reason: " << y.what();
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error);
+            Die(ctx);
+            return;
+        }
+        if (auto error = GetErrorInProducerState(producerState)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::PRODUCER_FENCED, error->data());
+            Die(ctx);
+            return;
+        }
+
+        // parse and validate consumers
+        std::unordered_map<TString, i32> consumerGenerationsByName = ParseConsumersGenerations(response);
+        if (auto error = GetErrorInConsumersStates(consumerGenerationsByName)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::PRODUCER_FENCED, error->data());
+            Die(ctx);
+            return;
+        }
+
+        KAFKA_LOG_D(TStringBuilder() << "Validated producer and consumers states. Everything is alright, sending commit");
+        auto kqpTxnId = response.Record.GetResponse().GetTxMeta().id();
+        // finally everything is valid and we can attempt to commit
+        SendCommitTxnRequest(kqpTxnId);
+    }
+
+    void TKafkaTransactionActor::HandleCommitResponse(const TActorContext& ctx) {
+        KAFKA_LOG_D("Successfully committed transaction. Sending ok and dying.");
+        SendOkResponse<TEndTxnResponseData>(EndTxnRequestPtr);
+        Die(ctx);
+    }
+
+    TMaybe<TString> TKafkaTransactionActor::GetErrorFromYdbResponse(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev) {
+        TStringBuilder builder = TStringBuilder() << "Recieved error on request to KQP. Last sent request: " << GetAsStr(LastSentToKqpRequest) << ". Reason: ";
+        if (ev->Cookie != KqpCookie) {
+            return builder << "Unexpected cookie in TEvQueryResponse. Expected KQP Cookie: " << KqpCookie << ", Actual: " << ev->Cookie << ".";
+        } else if (ev->Get()->Record.GetYdbStatus() != Ydb::StatusIds::SUCCESS) {
+            return builder << "Unexpected YDB status in TEvQueryResponse. Expected YDB SUCCESS status, Actual: " << ev->Get()->Record.GetYdbStatus() << ".";
+        } else {
+            return {};
+        }
+    }
+
+    TMaybe<TProducerState> TKafkaTransactionActor::ParseProducerState(const NKqp::TEvKqp::TEvQueryResponse& response) {
+        auto& resp = response.Record.GetResponse();
+
+        NYdb::TResultSetParser parser(resp.GetYdbResults(NKafkaTransactionSql::PRODUCER_STATE_REQUEST_INDEX));
+
+        // for this transactional id there is no rows
+        if (parser.RowsCount() == 0) {
+            return {};
+        } 
+        // there are multiple rows for this transactional id. This is unexpected and should not happen
+        else if (parser.RowsCount() > 1) {
+            throw yexception() << "Request returned more than one row: " << resp.GetYdbResults().size();
+        } else {
+            parser.TryNextRow();
+
+            TProducerState result; 
+
+            result.TransactionalId = parser.ColumnParser("transactional_id").GetUtf8();
+            result.ProducerId = parser.ColumnParser("producer_id").GetInt64();
+            result.ProducerEpoch = parser.ColumnParser("producer_epoch").GetInt16();
+
+            return result;
+        }
+    }
+
+    TMaybe<TString> TKafkaTransactionActor::GetErrorInProducerState(const TMaybe<TProducerState>& producerState) {
+        if (!producerState) {
+            return "No producer state found. May be it has expired";
+        } else if (producerState->TransactionalId != TransactionalId || producerState->ProducerId != ProducerId || producerState->ProducerEpoch != ProducerEpoch) {
+            return TStringBuilder() << "Producer state in producers_state table is different from the one in this actor. Producer state in table: "
path: ydb/core/kafka_proxy/actors/kafka_transaction_actor.cpp
user: login: Alek5andr-Kotov
body: Поля `ProducerId` и `ProducerEpoch` лучше заменить на `TProducerState`
created_at: 2025-04-21T07:36:04Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,448 @@
+#include "kafka_transaction_actor.h"
+#include "kafka_transaction_actor_sql.cpp"
+#include "txn_actor_response_builder.h"
+#include <ydb/core/kafka_proxy/kafka_transactions_coordinator.h>
+#include <ydb/core/kafka_proxy/kafka_transactional_producers_initializers.h>
+#include <ydb/core/kafka_proxy/kafka_consumer_groups_metadata_initializers.h>
+#include <ydb/core/kqp/common/simple/services.h>
+#include <regex>
+
+namespace NKafka {
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddPartitionsToTxnRequest::TPtr& ev, const TActorContext& ctx){
+        KAFKA_LOG_D("Receieved ADD_PARTITIONS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddPartitionsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                PartitionsInTxn.emplace(GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest));
+            }
+        }
+        SendOkResponse<TAddPartitionsToTxnResponseData>(ev);
+    }
+
+    // Method does nothing. In Kafka it will add __consumer_offsets topic to transaction, but 
+    // in YDB Topics we store offsets in table and do not need this extra action.
+    // Thus we can just ignore this request.
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddOffsetsToTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved ADD_OFFSETS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddOffsetsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        SendOkResponse<TAddOffsetsToTxnResponseData>(ev);
+    }
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvTxnOffsetCommitRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved TXN_OFFSET_COMMIT request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TTxnOffsetCommitResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        // save offsets for future use
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                TTopicPartition topicPartition = {GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest.PartitionIndex)};
+                TPartitionCommit newCommit{
+                    .Partition = partitionInRequest.PartitionIndex,
+                    .Offset = partitionInRequest.CommittedOffset,
+                    .ConsumerName = ev->Get()->Request->GroupId->c_str(),
+                    .ConsumerGeneration = ev->Get()->Request->GenerationId
+                };
+                auto it = OffsetsToCommit.find(topicPartition);
+                if (it == OffsetsToCommit.end()) {
+                    OffsetsToCommit.emplace(std::make_pair(topicPartition, newCommit));
+                } else {
+                    it->second = newCommit;
+                }
+            }
+        }
+        SendOkResponse<TTxnOffsetCommitResponseData>(ev);
+    }
+
+    /* 
+    If abort, will send back ok.
+    If commit, will do following sequence of calls and event handlers:
+    1. Open KQP Session
+    2. Handle(NKqp::TEvKqp::TEvCreateSessionResponse) - KQP session created
+    3. Send Select request for current producer and consumers state, begin transaction
+    4. Handle(NKqp::TEvKqp::TEvQueryResponse): Validate returned producer and consumers state. Return PRODUCER_FENCED if check failed
+    5. Send Commit request with properly created KafkaApiOperations
+    6. Handle(NKqp::TEvKqp::TEvQueryResponse): If everything committed successfully, return OK to the client
+    7. Close KQP session, send to coordinator TEvTransactionActorDied, die.
+    */
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvEndTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved END_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TEndTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+
+        bool txnAborted = !ev->Get()->Request->Committed;
+        if (CommitStarted) {
+            return; // we just ignore second and subsequent requests
+        } else if (txnAborted) {
+            SendOkResponse<TEndTxnResponseData>(ev);
+        } else {
+            CommitStarted = true;
+            EndTxnRequestPtr = std::move(ev);
+            StartKqpSession(ctx);
+        }
+    }
+
+    void TKafkaTransactionActor::Handle(TEvents::TEvPoison::TPtr&, const TActorContext& ctx) {
+        Die(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvCreateSessionResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "KQP session created");
+        if (!Kqp->HandleCreateSessionResponse(ev, ctx)) {
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, "Failed to create KQP session");
+            Die(ctx);
+            return;
+        }
+
+        SendToKqpValidationRequests(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Receieved query response from KQP for " << GetAsStr(LastSentToKqpRequest) << " request");
+        if (auto error = GetErrorFromYdbResponse(ev)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error->data());
+            Die(ctx);
+            return;
+        }
+
+        switch (LastSentToKqpRequest) {
+            case EKafkaTxnKqpRequests::SELECT:
+                HandleSelectResponse(*ev->Get(), ctx);
+                break;
+            case EKafkaTxnKqpRequests::COMMIT:
+                HandleCommitResponse(ctx);
+                break;
+            default:
+                Y_FAIL("Unexpected KQP request");
+        }
+    }
+
+    void TKafkaTransactionActor::StartKqpSession(const TActorContext& ctx) {
+        Kqp = std::make_unique<TKqpTxHelper>(DatabasePath);
+        KAFKA_LOG_D(TStringBuilder() << "Sending create session request to KQP for database " << DatabasePath);
+        Kqp->SendCreateSessionRequest(ctx, KqpActorId);
+    }
+
+    void TKafkaTransactionActor::SendToKqpValidationRequests(const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Sending select request to KQP for database " << DatabasePath);
+        Kqp->SendYqlRequest(
+            GetYqlWithTablesNames(NKafkaTransactionSql::SELECT_FOR_VALIDATION), 
+            BuildSelectParams(), 
+            ++KqpCookie, 
+            ctx,
+            false,
+            KqpActorId
+        );
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::SELECT;
+    }
+
+    void TKafkaTransactionActor::SendCommitTxnRequest(const TString& kqpTransactionId) {
+        auto request = BuildCommitTxnRequestToKqp(kqpTransactionId);
+
+        Send(KqpActorId, request.Release(), 0, ++KqpCookie);
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::COMMIT;
+    }
+    
+    // Response senders
+    template<class ErrorResponseType, class EventType>
+    void TKafkaTransactionActor::SendInvalidTransactionActorStateResponse(TAutoPtr<TEventHandle<EventType>>& evHandle) {
+        auto kafkaRequest = evHandle->Get()->Request;
+        KAFKA_LOG_CRIT(TStringBuilder() << "Recieved invalid request. Got: "
+            << "transactionalId=" << kafkaRequest->TransactionalId->c_str() 
+            << ", producerId=" << kafkaRequest->ProducerId 
+            << ", producerEpoch=" << kafkaRequest->ProducerEpoch
+        );
+        std::shared_ptr<ErrorResponseType> response = NKafkaTransactions::BuildResponse<ErrorResponseType>(kafkaRequest, EKafkaErrors::UNKNOWN_SERVER_ERROR);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, EKafkaErrors::UNKNOWN_SERVER_ERROR));
+    }
+
+    template<class ErrorResponseType, class EventType>
+    void TKafkaTransactionActor::SendFailResponse(TAutoPtr<TEventHandle<EventType>>& evHandle, EKafkaErrors errorCode, const TString& errorMessage) {
+        if (errorMessage) {
+            KAFKA_LOG_W(TStringBuilder() << "Sending fail response with error code: " << errorCode << ". Reason:  " << errorMessage);
+        } else {
+            KAFKA_LOG_W(TStringBuilder() << "Sending fail response with error code: " << errorCode);
+        }
+
+        std::shared_ptr<ErrorResponseType> response = NKafkaTransactions::BuildResponse<ErrorResponseType>(evHandle->Get()->Request, errorCode);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, errorCode));
+    }
+
+    template<class ResponseType, class EventType>
+    void TKafkaTransactionActor::SendOkResponse(TAutoPtr<TEventHandle<EventType>>& evHandle) {
+        auto kafkaRequest = evHandle->Get()->Request;
+        KAFKA_LOG_D(TStringBuilder() << "Sending OK response to " << evHandle->Get()->ConnectionId << " with correlationId " << evHandle->Get()->CorrelationId << " and transactionalId " << TransactionalId);
+        std::shared_ptr<ResponseType> response = NKafkaTransactions::BuildResponse<ResponseType>(kafkaRequest, EKafkaErrors::NONE_ERROR);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, EKafkaErrors::NONE_ERROR));
+    }
+
+    // helper methods
+    void TKafkaTransactionActor::Die(const TActorContext &ctx) {
+        KAFKA_LOG_D("Dying.");
+        if (Kqp) {
+            Kqp->CloseKqpSession(ctx);
+        }
+        Send(TxnCoordinatorActorId, new TEvKafka::TEvTransactionActorDied(TransactionalId, ProducerId, ProducerEpoch));
+        TBase::Die(ctx);
+    }
+
+    template<class EventType>
+    bool TKafkaTransactionActor::ProducerInRequestIsValid(TMessagePtr<EventType> kafkaRequest) {
+        return kafkaRequest->TransactionalId->c_str() == TransactionalId 
+            && kafkaRequest->ProducerId == ProducerId 
+            && kafkaRequest->ProducerEpoch == ProducerEpoch;
+    }
+
+    TString TKafkaTransactionActor::GetFullTopicPath(const TString& topicName) {
+        return NPersQueue::GetFullTopicPath(DatabasePath, topicName);
+    }
+
+    TString TKafkaTransactionActor::GetYqlWithTablesNames(const TString& templateStr) {
+        TString templateWithProducerStateTable = std::regex_replace(
+            templateStr.c_str(),
+            std::regex("<producer_state_table_name>"), 
+            NKikimr::NGRpcProxy::V1::TTransactionalProducersInitManager::GetInstant()->GetStorageTablePath().c_str()
+        );
+        TString templateWithConsumerStateTable = std::regex_replace(
+            templateWithProducerStateTable.c_str(),
+            std::regex("<consumer_state_table_name>"), 
+            NKikimr::NGRpcProxy::V1::TKafkaConsumerGroupsMetaInitManager::GetInstant()->GetStorageTablePath().c_str()
+        );
+
+        return templateWithConsumerStateTable;
+    }
+
+    NYdb::TParams TKafkaTransactionActor::BuildSelectParams() {
+        NYdb::TParamsBuilder params;
+        params.AddParam("$Database").Utf8(DatabasePath).Build();
+        params.AddParam("$TransactionalId").Utf8(TransactionalId).Build();
+        
+        // select unique consumer group names
+        std::unordered_set<TString> uniqueConsumerGroups;
+        for (auto& [partition, commitDetails] : OffsetsToCommit) {
+            uniqueConsumerGroups.emplace(commitDetails.ConsumerName);
+        }
+
+        // add unique consumer group names to request as params
+        auto& consumerGroupsParamBuilder = params.AddParam("$ConsumerGroups").BeginList();
+        for (auto& consumerGroupName : uniqueConsumerGroups) {
+            consumerGroupsParamBuilder.AddListItem().Utf8(consumerGroupName);
+        }
+        consumerGroupsParamBuilder.EndList().Build();
+
+        return params.Build();
+    }
+
+    THolder<NKikimr::NKqp::TEvKqp::TEvQueryRequest> TKafkaTransactionActor::BuildCommitTxnRequestToKqp(const TString& kqpTransactionId) {
+        auto ev = MakeHolder<TEvKqp::TEvQueryRequest>();
+
+        ev->Record.MutableRequest()->SetType(NKikimrKqp::QUERY_TYPE_UNDEFINED);
+        ev->Record.MutableRequest()->SetAction(NKikimrKqp::QUERY_ACTION_TOPIC);
+        ev->Record.MutableRequest()->SetDatabase(DatabasePath);
+        ev->Record.MutableRequest()->SetSessionId(KqpSessionId);
+
+        ev->Record.MutableRequest()->MutableTxControl()->set_commit_tx(true);
+        ev->Record.MutableRequest()->MutableTxControl()->set_tx_id(kqpTransactionId);
+
+        ev->Record.MutableRequest()->SetUsePublicResponseDataFormat(true);
+
+        auto* kafkaApiOperations = ev->Record.MutableRequest()->MutableKafkaApiOperations();
+        kafkaApiOperations->set_transactionalid(TransactionalId);
+        kafkaApiOperations->set_producerid(ProducerId);
+        kafkaApiOperations->set_producerepoch(ProducerEpoch);
+
+        for (auto& partition : PartitionsInTxn) {
+            auto* partitionInRequest = kafkaApiOperations->AddPartitionsInTxn();
+            partitionInRequest->set_topicpath(partition.TopicPath);
+            partitionInRequest->set_partitionid(partition.PartitionId);
+        }
+
+        for (auto& [partition, offsetDetails] : OffsetsToCommit) {
+            auto* offsetInRequest = kafkaApiOperations->AddOffsetsInTxn();
+            offsetInRequest->set_topicpath(partition.TopicPath);
+            offsetInRequest->set_partitionid(partition.PartitionId);
+            offsetInRequest->set_consumername(offsetDetails.ConsumerName);
+            offsetInRequest->set_consumergeneration(offsetDetails.ConsumerGeneration);
+            offsetInRequest->set_offset(offsetDetails.Offset);
+        }
+        
+        return ev;
+    }
+
+    void TKafkaTransactionActor::HandleSelectResponse(const NKqp::TEvKqp::TEvQueryResponse& response, const TActorContext& ctx) {
+        // YDB should return exactly two result sets for two queries: to producer and consumer state tables
+        if (response.Record.GetResponse().GetYdbResults().size() != 2) {
+            TString error = TStringBuilder() << "KQP returned wrong number of result sets on SELECT query. Expected 2, got " << response.Record.GetResponse().GetYdbResults().size() << ".";
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error);
+            Die(ctx);
+            return;
+        }
+
+        // parse and validate producer state
+        TMaybe<TProducerState> producerState;
+        try {
+            producerState = ParseProducerState(response);
+        } catch (const yexception& y) {
+            TString error = TStringBuilder() << "Error parsing producer state response from KQP. Reason: " << y.what();
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error);
+            Die(ctx);
+            return;
+        }
+        if (auto error = GetErrorInProducerState(producerState)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::PRODUCER_FENCED, error->data());
+            Die(ctx);
+            return;
+        }
+
+        // parse and validate consumers
+        std::unordered_map<TString, i32> consumerGenerationsByName = ParseConsumersGenerations(response);
+        if (auto error = GetErrorInConsumersStates(consumerGenerationsByName)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::PRODUCER_FENCED, error->data());
+            Die(ctx);
+            return;
+        }
+
+        KAFKA_LOG_D(TStringBuilder() << "Validated producer and consumers states. Everything is alright, sending commit");
+        auto kqpTxnId = response.Record.GetResponse().GetTxMeta().id();
+        // finally everything is valid and we can attempt to commit
+        SendCommitTxnRequest(kqpTxnId);
+    }
+
+    void TKafkaTransactionActor::HandleCommitResponse(const TActorContext& ctx) {
+        KAFKA_LOG_D("Successfully committed transaction. Sending ok and dying.");
+        SendOkResponse<TEndTxnResponseData>(EndTxnRequestPtr);
+        Die(ctx);
+    }
+
+    TMaybe<TString> TKafkaTransactionActor::GetErrorFromYdbResponse(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev) {
+        TStringBuilder builder = TStringBuilder() << "Recieved error on request to KQP. Last sent request: " << GetAsStr(LastSentToKqpRequest) << ". Reason: ";
+        if (ev->Cookie != KqpCookie) {
+            return builder << "Unexpected cookie in TEvQueryResponse. Expected KQP Cookie: " << KqpCookie << ", Actual: " << ev->Cookie << ".";
+        } else if (ev->Get()->Record.GetYdbStatus() != Ydb::StatusIds::SUCCESS) {
+            return builder << "Unexpected YDB status in TEvQueryResponse. Expected YDB SUCCESS status, Actual: " << ev->Get()->Record.GetYdbStatus() << ".";
+        } else {
+            return {};
+        }
+    }
+
+    TMaybe<TProducerState> TKafkaTransactionActor::ParseProducerState(const NKqp::TEvKqp::TEvQueryResponse& response) {
+        auto& resp = response.Record.GetResponse();
+
+        NYdb::TResultSetParser parser(resp.GetYdbResults(NKafkaTransactionSql::PRODUCER_STATE_REQUEST_INDEX));
+
+        // for this transactional id there is no rows
+        if (parser.RowsCount() == 0) {
+            return {};
+        } 
+        // there are multiple rows for this transactional id. This is unexpected and should not happen
+        else if (parser.RowsCount() > 1) {
+            throw yexception() << "Request returned more than one row: " << resp.GetYdbResults().size();
+        } else {
+            parser.TryNextRow();
+
+            TProducerState result; 
+
+            result.TransactionalId = parser.ColumnParser("transactional_id").GetUtf8();
+            result.ProducerId = parser.ColumnParser("producer_id").GetInt64();
+            result.ProducerEpoch = parser.ColumnParser("producer_epoch").GetInt16();
+
+            return result;
+        }
+    }
+
+    TMaybe<TString> TKafkaTransactionActor::GetErrorInProducerState(const TMaybe<TProducerState>& producerState) {
+        if (!producerState) {
+            return "No producer state found. May be it has expired";
+        } else if (producerState->TransactionalId != TransactionalId || producerState->ProducerId != ProducerId || producerState->ProducerEpoch != ProducerEpoch) {
+            return TStringBuilder() << "Producer state in producers_state table is different from the one in this actor. Producer state in table: "
+            << "transactionalId=" << producerState->TransactionalId 
+            << ", producerId=" << producerState->ProducerId
+            << ", producerEpoch=" << producerState->ProducerEpoch;
+        } else {
+            return {};
+        }
+    }
+
+        /**
+        * Parses the response to extract consumer group generations.
+        *
+        * @param response The response object containing the result set from the YDB query.
+        * @return A map where keys are consumer group names and values are their corresponding generations.
+        */
+    std::unordered_map<TString, i32> TKafkaTransactionActor::ParseConsumersGenerations(const NKqp::TEvKqp::TEvQueryResponse& response) {
+        std::unordered_map<TString, i32> generationByConsumerName;
+    
+        NYdb::TResultSetParser parser(response.Record.GetResponse().GetYdbResults(NKafkaTransactionSql::CONSUMER_STATES_REQUEST_INDEX));
+        while (parser.TryNextRow()) {
+            TString consumerName = parser.ColumnParser("consumer_group").GetUtf8().c_str();;
+            i32 generation = (i32)parser.ColumnParser("generation").GetUint64();
path: ydb/core/kafka_proxy/actors/kafka_transaction_actor.cpp
user: login: Alek5andr-Kotov
body: Лучше `IntegerCast` из `util/generic/cast.h`
created_at: 2025-04-21T07:38:19Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,448 @@
+#include "kafka_transaction_actor.h"
+#include "kafka_transaction_actor_sql.cpp"
+#include "txn_actor_response_builder.h"
+#include <ydb/core/kafka_proxy/kafka_transactions_coordinator.h>
+#include <ydb/core/kafka_proxy/kafka_transactional_producers_initializers.h>
+#include <ydb/core/kafka_proxy/kafka_consumer_groups_metadata_initializers.h>
+#include <ydb/core/kqp/common/simple/services.h>
+#include <regex>
+
+namespace NKafka {
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddPartitionsToTxnRequest::TPtr& ev, const TActorContext& ctx){
+        KAFKA_LOG_D("Receieved ADD_PARTITIONS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddPartitionsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                PartitionsInTxn.emplace(GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest));
+            }
+        }
+        SendOkResponse<TAddPartitionsToTxnResponseData>(ev);
+    }
+
+    // Method does nothing. In Kafka it will add __consumer_offsets topic to transaction, but 
+    // in YDB Topics we store offsets in table and do not need this extra action.
+    // Thus we can just ignore this request.
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddOffsetsToTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved ADD_OFFSETS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddOffsetsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        SendOkResponse<TAddOffsetsToTxnResponseData>(ev);
+    }
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvTxnOffsetCommitRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved TXN_OFFSET_COMMIT request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TTxnOffsetCommitResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        // save offsets for future use
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                TTopicPartition topicPartition = {GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest.PartitionIndex)};
+                TPartitionCommit newCommit{
+                    .Partition = partitionInRequest.PartitionIndex,
+                    .Offset = partitionInRequest.CommittedOffset,
+                    .ConsumerName = ev->Get()->Request->GroupId->c_str(),
+                    .ConsumerGeneration = ev->Get()->Request->GenerationId
+                };
+                auto it = OffsetsToCommit.find(topicPartition);
+                if (it == OffsetsToCommit.end()) {
+                    OffsetsToCommit.emplace(std::make_pair(topicPartition, newCommit));
+                } else {
+                    it->second = newCommit;
+                }
+            }
+        }
+        SendOkResponse<TTxnOffsetCommitResponseData>(ev);
+    }
+
+    /* 
+    If abort, will send back ok.
+    If commit, will do following sequence of calls and event handlers:
+    1. Open KQP Session
+    2. Handle(NKqp::TEvKqp::TEvCreateSessionResponse) - KQP session created
+    3. Send Select request for current producer and consumers state, begin transaction
+    4. Handle(NKqp::TEvKqp::TEvQueryResponse): Validate returned producer and consumers state. Return PRODUCER_FENCED if check failed
+    5. Send Commit request with properly created KafkaApiOperations
+    6. Handle(NKqp::TEvKqp::TEvQueryResponse): If everything committed successfully, return OK to the client
+    7. Close KQP session, send to coordinator TEvTransactionActorDied, die.
+    */
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvEndTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved END_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TEndTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+
+        bool txnAborted = !ev->Get()->Request->Committed;
+        if (CommitStarted) {
+            return; // we just ignore second and subsequent requests
+        } else if (txnAborted) {
+            SendOkResponse<TEndTxnResponseData>(ev);
+        } else {
+            CommitStarted = true;
+            EndTxnRequestPtr = std::move(ev);
+            StartKqpSession(ctx);
+        }
+    }
+
+    void TKafkaTransactionActor::Handle(TEvents::TEvPoison::TPtr&, const TActorContext& ctx) {
+        Die(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvCreateSessionResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "KQP session created");
+        if (!Kqp->HandleCreateSessionResponse(ev, ctx)) {
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, "Failed to create KQP session");
+            Die(ctx);
+            return;
+        }
+
+        SendToKqpValidationRequests(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Receieved query response from KQP for " << GetAsStr(LastSentToKqpRequest) << " request");
+        if (auto error = GetErrorFromYdbResponse(ev)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error->data());
+            Die(ctx);
+            return;
+        }
+
+        switch (LastSentToKqpRequest) {
+            case EKafkaTxnKqpRequests::SELECT:
+                HandleSelectResponse(*ev->Get(), ctx);
+                break;
+            case EKafkaTxnKqpRequests::COMMIT:
+                HandleCommitResponse(ctx);
+                break;
+            default:
+                Y_FAIL("Unexpected KQP request");
+        }
+    }
+
+    void TKafkaTransactionActor::StartKqpSession(const TActorContext& ctx) {
+        Kqp = std::make_unique<TKqpTxHelper>(DatabasePath);
+        KAFKA_LOG_D(TStringBuilder() << "Sending create session request to KQP for database " << DatabasePath);
+        Kqp->SendCreateSessionRequest(ctx, KqpActorId);
+    }
+
+    void TKafkaTransactionActor::SendToKqpValidationRequests(const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Sending select request to KQP for database " << DatabasePath);
+        Kqp->SendYqlRequest(
+            GetYqlWithTablesNames(NKafkaTransactionSql::SELECT_FOR_VALIDATION), 
+            BuildSelectParams(), 
+            ++KqpCookie, 
+            ctx,
+            false,
+            KqpActorId
+        );
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::SELECT;
+    }
+
+    void TKafkaTransactionActor::SendCommitTxnRequest(const TString& kqpTransactionId) {
+        auto request = BuildCommitTxnRequestToKqp(kqpTransactionId);
+
+        Send(KqpActorId, request.Release(), 0, ++KqpCookie);
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::COMMIT;
+    }
+    
+    // Response senders
+    template<class ErrorResponseType, class EventType>
+    void TKafkaTransactionActor::SendInvalidTransactionActorStateResponse(TAutoPtr<TEventHandle<EventType>>& evHandle) {
+        auto kafkaRequest = evHandle->Get()->Request;
+        KAFKA_LOG_CRIT(TStringBuilder() << "Recieved invalid request. Got: "
+            << "transactionalId=" << kafkaRequest->TransactionalId->c_str() 
+            << ", producerId=" << kafkaRequest->ProducerId 
+            << ", producerEpoch=" << kafkaRequest->ProducerEpoch
+        );
+        std::shared_ptr<ErrorResponseType> response = NKafkaTransactions::BuildResponse<ErrorResponseType>(kafkaRequest, EKafkaErrors::UNKNOWN_SERVER_ERROR);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, EKafkaErrors::UNKNOWN_SERVER_ERROR));
+    }
+
+    template<class ErrorResponseType, class EventType>
+    void TKafkaTransactionActor::SendFailResponse(TAutoPtr<TEventHandle<EventType>>& evHandle, EKafkaErrors errorCode, const TString& errorMessage) {
+        if (errorMessage) {
+            KAFKA_LOG_W(TStringBuilder() << "Sending fail response with error code: " << errorCode << ". Reason:  " << errorMessage);
+        } else {
+            KAFKA_LOG_W(TStringBuilder() << "Sending fail response with error code: " << errorCode);
+        }
+
+        std::shared_ptr<ErrorResponseType> response = NKafkaTransactions::BuildResponse<ErrorResponseType>(evHandle->Get()->Request, errorCode);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, errorCode));
+    }
+
+    template<class ResponseType, class EventType>
+    void TKafkaTransactionActor::SendOkResponse(TAutoPtr<TEventHandle<EventType>>& evHandle) {
+        auto kafkaRequest = evHandle->Get()->Request;
+        KAFKA_LOG_D(TStringBuilder() << "Sending OK response to " << evHandle->Get()->ConnectionId << " with correlationId " << evHandle->Get()->CorrelationId << " and transactionalId " << TransactionalId);
+        std::shared_ptr<ResponseType> response = NKafkaTransactions::BuildResponse<ResponseType>(kafkaRequest, EKafkaErrors::NONE_ERROR);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, EKafkaErrors::NONE_ERROR));
+    }
+
+    // helper methods
+    void TKafkaTransactionActor::Die(const TActorContext &ctx) {
+        KAFKA_LOG_D("Dying.");
+        if (Kqp) {
+            Kqp->CloseKqpSession(ctx);
+        }
+        Send(TxnCoordinatorActorId, new TEvKafka::TEvTransactionActorDied(TransactionalId, ProducerId, ProducerEpoch));
+        TBase::Die(ctx);
+    }
+
+    template<class EventType>
+    bool TKafkaTransactionActor::ProducerInRequestIsValid(TMessagePtr<EventType> kafkaRequest) {
+        return kafkaRequest->TransactionalId->c_str() == TransactionalId 
+            && kafkaRequest->ProducerId == ProducerId 
+            && kafkaRequest->ProducerEpoch == ProducerEpoch;
+    }
+
+    TString TKafkaTransactionActor::GetFullTopicPath(const TString& topicName) {
+        return NPersQueue::GetFullTopicPath(DatabasePath, topicName);
+    }
+
+    TString TKafkaTransactionActor::GetYqlWithTablesNames(const TString& templateStr) {
+        TString templateWithProducerStateTable = std::regex_replace(
+            templateStr.c_str(),
+            std::regex("<producer_state_table_name>"), 
+            NKikimr::NGRpcProxy::V1::TTransactionalProducersInitManager::GetInstant()->GetStorageTablePath().c_str()
+        );
+        TString templateWithConsumerStateTable = std::regex_replace(
+            templateWithProducerStateTable.c_str(),
+            std::regex("<consumer_state_table_name>"), 
+            NKikimr::NGRpcProxy::V1::TKafkaConsumerGroupsMetaInitManager::GetInstant()->GetStorageTablePath().c_str()
+        );
+
+        return templateWithConsumerStateTable;
+    }
+
+    NYdb::TParams TKafkaTransactionActor::BuildSelectParams() {
+        NYdb::TParamsBuilder params;
+        params.AddParam("$Database").Utf8(DatabasePath).Build();
+        params.AddParam("$TransactionalId").Utf8(TransactionalId).Build();
+        
+        // select unique consumer group names
+        std::unordered_set<TString> uniqueConsumerGroups;
+        for (auto& [partition, commitDetails] : OffsetsToCommit) {
+            uniqueConsumerGroups.emplace(commitDetails.ConsumerName);
+        }
+
+        // add unique consumer group names to request as params
+        auto& consumerGroupsParamBuilder = params.AddParam("$ConsumerGroups").BeginList();
+        for (auto& consumerGroupName : uniqueConsumerGroups) {
+            consumerGroupsParamBuilder.AddListItem().Utf8(consumerGroupName);
+        }
+        consumerGroupsParamBuilder.EndList().Build();
+
+        return params.Build();
+    }
+
+    THolder<NKikimr::NKqp::TEvKqp::TEvQueryRequest> TKafkaTransactionActor::BuildCommitTxnRequestToKqp(const TString& kqpTransactionId) {
+        auto ev = MakeHolder<TEvKqp::TEvQueryRequest>();
+
+        ev->Record.MutableRequest()->SetType(NKikimrKqp::QUERY_TYPE_UNDEFINED);
+        ev->Record.MutableRequest()->SetAction(NKikimrKqp::QUERY_ACTION_TOPIC);
+        ev->Record.MutableRequest()->SetDatabase(DatabasePath);
+        ev->Record.MutableRequest()->SetSessionId(KqpSessionId);
+
+        ev->Record.MutableRequest()->MutableTxControl()->set_commit_tx(true);
+        ev->Record.MutableRequest()->MutableTxControl()->set_tx_id(kqpTransactionId);
+
+        ev->Record.MutableRequest()->SetUsePublicResponseDataFormat(true);
+
+        auto* kafkaApiOperations = ev->Record.MutableRequest()->MutableKafkaApiOperations();
+        kafkaApiOperations->set_transactionalid(TransactionalId);
+        kafkaApiOperations->set_producerid(ProducerId);
+        kafkaApiOperations->set_producerepoch(ProducerEpoch);
+
+        for (auto& partition : PartitionsInTxn) {
+            auto* partitionInRequest = kafkaApiOperations->AddPartitionsInTxn();
+            partitionInRequest->set_topicpath(partition.TopicPath);
+            partitionInRequest->set_partitionid(partition.PartitionId);
+        }
+
+        for (auto& [partition, offsetDetails] : OffsetsToCommit) {
+            auto* offsetInRequest = kafkaApiOperations->AddOffsetsInTxn();
+            offsetInRequest->set_topicpath(partition.TopicPath);
+            offsetInRequest->set_partitionid(partition.PartitionId);
+            offsetInRequest->set_consumername(offsetDetails.ConsumerName);
+            offsetInRequest->set_consumergeneration(offsetDetails.ConsumerGeneration);
+            offsetInRequest->set_offset(offsetDetails.Offset);
+        }
+        
+        return ev;
+    }
+
+    void TKafkaTransactionActor::HandleSelectResponse(const NKqp::TEvKqp::TEvQueryResponse& response, const TActorContext& ctx) {
+        // YDB should return exactly two result sets for two queries: to producer and consumer state tables
+        if (response.Record.GetResponse().GetYdbResults().size() != 2) {
+            TString error = TStringBuilder() << "KQP returned wrong number of result sets on SELECT query. Expected 2, got " << response.Record.GetResponse().GetYdbResults().size() << ".";
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error);
+            Die(ctx);
+            return;
+        }
+
+        // parse and validate producer state
+        TMaybe<TProducerState> producerState;
+        try {
+            producerState = ParseProducerState(response);
+        } catch (const yexception& y) {
+            TString error = TStringBuilder() << "Error parsing producer state response from KQP. Reason: " << y.what();
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error);
+            Die(ctx);
+            return;
+        }
+        if (auto error = GetErrorInProducerState(producerState)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::PRODUCER_FENCED, error->data());
+            Die(ctx);
+            return;
+        }
+
+        // parse and validate consumers
+        std::unordered_map<TString, i32> consumerGenerationsByName = ParseConsumersGenerations(response);
+        if (auto error = GetErrorInConsumersStates(consumerGenerationsByName)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::PRODUCER_FENCED, error->data());
+            Die(ctx);
+            return;
+        }
+
+        KAFKA_LOG_D(TStringBuilder() << "Validated producer and consumers states. Everything is alright, sending commit");
+        auto kqpTxnId = response.Record.GetResponse().GetTxMeta().id();
+        // finally everything is valid and we can attempt to commit
+        SendCommitTxnRequest(kqpTxnId);
+    }
+
+    void TKafkaTransactionActor::HandleCommitResponse(const TActorContext& ctx) {
+        KAFKA_LOG_D("Successfully committed transaction. Sending ok and dying.");
+        SendOkResponse<TEndTxnResponseData>(EndTxnRequestPtr);
+        Die(ctx);
+    }
+
+    TMaybe<TString> TKafkaTransactionActor::GetErrorFromYdbResponse(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev) {
+        TStringBuilder builder = TStringBuilder() << "Recieved error on request to KQP. Last sent request: " << GetAsStr(LastSentToKqpRequest) << ". Reason: ";
+        if (ev->Cookie != KqpCookie) {
+            return builder << "Unexpected cookie in TEvQueryResponse. Expected KQP Cookie: " << KqpCookie << ", Actual: " << ev->Cookie << ".";
+        } else if (ev->Get()->Record.GetYdbStatus() != Ydb::StatusIds::SUCCESS) {
+            return builder << "Unexpected YDB status in TEvQueryResponse. Expected YDB SUCCESS status, Actual: " << ev->Get()->Record.GetYdbStatus() << ".";
+        } else {
+            return {};
+        }
+    }
+
+    TMaybe<TProducerState> TKafkaTransactionActor::ParseProducerState(const NKqp::TEvKqp::TEvQueryResponse& response) {
+        auto& resp = response.Record.GetResponse();
+
+        NYdb::TResultSetParser parser(resp.GetYdbResults(NKafkaTransactionSql::PRODUCER_STATE_REQUEST_INDEX));
+
+        // for this transactional id there is no rows
+        if (parser.RowsCount() == 0) {
+            return {};
+        } 
+        // there are multiple rows for this transactional id. This is unexpected and should not happen
+        else if (parser.RowsCount() > 1) {
+            throw yexception() << "Request returned more than one row: " << resp.GetYdbResults().size();
+        } else {
+            parser.TryNextRow();
+
+            TProducerState result; 
+
+            result.TransactionalId = parser.ColumnParser("transactional_id").GetUtf8();
+            result.ProducerId = parser.ColumnParser("producer_id").GetInt64();
+            result.ProducerEpoch = parser.ColumnParser("producer_epoch").GetInt16();
+
+            return result;
+        }
+    }
+
+    TMaybe<TString> TKafkaTransactionActor::GetErrorInProducerState(const TMaybe<TProducerState>& producerState) {
+        if (!producerState) {
+            return "No producer state found. May be it has expired";
+        } else if (producerState->TransactionalId != TransactionalId || producerState->ProducerId != ProducerId || producerState->ProducerEpoch != ProducerEpoch) {
+            return TStringBuilder() << "Producer state in producers_state table is different from the one in this actor. Producer state in table: "
+            << "transactionalId=" << producerState->TransactionalId 
+            << ", producerId=" << producerState->ProducerId
+            << ", producerEpoch=" << producerState->ProducerEpoch;
+        } else {
+            return {};
+        }
+    }
+
+        /**
+        * Parses the response to extract consumer group generations.
+        *
+        * @param response The response object containing the result set from the YDB query.
+        * @return A map where keys are consumer group names and values are their corresponding generations.
+        */
+    std::unordered_map<TString, i32> TKafkaTransactionActor::ParseConsumersGenerations(const NKqp::TEvKqp::TEvQueryResponse& response) {
+        std::unordered_map<TString, i32> generationByConsumerName;
+    
+        NYdb::TResultSetParser parser(response.Record.GetResponse().GetYdbResults(NKafkaTransactionSql::CONSUMER_STATES_REQUEST_INDEX));
+        while (parser.TryNextRow()) {
+            TString consumerName = parser.ColumnParser("consumer_group").GetUtf8().c_str();;
path: ydb/core/kafka_proxy/actors/kafka_transaction_actor.cpp
user: login: Alek5andr-Kotov
body: Лишняя точка с запятой :)
created_at: 2025-04-21T07:38:51Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,448 @@
+#include "kafka_transaction_actor.h"
+#include "kafka_transaction_actor_sql.cpp"
+#include "txn_actor_response_builder.h"
+#include <ydb/core/kafka_proxy/kafka_transactions_coordinator.h>
+#include <ydb/core/kafka_proxy/kafka_transactional_producers_initializers.h>
+#include <ydb/core/kafka_proxy/kafka_consumer_groups_metadata_initializers.h>
+#include <ydb/core/kqp/common/simple/services.h>
+#include <regex>
+
+namespace NKafka {
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddPartitionsToTxnRequest::TPtr& ev, const TActorContext& ctx){
+        KAFKA_LOG_D("Receieved ADD_PARTITIONS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddPartitionsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                PartitionsInTxn.emplace(GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest));
+            }
+        }
+        SendOkResponse<TAddPartitionsToTxnResponseData>(ev);
+    }
+
+    // Method does nothing. In Kafka it will add __consumer_offsets topic to transaction, but 
+    // in YDB Topics we store offsets in table and do not need this extra action.
+    // Thus we can just ignore this request.
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddOffsetsToTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved ADD_OFFSETS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddOffsetsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        SendOkResponse<TAddOffsetsToTxnResponseData>(ev);
+    }
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvTxnOffsetCommitRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved TXN_OFFSET_COMMIT request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TTxnOffsetCommitResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        // save offsets for future use
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                TTopicPartition topicPartition = {GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest.PartitionIndex)};
+                TPartitionCommit newCommit{
+                    .Partition = partitionInRequest.PartitionIndex,
+                    .Offset = partitionInRequest.CommittedOffset,
+                    .ConsumerName = ev->Get()->Request->GroupId->c_str(),
+                    .ConsumerGeneration = ev->Get()->Request->GenerationId
+                };
+                auto it = OffsetsToCommit.find(topicPartition);
+                if (it == OffsetsToCommit.end()) {
+                    OffsetsToCommit.emplace(std::make_pair(topicPartition, newCommit));
+                } else {
+                    it->second = newCommit;
+                }
+            }
+        }
+        SendOkResponse<TTxnOffsetCommitResponseData>(ev);
+    }
+
+    /* 
+    If abort, will send back ok.
+    If commit, will do following sequence of calls and event handlers:
+    1. Open KQP Session
+    2. Handle(NKqp::TEvKqp::TEvCreateSessionResponse) - KQP session created
+    3. Send Select request for current producer and consumers state, begin transaction
+    4. Handle(NKqp::TEvKqp::TEvQueryResponse): Validate returned producer and consumers state. Return PRODUCER_FENCED if check failed
+    5. Send Commit request with properly created KafkaApiOperations
+    6. Handle(NKqp::TEvKqp::TEvQueryResponse): If everything committed successfully, return OK to the client
+    7. Close KQP session, send to coordinator TEvTransactionActorDied, die.
+    */
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvEndTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved END_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TEndTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+
+        bool txnAborted = !ev->Get()->Request->Committed;
+        if (CommitStarted) {
+            return; // we just ignore second and subsequent requests
+        } else if (txnAborted) {
+            SendOkResponse<TEndTxnResponseData>(ev);
+        } else {
+            CommitStarted = true;
+            EndTxnRequestPtr = std::move(ev);
+            StartKqpSession(ctx);
+        }
+    }
+
+    void TKafkaTransactionActor::Handle(TEvents::TEvPoison::TPtr&, const TActorContext& ctx) {
+        Die(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvCreateSessionResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "KQP session created");
+        if (!Kqp->HandleCreateSessionResponse(ev, ctx)) {
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, "Failed to create KQP session");
+            Die(ctx);
+            return;
+        }
+
+        SendToKqpValidationRequests(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Receieved query response from KQP for " << GetAsStr(LastSentToKqpRequest) << " request");
+        if (auto error = GetErrorFromYdbResponse(ev)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error->data());
+            Die(ctx);
+            return;
+        }
+
+        switch (LastSentToKqpRequest) {
+            case EKafkaTxnKqpRequests::SELECT:
+                HandleSelectResponse(*ev->Get(), ctx);
+                break;
+            case EKafkaTxnKqpRequests::COMMIT:
+                HandleCommitResponse(ctx);
+                break;
+            default:
+                Y_FAIL("Unexpected KQP request");
+        }
+    }
+
+    void TKafkaTransactionActor::StartKqpSession(const TActorContext& ctx) {
+        Kqp = std::make_unique<TKqpTxHelper>(DatabasePath);
+        KAFKA_LOG_D(TStringBuilder() << "Sending create session request to KQP for database " << DatabasePath);
+        Kqp->SendCreateSessionRequest(ctx, KqpActorId);
+    }
+
+    void TKafkaTransactionActor::SendToKqpValidationRequests(const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Sending select request to KQP for database " << DatabasePath);
+        Kqp->SendYqlRequest(
+            GetYqlWithTablesNames(NKafkaTransactionSql::SELECT_FOR_VALIDATION), 
+            BuildSelectParams(), 
+            ++KqpCookie, 
+            ctx,
+            false,
+            KqpActorId
+        );
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::SELECT;
+    }
+
+    void TKafkaTransactionActor::SendCommitTxnRequest(const TString& kqpTransactionId) {
+        auto request = BuildCommitTxnRequestToKqp(kqpTransactionId);
+
+        Send(KqpActorId, request.Release(), 0, ++KqpCookie);
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::COMMIT;
+    }
+    
+    // Response senders
+    template<class ErrorResponseType, class EventType>
+    void TKafkaTransactionActor::SendInvalidTransactionActorStateResponse(TAutoPtr<TEventHandle<EventType>>& evHandle) {
+        auto kafkaRequest = evHandle->Get()->Request;
+        KAFKA_LOG_CRIT(TStringBuilder() << "Recieved invalid request. Got: "
+            << "transactionalId=" << kafkaRequest->TransactionalId->c_str() 
+            << ", producerId=" << kafkaRequest->ProducerId 
+            << ", producerEpoch=" << kafkaRequest->ProducerEpoch
+        );
+        std::shared_ptr<ErrorResponseType> response = NKafkaTransactions::BuildResponse<ErrorResponseType>(kafkaRequest, EKafkaErrors::UNKNOWN_SERVER_ERROR);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, EKafkaErrors::UNKNOWN_SERVER_ERROR));
+    }
+
+    template<class ErrorResponseType, class EventType>
+    void TKafkaTransactionActor::SendFailResponse(TAutoPtr<TEventHandle<EventType>>& evHandle, EKafkaErrors errorCode, const TString& errorMessage) {
+        if (errorMessage) {
+            KAFKA_LOG_W(TStringBuilder() << "Sending fail response with error code: " << errorCode << ". Reason:  " << errorMessage);
+        } else {
+            KAFKA_LOG_W(TStringBuilder() << "Sending fail response with error code: " << errorCode);
+        }
+
+        std::shared_ptr<ErrorResponseType> response = NKafkaTransactions::BuildResponse<ErrorResponseType>(evHandle->Get()->Request, errorCode);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, errorCode));
+    }
+
+    template<class ResponseType, class EventType>
+    void TKafkaTransactionActor::SendOkResponse(TAutoPtr<TEventHandle<EventType>>& evHandle) {
+        auto kafkaRequest = evHandle->Get()->Request;
+        KAFKA_LOG_D(TStringBuilder() << "Sending OK response to " << evHandle->Get()->ConnectionId << " with correlationId " << evHandle->Get()->CorrelationId << " and transactionalId " << TransactionalId);
+        std::shared_ptr<ResponseType> response = NKafkaTransactions::BuildResponse<ResponseType>(kafkaRequest, EKafkaErrors::NONE_ERROR);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, EKafkaErrors::NONE_ERROR));
+    }
+
+    // helper methods
+    void TKafkaTransactionActor::Die(const TActorContext &ctx) {
+        KAFKA_LOG_D("Dying.");
+        if (Kqp) {
+            Kqp->CloseKqpSession(ctx);
+        }
+        Send(TxnCoordinatorActorId, new TEvKafka::TEvTransactionActorDied(TransactionalId, ProducerId, ProducerEpoch));
+        TBase::Die(ctx);
+    }
+
+    template<class EventType>
+    bool TKafkaTransactionActor::ProducerInRequestIsValid(TMessagePtr<EventType> kafkaRequest) {
+        return kafkaRequest->TransactionalId->c_str() == TransactionalId 
+            && kafkaRequest->ProducerId == ProducerId 
+            && kafkaRequest->ProducerEpoch == ProducerEpoch;
+    }
+
+    TString TKafkaTransactionActor::GetFullTopicPath(const TString& topicName) {
+        return NPersQueue::GetFullTopicPath(DatabasePath, topicName);
+    }
+
+    TString TKafkaTransactionActor::GetYqlWithTablesNames(const TString& templateStr) {
+        TString templateWithProducerStateTable = std::regex_replace(
+            templateStr.c_str(),
+            std::regex("<producer_state_table_name>"), 
+            NKikimr::NGRpcProxy::V1::TTransactionalProducersInitManager::GetInstant()->GetStorageTablePath().c_str()
+        );
+        TString templateWithConsumerStateTable = std::regex_replace(
+            templateWithProducerStateTable.c_str(),
+            std::regex("<consumer_state_table_name>"), 
+            NKikimr::NGRpcProxy::V1::TKafkaConsumerGroupsMetaInitManager::GetInstant()->GetStorageTablePath().c_str()
+        );
+
+        return templateWithConsumerStateTable;
+    }
+
+    NYdb::TParams TKafkaTransactionActor::BuildSelectParams() {
+        NYdb::TParamsBuilder params;
+        params.AddParam("$Database").Utf8(DatabasePath).Build();
+        params.AddParam("$TransactionalId").Utf8(TransactionalId).Build();
+        
+        // select unique consumer group names
+        std::unordered_set<TString> uniqueConsumerGroups;
+        for (auto& [partition, commitDetails] : OffsetsToCommit) {
+            uniqueConsumerGroups.emplace(commitDetails.ConsumerName);
+        }
+
+        // add unique consumer group names to request as params
+        auto& consumerGroupsParamBuilder = params.AddParam("$ConsumerGroups").BeginList();
+        for (auto& consumerGroupName : uniqueConsumerGroups) {
+            consumerGroupsParamBuilder.AddListItem().Utf8(consumerGroupName);
+        }
+        consumerGroupsParamBuilder.EndList().Build();
+
+        return params.Build();
+    }
+
+    THolder<NKikimr::NKqp::TEvKqp::TEvQueryRequest> TKafkaTransactionActor::BuildCommitTxnRequestToKqp(const TString& kqpTransactionId) {
+        auto ev = MakeHolder<TEvKqp::TEvQueryRequest>();
+
+        ev->Record.MutableRequest()->SetType(NKikimrKqp::QUERY_TYPE_UNDEFINED);
+        ev->Record.MutableRequest()->SetAction(NKikimrKqp::QUERY_ACTION_TOPIC);
+        ev->Record.MutableRequest()->SetDatabase(DatabasePath);
+        ev->Record.MutableRequest()->SetSessionId(KqpSessionId);
+
+        ev->Record.MutableRequest()->MutableTxControl()->set_commit_tx(true);
+        ev->Record.MutableRequest()->MutableTxControl()->set_tx_id(kqpTransactionId);
+
+        ev->Record.MutableRequest()->SetUsePublicResponseDataFormat(true);
+
+        auto* kafkaApiOperations = ev->Record.MutableRequest()->MutableKafkaApiOperations();
+        kafkaApiOperations->set_transactionalid(TransactionalId);
+        kafkaApiOperations->set_producerid(ProducerId);
+        kafkaApiOperations->set_producerepoch(ProducerEpoch);
+
+        for (auto& partition : PartitionsInTxn) {
+            auto* partitionInRequest = kafkaApiOperations->AddPartitionsInTxn();
+            partitionInRequest->set_topicpath(partition.TopicPath);
+            partitionInRequest->set_partitionid(partition.PartitionId);
+        }
+
+        for (auto& [partition, offsetDetails] : OffsetsToCommit) {
+            auto* offsetInRequest = kafkaApiOperations->AddOffsetsInTxn();
+            offsetInRequest->set_topicpath(partition.TopicPath);
+            offsetInRequest->set_partitionid(partition.PartitionId);
+            offsetInRequest->set_consumername(offsetDetails.ConsumerName);
+            offsetInRequest->set_consumergeneration(offsetDetails.ConsumerGeneration);
+            offsetInRequest->set_offset(offsetDetails.Offset);
+        }
+        
+        return ev;
+    }
+
+    void TKafkaTransactionActor::HandleSelectResponse(const NKqp::TEvKqp::TEvQueryResponse& response, const TActorContext& ctx) {
+        // YDB should return exactly two result sets for two queries: to producer and consumer state tables
+        if (response.Record.GetResponse().GetYdbResults().size() != 2) {
+            TString error = TStringBuilder() << "KQP returned wrong number of result sets on SELECT query. Expected 2, got " << response.Record.GetResponse().GetYdbResults().size() << ".";
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error);
+            Die(ctx);
+            return;
+        }
+
+        // parse and validate producer state
+        TMaybe<TProducerState> producerState;
+        try {
+            producerState = ParseProducerState(response);
+        } catch (const yexception& y) {
+            TString error = TStringBuilder() << "Error parsing producer state response from KQP. Reason: " << y.what();
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error);
+            Die(ctx);
+            return;
+        }
+        if (auto error = GetErrorInProducerState(producerState)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::PRODUCER_FENCED, error->data());
+            Die(ctx);
+            return;
+        }
+
+        // parse and validate consumers
+        std::unordered_map<TString, i32> consumerGenerationsByName = ParseConsumersGenerations(response);
+        if (auto error = GetErrorInConsumersStates(consumerGenerationsByName)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::PRODUCER_FENCED, error->data());
+            Die(ctx);
+            return;
+        }
+
+        KAFKA_LOG_D(TStringBuilder() << "Validated producer and consumers states. Everything is alright, sending commit");
+        auto kqpTxnId = response.Record.GetResponse().GetTxMeta().id();
+        // finally everything is valid and we can attempt to commit
+        SendCommitTxnRequest(kqpTxnId);
+    }
+
+    void TKafkaTransactionActor::HandleCommitResponse(const TActorContext& ctx) {
+        KAFKA_LOG_D("Successfully committed transaction. Sending ok and dying.");
+        SendOkResponse<TEndTxnResponseData>(EndTxnRequestPtr);
+        Die(ctx);
+    }
+
+    TMaybe<TString> TKafkaTransactionActor::GetErrorFromYdbResponse(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev) {
+        TStringBuilder builder = TStringBuilder() << "Recieved error on request to KQP. Last sent request: " << GetAsStr(LastSentToKqpRequest) << ". Reason: ";
+        if (ev->Cookie != KqpCookie) {
+            return builder << "Unexpected cookie in TEvQueryResponse. Expected KQP Cookie: " << KqpCookie << ", Actual: " << ev->Cookie << ".";
+        } else if (ev->Get()->Record.GetYdbStatus() != Ydb::StatusIds::SUCCESS) {
+            return builder << "Unexpected YDB status in TEvQueryResponse. Expected YDB SUCCESS status, Actual: " << ev->Get()->Record.GetYdbStatus() << ".";
+        } else {
+            return {};
+        }
+    }
+
+    TMaybe<TProducerState> TKafkaTransactionActor::ParseProducerState(const NKqp::TEvKqp::TEvQueryResponse& response) {
+        auto& resp = response.Record.GetResponse();
+
+        NYdb::TResultSetParser parser(resp.GetYdbResults(NKafkaTransactionSql::PRODUCER_STATE_REQUEST_INDEX));
+
+        // for this transactional id there is no rows
+        if (parser.RowsCount() == 0) {
+            return {};
+        } 
+        // there are multiple rows for this transactional id. This is unexpected and should not happen
+        else if (parser.RowsCount() > 1) {
+            throw yexception() << "Request returned more than one row: " << resp.GetYdbResults().size();
+        } else {
+            parser.TryNextRow();
+
+            TProducerState result; 
+
+            result.TransactionalId = parser.ColumnParser("transactional_id").GetUtf8();
+            result.ProducerId = parser.ColumnParser("producer_id").GetInt64();
+            result.ProducerEpoch = parser.ColumnParser("producer_epoch").GetInt16();
+
+            return result;
+        }
+    }
+
+    TMaybe<TString> TKafkaTransactionActor::GetErrorInProducerState(const TMaybe<TProducerState>& producerState) {
+        if (!producerState) {
+            return "No producer state found. May be it has expired";
+        } else if (producerState->TransactionalId != TransactionalId || producerState->ProducerId != ProducerId || producerState->ProducerEpoch != ProducerEpoch) {
+            return TStringBuilder() << "Producer state in producers_state table is different from the one in this actor. Producer state in table: "
+            << "transactionalId=" << producerState->TransactionalId 
+            << ", producerId=" << producerState->ProducerId
+            << ", producerEpoch=" << producerState->ProducerEpoch;
+        } else {
+            return {};
+        }
+    }
+
+        /**
+        * Parses the response to extract consumer group generations.
+        *
+        * @param response The response object containing the result set from the YDB query.
+        * @return A map where keys are consumer group names and values are their corresponding generations.
+        */
+    std::unordered_map<TString, i32> TKafkaTransactionActor::ParseConsumersGenerations(const NKqp::TEvKqp::TEvQueryResponse& response) {
+        std::unordered_map<TString, i32> generationByConsumerName;
path: ydb/core/kafka_proxy/actors/kafka_transaction_actor.cpp
user: login: Alek5andr-Kotov
body: В ответе точно не может быть повторяющихся значений?
created_at: 2025-04-21T07:39:46Z
author_association: COLLABORATOR
diff_hunk: @@ -1,32 +1,141 @@
 #pragma once
 
+#include "kafka_init_producer_id_actor.h"
+#include <ydb/core/kafka_proxy/kafka_events.h>
 #include <ydb/library/actors/core/actor_bootstrapped.h>
+#include <ydb/core/kafka_proxy/kqp_helper.h>
 
 namespace NKafka {
     /* 
     This class is responsible for one kafka transaction.
 
     It accumulates transaction state (partitions in tx, offsets) and on commit submits transaction to KQP
     */
-    class TKafkaTransactionActor : public NActors::TActorBootstrapped<TKafkaTransactionActor> {
+    class TKafkaTransactionActor : public NActors::TActor<TKafkaTransactionActor> {
 
-        using TBase = NActors::TActorBootstrapped<TKafkaTransactionActor>;
+        using TBase = NActors::TActor<TKafkaTransactionActor>;
         
         public:
-            void Bootstrap(const NActors::TActorContext&) {
-                TBase::Become(&TKafkaTransactionActor::StateWork);
-            }
+            struct TTopicPartition {
+                TString TopicPath;
+                ui32 PartitionId;
+
+                bool operator==(const TTopicPartition &other) const = default;
+            };
+
+            struct TopicPartitionHashFn {
path: ydb/core/kafka_proxy/actors/kafka_transaction_actor.h
user: login: Alek5andr-Kotov
body: А почему не сделать специализацию `std::hash`?
created_at: 2025-04-21T07:48:45Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,448 @@
+#include "kafka_transaction_actor.h"
+#include "kafka_transaction_actor_sql.cpp"
path: ydb/core/kafka_proxy/actors/kafka_transaction_actor.cpp
user: login: Alek5andr-Kotov
body: А я всё удивлялся как собирается программа если `SELECT_FOR_VALIDATION` объявлен статическим...
created_at: 2025-04-21T07:51:30Z
author_association: COLLABORATOR
diff_hunk: @@ -43,6 +46,23 @@ namespace NKafka {
         HandleTransactionalRequest<TEndTxnResponseData>(ev, ctx);
     };
 
+    void TKafkaTransactionsCoordinator::Handle(TEvKafka::TEvTransactionActorDied::TPtr& ev, const TActorContext&) {
+        auto it = ProducersByTransactionalId.find(ev->Get()->TransactionalId);
+        TProducerState currentProducerState = ProducersByTransactionalId[ev->Get()->TransactionalId];
+
+        if (it == ProducersByTransactionalId.end() || it->second != currentProducerState) {
path: ydb/core/kafka_proxy/kafka_transactions_coordinator.cpp
user: login: Alek5andr-Kotov
body: Я не понял этот код. В строке 51 создаётся объект для неизвестной транзакции `TransactionalId`
created_at: 2025-04-21T08:00:34Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,448 @@
+#include "kafka_transaction_actor.h"
+#include "kafka_transaction_actor_sql.cpp"
+#include "txn_actor_response_builder.h"
+#include <ydb/core/kafka_proxy/kafka_transactions_coordinator.h>
+#include <ydb/core/kafka_proxy/kafka_transactional_producers_initializers.h>
+#include <ydb/core/kafka_proxy/kafka_consumer_groups_metadata_initializers.h>
+#include <ydb/core/kqp/common/simple/services.h>
+#include <regex>
+
+namespace NKafka {
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddPartitionsToTxnRequest::TPtr& ev, const TActorContext& ctx){
+        KAFKA_LOG_D("Receieved ADD_PARTITIONS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddPartitionsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                PartitionsInTxn.emplace(GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest));
+            }
+        }
+        SendOkResponse<TAddPartitionsToTxnResponseData>(ev);
+    }
+
+    // Method does nothing. In Kafka it will add __consumer_offsets topic to transaction, but 
+    // in YDB Topics we store offsets in table and do not need this extra action.
+    // Thus we can just ignore this request.
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddOffsetsToTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved ADD_OFFSETS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddOffsetsToTxnResponseData>(ev);
+            Die(ctx);
path: ydb/core/kafka_proxy/actors/kafka_transaction_actor.cpp
user: login: a-serebryanskiy
body: Обсудили на звонке, что через ивент внутри метода Die все удаляется
created_at: 2025-04-21T14:44:29Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,448 @@
+#include "kafka_transaction_actor.h"
+#include "kafka_transaction_actor_sql.cpp"
+#include "txn_actor_response_builder.h"
+#include <ydb/core/kafka_proxy/kafka_transactions_coordinator.h>
+#include <ydb/core/kafka_proxy/kafka_transactional_producers_initializers.h>
+#include <ydb/core/kafka_proxy/kafka_consumer_groups_metadata_initializers.h>
+#include <ydb/core/kqp/common/simple/services.h>
+#include <regex>
+
+namespace NKafka {
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddPartitionsToTxnRequest::TPtr& ev, const TActorContext& ctx){
+        KAFKA_LOG_D("Receieved ADD_PARTITIONS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddPartitionsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                PartitionsInTxn.emplace(GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest));
+            }
+        }
+        SendOkResponse<TAddPartitionsToTxnResponseData>(ev);
+    }
+
+    // Method does nothing. In Kafka it will add __consumer_offsets topic to transaction, but 
+    // in YDB Topics we store offsets in table and do not need this extra action.
+    // Thus we can just ignore this request.
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddOffsetsToTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved ADD_OFFSETS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddOffsetsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        SendOkResponse<TAddOffsetsToTxnResponseData>(ev);
+    }
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvTxnOffsetCommitRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved TXN_OFFSET_COMMIT request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TTxnOffsetCommitResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        // save offsets for future use
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                TTopicPartition topicPartition = {GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest.PartitionIndex)};
+                TPartitionCommit newCommit{
+                    .Partition = partitionInRequest.PartitionIndex,
+                    .Offset = partitionInRequest.CommittedOffset,
+                    .ConsumerName = ev->Get()->Request->GroupId->c_str(),
+                    .ConsumerGeneration = ev->Get()->Request->GenerationId
+                };
+                auto it = OffsetsToCommit.find(topicPartition);
+                if (it == OffsetsToCommit.end()) {
+                    OffsetsToCommit.emplace(std::make_pair(topicPartition, newCommit));
+                } else {
+                    it->second = newCommit;
+                }
+            }
+        }
+        SendOkResponse<TTxnOffsetCommitResponseData>(ev);
+    }
+
+    /* 
+    If abort, will send back ok.
+    If commit, will do following sequence of calls and event handlers:
+    1. Open KQP Session
+    2. Handle(NKqp::TEvKqp::TEvCreateSessionResponse) - KQP session created
+    3. Send Select request for current producer and consumers state, begin transaction
+    4. Handle(NKqp::TEvKqp::TEvQueryResponse): Validate returned producer and consumers state. Return PRODUCER_FENCED if check failed
+    5. Send Commit request with properly created KafkaApiOperations
+    6. Handle(NKqp::TEvKqp::TEvQueryResponse): If everything committed successfully, return OK to the client
+    7. Close KQP session, send to coordinator TEvTransactionActorDied, die.
+    */
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvEndTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved END_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TEndTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+
+        bool txnAborted = !ev->Get()->Request->Committed;
+        if (CommitStarted) {
+            return; // we just ignore second and subsequent requests
+        } else if (txnAborted) {
+            SendOkResponse<TEndTxnResponseData>(ev);
+        } else {
+            CommitStarted = true;
+            EndTxnRequestPtr = std::move(ev);
+            StartKqpSession(ctx);
+        }
+    }
+
+    void TKafkaTransactionActor::Handle(TEvents::TEvPoison::TPtr&, const TActorContext& ctx) {
+        Die(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvCreateSessionResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "KQP session created");
+        if (!Kqp->HandleCreateSessionResponse(ev, ctx)) {
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, "Failed to create KQP session");
+            Die(ctx);
+            return;
+        }
+
+        SendToKqpValidationRequests(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Receieved query response from KQP for " << GetAsStr(LastSentToKqpRequest) << " request");
+        if (auto error = GetErrorFromYdbResponse(ev)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error->data());
+            Die(ctx);
+            return;
+        }
+
+        switch (LastSentToKqpRequest) {
+            case EKafkaTxnKqpRequests::SELECT:
+                HandleSelectResponse(*ev->Get(), ctx);
+                break;
+            case EKafkaTxnKqpRequests::COMMIT:
+                HandleCommitResponse(ctx);
+                break;
+            default:
+                Y_FAIL("Unexpected KQP request");
+        }
+    }
+
+    void TKafkaTransactionActor::StartKqpSession(const TActorContext& ctx) {
+        Kqp = std::make_unique<TKqpTxHelper>(DatabasePath);
+        KAFKA_LOG_D(TStringBuilder() << "Sending create session request to KQP for database " << DatabasePath);
+        Kqp->SendCreateSessionRequest(ctx, KqpActorId);
+    }
+
+    void TKafkaTransactionActor::SendToKqpValidationRequests(const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Sending select request to KQP for database " << DatabasePath);
+        Kqp->SendYqlRequest(
+            GetYqlWithTablesNames(NKafkaTransactionSql::SELECT_FOR_VALIDATION), 
+            BuildSelectParams(), 
+            ++KqpCookie, 
+            ctx,
+            false,
+            KqpActorId
+        );
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::SELECT;
+    }
+
+    void TKafkaTransactionActor::SendCommitTxnRequest(const TString& kqpTransactionId) {
+        auto request = BuildCommitTxnRequestToKqp(kqpTransactionId);
+
+        Send(KqpActorId, request.Release(), 0, ++KqpCookie);
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::COMMIT;
+    }
+    
+    // Response senders
+    template<class ErrorResponseType, class EventType>
+    void TKafkaTransactionActor::SendInvalidTransactionActorStateResponse(TAutoPtr<TEventHandle<EventType>>& evHandle) {
+        auto kafkaRequest = evHandle->Get()->Request;
+        KAFKA_LOG_CRIT(TStringBuilder() << "Recieved invalid request. Got: "
+            << "transactionalId=" << kafkaRequest->TransactionalId->c_str() 
+            << ", producerId=" << kafkaRequest->ProducerId 
+            << ", producerEpoch=" << kafkaRequest->ProducerEpoch
+        );
+        std::shared_ptr<ErrorResponseType> response = NKafkaTransactions::BuildResponse<ErrorResponseType>(kafkaRequest, EKafkaErrors::UNKNOWN_SERVER_ERROR);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, EKafkaErrors::UNKNOWN_SERVER_ERROR));
+    }
+
+    template<class ErrorResponseType, class EventType>
+    void TKafkaTransactionActor::SendFailResponse(TAutoPtr<TEventHandle<EventType>>& evHandle, EKafkaErrors errorCode, const TString& errorMessage) {
+        if (errorMessage) {
+            KAFKA_LOG_W(TStringBuilder() << "Sending fail response with error code: " << errorCode << ". Reason:  " << errorMessage);
+        } else {
+            KAFKA_LOG_W(TStringBuilder() << "Sending fail response with error code: " << errorCode);
+        }
+
+        std::shared_ptr<ErrorResponseType> response = NKafkaTransactions::BuildResponse<ErrorResponseType>(evHandle->Get()->Request, errorCode);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, errorCode));
+    }
+
+    template<class ResponseType, class EventType>
+    void TKafkaTransactionActor::SendOkResponse(TAutoPtr<TEventHandle<EventType>>& evHandle) {
+        auto kafkaRequest = evHandle->Get()->Request;
+        KAFKA_LOG_D(TStringBuilder() << "Sending OK response to " << evHandle->Get()->ConnectionId << " with correlationId " << evHandle->Get()->CorrelationId << " and transactionalId " << TransactionalId);
+        std::shared_ptr<ResponseType> response = NKafkaTransactions::BuildResponse<ResponseType>(kafkaRequest, EKafkaErrors::NONE_ERROR);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, EKafkaErrors::NONE_ERROR));
+    }
+
+    // helper methods
+    void TKafkaTransactionActor::Die(const TActorContext &ctx) {
+        KAFKA_LOG_D("Dying.");
+        if (Kqp) {
+            Kqp->CloseKqpSession(ctx);
+        }
+        Send(TxnCoordinatorActorId, new TEvKafka::TEvTransactionActorDied(TransactionalId, ProducerId, ProducerEpoch));
+        TBase::Die(ctx);
+    }
+
+    template<class EventType>
+    bool TKafkaTransactionActor::ProducerInRequestIsValid(TMessagePtr<EventType> kafkaRequest) {
+        return kafkaRequest->TransactionalId->c_str() == TransactionalId 
+            && kafkaRequest->ProducerId == ProducerId 
+            && kafkaRequest->ProducerEpoch == ProducerEpoch;
path: ydb/core/kafka_proxy/actors/kafka_transaction_actor.cpp
user: login: a-serebryanskiy
body: Тут к сожалению нельзя) Тут поля в kafkarequest достаются из API сообщения самой кафки - по ее протоколу. А в нем не поправишь на `TProducerState`)
created_at: 2025-04-21T14:48:33Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,448 @@
+#include "kafka_transaction_actor.h"
+#include "kafka_transaction_actor_sql.cpp"
+#include "txn_actor_response_builder.h"
+#include <ydb/core/kafka_proxy/kafka_transactions_coordinator.h>
+#include <ydb/core/kafka_proxy/kafka_transactional_producers_initializers.h>
+#include <ydb/core/kafka_proxy/kafka_consumer_groups_metadata_initializers.h>
+#include <ydb/core/kqp/common/simple/services.h>
+#include <regex>
+
+namespace NKafka {
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddPartitionsToTxnRequest::TPtr& ev, const TActorContext& ctx){
+        KAFKA_LOG_D("Receieved ADD_PARTITIONS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddPartitionsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                PartitionsInTxn.emplace(GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest));
+            }
+        }
+        SendOkResponse<TAddPartitionsToTxnResponseData>(ev);
+    }
+
+    // Method does nothing. In Kafka it will add __consumer_offsets topic to transaction, but 
+    // in YDB Topics we store offsets in table and do not need this extra action.
+    // Thus we can just ignore this request.
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddOffsetsToTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved ADD_OFFSETS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddOffsetsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        SendOkResponse<TAddOffsetsToTxnResponseData>(ev);
+    }
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvTxnOffsetCommitRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved TXN_OFFSET_COMMIT request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TTxnOffsetCommitResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        // save offsets for future use
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                TTopicPartition topicPartition = {GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest.PartitionIndex)};
+                TPartitionCommit newCommit{
+                    .Partition = partitionInRequest.PartitionIndex,
+                    .Offset = partitionInRequest.CommittedOffset,
+                    .ConsumerName = ev->Get()->Request->GroupId->c_str(),
+                    .ConsumerGeneration = ev->Get()->Request->GenerationId
+                };
+                auto it = OffsetsToCommit.find(topicPartition);
+                if (it == OffsetsToCommit.end()) {
+                    OffsetsToCommit.emplace(std::make_pair(topicPartition, newCommit));
+                } else {
+                    it->second = newCommit;
+                }
+            }
+        }
+        SendOkResponse<TTxnOffsetCommitResponseData>(ev);
+    }
+
+    /* 
+    If abort, will send back ok.
+    If commit, will do following sequence of calls and event handlers:
+    1. Open KQP Session
+    2. Handle(NKqp::TEvKqp::TEvCreateSessionResponse) - KQP session created
+    3. Send Select request for current producer and consumers state, begin transaction
+    4. Handle(NKqp::TEvKqp::TEvQueryResponse): Validate returned producer and consumers state. Return PRODUCER_FENCED if check failed
+    5. Send Commit request with properly created KafkaApiOperations
+    6. Handle(NKqp::TEvKqp::TEvQueryResponse): If everything committed successfully, return OK to the client
+    7. Close KQP session, send to coordinator TEvTransactionActorDied, die.
+    */
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvEndTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved END_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TEndTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+
+        bool txnAborted = !ev->Get()->Request->Committed;
+        if (CommitStarted) {
+            return; // we just ignore second and subsequent requests
+        } else if (txnAborted) {
+            SendOkResponse<TEndTxnResponseData>(ev);
+        } else {
+            CommitStarted = true;
+            EndTxnRequestPtr = std::move(ev);
+            StartKqpSession(ctx);
+        }
+    }
+
+    void TKafkaTransactionActor::Handle(TEvents::TEvPoison::TPtr&, const TActorContext& ctx) {
+        Die(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvCreateSessionResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "KQP session created");
+        if (!Kqp->HandleCreateSessionResponse(ev, ctx)) {
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, "Failed to create KQP session");
+            Die(ctx);
+            return;
+        }
+
+        SendToKqpValidationRequests(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Receieved query response from KQP for " << GetAsStr(LastSentToKqpRequest) << " request");
+        if (auto error = GetErrorFromYdbResponse(ev)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error->data());
+            Die(ctx);
+            return;
+        }
+
+        switch (LastSentToKqpRequest) {
+            case EKafkaTxnKqpRequests::SELECT:
+                HandleSelectResponse(*ev->Get(), ctx);
+                break;
+            case EKafkaTxnKqpRequests::COMMIT:
+                HandleCommitResponse(ctx);
+                break;
+            default:
+                Y_FAIL("Unexpected KQP request");
+        }
+    }
+
+    void TKafkaTransactionActor::StartKqpSession(const TActorContext& ctx) {
+        Kqp = std::make_unique<TKqpTxHelper>(DatabasePath);
+        KAFKA_LOG_D(TStringBuilder() << "Sending create session request to KQP for database " << DatabasePath);
+        Kqp->SendCreateSessionRequest(ctx, KqpActorId);
+    }
+
+    void TKafkaTransactionActor::SendToKqpValidationRequests(const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Sending select request to KQP for database " << DatabasePath);
+        Kqp->SendYqlRequest(
+            GetYqlWithTablesNames(NKafkaTransactionSql::SELECT_FOR_VALIDATION), 
+            BuildSelectParams(), 
+            ++KqpCookie, 
+            ctx,
+            false,
+            KqpActorId
+        );
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::SELECT;
+    }
+
+    void TKafkaTransactionActor::SendCommitTxnRequest(const TString& kqpTransactionId) {
+        auto request = BuildCommitTxnRequestToKqp(kqpTransactionId);
+
+        Send(KqpActorId, request.Release(), 0, ++KqpCookie);
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::COMMIT;
+    }
+    
+    // Response senders
+    template<class ErrorResponseType, class EventType>
+    void TKafkaTransactionActor::SendInvalidTransactionActorStateResponse(TAutoPtr<TEventHandle<EventType>>& evHandle) {
+        auto kafkaRequest = evHandle->Get()->Request;
+        KAFKA_LOG_CRIT(TStringBuilder() << "Recieved invalid request. Got: "
+            << "transactionalId=" << kafkaRequest->TransactionalId->c_str() 
+            << ", producerId=" << kafkaRequest->ProducerId 
+            << ", producerEpoch=" << kafkaRequest->ProducerEpoch
+        );
+        std::shared_ptr<ErrorResponseType> response = NKafkaTransactions::BuildResponse<ErrorResponseType>(kafkaRequest, EKafkaErrors::UNKNOWN_SERVER_ERROR);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, EKafkaErrors::UNKNOWN_SERVER_ERROR));
+    }
+
+    template<class ErrorResponseType, class EventType>
+    void TKafkaTransactionActor::SendFailResponse(TAutoPtr<TEventHandle<EventType>>& evHandle, EKafkaErrors errorCode, const TString& errorMessage) {
+        if (errorMessage) {
+            KAFKA_LOG_W(TStringBuilder() << "Sending fail response with error code: " << errorCode << ". Reason:  " << errorMessage);
+        } else {
+            KAFKA_LOG_W(TStringBuilder() << "Sending fail response with error code: " << errorCode);
+        }
+
+        std::shared_ptr<ErrorResponseType> response = NKafkaTransactions::BuildResponse<ErrorResponseType>(evHandle->Get()->Request, errorCode);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, errorCode));
+    }
+
+    template<class ResponseType, class EventType>
+    void TKafkaTransactionActor::SendOkResponse(TAutoPtr<TEventHandle<EventType>>& evHandle) {
+        auto kafkaRequest = evHandle->Get()->Request;
+        KAFKA_LOG_D(TStringBuilder() << "Sending OK response to " << evHandle->Get()->ConnectionId << " with correlationId " << evHandle->Get()->CorrelationId << " and transactionalId " << TransactionalId);
+        std::shared_ptr<ResponseType> response = NKafkaTransactions::BuildResponse<ResponseType>(kafkaRequest, EKafkaErrors::NONE_ERROR);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, EKafkaErrors::NONE_ERROR));
+    }
+
+    // helper methods
+    void TKafkaTransactionActor::Die(const TActorContext &ctx) {
+        KAFKA_LOG_D("Dying.");
+        if (Kqp) {
+            Kqp->CloseKqpSession(ctx);
+        }
+        Send(TxnCoordinatorActorId, new TEvKafka::TEvTransactionActorDied(TransactionalId, ProducerId, ProducerEpoch));
+        TBase::Die(ctx);
+    }
+
+    template<class EventType>
+    bool TKafkaTransactionActor::ProducerInRequestIsValid(TMessagePtr<EventType> kafkaRequest) {
+        return kafkaRequest->TransactionalId->c_str() == TransactionalId 
+            && kafkaRequest->ProducerId == ProducerId 
+            && kafkaRequest->ProducerEpoch == ProducerEpoch;
+    }
+
+    TString TKafkaTransactionActor::GetFullTopicPath(const TString& topicName) {
+        return NPersQueue::GetFullTopicPath(DatabasePath, topicName);
+    }
+
+    TString TKafkaTransactionActor::GetYqlWithTablesNames(const TString& templateStr) {
+        TString templateWithProducerStateTable = std::regex_replace(
+            templateStr.c_str(),
+            std::regex("<producer_state_table_name>"), 
+            NKikimr::NGRpcProxy::V1::TTransactionalProducersInitManager::GetInstant()->GetStorageTablePath().c_str()
+        );
+        TString templateWithConsumerStateTable = std::regex_replace(
+            templateWithProducerStateTable.c_str(),
+            std::regex("<consumer_state_table_name>"), 
+            NKikimr::NGRpcProxy::V1::TKafkaConsumerGroupsMetaInitManager::GetInstant()->GetStorageTablePath().c_str()
+        );
+
+        return templateWithConsumerStateTable;
+    }
+
+    NYdb::TParams TKafkaTransactionActor::BuildSelectParams() {
+        NYdb::TParamsBuilder params;
+        params.AddParam("$Database").Utf8(DatabasePath).Build();
+        params.AddParam("$TransactionalId").Utf8(TransactionalId).Build();
+        
+        // select unique consumer group names
+        std::unordered_set<TString> uniqueConsumerGroups;
+        for (auto& [partition, commitDetails] : OffsetsToCommit) {
+            uniqueConsumerGroups.emplace(commitDetails.ConsumerName);
+        }
+
+        // add unique consumer group names to request as params
+        auto& consumerGroupsParamBuilder = params.AddParam("$ConsumerGroups").BeginList();
+        for (auto& consumerGroupName : uniqueConsumerGroups) {
+            consumerGroupsParamBuilder.AddListItem().Utf8(consumerGroupName);
+        }
+        consumerGroupsParamBuilder.EndList().Build();
+
+        return params.Build();
+    }
+
+    THolder<NKikimr::NKqp::TEvKqp::TEvQueryRequest> TKafkaTransactionActor::BuildCommitTxnRequestToKqp(const TString& kqpTransactionId) {
+        auto ev = MakeHolder<TEvKqp::TEvQueryRequest>();
+
+        ev->Record.MutableRequest()->SetType(NKikimrKqp::QUERY_TYPE_UNDEFINED);
+        ev->Record.MutableRequest()->SetAction(NKikimrKqp::QUERY_ACTION_TOPIC);
+        ev->Record.MutableRequest()->SetDatabase(DatabasePath);
+        ev->Record.MutableRequest()->SetSessionId(KqpSessionId);
+
+        ev->Record.MutableRequest()->MutableTxControl()->set_commit_tx(true);
+        ev->Record.MutableRequest()->MutableTxControl()->set_tx_id(kqpTransactionId);
+
+        ev->Record.MutableRequest()->SetUsePublicResponseDataFormat(true);
+
+        auto* kafkaApiOperations = ev->Record.MutableRequest()->MutableKafkaApiOperations();
+        kafkaApiOperations->set_transactionalid(TransactionalId);
+        kafkaApiOperations->set_producerid(ProducerId);
+        kafkaApiOperations->set_producerepoch(ProducerEpoch);
+
+        for (auto& partition : PartitionsInTxn) {
+            auto* partitionInRequest = kafkaApiOperations->AddPartitionsInTxn();
+            partitionInRequest->set_topicpath(partition.TopicPath);
+            partitionInRequest->set_partitionid(partition.PartitionId);
+        }
+
+        for (auto& [partition, offsetDetails] : OffsetsToCommit) {
+            auto* offsetInRequest = kafkaApiOperations->AddOffsetsInTxn();
+            offsetInRequest->set_topicpath(partition.TopicPath);
+            offsetInRequest->set_partitionid(partition.PartitionId);
+            offsetInRequest->set_consumername(offsetDetails.ConsumerName);
+            offsetInRequest->set_consumergeneration(offsetDetails.ConsumerGeneration);
+            offsetInRequest->set_offset(offsetDetails.Offset);
+        }
+        
+        return ev;
+    }
+
+    void TKafkaTransactionActor::HandleSelectResponse(const NKqp::TEvKqp::TEvQueryResponse& response, const TActorContext& ctx) {
+        // YDB should return exactly two result sets for two queries: to producer and consumer state tables
+        if (response.Record.GetResponse().GetYdbResults().size() != 2) {
+            TString error = TStringBuilder() << "KQP returned wrong number of result sets on SELECT query. Expected 2, got " << response.Record.GetResponse().GetYdbResults().size() << ".";
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error);
+            Die(ctx);
+            return;
+        }
+
+        // parse and validate producer state
+        TMaybe<TProducerState> producerState;
+        try {
+            producerState = ParseProducerState(response);
+        } catch (const yexception& y) {
+            TString error = TStringBuilder() << "Error parsing producer state response from KQP. Reason: " << y.what();
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error);
+            Die(ctx);
+            return;
+        }
+        if (auto error = GetErrorInProducerState(producerState)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::PRODUCER_FENCED, error->data());
+            Die(ctx);
+            return;
+        }
+
+        // parse and validate consumers
+        std::unordered_map<TString, i32> consumerGenerationsByName = ParseConsumersGenerations(response);
+        if (auto error = GetErrorInConsumersStates(consumerGenerationsByName)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::PRODUCER_FENCED, error->data());
+            Die(ctx);
+            return;
+        }
+
+        KAFKA_LOG_D(TStringBuilder() << "Validated producer and consumers states. Everything is alright, sending commit");
+        auto kqpTxnId = response.Record.GetResponse().GetTxMeta().id();
+        // finally everything is valid and we can attempt to commit
+        SendCommitTxnRequest(kqpTxnId);
+    }
+
+    void TKafkaTransactionActor::HandleCommitResponse(const TActorContext& ctx) {
+        KAFKA_LOG_D("Successfully committed transaction. Sending ok and dying.");
+        SendOkResponse<TEndTxnResponseData>(EndTxnRequestPtr);
+        Die(ctx);
+    }
+
+    TMaybe<TString> TKafkaTransactionActor::GetErrorFromYdbResponse(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev) {
+        TStringBuilder builder = TStringBuilder() << "Recieved error on request to KQP. Last sent request: " << GetAsStr(LastSentToKqpRequest) << ". Reason: ";
+        if (ev->Cookie != KqpCookie) {
+            return builder << "Unexpected cookie in TEvQueryResponse. Expected KQP Cookie: " << KqpCookie << ", Actual: " << ev->Cookie << ".";
+        } else if (ev->Get()->Record.GetYdbStatus() != Ydb::StatusIds::SUCCESS) {
+            return builder << "Unexpected YDB status in TEvQueryResponse. Expected YDB SUCCESS status, Actual: " << ev->Get()->Record.GetYdbStatus() << ".";
path: ydb/core/kafka_proxy/actors/kafka_transaction_actor.cpp
user: login: a-serebryanskiy
body: К сожалению, нет. У кафки очень мало доступных кодов ответа для транзакционных запросов. Придется разбираться по логам сервера.
created_at: 2025-04-21T14:59:46Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,448 @@
+#include "kafka_transaction_actor.h"
+#include "kafka_transaction_actor_sql.cpp"
+#include "txn_actor_response_builder.h"
+#include <ydb/core/kafka_proxy/kafka_transactions_coordinator.h>
+#include <ydb/core/kafka_proxy/kafka_transactional_producers_initializers.h>
+#include <ydb/core/kafka_proxy/kafka_consumer_groups_metadata_initializers.h>
+#include <ydb/core/kqp/common/simple/services.h>
+#include <regex>
+
+namespace NKafka {
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddPartitionsToTxnRequest::TPtr& ev, const TActorContext& ctx){
+        KAFKA_LOG_D("Receieved ADD_PARTITIONS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddPartitionsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                PartitionsInTxn.emplace(GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest));
+            }
+        }
+        SendOkResponse<TAddPartitionsToTxnResponseData>(ev);
+    }
+
+    // Method does nothing. In Kafka it will add __consumer_offsets topic to transaction, but 
+    // in YDB Topics we store offsets in table and do not need this extra action.
+    // Thus we can just ignore this request.
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvAddOffsetsToTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved ADD_OFFSETS_TO_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TAddOffsetsToTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        SendOkResponse<TAddOffsetsToTxnResponseData>(ev);
+    }
+
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvTxnOffsetCommitRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved TXN_OFFSET_COMMIT request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TTxnOffsetCommitResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+        // save offsets for future use
+        for (auto& topicInRequest : ev->Get()->Request->Topics) {
+            for (auto& partitionInRequest : topicInRequest.Partitions) {
+                TTopicPartition topicPartition = {GetFullTopicPath(topicInRequest.Name->c_str()), static_cast<ui32>(partitionInRequest.PartitionIndex)};
+                TPartitionCommit newCommit{
+                    .Partition = partitionInRequest.PartitionIndex,
+                    .Offset = partitionInRequest.CommittedOffset,
+                    .ConsumerName = ev->Get()->Request->GroupId->c_str(),
+                    .ConsumerGeneration = ev->Get()->Request->GenerationId
+                };
+                auto it = OffsetsToCommit.find(topicPartition);
+                if (it == OffsetsToCommit.end()) {
+                    OffsetsToCommit.emplace(std::make_pair(topicPartition, newCommit));
+                } else {
+                    it->second = newCommit;
+                }
+            }
+        }
+        SendOkResponse<TTxnOffsetCommitResponseData>(ev);
+    }
+
+    /* 
+    If abort, will send back ok.
+    If commit, will do following sequence of calls and event handlers:
+    1. Open KQP Session
+    2. Handle(NKqp::TEvKqp::TEvCreateSessionResponse) - KQP session created
+    3. Send Select request for current producer and consumers state, begin transaction
+    4. Handle(NKqp::TEvKqp::TEvQueryResponse): Validate returned producer and consumers state. Return PRODUCER_FENCED if check failed
+    5. Send Commit request with properly created KafkaApiOperations
+    6. Handle(NKqp::TEvKqp::TEvQueryResponse): If everything committed successfully, return OK to the client
+    7. Close KQP session, send to coordinator TEvTransactionActorDied, die.
+    */
+    void TKafkaTransactionActor::Handle(TEvKafka::TEvEndTxnRequest::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D("Receieved END_TXN request");
+        if (!ProducerInRequestIsValid(ev->Get()->Request)) {
+            SendInvalidTransactionActorStateResponse<TEndTxnResponseData>(ev);
+            Die(ctx);
+            return;
+        }
+
+        bool txnAborted = !ev->Get()->Request->Committed;
+        if (CommitStarted) {
+            return; // we just ignore second and subsequent requests
+        } else if (txnAborted) {
+            SendOkResponse<TEndTxnResponseData>(ev);
+        } else {
+            CommitStarted = true;
+            EndTxnRequestPtr = std::move(ev);
+            StartKqpSession(ctx);
+        }
+    }
+
+    void TKafkaTransactionActor::Handle(TEvents::TEvPoison::TPtr&, const TActorContext& ctx) {
+        Die(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvCreateSessionResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "KQP session created");
+        if (!Kqp->HandleCreateSessionResponse(ev, ctx)) {
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, "Failed to create KQP session");
+            Die(ctx);
+            return;
+        }
+
+        SendToKqpValidationRequests(ctx);
+    }
+
+    void TKafkaTransactionActor::Handle(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev, const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Receieved query response from KQP for " << GetAsStr(LastSentToKqpRequest) << " request");
+        if (auto error = GetErrorFromYdbResponse(ev)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error->data());
+            Die(ctx);
+            return;
+        }
+
+        switch (LastSentToKqpRequest) {
+            case EKafkaTxnKqpRequests::SELECT:
+                HandleSelectResponse(*ev->Get(), ctx);
+                break;
+            case EKafkaTxnKqpRequests::COMMIT:
+                HandleCommitResponse(ctx);
+                break;
+            default:
+                Y_FAIL("Unexpected KQP request");
+        }
+    }
+
+    void TKafkaTransactionActor::StartKqpSession(const TActorContext& ctx) {
+        Kqp = std::make_unique<TKqpTxHelper>(DatabasePath);
+        KAFKA_LOG_D(TStringBuilder() << "Sending create session request to KQP for database " << DatabasePath);
+        Kqp->SendCreateSessionRequest(ctx, KqpActorId);
+    }
+
+    void TKafkaTransactionActor::SendToKqpValidationRequests(const TActorContext& ctx) {
+        KAFKA_LOG_D(TStringBuilder() << "Sending select request to KQP for database " << DatabasePath);
+        Kqp->SendYqlRequest(
+            GetYqlWithTablesNames(NKafkaTransactionSql::SELECT_FOR_VALIDATION), 
+            BuildSelectParams(), 
+            ++KqpCookie, 
+            ctx,
+            false,
+            KqpActorId
+        );
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::SELECT;
+    }
+
+    void TKafkaTransactionActor::SendCommitTxnRequest(const TString& kqpTransactionId) {
+        auto request = BuildCommitTxnRequestToKqp(kqpTransactionId);
+
+        Send(KqpActorId, request.Release(), 0, ++KqpCookie);
+        
+        LastSentToKqpRequest = EKafkaTxnKqpRequests::COMMIT;
+    }
+    
+    // Response senders
+    template<class ErrorResponseType, class EventType>
+    void TKafkaTransactionActor::SendInvalidTransactionActorStateResponse(TAutoPtr<TEventHandle<EventType>>& evHandle) {
+        auto kafkaRequest = evHandle->Get()->Request;
+        KAFKA_LOG_CRIT(TStringBuilder() << "Recieved invalid request. Got: "
+            << "transactionalId=" << kafkaRequest->TransactionalId->c_str() 
+            << ", producerId=" << kafkaRequest->ProducerId 
+            << ", producerEpoch=" << kafkaRequest->ProducerEpoch
+        );
+        std::shared_ptr<ErrorResponseType> response = NKafkaTransactions::BuildResponse<ErrorResponseType>(kafkaRequest, EKafkaErrors::UNKNOWN_SERVER_ERROR);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, EKafkaErrors::UNKNOWN_SERVER_ERROR));
+    }
+
+    template<class ErrorResponseType, class EventType>
+    void TKafkaTransactionActor::SendFailResponse(TAutoPtr<TEventHandle<EventType>>& evHandle, EKafkaErrors errorCode, const TString& errorMessage) {
+        if (errorMessage) {
+            KAFKA_LOG_W(TStringBuilder() << "Sending fail response with error code: " << errorCode << ". Reason:  " << errorMessage);
+        } else {
+            KAFKA_LOG_W(TStringBuilder() << "Sending fail response with error code: " << errorCode);
+        }
+
+        std::shared_ptr<ErrorResponseType> response = NKafkaTransactions::BuildResponse<ErrorResponseType>(evHandle->Get()->Request, errorCode);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, errorCode));
+    }
+
+    template<class ResponseType, class EventType>
+    void TKafkaTransactionActor::SendOkResponse(TAutoPtr<TEventHandle<EventType>>& evHandle) {
+        auto kafkaRequest = evHandle->Get()->Request;
+        KAFKA_LOG_D(TStringBuilder() << "Sending OK response to " << evHandle->Get()->ConnectionId << " with correlationId " << evHandle->Get()->CorrelationId << " and transactionalId " << TransactionalId);
+        std::shared_ptr<ResponseType> response = NKafkaTransactions::BuildResponse<ResponseType>(kafkaRequest, EKafkaErrors::NONE_ERROR);
+        Send(evHandle->Get()->ConnectionId, new TEvKafka::TEvResponse(evHandle->Get()->CorrelationId, response, EKafkaErrors::NONE_ERROR));
+    }
+
+    // helper methods
+    void TKafkaTransactionActor::Die(const TActorContext &ctx) {
+        KAFKA_LOG_D("Dying.");
+        if (Kqp) {
+            Kqp->CloseKqpSession(ctx);
+        }
+        Send(TxnCoordinatorActorId, new TEvKafka::TEvTransactionActorDied(TransactionalId, ProducerId, ProducerEpoch));
+        TBase::Die(ctx);
+    }
+
+    template<class EventType>
+    bool TKafkaTransactionActor::ProducerInRequestIsValid(TMessagePtr<EventType> kafkaRequest) {
+        return kafkaRequest->TransactionalId->c_str() == TransactionalId 
+            && kafkaRequest->ProducerId == ProducerId 
+            && kafkaRequest->ProducerEpoch == ProducerEpoch;
+    }
+
+    TString TKafkaTransactionActor::GetFullTopicPath(const TString& topicName) {
+        return NPersQueue::GetFullTopicPath(DatabasePath, topicName);
+    }
+
+    TString TKafkaTransactionActor::GetYqlWithTablesNames(const TString& templateStr) {
+        TString templateWithProducerStateTable = std::regex_replace(
+            templateStr.c_str(),
+            std::regex("<producer_state_table_name>"), 
+            NKikimr::NGRpcProxy::V1::TTransactionalProducersInitManager::GetInstant()->GetStorageTablePath().c_str()
+        );
+        TString templateWithConsumerStateTable = std::regex_replace(
+            templateWithProducerStateTable.c_str(),
+            std::regex("<consumer_state_table_name>"), 
+            NKikimr::NGRpcProxy::V1::TKafkaConsumerGroupsMetaInitManager::GetInstant()->GetStorageTablePath().c_str()
+        );
+
+        return templateWithConsumerStateTable;
+    }
+
+    NYdb::TParams TKafkaTransactionActor::BuildSelectParams() {
+        NYdb::TParamsBuilder params;
+        params.AddParam("$Database").Utf8(DatabasePath).Build();
+        params.AddParam("$TransactionalId").Utf8(TransactionalId).Build();
+        
+        // select unique consumer group names
+        std::unordered_set<TString> uniqueConsumerGroups;
+        for (auto& [partition, commitDetails] : OffsetsToCommit) {
+            uniqueConsumerGroups.emplace(commitDetails.ConsumerName);
+        }
+
+        // add unique consumer group names to request as params
+        auto& consumerGroupsParamBuilder = params.AddParam("$ConsumerGroups").BeginList();
+        for (auto& consumerGroupName : uniqueConsumerGroups) {
+            consumerGroupsParamBuilder.AddListItem().Utf8(consumerGroupName);
+        }
+        consumerGroupsParamBuilder.EndList().Build();
+
+        return params.Build();
+    }
+
+    THolder<NKikimr::NKqp::TEvKqp::TEvQueryRequest> TKafkaTransactionActor::BuildCommitTxnRequestToKqp(const TString& kqpTransactionId) {
+        auto ev = MakeHolder<TEvKqp::TEvQueryRequest>();
+
+        ev->Record.MutableRequest()->SetType(NKikimrKqp::QUERY_TYPE_UNDEFINED);
+        ev->Record.MutableRequest()->SetAction(NKikimrKqp::QUERY_ACTION_TOPIC);
+        ev->Record.MutableRequest()->SetDatabase(DatabasePath);
+        ev->Record.MutableRequest()->SetSessionId(KqpSessionId);
+
+        ev->Record.MutableRequest()->MutableTxControl()->set_commit_tx(true);
+        ev->Record.MutableRequest()->MutableTxControl()->set_tx_id(kqpTransactionId);
+
+        ev->Record.MutableRequest()->SetUsePublicResponseDataFormat(true);
+
+        auto* kafkaApiOperations = ev->Record.MutableRequest()->MutableKafkaApiOperations();
+        kafkaApiOperations->set_transactionalid(TransactionalId);
+        kafkaApiOperations->set_producerid(ProducerId);
+        kafkaApiOperations->set_producerepoch(ProducerEpoch);
+
+        for (auto& partition : PartitionsInTxn) {
+            auto* partitionInRequest = kafkaApiOperations->AddPartitionsInTxn();
+            partitionInRequest->set_topicpath(partition.TopicPath);
+            partitionInRequest->set_partitionid(partition.PartitionId);
+        }
+
+        for (auto& [partition, offsetDetails] : OffsetsToCommit) {
+            auto* offsetInRequest = kafkaApiOperations->AddOffsetsInTxn();
+            offsetInRequest->set_topicpath(partition.TopicPath);
+            offsetInRequest->set_partitionid(partition.PartitionId);
+            offsetInRequest->set_consumername(offsetDetails.ConsumerName);
+            offsetInRequest->set_consumergeneration(offsetDetails.ConsumerGeneration);
+            offsetInRequest->set_offset(offsetDetails.Offset);
+        }
+        
+        return ev;
+    }
+
+    void TKafkaTransactionActor::HandleSelectResponse(const NKqp::TEvKqp::TEvQueryResponse& response, const TActorContext& ctx) {
+        // YDB should return exactly two result sets for two queries: to producer and consumer state tables
+        if (response.Record.GetResponse().GetYdbResults().size() != 2) {
+            TString error = TStringBuilder() << "KQP returned wrong number of result sets on SELECT query. Expected 2, got " << response.Record.GetResponse().GetYdbResults().size() << ".";
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error);
+            Die(ctx);
+            return;
+        }
+
+        // parse and validate producer state
+        TMaybe<TProducerState> producerState;
+        try {
+            producerState = ParseProducerState(response);
+        } catch (const yexception& y) {
+            TString error = TStringBuilder() << "Error parsing producer state response from KQP. Reason: " << y.what();
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::BROKER_NOT_AVAILABLE, error);
+            Die(ctx);
+            return;
+        }
+        if (auto error = GetErrorInProducerState(producerState)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::PRODUCER_FENCED, error->data());
+            Die(ctx);
+            return;
+        }
+
+        // parse and validate consumers
+        std::unordered_map<TString, i32> consumerGenerationsByName = ParseConsumersGenerations(response);
+        if (auto error = GetErrorInConsumersStates(consumerGenerationsByName)) {
+            KAFKA_LOG_W(error);
+            SendFailResponse<TEndTxnResponseData>(EndTxnRequestPtr, EKafkaErrors::PRODUCER_FENCED, error->data());
+            Die(ctx);
+            return;
+        }
+
+        KAFKA_LOG_D(TStringBuilder() << "Validated producer and consumers states. Everything is alright, sending commit");
+        auto kqpTxnId = response.Record.GetResponse().GetTxMeta().id();
+        // finally everything is valid and we can attempt to commit
+        SendCommitTxnRequest(kqpTxnId);
+    }
+
+    void TKafkaTransactionActor::HandleCommitResponse(const TActorContext& ctx) {
+        KAFKA_LOG_D("Successfully committed transaction. Sending ok and dying.");
+        SendOkResponse<TEndTxnResponseData>(EndTxnRequestPtr);
+        Die(ctx);
+    }
+
+    TMaybe<TString> TKafkaTransactionActor::GetErrorFromYdbResponse(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev) {
+        TStringBuilder builder = TStringBuilder() << "Recieved error on request to KQP. Last sent request: " << GetAsStr(LastSentToKqpRequest) << ". Reason: ";
+        if (ev->Cookie != KqpCookie) {
+            return builder << "Unexpected cookie in TEvQueryResponse. Expected KQP Cookie: " << KqpCookie << ", Actual: " << ev->Cookie << ".";
+        } else if (ev->Get()->Record.GetYdbStatus() != Ydb::StatusIds::SUCCESS) {
+            return builder << "Unexpected YDB status in TEvQueryResponse. Expected YDB SUCCESS status, Actual: " << ev->Get()->Record.GetYdbStatus() << ".";
+        } else {
+            return {};
+        }
+    }
+
+    TMaybe<TProducerState> TKafkaTransactionActor::ParseProducerState(const NKqp::TEvKqp::TEvQueryResponse& response) {
+        auto& resp = response.Record.GetResponse();
+
+        NYdb::TResultSetParser parser(resp.GetYdbResults(NKafkaTransactionSql::PRODUCER_STATE_REQUEST_INDEX));
+
+        // for this transactional id there is no rows
+        if (parser.RowsCount() == 0) {
+            return {};
+        } 
+        // there are multiple rows for this transactional id. This is unexpected and should not happen
+        else if (parser.RowsCount() > 1) {
+            throw yexception() << "Request returned more than one row: " << resp.GetYdbResults().size();
+        } else {
+            parser.TryNextRow();
+
+            TProducerState result; 
+
+            result.TransactionalId = parser.ColumnParser("transactional_id").GetUtf8();
+            result.ProducerId = parser.ColumnParser("producer_id").GetInt64();
+            result.ProducerEpoch = parser.ColumnParser("producer_epoch").GetInt16();
+
+            return result;
+        }
+    }
+
+    TMaybe<TString> TKafkaTransactionActor::GetErrorInProducerState(const TMaybe<TProducerState>& producerState) {
+        if (!producerState) {
+            return "No producer state found. May be it has expired";
+        } else if (producerState->TransactionalId != TransactionalId || producerState->ProducerId != ProducerId || producerState->ProducerEpoch != ProducerEpoch) {
+            return TStringBuilder() << "Producer state in producers_state table is different from the one in this actor. Producer state in table: "
path: ydb/core/kafka_proxy/actors/kafka_transaction_actor.cpp
user: login: a-serebryanskiy
body: Тут к сожалению нельзя заменить - это значения, получаемые из таблички в том формате, в котором они лежат в табличке
created_at: 2025-04-22T10:07:00Z
author_association: COLLABORATOR
diff_hunk: @@ -1,32 +1,144 @@
 #pragma once
 
+#include "kafka_init_producer_id_actor.h"
+#include <ydb/core/kafka_proxy/kafka_events.h>
 #include <ydb/library/actors/core/actor_bootstrapped.h>
+#include <ydb/core/kafka_proxy/kqp_helper.h>
 
 namespace NKafka {
     /* 
     This class is responsible for one kafka transaction.
 
     It accumulates transaction state (partitions in tx, offsets) and on commit submits transaction to KQP
     */
-    class TKafkaTransactionActor : public NActors::TActorBootstrapped<TKafkaTransactionActor> {
+    class TKafkaTransactionActor : public NActors::TActor<TKafkaTransactionActor> {
 
-        using TBase = NActors::TActorBootstrapped<TKafkaTransactionActor>;
+        using TBase = NActors::TActor<TKafkaTransactionActor>;
         
         public:
-            void Bootstrap(const NActors::TActorContext&) {
-                TBase::Become(&TKafkaTransactionActor::StateWork);
-            }
+            struct TTopicPartition {
+                TString TopicPath;
+                ui32 PartitionId;
+
+                bool operator==(const TTopicPartition &other) const = default;
+            };
+
+            struct TopicPartitionHashFn {
+                size_t operator()(const TTopicPartition& partition) const {
+                    return std::hash<TString>()(partition.TopicPath) ^ std::hash<int64_t>()(partition.PartitionId);
+                }
+            };
+
+            struct TPartitionCommit {
+                i32 Partition; 
+                i64 Offset;
+                TString ConsumerName;
+                i64 ConsumerGeneration;
+            };
+
+            enum EKafkaTxnKqpRequests : ui8 {
+                NO_REQUEST = 0,
+                
+                // This request selects up-to-date producer and consumers states from relevant tables
+                // After this request a check will happen, that no transaction details has expired.
+                SELECT,
+                // This request sends to KQP a command to commit transaction
+                // Both these requests happen in same transaction
+                COMMIT 
+            };
+
+            // we need to exlplicitly specify kqpActorId and txnCoordinatorActorId for unit tests
+            TKafkaTransactionActor(const TString& transactionalId, i64 producerId, i16 producerEpoch, const TString& DatabasePath, const TActorId& kqpActorId, const TActorId& txnCoordinatorActorId) : 
+                TActor<TKafkaTransactionActor>(&TKafkaTransactionActor::StateFunc),
+                TransactionalId(transactionalId),
+                ProducerId(producerId),
+                ProducerEpoch(producerEpoch),
+                DatabasePath(DatabasePath),
+                TxnCoordinatorActorId(txnCoordinatorActorId),
+                KqpActorId(kqpActorId) {};
 
             TStringBuilder LogPrefix() const {
-                return TStringBuilder() << "KafkaTransactionActor";
+                return TStringBuilder() << "KafkaTransactionActor{TransactionalId=" << TransactionalId << "; ProducerId=" << ProducerId << "; ProducerEpoch=" << ProducerEpoch << "}: ";
             }
         
         private:
-            STFUNC(StateWork) {
-                switch (ev->GetTypeRewrite()) {
-                    // will be eimplemented in a future PR
-                    // ToDo: add poison pill handler
+            STFUNC(StateFunc) {
+                try {
+                    switch (ev->GetTypeRewrite()) {
+                        HFunc(TEvKafka::TEvAddPartitionsToTxnRequest, Handle);
+                        HFunc(TEvKafka::TEvAddOffsetsToTxnRequest, Handle);
+                        HFunc(TEvKafka::TEvTxnOffsetCommitRequest, Handle);
+                        HFunc(TEvKafka::TEvEndTxnRequest, Handle);
+                        HFunc(NKqp::TEvKqp::TEvCreateSessionResponse, Handle);
+                        HFunc(NKqp::TEvKqp::TEvQueryResponse, Handle);
+                        HFunc(TEvents::TEvPoison, Handle);
+                    }
+                } catch (const yexception& y) {
+                    KAFKA_LOG_CRIT(TStringBuilder() << "Critical error happened. Reason: " << y.what());
+                    Die(ActorContext());
                 }
             }
+
+            // Kafka API events
+            void Handle(TEvKafka::TEvAddPartitionsToTxnRequest::TPtr& ev, const TActorContext& ctx);
+            void Handle(TEvKafka::TEvAddOffsetsToTxnRequest::TPtr& ev, const TActorContext& ctx);
+            void Handle(TEvKafka::TEvTxnOffsetCommitRequest::TPtr& ev, const TActorContext& ctx);
+            void Handle(TEvKafka::TEvEndTxnRequest::TPtr& ev, const TActorContext& ctx);
+            // KQP events
+            void Handle(NKqp::TEvKqp::TEvCreateSessionResponse::TPtr& ev, const TActorContext& ctx);
+            void Handle(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev, const TActorContext& ctx);
+            
+            // Poison pill
+            void Handle(TEvents::TEvPoison::TPtr& ev, const TActorContext& ctx);
+
+            // Transaction commit logic
+            void StartKqpSession(const TActorContext& ctx);
+            void SendToKqpValidationRequests(const TActorContext& ctx);
+            void SendCommitTxnRequest(const TString& kqpTransactionId);
+
+            // Response senders
+            template<class ErrorResponseType, class EventType>
+            void SendFailResponse(TAutoPtr<TEventHandle<EventType>>& evHandle, EKafkaErrors errorCode, const TString& errorMessage = {});
+            template<class ResponseType, class EventType>
+            void SendOkResponse(TAutoPtr<TEventHandle<EventType>>& evHandle);
+
+            // helper methods
+            void Die(const TActorContext &ctx);
+            template<class EventType>
+            bool ProducerInRequestIsValid(TMessagePtr<EventType> kafkaRequest);
+            TString GetFullTopicPath(const TString& topicName);
+            TString GetYqlWithTablesNames(const TString& templateStr);
+            NYdb::TParams BuildSelectParams();
+            THolder<NKikimr::NKqp::TEvKqp::TEvQueryRequest> BuildCommitTxnRequestToKqp(const TString& kqpTransactionId);
+            void HandleSelectResponse(const NKqp::TEvKqp::TEvQueryResponse& response, const TActorContext& ctx);
+            void HandleCommitResponse(const TActorContext& ctx);
+            TMaybe<TString> GetErrorFromYdbResponse(NKqp::TEvKqp::TEvQueryResponse::TPtr& ev);
+            TMaybe<TProducerState> ParseProducerState(const NKqp::TEvKqp::TEvQueryResponse& response);
+            TMaybe<TString> GetErrorInProducerState(const TMaybe<TProducerState>& producerState);
+            std::unordered_map<TString, i32> ParseConsumersGenerations(const NKqp::TEvKqp::TEvQueryResponse& response);
+            TMaybe<TString> GetErrorInConsumersStates(const std::unordered_map<TString, i32>& consumerGenerationByName);
+            TString GetAsStr(EKafkaTxnKqpRequests request);
+
+            // data from fields below will be sent to KQP on END_TXN request
+            std::unordered_map<TTopicPartition, TPartitionCommit, TopicPartitionHashFn> OffsetsToCommit = {};
+            std::unordered_set<TTopicPartition, TopicPartitionHashFn> PartitionsInTxn = {};
+            const TString TransactionalId;
+            const i64 ProducerId;
+            const i16 ProducerEpoch;
path: ydb/core/kafka_proxy/actors/kafka_transaction_actor.h
user: login: Alek5andr-Kotov
body: TProducerInstanceId?
created_at: 2025-04-22T11:50:22Z
author_association: COLLABORATOR
diff_hunk: @@ -8,20 +10,20 @@ namespace NKafka {
     void TKafkaTransactionsCoordinator::Handle(TEvKafka::TEvSaveTxnProducerRequest::TPtr& ev, const TActorContext& ctx){
         TEvKafka::TEvSaveTxnProducerRequest* request = ev->Get();
 
-        if (ProducersByTransactionalId.contains(request->TransactionalId)) {
-            TProducerState& currentProducerState = ProducersByTransactionalId[request->TransactionalId];
-            TProducerState newProducerState = TProducerState(request->ProducerId, request->ProducerEpoch);
+        auto it = ProducersByTransactionalId.find(request->TransactionalId);
+        if (it != ProducersByTransactionalId.end()) {
+            TEvKafka::TProducerInstanceId& currentProducerState = it->second;
+            const TEvKafka::TProducerInstanceId& newProducerState = request->ProducerState;
 
             if (NewProducerStateIsOutdated(currentProducerState, newProducerState)) {
                 TString message = GetProducerIsOutdatedError(request->TransactionalId, currentProducerState, newProducerState);
                 ctx.Send(ev->Sender, new TEvKafka::TEvSaveTxnProducerResponse(TEvKafka::TEvSaveTxnProducerResponse::EStatus::PRODUCER_FENCED, message));
                 return;
             } 
 
-            currentProducerState.Id = request->ProducerId;
-            currentProducerState.Epoch = request->ProducerEpoch;
+            it->second = std::move(newProducerState);
         } else {
-            ProducersByTransactionalId[request->TransactionalId] = TProducerState(request->ProducerId, request->ProducerEpoch);
+            ProducersByTransactionalId.insert(std::make_pair(request->TransactionalId, request->ProducerState));
path: ydb/core/kafka_proxy/kafka_transactions_coordinator.cpp
user: login: Alek5andr-Kotov
body: Лучше `ProducersByTransactionalId.emplace(request->TransactionalId, request->ProducerState);`
created_at: 2025-04-22T12:09:21Z
author_association: COLLABORATOR
diff_hunk: @@ -8,20 +10,20 @@ namespace NKafka {
     void TKafkaTransactionsCoordinator::Handle(TEvKafka::TEvSaveTxnProducerRequest::TPtr& ev, const TActorContext& ctx){
         TEvKafka::TEvSaveTxnProducerRequest* request = ev->Get();
 
-        if (ProducersByTransactionalId.contains(request->TransactionalId)) {
-            TProducerState& currentProducerState = ProducersByTransactionalId[request->TransactionalId];
-            TProducerState newProducerState = TProducerState(request->ProducerId, request->ProducerEpoch);
+        auto it = ProducersByTransactionalId.find(request->TransactionalId);
+        if (it != ProducersByTransactionalId.end()) {
+            TEvKafka::TProducerInstanceId& currentProducerState = it->second;
+            const TEvKafka::TProducerInstanceId& newProducerState = request->ProducerState;
 
             if (NewProducerStateIsOutdated(currentProducerState, newProducerState)) {
                 TString message = GetProducerIsOutdatedError(request->TransactionalId, currentProducerState, newProducerState);
                 ctx.Send(ev->Sender, new TEvKafka::TEvSaveTxnProducerResponse(TEvKafka::TEvSaveTxnProducerResponse::EStatus::PRODUCER_FENCED, message));
                 return;
             } 
 
-            currentProducerState.Id = request->ProducerId;
-            currentProducerState.Epoch = request->ProducerEpoch;
+            it->second = std::move(newProducerState);
path: ydb/core/kafka_proxy/kafka_transactions_coordinator.cpp
user: login: Alek5andr-Kotov
body: Ты же выше завёл ссылку `currentProducerState`...
created_at: 2025-04-22T12:13:15Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-12T15:54:42Z
body: <!-- status pr=17141, validate PR description status -->
:green_circle: `2025-04-12 15:54:41 UTC` The validation of the Pull Request description is successful.
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-13T06:04:56Z
body: <!-- status pr=17141, preset=relwithdebinfo, run=40218 -->
:white_circle: `2025-04-13 06:04:56 UTC` Pre-commit [check](https://github.com/ydb-platform/ydb/actions/runs/14426743266/job/40456664289) **linux-x86_64-relwithdebinfo** for 2b7a729aed69881d5c920c5102ad132b9cbf40e8 has started.
:white_circle: `2025-04-13 06:05:42 UTC` Artifacts will be uploaded [here](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64/index.html)
:white_circle: `2025-04-13 06:09:09 UTC` ya make is running...
:yellow_circle: `2025-04-13 07:46:39 UTC` Some tests failed, follow the links below. Going to retry failed tests...

<details>


[Test history](https://nebius.testmo.net/automation/runs/view/141794) | [Ya make output](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64/ya_make_output.txt) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64/try_1/test_bloat/tree_map.html)
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/ydb/tree/main/.github/config/muted_ya.txt "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [28963](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64/try_1/ya-test.html) | [26271](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64/try_1/ya-test.html#PASS) | 0 | [4](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64/try_1/ya-test.html#FAIL) | [2570](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64/try_1/ya-test.html#SKIP) | [118](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64/try_1/ya-test.html#MUTE) |

</details>

:white_circle: `2025-04-13 07:48:57 UTC` ya make is running... (failed tests rerun, try 2)
:green_circle: `2025-04-13 08:19:11 UTC` Tests successful.

[Test history](https://nebius.testmo.net/automation/runs/view/141810) | [Ya make output](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64/ya_make_output.txt) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64/try_1/test_bloat/tree_map.html) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64/try_2/test_bloat/tree_map.html)
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/ydb/tree/main/.github/config/muted_ya.txt "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [186 (only retried tests)](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64/try_2/ya-test.html) | [79](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64/try_2/ya-test.html#PASS) | 0 | 0 | 0 | [107](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64/try_2/ya-test.html#MUTE) |

:green_circle: `2025-04-13 08:19:17 UTC` Build successful.
:yellow_circle: `2025-04-13 08:19:37 UTC` ydbd size **2.2 GiB** changed* by **+653.4 KiB**, which is >= 100.0 KiB vs main: **Warning**
|[ydbd size dash](https://datalens.yandex/cu6hzmpaki700)|main: ff4f214225d313fd54806ef83fc9c4016df7ebc8 |merge: 2b7a729aed69881d5c920c5102ad132b9cbf40e8 |diff | diff %|
|:--- | ---: | ---: | ---: | ---: |
|ydbd size|**2 359 626 824** Bytes |**2 360 295 880** Bytes|**+653.4 KiB**|**+0.028%**|
|ydbd stripped size|**493 087 136** Bytes|**493 171 072** Bytes|**+82.0 KiB**|**+0.017%**|

<sup>*please be aware that the difference is based on comparing your commit and the last completed build from the post-commit, check [comparation](https://github.com/ydb-platform/ydb/compare/ff4f214225d313fd54806ef83fc9c4016df7ebc8..2b7a729aed69881d5c920c5102ad132b9cbf40e8)</sup>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-13T06:07:21Z
body: <!-- status pr=17141, preset=release-asan, run=40218 -->
:white_circle: `2025-04-13 06:07:20 UTC` Pre-commit [check](https://github.com/ydb-platform/ydb/actions/runs/14426743266/job/40456664290) **linux-x86_64-release-asan** for 2b7a729aed69881d5c920c5102ad132b9cbf40e8 has started.
:white_circle: `2025-04-13 06:07:35 UTC` Artifacts will be uploaded [here](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64-asan/index.html)
:white_circle: `2025-04-13 06:10:39 UTC` ya make is running...
:yellow_circle: `2025-04-13 08:02:36 UTC` Some tests failed, follow the links below. This fail is not in blocking policy yet Going to retry failed tests...

<details>


[Test history](https://nebius.testmo.net/automation/runs/view/141795) | [Ya make output](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64-asan/ya_make_output.txt) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64-asan/try_1/test_bloat/tree_map.html)
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/ydb/tree/main/.github/config/muted_ya.txt "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [14306](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64-asan/try_1/ya-test.html) | [14130](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64-asan/try_1/ya-test.html#PASS) | 0 | [105](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64-asan/try_1/ya-test.html#FAIL) | [33](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64-asan/try_1/ya-test.html#SKIP) | [38](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64-asan/try_1/ya-test.html#MUTE) |

</details>

:white_circle: `2025-04-13 08:03:49 UTC` ya make is running... (failed tests rerun, try 2)
:yellow_circle: `2025-04-13 08:17:13 UTC` Some tests failed, follow the links below. This fail is not in blocking policy yet Going to retry failed tests...

<details>


[Test history](https://nebius.testmo.net/automation/runs/view/141814) | [Ya make output](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64-asan/ya_make_output.txt) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64-asan/try_1/test_bloat/tree_map.html) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64-asan/try_2/test_bloat/tree_map.html)
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/ydb/tree/main/.github/config/muted_ya.txt "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [245 (only retried tests)](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64-asan/try_2/ya-test.html) | [142](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64-asan/try_2/ya-test.html#PASS) | 0 | [51](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64-asan/try_2/ya-test.html#FAIL) | [18](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64-asan/try_2/ya-test.html#SKIP) | [34](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64-asan/try_2/ya-test.html#MUTE) |

</details>

:white_circle: `2025-04-13 08:17:22 UTC` ya make is running... (failed tests rerun, try 3)
:yellow_circle: `2025-04-13 08:31:37 UTC` Some tests failed, follow the links below. This fail is not in blocking policy yet

[Test history](https://nebius.testmo.net/automation/runs/view/141821) | [Ya make output](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64-asan/ya_make_output.txt) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64-asan/try_1/test_bloat/tree_map.html) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64-asan/try_2/test_bloat/tree_map.html) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64-asan/try_3/test_bloat/tree_map.html)
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/ydb/tree/main/.github/config/muted_ya.txt "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [148 (only retried tests)](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64-asan/try_3/ya-test.html) | [67](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64-asan/try_3/ya-test.html#PASS) | 0 | [44](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64-asan/try_3/ya-test.html#FAIL) | [4](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64-asan/try_3/ya-test.html#SKIP) | [33](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14426743266/ya-x86-64-asan/try_3/ya-test.html#MUTE) |

:green_circle: `2025-04-13 08:31:44 UTC` Build successful.
:yellow_circle: `2025-04-13 08:32:16 UTC` ydbd size **3.9 GiB** changed* by **+1.1 MiB**, which is >= 100.0 KiB vs main: **Warning**
|[ydbd size dash](https://datalens.yandex/cu6hzmpaki700)|main: ff4f214225d313fd54806ef83fc9c4016df7ebc8 |merge: 2b7a729aed69881d5c920c5102ad132b9cbf40e8 |diff | diff %|
|:--- | ---: | ---: | ---: | ---: |
|ydbd size|**4 140 512 560** Bytes |**4 141 626 880** Bytes|**+1.1 MiB**|**+0.027%**|
|ydbd stripped size|**1 429 829 448** Bytes|**1 430 081 480** Bytes|**+246.1 KiB**|**+0.018%**|

<sup>*please be aware that the difference is based on comparing your commit and the last completed build from the post-commit, check [comparation](https://github.com/ydb-platform/ydb/compare/ff4f214225d313fd54806ef83fc9c4016df7ebc8..2b7a729aed69881d5c920c5102ad132b9cbf40e8)</sup>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-14T06:05:07Z
body: <!-- status pr=17141, preset=relwithdebinfo, run=40245 -->
:white_circle: `2025-04-14 06:05:07 UTC` Pre-commit [check](https://github.com/ydb-platform/ydb/actions/runs/14438560893/job/40483788746) **linux-x86_64-relwithdebinfo** for d70b71c4985da2aee833e94a8d901f8de07f1b17 has started.
:white_circle: `2025-04-14 06:05:25 UTC` Artifacts will be uploaded [here](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14438560893/ya-x86-64/index.html)
:white_circle: `2025-04-14 06:08:30 UTC` ya make is running...
:black_circle: `2025-04-14 06:31:45 UTC` Check cancelled
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-14T06:05:22Z
body: <!-- status pr=17141, preset=release-asan, run=40245 -->
:white_circle: `2025-04-14 06:05:21 UTC` Pre-commit [check](https://github.com/ydb-platform/ydb/actions/runs/14438560893/job/40483788752) **linux-x86_64-release-asan** for d70b71c4985da2aee833e94a8d901f8de07f1b17 has started.
:white_circle: `2025-04-14 06:05:36 UTC` Artifacts will be uploaded [here](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14438560893/ya-x86-64-asan/index.html)
:white_circle: `2025-04-14 06:08:48 UTC` ya make is running...
:black_circle: `2025-04-14 06:31:46 UTC` Check cancelled
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-14T06:35:20Z
body: <!-- status pr=17141, preset=release-asan, run=40248 -->
:white_circle: `2025-04-14 06:35:20 UTC` Pre-commit [check](https://github.com/ydb-platform/ydb/actions/runs/14439004705/job/40485028217) **linux-x86_64-release-asan** for cdc2bea73f44ab58de8e0933f27ddd10faa3f83f has started.
:white_circle: `2025-04-14 06:35:34 UTC` Artifacts will be uploaded [here](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14439004705/ya-x86-64-asan/index.html)
:white_circle: `2025-04-14 06:38:40 UTC` ya make is running...
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-14T06:35:45Z
body: <!-- status pr=17141, preset=relwithdebinfo, run=40248 -->
:white_circle: `2025-04-14 06:35:45 UTC` Pre-commit [check](https://github.com/ydb-platform/ydb/actions/runs/14439004705/job/40485028200) **linux-x86_64-relwithdebinfo** for cdc2bea73f44ab58de8e0933f27ddd10faa3f83f has started.
:white_circle: `2025-04-14 06:36:00 UTC` Artifacts will be uploaded [here](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14439004705/ya-x86-64/index.html)
:white_circle: `2025-04-14 06:39:16 UTC` ya make is running...
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-14T08:06:50Z
body: <!-- status pr=17141, preset=release-asan, run=40254 -->
:white_circle: `2025-04-14 08:06:49 UTC` Pre-commit [check](https://github.com/ydb-platform/ydb/actions/runs/14440484588/job/40489640175) **linux-x86_64-release-asan** for 957cd8244e7347d08430a49513ded4c7ea548f08 has started.
:white_circle: `2025-04-14 08:06:53 UTC` Artifacts will be uploaded [here](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64-asan/index.html)
:white_circle: `2025-04-14 08:09:56 UTC` ya make is running...
:yellow_circle: `2025-04-14 09:25:31 UTC` Some tests failed, follow the links below. This fail is not in blocking policy yet Going to retry failed tests...

<details>


[Test history](https://nebius.testmo.net/automation/runs/view/142175) | [Ya make output](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64-asan/ya_make_output.txt) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64-asan/try_1/test_bloat/tree_map.html)
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/ydb/tree/main/.github/config/muted_ya.txt "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [14307](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64-asan/try_1/ya-test.html) | [14181](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64-asan/try_1/ya-test.html#PASS) | 0 | [62](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64-asan/try_1/ya-test.html#FAIL) | [27](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64-asan/try_1/ya-test.html#SKIP) | [37](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64-asan/try_1/ya-test.html#MUTE) |

</details>

:white_circle: `2025-04-14 09:26:49 UTC` ya make is running... (failed tests rerun, try 2)
:yellow_circle: `2025-04-14 09:42:13 UTC` Some tests failed, follow the links below. This fail is not in blocking policy yet Going to retry failed tests...

<details>


[Test history](https://nebius.testmo.net/automation/runs/view/142211) | [Ya make output](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64-asan/ya_make_output.txt) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64-asan/try_1/test_bloat/tree_map.html) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64-asan/try_2/test_bloat/tree_map.html)
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/ydb/tree/main/.github/config/muted_ya.txt "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [175 (only retried tests)](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64-asan/try_2/ya-test.html) | [103](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64-asan/try_2/ya-test.html#PASS) | 0 | [35](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64-asan/try_2/ya-test.html#FAIL) | [4](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64-asan/try_2/ya-test.html#SKIP) | [33](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64-asan/try_2/ya-test.html#MUTE) |

</details>

:white_circle: `2025-04-14 09:42:21 UTC` ya make is running... (failed tests rerun, try 3)
:yellow_circle: `2025-04-14 09:54:38 UTC` Some tests failed, follow the links below. This fail is not in blocking policy yet

[Test history](https://nebius.testmo.net/automation/runs/view/142225) | [Ya make output](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64-asan/ya_make_output.txt) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64-asan/try_1/test_bloat/tree_map.html) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64-asan/try_2/test_bloat/tree_map.html) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64-asan/try_3/test_bloat/tree_map.html)
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/ydb/tree/main/.github/config/muted_ya.txt "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [106 (only retried tests)](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64-asan/try_3/ya-test.html) | [36](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64-asan/try_3/ya-test.html#PASS) | 0 | [36](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64-asan/try_3/ya-test.html#FAIL) | [2](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64-asan/try_3/ya-test.html#SKIP) | [32](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64-asan/try_3/ya-test.html#MUTE) |

:green_circle: `2025-04-14 09:54:45 UTC` Build successful.
:yellow_circle: `2025-04-14 09:55:16 UTC` ydbd size **3.9 GiB** changed* by **+1.1 MiB**, which is >= 100.0 KiB vs main: **Warning**
|[ydbd size dash](https://datalens.yandex/cu6hzmpaki700)|main: ade5e4f27b701bfc46936c0b2b292ce2ae5f1e97 |merge: 957cd8244e7347d08430a49513ded4c7ea548f08 |diff | diff %|
|:--- | ---: | ---: | ---: | ---: |
|ydbd size|**4 140 616 568** Bytes |**4 141 729 944** Bytes|**+1.1 MiB**|**+0.027%**|
|ydbd stripped size|**1 429 856 712** Bytes|**1 430 105 416** Bytes|**+242.9 KiB**|**+0.017%**|

<sup>*please be aware that the difference is based on comparing your commit and the last completed build from the post-commit, check [comparation](https://github.com/ydb-platform/ydb/compare/ade5e4f27b701bfc46936c0b2b292ce2ae5f1e97..957cd8244e7347d08430a49513ded4c7ea548f08)</sup>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-14T08:08:48Z
body: <!-- status pr=17141, preset=relwithdebinfo, run=40254 -->
:white_circle: `2025-04-14 08:08:47 UTC` Pre-commit [check](https://github.com/ydb-platform/ydb/actions/runs/14440484588/job/40489640195) **linux-x86_64-relwithdebinfo** for 957cd8244e7347d08430a49513ded4c7ea548f08 has started.
:white_circle: `2025-04-14 08:08:51 UTC` Artifacts will be uploaded [here](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64/index.html)
:white_circle: `2025-04-14 08:11:53 UTC` ya make is running...
:yellow_circle: `2025-04-14 09:24:11 UTC` Some tests failed, follow the links below. Going to retry failed tests...

<details>


[Test history](https://nebius.testmo.net/automation/runs/view/142176) | [Ya make output](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64/ya_make_output.txt) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64/try_1/test_bloat/tree_map.html)
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/ydb/tree/main/.github/config/muted_ya.txt "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [28964](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64/try_1/ya-test.html) | [26272](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64/try_1/ya-test.html#PASS) | 0 | [6](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64/try_1/ya-test.html#FAIL) | [2570](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64/try_1/ya-test.html#SKIP) | [116](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64/try_1/ya-test.html#MUTE) |

</details>

:white_circle: `2025-04-14 09:26:46 UTC` ya make is running... (failed tests rerun, try 2)
:yellow_circle: `2025-04-14 09:38:13 UTC` Some tests failed, follow the links below. Going to retry failed tests...

<details>


[Test history](https://nebius.testmo.net/automation/runs/view/142210) | [Ya make output](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64/ya_make_output.txt) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64/try_1/test_bloat/tree_map.html) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64/try_2/test_bloat/tree_map.html)
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/ydb/tree/main/.github/config/muted_ya.txt "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [190 (only retried tests)](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64/try_2/ya-test.html) | [84](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64/try_2/ya-test.html#PASS) | 0 | [3](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64/try_2/ya-test.html#FAIL) | 0 | [103](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64/try_2/ya-test.html#MUTE) |

</details>

:white_circle: `2025-04-14 09:38:23 UTC` ya make is running... (failed tests rerun, try 3)
:green_circle: `2025-04-14 09:50:04 UTC` Tests successful.

[Test history](https://nebius.testmo.net/automation/runs/view/142222) | [Ya make output](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64/ya_make_output.txt) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64/try_1/test_bloat/tree_map.html) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64/try_2/test_bloat/tree_map.html) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64/try_3/test_bloat/tree_map.html)
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/ydb/tree/main/.github/config/muted_ya.txt "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [147 (only retried tests)](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64/try_3/ya-test.html) | [45](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64/try_3/ya-test.html#PASS) | 0 | 0 | 0 | [102](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14440484588/ya-x86-64/try_3/ya-test.html#MUTE) |

:green_circle: `2025-04-14 09:50:14 UTC` Build successful.
:yellow_circle: `2025-04-14 09:50:36 UTC` ydbd size **2.2 GiB** changed* by **+659.1 KiB**, which is >= 100.0 KiB vs main: **Warning**
|[ydbd size dash](https://datalens.yandex/cu6hzmpaki700)|main: ade5e4f27b701bfc46936c0b2b292ce2ae5f1e97 |merge: 957cd8244e7347d08430a49513ded4c7ea548f08 |diff | diff %|
|:--- | ---: | ---: | ---: | ---: |
|ydbd size|**2 359 676 280** Bytes |**2 360 351 176** Bytes|**+659.1 KiB**|**+0.029%**|
|ydbd stripped size|**493 094 080** Bytes|**493 182 272** Bytes|**+86.1 KiB**|**+0.018%**|

<sup>*please be aware that the difference is based on comparing your commit and the last completed build from the post-commit, check [comparation](https://github.com/ydb-platform/ydb/compare/ade5e4f27b701bfc46936c0b2b292ce2ae5f1e97..957cd8244e7347d08430a49513ded4c7ea548f08)</sup>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-20T12:59:04Z
body: <!-- status pr=17141, preset=relwithdebinfo, run=40920 -->
:white_circle: `2025-04-20 12:59:03 UTC` Pre-commit [check](https://github.com/ydb-platform/ydb/actions/runs/14559700039/job/40841339858) **linux-x86_64-relwithdebinfo** for a04762f5ccb5d3edc8ca29d5b794ce5194395123 has started.
:white_circle: `2025-04-20 12:59:51 UTC` Artifacts will be uploaded [here](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64/index.html)
:white_circle: `2025-04-20 13:03:28 UTC` ya make is running...
:yellow_circle: `2025-04-20 14:47:46 UTC` Some tests failed, follow the links below. Going to retry failed tests...

<details>


[Test history](https://nebius.testmo.net/automation/runs/view/145975) | [Ya make output](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64/ya_make_output.txt) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64/try_1/test_bloat/tree_map.html)
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/ydb/tree/main/.github/config/muted_ya.txt "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [29234](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64/try_1/ya-test.html) | [26553](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64/try_1/ya-test.html#PASS) | 0 | [5](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64/try_1/ya-test.html#FAIL) | [2569](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64/try_1/ya-test.html#SKIP) | [107](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64/try_1/ya-test.html#MUTE) |

</details>

:white_circle: `2025-04-20 14:50:09 UTC` ya make is running... (failed tests rerun, try 2)
:green_circle: `2025-04-20 15:02:14 UTC` Tests successful.

[Test history](https://nebius.testmo.net/automation/runs/view/145993) | [Ya make output](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64/ya_make_output.txt) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64/try_1/test_bloat/tree_map.html) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64/try_2/test_bloat/tree_map.html)
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/ydb/tree/main/.github/config/muted_ya.txt "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [170 (only retried tests)](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64/try_2/ya-test.html) | [74](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64/try_2/ya-test.html#PASS) | 0 | 0 | 0 | [96](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64/try_2/ya-test.html#MUTE) |

:green_circle: `2025-04-20 15:02:20 UTC` Build successful.
:yellow_circle: `2025-04-20 15:02:38 UTC` ydbd size **2.2 GiB** changed* by **+664.1 KiB**, which is >= 100.0 KiB vs main: **Warning**
|[ydbd size dash](https://datalens.yandex/cu6hzmpaki700)|main: c590a014000b0fa441afb90e28d3748d09c1f5ab |merge: a04762f5ccb5d3edc8ca29d5b794ce5194395123 |diff | diff %|
|:--- | ---: | ---: | ---: | ---: |
|ydbd size|**2 363 960 776** Bytes |**2 364 640 840** Bytes|**+664.1 KiB**|**+0.029%**|
|ydbd stripped size|**493 946 624** Bytes|**494 032 640** Bytes|**+84.0 KiB**|**+0.017%**|

<sup>*please be aware that the difference is based on comparing your commit and the last completed build from the post-commit, check [comparation](https://github.com/ydb-platform/ydb/compare/c590a014000b0fa441afb90e28d3748d09c1f5ab..a04762f5ccb5d3edc8ca29d5b794ce5194395123)</sup>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-20T13:01:24Z
body: <!-- status pr=17141, preset=release-asan, run=40920 -->
:white_circle: `2025-04-20 13:01:24 UTC` Pre-commit [check](https://github.com/ydb-platform/ydb/actions/runs/14559700039/job/40841339859) **linux-x86_64-release-asan** for a04762f5ccb5d3edc8ca29d5b794ce5194395123 has started.
:white_circle: `2025-04-20 13:01:38 UTC` Artifacts will be uploaded [here](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64-asan/index.html)
:white_circle: `2025-04-20 13:04:42 UTC` ya make is running...
:yellow_circle: `2025-04-20 14:56:19 UTC` Some tests failed, follow the links below. This fail is not in blocking policy yet Going to retry failed tests...

<details>


[Test history](https://nebius.testmo.net/automation/runs/view/145976) | [Ya make output](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64-asan/ya_make_output.txt) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64-asan/try_1/test_bloat/tree_map.html)
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/ydb/tree/main/.github/config/muted_ya.txt "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [14557](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64-asan/try_1/ya-test.html) | [14430](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64-asan/try_1/ya-test.html#PASS) | 0 | [74](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64-asan/try_1/ya-test.html#FAIL) | [28](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64-asan/try_1/ya-test.html#SKIP) | [25](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64-asan/try_1/ya-test.html#MUTE) |

</details>

:white_circle: `2025-04-20 14:57:30 UTC` ya make is running... (failed tests rerun, try 2)
:yellow_circle: `2025-04-20 15:10:13 UTC` Some tests failed, follow the links below. This fail is not in blocking policy yet Going to retry failed tests...

<details>


[Test history](https://nebius.testmo.net/automation/runs/view/145994) | [Ya make output](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64-asan/ya_make_output.txt) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64-asan/try_1/test_bloat/tree_map.html) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64-asan/try_2/test_bloat/tree_map.html)
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/ydb/tree/main/.github/config/muted_ya.txt "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [165 (only retried tests)](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64-asan/try_2/ya-test.html) | [103](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64-asan/try_2/ya-test.html#PASS) | 0 | [34](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64-asan/try_2/ya-test.html#FAIL) | [6](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64-asan/try_2/ya-test.html#SKIP) | [22](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64-asan/try_2/ya-test.html#MUTE) |

</details>

:white_circle: `2025-04-20 15:10:21 UTC` ya make is running... (failed tests rerun, try 3)
:yellow_circle: `2025-04-20 15:22:32 UTC` Some tests failed, follow the links below. This fail is not in blocking policy yet

[Test history](https://nebius.testmo.net/automation/runs/view/145999) | [Ya make output](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64-asan/ya_make_output.txt) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64-asan/try_1/test_bloat/tree_map.html) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64-asan/try_2/test_bloat/tree_map.html) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64-asan/try_3/test_bloat/tree_map.html)
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/ydb/tree/main/.github/config/muted_ya.txt "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [83 (only retried tests)](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64-asan/try_3/ya-test.html) | [23](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64-asan/try_3/ya-test.html#PASS) | 0 | [34](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64-asan/try_3/ya-test.html#FAIL) | [5](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64-asan/try_3/ya-test.html#SKIP) | [21](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14559700039/ya-x86-64-asan/try_3/ya-test.html#MUTE) |

:green_circle: `2025-04-20 15:22:39 UTC` Build successful.
:yellow_circle: `2025-04-20 15:23:07 UTC` ydbd size **3.9 GiB** changed* by **+1.1 MiB**, which is >= 100.0 KiB vs main: **Warning**
|[ydbd size dash](https://datalens.yandex/cu6hzmpaki700)|main: c590a014000b0fa441afb90e28d3748d09c1f5ab |merge: a04762f5ccb5d3edc8ca29d5b794ce5194395123 |diff | diff %|
|:--- | ---: | ---: | ---: | ---: |
|ydbd size|**4 148 273 440** Bytes |**4 149 407 368** Bytes|**+1.1 MiB**|**+0.027%**|
|ydbd stripped size|**1 432 101 400** Bytes|**1 432 359 032** Bytes|**+251.6 KiB**|**+0.018%**|

<sup>*please be aware that the difference is based on comparing your commit and the last completed build from the post-commit, check [comparation](https://github.com/ydb-platform/ydb/compare/c590a014000b0fa441afb90e28d3748d09c1f5ab..a04762f5ccb5d3edc8ca29d5b794ce5194395123)</sup>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-22T11:47:53Z
body: <!-- status pr=17141, preset=release-asan, run=41099 -->
:white_circle: `2025-04-22 11:47:52 UTC` Pre-commit [check](https://github.com/ydb-platform/ydb/actions/runs/14593959282/job/40935335492) **linux-x86_64-release-asan** for 753ad24b06618d606c95d70aba175efc628eaab2 has started.
:white_circle: `2025-04-22 11:48:08 UTC` Artifacts will be uploaded [here](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14593959282/ya-x86-64-asan/index.html)
:white_circle: `2025-04-22 11:51:22 UTC` ya make is running...
:black_circle: `2025-04-22 13:31:16 UTC` Check cancelled
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-22T11:58:34Z
body: <!-- status pr=17141, preset=relwithdebinfo, run=41099 -->
:white_circle: `2025-04-22 11:58:33 UTC` Pre-commit [check](https://github.com/ydb-platform/ydb/actions/runs/14593959282/job/40935335488) **linux-x86_64-relwithdebinfo** for 753ad24b06618d606c95d70aba175efc628eaab2 has started.
:white_circle: `2025-04-22 11:58:48 UTC` Artifacts will be uploaded [here](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14593959282/ya-x86-64/index.html)
:white_circle: `2025-04-22 12:02:02 UTC` ya make is running...
:yellow_circle: `2025-04-22 13:31:39 UTC` ydbd size **2.2 GiB** changed* by **+671.3 KiB**, which is >= 100.0 KiB vs main: **Warning**
|[ydbd size dash](https://datalens.yandex/cu6hzmpaki700)|main: c45eac273e9ee693ff9553abd013e24ad60c3f1e |merge: 753ad24b06618d606c95d70aba175efc628eaab2 |diff | diff %|
|:--- | ---: | ---: | ---: | ---: |
|ydbd size|**2 352 323 424** Bytes |**2 353 010 832** Bytes|**+671.3 KiB**|**+0.029%**|
|ydbd stripped size|**493 847 008** Bytes|**493 934 080** Bytes|**+85.0 KiB**|**+0.018%**|

<sup>*please be aware that the difference is based on comparing your commit and the last completed build from the post-commit, check [comparation](https://github.com/ydb-platform/ydb/compare/c45eac273e9ee693ff9553abd013e24ad60c3f1e..753ad24b06618d606c95d70aba175efc628eaab2)</sup>
:black_circle: `2025-04-22 13:31:41 UTC` Check cancelled
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-22T13:33:03Z
body: <!-- status pr=17141, preset=release-asan, run=41128 -->
:white_circle: `2025-04-22 13:33:02 UTC` Pre-commit [check](https://github.com/ydb-platform/ydb/actions/runs/14596068970/job/40942256822) **linux-x86_64-release-asan** for 3535f32d48abfbc66366d026c241f4feae38b48e has started.
:white_circle: `2025-04-22 13:33:17 UTC` Artifacts will be uploaded [here](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64-asan/index.html)
:white_circle: `2025-04-22 13:36:26 UTC` ya make is running...
:yellow_circle: `2025-04-22 15:25:29 UTC` Some tests failed, follow the links below. This fail is not in blocking policy yet Going to retry failed tests...

<details>


[Test history](https://nebius.testmo.net/automation/runs/view/147218) | [Ya make output](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64-asan/ya_make_output.txt) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64-asan/try_1/test_bloat/tree_map.html)
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/ydb/tree/main/.github/config/muted_ya.txt "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [14651](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64-asan/try_1/ya-test.html) | [14470](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64-asan/try_1/ya-test.html#PASS) | 0 | [109](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64-asan/try_1/ya-test.html#FAIL) | [48](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64-asan/try_1/ya-test.html#SKIP) | [24](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64-asan/try_1/ya-test.html#MUTE) |

</details>

:white_circle: `2025-04-22 15:26:47 UTC` ya make is running... (failed tests rerun, try 2)
:yellow_circle: `2025-04-22 15:40:13 UTC` Some tests failed, follow the links below. This fail is not in blocking policy yet Going to retry failed tests...

<details>


[Test history](https://nebius.testmo.net/automation/runs/view/147308) | [Ya make output](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64-asan/ya_make_output.txt) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64-asan/try_1/test_bloat/tree_map.html) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64-asan/try_2/test_bloat/tree_map.html)
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/ydb/tree/main/.github/config/muted_ya.txt "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [221 (only retried tests)](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64-asan/try_2/ya-test.html) | [140](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64-asan/try_2/ya-test.html#PASS) | 0 | [38](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64-asan/try_2/ya-test.html#FAIL) | [21](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64-asan/try_2/ya-test.html#SKIP) | [22](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64-asan/try_2/ya-test.html#MUTE) |

</details>

:white_circle: `2025-04-22 15:40:21 UTC` ya make is running... (failed tests rerun, try 3)
:yellow_circle: `2025-04-22 15:52:41 UTC` Some tests failed, follow the links below. This fail is not in blocking policy yet

[Test history](https://nebius.testmo.net/automation/runs/view/147317) | [Ya make output](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64-asan/ya_make_output.txt) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64-asan/try_1/test_bloat/tree_map.html) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64-asan/try_2/test_bloat/tree_map.html) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64-asan/try_3/test_bloat/tree_map.html)
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/ydb/tree/main/.github/config/muted_ya.txt "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [105 (only retried tests)](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64-asan/try_3/ya-test.html) | [39](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64-asan/try_3/ya-test.html#PASS) | 0 | [36](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64-asan/try_3/ya-test.html#FAIL) | [8](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64-asan/try_3/ya-test.html#SKIP) | [22](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64-asan/try_3/ya-test.html#MUTE) |

:green_circle: `2025-04-22 15:52:48 UTC` Build successful.
:yellow_circle: `2025-04-22 15:53:18 UTC` ydbd size **3.8 GiB** changed* by **+1.1 MiB**, which is >= 100.0 KiB vs main: **Warning**
|[ydbd size dash](https://datalens.yandex/cu6hzmpaki700)|main: 73bc54207426e222268e0589263281497de02187 |merge: 3535f32d48abfbc66366d026c241f4feae38b48e |diff | diff %|
|:--- | ---: | ---: | ---: | ---: |
|ydbd size|**4 132 399 536** Bytes |**4 133 548 272** Bytes|**+1.1 MiB**|**+0.028%**|
|ydbd stripped size|**1 430 872 280** Bytes|**1 431 133 112** Bytes|**+254.7 KiB**|**+0.018%**|

<sup>*please be aware that the difference is based on comparing your commit and the last completed build from the post-commit, check [comparation](https://github.com/ydb-platform/ydb/compare/73bc54207426e222268e0589263281497de02187..3535f32d48abfbc66366d026c241f4feae38b48e)</sup>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-22T13:35:40Z
body: <!-- status pr=17141, preset=relwithdebinfo, run=41128 -->
:white_circle: `2025-04-22 13:35:40 UTC` Pre-commit [check](https://github.com/ydb-platform/ydb/actions/runs/14596068970/job/40942256811) **linux-x86_64-relwithdebinfo** for 3535f32d48abfbc66366d026c241f4feae38b48e has started.
:white_circle: `2025-04-22 13:35:44 UTC` Artifacts will be uploaded [here](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64/index.html)
:white_circle: `2025-04-22 13:39:00 UTC` ya make is running...
:yellow_circle: `2025-04-22 15:27:03 UTC` Some tests failed, follow the links below. Going to retry failed tests...

<details>


[Test history](https://nebius.testmo.net/automation/runs/view/147221) | [Ya make output](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64/ya_make_output.txt) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64/try_1/test_bloat/tree_map.html)
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/ydb/tree/main/.github/config/muted_ya.txt "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [29418](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64/try_1/ya-test.html) | [26726](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64/try_1/ya-test.html#PASS) | 0 | [8](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64/try_1/ya-test.html#FAIL) | [2582](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64/try_1/ya-test.html#SKIP) | [102](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64/try_1/ya-test.html#MUTE) |

</details>

:white_circle: `2025-04-22 15:29:34 UTC` ya make is running... (failed tests rerun, try 2)
:green_circle: `2025-04-22 15:41:26 UTC` Tests successful.

[Test history](https://nebius.testmo.net/automation/runs/view/147310) | [Ya make output](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64/ya_make_output.txt) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64/try_1/test_bloat/tree_map.html) | [Test bloat](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64/try_2/test_bloat/tree_map.html)
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/ydb/tree/main/.github/config/muted_ya.txt "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [173 (only retried tests)](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64/try_2/ya-test.html) | [80](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64/try_2/ya-test.html#PASS) | 0 | 0 | 0 | [93](https://storage.yandexcloud.net/ydb-gh-logs/ydb-platform/ydb/PR-check/14596068970/ya-x86-64/try_2/ya-test.html#MUTE) |

:green_circle: `2025-04-22 15:41:33 UTC` Build successful.
:yellow_circle: `2025-04-22 15:41:54 UTC` ydbd size **2.2 GiB** changed* by **+669.4 KiB**, which is >= 100.0 KiB vs main: **Warning**
|[ydbd size dash](https://datalens.yandex/cu6hzmpaki700)|main: d7f39a11c7b50063a903340c74965d762d7024e7 |merge: 3535f32d48abfbc66366d026c241f4feae38b48e |diff | diff %|
|:--- | ---: | ---: | ---: | ---: |
|ydbd size|**2 352 326 568** Bytes |**2 353 012 040** Bytes|**+669.4 KiB**|**+0.029%**|
|ydbd stripped size|**493 847 392** Bytes|**493 934 272** Bytes|**+84.8 KiB**|**+0.018%**|

<sup>*please be aware that the difference is based on comparing your commit and the last completed build from the post-commit, check [comparation](https://github.com/ydb-platform/ydb/compare/d7f39a11c7b50063a903340c74965d762d7024e7..3535f32d48abfbc66366d026c241f4feae38b48e)</sup>
author_association: NONE