Егор Урванов

Как найти состояние гонки (race condition) в кодеМодель на Quint

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

создано · обновлено · 14 мин · · Спеки и контекст

Состояние гонки (race condition) в коде находят, сравнив все порядки, в которых могут выполниться две операции. Тесты проверяют порядки, которые вы придумали, а модель на Quint перебирает их сама и показывает цепочку шагов, где правило нарушается. Так ловят «упало посреди записи» и «два администратора сразу», в том числе в коде ИИ-агента.

!Тестодна дорожка, жук в тениМодельвсё поле, жук найден● модель, люди, системы, данные  ● агент и харнесс  ● риск, стоп, граница

Тест идёт по сценарию, который вы придумали, а модель обходит все сценарии маленького мира.

Как работает модель Quint: идея вместо угадывания сценариев

Название Quint легко спутать с тестовым движком QUnit, но это другая программа: сравнение с тестами и TLA+. Чем Quint отличается от TLA+ и Alloy, разобрано в сравнении трёх языков. Обычный тест — это один сценарий, который вы придумали заранее: нажали, подождали, проверили результат. Бага, о котором вы не подумали, в таком сценарии нет. Модель устроена иначе. Вы описываете состояния системы и разрешённые действия, а программа сама пробует все порядки действий в маленьком мире и показывает первый, где правило нарушилось.

ТестМодель▶12345▶Один придуманный сценарийкрасная комната не найденаВсе ходы, пять шагов до нарушенияцепочка шагов — готовый контрпример● модель, люди, системы, данные  ● агент и харнесс  ● риск, стоп, граница

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

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

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

Вывод
В этом примереТест проверяет сценарий, который вы придумали, а модель проверяет все, которые возможны в выбранном маленьком мире.
В общем случаеБаги вида «два действия пересеклись во времени» почти не приходят в голову заранее.
Что сделатьВыберите одно правило, которое вам страшно потерять, и опишите его в десяти строках.

Какие ошибки находит модель Quint: примеры

Главная ценность модели в находках, которые никто не искал. Ниже две самые показательные и список остальных. У всех одна форма, это гонка: два действия, каждое по отдельности правильное, вместе дают потерю или путаницу.

История 1: гонка входа и выхода

Разберём первую историю подробно. Допустим, у человека в системе есть сессия в Telegram, и админ может её завершить. Одновременно бот может использовать эту сессию для отправки сообщения. Код сначала проверил, что сессия живая, затем пошёл отправлять. Между проверкой и отправкой админ успел выйти. В итоге сообщение ушло от имени человека, который уже закрыл доступ.

Ни один тест такого не ловил, потому что каждый шаг по отдельности работает правильно. Ошибка живёт только в порядке «проверка, выход, отправка», и модель нашла этот порядок сама.

?Как быловошли разом, файла нетКак сталозамок, по очереди, файл цел● модель, люди, системы, данные  ● агент и харнесс  ● риск, стоп, граница

Админ выходит из учётной записи, а в это же время бот входит по старой сессии. Код проверил права один раз, в начале, и дальше доверял результату.

История 2: запись и падение

Вторая история о потере данных. Система расшифровки ведёт запись встречи и в какой-то момент должен отметить, что запись идёт. Если процесс падает между началом записи и этой отметкой, после перезапуска система не знает, что запись была. Файл остался, а записи в базе нет. Пользователь видит пустое место там, где должна быть встреча.

Лечение простое: ставить отметку до начала записи, а не после. Но чтобы заметить проблему, нужно представить падение именно в этой щели, и модель делает это без усилий, потому что падение для неё одно из обычных действий.

?Пишети падает на ходуОтметки нетследа в облаке нет● модель, люди, системы, данные  ● агент и харнесс  ● риск, стоп, граница

Запись встречи шла, процесс упал, а отметки «запись идёт» нигде не осталось. После перезапуска система считает, что записи не было, и данные теряются без следа.

Остальные находки
Пункт в двух карточкахЗапись продолжили, анализ назвал другого владельца
Потерянная отметка ☑Пункт остался без владельца и ушёл вместе с «выполнено»
Удаление непустогоОкно «удалить пустую метку» висело, бот положил туда участника
Ключи шире правПрава человеку сузили, а ключи работают как раньше
Сообщение постороннемуНик перешёл к другому человеку
Чужая цитатаСообщение перенесли на другую вакансию, подсказка осталась

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

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

Вывод
В этом примереПочти все находки одного класса: два действия пересеклись, а код проверил условие один раз.
В общем случаеЛечение одно: перепроверить условие в момент действия на сервере или поставить действия под один замок.
Что сделатьНайдите в своём коде место, где проверка и действие разделены ожиданием ответа.

Пара моделей: как есть и как надо

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

currentfixed123451234Код как естьправило нарушается, баг воспроизведёнТребованиеодна правка закрывает последний ход● модель, люди, системы, данные  ● агент и харнесс  ● риск, стоп, граница

Слева путь к красной комнате, то есть к нарушению, открыт. Справа на этом пути стоит преграда, и правило держится.

