Forma 1: Naprawa stanu. Forma 2: Marnotrawne raportowanie.
Serce na liczniku bije według zegara. Nie według potrzeby. Nie według zmiany. Według timera.
Dwie formy, jedna przyczyna: zaplanowane zadanie zastępujące poprawne projektowanie.
Forma 1: Naprawa stanu
Przejście stanu nie kończy się atomowo. Zamiast naprawić przejście, zadanie w tle uruchamia się z opóźnieniem i dokonuje uzgodnienia. Użytkownicy widzą uszkodzony stan podczas okna uzgodnienia.
Przykład z GitHub (2026-04-08): Pull request z upstreamowego repozytorium stał się prywatny. GitHub próbował wykonać przejście stanu: zamknąć PR, zaktualizować status gałęzi, wyczyścić status scalania. Przejście nie zostało wykonane atomowo. Status PR pokazywał jednocześnie „branch-forced-closed” i „Merge status cannot be loaded”. Zadanie w tle Sidekiq wykonało się kilka minut później i zakończyło uzgodnienie. Obserwatorzy widzieli uszkodzony stan przez cały czas trwania okna.
The Metered Heart: zadanie Sidekiq uruchamiało się według harmonogramu. Nie uruchomiło się dlatego, że GitHub wykrył uszkodzony stan — uruchomiło się, ponieważ timer wybił. Użytkownik obserwujący PR w czasie rzeczywistym widział PR, który sam sobie przeczył aż do następnego wykonania zadania.
Form 2: Wasteful Report
Raport lub agregacja przelicza się od zera w stałych odstępach czasu. Brak sprawdzenia cache’a. Brak strażnika idempotencji. Brak aktualizacji przyrostowej. Każde wykonanie: pełne skanowanie.
Przykłady: nocne zadanie cron, które przelicza całkowitą kwotę zakupów każdego użytkownika, skanując wszystkie zamówienia od początku istnienia systemu. Codzienne zadanie analityczne regenerujące dashboard z surowych logów zdarzeń. Cotygodniowy e-mail z podsumowaniem, który odpytuje każdy wiersz tabeli aktywności.
Każde z nich uruchamia się niezależnie od tego, czy dane zmieniły się od ostatniego wykonania. Każde skanuje całą historię, nawet gdy tylko ostatnie 24 godziny zawierają nowe dane. Każde zastępuje zaplanowane powtarzanie projektem przyrostowym.
The Shared Root
A Metered Heart nie potrafi powiedzieć prawdy o własnym stanie. Zna tylko zegar. Form 1: zadanie naprawiające stan uruchamia się o T+5 minut niezależnie od tego, czy stan był uszkodzony o T+0. Form 2: zadanie raportowe uruchamia się o 2:00 niezależnie od tego, czy jakiekolwiek dane zmieniły się od wczoraj.
Zegar nie niesie informacji o tym, co trzeba zrobić. Informację tę niesie zdarzenie: „właśnie nastąpiła zmiana stanu”, „właśnie nadeszły nowe zamówienia”. Metered Heart wyrzuca tę informację i zastępuje ją harmonogramem.
Wyciek kapitału
Metered Heart wyczerpuje żywy kapitał: inżynierów czekających na awarie stanu. Niszczy zaufanie społeczne: użytkownicy widzą niespójne dane i zgłaszają błędy, które same znikają. Wzmacnia inne MOAD-y: zadanie naprawy stanu, które skanuje wszystkie rekordy, często zawiera MOAD-0001 (skan O(N²)). Zadanie raportowe, które przelicza dane „na zimno”, może wywołać MOAD-0005 (cache stampede). MOAD-0009 potęguje inne defekty.
Wspólny korzeń
Form 1 i Form 2 wyglądają na powierzchni inaczej: jedna naprawia stan, druga przelicza dane. Łączy je wspólna przyczyna.
Reaguj na zmianę, nie na zegar
Projekt sterowany zdarzeniami uruchamia się, gdy coś się zmienia. Zmiana stanu jest zdarzeniem. Zdarzenie jest wyzwalaczem.
Forma 1: atomowe przejście zastępuje zadanie naprawcze.
Jeśli przejście stanu może pozostawić system w uszkodzonym stanie pośrednim, wada tkwi w samym przejściu, a nie w braku zadania naprawczego. Napraw przejście tak, aby było atomowe (lub transakcyjne). Gdy przejście zakończy się atomowo, uszkodzony stan nigdy nie występuje. Zadanie naprawcze nie ma nic do naprawienia.
# DEFEKT: nieatomowe przejście pozostawia uszkodzony stan
def close_pr_on_repo_private(pr_id):
pr = PR.get(pr_id)
pr.status = 'branch-forced-closed' # krok 1: stan częściowy
pr.save() # widoczne dla użytkowników TERAZ
# ... inne kroki mogą się nie powieść ...
pr.merge_status = 'not_applicable'
pr.save() # krok 2: teraz spójny
# Zadanie Sidekiq naprawia stan, jeśli krok 2 nie powiedzie się
# POPRAWKA: atomowe przejście; brak widocznego stanu pośredniego
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() # oba pola zapisywane atomowo; nigdy częściowo zapisane
Forma 2: aktualizacja przyrostowa zastępuje pełne przeliczanie.
Raport, który przelicza wszystko od zera, uruchamia się, ponieważ stare dane + nowe dane = nowy wynik. Ale stary wynik + delta = ten sam nowy wynik, obliczony przyrostowo. Zdarzenie: nadeszły nowe dane. Wyzwalacz: zaktualizuj agregat tylko dla nowych danych.
# DEFECT: pełne przeliczanie według harmonogramu
def nightly_totals_job():
for user in all_users():
total = sum(o.amount for o in user.orders) # skanuj wszystkie rekordy
user.total_purchases = total
user.save()
# POPRAWKA: przyrostowa aktualizacja zdarzeniowa
def on_order_placed(order):
order.user.total_purchases += order.amount # tylko delta
order.user.save()
Aktualizacja przyrostowa uruchamia się w momencie złożenia zamówienia, a nie o 2 w nocy. Aktualizuje tylko dotkniętego użytkownika. Odczytuje tylko nowe zamówienie, a nie wszystkie zamówienia ze wszystkich czasów. Zadanie nocne znika.
Dlaczego Formularz 1 ujawnia uszkodzony przejście
Formularz 1 Metered Heart ujawnia, że przejście stanu zostało pozostawione niekompletnym. Zadanie naprawcze istnieje, ponieważ inżynier zauważył uszkodzony stan i dodał mechanizm uzgadniania zamiast naprawić przejście. Zadanie naprawcze: łatka na zepsutą decyzję architektoniczną.
MOAD-0009 jako wzmacniacz
MOAD-0009 wzmacnia inne MOAD-y. Zadanie naprawy stanu, które skanuje wszystkie rekordy w poszukiwaniu uszkodzonego stanu: MOAD-0001 (skan O(N) lub O(N²) przy każdym uruchomieniu zadania). Zadanie raportowe, które przelicza wszystko od zera: MOAD-0005 (burza pamięci podręcznej, gdy zadanie się uruchamia i trafia na ciepły upstream). MOAD-0009 nie tylko szkodzi samodzielnie; dostarcza inne MOAD-y według harmonogramu.
Diagnoza i przeprojektowanie
Zespół uruchamia nocne zadanie cron o 2 w nocy. Zadanie skanuje wszystkie zamówienia wszystkich użytkowników i przelicza od zera całkowitą kwotę zakupów każdego użytkownika. Zadanie trwa 4 godziny. O 6 rano pulpit pokazuje świeże sumy. Między 2 a 6 rano pulpit pokazuje wczorajsze sumy.