惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

H
Help Net Security
T
ThreatConnect
SecWiki News
SecWiki News
F
Future of Privacy Forum
AWS News Blog
AWS News Blog
C
Cisco Blogs
A
Arctic Wolf
Vercel News
Vercel News
The GitHub Blog
The GitHub Blog
Scott Helme
Scott Helme
V
V2EX
博客园 - 叶小钗
阮一峰的网络日志
阮一峰的网络日志
K
Kaspersky official blog
G
Google Developers Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
P
Privacy International News Feed
C
Cyber Attacks, Cyber Crime and Cyber Security
N
News | PayPal Newsroom
Schneier on Security
Schneier on Security
NISL@THU
NISL@THU
Microsoft Azure Blog
Microsoft Azure Blog
量子位
The Hacker News
The Hacker News
Stack Overflow Blog
Stack Overflow Blog
Security Latest
Security Latest
M
Microsoft Research Blog - Microsoft Research
Google Online Security Blog
Google Online Security Blog
博客园_首页
C
CXSECURITY Database RSS Feed - CXSecurity.com
I
InfoQ
Google DeepMind News
Google DeepMind News
Y
Y Combinator Blog
The Cloudflare Blog
Microsoft Security Blog
Microsoft Security Blog
Martin Fowler
Martin Fowler
Cisco Talos Blog
Cisco Talos Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Troy Hunt's Blog
F
Fox-IT International blog
S
Security @ Cisco Blogs
博客园 - 司徒正美
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
C
Comments on: Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
L
LINUX DO - 最新话题
GbyAI
GbyAI
Project Zero
Project Zero
腾讯CDC
T
Tailwind CSS Blog

Все публикации подряд на Хабре

