Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 16 из 25 ~20 мин +50 XP

Транзакции в ent

Тринадцатый урок обещал: составные операции нужно завернуть в транзакции, четырнадцатый показал, что бывает, когда «дети, потом родитель» разъезжаются по разным коммитам. Сегодня — ent транзакции: один вызов client.Tx(ctx) даёт транзакционный клиент, сгенерированный ent/tx.go, с теми же билдерами User, Group, Post, но на одном соединении и одним судьёй — Commit или Rollback. Разберём контракт обоих методов и идиому defer, соберём хелпер WithTx, который делит код на «внутри транзакции» и «снаружи», передадим транзакцию чужому коду через tx.Client(), отрегулируем изоляцию через BeginTx и sql.TxOptions и заглянем в вложенные транзакции через savepoints. По канону курса: анна, борис, вера, группа go-ninja и счётчик posts_count из двенадцатого урока.

Содержание урока · 12 разделов

Маршрут урока — от первого client.Tx до вложенных savepoints. Сначала — зачем это нужно: сцена с постом и счётчиком из тринадцатого урока, где две отдельные мутации обязаны жить или умереть вместе; здесь же — что транзакция даёт и чего не обещает. Затем — механика: client.Tx(ctx), сгенерированный файл ent/tx.go и контракт Commit/Rollback, включая sql.ErrTxDone. Третья часть — дисциплина: идиома defer и хелпер WithTx, стандарт де-факто из документации ent. Четвёртая — интеграция: tx.Client() для функций, чьи подписи принимают *ent.Client, With-цепочки из одиннадцатого урока внутри транзакции и BeginTx с sql.TxOptions. Пятая — вложенные транзакции: tx.Tx(ctx) и savepoints, где подоперация может откатиться, не роняя внешнюю. Финиш — ошибки: ConstraintError из четырнадцатого урока становится штатной веткой, а «полуудалённое состояние» исчезает из вашего словаря. Если вы искали по запросу «ent tx» — всё это здесь.

Зачем транзакции, когда ent уже «сам всё делает»#

Восьмой урок заметил: мутация с рёбрами уже открывает транзакцию сама — внутренний mayTx оборачивает INSERT узла и вставки в join-таблицу одним блоком. Но эта автоматика покрывает ровно одну мутацию. Две отдельные мутации — пост и инкремент счётчика, удаление дружб и удаление пользователя, создание группы и вступление в неё — в транзакции не живут: между их Exec природа может вставить что угодно, от падения процесса до сетевого сбоя. Итог известен из четырнадцатого урока: пост есть, счётчик не сдвинулся; дружбы стёрты, пользователь жив — полуудалённое состояние, которое не чинится ретраем, потому что часть операций уже необратима. Транзакция на уровне SQL — это BEGIN, набор стейтментов и либо COMMIT, либо ROLLBACK: атомарность, консистентность и изоляция на стороне базы. ent транзакции не переизобретают это — они дают типобезопасный доступ: сгенерированный tx-клиент, те же билдеры, компилятор следит за тем, чтобы вы не позвали клиентские билдеры там, где открыта транзакция, — ну, почти следит, и о дыре в этом заборе — в конце урока.

client.Tx: транзакционный клиент

GO
// пост + счётчик — атомарно:
tx, err := client.Tx(ctx)
if err != nil {
        return err
}

if _, err = tx.Post.Create().
        SetTitle("про рёбра").
        SetBody("...").
        SetAuthorID(1).
        Save(ctx); err != nil {
        return err // до Commit база не увидит ничего
}
if err = tx.User.UpdateOneID(1).
        AddPostsCount(1).
        Exec(ctx); err != nil {
        return err
}

return tx.Commit()

client.Tx(ctx) возвращает (*ent.Tx, error) — из сгенерированного ent/tx.go. У Tx те же поля: tx.Post, tx.User — и те же билдеры, что у client, но драйвер у них один: соединение, открытое для этой транзакции. AddPostsCount — тот же мутатор из тринадцатого урока; разница в окружении: обе мутации теперь либо зафиксируются вместе на Commit, либо исчезнут вместе при откате.

Проверка: что такое *ent.Tx

