архитектурное решениеобновлено 2026-08-20

Состояние спортивного события: счёт, фазы и завершение матча

Счёт, таймер и статус матча нельзя обновлять как независимые поля: так легко получить невозможное состояние. Разбираем state machine, нормализованные команды и правила приоритета для live-события.

Иллюстрация к материалу: Состояние live-события: таймер, счёт, фазы и завершение матча
Citizen59 / Wikimedia CommonsCC BY-SA 2.0

Состояние события — это не набор независимых полей

Если отдельно обновлять clock, score, phase и is_finished, легко получить невозможную комбинацию: «матч завершён», но таймер идёт; новая атака приходит после final; halftime внезапно откатывается в first half. Нужна модель допустимых переходов и приоритетов.

Сырой фид не управляет интерфейс напрямую

Provider adapter сначала приводит события к нормализованным командам: SCORE_CHANGED, PHASE_CHANGED, CLOCK_SYNC, EVENT_FINISHED. State machine применяет команду только если переход допустим для текущего состояния и версии. UI читает уже собранный event snapshot.

Строгость против «показывать хоть что-то»

Слишком строгая машина может отбросить полезное обновление от нестабильного поставщика. Слишком мягкая превращает ошибку поставщика в ошибку продукта. Практичный вариант — различать hard transition и soft projection: счёт можно обновить при восстановлении, а фазу турнира нельзя откатить без специального reconciliation-события.

Приоритет событий

EVENT_FINISHED закрывает обычные live-переходы, но допускает correction-класс событий. CLOCK_SYNC не продвигает фазу. PHASE_CHANGED может сбросить локальную интерполяцию таймера. Все команды имеют source sequence или внутреннюю version, чтобы запоздавшее событие не перезаписало новое состояние.

Невозможный переход — отдельный сигнал

Нельзя просто логировать reject строкой. Нужен счетчик по типу перехода и поставщику, плюс сохранение компактного контекста: current state, command, provider sequence. Это позволяет отличить баг mapping от реального out-of-order в feed.

Какие инварианты стоит записать явно

State machine полезна не сама по себе, а потому что заставляет сформулировать невозможные сочетания. Завершённый матч не должен снова стать live из-за позднего пакета; таймер не должен идти в паузе; счёт из более старой версии не должен перезаписывать новый. Эти правила лучше хранить рядом с переходами, а не распределять по условным операторам в UI.

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

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

Невозможное состояние должно шуметь

Допустим, событие уже помечено final, но через несколько секунд приходит provider message со статусом live и старым счётом. «Просто применить последнее сообщение» опасно: пользователь увидит откат завершённого матча. State machine должна отклонить переход и записать диагностический факт, а восстановление пройти через отдельную reconciliation-процедуру.

Полезно хранить причины переходов и version/sequence. Тогда расследование отвечает не только «какой статус сейчас», но и «почему система решила перейти сюда». EventNormalizer может сохранить исходную provider-семантику, а продуктовая модель — принять только разрешённый переход.

Contract tests здесь сильнее обычных snapshot tests: они прогоняют последовательности, включая дубли, запоздание и конфликт источников. Ошибка должна проявиться до того, как невозможная комбинация попадёт в UI.

Сверка состояния вместо ручного «подправили в базе»

Для долгоживущих событий нужен контролируемый reconciliation: получить authoritative snapshot у провайдера, сравнить с текущим состоянием и выполнить специальный переход с причиной. Так исправление остаётся наблюдаемым и не ломает историю.

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

Связанные разборы по той же архитектурной цепочке

Букмекерская логика

Поток коэффициентов и событий: путь данных от поставщика до экрана

Из каких участков складывается доставка коэффициента в реальном времени и где появляются очереди, рассинхронизация и устаревшие данные.

Перейти к разбору →
Интеграции данных

Сопоставление спортивных событий между поставщиками данных

Как сопоставлять спортивные события разных поставщиков: внутренние идентификаторы, кандидаты, уверенность совпадения, конфликты и ручная проверка.

Перейти к разбору →
Архитектура

Архитектура потоковых спортивных данных: где теряется задержка

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

Перейти к разбору →
Букмекерская логика

Кнопка коэффициента: доступность, изменение цены и приостановка

Как проектировать кнопку коэффициента: изменение цены, приостановка, фокус, клавиатурное управление и понятные состояния после выбора пользователя.

Перейти к разбору →