T27.AI

Блог

Акторы обходят цикл в восстановлении, но не везде: семь измеренных изменений рантайма Queen

2026-10-10 · 14 мин чтения

[симуляция: ревьюер, параллелизм, остановка ходов, диспетчер; замер: хаос связи узлов и ожидания на PostgreSQL 16, повтор решений, шлюз симуляции; ПЛИС: только синтез; в production ничего нет] На одном входе с фиксированным зерном рантайм акторов Queen сокращает худшее восстановление с 1181 с до 27 с и при перегрузке завершает на 18.7% больше рецензий, но выигрывает не везде. 2026-10-09 за флагами легли семь изменений по итогам изучения конкурентов, у каждого спецификация на t27 и бенчмарк против кода, который оно заменяет: журнал, повторяющий 73 435 решений с 0 расхождений, ходы, которые действительно останавливаются, ожидания как строки Postgres (чтений GitHub 24 вместо 180), связь узлов с фенсингом (потерянных писем 0 вместо 21) и шлюз симуляции с зерном, который проходит за 33.5 с и первым нашёл настоящую ошибку рантайма. Большую часть выигрыша диспетчера дала 60-минутная граница хода, а не акторы, а адаптивный параллелизм проигрывает при 3 полосах. На XC7A200T карта как она написана стоит 26 737 LUT, и 13 функций не синтезируются; предел около 37 000 акторов на плату ставит память, а не логика.

Титульная карточка статьи, иллюстрации пока нет: Акторы обходят цикл в восстановлении, но не везде: семь измеренных изменений рантайма Queen
Открыть титульную карточку в исходном размере
#t27#Queen#Measurement#FPGA

На одном и том же входе с фиксированным зерном рантайм акторов Queen обходит старый цикл опроса там, где работа ломается: худшее восстановление после падения или зависания сокращается с 1181 с до 27 с, а при перегрузке рецензий выходит на 18.7% больше. Обходит не везде. 2026-10-09 на интеграционную ветку actors-next легли семь изменений по итогам изучения конкурентов, у каждого — спецификация на t27, тесты и бенчмарк против кода, который оно должно заменить. Два из них проигрывают в случаях, которые мы можем назвать, одно сознательно отдаёт пропускную способность, а большая часть выигрыша диспетчера оказалась заслугой простого лимита времени, а не акторов. Новый шлюз симуляции на первых же прогонах нашёл настоящую ошибку рантайма. Каждое новое поведение стоит за флагом, выключенным по умолчанию, а исправлениям ошибок флаг не нужен. В production пока ничего из этого не работает.

Ниже встречаются числа двух видов, и у каждого есть пометка. Симуляция — прогон с фиксированным зерном на виртуальных часах, где частоты сбоев и длительности задач — допущения. Замер — настоящие процессы ОС, настоящий PostgreSQL 16 или настоящий инструмент синтеза.

Queen и правило, которому она подчиняется

