Форма 1: Відновлення стану. Форма 2: Марнотратний звіт.
Зміряне серце б’ється за годинником. Не за потребою. Не за зміною. За таймером.
Дві форми, одна коренева причина: заплановане завдання замість правильного дизайну.
Форма 1: Відновлення стану
Перехід стану не завершується атомарно. Замість виправлення переходу, фонове завдання запускається із затримкою та узгоджує. Користувачі бачать пошкоджений стан протягом вікна узгодження.
Приклад GitHub (2026-04-08): Запит на злиття (pull request) мав upstream-репозиторій, який став приватним. GitHub спробував виконати перехід стану: закрити PR, оновити статус гілки, очистити статус злиття. Перехід не завершився атомарно. Статус PR одночасно показував «branch-forced-closed» та «Merge status cannot be loaded». Фонова задача Sidekiq запустилася за кілька хвилин і завершила узгодження. Спостерігачі бачили пошкоджений стан протягом усього проміжку часу.
The Metered Heart: задача Sidekiq виконувалася за розкладом. Вона запустилася не тому, що GitHub виявив пошкоджений стан, а тому, що спрацював таймер. Користувач, який спостерігав за PR у реальному часі, бачив суперечливий PR до наступного виконання задачі.
Form 2: Wasteful Report
Звіт або агрегація переобчислюється з нуля через фіксований інтервал. Без перевірки кешу. Без захисного механізму ідемпотентності. Без інкрементного оновлення. Кожне виконання: повне сканування.
Приклади: нічне cron-завдання, яке переобчислює загальну суму покупок кожного користувача, скануючи всі замовлення з самого початку. Щоденне аналітичне завдання, яке регенерує дашборд із необроблених логів подій. Щотижневий підсумковий лист, який запитує кожен рядок у таблиці активності.
Кожне завдання запускається незалежно від того, чи змінилися дані з моменту останнього виконання. Кожне сканує всю історію, навіть коли нові дані містяться лише за останні 24 години. Кожне замінює інкрементний дизайн запланованим повторенням.
The Shared Root
A Metered Heart не може сказати правду про власний стан. Він знає лише годинник. Form 1: задача відновлення стану запускається о T+5 хвилин незалежно від того, чи стан пошкоджено о T+0. Form 2: задача звіту запускається о 2 годині ночі незалежно від того, чи змінилися дані з учорашнього дня.
Годинник не несе інформації про те, що потрібно робити. Цю інформацію несе подія: «перехід стану щойно завершився невдало», «надійшли нові замовлення». Metered Heart відкидає цю інформацію та замінює її розкладом.
Витік капіталу
Metered Heart виснажує живий капітал: інженери чергують на інциденти з порушеним станом. Підриває соціальну довіру: користувачі бачать неузгоджені дані та повідомляють про дефекти, які зникають самі. Підсилює інші MOAD: робота з відновлення стану, яка сканує всі записи, щоб знайти порушений стан, часто містить MOAD-0001 (сканування O(N²)). Звітна робота, яка переобчислює холодні дані, може спровокувати MOAD-0005 (штампед кешу). MOAD-0009 посилює інші дефекти.
Спільний корінь
Form 1 та Form 2 виглядають по-різному на поверхні: одна відновлює стан, інша переобчислює дані. Їх об’єднує спільна коренева причина.
Fire on Change, Not on Clock
Event-driven дизайн спрацьовує, коли щось змінюється. Зміна стану — це подія. Подія є тригером.
Форма 1: атомарний перехід замінює ремонтне завдання.
Якщо перехід стану може залишити систему в пошкодженому проміжному стані, дефект міститься в самому переході, а не в відсутності ремонтного завдання. Виправте перехід, щоб він завершувався атомарно (або транзакційно). Коли перехід завершується атомарно, пошкоджений стан ніколи не виникає. Ремонтному завданню нічого виправляти.
# DEFECT: non-atomic transition leaves broken state
def close_pr_on_repo_private(pr_id):
pr = PR.get(pr_id)
pr.status = 'branch-forced-closed' # крок 1: частковий стан
pr.save() # видно користувачам ЗАРАЗ
# ... інші кроки можуть завершитися невдало ...
pr.merge_status = 'not_applicable'
pr.save() # крок 2: тепер узгоджено
# Sidekiq job узгоджує, якщо крок 2 не вдається
# ВИПРАВЛЕННЯ: атомарний перехід; немає проміжного стану
def close_pr_on_repo_private(pr_id):
with db.transaction():
pr = PR.get(pr_id)
pr.status = 'branch-forced-closed'
pr.merge_status = 'not_applicable'
pr.save() # обидва поля зберігаються атомарно; ніколи не напівзаписані
Форма 2: поступкове оновлення замінює повний перерахунок.
Звіт, який перераховується з нуля, спрацьовує тому, що старі дані + нові дані = новий результат. Але старий результат + дельта = той самий новий результат, обчислений поступково. Подія: надійшли нові дані. Тригер: оновити агрегат лише для нових даних.
# ДЕФЕКТ: повний перерахунок за розкладом
def nightly_totals_job():
for user in all_users():
total = sum(o.amount for o in user.orders) # сканувати весь час
user.total_purchases = total
user.save()
# ВИПРАВЛЕННЯ: інкрементне оновлення на основі подій
def on_order_placed(order):
order.user.total_purchases += order.amount # лише дельта
order.user.save()
Інкрементне оновлення спрацьовує, коли надходить замовлення, а не о 2 годині ночі. Воно оновлює лише відповідного користувача. Воно зчитує лише нове замовлення, а не всі замовлення за весь час. Нічний джоб зникає.
Чому Форма 1 виявляє зламаний перехід
Лічильник серця Form 1 показує, що перехід стану залишився незавершеним. Джоб відновлення існує, бо інженер помітив зламаний стан і додав механізм узгодження замість виправлення переходу. Джоб відновлення: лата над зламаним архітектурним рішенням.
MOAD-0009 як підсилювач
MOAD-0009 підсилює інші MOAD. Джоб відновлення стану, який сканує всі записи, щоб знайти зламаний стан: MOAD-0001 (сканування O(N) або O(N²) за кожне виконання джобу). Джоб звіту, який переобчислює все з нуля: MOAD-0005 (штампед кешу, коли джоб запускається і звертається до теплого upstream). MOAD-0009 не просто шкодить сам по собі; він доставляє інші MOAD за розкладом.
Діагностика та редизайн
Команда запускає нічний cron-джоб о 2 годині ночі. Джоб сканує всі замовлення всіх користувачів і переобчислює загальну суму покупок кожного користувача з нуля. Джоб триває 4 години. До 6 години ранку дашборд показує свіжі суми. Між 2 і 6 годинами ранку дашборд показує суми за вчора.