current: код как есть
fixed: требование
Назначение
воспроизводит баг сценарием
показывает, что правка закрывает баг
Правило
нарушается
держится
Правки
нет
каждая помечена и подписана «было: …»

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

Третья строка о правках. Каждое отличие правой модели от левой помечено номером вида П1, П2 и подписью «было: …». По этим меткам можно пройти от требования к конкретному месту в коде и понять, что именно надо исправить.

Вывод
В этом примереОдин файл содержит оба варианта: переключатель включает правки, и сравнение «до и после» всегда под рукой.
В общем случаеКаждый баг превращается в короткий сценарий-регрессию, который падает на старой версии и проходит на новой.
Что сделатьЗаведите таблицу требований с колонкой «годится для модели» и отметьте в ней первые три строки.

Модель на каждый домен

У каждого ограниченного контекста свои правила: их записывают как MUST в карточке агрегата. Формальная модель делает эти правила проверяемыми, а агент на контекст работает внутри их границ.

Продажиагент и модельДоставкаагент и модельОплатаагент и модельконтрактконтрактчеловек решает, какие правила нужны● модель, люди, системы, данные  ● агент и харнесс  ● риск, стоп, граница
ВстречаУчастники одной записи не переходят в следующую на той же ссылке
ПриватноеПриватная запись не уходит в общее хранилище
УчастникОдин Telegram ID — одна запись, история не теряется
ВакансияСтатус вакансии принадлежит только кандидату её матча

Что стоит за каждым примером. «Встреча» — правило о том, что люди из прошлой записи не попадают в следующую, даже если ссылка на встречу та же. «Приватное» — приватная запись не может оказаться в общем хранилище ни при каком порядке действий. «Участник» — один Telegram-аккаунт соответствует одной записи участника, и при слиянии история сохраняется. «Вакансия» — статус по вакансии принадлежит конкретной паре «кандидат и вакансия», а не кандидату вообще.

Вывод
В этом примереПравило MUST из карточки агрегата становится свойством модели, а нарушение возвращается агенту этого контекста цепочкой шагов.
В общем случаеГраницу контекста удобно использовать сразу для трёх вещей: для спеки, для агента и для модели.
Что сделатьВыберите один контекст и одно правило MUST и напишите по нему первую модель.

Проверка проверок

Зелёный результат может означать «всё правильно», а может означать «проверка ничего не проверяла». Поэтому проверяют сами проверки: модель ломает нарочно сам автор или агент (например, убирает одно условие) и смотрят, заметит ли правило поломку. Если не заметило, правило бесполезно.

Пример: какая доля правил нарушается в текущем коде системы записи встреч
Участники2 из 2
Карточки людей3 из 4
Доставка багов2 из 3
Цель встречи4 из 7
Запись2 из 4
Приватность1 из 4

Столбики показывают, какая доля правил в текущем коде нарушается. Чем длиннее столбик, тем больше правил модель поймала на реальном коде. «Участники 2 из 2» означает, что обе проверки нашли проблему. «Приватность 1 из 4» означает, что в основном код держится, но одна дыра есть.

Это не оценка качества кода. Столбик зависит от того, какие правила были выбраны, и показывает скорее, где модель принесла больше пользы.

агентили человекпортит нарочнокопия моделиденьги списаныесли есть заказубрали условие «нужен заказ»правилопроверяет копиюзаметилапроверка работаетне заметилапроверка ничегоне проверяет● модель, люди, системы, данные  ● агент и харнесс  ● риск, стоп, граница

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

Автопроверка после каждой правки модели
!ПравкаПроверкаАгентнарушение возвращается, агент чинит до зелёного● модель, люди, системы, данные  ● агент и харнесс  ● риск, стоп, граница

После каждой правки модели автоматически запускается проверка. Если правило нарушено, агент получает цепочку шагов и сразу исправляет модель или код.

Шапка как контрактКакие правила, что должно нарушаться, какие ситуации должны случаться
Испорченная копияПравило, которое код и так соблюдает, обязано сломаться на копии с одной порчей
Обязательные ситуации«Если А, то Б» бесполезно, пока А ни разу не случилось
ТупикиПрограмма молча выбрасывает прогоны-тупики и пишет «ок»
Желание отдельноЧто хотел человек, записывается независимо от проверяемого поля
АвтопроверкаНарушение возвращается агенту сразу после правки

Что значит каждая строка. «Шапка как контракт» — в начале файла перечислены правила, ожидаемые нарушения и ситуации, которые обязаны случиться. «Испорченная копия» — проверка, что правило вообще способно сработать. «Обязательные ситуации» — защита от правил вида «если А, то Б», где А ни разу не наступило. «Тупики» — случаи, когда программа молча отбрасывает прогон, и результат «ок» получается сам собой.

Последние две строки о людях и агентах. «Желание отдельно» значит, что намерение человека записывается в модель независимо от проверяемого поля, иначе правило сверяет поле с самим собой. «Автопроверка» (по сути хук) значит, что агент не может забыть запустить модель: запуск происходит после каждой правки сам.