Что возвращает client.Tx(ctx) и чем tx.Post отличается от client.Post?

Контракт Commit и Rollback#

Два метода *ent.Tx решают судьбу транзакции, и у обоих контракты стоит знать наизусть. tx.Commit() возвращает error: успех — nil, и все мутации зафиксированы; ошибка может прийти и здесь — констрейнты с отложенной проверкой, обрыв соединения, конфликт изоляции. tx.Rollback() тоже возвращает error — и это главное источник путаницы: rollback после успешного Commit вернёт ошибку уже завершённой транзакции, из database/sql она приходит как sql.ErrTxDone. Это не баг и не паника: вы просите отменить то, чего уже нет. Обратный порядок безопаснее по данным: Rollback после неудачного Commit может вытащить соединение из комы, если часть стейтментов исполнилась, а фиксировать нечего. Ещё одно свойство, о котором забывают: транзакция держит соединение из пула. Забытый Tx без Commit и без Rollback — соединение, которое не вернётся в пул до таймаута; три таких утечки — и приложение стоит, хотя процесс здоров. Отсюда железное правило: у каждой транзакции либо Commit, либо Rollback обязаны быть исполнены — на любом пути, включая панику.

Идиома: defer tx.Rollback

GO
tx, err := client.Tx(ctx)
if err != nil {
        return err
}
defer func() { _ = tx.Rollback() }()

// ... бизнес-код, любые return внутри ...

return tx.Commit()

Классическая идиома: defer с Rollback страхует все пути. На успешном пути Commit уже зафиксировал транзакцию, и отложенный Rollback вернёт sql.ErrTxDone — её осознанно глотают подчёркиванием. На любом ошибочном или паническом пути rollback сработает сам и вернёт соединение в пул. Заметьте: Commit здесь последний return — defer исполняется после него, но не мешает: ErrTxDone безвредна.

Проверка: defer после Commit

Код: tx открыта, defer func() { _ = tx.Rollback() }(), бизнес-код прошёл, return tx.Commit() вернул nil. Что исполнилось и что вернулось?

WithTx: хелпер, который делит код на «внутри» и «снаружи»#

Идиома с defer требует дисциплины в каждом месте: ошибку бизнес-кода надо не потерять, rollback не забыть, панику не упустить. Документация ent предлагает шаблон, который закрывает это раз и навсегда — хелпер WithTx. Он принимает клиент и функцию fn func(tx *ent.Tx) error; всё, что fn делает, живёт внутри транзакции, всё, что после её возврата, — снаружи. Возврат nil — Commit, возврат ошибки — Rollback с сохранением оригинальной ошибки: заметьте, как она оборачивается, если откат сам споткнулся, — ошибка мутации для вас ценнее ошибки отката, и терять её нельзя. Тот же принцип с паникой: recover поднимает её после отката — соединение вернётся в пул, а стектрейс останется честным. С таким хелпером ent транзакции становятся декларативными: читатель функции сразу видит границы атомарного блока, а не ищет парные Commit/Rollback по коду.

WithTx: стандартный хелпер

GO
func WithTx(ctx context.Context, client *ent.Client,
        fn func(tx *ent.Tx) error) error {
        tx, err := client.Tx(ctx)
        if err != nil {
                return err
        }
        defer func() {
                if v := recover(); v != nil {
                        _ = tx.Rollback()
                        panic(v) // откатились — и подняли честно
                }
        }()
        if err := fn(tx); err != nil {
                if rerr := tx.Rollback(); rerr != nil {
                        // ошибка мутации важнее ошибки отката:
                        err = fmt.Errorf("откат: %v: %w", rerr, err)
                }
                return err
        }
        if err := tx.Commit(); err != nil {
                return err
        }
        return nil
}

Три ветки — успех (Commit), ошибка (Rollback + оригинальная ошибка наружу), паника (Rollback + repanic). Хелпер живёт в одном файле проекта и обслуживает все транзакции приложения. Это адаптация примера из документации ent: если в вашей версии исходники отличаются мелочами — порядок идей тот же.

Что в базе после падения внутри WithTx

Прочитайте код и выберите, что он выведет:

