ADR-007 — Долгие ожидания в памяти: дегидратация и пробуждение по триггеру¶
| Поле | Значение |
|---|---|
| Статус | Принято |
| Версия | v.2.1 |
| Дата | 2026-07-29 |
| Владелец | Руслан Габитов |
| Уточняет | ADR-001 v.6 Execution Model §4.7 (инвариант долгого ожидания, который здесь реализуется) |
| Связано | ADR-006 v.4 Events & Subscriptions (доставка триггера), ADR-033 v.1 Persistence & State §2.4/§2.5 (долговечная проекция этой in-memory модели), ADR-013 v.2 Observability (факты резидентности), ADR-020 v.1 Human Interaction / ADR-021 v.1 Service Task (два из держателей ожиданий) |
Этот ADR решает, как долгое ожидание освобождает свои горутины и как триггер его возобновляет — в памяти: механизм, которого требует ADR-001 §4.7 и который черновик v.1 обещал написать, когда работа над долгими ожиданиями дойдёт до реализации. Долговечная проекция (та же модель, пережившая рестарт) — предмет ADR-033; это одна модель на двух уровнях резидентности (§2.6). Сопровождающий SRD приземляет механику.
1. Контекст и проблема¶
- Инвариант ADR-001 §4.7: долгое ожидание (UserTask, ждущий три дня,
Timer, ReceiveTask) не должно удерживать горутину. Сегодняшний рантайм
его нарушает: дорожка, дошедшая до узла ожидания, переходит в
TrackWaitForEvent, но её горутина остаётся живой и заблокированной на канале событий всё время ожидания, а рядом остаётся резидентным цикл событий экземпляра. Один экземпляр, ждущий одного таймера, удерживает поэтому три горутины — цикл экземпляра, припаркованную дорожку и (для таймера) отдельную горутину-вейтер, держащую дедлайн. Десять тысяч экземпляров, каждый ждущий неделю, удерживают тридцать тысяч горутин, которые не делают ничего, кроме занятия стеков. - Ожидание — это не исполнение. Семантика исполнения BPMN говорит, что ловящее промежуточное событие / Receive Task / граничное событие ждёт своего триггера и, когда триггер приходит, срабатывает — связывает payload и активирует исходящий поток. Стандарт молчит о среде исполнения: является ли ожидание заблокированным потоком, подпиской или сохранённой записью — выбор движка. Выбор gobpm (ADR-001): горутины — среда исполнения, запись — это состояние, поэтому ожидание, которое не исполняется, не имеет прав на горутину.
- Состояние продолжения уже полностью выносимо наружу (ADR-001 §4.7,
сохранено в §3): будущее дорожки описывается её позицией (узлом),
состоянием дорожки/шага, данными Scope и родословной — на объекте токена
нет скрытого состояния, а возобновляемое состояние узла (дедлайн таймера,
ключ подписки) живёт по контракту состояния на узел, никогда — в
разделяемом неизменяемом определении узла. SRD-070 сделал это конкретным для
восстановления после рестарта: чекпоинт записывает ровно это, а
Restoreпересобирает из него ждущий экземпляр. Дегидратация — та же пересборка, запущенная простоем, а не падением.
2. Решение¶
2.1 Состояние дегидратированной дорожки — горутина завершается¶
Новое терминальное состояние дорожки TrackDehydrated отмечает дорожку,
чьё долгое ожидание вынесено наружу, а горутина завершилась. Когда
экземпляр дегидратируется, каждая его припаркованная на долгом ожидании дорожка
переходит из TrackWaitForEvent в TrackDehydrated, а её рабочая процедура
возвращает управление вместо того, чтобы продолжать блокироваться.
TrackDehydrated терминально для горутины, но не терминально для потока:
дорожка не завершила свой узел, она освободила свой поток исполнения — ожидание
переживает горутину как учётная запись (реестр ожиданий цикла) и, долговечно,
как запись чекпоинта.
Переиспользование машины состояний (вместо отдельного канального протокола) оставляет освобождение на тех же рельсах, что и любой другой переход дорожки: смена состояния, которую цикл уже сериализует, наблюдаемая как факт и захватываемая существующим механизмом чекпоинтов SRD-070.
2.2 Два уровня резидентности¶
У экземпляра два уровня резидентности, и дегидратация — переход между ними:
- Резидентный — цикл экземпляра и горутины его дорожек живы; экземпляр сам держит свои ожидания (сегодняшнее поведение). Экземпляр остаётся резидентным, пока у него есть любая выполняющаяся работа.
- Дегидратированный — каждая дорожка на долгом ожидании, экземпляр
полностью простаивает и освободил все свои горутины: и дорожки
(
§2.1), и сам цикл экземпляра. Экземпляр стоит памяти (свой чекпоинт) и записи в реестре ожиданий движка, но ноль горутин.
Экземпляр дегидратируется, когда достигает условия полного простоя: все живые дорожки припаркованы на долгих ожиданиях и не осталось незавершённой короткоживущей внутренней синхронизации (нет барьера join в процессе слияния, нет выполняющегося шага). Частичная дегидратация — освобождение части горутин дорожек при резидентном цикле — допустимое промежуточное состояние, которое механизм разрешает (оно уже помогает в случае «много ожиданий в одном экземпляре», например при широком параллельном multi-instance), но главный выигрыш — полностью простаивающий экземпляр, освобождающий свой цикл.
2.3 Пробуждение — это fork-продолжение¶
Когда для дегидратированного ожидания приходит триггер, экземпляр не
возобновляет исчезнувшую дорожку. Он гидратируется (пересобирает
освобождённые горутины из записи — путь Restore из SRD-070, теперь запускаемый
триггером, а не стартом движка) и продолжает разбуженное ожидание свежей
дорожкой, родословно-порождённой от дегидратированной (её цепочка prev — id
дегидратированной дорожки и её предки). Это fork-продолжение: он едет на уже
существующей в рантайме машинерии fork/родословной, а проекция токенов/истории
остаётся связной — дегидратированная дорожка показывает узел ожидания как своё
последнее посещение, ребёнок показывает, к чему привёл триггер, и связаны они
родословной.
Свежая дорожка ВХОДИТ В УЗЕЛ ОЖИДАНИЯ ЗАНОВО, с триггером на руках. Она не стартует на узел позже ожидания: узел ожидания выполняет работу момента срабатывания, которая живёт на самом узле — Message-catch связывает пришедший payload с данными процесса и выводит из него корреляционные ключи; catch/receive может нести выходной маппинг. Повторный вход в узел с триггером на руках выполняет этот путь срабатывания ровно один раз и переходит на исходящий поток. Отсюда самый острый инвариант этого ADR, который заодно уточняет восстановление после рестарта:
Есть триггер — продолжаем; нет триггера — перевзводим. Пробуждение по триггеру входит в узел ожидания с триггером и срабатывает сквозь него. Холодный рестарт (SRD-070) входит в узел ожидания без триггера и перевзводит ожидание. Единственный различитель — сопровождает ли гидратацию ожидающий триггер: тот же узел, тот же вход, два исхода.
Триггер граничного события — исключение, подтверждающее правило. Он принадлежит не тому узлу, на котором стоит токен, поэтому он НЕ должен ехать как ожидающий триггер этого узла: повторный вход в охраняемый узел с триггером границы сработает не тем элементом. Поэтому пробуждение по границе — без триггера: экземпляр пересобирается, его ожидания перевзводятся, и граница перевзводится на свой записанный дедлайн. Этот дедлайн уже позади (граница сработала, пока экземпляра не было), поэтому взвод не ждёт снова: токен форкается на самом граничном событии, а охраняемая дорожка становится его родителем — прерывающая граница отменяет этого родителя, непрерывающая оставляет его работать, — что граничное срабатывание и означало всегда. Fork-продолжение и fork границы — одна и та же машинерия, направленная на разные узлы.
2.4 Какие ожидания освобождаются и кто их держит¶
Право на освобождение — это capability, которую объявляет элемент, а не вид,
который перечисляет рантайм. Узел ожидания подписывается на дегидратацию,
реализуя capability Dehydratable, к которой цикл обращается в момент
парковки; узел, который её не реализует, недегидратируем и держит экземпляр
резидентным. Это следует установившейся идиоме рантайма (опциональные
capability-интерфейсы — DeadlineHinter, capability узлов ожидания) и даёт три
вещи, которых жёсткий switch по видам дать не может:
- Расширяемость — пользовательское событие/задача подписывается на дегидратацию, а рантайму не нужно знать о его существовании.
- Политика на элемент, управляемая данными — решение может опираться на собственное вычисленное состояние элемента, то есть это не статическая константа на вид. Timer возвращает false для подпорогового дедлайна (двадцатиминутное ожидание не стоит круговорота «чекпоинт + гидратация») и true для долгого; внешне-воркерный ServiceTask возвращает false безусловно (задание в работе — это активная работа, а не пассивное ожидание, ADR-021).
- Безопасное инкрементальное внедрение — умолчание (нет capability) — «оставаться резидентным», поэтому элементы получают дегидратацию по одному, в ногу со своими держателями.
Право на освобождение (элемент готов освободиться) отличается от держателя
(что-то способно его разбудить) и композируется с ним: экземпляр
дегидратирует ожидание, только если узел Dehydratable и у каждого
взведённого ожидания есть держатель своего вида. Элемент, объявивший себя
дегидратируемым для вида без держателя, — ошибка, от которой рантайм страхует:
экземпляр остаётся резидентным и пишет лог, но триггер не теряется. Узел
catch/receive с одним определением отвечает по своему определению; узел-задача
отвечает напрямую.
У охраняемой активности ожиданий больше, чем видит её токен. Прерывающее граничное событие — это ожидание («согласовать за 24 часа или эскалировать» — это две вещи, которых ждут, а не одна), но оно висит на активности, а не занимает её, поэтому ничто в ожидании токена его не описывает. Освобождение экземпляра, у которого задача была удержана, а граница нет, молча потеряло бы эскалацию: дедлайн проходит, ничего не срабатывает, а запись остаётся в полёте без пути назад. Пропущенный бизнес-дедлайн, не давший никакой ошибки, — худший отказ, который это решение способно породить.
Поэтому взведённая граница — удерживаемое ожидание в собственном праве, а право на освобождение считается по ожиданию, а не по токену: экземпляр освобождается, только когда у каждого ожидания, охраняющего дорожку — и того, на котором она стоит, и каждой границы над ней, — есть держатель. Это то же поарочное правило, которое уже подразумевает Event-Based Gateway, применённое с той гранулярностью, которая нужна границе. Виды границ, разрешаемые напрямую, а не ожиданием (Error, Escalation, Compensation, Cancel — сопоставляются в точке броска или отказа и никогда не подписываются), ничего не взводят и потому не стоят резидентности; Conditional-граница принадлежит циклу и потому неудержима — она держит свой экземпляр резидентным ровно так же, как Conditional-catch.
Дедлайн границы — долговечное состояние, а не значение, которое можно пересчитать. Перевзвод восстанавливает о границе из модели всё, кроме того, когда она срабатывает: повторное вычисление «через 24 часа от текущего момента» при восстановлении даёт 24 часа от восстановления, поэтому эскалация, заданная длительностью, уползает вперёд при каждом восстановлении, и экземпляр, восстановленный достаточно часто, не эскалирует никогда. Поэтому разрешённый дедлайн записывается и восстанавливается — то же правило, которое рантайм уже применяет к собственному таймеру дорожки и от которого граница ускользала.
Event-Based Gateway — это узел ожидания, а не его ветви. EBG взводит
несколько catch-событий на одной дорожке и устраивает между ними гонку
«первый сработавший побеждает», проигравшие отзываются. Решение
Dehydratable — свойство узла ожидания, а для EBG узел ожидания — сам
шлюз, поэтому EBG возвращает Dehydratable == true безусловно и
игнорирует политики своих ветвей-событий: EBG существует только чтобы ждать
гонку, значит он всегда чистая точка ожидания, ради которой стоит
освобождаться (обычный промежуточный catch по таймеру — может и нет, EBG —
всегда). Поэтому дегидратированная дорожка держит множество держателей
ветвей (по одному на взведённое ожидание; для обычного catch множество
вырождается в один); по триггеру победившей ветви fork срабатывает этой ветвью
и отзывает держателей соседних ветвей (освобождая их дедлайны/подписки) —
ровно тот Withdrawn, который резидентный EBG выполняет сегодня. Гарантия
существования держателя по-прежнему применяется поарочно: EBG освобождается,
только когда у каждого вида его ветвей есть держатель (полностью таймерный EBG
— как только появится таймер-сервис; EBG с message-ветвью — как только появится
держатель сообщений).
Кто держит освобождённое ожидание. Резидентный экземпляр держит свои ожидания сам — его живой цикл и есть подписчик, которому доставляется триггер. Дегидратированный не может: его цикла нет, а триггер, доставленный освободившемуся экземпляру, теряется. Решение:
Освобождаемое ожидание регистрирует как
EventProcessorсвоего держателя — не дорожку и не экземпляр, — помеченного id экземпляра, id дорожки и дескриптором ожидания. Держатель — постоянный подписчик; источник триггера (EventHub или сервис таймеров движка) указывает на него с момента взвода ожидания, поэтому триггер никогда не приходит освободившемуся экземпляру.
По триггеру держатель ветвится по резидентности экземпляра:
- Резидентный (у экземпляра ещё есть цикл — например, он дегидратировал только часть дорожек или не дегидратировался вовсе) → держатель возобновляет живую припаркованную дорожку — сегодняшняя доставка, достигнутая через держателя.
- Дегидратированный → держатель будит экземпляр:
Hydrate(пересборка из чекпоинта), а затем — вместо того чтобы перевзвести разбуженное ожидание и доставить повторно — порождает fork-продолжение, которое готовит вход узла из триггера и выполняет путь срабатывания узла ожидания напрямую (§2.3); fork не регистрируется какEventProcessorзаново, потому что триггер уже на руках. Это закрывает разрыв «доставка освободившемуся циклу» в корне: ничего никогда не проталкивается исчезнувшему экземпляру — подписан был держатель.
Паттерн держателя — одно решение с реализацией на каждый вид, потому что каждый источник триггера — своя подсистема:
- Timer → держатель — сервис таймеров уровня движка. Горутина-вейтер на каждое ожидание (сегодня — по одной на ждущий экземпляр) заменяется единым сервисом, хранящим дедлайны с ключом по id экземпляров; на ближайшем дедлайне он срабатывает. Это тот самый долговечный сервис таймеров, который нужен ADR-033 §2.5 и который отслеживает issue #84, — дедлайн и есть дескриптор ожидания, который SRD-070 уже записывает.
- Message / Signal → подписка EventHub нацелена на держателя, с ключом по id экземпляра и с корреляционными ключами из чекпоинта. Подписка переживает освобождение экземпляра; корреляция (ADR-016 v.1) переезжает на держателя — он сам проверяет совпадение ключей разговора, поэтому сообщение с чужим ключом никогда не разбудит экземпляр.
- Пользовательская задача → держатель — путь завершения в дистрибьюторе задач. Припаркованная задача уже живёт во «входящих» дистрибьютора (ADR-020) независимо от резидентности экземпляра; завершение будит экземпляр, а выходы завершения становятся входом узла для fork'а — там, где сегодня проталкивается запрос в живой канал.
- Задание внешнего воркера → не дегидратируется (
Dehydratableего узла возвращает false). Задание воркера — это активная работа в полёте (ей владеет очередь fetch-and-lock из ADR-021), а не пассивное ожидание: экземпляр остаётся резидентным, пока задание не отчитается. Долговечность самой очереди — предмет ADR-021.
Экземпляр дегидратируется, только когда каждая живая дорожка припаркована на
Dehydratable-ожидании, у вида которого есть держатель; всё остальное —
недегидратируемый элемент, вид ожидания, чей держатель ещё не приземлился,
задание воркера в полёте — держит экземпляр резидентным. Право на освобождение и
держатели внедряются поэлементно и повидово, и пока нет обоих, такое ожидание
просто остаётся резидентным — но триггер не теряется.
2.5 Эффекты и идемпотентность при пробуждении¶
Пробуждение заново входит в узел ожидания и заново выполняет его путь срабатывания — позиция at-least-once, согласованная с ADR-033 §2.3. Срабатывание идемпотентно по построению: таймер срабатывает своим исходящим один раз (просроченный дедлайн схлопывается в одно срабатывание, ADR-033 §2.5); сообщение связывает payload и идёт дальше; корреляция дедуплицирует повторно доставленное сообщение по ключу (ADR-016). Никакое пробуждение не переигрывает завершённый шаг — дегидратированная дорожка не завершала свой узел, поэтому пробуждение и есть первое и единственное срабатывание узла.
2.6 Одна модель, два уровня резидентности¶
Этот ADR — in-memory утверждение; ADR-033 §2.4/§2.5 — его долговечная проекция: одна модель, два уровня резидентности.
- В памяти дегидратация освобождает горутины, а держатель ожидания хранит само ожидание; гидратация пересобирает горутины из живой записи.
- Долговечно та же запись и есть чекпоинт (SRD-070): она переживает
рестарт, а восстановление после рестарта — это гидратация, триггером которой
служит «движок запустился». Дегидратированные экземпляры упавшего движка и его
резидентные экземпляры восстанавливаются одним и тем же путём
Restore— дегидратация не сделала их особым случаем, она сделала каждый ждущий экземпляр заранее пригодным к восстановлению.
Поэтому дегидратация не добавляет новой модели персистентности: она переиспользует чекпоинт SRD-070 как источник гидратации и добавляет только триггер освобождения горутин (простой), триггер пробуждения (событие через держателя) и повторный вход через fork-продолжение.
2.7 Наблюдаемость¶
Резидентность — свойство жизненного цикла, значимое для оператора («сколько из
моих десяти тысяч экземпляров дегидратированы» — реальный вопрос), поэтому оба
перехода резидентности наблюдаемы на объёме жизненного цикла (ADR-013 v.2):
факт Dehydrated, когда экземпляр освобождает горутины (детали:
припаркованные виды ожиданий и их количество), и факт Hydrated, когда
держатель его будит (детали: вид разбудившего триггера, продолжил ли он поток
или завершил экземпляр). Один факт на переход — не на чекпоинт (чекпоинт
сопровождает уже наблюдаемый переход) и не на каждое ещё взведённое ожидание.
Переходы резидентности едут по существующему потоку наблюдаемых событий и по
эху в операторский лог, чисто с точки зрения маскирования (имена и счётчики,
никогда payload). Смена состояния TrackDehydrated видна через существующую
проекцию состояний дорожек/токенов, поэтому токены дегидратированного экземпляра
по-прежнему проецируют свои позиции ожидания.
3. Инварианты рантайма, которые это сохраняет (ADR-001 §4.7)¶
- Продолжение дорожки полностью описывается позицией, состоянием
дорожки/шага, данными Scope и родословной — скрытого состояния токена нет.
Дегидратация на это опирается: дочерний fork пересобирается ровно из этого, а
дегидратированная дорожка добавляет только состояние (
TrackDehydrated) и свой дескриптор ожидания. - Возобновляемое состояние на узел (дедлайн таймера, ключ подписки) живёт по контракту состояния на узел, а никогда — в разделяемом неизменяемом определении узла, поэтому множество экземпляров, дегидратированных на одном узле, разделяют одно определение и держат свои различные ожидания в собственных записях/держателях.
- Цикл — единственный писатель состояния экземпляра (ADR-017): дегидратация
и переход в
TrackDehydratedпроисходят на цикле до его выхода, поэтому освобождение — согласованный срез, а пробуждение входит обратно через цикл, который оно же пересобирает.
4. Рассмотренные альтернативы¶
| Альтернатива | Плюсы | Минусы | Решение |
|---|---|---|---|
| A. Блокировать горутину на время ожидания (статус-кво) | тривиально; объект дорожки остаётся живым и возобновляется на месте | нарушает ADR-001 §4.7; O(ждущих) горутин удерживаются бесконечно — ровно та цена, ради устранения которой эта работа и существует | ❌ отклонено — это и есть постановка проблемы |
| B. Освободить горутину, но возобновлять ТУ ЖЕ дорожку (регидратировать заблокированный объект) | нет форка родословной; токен остаётся одной непрерывной дорожкой | требует восстановления точного состояния горутины/стека либо отдельного объекта «приостановленной дорожки» со своим протоколом возобновления вне машины состояний; воюет с моделью неизменяемого продолжения | ❌ отклонено — §2.1/§2.3: свежий потомок родословной пересобирается из вынесенного состояния без отдельного протокола |
| C. Свежая дорожка, стартующая на узел ПОСЛЕ ожидания | простейшее продолжение — без повторного входа в узел ожидания | пропускает работу момента срабатывания узла — Message-catch потерял бы связывание payload'а и вывод корреляционных ключей; корректно только для триггеров без работы срабатывания (таймеров) | ❌ отклонено — §2.3: входим в узел С триггером, чтобы работа срабатывания выполнилась; для таймеров это совпадает со «следующим узлом», для сообщений — корректно |
D. Состояние TrackDehydrated + fork-продолжение с повторным входом в узел ожидания (выбрано) |
переиспользует машину состояний, машинерию fork/родословной и чекпоинт SRD-070; одна модель с восстановлением после рестарта; повидовое инкрементальное внедрение | нужен держатель ожидания уровня движка на каждый вид (§2.4) — реально, но приземляется независимо | ✅ выбрано |
| E. Долговечный журнал подписок / event-sourced пробуждения | идеальный аудит пробуждений | вторая модель персистентности рядом с чекпоинтом; ADR-033 §4 уже отклонил event sourcing для состояния | ❌ отклонено — §2.6: чекпоинт — единственная запись, а держатели — in-memory индексы, восстанавливаемые из неё |
5. Отложенное и порядок внедрения¶
- Держатели по видам внедряются независимо (§2.4). Таймер (закрывающий #84) — самый чистый первый держатель: внешнего актора нет, детерминированный дедлайн уже лежит в записи. Держатели message/signal и пользовательских задач следуют за ним; пока у вида нет держателя, он остаётся резидентным, не теряя триггеров.
- Приостановка/возобновление оператором (ADR-033 §2.6) — та же дегидратация, запускаемая оператором, а не простоем, на той же машинерии; следующий срез.
- Пробуждение в мультиузловой конфигурации (триггер приходит движку B для экземпляра, дегидратированного под арендой движка A) едет на ограждении владения из ADR-033 §2.8 и на ADR Distribution & Scale — вне области этого документа; одноузловой дегидратированный экземпляр будит его собственный держатель.
Сопровождающий SRD приземляет механизм (TrackDehydrated, детектор
простоя, пробуждение через fork-продолжение) и держателей ожиданий поверх
чекпоинта SRD-070.
6. Ссылки¶
- ADR-001 v.6 Execution Model §4.7 — инвариант, который здесь реализуется.
- ADR-006 v.4 Events & Subscriptions — доставка триггеров.
- ADR-033 v.1 Persistence & State §2.4/§2.5 — долговечная проекция.
- ADR-016 v.1 Message Correlation, ADR-020 v.1, ADR-021 v.1 — корреляция и держатели ожиданий.
История документа¶
| Версия | Дата | Автор | Изменения |
|---|---|---|---|
| v.2.1 | 2026-07-29 | Руслан Габитов | Граничные события — удерживаемые ожидания: концепция, установленная реализацией, записана на том слое, которому принадлежит. §2.4: у охраняемой активности ожиданий больше, чем видит её токен, поэтому право на освобождение считается по ожиданию, а не по токену — экземпляр освобождается, только когда удержаны и ожидание, на котором стоит токен, и каждая граница над этой дорожкой. Освобождение при удержанной задаче и неудержанной границе молча теряет эскалацию «согласовать за 24 часа или эскалировать» — худший отказ, который это решение способно породить. Виды границ, разрешаемые в точке броска/отказа (Error, Escalation, Compensation, Cancel), ничего не взводят и не стоят резидентности; Conditional-граница принадлежит циклу и неудержима. Дедлайн границы — долговечное состояние: повторное вычисление длительности при восстановлении перезапускает отсчёт, и экземпляр, восстановленный достаточно часто, не эскалирует никогда — то же правило, которое уже действовало для собственного таймера дорожки и от которого граница ускользала. §2.3: триггер границы принадлежит не припаркованному узлу, поэтому пробуждение по границе — без триггера, а срабатывание — fork на самом граничном событии с охраняемой дорожкой в роли родителя; fork-продолжение и fork границы — одна машинерия, направленная на разные узлы. |
| v.2 | 2026-07-27 | Руслан Габитов | Написан полностью, когда работа над долгими ожиданиями дошла до приземления (обещание черновика v.1). Решает in-memory механизм дегидратации/пробуждения: терминальное состояние TrackDehydrated (горутина дорожки завершается), два уровня резидентности (резидентный / полностью простаивающий дегидратированный, освобождающий и цикл), пробуждение как fork-продолжение (свежая дорожка, родословно-порождённая от дегидратированной, входящая в узел ожидания заново с триггером на руках, чтобы работа момента срабатывания — связывание payload'а сообщения, выходной маппинг — выполнилась один раз) и заострённый инвариант есть триггер — продолжаем / нет триггера — перевзводим (который заодно уточняет восстановление после рестарта из SRD-070). §2.4 делает право на освобождение capability Dehydratable, объявляемой элементом (управляется данными: Timer остаётся резидентным ниже порога дедлайна, воркерный ServiceTask не дегидратируется никогда; Event-Based Gateway — это узел ожидания и возвращает true безусловно, игнорируя свои ветви, держит множество держателей ветвей и отзывает проигравших при пробуждении), композируемой с держателем (освобождаемся, если элемент готов И у каждого взведённого вида есть держатель); и держатель — а не дорожка и не экземпляр — регистрируется как EventProcessor (помеченный id экземпляра/дорожки): постоянный подписчик ветвится по резидентности (резидентный → возобновить живую дорожку; дегидратированный → гидратация + прямое срабатывание узла ожидания из триггера, без перерегистрации), что закрывает в корне разрыв «доставка освободившемуся циклу» и переносит корреляцию сообщений на держателя. Паттерн держателя ожидания на каждый вид (сервис таймеров движка — #84; подписка EventHub, нацеленная на держателя; завершение в дистрибьюторе задач), внедряемый инкрементально; задания воркеров остаются резидентными (активная работа, а не ожидание). Одна модель, два уровня резидентности вместе с ADR-033: новой модели персистентности нет — чекпоинт SRD-070 и есть источник гидратации. §2.7 делает резидентность наблюдаемой (факты Dehydrated/Hydrated на объёме жизненного цикла). Отклонённые альтернативы: блокировать горутину, возобновлять ту же дорожку, стартовать после узла ожидания, event-sourced пробуждения. |
| v.1 | 2026-06-07 | Руслан Габитов | Первичный черновик — модель освобождения долгих ожиданий в памяти, перенесённая из ADR-001 v.3 §4.7. Долговечная версия остаётся в ADR по Persistence & State. Ещё не реализовано. |