Вывод
В этом примереЗелёная модель без этих проверок ничего не доказывает: правило может держаться, потому что нужная ситуация не наступает.
В общем случаеПо умолчанию программа отвечает «ок» на выборку, а не на доказательство.
Что сделатьДля каждого правила, которое сразу зелёное, напишите порчу, которая его ломает.

Граница с человеком

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

МодельЧеловекправила «всегда» и «никогда»оценка глазамизаписьпропаламодельэто найдётпанель перегружена??решаетчеловекграницачто записывается правилом, проверяет модель; что оценивается глазами, проверяет человек● модель, люди, системы, данные  ● агент и харнесс  ● риск, стоп, граница
Проверяет модель
  • пропала ли запись
  • не смешались ли участники
  • есть ли подтверждение у опасного действия
  • есть ли выход из тупика
Проверяет человек
  • понятны ли подписи
  • не перегружена ли панель
  • удобно ли выбрать
  • ставится ли расширение на чистый профиль

Список слева описывает всё, что можно сформулировать как «всегда» или «никогда»: запись не исчезает, участники не смешиваются, опасное действие требует подтверждения. Такие правила модель проверяет полностью.

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

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

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

У метода есть пределы, и о них лучше знать до начала работы. Модель сама по себе не следит за кодом, не проверяет «рано или поздно» и видит только тот мир, который в неё положили.

Нет связи с кодомМодели пишутся руками, поэтому со временем расходятся с кодом
Нет проверки «рано или поздно»Что баг в итоге дойдёт до команды, проверяется косвенно
Маленький мирБаг, которому нужно пять сущностей, можно пропустить
Пустые зелёныеПравило держится, потому что функции в коде ещё нет

Нет связи с кодом: модель написана руками, и никто не заставляет её меняться вместе с кодом. Через месяц она может описывать то, чего уже нет. Лекарство: хранить в шапке коммит, с которого снята модель, и обновлять вместе с кодом. Более сильное лекарство — прогонять трассы модели через настоящий обработчик, но этого шага пока нет. Похожая задача, держать желаемое, код и замер в согласии, разобрана в статье про петлю сверки спек.

Нет проверки «рано или поздно»: что баг в итоге дойдёт до команды, проверяется только косвенно. Маленький мир: баг, которому нужно пять сущностей одновременно, можно пропустить. Пустые зелёные: правило держится только потому, что нужной функции в коде ещё нет, и зелёный цвет здесь обманывает.

Вывод
В этом примереМодель описывает то, что вы в неё положили, и без сверки с кодом отстаёт от него.
В общем случаеСледующий шаг в этой области — прогонять трассы модели через настоящий обработчик и сравнивать результат.
Что сделатьЗапишите в шапке модели, на каком коммите она снята, и обновляйте вместе с кодом.

С чего начать с Quint: первая модель за вечер

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

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

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

Как начать вместе с агентом

агент пишетдва списка требованийЛогикаЭкран (UX)правило:деньги не списаны без заказаQuint перебирает все ходынашёл: оплата, сбой,деньги ушли без заказаОплатитькнопка серая, причины нет?понятно ли?решает человекдве отдельные просьбы и две разные проверки● модель, люди, системы, данные  ● агент и харнесс  ● риск, стоп, граница

1. Поставить Quint. Агент установит его сам через npm.

Поставь себе Quint и убедись, что он запускается.

2. Сначала требования, не код. Так агент сначала описывает мир, а правит код потом.

Не меняй код. Сделай требования на Quint для [область].
Для каждого правила покажи две версии: как сейчас и как должно быть.

3. Проверить результат.

Две версииПравило нарушается «как есть» и держится «как надо»
Нужные ситуацииОни случаются, а не обходятся стороной
Испорченная копияОна ломает правило
Реальный случайМодель его воспроизводит

4. Логику и экран — отдельно. Агент по умолчанию их путает: правила экрана пишет как логику, а в логике пропускает случаи.

Сделай два списка требований.
1. Логика: только «всегда» и «никогда», проверяем моделью на Quint.
2. Экран: понятность и удобство, их я проверю сам на макете.

Частые вопросы

Что такое Quint и зачем он нужен?
Quint — язык, на котором описывают состояния системы, действия и правила, которые должны выполняться всегда. Программа перебирает порядки действий и показывает цепочку шагов, где правило нарушается.
Как найти гонки и потерянные данные, если тесты их не видят?
Опишите в модели участников и два-три действия и запустите перебор. Модель находит порядки вроде «проверка, выход, отправка», которые никто не придумывал в тесте.
Заменяет ли модель на Quint обычные тесты?
Нет, они закрывают разные вопросы. Тест защищает конкретную функцию, а модель проверяет правила, которые зависят от порядка событий; из найденной цепочки шагов получается короткий тест-регрессия.
С чего начать работу с Quint?
Возьмите недавно исправленный баг, причина которого в порядке или времени событий. Опишите его участников и два-три действия, запустите перебор и убедитесь, что программа сама находит баг.
Факты: разобранные случаи на 25 октября 2026, документация Quint и Apalache. Числа в примерах иллюстративны, в вашем коде картина будет другой.
© 2026 Егор Урванов