err := WithTx(ctx, client, func(tx *ent.Tx) error {
        if _, err := tx.Post.Create().
                SetTitle("черновик").
                SetAuthorID(1).
                Save(ctx); err != nil {
                return err
        }
        return errors.New("feature flag выключен: выходим")
})
// анна (id 1) имела posts_count = 2. Что в базе после возврата?

tx.Client() и BeginTx: интеграция с чужим кодом#

Первый вопрос интеграции: что делать с функциями, чьи подписи принимают ent.Client — сервисный слой из восьмого урока, репозитории, хелперы. Переписывать всё на ent.Tx — долго и заразно. Ответ — tx.Client(): сгенерированный метод возвращает *ent.Client, чьи все билдеры привязаны к этой транзакции; передайте его в функцию — и её client.User.Query() исполнится внутри tx, а не на пуле. Внутри транзакции работают и With-цепочки из одиннадцатого урока: WithPosts, WithOwner — всё на том же соединении, консистентный снимок данных. Второй вопрос — изоляция. client.Tx(ctx) — сахар для BeginTx с нулевыми опциями; когда нужны особые уровни, вызывайте client.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead}) — уровень согласован с диалектом базы: Postgres и MySQL поддерживают Repeatable Read, SQLite в режимах гонок ограничен. Третий — ReadOnly: для чисто читающих задач транзакция только с чтением освобождает базу от лишних блокировок. ent транзакции здесь не добавляют своей магии — они честно пробрасывают database/sql.

Транзакция внутри чужих функций и изоляция

GO
// функция ждёт *ent.Client — не переписываем:
if err := publishToFeed(ctx, tx.Client(), post); err != nil {
        return err // всё внутри publishToFeed — в этой же tx
}

// особая изоляция для отчёта по балансам:
tx, err := client.BeginTx(ctx, &sql.TxOptions{
        Isolation: sql.LevelRepeatableRead,
})

tx.Client() — мост между tx-миром и клиентскими подписями: один вызов, и чужой код работает в вашей транзакции, не зная об этом. BeginTx с sql.TxOptions — единственный способ задать уровень изоляции и ReadOnly: у client.Tx таких параметров нет. Выбор уровня — тема базы, не ent: ent лишь гарантирует, что билдеры ничего не перепутают.

Вложенные транзакции: tx.Tx и savepoints#

Реальные операции состоят из обязательной и необязательной части: основное должно зафиксироваться любой ценой, а необязательное — пусть откатится отдельно, не роняя всё. SQL решает это savepoints: точка внутри транзакции, к которой можно откатиться частично. ent поддерживает вложенные транзакции через savepoints: у ent.Tx есть метод Tx(ctx) — он возвращает новую ent.Tx, чья Commit исполняет RELEASE SAVEPOINT, а Rollback — ROLLBACK TO SAVEPOINT. Внешняя транзакция продолжает жить: ошибка вложенного блока, если вы её обработали, не смертельна для основной. Это тот же контракт, что и у client.Tx, только точка отсчёта другая: не пул, а текущая транзакция. Поддержка диалектами хорошая: Postgres и MySQL/InnoDB умеют savepoints нативно, SQLite — тоже. Дисциплина остаётся прежней: вложенная транзакция — тот же WithTx-паттерн, только корнем служит tx, а не client. Глубину не зарывайте: savepoint в savepoint — уже запутанный след в логе базы, и не каждая операция это заслуживает.

WithNestedTx: savepoint по-человечески

GO
func WithNestedTx(ctx context.Context, rootTx *ent.Tx,
        fn func(tx *ent.Tx) error) error {
        tx, err := rootTx.Tx(ctx) // savepoint внутри внешней
        if err != nil {
                return err
        }
        if err := fn(tx); err != nil {
                if rerr := tx.Rollback(); rerr != nil {
                        err = fmt.Errorf("откат вложенной: %v: %w", rerr, err)
                }
                return err // внешняя решает, что с этим делать
        }
        return tx.Commit() // RELEASE SAVEPOINT
}

