Skip to content

feature: Transaction retrier for YDB #1127

@Gazizonoki

Description

@Gazizonoki

Суть изменений: добавить ретраер над транзакцией в userver, создание транзакции без ретраера - закопать, чтобы даже не было возможности выстрелить в ногу.

Было

/// settings.hpp:

enum class TransactionMode { kSerializableRW, kOnlineRO, kStaleRO };

/// table.hpp:

class TableClient final {
public:

Transaction Begin(utils::StringLiteral transaction_name, OperationSettings settings = {});

Transaction Begin(DynamicTransactionName transaction_name, OperationSettings settings = {});

Transaction Begin(utils::StringLiteral transaction_name, TransactionMode tx_mode);

};

Стало

/// settings.hpp:

enum class TransactionMode { kSerializableRW, kOnlineRO, kStaleRO, kImplicitTx };

struct RequestSettings final {
    std::chrono::milliseconds timeout_ms{0};

    std::string trace_id{};
};

using ExecuteSettings = RequestSettings;
using CommitSettings = RequestSettings;
using RollbackSettings = RequestSettings;

struct RetryTxSettings final {
    TransactionMode tx_mode{TransactionMode::kImplicitTx};
    std::chrono::milliseconds timeout_ms{0};
    std::uint32_t retries{10};
    bool is_idempotent{false};

    CommitSettings commit_settings;
    RollbackSettings rollback_settings;
};

/// transaction.hpp

class TxActor {
public:
    /// Execute a single data query as a part of the transaction. Query parameters
    /// are passed in `Args` as "string key - value" pairs:
    ///
    /// @code
    /// client.Execute(query, "name1", value1, "name2", value2, ...);
    /// @endcode
    ///
    /// Use ydb::PreparedArgsBuilder for storing a generic buffer of query params
    /// if needed.
    ///
    /// @{
    template <typename... Args>
    ExecuteResponse Execute(const Query& query, Args&&... args);

    template <typename... Args>
    ExecuteResponse Execute(ExecuteSettings settings, const Query& query, Args&&... args);

    ExecuteResponse Execute(ExecuteSettings settings, const Query& query, PreparedArgsBuilder&& builder);
    /// @}
};

enum class TxAction {
    kCommit,
    kRollback,
};

using RetryTxFunction = std::function<TxAction(TxActor&)>;

/// table.hpp:

class TableClient final {
public:

/// объявить deprecated, правильно создавать транзакции только через ретраер
/// @{
Transaction Begin(utils::StringLiteral transaction_name, OperationSettings settings = {});

Transaction Begin(DynamicTransactionName transaction_name, OperationSettings settings = {});

Transaction Begin(utils::StringLiteral transaction_name, TransactionMode tx_mode);
/// @}

/// @{
void RetryTx(utils::StringLiteral transaction_name, RetryTxSettings retry_settings, RetryTxFunction fn)

void RetryTx(DynamicTransactionName transaction_name, RetryTxSettings retry_settings, RetryTxFunction fn)
/// @}

};

Пример:

client.RetryTx("transaction_name", {
        .retries = 3,
        .timeout_ms = std::chrono::seconds(10),
        .tx_mode = TransactionMode::kSerializableRW,
        .is_idempotent = true,
    },
    [](TxActor& tx) {
        tx.Execute("SELECT * FROM users WHERE id = $id", "$id", 1);
        return TxAction::kCommit;
    }
);

Теперь интерактивные транзакции будут предоставляться через RetryTx ручку. Пользователь передает всю лямбду со своей транзакцией в ретраер, и при ошибках она будет ретраится.

timeout_ms - таймаут на все ретраи вместе. В userver он будет передан в RetryQuery, который сам гарантирует его соблюдение.

TransactionMode::kImplicitTx будет добавлен после поддержки QueryService в userver, если не успеем, дефолт временно будет kSerializableRW, потом поменяем на kImplicitTx.

Метод RetryTx будет вызывать метод RetryQuery из SDK. На полученной сессии будет создана транзакция, обернута в userver'ный класс и передан пользователю. Идейно это будет выглядеть так:

void RetryTx(utils::StringLiteral transaction_name, RetryTxSettings retry_settings, RetryFunction fn) {
    client.RetryQuery([&](NYdb::NQuery::TSession session) -> NYdb::TStatus {
        try {
            auto tx = session.BeginTransaction(retry_settings);
            Transaction tx(table_client_, std::move(tx), transaction_name);
            auto tx_action = fn(tx);
            ...
        } catch (const YdbResponseError& e) {
            return e.GetStatus();
        }
    }, PrepareRetrySettings(retry_settings));
}

Пользователь в своей лямбде должен вернуть, хочет ли он сделать коммит или роллбек. После этого работа ретраера завершится. Отдельные ручки Commit и Rollback мы специально убираем.

Ошибки будут обрабатываться через исключения. Если это YdbResponseError - мы делаем (или не делаем) ретрай на основе статуса, просто передаем его в SDK'шный ретраер. Если это неизвестное исключение - завершаем работу ретраера, это ошибка в пользовательской логике.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions