Parę tygodni temu jeden z użytkowników wspomniał mi, że coś jest nie tak z Watchbotowym wyszukiwaniem. It seems WatchBot requires the starting date to be before 2011-10-18. A był już marzec 2012.
Co??? Kod stary i z braku czasu traktowany przeze mnie po macoszemu, funkcja dość niszowa ale … czyżbym naprawdę gdzieś zhardcodował 2011 jako niesłychanie odległą przyszłość?
Odpaliłem wersję roboczą, przetestowałem formularz – wszytko działało dobrze. Znowu jakieś fałszywe zgłoszenie? No ale dla pewności sprawdziłem na produkcji.
Ups:
Dopiero po dłuższym wpatrywaniu znalazłem usterkę. Robiąc kiedyś roboczą klasę weryfikującą, czy data należy do zadanego zakresu, przewidziałem możliwość zadawania brzegowych wartości zarówno statycznie (jako stałych), jak dynamicznie (jako funkcji wołanych w momencie walidacji). Najpierw używałem jej poprawnie:
after = DateRangeValidator(
earliest_date = DateTime.DateTime(2005, 1, 1),
latest_date = DateTime.now))
Parę lat po napisaniu powyższego kodu zdecydowałem się pozbyć mxDateTime
na rzecz standardowego datetime
. Przerobiłem co trzeba a w tym miejscu napisałem:
after = DateRangeValidator(
earliest_date = date(2005, 1, 1),
latest_date = date.today())
chwilę potestowałem, wgrałem i zająłem się innymi sprawami.
A żeby zauważyć efekt nawiasków po today
trzeba było odczekać tę
dobę od uruchomienia testowej wersji (i spróbować dość specyficznego
wyszukiwania)…
18 października to data, gdy ostatni raz restartowałem aplikację po wgraniu paru poprawek. Jak widać Linode ma przyzwoity uptime…
Zmiana czasu
Dość już wiele lat temu zetknąłem się z aplikacją, która dość finezyjnie generowała unikalne rosnące klucze łącząc bieżący czas z niewielkim dodatkowym licznikiem. Działało to bardzo ładnie…
… aż pewnego ranka okazało się, że pojawiły się nieoczekiwane zaburzenia kolejności a nawet duplikaty.
Wcześniejszej nocy miała miejsce jesienna zmiana czasu. Ta, w której śpimy dłużej i zegary cofamy z trzeciej na drugą w nocy. Program posługiwał się czasem lokalnym więc zakres 2:00-3:00 wystąpił dwukrotnie a klucze z tych godzin pięknie się wymieszały.
Od tego czasu ze sporą dyscypliną staram się używać bądź czasu uniwersalnego, bądź anotowanego strefą czasową.
Inny przypadek zbliżonej choroby. Program, który wykonywał pewne operacje co 30 sekund, nagle zawiesił działalność na 60 minut i 30 sekund. Cóż - aplikacja wyliczała sobie czas następnej aktywacji dodając owe pół minuty do bieżącego zegara. 2:59:47 + 0:0:30 = 3:00:17. A że używany był czas lokalny, w nocy jesiennej zmiany czasu na ową trzecią przyszło trochę poczekać.
Wiosenne przestawianie zegarków wiąże się z innego rodzaju ryzykiem, ewentualne działania przewidziane na - np. - 2:30 w nocy mogą się nie wykonać w ogóle. A programy liczące jakieś statystyki czy monitorujące także łatwo mogą dostarczyć skrzywionych wyników (jak ostrzeżenie że najprawdopodobniej coś nie działa, bo przez godzinę nie zanotowano żadnej operacji).
Czas może skakać zawsze
Do zaburzenia kolejności opartej o zegar nie trzeba zmiany czasu. Zupełnie wystarczy … działający serwer NTP (synchronizacji zegarów z internetowymi serwerami czasu). Jeśli systemowy zegar zacznie się nieco rozjeżdżać, zupełnie normalne mogą być powodowane przez NTP skoki o kilka sekund czy nawet kilka minut.
Na VPS i innych wirtualnych maszynach zegar może się zmienić w efekcie zmiany na maszynie-hoście.
Zresztą, zegar systemowy może po prostu poprawić ręcznie administrator…
W ostatnich latach dość powszechnie dostępne są już różnego typu monotoniczne zegary (jak
ClOCK_MONOTONIC
dlaclock_gettime
w odpowiednio nowych Linuksach). Mają jednak tylko częściowe zastosowanie (do względnego a nie bezwzględnego pomiaru czasu).
Innego rodzaju smaczku dostarcza hibernacja (tak laptopowa, jak dotycząca wirtualnych maszyn). Program po wykonaniu części pętli może zostać (wraz z systemem) uśpiony i wznowić działanie … po tygodniu. Albo po miesiącu.
Dla kompletności wspomnę jeszcze o leap second, choć efekt tej sekundy jest w praktycznych sytuacjach zaniedbywalny.
29 lutego
29 lutego 2012 administratorzy zrestartowali jedną z kopii całkiem poważnej aplikacji. Program (porządnie wcześniej testowany i używany od dawna) nie uruchomił się, zgłaszając błędy.
Instalacja na szczęście była zreplikowana w kilku kopiach i działała ale zaciskanie kciuków oby nie było trzeba nic więcej zrestartować nim pojawi się poprawka miało szczególny urok.
Co się stało? W trakcie inicjalizacji było do czegoś potrzebne wyliczenie daty za rok. Programista napisał kod w stylu:
today = date.today()
next_year = date(today.year + 1, today.month, today.day)
To prawie zawsze dobrze działa. Niestety - prawie. 29 lutego 2012 spowodowało próbę utworzenia obiektu daty dla 29 lutego 2013. A takiego dnia nie ma w kalendarzu…
Rozmaite, nieraz dość pokrętne, API do rachunków na datach, służą właśnie unikaniu takich sytuacji. W Pythonie może się przydać np. klasa
dateutil.relativedelta
.
Casus 29 lutego jest szczególnie smakowity (możliwość testów raz na 4 lata – i to z wyjątkami) ale problem miewa też częstsze odmiany (widziałem kiedyś rachunki początku następnego miesiąca, które dobrze działały przez większość roku ale z 31 stycznia skakały do 1 marca, szczegółów już nie pamiętam). Nawet dodawanie dni lub godzin, jeśli jest prowadzone w czasie lokalnym, może trafić w nieistniejącą 2:30 nocy wiosennej zmiany czasu.
To jaki dzień właściwie mamy?
Wspominałem wyżej, że staram się używać czasu uniwersalnego lub opisanego strefą. Tutaj też jednak można się pomylić.
Fragment kodu odczytywał bieżącą datę z czasem i wykorzystywał ją do kilku celów. Min. zapisywał w bazie danych … tylko datę, pomijając czas i strefę.
Wszystko dobrze, ale 20 kwietnia 2012, godzina 0:17, to w czasie uniwersalnym 19 kwietnia, godzina 22:17. Po naiwnym obcięciu czasu – 19 kwietnia. Ups.
Kłopot niby występuje codziennie ale tylko tuż po północy. Kto testuje o tej porze?
Przeleżało się całkiem długo…
Sortowanie godzinami
Jeden z serwisów szachowych pozwala darmowym użytkownikom na grę w turniejach tylko w środy i niedziele. Ba – ale co to jest środa, skoro grają ludzie z niemal całego świata? Ustalono, że obowiązuje czas lokalny serwisu (akurat - rosyjski). Ja zatem mogę zagrać za darmo od wtorku 22:00 do środy 22:00.
Zalogowałem się w zeszły wtorek około 23:00. Zerknąłem na listę turniejów, która standardowo jest sortowaną po czasie rozpoczęcia, od najbliższych. Lista rozpoczynała się od turnieju planowanego na 22:00, potem 22:10, 22:20… - i do wszystkich można się jeszcze było zapisać, choć serwer normalnie nie obsługuje dołączania do już rozpoczętych turniejów.
Dopiero po dłuższej chwili skojarzyłem, że są to turnieje które rozpoczną się … za 23 godziny, a dopiero po nich pojawiają się te planowane za chwilę. Programista prawdopodobnie posortował używając tylko czasu, bez daty.
A aby zauważyć problem, trzeba było być w innej strefie czasowej niż serwer.
Morał
Rzuciłem parę skromnych własnych przykładów, oczywiście można znaleźć i głośniejsze (przychodzą mi na myśl kłopoty iPhone z budzikiem czy pad odtwarzaczy Microsoftu na przywitanie Nowego Roku).
Główny morał: to jest piękny przykład funkcjonalności, w której usterki bardzo trudno złapać jakimikolwiek testami. Jak złapać coś, co dzieje się tylko 29 lutego, tylko w chwili zmiany czasu, tylko w razie rozjechania się systemowego zegara czy nawet codziennie ale tylko tuż po północy i pod warunkiem działania z innej strefy czasowej? Albo tylko, gdy program pozostaje w działaniu ponad dobę (lub, o zgrozo, ponad miesiąc czy rok)? Nawet przy gotowości do manipulowania zegarami albo mockowania obiektów dostarczających czasu, mogą się wymknąć problemy w których ten czas pochodzi z zewnątrz (np. z integrowanych usług sieciowych albo zewnętrznych baz danych).
Tak naprawdę nic nie zastąpi ostrożności programisty i kilkukrotnego poważnego zastanowienia ilekroć w jakiejkolwiek formie trzeba się dotknąć zegara lub kalendarza…