// использование: основное — обязательно, аватар — пусть откатится:
return WithTx(ctx, client, func(tx *ent.Tx) error {
        if _, err := tx.Group.Create().SetName("go-ent").Save(ctx); err != nil {
                return err
        }
        err := WithNestedTx(ctx, tx, func(ntx *ent.Tx) error {
                return makeAvatar(ctx, ntx.Client()) // может упасть
        })
        if err != nil {
                log.Printf("аватар не удался, группа жива: %v", err)
        }
        return nil
})

rootTx.Tx(ctx) открывает savepoint; Commit вложенной — RELEASE, Rollback — ROLLBACK TO. Ошибка makeAvatar проглочена логом: группа go-ent создана и зафиксируется, аватар — нет. Это точный ответ на «необязательная часть не должна ронять обязательную» без второго соединения и без переписывания бизнес-кода.

Проверка: судьба внешней транзакции

Внешняя транзакция создала группу. Вложенный savepoint упал, ошибку перехватили логом. Что окажется в базе после внешнего Commit?

Ошибки внутри транзакции: ConstraintError становится штатной#

Четырнадцатый урок кончался обещанием: пара «дети, потом родитель» обязана быть атомарной. Теперь это один блок: Delete по дружбам и DeleteOneID пользователя — внутри WithTx; ConstraintError, если что-то ссылается, останавливает блок до второго DELETE, и Rollback стирает след первого. Полуудалённого состояния больше не существует — в логе либо оба DELETE и COMMIT, либо первый DELETE и ROLLBACK. То же с ent.IsConstraintError и ent.IsNotFound: они ловятся как обычно, но теперь означают «откат + решение», а не «частичный успех». Отдельная честность: ошибки базы в транзакции бывают неконтролируемыми — после грубого нарушения (deadlock, серийный конфликт) база может сама откатить транзакцию, и дальнейшие стейтменты бесполезны; в этом случае ретрай — это новый WithTx с самого начала, а не продолжение старого. И ещё одна дыра забора, обещанная в начале: билдеры client.Post всё ещё доступны внутри WithTx — компилятор не запретит вам по ошибке вызвать клиентский билдер и уехать мимо транзакции на пул. Дисциплина простая: внутри fn(tx) используйте только tx.*, а ловлю мимо-транзакционных вызовов отдайте код-ревью и статическому анализу.

«Дети, потом родитель» — атомарно

GO
err := WithTx(ctx, client, func(tx *ent.Tx) error {
        // дети: дружбы через through-таблицу из шестого урока
        if _, err := tx.Friendship.Delete().
                Where(friendship.HasUserWith(user.ID(1))).
                Exec(ctx); err != nil {
                return err
        }
        // родитель: если FK откажет — откатится всё
        return tx.User.DeleteOneID(1).Exec(ctx)
})
if ent.IsConstraintError(err) {
        // кто-то ещё держит строку: карта рёбер из пятого урока
}

Тот же порядок, что в четырнадцатом уроке, но теперь разрыв невозможен: ConstraintError на втором шаге откатывает первый. ent.IsConstraintError ловится снаружи — это сигнал карты зависимостей, а не 500. Внутри fn(tx) только tx-билдеры: Friendship и User из транзакционного клиента.

Чеклист транзакций#

  • Граница транзакции — граница бизнес-операции, не HTTP-запроса: держать tx открытым на время походов в сеть и ответов пользователю — способ связать весь пул в узел.
  • WithTx в проекте один, его шаблон не переизобретают на месте: успех — Commit, ошибка — Rollback с оригинальной ошибкой, паника — Rollback + repanic.
  • defer tx.Rollback — страховка каждого ручного client.Tx без WithTx: sql.ErrTxDone после успешного Commit — норма, а не баг.
  • Внутри fn(tx) — только tx.*: tx.Client() для чужих подписей, client.Post внутри транзакции — способ тихо уехать мимо неё.
  • Счётчики и инкременты — Add-мутаторами из тринадцатого урока и в той же транзакции, что и сама мутация: posts_count обязан догонять посты атомарно.
  • ConstraintError и NotFoundError — штатные ветки: откат, лог карты зависимостей, решение; ретрай — новый WithTx с нуля, а не продолжение мёртвой транзакции.
  • Внешние сайд-эффекты — после Commit: письмо и очередь, отправленные внутри транзакции, уйдут зря при откате; паттерн outbox-таблицы, записанной в той же tx, разберём в семнадцатом.

