Siedem ukrytych założeń, które powodują awarie JavaScript przy rzeczywistym ruchu sieciowym
Dowiedz się, jakie założenia dotyczące typów, ponawiania prób, równoczesności, kolejności odpowiedzi, objętości danych, brakujących danych oraz stanu w pamięci powodują błędy w kodzie JavaScript po jego wprowadzeniu do środowiska produkcyjnego.
Większość kodu JavaScript, który zawodzi w środowisku produkcyjnym, nigdy nie była błędna w sensie błędu składniowego. Był poprawny przy określonych warunkach, które istniały tylko na laptopie programisty: małe zbiory danych, szybkie odpowiedzi, jeden ostrożny użytkownik, jeden proces. Poniżej przedstawiono siedem takich ukrytych warunków, sposób, w jaki każdy z nich przestaje być spełniony przy rzeczywistym ruchu, oraz konkretne rozwiązania projektowe, które pozwalają przekształcić je w jawną, sprawdzoną decyzję zamiast w niespodziankę podczas incydentu.
Dlaczego lokalne rozwojowanie jest słabym wskaźnikiem
Środowisko rozwojowe jest niezwykle wyrozumiałe. Baza danych zawiera zaledwie kilka wierszy, żądania wracają w ułamku sekundy, nikt nie klikuje niczego dwa razy, a każda usługa działa na tym samym komputerze lub w niezawodnej sieci lokalnej. Kod napisany w takich warunkach może wyglądać solidnie przez całe tygodnie: funkcje działają poprawnie, każde obietnictwo jest oczekiwane, zestaw testów daje pozytywne wyniki, a konsola pozostaje cicha.
Produkcja zmienia dane wejściowe, a nie język. Zapisy zostały stworzone przez pięć różnych wersji aplikacji i nie wszystkie mają taki sam format. Ludzie klikają dwukrotnie, ponieważ wydaje się, że ekran utknął. Odpowiedzi przychodzą w innej kolejności niż wysłane zapytania. API dostawców zewnętrznych działają powoli, zwracają częściowe dane lub wygasają. Kilka instancji tej samej usługi aktualizuje te same wiersze w tym samym momencie. Pola, które zawsze były wypełniane lokalnie, pojawiają się jako "", null, wartość z typu enum wycofanego dwa wersje temu lub ciąg znaków, w którym powinna znajdować się liczba.
Nic z tego nie pogarsza kodu po wdrożeniu. Po prostu usuwa warunki, które sprawiały, że słabe granice wydawały się bezpieczne. Przydatnym nawykiem jest przestanie zadawania tylko pytania „czy to działa przy oczekiwanych danych wejściowych?” i zaczęcie zastanawiania się nad tym, co kod w tle zakłada dotycząc czasu działania, ilości danych, właściciela, struktury danych oraz możliwych awarii. Wiele z tych założeń jest całkowicie uzasadnionych. Ryzyko polega na pozostawianiu ich niewyrażonych, dopóki rzeczywiści użytkownicy nie obalą ich.
1. Wartości nie zawsze mają ten typ, za który się podają
W JavaScript wartość może przekraczać różne granice i przy każdej z nich traci swój pierwotny typ. Numerowy identyfikator bazy danych zamienia się w ciąg znaków, gdy znajduje się w adreście URL. Pole wyboru przekazywane jest na serwer jako "true" lub "false". Puste pole wprowadzania staje się "", podczas gdy serwer oczekiwał wartości null. Zapis czasu ISO przekazywany jest jako zwykły tekst i jest traktowany jak obiekt Date, tylko dlatego, że tak wygląda.
W środowisku lokalnym rzadko występuje to zjawisko, ponieważ ten sam programista pisze kod po obu stronach żądania, a przykładowe dane są już dostosowane do testowanej funkcji. W środowisku produkcyjnym dane pochodzą od starszych aplikacji mobilnych, standardowego zachowania formularzy w przeglądarce, integracji z partnerami, przestarzałych danych z pamięci podręcznej oraz rekordów ręcznie edytowanych w narzędziach administracyjnych.
Konwersja powoduje wyniki, które wyglądają prawie poprawnie
Twardymi błędami są te, w których JavaScript przeprowadza wiarygodną konwersję zamiast rzucić błędem. "10" poprawnie porównuje się z liczbami za pomocą < i >, jednak "10" + 1 daje wynik "101". Ciąg znaków "false" jest uznawany za prawdziwy, więc flaga przeznaczona do wyłączenia określonej funkcji może ją w rzeczywistości włączyć. Pusty ciąg znaków jest traktowany jako „brakujący” w jednym helperze, a jako prawidłowa wartość w innym.
Nic się nie zawala. Stronikowanie pomija niewłaściwą stronę, opcja wyłączona staje się dostępna do wyboru, albo userId === record.ownerId po cichu zawodzi, ponieważ jedna ze stron to liczba, a druga ciąg znaków. Zespoły potem naprawiają każdy pojedynczy objaw, w którym się pojawia, a baza kodu stopniowo gromadzi kilka subtelnie różnych reguł normalizacji.
Znormalizuj raz, na granicy
Zaawansowane systemy konwertują wartości, gdy przekraczają określoną granicę, i już nigdy tego nie robią ponownie. Parametry zapytań oraz treści żądań są analizowane i przekształcane w zweryfikowane typy wewnętrzne, zanim dotknie je logika biznesowa. Odpowiedzi z zewnętrznych API są mapowane na stabilne modele domenowe. Dane wprowadzane w formularzach są celowo konwertowane, zamiast polegać na mechanizmach sprawdzania prawdziwości lub przymusowej konwersji operatorów. Celem nie jest przekształcenie JavaScripta w język o ścisłym typowaniu; chodzi raczej o to, aby reszta aplikacji nigdy nie musiała zgadywać, co oznacza dana wartość.
TypeScript dokumentuje zamierzony kształt danych, ale typy są usuwane w czasie wykonywania i nie mogą sprawdzić, co faktycznie zostanie przesłane. Obsługa, której parametr ma idealnie określony typ, nadal może otrzymać cokolwiek. Bezpieczniejszym podejściem jest użycie schematu w czasie wykonywania oraz typu statycznego, które opisują ten sam kontrakt, najlepiej takiego, w którym typ pochodzi bezpośrednio ze schematu. Biblioteki do tworzenia schematów, takie jak Zod, ułatwiają to, ponieważ jedna definicja może zarówno weryfikować dane w czasie wykonywania, jak i generować typ dla TypeScript.
2. Jeden kliknięcie nie oznacza jednej eksploatacji
Na ekranie widnieje tylko jeden przycisk, więc naturalne jest wyobrażenie sobie jednej prośby: użytkownik kliknie, serwer wykona zadanie, a odpowiedź to potwierdzi. Testy ręczne wzmacniają to wyobrażenie, ponieważ programiści klikają raz i cierpliwie czekają.
Rzeczywiści użytkownicy i rzeczywiste sieci zachowują się inaczej. Ktoś kliknie ponownie, ponieważ nie pojawił się wskaźnik ładowania. Aplikacja mobilna próbuje ponownie po przerwaniu połączenia. Proxy lub balanser obciążeń odtwarza żądanie po chwilowej awarii. Kolejka wiadomości dostarcza zadanie ponownie, ponieważ proces skończył zadanie, ale zakończył się błędem przed potwierdzeniem go. To, co wyglądało jak jeden punkt wejścia, ma teraz kilka sposobów na wykonanie czynności dwukrotnie.
Gdzie duplikaty faktycznie szkodzą
Powtarzanie operacji odczytu zazwyczaj nie jest szkodliwe. Natomiast powtarzanie tworzenia konta, rezerwacji towaru, płatności, zaproszenia lub wygenerowanego eksportu jest szkodliwe: powstają duplikatowe wiersze, kilka e-maili, zapasy są obniżane dwukrotnie lub klient jest opłacany dwa razy.
Wyłączenie przycisku po pierwszym kliknięciu poprawia doświadczenie użytkownika, ale nie jest to gwarancja. Można ominąć mechanizmy kontrolne, próby mogą być powtórzone poza interfejsem użytkownika, a dwie instancje usługi mogą otrzymać tę samą logiczną akcję. Zabezpieczenie na poziomie frontendu jest jedynie uprzejmością; to backend i baza danych muszą zapewnić zachowanie niezmienników.
Projektowanie operacji zapisu tolerujących powtórzenia
Ważne operacje zapisu powinny być projektowane przy założeniu, że będą próbowane więcej niż raz:
- Klucz idempotencji, który pozostaje stabilny przy każdej próbie, umożliwia serwerowi traktowanie ich jako jednej logicznej operacji.
- Ograniczenie unikalności w bazie danych zapobiega duplikatom, nawet jeśli dwie równoczesne prośby przejdą sprawdzenie na poziomie aplikacji „czy to istnieje?”.
- Tabela z identyfikatorami przetworzonych wiadomości pozwala odbiorcy kolejki pominąć zdarzenie, które już zostało przetworzone.
Kluczowym spostrzeżeniem jest to, że timeout lub odpowiedź błędu nie dowodzą porażki operacji. Serwer mógł zakończyć pracę po tym, jak klient przestał czekać. Ponawianie prób bez stabilnej tożsamości operacji przekształca tę niepewność w duplikację. Niezawodny system to taki, który nie zapobiega każdej ponownej próbie, lecz w którym taka próba nie zmienia ostatecznego znaczenia operacji. Mechanizmy po stronie serwera są omówione bardziej szczegółowo w rozumieniu kluczy idempotentności w punktach końcowych POST w Node.js.
3. Kod z użyciem Awaited nadal może wykazywać problemy
async/await sprawia, że funkcja wygląda jak chroniona sekwencja: załaduj rekord, sprawdź jego stan, zaktualizuj go i zwróć wynik. Każdy krok jest oczekiwany, więc przepływ wydaje się kontrolowany. Kontrola ta istnieje jednak tylko w ramach jednej wywołania.
Gdy jedna wywołanie jest wstrzymane przy użyciu await, silnik wykonawczy może swobodnie uruchomić inny obsługujący żądanie, funkcję zwrotną zdarzenia lub zadanie dla tej samej funkcji. Dwie takie operacje mogą odczytać ten sam stan, obie uznać, że działanie jest dozwolone, i obie zapisać zmiany. Każda z tych operacji jest poprawna sama w sobie, ale łącznie łamią one zasadę biznesową.
Konkurencja typu „sprawdź, potem działaj” na serwerze
Załóżmy punkt końcowy służący do zatwierdzania, który ładuje pozycję w stanie oczekiwania, a następnie ustawia ją na zatwierdzoną. Dwóch administratorów otwiera tę samą stronę i kliknie „Zatwierdź” z odstępem kilku sekund. Obie prośby odczytują wartość pending przed dokonaniem jakichkolwiek aktualizacji. Ostateczny stan może wyglądać poprawnie, ale jeśli działanie to wysyła również powiadomienie lub zapisuje wpis audytowy, oba te efekty uboczne wystąpią dwa razy.
Ta sama konkurencja w przeglądarce
W interfejsie użytkownika wzorzec przypomina pole wyszukiwania. Najpierw wysyłana jest prośba o przetworzenie starszego zapytania, a po niej prośba o nowsze – odpowiedź od nowszego zapytania przychodzi pierwsza. Ekran pokazuje odpowiednie wyniki przez chwilę, po czym przychodzi starsza odpowiedź i je zastępuje. Obie prośby zakończyły się sukcesem, a oba aktualizacje stanu wykorzystały ważne dane. Brakowało jedynie decyzji o tym, które zapytanie nadal ma prawo aktualizować ekran.
Wyrażenie await nie blokuje zasobów, nie zamraża wartości odczytanych wcześniej ani nie umieszcza w kolejce wywołań tej samej funkcji. Wstrzymuje jedną realizację właśnie po to, aby inne zadania mogły być kontynuowane.
Wybór mechanizmu ochrony
Rozwiązanie zależy od tego, gdzie ma miejsce konkurowanie:
- Uaktualnienie warunkowe, takie jak „ustaw status na zatwierdzony, jeśli status to w oczekiwaniu”, przy jednoczesnej sprawdzeniu liczby wpływających wierszy.
Niebezpieczne przekonanie polega na tym, że kod zapisany sekwencyjnie oznacza system, który zachowuje się sekwencyjnie. JavaScript może sprawić, że pojedyncza funkcja będzie łatwa do zrozumienia, mimo że wiele jej jednoczesnych kopii jest wykonywanych w tym samym czasie.
4. Żądania nie kończą się w kolejności, w której zostały rozpoczęte
Ponieważ kod jest pisany od góry do dołu, łatwo jest myśleć o pracy asynchronicznej według kolejności tworzenia: żądanie A zostało wysłane przed żądaniem B, więc A powinno wrócić pierwsze. Sieci, pamięci cache, bazy danych i dostawcy zewnętrzni nie dają takich gwarancji.
Pierwsza prośba może napotkać wolne zapytanie, podczas gdy druga jest serwowana z pamięci cache. Jeden region dostawcy odpowiada natychmiast, podczas gdy inny próbuje ponownie wewnętrznie. Duży obciążenie danych wymaga więcej czasu na przetworzenie, nawet jeśli serwer odpowiedział szybciej.
Zastarzałe odpowiedzi to poprawne dane w niewłaściwym czasie
To staje się błędem w momencie, gdy kolejność realizacji decyduje o aktualnym stanie. Zwykłymi ofiarami są wyniki wyszukiwania, walidacja formularzy, ładowarki tras, panele sterowania oraz funkcja autodopasowywania: użytkownik robi krok naprzód, a późna odpowiedź spowalnia interfejs. Dane w tej odpowiedzi nie są błędne – po prostu nie odnoszą się już do tego, na co patrzy użytkownik.
Rozwiązanie typu debouncing pomaga poprzez zmniejszenie liczby rozpoczynanych żądań, ale nie eliminuje ich nakładania się. Użytkownik może zrobić przerwę na tyle długo, by wywołać żądanie, a następnie kontynuować wpisywanie tekstu, podczas gdy żądanie jest jeszcze w trakcie przetwarzania – wtedy starsze żądanie może i tak zostać zrealizowane jako ostatnie.
Decyduj, kto posiada wynik
Niezawodne rozwiązanie zaczyna się od reguły przynależności. W wielu interfejsach powinien zwyciężyć najnowszy żądanie, więc albo anuluj starsze żądania, albo oznacz każde z nich numerem sekwencyjnym i odrzuć wyniki, które nie są już aktualne. Inne procesy wymagają obsługi według zasady „kto pierwszy, ten lepszy” lub niezależnego stanu dla każdej operacji. Ta sama zasada przynależności musi obejmować również stan ładowania i błędów: porzucone żądanie nie powinno usuwać wskaźnika ładowania dla aktywnego żądania ani wyświetlać błędu dla zapytania, które użytkownik już zastąpił. Aby zapoznać się z rozwiązaniem specyficznym dla React, patrz React search with clear state ownership.
W środowisku produkcyjnym nie uwzględnia się kolejności utworzenia obietnic. Twój kod musi w momencie finalizacji zdecydować, czy dany wynik nadal ma znaczenie.
5. Małe kolekcje nie pozostają małe
Część transformacji wydaje się nieszkodliwa przy zaledwie kilkudziesięciu elementach: mapowanie tablicy i wywoływanie funkcji find na innej tablicy dla każdego elementu, filtrowanie jednej listy za pomocą includes w odniesieniu do drugiej lub tworzenie raportu poprzez filtrowanie całej kolekcji raz na grupę.
Dzięki przygotowanym narzędziom testowym operacje te kończą się błyskawicznie, a kod pozostaje na tyle krótki, że jego algorytmiczny koszt jest niewidoczny. W środowisku produkcyjnym dane się mnożą, bez zmiany wyglądu kodu. Funkcja find wewnątrz map na dwóch kolekcjach po dziesięć tysięcy rekordów oznacza maksymalnie około stu milionów porównań. Funkcja includes wewnątrz pętli dodaje kolejne liniowe przeszukiwanie dla każdego elementu. Raport stworzony dla jednej grupy nagle jest wykonywany w całej organizacji.
Dostosuj strukturę do wzorca dostępu
Metody tablic nie są problemem; problemem jest używanie struktury sekwencyjnej do powtarzanych wyszukiwań według klucza lub testów przynależności. Stwórz raz Map oparty na kluczu id, a każde wyszukiwanie będzie trwać średnio stały czas. Użyj Set, gdy pytanie brzmi „czy to jest w kolekcji?”. Często lepszą opcją jest połączenie z bazą danych, dzięki czemu nigdy nie ładujesz obu kolekcji do pamięci i nie łączysz ich w kodzie aplikacji.
To nie oznacza konieczności zastępowania każdej tablicy. Tworzenie indeksu ma swoje koszty, a w przypadku naprawdę małej listy find może być najprostszą opcją. Obliczenia zmieniają się, gdy operacja jest wykonywana często lub dane mogą znacznie urosnąć.
Pamięć i współdzielenie zasobów również się skalują
Ilość danych wpływa również na pamięć i zużycie zasobów. Ładowanie każdego wiersza przed filtrowaniem, łączenie kilku wywołań map i filter, przy których każde tworzy nowy tablicę, lub używanie Promise.all do obsługi jednej obietnicy na każdy element może być w porządku w środowisku lokalnym, ale szkodliwe w produkcji. Proces może wyczerpać pamięć, opróżnić zasoby puli połączeń do bazy danych lub zajmować cały obieg zdarzeń, przez co wszystkie inne żądania czekające na niego działają wolniej. Paginacja, strumieniowanie i ograniczanie jednoczesności to typowe rozwiązania.
Większość problemów z wydajnością wynika z tego, że zwykły kod musi radzić sobie z nadzwyczaj dużą ilością danych. Znajom się z oczekiwanym rozmiarem obciążenia, unikaj niepotrzebnych działań i przeanalizuj rzeczywisty tok wykonywania zadań, zanim zaczniesz stosować skomplikowane optymalizacje. Najdroższa linia kodu to często ta, która wydaje się zbyt znajoma, by można ją było zakwestionować.
6. Brak danych nie jest dowodem na to, że nic się nie stało
JavaScript ułatwia tworzenie eleganckich rozwiązań awaryjnych. Łańcuchowanie opcjonalne zapobiega błędom dostępu do właściwości, spajanie wartości nullish dostarcza wartości domyślne, a blok catch może zwrócić pusty tablicę. Są one przydatne, gdy brak jest oczekiwany i zrozumiały. Stają się szkodliwe, gdy zacierają różnicę między „nie ma nic” a „nie udało nam się to ustalić”.
Gdy rozwiązania awaryjne ukrywają incydenty
Zapytanie w panelu sterowania zawodzi, ponieważ baza danych nie działa, usługa zwraca [], a interfejs użytkownika radośnie informuje „Brak znalezionych rekordów”. Sprawdzenie uprawnień również się nie udaje, opcjonalne łączenie zwraca undefined, a kod traktuje to jak zwykłe false. Błędnie sformatowana odpowiedź generuje undefined, które po trzech poziomach głębokości zamienia się w obiekt domyślny. Aplikacja wydaje się stabilna, ponieważ nigdy się nie zawala, ale podaje użytkownikom informacje, których tak naprawdę nie zna.
Te wyniki nie są wzajemnie zamienialne:
- Zapytanie, które zostało wykonane i zwróciło zero wierszy, w porównaniu z zapytaniem, które w ogóle nie zostało wykonywane.
- Opcjonalny awatar, który jest brakujący, w porównaniu z obiektem użytkownika, który jest brakujący.
- Celowane odrzucenie transakcji przez system biznesowy, w porównaniu z przestojem sieciowym.
Każdy z nich może wymagać własnej wiadomości dla użytkownika, polityki ponawiania prób, alertów oraz procedur wsparcia. W środowisku produkcyjnym takie awarie są czymś rutynowym, a nie teoretycznym: zależności przestają działać, uprawnienia się zmieniają, aktualizacje stopniowe tymczasowo uruchamiają mieszane wersje oprogramowania, a stare dane naruszają nowe zasady. Jeśli każdy nietypowy wynik zostanie sprowadzony do „pustego” stanu, incydenty pozostają niewidoczne, dopóki jakiś inny sygnał nie stanie się na tyle wyraźny, by zostać zauważonym.
Najpierw zdefiniuj umowę, a następnie dodaj elementy obronne
Niech łańcuchowanie opcjonalne będzie używane tam, gdzie wartość jest rzeczywiście opcjonalna. Wykorzystaj rozwiązanie awaryjne, gdy system ma rzetelną alternatywę do zaoferowania. Podczas łapania błędów przekształcaj je na stabilne kategorie, takie jak „nie znaleziono”, „zabronione”, „niedostępne” lub „nieprawidłowe”, ale zachowaj oryginalną przyczynę i kontekst do celów logowania i monitoringu. Eleganckie obniżanie jakości powinno zapewniać użyteczność aplikacji przy jednoczesnym zachowaniu dokładności; nie powinno nigdy sprawiać wrażenia sukcesu systemu poprzez zwracanie wiarygodnej wartości.
7. Stan w pamięci nie jest udostępniany w całej aplikacji
Zakres modułu ułatwia zarządzanie stanem w pamięci. Zmienna najwyższego poziomu może przechowywać cache, śledzić trwające zadania, liczyć żądania w celu ograniczenia przepustowości lub zapamiętywać, czy inicjalizacja już miała miejsce. Lokalnie pojedynczy proces obsługuje każde żądanie, dzięki czemu ta zmienna zachowuje się jak globalny stan aplikacji.
W środowisku produkcyjnym ten sam kod może być uruchamiany w kilku procesach, kontenerach, instancjach serverless lub regionach, a każdy z nich ma własną pamięć:
- Cache zaktualizowany na jednej instancji pozostaje przestarzały na innych.
- Flaga „już zainicjowana” chroni tylko proces, który ją ustawił.
- Limiter szybkości działający w pamięci pozwala klientowi przekroczyć limit, po prostu korzystając z różnych instancji.
- Timer zaplanowany w pamięci znika po ponownym uruchomieniu kontenera.
Nawet jeden proces nie jest tak trwały, jak się wydaje. Przy każdej aktualizacji musi być restartowany, platformy serverless zamrażają i ponownie tworzą instancje, awarie niszczą wszystko, co nie zostało zapisane, a pod presją pamięci platforma może zakończyć działanie procesu, nie dając mu szansy na zapisanie prac w toku.
Daj stanowi taką skalę, jakiej faktycznie potrzebuje
Stan w pamięci jest nadal doskonały do buforów na poziomie procesu, optymalizacji lokalnych, krótkotrwałej koordynacji w ramach jednej prośby oraz dla wszelkich wartości, których utrata jest do przyjęcia. Problemy pojawiają się, gdy przypisuje mu się zadania wymagające uprawnień na poziomie całego systemu lub trwałości danych. Udziałowe limity przepustowości zazwyczaj powinny być przechowywane w centralnym magazynie, takim jak Redis. Zadania, które muszą przetrwać restarty, powinny znajdować się w kolejce lub bazie danych. Rozproszone blokady wymagają mechanizmu widocznego dla wszystkich procesów konkurujących o dostęp. Krytyczna konfiguracja powinna pochodzić z niezawodnego źródła, a nie z zmiennej możliwej do modyfikacji w jednym instancji.
Pytania, które należy zadać, dotyczą zakresu i czasu trwania. Czy ten stan istnieje przy każdej wywołaniu funkcji, w ramach sesji użytkownika, procesu, konkretnego rozwiązania, czy w całym systemie? A co się z nim dzieje, gdy proces znika? W większości nowoczesnych architektur jeden silnik JavaScript nie jest aplikacją – jest jedynie tymczasowym elementem w większym systemie.
Produkcja jest bardziej uczciwa, a nie bardziej losowa
Jest kuszące określanie procesu produkcyjnego jako nieprzewidywalnego. Dokładniej jest powiedzieć, że w końcu zapewnia on czas realizacji, skalę, dane historyczne, jednoczesnych użytkowników oraz ograniczenia infrastruktury, z którymi aplikacja od początku miała się mierzyć. JavaScript nadal stosuje dokładnie te same zasady: niepuste łańcuchy są uważane za prawdziwe, funkcje asynchroniczne nakładają się przy kolejnych wywołaniach, tablice są przeszukiwane liniowo, a zmienne modułowe należą do jednego środowiska wykonawczego. Zaskoczenia pochodzą z założeń, których lokalne warunki nigdy nie zmusiły nikogo do sprawdzenia.
Kod niezawodny nie próbuje chronić się przed każdym możliwym scenariuszem. Identyfikuje założenia, które okazałyby się kosztowne w przypadku błędu, i czyni je jawnymi. Dodatkowy kod jest zazwyczaj niewielki; prawdziwą korzyścią jest usunięcie niejednoznaczności. Następny programista może zobaczyć, które wartości są ważne, która operacja generuje wynik, co oznacza sukces oraz czy ponowna próba jest bezpieczna.
Główne wnioski
- Analizuj i weryfikuj wartości zewnętrzne raz na granicy, utrzymując synchronizację schematów w czasie wykonywania i statycznych typów.
- Traktuj każdą ważną operację zapisu jako taką, która może zostać wykonana więcej razy, i zapewnij unikalność w miejscu przechowywania danych.
- Pamiętaj, że
awaitwstrzymuje tylko jedną wywołanie; nie serwializuje systemu ani nie blokuje wspólnego stanu. - Decyduj, który asynchroniczny wynik odpowiada za bieżący stan, w tym wskaźniki ładowania i błędów.
- Wybieraj struktury danych i zapytania odpowiednie do objętości danych, jaką będziesz miał, a nie do przykładowego scenariusza testowego.
- Zachowuj „puste” i „nieudane” jako odrębne wyniki aż do poziomu użytkownika i systemów monitoringu.
- Przechowuj stan w zakresie i z trwałością, jakich rzeczywiście wymaga, zakładając, że każdy pojedynczy proces może zniknąć.
Pozycje pokrewne
- Dlaczego kod backendu, który działa lokalnie, lamie się przy rzeczywistym obciążeniu produkcji — Praktyczny przegląd założeń dotyczących środowiska, bazy danych, bezpieczeństwa i niezawodności, które sprawnie działają na localhost, ale powodują awarie, gdy rzeczywisty ruch trafia do środowiska produkcyjnego.
- Powszechne błędne pojęcia o async/await, które powodują błędy w produkcji — Wyjaśnia dziewięć subtelnych nieporozumień związanych z async/await – od warunków konkurencyjnych po nierozpatrzone odrzucenia – które potajemnie psują aplikacje JavaScript w rzeczywistych warunkach.