Queen — планировщик роя агентов t27. Она раздаёт issue на GitHub рабочим агентам — пчёлам, рецензирует их pull request и запускает релизные задания. С 2026-10-08 действует правило владельца: каждая параллельная часть Queen становится актором с pid, почтовым ящиком и супервизором, и ни один цикл не заменяется, пока не опубликованы MVP, его тесты и бенчмарк на том же входе (t27#7851). Каждое решение рантайм получает вызовом скомпилированной спецификации t27 — «карты». Хост на TypeScript только держит состояние и выполняет ввод-вывод.

С чего начали: победа в симуляции, горячий цикл в production

Первым доменом акторов стал ревьюер pull request (trios#1698). Нагрузка: пуассоновские прибытия, 5% попыток падают, 2% зависают на 30 минут, горизонт 7 часов симуляции. Без сбоев цикл и акторы идут вровень: done p95 319 с против 309 с. Со сбоями акторы сокращают done p95 с 553 с до 384 с, а худшее восстановление — с 1181 с до 27 с. При 120 прибытиях в час они завершают 565 строк из 660 против 476. Сообщение стоит 3.7 мкс на кольце из 1000 акторов. [симуляция]

Production показал то, чего симуляция не моделировала. Ревьюер на акторах заработал в 19:00Z 2026-10-08. К 22 строкам, ветку которых нельзя прочитать, он ходил примерно 184 раза в минуту, а цикл — около 20 раз. Backoff исправил это в течение часа (trios#1702). За первые 10.5 минуты после него ревьюер был занят 23% своих воркер-секунд (584 из 2520). Через день живая Queen показывала около 11 рецензий в час против примерно 58 завершённых диспетчеризаций (reviewer.t27). Ничто в рантайме не могло сказать, какой из замеров верен. [замер, production]

Что есть у конкурентов и чего не было у нас

Исследование сравнило рантайм с BEAM/OTP, Akka, Orleans, движками долговременного исполнения — DBOS, Temporal, Restate, Hatchet — и агентными фреймворками на акторах. Оно нашло три пробела, и ни один из них не является недостающей функцией BEAM. Akka и Orleans из коробки меряют глубину ящика, время в ящике и долгие ходы; у нас были строки логов. OTP, Orleans и Effect останавливают работу по протоколу: сигнал, дедлайн, финализаторы, затем kill. Мы же бросали ход и давали ему работать дальше. DBOS, Hatchet и Cloudflare делают из долгого ожидания строку базы, которая не держит процесс.

Нашлось и то, в чём рантайм уже впереди. Аренда узла в 20 с при пульсе 5 с замечает мёртвый узел раньше, чем net_ticktime BEAM по умолчанию (от 45 до 75 с), чем split-brain resolver Akka по умолчанию (около 45 с) и чем Orleans 9 (90 с). Наша контрольная полоса, которая обслуживает сигналы раньше данных, — та же идея, что OTP 28 выпустил как priority messages. Зависший ход мы убиваем, а Orleans о нём только сообщает (MaxRequestProcessingTime). Это задокументированные значения по умолчанию самих вендоров, а не наши замеры.

Семь изменений, и каждое измерено против того, что оно заменяет

Семь пунктов — это эпик trios#1712. Каждая спецификация сначала влита в t27, а каждый рантайм лёг на actors-next отдельной PR. В таблице — главное число каждого пункта; цена — в разделах после неё.

#Изменение (спецификация, рантайм, флаг)Что сдвинулосьОснование
1Телеметрия и воспроизводимый журнал решений (t27#8285, trios#1725, TRIOS_QUEEN_ACTORS_TELEMETRY=on)73 435 решений карт воспроизведены с 0 расхождений; с телеметрией и без неё расписание одно и то жезамер повтора; цена — оценка
2Ходы, которые действительно останавливаются (t27#8281, trios#1720, TRIOS_QUEEN_TURN_STOP=on)одновременных рецензий во время зависаний: до 7-10, теперь не больше 4; живых процессов-внуков после kill: 10 из 10, теперь 0симуляция; тест процессов — замер
3Параллелизм по очереди (t27#8275, trios#1716, TRIOS_QUEEN_REVIEWER_ADAPTIVE=1)перегрузка: 565 из 660 строк при 80.7/ч, теперь 660 из 660 при 94.3/чсимуляция
4Долгие ожидания как строки (t27#8272, t27#8282, trios#1722, TRIOS_QUEEN_WAITS=rows)одно 90-минутное ожидание CI: 180 чтений GitHub, теперь 24; 180 записей в журнал задания, теперь 0замер на PostgreSQL 16, виртуальные часы
5Симуляция с зерном как шлюз CI (t27#8292, t27#8306, trios#1726)18 случаев по 10 000 шагов, каждый дважды, за 33.5 с в CI и с 0 расхождений; 4 воссозданных дефекта пойманы, и одна настоящая ошибка найдена первойзамер (собственные прогоны шлюза)
6Фенсинг связи узлов и инкарнации (t27#8278, trios#1724, вызывающих пока нет)потеряно писем: 21, теперь 0; pid переиспользован: 5 из 5, теперь 0; писем от замёрзшего узла: 40, теперь 0замер на PostgreSQL 16, процессы ОС
7Ключевые акторы и диспетчер пчёл (t27#8286, trios#1723, TRIOS_QUEEN_DISPATCH=actors)восстановление после падения, p50 при 6-минутных раундах: 701 с, теперь 149 с; двойных захватов на двух узлах: 0симуляция

Видеть: журнал решений, который воспроизводится

Пункт 1 считает события по видам акторов, ведёт гистограммы времени хода и времени в ящике и поднимает пороговые события с зазором, как монитор long_message_queue в OTP 27. Тревога по глубине ящика включается на 192 и выключается на 64 из 256 мест. Долгий ход — половина границы kill, 150 с для рецензии. Шторм рестартов отмечается за один рестарт до того, как супервизор сдастся. Записи берутся выборочно: 8 сообщений на вид актора и 16 вызовов на функцию карты в секунду, в кольцо на 4096 записей.

Каждое решение уже является вызовом скомпилированной спецификации, поэтому журнал этих вызовов делает рантайм воспроизводимым. Прогон ревьюера с зерном записал 73 435 решений от 38 функций трёх карт; повтор на закреплённых картах дал 0 расхождений. Испорченный, пустой или чужой журнал, как и журнал с записями вне выборки, проверку не проходит. Из 13 впрыснутых падений счётчики увидели 12: тринадцатое закончилось после того, как рестарт rest_for_one уже заменил его воркер. [замер]

На спокойной машине (нагрузка около 4; 1000 акторов передают 100 000 сообщений, 21 прогон) телеметрия добавляет +3.3% CPU на сообщение по медиане, от +1.6 до +5.2% между p25 и p75. Это в пределах цели +10%. Тот же прогон нашёл цену, которой мы не хотели: без телеметрии сообщение стоило 4.5 мкс против 3.7 мкс, опубликованных до семи лейнов. Профиль (trios#1746) показал, что эта цена старше лейнов: 44% времени уходило на повторный поиск карты при каждом вызове по ключу длиной больше 100 символов, а слот, известный с момента запуска, запрашивался трижды на сообщение. После обоих исправлений сообщение стоит 1.55 мкс — в 2.4 раза меньше прежней базы; это исправление ушло в production со второй партией (trios#1754). Телеметрия стоит столько же в абсолютных числах, поэтому на подешевевшей базе добавляет 8–13% — на границе цели +10%. Следующее изменение (trios#1755) сократило собственные инструкции телеметрии на сообщение на 27–33%; отношение на спокойной машине ещё предстоит измерить. Production-замера ещё нет, поэтому вопрос «23% занятости против 11 рецензий в час» остаётся открытым.

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

Пункт 2 даёт каждому ходу AbortSignal. Вызов модели, команды git и проверки критериев ему подчиняются, остановленная рецензия не пишет вердикт, а эскалация убивает всю группу процессов, никогда не один pid. Раньше рецензия, убитая на своей границе, держала полосу z.ai до собственного 120-секундного таймаута. В симуляции одновременных рецензий во время зависаний становится не больше 4 вместо 7-10, и полоса освобождается через 0 с виртуального времени после kill вместо 48-74 с (p50); настоящая отмена вызова модели заняла 2.6-27.5 мс. [симуляция]

Тест с настоящими процессами: 10 ходов, каждый оставляет внука sleep, половина глуха к SIGTERM, kill на границе в 1 с. При старом proc.kill(9) одному pid все 10 внуков были живы через 10 с. При убийстве группы не выжил ни один, а глухой к SIGTERM ход освобождался за 3012 мс. [замер]

Цена видна при перегрузке. С глухими зависаниями 78.4 рецензии в час падают до 60.7; с зависаниями, которые слышат отмену, 76.6 падают до 68.4. Старый код не был быстрее: он работал за своей границей. Во втором случае он отработал за границей 12 216 рецензия-секунд, это около 68 рецензий медианной длины при разнице в 57. В симуляции нет лимита полос на ключ, поэтому рецензия за границей там бесплатна. В production это третий одновременный запрос на ключ, и z.ai отвечает на него ошибкой 1302. При постоянной нагрузке пропускная способность одинакова. [симуляция]

Дозировать: больше воркеров выигрывают, только если для них есть полосы

Пункт 3 заменяет фиксированный пул из 4 ревьюеров пулом, размер которого выводится из очереди, свободных полос модели и памяти (reviewer_sizing.t27). При 47 свободных полосах — столько показывал /queen/status 2026-10-09 — он разгребает перегрузку: 660 из 660 строк при 94.3 в час против 565 из 660 при 80.7. Всплеск из 120 строк закрывается за 2895 с вместо 11 690 с. Цена — больше одновременных вызовов модели: до 16 против 7. [симуляция]

Проигрывает он в двух случаях. При всего 3 полосах фиксированный пул завершает 429 из 660, а адаптивный — 360. Рецензия держит полосу только на время вызова модели, поэтому пул, ограниченный числом полос, оставляет каждую полосу простаивать до конца рецензии. В контейнере с 1.5 ГБ свободной памяти и принятыми 512 МБ на рецензию пул ограничен тремя и завершает 399 против 565. Флаг остаётся выключенным, пока телеметрия не измерит свободные полосы и память на рецензию. [симуляция]

Ждать: строка вместо вопроса каждые 30 с

Пункт 4 паркует долгое ожидание строкой в Postgres, queen_wait, а будит её актор-планировщик. Начинается он с поправки к исследованию. 90-минутное ожидание CI в релизном задании не жило в памяти процесса: задание уже было строкой, и рестарт ничего не терял. Стоило оно чтения GitHub и записи в журнал задания примерно каждые 30 с.

За это ожидание, на PostgreSQL 16 и виртуальных часах, чтений GitHub становится 24 вместо 180, а записей в журнал задания — 0 вместо 180. Ни одно ожидание не теряется при принудительном рестарте ни в старой, ни в новой версии. Заметить, что прогон CI закончился, теперь занимает 83 с вместо 23 с при границе 240 с вместо 30 с, потому что раунд заменён опросом раз в 15 с. Время процесса без задержки GitHub растёт с 1108 мс до 1463 мс, около половины — этот общий опрос. С учётом измеренных 51 мс на чтение GitHub старый способ ждал GitHub 9.2 с, а новый — 1.2 с. Разрыв в обнаружении закрыл бы вебхук; он не подключён. [замер]

Доказывать: шлюз симуляции, который нашёл настоящую ошибку

Пункт 5 превращает виртуальные часы в шлюз CI (simulation.t27, t27#8292 и t27#8306; харнесс trios#1726). Он гоняет настоящий рантайм на трёх узлах: акторы ревьюера, удалённых детей, сеть в памяти, а на двух зёрнах — настоящую связь через Postgres поверх симулированного хранилища. Зёрна и смесь сбоев выбирает карта: потеря узла, зависший ход, переполнение ящика, нечитаемая строка, 429 от провайдера, устаревшая аренда, падение рецензии. Случаев 18: 16 в сети в памяти и 2 на хранилище, по 10 000 шагов каждый. Каждый случай прогоняется дважды, два лога обязаны совпасть, а все случаи вместе обязаны достичь восьми редких состояний, которых требует карта. В CI шлюз занимает 33.5 с, всё задание — 52 с. Четыре прогона шлюза подряд на итоговом коде дали 0 прогонов с разными логами. [замер]

Чтобы проверить, что он ловит то, что должен, каждый известный дефект возвращали в настоящий исходный код и снова запускали шлюз:

Возвращённый дефектПровалено случаевПервый провал
Нет backoff ожидания (горячий цикл при запуске)18 из 18зерно 3600507402, шаг 421: одну строку посетили 5 раз за виртуальную минуту
Связь узлов подтверждает письмо до его обработки2 из 2 случаев на хранилищезерно 3600507402, шаг 2637
pid без инкарнации18 из 18зерно 3600507402, шаг 2543
Ход выполняется после остановки своего процесса2 из 18зерно 3600507402, шаг 7690

О дефекте из последней строки мы не знали: его нашёл шлюз. На первых прогонах шлюз провалил 5 из 64 зёрен: ход выполнялся для процесса, который уже завершился. Рантайм забирает письмо и выполняет ход на микрозадачу позже, а между этими моментами супервизор может остановить процесс. Теперь queen-actors.ts выполняет ход, только пока его процесс актуален, и на это есть регрессионный тест. На нагруженном Mac проверка ничего не стоила сверх шума (медиана 11.4 мкс на сообщение с ней и 11.65 мкс без неё). А на коде до пункта 6 симулированное хранилище воспроизвело оба дефекта связи узлов без единого изменения исходного кода.

У шлюза два ограничения. Ключевых акторов и адаптивного пула ревьюеров в его симулированном мире пока нет. И каждую ночь его никто не запускает: GitHub выполняет расписания только на ветке по умолчанию, а кода Queen там нет.

Фенсинг: связь узлов стала безопасной до первого вызывающего

Пункт 6 чинит связь узлов через Postgres раньше, чем ею кто-то воспользуется; в production у неё нет вызывающих. netlink.t27 кладёт номер инкарнации в каждый pid и проверяет эпоху аренды при каждой записи. Пульс переезжает в свой поток, а узел отключает себя сам через 15 с без продления — на один пульс раньше, чем любой сосед сможет счесть его мёртвым по аренде в 20 с. Один и тот же сценарий хаоса прогнан до и после, на настоящем PostgreSQL 16, где каждый узел — отдельный процесс ОС. [замер]

Случай хаосаДоПосле
Потеряно писем, 10 ответов на чтение сброшены посреди доставки (из 200)210
Потеряно писем за одной недекодируемой строкой (из 20)100
pid переиспользован за 5 рестартов5 из 50
Письма прошлой инкарнации доставлены новой25 из 250
Писем от узла, замёрзшего на 30 с, после того как сосед счёл его мёртвым400
Ложный node-down, пока один ход держал цикл до 25 с10

Каждый из шести дефектов сначала воспроизведён красным. Цена — подтверждение: 6.16 SQL-операторов почты на круг вместо 4.11. CPU сдвинулся на 7-8%, в пределах разброса между прогонами на Mac с нагрузкой от 21 до 42. Запись ниже прогоняет само правило фенсинга: спецификация проходит, фенсинг сдвигается с 15 с на 20 с аренды, падает ровно этот тест, и git возвращает файл.

t27c test-report specs/queen/netlink.t27 · фенсинг раньше решения соседей

Семь команд, 51.2 с реального времени (показано 34.2 с); каждая строка оболочки возвращает 0. netlink.t27 проходит 9 из 9 тестов, ни один не пустой. Сдвиг самоотключения с 15 с на 20 с аренды проваливает ровно a_node_stops_before_any_peer_can_see_it_down, и git checkout возвращает тот же sha256. Эта сборка t27c 0.5.0 старше t27#8152, поэтому test-report ещё выходит с 0 при FAIL; вердикт — строка FAIL. Открыть запись на отдельной странице.

Диспетчеризация: большую часть выигрыша дал лимит времени

Пункт 7 запускает диспетчер пчёл как набор акторов, по одному на issue, со связями, вызовом и упорядоченной остановкой. В его бенчмарк добавлен контроль: старый цикл, у которого раннеры ограничены 60-минутной границей хода, как у акторов. При 16 полосах со сбоями цикл завершает 262 issue, ограниченный цикл — 349, акторы — 352. Акторы добавляют 3, то есть 0.9%; остальное дал лимит. [симуляция]

Акторы выигрывают там, где раунды медленные и где пчёлы падают. При 6-минутных раундах они завершают 330 против 307 (+7.5%), а p50 восстановления после падения снижается с 701 с до 149 с. Повтор при нехватке полос они проигрывают: 20-27 с у цикла против 60-98 с. На всплеске они завершают 612 против 616, потому что бросают issue после 3 засчитанных провалов. Двойных захватов 0 у каждого рантайма на одном узле. На двух узлах диспетчера акторы держат 0, когда держатель захвата — pid, а в негативном контроле их 273. Предложение в t27#7851 — не заменять цикл по этим числам: 60-минутный лимит для нынешнего цикла сразу забирает большую часть выигрыша. [симуляция]

Может ли карта работать на ПЛИС?

Карта решений — чистая спецификация t27, поэтому t27c может опустить её в Verilog. Мы синтезировали её под XC7A200T нашей стендовой платы, чтобы увидеть, сколько стоил бы рантайм акторов в железе. Это только синтез, yosys 0.63: ни размещения и трассировки, ни тактовой частоты, на плату ничего не загружалось. Происхождение чисел — в trinity#1582.

Чего это не показывает

Чего это не решает

Пруфы

Поработаем вместе

Нужно довести FPGA/RTL-задачу до замеров на железе?

Работаю по контракту и part-time с hardware-AI, FPGA/RTL и ML-системами — от спецификации и открытого тулчейна до воспроизводимых измерений.