Карта случаев: что и как оборачивать#

СценарийИнструментЧто атомарно
Пост + инкремент posts_countWithTx: tx.Post.Create + AddPostsCountобе мутации вместе
Дети, потом родитель (14-й урок)WithTx: два Delete в fnвесь каскад или ничего
Мутация с рёбрами (AddGroupIDs)обычный Save — ent сам открывает mayTxвставка узла + join-строки
Чужая функция с *ent.Clienttx.Client() как аргументвсё, что она делает
Необязательная подоперацияWithNestedTx: tx.Tx(ctx) → savepointвложенный блок отдельно
Особый уровень изоляцииclient.BeginTx + sql.TxOptionsвся транзакция по правилам уровня
Ручной client.Tx без хелпераdefer tx.Rollback + явный Commitлюбой путь выхода
Массовый upsert из восьмогоCreateBulk + OnConflict внутри WithTxвесь каталог целиком
Паника в бизнес-кодеrecover в WithTx: Rollback + repanicсоединение вернётся в пул
Аудит-лог той же судьбойхук с m.Tx() — семнадцатый урокмутация + запись аудита

Соберите каркас транзакции

Впишите открытие транзакции, страховку отката и фиксацию.

tx, err := client.(ctx) if err != nil { return err } defer func() { _ = tx.() }() // ... мутации на tx.Post и tx.User ... return tx.()

Жизнь одной транзакции

Соберите по порядку всё, что происходит с WithTx от вызова до возврата.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Что дальше#

Вы умеете строить атомарные операции: client.Tx и BeginTx с sql.TxOptions, контракт Commit и Rollback с sql.ErrTxDone, идиому defer и хелпер WithTx, tx.Client() для чужого кода, вложенные транзакции через savepoints — и ошибки, которые больше не оставляют полуудалённых состояний. Транзакция — это контейнер; семнадцатый урок покажет, кто исполняется внутри него рядом с вашими мутациями: хуки перехватывают каждую Create/Update/Delete до записи, могут валидировать, переписывать поля и писать аудит-лог в ту же транзакцию — счётчик posts_count наконец начнёт двигаться сам. Домашнее задание ниже заставит транзакцию провалиться красиво и доказать по логу, что база этого не заметила. Полный план из 25 уроков — на странице курса, счётчик — в тринадцатом уроке, жадная загрузка внутри tx — в одиннадцатом, источник пары «дети, потом родитель» — в четырнадцатом.

Карточки: транзакции

Термин

client.Tx: контракт

нажмите, чтобы перевернуть
Определение

client.Tx(ctx) → (*ent.Tx, error) из ent/tx.go: те же билдеры User/Post/Group, но драйвер — одно соединение. tx.Commit() фиксирует всё, tx.Rollback() стирает; оба возвращают error. client.Tx — сахар для BeginTx(ctx, nil).

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • ent транзакции начинаются с client.Tx(ctx) → (*ent.Tx, error): сгенерированный tx-клиент из ent/tx.go с теми же билдерами User/Post/Group, но на одном соединении; Commit и Rollback — единственные два исхода.
  • Идиома defer tx.Rollback делает забытую транзакцию невозможной: после успешного Commit она вернёт sql.ErrTxDone — штатно и безвредно; на ошибке и панике — отменит изменения и вернёт соединение в пул.
  • WithTx — стандартный хелпер: fn func(tx *ent.Tx) error, nil → Commit, ошибка → Rollback с сохранением оригинальной ошибки, паника → Rollback + repanic; границы атомарного блока видны в сигнатуре.
  • tx.Client() возвращает *ent.Client, привязанный к транзакции, — чужой код с клиентскими подписями работает внутри tx; With-цепочки из одиннадцатого урока исполняются на том же соединении.
  • Изоляция задаётся только через client.BeginTx(ctx, &sql.TxOptions{Isolation: ...}) — client.Tx параметров не принимает; ReadOnly и уровни изоляции — ответственность базы, ent честно пробрасывает database/sql.
  • Вложенные транзакции — tx.Tx(ctx): savepoint, где Commit = RELEASE, Rollback = ROLLBACK TO; обработанная ошибка вложенного блока не роняет внешнюю транзакцию.
  • Ошибка внутри tx отменяет всё: ConstraintError из четырнадцатого урока становится штатной веткой, полуудалённое состояние исчезает; после deadlock ретрай — новый WithTx с нуля.
  • Дисциплина: внутри fn(tx) только tx.* (tx.Client() для чужих подписей), внешние сайд-эффекты после Commit, счётчики Add-мутаторами в той же транзакции.

