Operując na danych, również na danych w WordPressie – czy to na poziomie wtyczek, szablonów, czy własnych integracji – niemal nieustannie spotykamy się z pojęciem identyfikatora. Najczęściej jest nim po prostu liczba całkowita. Czasem to liczba monotonicznie rosnąca (auto_increment w MySQL), czasem losowa liczba, a czasem jeszcze hash. Wspólnym mianownikiem dla całej kategorii identyfikatorów jest jednak to, że identyfikator musi być unikatowy i zgodny ze specyfikacją całego systemu, w którym się pojawia.
Integer – szczypta teorii
1. Liczby całkowite w programowaniu to tzw. integer (int). Owszem, używamy tego określenia, ale czy faktycznie oznacza ono zawsze to samo? W większości języków programowania (takich jak PHP, Python, Java, czy JavaScript) typ integer służy właśnie do przechowywania liczb całkowitych. Przykładowa definicja dla języka PHP: https://www.php.net/manual/en/language.types.integer.php
2. W sensie czysto matematycznym, liczba całkowita to liczba bez części ułamkowej – nie ma ani części dziesiętnych, ani setnych, itd. Wliczamy tu zarówno liczby dodatnie, zero jak i ujemne. Ponadto, w matematyce, liczby całkowite są nieograniczone – żadne maksimum ani minimum ich nie dotyczą.
3. W programowaniu jednak już tak pięknie nie jest. Musimy żyć z ograniczeniami architektury i implementacji. Integer, nawet jeśli w kodzie wygląda tak samo, może kryć w sobie poważny problem: limity. Dla 32-bitowej implementacji (np. stare serwery, domyślne ustawienia niektórych baz danych MySQL czy praktyka systemów legacy):
- Maksymalna wartość: 2 147 483 647
- Minimalna wartość: -2 147 483 648
Odpowiednio, dla architektury 64-bitowej (coraz popularniejszej, ale nie zawsze dostępnej):
- Maksymalna wartość: 9 223 372 036 854 775 807
- Minimalna wartość: -9 223 372 036 854 775 808
(Potrzebujesz źródła? Zerknij na oficjalną dokumentację PHP (https://www.php.net/manual/en/language.types.integer.php), MySQL (https://dev.mysql.com/doc/refman/8.0/en/integer-types.html) i dokumentację innych języków – rozbieżności będą, przede wszystkim wynikające z platformy i specyfiki języka).
Problemy wynikające z wielkości
Z tego, teoretycznie niewinnego, ograniczenia potrafią wyniknąć gigantyczne i trudne do wykrycia problemy w produkcyjnych wdrożeniach. Sam miałem do czynienia z dwoma z nich, trzeci jest anegdotą, która może – lecz nie musi – być przyczynowo powiązana z opisywanym tu zagadnieniem.
E-commerce w opałach
Scenariusz pierwszy. Sprawa całkiem świeża i z rodzimego podwórka. Nowa, dynamicznie rozwijająca się platforma e-commerce oraz wtyczka do obsługi pewnej popularnej w Polsce bramki płatności, rozwijana przez rozpoznawalne studio developerskie. W swojej bazie danych używa ona kolumny order_id na powiązanie konkretnej płatności z konkretnym zamówieniem w systemie (bądź używała, najnowszych wersji nie sprawdzałem). Kolumna ta zaprojektowana jest do przechowywania danych jako INT. Na początku wydaje się to rozsądne: liczb całkowitych raczej nie zabraknie, prawda? Zamówienia rosną, firma się rozwija. Dodatkowo, w trakcie masowego importu produktów obsługa strony popełnia mały błąd: ręcznie „podbija” numer zamówienia, „podkręcając” w ten sposób autoinkrementujący się indeks identyfikatorów w WordPressie. Wartości identyfikatorów idą w miliony. W końcu trafiamy na ścianę: numer zamówienia przekracza 2 147 483 647 i o ile sama platforma e-commercowa radzi sobie z tym problemem (gdyż kolumny w których przechowywane są identyfikatory zdefiniowane są w niej jako BIGINT), to jednak bramka płatności się poddaje. Co dzieje się potem? Każda próba zapisania statusu płatności dla nowego zamówienia kończy się wpisem o identyfikatorze 2 147 483 647 (INT osiągnął maksimum, nie przyjął już wyższej liczby). W efekcie klienci po dokonaniu płatności trafiają na stronę podziękowania… ale z nie swoimi produktami, bo cały mechanizm mapuje zamówienia na ten jeden numer! Natomiast obsługa platformy widzi zamówienia jako nieopłacone. Debugging i diagnoza problemu nie były proste, udało się jednak wytropić przyczynę: zbyt mały zakres typu danych. Naprawa? Zamiana INT na BIGINT w odpowiedniej kolumnie. Skutek uboczny: godziny sprzątania błędnie przypisanych rekordów.
API odmawia współpracy
Scenariusz drugi. Bardzo podobny, tyle że na poziomie API zewnętrznej bramki płatności. Podczas wysyłki danych na konkretny endpoint API należy określić identyfikator transakcji. Dokumentacja API mówi, transaction_id musi przyjmować wartość integer. Kod naszej integracji generuje więc losowy identyfikator – z założenia „im wyżej, tym bezpieczniej”, gdyż losowy ciąg, dajmy na to, dziesięciu cyfr ma mniejsze ryzyko powtórzenia się, niż losowy ciąg czterech czy pięciu cyfr. Prawda? Tyle tylko, że tak zaprojektowany generator tworzy liczby powyżej zakresu 32-bitowego INT. Skutek: każde odwołanie do API kończy się niezwłocznym błędem „invalid transaction_id”. API nie potrafi bowiem przyjąć liczby z poza zakresu, do którego zostało stworzone, odrzuca zatem takie przypadki z automatu. Efekt? Zablokowane płatności, zatrzymanie działania firmy, zirytowani klienci, śledztwo, nerwy. Po identyfikacji problemu wystarczyło zmodyfikować generator, aby sytuacja wróciła do normy. Ponownie – mały błąd wynikający ze zbyt wielkich wartości wygenerował duże kłopoty.
Inwestycje wstrzymane. Czyżby znów integer?
Scenariusz trzeci. Kilka miesięcy temu społeczność developerów na LinkedIn spekulowała o przyczynach awarii na jednej z dużych platform inwestycyjnych obsługujących dziesiątki tysięcy użytkowników. Pojawiły się podejrzenia, iż winowajcą mógł być właśnie błąd typu „integer overflow”. Objaw: z dnia na dzień użytkownicy stracili możliwość wystawiania dyspozycji zakupu i sprzedaży aktywów. Co więcej, system stał się niestabilny – przy próbie wpisu nowych dyspozycji pojawiały się niejasne błędy serwera. Brzmi znajomo? Integer, który „przekręcił się” na maksimum swojego zakresu, generowałby właśnie tego rodzaju dziwne zachowania. Ostatecznie firma nie potwierdziła oficjalnie tej przyczyny, ale środowisko developerów konsensualnie uznało ją za najbardziej prawdopodobną.
Nie pamiętasz? Czytaj dokumentację (…lub zapytaj agenta)
Co z tego wynika? Tworząc aplikacje, wtyczki, integracje i architekturę danych pod WordPressa (i nie tylko) musimy myśleć nie tylko o liczbie identyfikatorów, ale też o tym, jakiego typu danych używamy do ich reprezentacji. To, co na etapie deweloperki lub MVP wydaje się detalem („przecież int na pewno wystarczy!”), w produkcji może okazać się poważnym ryzykiem dla integralności i funkcjonalności całego systemu.
Zatem, ku pamięci – dla architektury 32-bitowej (dla bezpieczeństwa lepiej przyjąć założenie o takiej właśnie architekturze) najwyższa możliwa wartość integer to dokładnie 2 147 483 647? Analogicznie, z ujemnym znakiem – najniższa to -2 147 483 648.
Podsumowując: ograniczenia techniczne typu danych są równie ważne, jak ich poprawne użycie. INT może wyglądać niewinnie, ale jeśli przez nieuwagę nie przewidzisz jego ograniczeń, to możesz podłożyć bombę z opóźnionym zapłonem w swoim kodzie. Jeśli masz choć minimalną wątpliwość co do zakresu przechowywanych wartości – sięgnij np. po BIGINT. Zaufaj doświadczeniu – te kilka procent więcej przestrzeni na dysku jest niczym wobec potencjalnych strat produkcyjnych oraz godzin (albo dni) sprzątania konsekwencji źle wybranego typu danych.
Fot. obraz wygenerowany przez ChatGPT