Zune 30 zamroziły się ostatniego dnia 2008 r., bo algorytm liczenia dni nie umiał zakończyć roku przestępnego
31 grudnia 2008 r. wiele 30-gigabajtowych odtwarzaczy Microsoft Zune jednocześnie przestało się poprawnie uruchamiać. Problem był powiązany z obsługą 366. dnia roku przestępnego. Algorytm zegara znalazł się w stanie, z którego nie wychodził poprawnie. Najbardziej niezwykła „naprawa” polegała w praktyce na przeczekaniu do zmiany daty na 1 stycznia 2009 r.; po przejściu zegara do nowego roku urządzenia znów działały. Microsoft później informował, że problem poprawiono. Rzadki dzień kalendarza zsynchronizował usterkę całej populacji urządzeń.
Rzadkie wejście stworzyło globalnie zsynchronizowany bug
Wszystkie urządzenia tego modelu korzystały z tej samej logiki daty. Nie miało znaczenia, gdzie fizycznie się znajdowały ani jak użytkownik z nich korzystał. Gdy wewnętrzny kalendarz osiągnął problematyczny dzień, wiele egzemplarzy weszło w ten sam stan praktycznie jednocześnie.
Problem dotyczył algorytmu konwersji dni roku
Analizy opublikowanego kodu sterownika wskazywały na pętlę używaną do przeliczania liczby dni od epoki na rok i dzień. Warunek graniczny dla końca roku przestępnego był obsłużony w sposób powodujący brak postępu. To klasyczny off-by-one na granicy kalendarza.
Restart nie pomagał
Urządzenie po uruchomieniu ponownie odczytywało ten sam problematyczny stan daty i wracało do wadliwej ścieżki. Dopóki zegar nie znalazł się w nowym roku, reset nie usuwał przyczyny. To ważna różnica między transient fault a deterministic bug.
Upływ czasu sam usunął trigger
Po przejściu do 1 stycznia algorytm otrzymywał wejście należące do zwykłego przypadku i urządzenia mogły znów wystartować. Rzadko zdarza się, aby oficjalnie rozsądnym zaleceniem dla elektroniki było „poczekaj do jutra”, dlatego incydent stał się tak zapamiętywalny.
Leap year jest klasycznym testem granicznym
Rok przestępny ma 366 dni, ale tylko w określonych latach. Dodatkowo lata podzielne przez 100 nie są przestępne, chyba że dzielą się przez 400. Kod daty powinien testować 28/29 lutego, 31 grudnia oraz reguły stuleci.
Mały produkt może ujawnić ogólną zasadę
Zune nie był systemem krytycznym, ale błąd jest edukacyjnie świetny: jeśli milion urządzeń wykonuje ten sam deterministyczny kod i ma zsynchronizowane wejście czasowe, drobny edge case może stworzyć jednoczesną awarię ogromnej floty. Z tego powodu globalne floty potrzebują również recovery channel niezależnego od głównego systemu. Jeśli urządzenie nie bootuje do normalnego interfejsu, producent nadal musi mieć sposób na aktualizację firmware’u albo instrukcję bezpiecznego obejścia.
Deterministyczny bug czasowy tworzy niezwykłą falę jednoczesnych zgłoszeń
Gdy awaria zależy od losowego uszkodzenia dysku, poszczególni użytkownicy trafiają na nią w różnym czasie. Gdy triggerem jest konkretna data, cała populacja urządzeń może wejść w ten sam stan w podobnej godzinie. To zmienia skalę supportu i recovery: producent nie ma do czynienia z pojedynczymi egzemplarzami, lecz z flotą, która jednocześnie potrzebuje pomocy. Podobny problem występuje przy wygaśnięciu certyfikatu root CA, rolloverze licznika czy globalnej zmianie konfiguracji. Dlatego czas jest szczególnym source of correlated failure. Testując embedded fleet warto pytać, czy wszystkie urządzenia mają identyczny timer, certyfikat lub datę graniczną. Jeśli tak, nawet mało prawdopodobny edge case nie jest niezależny — może uderzyć w milion urządzeń w tej samej sekundzie. Co ciekawe, taki błąd jest łatwy do wykrycia dziś: wystarczy test uruchamiający algorytm dla 365., 366. dnia oraz przejścia do następnego roku. W 2008 r. problem pokazał jednak, jak łatwo pominąć przypadek, który pojawia się tylko raz na 1461 dni. Ten sam mechanizm dotyczy certyfikatów, licencji i urządzeń embedded z datą ważności. Jeśli milion egzemplarzy otrzyma identyczny stan graniczny, z pozornie rzadkiego błędu powstaje problem całej floty. To prosty przykład, jak jeden rzadki dzień może działać jak globalny test integracyjny uruchamiany jednocześnie na wszystkich egzemplarzach produktu.