Домашнее задание

Работайте на базе из восьмого урока — анна, борис, вера, их посты и группа go-ninja — с включённым логом SQL драйвера. Первое: соберите WithTx из урока и проведите через него пару «пост + AddPostsCount(1)»: найдите в логе BEGIN и COMMIT, затем верните ошибку из середины fn и найдите ROLLBACK; после каждого прогона проверяйте SELECT-ом, что posts_count и число постов согласованы. Второе: смоделируйте «полуудалённое состояние» без транзакции — отдельные Exec на удаление дружб и пользователя с падением посередине — и письменно опишите, что зашилось в базе; затем повторите то же внутри WithTx и убедитесь, что база вернулась к исходному. Третье: соберите WithNestedTx на tx.Tx(ctx): основная часть создаёт группу и добавляет участника, вложенная «рисует аватар» и падает — по логу найдите SAVEPOINT и ROLLBACK TO SAVEPOINT и докажите, что группа зафиксировалась. Четвёртое: откройте BeginTx с sql.TxOptions и попробуйте два уровня изоляции на гонке «две горутины двигают posts_count по сотне итераций» — сравните итоговые числа с ожидаемыми и опишите, что изменил уровень. Пятое: передайте tx.Client() в функцию с подписью *ent.Client и с помощью лога докажите, что её запросы исполнились внутри транзакции; затем по ошибке вызовите внутри fn клиентский билдер client.Post — зафиксируйте в комменте, какой след в логе выдаёт утечку мимо транзакции. Вопрос на подумать: почему сайд-эффект во внешнюю систему (письмо, HTTP-вызов) внутри WithTx — это баг, даже если мутации пройдут без ошибок, и как из него корректно выйти?

Личные заметкиПусто
Хранятся только в вашем браузере (ent · Транзакции в ent)

Вопросы и ответы по уроку

client.Tx или client.BeginTx — что использовать?

По умолчанию client.Tx(ctx) — сахар над BeginTx с нулевыми опциями. BeginTx нужен, когда вы хотите задать sql.TxOptions: уровень изоляции (LevelRepeatableRead и другие) или ReadOnly. API билдеров у них одинаковый: оба возвращают *ent.Tx, и весь остальной урок применим без изменений.

Можно ли держать транзакцию открытым весь HTTP-запрос?

Технически — да, tx.Client() в начале запроса и defer Commit в конце. Практически — не надо: транзакция держит соединение из пула, и пара десятков параллельных запросов вычерпают пул, а любое ожидание внешней системы растянет транзакцию на секунды. Граница транзакции — граница бизнес-операции: короткий блок мутаций, снаружи — чтения и сайд-эффекты.

Почему Rollback после Commit не паникует и что делать с ошибкой?

Потому что запрос «отменить уже завершённую транзакцию» — штатная ситуация для идиомы defer: database/sql отвечает sql.ErrTxDone, и её осознанно глотают подчёркиванием. Паника была бы вредительством: страховка должна быть бесплатной. Если Rollback спотыкается на живой транзакции — это уже информация о сбое соединения, и WithTx оборачивает её вместе с оригинальной ошибкой мутации.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

go-kit25 уроков~14 чпродвинутый25 уроков уже доступны

go-kit: микросервисный тулкит

Курс go-kit: endpoint, транспорт, логирование, метрики, трейсинг, circuit breaker, service discovery. 25 уроков с итоговым микросервисом.

  • Понимать архитектуру endpoint → transport → service
  • Оборачивать сервисы middleware: логи, метрики, трейсинг
  • Подключать HTTP и gRPC транспорты
  • Ставить rate limiting и circuit breaker