Как создать дебат-клуб в компании: пошаговое руководство от бизнес-тренера Как экономят на метановых автозаправках Самодельный elgato-like макропад. Часть 1, железная Всё есть код, или зачем внедрять GitOps в разработку Как получить root на Urovo DT40 Pro (CT48): Android 12 (Проверено на практике) C# мне нравится больше Java. Но в банковском enterprise мне всё равно понадобилась Java Биткоин на Московской бирже — что это? Как мы переводим миллионы iOS-пользователей на новое приложение каждые несколько месяцев Кейс. Zero Bug Policy: как мы снизили бэклог багов в 4 раза за месяц Shadow AI: 80% сотрудников уже пишут в ChatGPT. Почему мы делим задачи на красные, зелёные и серые Попытка пересмотреть ограничения рынка тяжелых БАС: нужен ли вообще кому-то легкий и дешевый электромотор Менеджер, который хакнул систему. И что AI на самом деле умножает Spec-driven development в микросервисах, часть 2: как archspec делает контекст сервисов явным Запись в Kubernetes: как контроллеры учились не перезаписывать друг друга Игровой движок 2.5D, короткие тренировки для ПК-пользователей –и еще 8 российских стартапов MCP в системе управления проектами: как поручить ИИ работу с корпоративными данными Бэклог болей: как hh работает с тем, что не нравится пользователям brec: контролируемая обратная совместимость протокола AI обнулил benchmark и пытался шантажировать инженера. И почему это решаемо Почему пластиковый корпус оказался в 3 раза дороже металлического Как спроектировать API, которое не придется переписывать через полгода Трекинг посетителей на fisheye-камерах: задача “со звездочкой” Красивый скриншот вашего кода. Большое обновление Я создаю проекты без единого созвона с командой Content Pipeline в MonoGame: почему я его не использую Гемблинг партнерки: Как выбрать, ТОП 5 в 2026 За пределами LLM, часть 2: якорная таблица Кэли, которая не является ни полем, ни моноидом Pixverse купить подписку: для чего нужна Пиксверс подписка, как выбрать тариф и оплатить в рублях Meshy AI нейросеть: как создавать 3D-модели из текста и изображений в Меши АИ на русском бесплатно Skywork AI: как использовать Скайворк АИ нейросеть на русском бесплатно, работать с промтами и создавать видео Технотекст 8: победа естественного интеллекта Capacitor: от веба к мобильным приложениям. Часть 4. Интегрируем локальный LLM в проект 20 лет видеокарт в цифрах: как росли FLOPS и TDP и кто вёл в дуэли NVIDIA vs AMD (+ открытый датасет на 13 500 GPU) Архитектура крипто-сканера для биржи: Open Interest, Funding Rate, EMA и MACD в реальном времени @tanstack/vue-table: почему я почти отказался от этого… WHERE превращает ваш LEFT JOIN в INNER JOIN. И никто вам об этом не скажет Гравитация не существует. Вы задали 454 вопроса о времени. Вот ответы с уравнениями Эйнштейна Конец бесплатного кремния: как Google AI Studio превратилась из рая для инженеров в симулятор смены аккаунтов Свой AI-агент из почты, systemd и LLM MemForge2: загрузочная флешка, которая за минуту говорит — какую планку памяти менять Лицензии важны. Разбор ошибок авторов и пользователей программ От RAG-прототипа к агенту в продакшн: путь по метрикам, а не по моде Serial Terminal: кастомный веб-терминал для последовательного порта на Web Serial API Китайский стартап GigaAI обещает робота-домработника за 1 млн рублей уже в 2027 году — правда или PR? Open-source VPN клиент Tunguska Роман за 6 недель без идеи на старте: миф или реальность? ИИ построит ваш план действий за 10 секунд Security Week 2622: эффективность Claude Mythos по версии Cloudflare Reactive Forms vs Signal Forms: Эволюция сложных форм в Angular TorFlash — приложение для Linux: поиск торрентов, скачивание и копирование на флешку в одно нажатие Как я решил проблему русской диктовки для ИИ Оверинжиниринг, потопивший немецкую подлодку или некоторые «баги» не чинятся десятилетиями Как ставить цели и не забывать о них: пошаговая система с примерами в таск-менеджере Как настроить observability в Spring Boot 3 HackTheBox. Прохождение Mini Pro Lab Puppet Обзор серверного ускорителя NVIDIA Tesla V100 16 Gb в корпусе от RTX 4090: Часть 3 — Запуск локальных моделей ИИ Редактирование текста нейросетью: как сделать диплом и курсовую более человечными Самодельный ARM ноутбук, реально ли? Как 100+ авторов пишут 100+ процессов в 3 версиях и не путаются. Или как мы переехали с Wiki на Git Прошла AnalystDays – хорошие выступления и нетворкинг VSCode как IDE для embedded разработки Моделирование широкополосной антенны с двойной круговой поляризацией и высокой изоляцией Ваше прошлое физически существует прямо сейчас. И вы заморожены там навсегда От списка инструментов к technical output: как security engineer’у описывать hands-on опыт в CV и на интервью I just want an agent. Часть 1. Как я научил ИИ собирать ИИ-агентов за пользователей и выиграл конкурс I just want an agent. Часть 1. Как я научил ИИ собирать ИИ-агентов за пользователей и выиграл конкурс Вайбкодинг спас меня от подрядчиков. А потом я поняла, что сама стала подрядчиком для своих агентов Святой Августин и GAN: почему борьба добра и зла — это генеративная состязательная сеть В каждом QR-коде зашита половина лишней информации. Намеренно Я открываю автомат ключом, меняю рулон бумаги и зарабатываю 180 тысяч в месяц с точки Мастер восстановления. Культура достиженства и выгорание Недельный геймдев: #279 — 24 мая, 2026 Защита от дублирования кода агентами: семантические концепции Frontend Status: свежий дайджест фронтенда и AI — 25.05.2026 Где искать IT-работу кроме HH: подборка платформ 2026 Почему простые числа собираются в спирали? OCR для Data Lakehouse: от Apache Tika к собственному решению на базе Docling Jira — Тьюринг-полная Kubernetes-аудит после Wiz и Prisma: как живут без CNAPP в 2026 «Тестируем MVP в 4 раза быстрее»: как нейросети изменили жизнь предпринимателей На каком стеке и железе работает умное наблюдение в вашем городе: обзор технологий от разработчиков видеоаналитики Как мы ускорили согласования на двух заводах в 24 раза Heartbeat-мониторинг cron-job'ов: dead-man-switch на FastAPI [Перевод] Сегодня нет джуниоров, а в 2031 году не станет и синьоров Профайлер для PostgreSQL: от идеи до работающего MVP за сутки [Перевод] Ограничения размера cookie в ASP.NET Core в продакшене: причины и способы решения Проблема «божественного» Obsidian: почему я отказался от централизованного подхода в работе Лицензии GNU GPL: как пройти проверку Минцифры и заказчика для госзакупок и КИИ Хакатон Samsung IT Academy Hack 2026: как студенты оптимизировали поиск в корпоративном мессенджере Хакатон Samsung IT Academy Hack 2026: как студенты оптимизировали поиск в корпоративном мессенджере MTProxy jumper — делаем автоматическое переключение прокси-серверов Telegram Ты уже используешь агента. Просто не заметил Книжный салон. Послевкусие и благодарности Как отлаживать мини‑приложения в MAX и почему без DevTools это боль Cбор биометрических данных. Как защищается наша биометрия на практике Как запустить учет активов без цифровой свалки: первые 90 дней CGE: визуализация кравлера и скрытых связей между поддоменами Зачем банки тратят миллиарды на науку (спойлер: не благотворительности ради) Книга: «Современный Java Concurrency. Глубокое погружение в Virtual Threads, Structured Concurrency и Scoped Values» Как использовать подписку ChatGPT и Claude в Cursor без оплаты за API токены
Не наступайте на наши грабли, если собираетесь использовать Temporal
m03r (Яндекс · 2026-05-26 · via Все публикации подряд на Хабре

Всем привет! Меня зовут Миша, я разрабатываю платформу Яндекс Еды. В декабре я рассказывал, как Temporal без боли решает привычную проблему распределённой бизнес‑логики.

В продолжение темы я задумал написать такую статью, которую мне самому хотелось бы прочитать перед тем, как мы начали миграцию на Temporal. Всё изложенное проверено на практике: процессинг заказов Яндекс Еды уже почти год работает целиком на Temporal. Об общих принципах работы с Temporal я уже рассказал в предыдущей статье, а здесь я поделюсь полезными советами, выведенными из нашего опыта.

TL;DR:

  • Парадигма Temporal весьма самобытна, и к ней придётся привыкать, но она действительно удобна.

  • У Temporal SDK довольно много неочевидных, но полезных возможностей.

  • Temporal создаёт иллюзию чистой архитектуры из коробки, но ей нельзя поддаваться.

  • Детерминизм бизнес‑логики — это очень важно.

  • Есть удобные методы обеспечить хорошую observability.

Некоторые части специфичны для разработки на Go, а другие вполне универсальны. Они организованы от общих к частным. Поделюсь практическими советами по архитектуре, тестированию, детерминизму и безопасному развитию Workflow. Покажу, как организованы миграции и эксплуатации в крупном продакшене.


Прежде чем начнём

Первый и, возможно, самый главный совет — внимательно прочитайте документацию. У меня сложилось впечатление, что документация структурирована не лучшим образом, и я рекомендую вот такой порядок чтения:

  1. Quickstart — выбрав язык программирования, который планируете использовать.

  2. Encyclopedia — от начала и до раздела Workflow message passing включительно, это must‑have. Остальное можно просмотреть по необходимости.

  3. Раздел Develop для избранного языка — целиком, а также общие разделы (от Activity Retry Simulator и далее).

  4. Примеры из репозиториев — просмотреть (на конкретном языке), чтобы убедиться, что в памяти отложились все возможности Temporal.

Почему именно так? На мой взгляд, разделы Develop содержат больше всего полезной информации, но при этом написаны так, как будто читатель уже хорошо представляет, как работает Temporal в целом, и ему нужно просто узнать, как ту или иную вещь реализовать в конкретном SDK.

НАШИ ГРАБЛИ

Из‑за того, что мы недостаточно хорошо изучили различные разделы документации, у нас в коде до сих пор остаются некоторые архитектурные костыли. Их можно было бы избежать, используя встроенные механизмы Temporal SDK. Во врезках с заголовком «Наши грабли» я буду рассказывать, с какими сложностями мы столкнулись и как можно было бы сделать хорошо с самого начала.

В этой статье я постараюсь обратить внимание на полезные вещи, которые «спрятаны» в документации.

Как думать на Temporal

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

  • Temporal позволяет чётко разграничить чистую бизнес‑логику (Workflow) и «грязную» реализацию (вызываемые им Activity).

  • Чем больше структура Workflow похожа на описание процесса, понятное для бизнеса, тем лучше (ср.: кричащая архитектура Роберта Мартина).

  • Подход Event Sourcing даёт гарантии обработки, которая будет повторяться до победного — с заданными настройками этого самого победного.

Отсюда следует вывод: код Workflow должен выглядеть, как изложенное в коде описание бизнес‑логики. Ровно так же, как ChatGPT в 2022 году пытался писать программу для захвата мира, начиная с высокоуровневых функций и постепенно детализируя их. В случае с Workflow детализация заканчивается там, где продакт‑менеджер сказал бы: «Это детали реализации».

Детали реализации — это Activity, элементарное идемпотентное «техническое действие». Важно помнить, что Temporal повторно запустит (в пределах обозначенной политики повторов) упавшую Activity, из чего и выводятся следующие требования:

  1. Идемпотентность. Без этого свойства вызовов в распределённых системах в целом сложно, но в случае Activity это абсолютная необходимость.

  2. Элементарность. Если Activity делает несколько вещей (прочитать из одного сервиса, записать в другой), то при падении второго первое также будет повторяться, что может привести к лишней нагрузке.

Если Activity удовлетворяет этим критериям, то выводятся ещё два полезных свойства относительно повторных попыток выполнения (ретраев):

  • Быстро поднятое не считается упавшим. Если в рамках retry policy удалось получить нормальное завершение Activity, то с точки зрения Workflow это выглядит просто как успешное завершение.

  • Не ломиться в закрытую дверь. Важно выделить non‑retryable‑ошибки, если таковые могут случиться, чтобы не было повторных попыток заведомо безнадёжных действий.

У Temporal также есть возможность завернуть любые колбэки и другие подобные вещи в Activity с помощью асинхронного завершения. Для этого Activity должна передать на сторону колбэка свой токен и вернуть специальный результат. Таким образом, любой ввод‑вывод и какое‑то действие можно превратить в Activity.

Во многих случаях бизнес‑логика подразумевает возможность или даже необходимость получить какое‑то сообщение от внешнего мира. Temporal предоставляет для этого два разных инструмента: сигналы и апдейты.

Сигналы — это что‑то вроде письма в Workflow. Внешний мир может его передать (имя канала и содержимое), а Workflow — попытаться прочитать сигналы из определённого канала и даже заблокироваться в ожидании такого сигнала.

В процессинге Яндекс Еды такое ожидание сигнала происходит, когда мы ждём обновления заказа из внешней системы, например подтверждения от ресторана или новых данных от логистики. Важно понимать, что сигналы работают по принципу fire‑and‑forget, и вызывающая сторона не может получить никакой реакции на них, кроме «сигнал принят» или «такого Workflow не существует».

НАШИ ГРАБЛИ

Мы недостаточно хорошо спроектировали логику обработки платёжного колбэка. В результате вместо оптимальной схемы с асинхронным завершением Activity до сих пор используем более сложную логику: пришедший колбэк посылает сигнал в Workflow, который его дожидается с определённым таймаутом.

Если же нужно не просто сигнализировать в Workflow, а получить какую‑то реакцию — нужен апдейт. Отправка апдейта приводит к тому, что Workflow просыпается и синхронно выполняет установленный обработчик апдейта такого типа (он здесь должен быть обязательно — в отличие от сигнала). Этот обработчик выполняется в контексте Workflow, что‑то там делает по бизнес‑логике и возвращает результат.

У нас через апдейт реализован перевод заказа в финальный статус («доставлен» или «отменён»). Апдейт решает здесь сразу несколько проблем. Во‑первых, обработчик (как и весь код Workflow‑функции) выполняется в один поток, поэтому гонки исключены. Во‑вторых, он может сразу проверить, что состояние заказа не противоречит такому изменению, и вернуть соответствующий ответ, если это не так. В‑третьих, если уж апдейт прошёл проверку и обработчик выполнился, то можно быть уверенным, что это изменение зафиксировано в истории событий (event history) и никуда не пропадёт.

НАШИ ГРАБЛИ

Первая версия обработки финального статуса выглядела следующим образом: доставка была сигналом, а отмена — апдейтом. К счастью, мы очень быстро поняли, что обе операции должны быть блокирующими. Более того, в итоге — с учётом требований идемпотентности — повторная доставка доставленного заказа всегда успешна (просто ничего не делает), а вот повторная отмена дополнительно сверяет причину отмены (и повторная отмена успешна лишь с указанием той же причины отмены).

Кстати, в Go SDK отмену заказа очень удобно реализовывать через отмену контекста. А ещё отмена контекста работает и в других местах: в ожидании таймеров, в чтении каналов и тому подобное. Отмена контекста, с которым была вызвана Activity, приведёт к отмене её повторения (или даже прерыванию её на середине, если она использует специальный механизм heartbeat для длинных Activity). В общем, здесь сделано привычно для Go‑разработчиков, что, без сомнения, удобно.

Единственное, для всей привычной Go‑машинерии (горутины, каналы, селекторы) нужно использовать специальные Temporal‑аналоги: workflow.Context, workflow.Go, workflow.NewSelector и тому подобное. Это связано с тем, что обычные Go‑средства не гарантируют одинакового порядка выполнения при повторных запусках, а с точки зрения воспроизводимости истории событий это критично.

Приведу пример, где используются сразу все эти штуки. На определённом этапе заказа у пользователя есть возможность уточнить адрес (и некоторые это делают даже несколько раз).

// Создаём отменяемый контекст для слушателя
listenerCtx, stopListener := workflow.WithCancel(ctx)

// Запускаем слушатель сигналов в отдельной горутине
workflow.Go(listenerCtx, func(ctx workflow.Context) {
    var cancelFunc workflow.CancelFunc

    // workflow.NewSelector -- аналог select {}
    selector := workflow.NewSelector(ctx)

    selector.AddReceive(
        workflow.GetSignalChannel(ctx, "UpdateOrderAddress"), // аналог chan
        func(c workflow.ReceiveChannel, _ bool) {
            var (
                signal UpdateOrderAddress
                activityCtx workflow.Context
            )
            c.Receive(ctx, &signal)

            // Отменяем предыдущий запуск, если пользователь
            // уточняет адрес повторно
            if cancelFunc != nil {
                cancelFunc()
            }

            // Disconnected context: stopListener() остановит приём сигналов,
            // но не отменит уже запущенную Activity -- она доработает до конца
            disconnectedCtx, _ := workflow.NewDisconnectedContext(ctx)
            activityCtx, cancelFunc = workflow.WithCancel(disconnectedCtx)

            workflow.Go(activityCtx, func(ctx workflow.Context) { // аналог go func()
                activities.UpdateOrderAddress(ctx, signal)
            })
        },
    )

    for {
        selector.Select(ctx)
        if ctx.Err() != nil {
            return
        }
    }
})

Да, сам этот код выглядит достаточно низкоуровнево, но в целом он обёрнут в функцию canceller := StartListeningForAddressChange(ctx), которая вызывается в нужный момент.

Есть ещё такая штука как Query — что‑то вроде GET‑запроса в Workflow, на который тот может вернуть какую‑то часть своего внутреннего состояния или что‑то, вычисленное на его основе.

В случае с процессингом Яндекс Еды с помощью Query можно было бы узнавать статус заказа. Однако мы полностью отказались от использования этой функциональности, так как она довольно дорогая по сравнению с более простой альтернативой: хранением статусов (и ключевых событий) рядом, в обычной БД. В нашем Workflow есть Activity, которая после смены статуса немедленно записывает обновление в БД, а все клиенты, которым необходима эта информация, делают обычные запросы. Так что Query мы используем очень ограниченно.

Архитектура и проектирование

Пара слов об организации кода. Чистая бизнес‑логика в Temporal Workflow выглядит очень соблазнительно — как чистая архитектура. Не поддавайтесь!

Изоляция бизнес‑логики от ввода‑вывода (I/O) сама по себе не гарантирует хорошую архитектуру. В идеале эта доменная область вообще ничего не должна знать о «каком‑то там темпорале», а только предоставлять интерфейсы, более или менее соответствующие тому, что мы от него хотим. Например, интерфейс системы платежей с методами, в целом соответствующими бизнес‑действиям («захолдировать средства», «списать средства», «отменить холд»). И код, отвечающий за Temporal‑вызовы, уже будет эти интерфейсы реализовывать.

Иными словами, в идеале сама бизнес‑логика должна тестироваться (желательно не слишком крупными частями) изолированно от Temporal SDK. Правда, в случае с Go избавиться полностью от вездесущего workflow.Context будет затруднительно, но в этом направлении стоит работать.

Вначале мы писали юнит‑тесты на весь Workflow заказа целиком, но количество комбинаций параметров быстро вышло за разумные рамки, поэтому тестируемые блоки стали становиться меньше. Если бизнес‑логика, стоящая за Workflow, может быть разбита на несколько логических шагов, то сразу стоит подумать о том, чтобы писать их тестируемыми по отдельности, и желательно без связки с Temporal.

НАШИ ГРАБЛИ

Первая версия нашего Workflow была просто простынёй вызовов всех Activity в правильном порядке. И несмотря на то что была реализована лишь часть бизнес‑логики, объём всяческого бойлерплейта для юнит‑тестов был весьма значителен. Хуже всего было добавление новых: половину приходилось копипастить, а вторую половину по‑умному адаптировать (так как там уже были написаны некоторые дополнительные функции для уменьшения бойлерплейта). В результате сложность этой системы разрослась до совершенно неподдерживаемой, и нам пришлось поменять подход (и это было к лучшему).

На самом деле есть ещё один слой, в который можно вынести довольно много Temporal‑специфичных вещей — интерцепторы (по сути, middleware). Их можно посадить между вызовами Temporal SDK и отправкой сообщений серверу Temporal по сети. В интерцепторы приземляются всякие вещи типа динамического конфигурирования retry policy, кастомных метрик и кастомного логирования, особой системной логики обработки ошибок и тому подобное. Этим «деталям реализации» не место в коде Workflow.

Если разработка идёт на Go, обязательно нужно подумать о кодогенераторе для вызовов Activity. Дело в том, что Go SDK никак не реализует контроль соответствия типа Activity и переданных ей аргументов. Это упущение исправляют сторонние пакеты типа protoc‑gen‑go‑temporal (мы используем внутреннюю разработку, в целом аналогичную). В других языках с более проработанной системой типов такого рода проблем гораздо меньше.

Детерминизм и развитие бизнес‑логики

Бизнес‑логика должна всегда приводить к одинаковому результату на одинаковой истории событий — это суть детерминистического подхода в Temporal, который обеспечивает такую надёжность. Платим мы за неё тем, что вынуждены хранить в истории событий всё, что способно повлиять на работу бизнес‑логики.

Здесь полезны следующие инструменты:

LocalActivity. Это Activity, которая выполняется в том же процессе, что и Workflow‑воркер, без планирования задачи через сервер Temporal. Подходит для быстрых и лёгких операций: например, прочитать что‑то из локального кеша или сделать быстрый HTTP‑запрос на localhost. Вместо цепочки Workflow‑воркер → Temporal → Activity‑воркер → Temporal → Workflow‑воркер в Temporal отправляется одно событие с итогами исполнения LocalActivity (при завершении соответствующей Workflow Task).

Сюда стоит выносить те вещи, которые нужно зафиксировать в истории, но можно сделать локально, — это может неплохо сэкономить ресурсы. Мы, например, получаем retry policy для Activity именно так, поскольку там происходит запрос на sidecar конфигов, расположенных на том же сервере. Сами LocalActivity выполняются в отдельном локальном воркере для LocalActivity, и у них есть все те же самые плюшки, что и у настоящих: они могут повторяться при ошибках с заданными параметрами, им можно задать таймаут и тому подобное

SideEffect. Способ зафиксировать в истории событий результат недетерминированного вычисления, не оформляя его как отдельную Activity. Это вычисление выполняется прямо в том же потоке, что и Workflow Task, и не имеет никаких гарантий от Activity.

Типичный сценарий — генерация случайных чисел и что‑нибудь в таком роде. Значение вычисляется один раз и фиксируется в истории событий, при повторных проигрываниях Temporal просто достаёт сохранённое значение — никаких повторных вызовов. Это гарантирует, что пользователь всегда идёт по одной и той же ветке. Мы сохраняем в SideEffect, например, результаты запроса к локальному кешу настроек: они со временем могут поменяться, но в истории останется изначальное значение.

Важное ограничение: SideEffect работает только в рамках изолированной бизнес‑логики внутри Workflow. Если нужна согласованность с внешними системами — например, другие сервисы тоже должны знать это случайное число, — одним SideEffect здесь не обойтись. Придётся через обычную Activity отправить его на хранение в соседний сервис.

НАШИ ГРАБЛИ

Дело в том, что все SideEffect записываются в историю событий единообразно: просто как маркер с типом SideEffect и произвольным значением. Из‑за ошибки в коде в некоторых случаях один SideEffect пропускался, но сама Workflow panic с ошибкой детерминизма случалась позже, так как последующие маркеры SideEffect в истории читались со смещением на единицу и проблема детектировалась только на последнем, который отсутствовал. Распутать этот кейс помогла только внимательная отладка.

Версионирование. Оно создано для того, чтобы при релизе новой версии кода бизнес‑логики старые Workflow продолжали выполняться по‑старому, а новые — по‑новому. По сути, мы вынуждены держать в коде оба варианта и с помощью специальной конструкции определять, записан ли у нас маркер версии (можем делать по‑новому) или нет (повторяем старую логику).

В какой‑то момент в одном месте у нас поддерживалось чуть ли не четыре разные версии: было несколько хотфиксов, каждый успел наплодить какое‑то количество Workflow (которые могут быть долгоживущими из‑за заказов, отложенных «на послезавтра»). Честно говоря, для нас эта возможность оказалась не такой радужной, как это выглядело вначале, и в результате мы практически полностью перешли на использование парадигмы фич‑флагов (про это я ещё подробно расскажу).

НАШИ ГРАБЛИ

Из‑за того, что мы хотели включать новые версии бизнес‑логики не на всех пользователей сразу, а на какую‑то небольшую часть, мы сделали систему релизных флагов, дополнительно ограничивающих максимальную версию, которую мог получить Workflow. Таким образом, сам механизм версионирования здесь оказывался совершенно лишним, но осознали мы это не сразу и продолжали какое‑то время пользоваться таким странным гибридом.

Тем не менее версионирование по‑настоящему полезно именно там, где бизнес‑логика обновляется без возможности отката, и здесь уместно архитектурное решение. На первый взгляд оно кажется довольно диким: лучший способ не выстрелить себе ногу с версионированием — это копипаст. Весь текущий код, который мы хотим заменить, оборачивается в if version == oldVersion, а рядом просто копируется новый вариант — уже с нужными улучшениями. Спустя какое‑то время, когда Workflow на старой версии больше не останется, старый код вместе с этим ветвлением удаляется. Причём копипастить можно не только отдельные строки, но и целые директории: формально это выглядит как нарушение DRY, но на деле противоречия здесь нет — этот код всё равно временный и скоро исчезнет. Главное — действительно потом его удалить.

Тестирование

Несмотря на то что тесты Workflow формально выполняются без внешних зависимостей (всё мокируется на уровне Temporal SDK), ощущаются они как интеграционные тесты, так как тестируется Workflow целиком. Это определяет стратегию: не пытаться покрыть все комбинации на уровне Workflow‑тестов, а распределить ответственность между уровнями.

В целом у нас есть четыре вида тестов:

  1. Юнит‑тесты бизнес‑логики — изолированно от Temporal, небольшими блоками.

  2. Юнит‑тесты через Temporal Test Suite — с управлением временем.

  3. Интеграционные тесты с Temporal‑сервером.

  4. Replay‑тесты — защита от недетерминизма при изменениях кода.

Если бизнес‑логика правильно изолирована от Temporal SDK, большую её часть можно и нужно тестировать обычными юнит‑тестами. Вряд ли здесь нужны какие‑то специальные пояснения.

Интересное начинается с Temporal Test Suite: он умеет путешествовать во времени. Поскольку время внутри Workflow (при всяких workflow.Sleep) идёт не совсем по‑настоящему, то можно написать тест на следующую ситуацию: Workflow ждёт нескольких сигналов в определённом порядке, а приходят они в другом. Упомянутую в прошлой статье схему «если пришёл следующий сигнал, то ещё немножко подождём нужный, а потом пойдём дальше» как раз очень удобно тестировать с помощью этого механизма.

Технически это выглядит так: реализующая это структура обёрнута в небольшой тестовый Workflow, в который планируется отправка сигналов с указанным временем. Temporal Test Suite просто «проигрывает» этот Workflow без лишних ожиданий (так же, как при пробуждении настоящего Workflow проигрывает его Temporal SDK) и проверяет то, что нужно проверить.

Так можно проверять и другие сценарии, например «если в течение 30 секунд эта Activity не завершилась, то больше не ждём и идём дальше», и вообще всё что угодно. Единственное, тестируемый фрагмент бизнес‑логики не должен быть слишком большим, чтобы не произошло комбинаторного взрыва количества тест‑кейсов.

Интеграционные тесты с настоящим Temporal‑сервером уже выполняются в режиме реального времени, поэтому здесь не получится потестировать сценарии с таймаутами. Зато можно проверить всё целиком, в том числе корректность внешних вызовов, обработки внешних сигналов/апдейтов и тому подобное. Кроме того, эти тесты служат отличным источником синтетических данных для replay‑тестов.

Replay‑тесты — это must‑have для защиты от случайного недетерминизма. Суть их проста: прогнать старую историю событий на новой версии кода. Сложности могут возникнуть с тем, где эту старую историю событий получить. Вариантов здесь несколько:

  1. Синтетическая история событий. Прогоняем основные сценарии с настоящим сервером Temporal, получаем историю событий и сохраняем её для следующих запусков. У нас этот процесс настроен после успешного релиза: мы уверены, что в типичных сценариях при релизе ничего не сломается.

  2. Скачивать настоящую историю событий с прода. Это хороший вариант, но здесь есть масса разных сложностей, начиная с правильного выбора того, что скачивать (можно, конечно, скачать всё имеющееся, но тесты тогда будут прогоняться вечность), и заканчивая зачисткой в истории событий чувствительных данных. Мы планируем реализовать и этот вариант тоже.

Ещё один инструмент защиты от недетерминизма — встроенный чекер, который находит недетерминированный код внутри Workflow на Go.

Плавная миграция на Temporal (и фич‑флаги)

Когда мы переходили на Temporal, то непременным требованием была плавность перехода. Мы прекрасно осознавали, что если перенести всё разом, то проблем не оберёшься. Поэтому нужно было придумать такой механизм, с помощью которого можно было бы плавно переносить процессинг заказов по частям, и при этом с возможностью переключать сначала внутренних пользователей, потом небольшой процент внешних и так далее.

В целом это напоминает обыкновенные фич‑флаги, выставляемые на основе разделения пользователей по какому‑то условию. Однако требование детерминированности процесса накладывает здесь ограничения. Если в обычной ситуации мы можем переключить флаг и сразу же увидеть изменения, то в парадигме детерминированной бизнес‑логики это невозможно.

Поэтому наш процесс был устроен следующим образом: в начале обработки каждого заказа мы с помощью сервиса экспериментов получали конфигурацию процессинга для него. Там буквально было записано следующее: этот участок обрабатывается в новой системе, тот — в старой, а вот этот — и в старой, и в новой. По умолчанию в новом всё выключено, а в старом всё включено. Эта конфигурация фиксируется и в Workflow (одним из первых вызовов), оттуда копируется в старый процессинг и более не меняется.

Это выглядело как‑то так: новый участок процессинга переведён на Temporal и протестирован на тестовых стендах — соответственно, протестированы и включатели/выключатели разных участков в старом и новом процессинге. После релиза обновлённой версии Workflow вместе с новым кодом сервиса сначала необходимо убедиться, что всё продолжает хорошо работать. После этого мы включаем процессинг по‑новому только для наших коллег из Яндекс Еды (а иногда даже для отдельных разработчиков) и внимательно наблюдаем, нет ли каких‑то проблем. После тестирования на внутренних пользователях мы можем переключить на 1% пользователей и так далее.

Конечно, процесс кардинальной переработки процессинга не обходился без инцидентов, но благодаря аккуратному и плавному переключению серьёзных проблем удавалось избегать. Кстати, сама система с хранением конфигурации миграции практически не давала сбоев. Только один раз нам пришлось экстренно переключать несколько тысяч заказов на старую систему, исправляя сохранённое в её БД состояние. Но и здесь речь шла о том, чтобы корректно проставить статус «доставлено» у фактически доставленных клиентам заказов.

После миграции эта система продолжает работать уже как фич‑флаги процессинга и полностью заменяет собой версионирование встроенными средствами Temporal. Она обеспечивает гораздо больше возможностей, в первую очередь — гибкую сегментацию.

Из недостатков такого подхода могу отметить один — наша реализация получает все флаги в начале Workflow, поэтому если параметр отвечает за что‑то, связанное с доставкой, то придётся какое‑то время подождать эффекта от переключения. Но в целом это можно исправить, получая конфигурацию по частям в нужные моменты и обновляя записи во всех хранилищах (здесь придётся предпринять некоторые дополнительные шаги, чтобы обеспечить надёжность записи).

Разные полезности

Напоследок хочется поделиться некоторыми полезностями, которые, как мне кажется, будут не лишними для любого проекта с Temporal.

Observability: метрики, логи, трейсинг 

Трейсинг в Temporal у нас выглядит так: всё, что происходит в рамках одного Workflow, — один trace. Можно видеть всю цепочку действий, их тайминги и ошибки в одном месте. Альтернативный подход — менять trace.id в каких‑то местах Workflow или даже на каждое пробуждение Workflow‑функции. Полагаю, что для долгих Workflow, которые могут длиться днями или месяцами, это может быть вполне удобно, особенно если они периодические.

Сам кластер Temporal отдаёт огромное количество метрик, и почти все они реально полезны в диагностике самых разных проблем. Ирония ситуации состоит в том, что связь некоторых графиков и проблем обычно выявляется по факту инцидентов и их разбора. Здесь лучший способ — это нагрузочное тестирование и внимательный анализ результатов со знанием внутреннего устройства Temporal. Этот универсальный, но не очень конкретный совет — лучшее, что я могу предложить, так как в разных условиях узкими местами будут разные участки системы, и это нужно выяснять в каждом конкретном случае. Хорошей отправной точкой будут дашборды для Grafana, собранные сообществом.

Кроме этого, сам Workflow может писать какие‑то бизнес‑метрики (здесь важно перед записью проверять !workflow.isReplaying(ctx), чтобы не писать повторно одно и то же), а на интерцепторах можно накрутить любые кастомные метрики по работе Activity и Workflow. Например, мы сделали отдельный конфиг, в котором задано, сколько ретраев считается нормальным для разных видов Activity, и при превышении этого количества пишем специальную метрику.

Наши дашборды по Temporal чётко делятся на два типа: информация о бизнес‑процессах (например, количество заказов или срез их распределения по статусам) и техническое состояние всей системы вокруг Temporal (загруженность воркеров, падения Activity и тому подобное).

Самое неприятное, что может случиться, — это ситуация Workflow panic (или аналоги в других языках), которая обычно случается из‑за недетерминизма. На это обязательно нужен алертинг, который не будет гореть только тогда, когда их ровно 0. Паникующие Workflow, как правило, требуют немедленного внимания в случае критичных бизнес‑процессов, поэтому их обработка полностью блокируется.

Если у бизнес‑процесса есть некие параметры, которые хотелось бы использовать для фильтрации в Web UI, то Temporal даёт возможность задать некоторое множество Search Attributes. Мы, например, вынесли в них статус заказа, тип заказа и тип доставки. На этапе разработки было очень удобно фильтровать только нужные сочетания параметров и по истории событий проверять, всё ли работает так, как задумано.

Если в Workflow нужно выполнить слишком много действий

У Temporal есть ограничение на историю событий в одном Workflow. Оно достаточно велико, но для некоторых сценариев его может не хватить. Temporal предлагает несколько разных решений в зависимости от проблемы, которая приводит к засорению истории событий.

Многочисленные однотипные операции ввода‑вывода, которые порождают соответствующее количество вызовов Activity. Здесь подходящим решением будут долгие Activity с heartbeat. Например, вместо того чтобы запускать 1000 Activity для скачивания 1000 файлов, можно запустить одну и передать ей весь список файлов. Периодически эта Activity может посылать на сервер Temporal так называемый heartbeat, сообщающий о её текущем прогрессе. Если Activity‑воркер внезапно завершится, Temporal сможет перезапустить эту Activity, передав новой попытке последний heartbeat от предыдущего экземпляра, чтобы не делать повторно уже выполненную работу.

Ещё один пример — повторяющийся опрос статуса внешнего источника. Этот опрос можно делать в рамках одной Activity, гибко конфигурировать её параметры (Schedule‑To‑Close Timeout, Start‑To‑Close Timeout, Heartbeat Timeout), определяя, сколько именно мы готовы ждать изменений, и избегать зависания самого опроса. Кстати, если вдруг внешний источник перейдёт на колбэк, то можно будет только изменить код Activity, чтобы она стала асинхронной, — и даже не придётся менять код Workflow.

Логически отделённые участки бизнес‑логики. Здесь Temporal даёт возможность запустить дочерний Workflow, который может быть более или менее жёстко связан с родительским (в основном меняется поведение в том случае, когда родительский завершается, а дочерний ещё работает). У нас таким образом работает специальный Workflow, отвечающий за действия, произошедшие после завершения заказа: фактически основной процессинг уже завершён, но есть определённое количество служебных действий, которые ещё нужно завершить (что может занять довольно много времени, например из‑за ожидания информации от партнёров).

Continue‑As‑New. Если бизнес‑логика может крутиться в бесконечном цикле (например, подписка или что‑то вроде этого), то переполнение истории событий неизбежно. Для решения этой проблемы Temporal предлагает технику Continue‑As‑New: передать в новый Workflow сохранённое состояние из предыдущего, но начать историю с чистого листа.

Скрипты для массовых операций

Если что‑то идёт не так, то Temporal предлагает несколько разных инструментов, чтобы «починить» сломанный экземпляр Workflow. Основной из них — Reset, который прекращает все действия в старом Workflow, копирует заданную часть истории событий в новый и продолжает его выполнение. Таким образом после каких‑то исправлений можно вернуться назад во времени и построить другую версию истории.

Но если нужно сделать это массово, то возможности Temporal ограничены: массовый Reset возможен только на самую первую Workflow task или на самую последнюю (потому что разные Workflow могут быть разной длины — под одинаковым номером в истории будут разные события). Чтобы делать это более гибко, мы написали свой собственный инструментарий вокруг Temporal CLI — более детальный Reset, поиск ошибок, связанных с недетерминизмом, массовую рассылку сигналов/апдейтов в Workflow и тому подобное. Работает он не очень быстро, так как для многих операций приходится выгружать историю всех Workflow по фильтру и исследовать её, но это в любом случае быстрее, чем делать то же самое руками.


Temporal — это не просто ещё один инструмент для оркестрации, а другой способ разработки распределённых процессов. Он хорошо раскрывается там, где важно явно выражать бизнес‑логику, мпокойно переживать сбои и контролируемо развивать долгоживущие сценарии. Но за эти преимущества приходится платить дисциплиной: нужно помнить про детерминизм, аккуратно выделять Activity, строить архитектуру так, чтобы доменная логика не смешивалась с вызовами SDK, и заранее продумывать стратегию тестирования, observability и изменений в коде.

Наш опыт показал, что Temporal действительно способен стать надёжной основой для критичного процессинга, если внедрять его постепенно и осознанно. Продуманная архитектура, плавная миграция через фич‑флаги, replay‑тесты и собственные операционные инструменты позволяют использовать его сильные стороны без лишней боли. Если подойти к внедрению с правильным мышлением и вниманием к деталям, Temporal даёт не только техническую устойчивость, но и гораздо более понятную и прозрачную модель исполнения бизнес‑процессов, а ещё лёгкость в их поддержке и развитии.