Elixir zapewnia odporność na awarie i współbieżność, które inne języki dorabiają jako dodatek. Narzędzia AI czasem generują kod, który walczy z wzorcami OTP zamiast je wykorzystywać. Te przepisy produkują idiomatyczny Elixir — prawidłowe GenServery, zestawy zmian Ecto z prawdziwymi walidacjami i Phoenix LiveView, które poprawnie korzysta z socketa.
Przepisy na Phoenix API i LiveView z prawidłowymi wzorcami kanałów/socketów
Przepisy na schematy i zapytania Ecto z kompozycyjnymi zapytaniami i prawidłowymi zestawami zmian
Wzorce OTP: GenServer, drzewa Supervisorów i nadzór Tasków
Przepisy na testy dla ExUnit z prawidłowym sandboxem i wzorcami asynchronicznymi
Scenariusz: Potrzebujesz REST API do aplikacji zarządzania zadaniami z prawidłową walidacją i formatowaniem błędów.
Wskazówka
Stwórz Phoenix JSON API dla zadań. (1) Wygeneruj kontekst: moduł kontekstu Tasks ze schematami Ecto — Task (title string wymagany min 3, description text opcjonalny, status enum :todo/:in_progress/:done/:cancelled, priority enum :low/:medium/:high/:urgent, due_date date opcjonalny musi być w przyszłości, assignee_id references users). (2) Changeset: validate_required dla title i status, validate_length dla title (3..200), validate_inclusion dla status i priority, niestandardowy validate_status_transition który uniemożliwia przejście done->todo. (3) Funkcje kontekstu: list_tasks(filters) z kompozycyjnymi zapytaniami Ecto (filtrowanie po status, priority, assignee, zakres due_date, wyszukiwanie pełnotekstowe po title), create_task(attrs), update_task(task, attrs), delete_task(task). (4) Kontroler z prawidłową obsługą błędów: renderuje błędy changesetu jako { "errors": { "title": ["can't be blank"] } }. (5) Specyfikacja OpenAPI z użyciem open_api_spex dla każdego endpointu. (6) Testy: testy kontekstu z DataCase, testy kontrolera z ConnCase testujące sukces, walidacje i autoryzacje.
Oczekiwany wynik: Moduł kontekstu, schemat z changesetem, kontroler, widoki JSON, specyfikacja OpenAPI i testy.
Scenariusz: Twój panel wyświetla metryki na żywo, które aktualnie wymagają odświeżania strony. Potrzebujesz aktualizacji w czasie rzeczywistym.
Wskazówka
Zbuduj panel LiveView. (1) Moduł DashboardLive, który przypisuje początkowe dane w mount (liczba aktywnych użytkowników, ostatnie zamówienia, stan systemu). (2) Subskrybuj tematy PubSub przy mount: “orders:new”, “users:activity”, “system:health”. Obsługuj każdą wiadomość w handle_info aktualizując assigns. (3) Użyj temporary_assigns dla listy zamówień, aby zapobiec wzrostowi pamięci — przechowuj w pamięci tylko 20 najnowszych. (4) Stwórz LiveComponents: StatsCardComponent (wyświetla metrykę ze strzałką trendu), OrderTableComponent (sortowalna tabela z phx-click do sortowania), HealthGaugeComponent (animowany wskaźnik używający SVG). (5) Dodaj wyszukiwanie na żywo: phx-change na inpucie z debouncingiem 300ms, handle_event “search” filtruje zamówienia w sockecie. (6) Wysyłaj zdarzenia do aktualizacji wykresów po stronie klienta używając push_event i hooków JavaScript. (7) Obsłuż rozłączenie elegancko: pokaż baner “reconnecting” używając klasy CSS phx-disconnected. Testuj za pomocą LiveViewTest, weryfikując że wiadomości PubSub aktualizują stronę.
Oczekiwany wynik: Moduł LiveView, 3 LiveComponents, subskrypcje PubSub, hooki JS i testy LiveViewTest.
Scenariusz: Twoje API potrzebuje ograniczania liczby zapytań per użytkownik, ale nie chcesz zewnętrznej zależności jak Redis.
Wskazówka
Zaimplementuj rate limiter oparty na GenServer. (1) GenServer RateLimiter: stan to mapa %{user_id => {count, window_start}}. init uruchamia okresowy timer czyszczenia. (2) check_rate(user_id, limit, window_seconds) — wywołaj GenServer, sprawdź czy użytkownik jest w limitach, zwróć :ok lub {:error, :rate_limited, retry_after}. Użyj tablicy :ets do szybkich odczytów bez blokowania GenServera dla operacji tylko-odczyt. (3) handle_info(:cleanup, state) — usuwa wygasłe wpisy co minutę. (4) Dodaj do drzewa nadzoru pod DynamicSupervisor, aby móc mieć jeden RateLimiter na endpoint lub na tenanta dla izolacji. (5) Stwórz middleware Plug RateLimitPlug, który wywołuje GenServer, zwraca 429 z nagłówkiem Retry-After przy przekroczeniu limitu. (6) Uczyńs go rozproszonym: użyj :pg (grup procesów) do synchronizacji stanu rate limitu między węzłami klastra. (7) Konfiguracja przez application env: domyślne limity na plan taryfowy. Testuj egzekwowanie limitów, reset okna i czyszczenie.
Oczekiwany wynik: GenServer, optymalizacja ETS, middleware Plug, wsparcie dystrybucji i testy.
Scenariusz: Tworzenie zamówienia obejmuje aktualizacje stanów magazynowych, tworzenie rekordów płatności i wysyłanie powiadomień — jeśli jakikolwiek krok zawiedzie, wszystko powinno zostać wycofane.
Wskazówka
Zaimplementuj tworzenie zamówienia używając Ecto.Multi dla bezpieczeństwa transakcyjnego. (1) Orders.create_order(user, items, payment_info) buduje potok Ecto.Multi: :validate_stock — dla każdego elementu sprawdź czy produkt ma wystarczający stan (zwróć błąd jeśli nie). :create_order — wstaw rekord zamówienia. :create_items — wstaw wszystkie pozycje zamówienia z cenami jednostkowymi uchwyconymi w momencie zakupu. :update_stock — zmniejsz stan dla każdego produktu używając optymistycznego blokowania (where: [stock: fragment("stock >= ?", ^quantity)]). :process_payment — wywołaj bramkę płatności (owiń w try/catch, jeśli zawiedzie po zmniejszeniu stanu, transakcja zostanie wycofana). :calculate_total — zaktualizuj sumę zamówienia z cen pozycji. (2) Obsłuż wyniki Ecto.Multi: przy sukcesie rozgłoś “orders:new” przez PubSub i dodaj do kolejki e-mail z potwierdzeniem. Przy niepowodzeniu zwróć konkretny błąd (out_of_stock z nazwą produktu, payment_failed z błędem bramki). (3) Stwórz łańcuch with w kontekście, który mapuje błędy Multi na przyjazne dla użytkownika komunikaty. Testuj szczęśliwą ścieżkę, niewystarczający stan, awarie płatności i wyścig przy równoczesnych zamówieniach.
Oczekiwany wynik: Potok Ecto.Multi, obsługa błędów, rozgłaszanie PubSub i testy transakcji.
Scenariusz: Potrzebujesz niezawodnych zadań w tle z ponawianiem, harmonogramowaniem i monitorowaniem.
Wskazówka
Skonfiguruj Oban do przetwarzania zadań w tle. (1) Skonfiguruj Oban w config/config.exs z notyfikatorem PostgreSQL, kolejki: default (concurrency 10), emails (concurrency 5), reports (concurrency 2), imports (concurrency 1). (2) Stwórz workery: EmailWorker — wysyła transakcyjne e-maile przez Swoosh, ponawia 3 razy z wykładniczym opóźnieniem, odrzuca po 24 godzinach. ReportWorker — generuje raporty CSV/PDF, przesyła do S3, powiadamia użytkownika. ImportWorker — przetwarza importy CSV w partiach po 500, raportuje postęp przez PubSub (LiveView subskrybuje, aby pokazać pasek postępu). (3) Dodaj ograniczenia unikalności: zadania e-mail unikalne po {recipient, template} w ciągu 1 minuty (zapobieganie duplikatom). (4) Zaplanowane zadania z Oban.Cron: codziennie o 2:00 uruchom czyszczenie, w poniedziałki uruchom raport analityczny. (5) Dodaj Oban.Web do panelu monitorowania pod /admin/jobs. (6) Zaimplementuj anulowanie zadań: przechowuj ID zadania Oban przy rozpoczynaniu długiego importu, pozwól na anulowanie z interfejsu użytkownika. Testuj workery używając Oban.Testing.perform_job/3 z zamockowanymi zależnościami.
Oczekiwany wynik: Konfiguracja Oban, 3 workery, harmonogram cron, ograniczenia unikalności i testy workerów.
Scenariusz: Twoje API potrzebuje uwierzytelniania JWT z kontrolą dostępu opartą na rolach i odświeżaniem tokenów.
Wskazówka
Zaimplementuj uwierzytelnianie używając Guardian. (1) Moduł Guardian z callbackami subject_for_token i resource_from_claims używającymi ID użytkownika. Skonfiguruj z HS256 i 15-minutowym TTL dla tokenów dostępu, 30-dniowym dla tokenów odświeżania. (2) Potok AuthPipeline plug: Guardian.Plug.VerifyHeader (scheme: “Bearer”), Guardian.Plug.EnsureAuthenticated, Guardian.Plug.LoadResource. (3) Plug EnsureRole, który sprawdza Guardian.Plug.current_resource(conn).role względem wymaganych ról, zwraca 403 jeśli uprawnienia są niewystarczające. (4) Kontroler auth: rejestracja (hashowanie z Argon2, zwróć tokeny), logowanie (weryfikacja hasła, zwróć tokeny), odświeżanie (wymień token odświeżania na nową parę), wylogowanie (unieważnij w bazie danych). (5) Unieważnanie tokenów: przechowuj unieważnione tokeny w GenServer wspieranym przez ETS (sprawdzaj przy każdym żądaniu) z okresową synchronizacją do bazy danych dla trwałości między restartami. (6) Zastosuj potoki w routerze: pipe_through [:api, :auth] dla chronionych tras, pipe_through [:api, :auth, :admin] dla tras administracyjnych. Testuj każdy scenariusz auth włączając wygasanie i unieważnanie tokenów.
Oczekiwany wynik: Konfiguracja Guardian, potok auth, plug roli, kontroler auth, unieważnanie tokenów i testy.
Scenariusz: Twoje zapytania są zduplikowane między kontekstami z drobnymi różnicami. Potrzebujesz kompozycyjnego, wielokrotnego użytku w budowaniu zapytań.
Wskazówka
Stwórz system kompozycyjnych zapytań. (1) Moduł QueryBuilder z funkcjami, które przyjmują i zwracają Ecto.Queryable: filter_by(query, :status, value), filter_by(query, :date_range, {from, to}), filter_by(query, :search, term) używając ILIKE z prawidłowym escapingiem, sort_by(query, field, direction) z białą listą dozwolonych pól, paginate(query, page, page_size) zwracając %{entries: list, total: int, page: int, total_pages: int}. (2) Kompozycja w kontekście: list_products(params) potokuje Product |> filter_by(:status, params.status) |> filter_by(:search, params.q) |> sort_by(params.sort_by, params.sort_dir) |> paginate(params.page, params.page_size). (3) Dodaj ładowanie preload zapytań: with_preloads(query, [:category, :variants]) które warunkowo ładuje preloady w zależności od potrzeb wywołującego. (4) Dodaj zakresy zapytań: active(query) dodaje where: [is_active: true], published(query) dodaje where: [status: :published]. (5) Zapytania agregujące: with_stats(query) dodaje podzapytanie dla order_count i avg_rating. Testuj każdą funkcję zapytania indywidualnie i w kompozycji.
Oczekiwany wynik: Moduł QueryBuilder, zakresy, helpery preload, zapytania agregujące i testy kompozycji.
Scenariusz: Potrzebujesz czatu wielopokojowego ze śledzeniem obecności i trwałym przechowywaniem wiadomości.
Wskazówka
Zbuduj system czatu na kanałach Phoenix. (1) RoomChannel: dołączenie wymaga uwierzytelnienia, ładuje ostatnie 50 wiadomości z bazy danych przy dołączeniu, rozgłasza nowe wiadomości do pokoju. (2) Obsługa wiadomości: waliduj treść wiadomości (niepusta, max 5000 znaków), zapisz do bazy danych, rozgłoś do pokoju z informacjami o użytkowniku i znacznikiem czasu. (3) Śledzenie obecności używając Phoenix.Presence: śledź dołączanie/wychodzenie użytkowników per pokój, rozgłaszaj presence_state i presence_diff, udostępniaj listę “użytkowników online”, którą LiveView może wyświetlić. (4) Wskaźniki pisania: obsłuż zdarzenie “typing”, rozgłoś do pokoju z wyłączeniem nadawcy, automatyczne wygasanie po 3 sekundach. (5) Załączniki plików: akceptuj przesyłanie plików przez kanał, prześlij do S3 w Tasku, rozgłoś URL załącznika po zakończeniu. (6) Ograniczanie liczby zapytań: max 10 wiadomości na sekundę na użytkownika używając GenServera per kanał. (7) Stwórz helper testowy ChannelCase do testowania dołączania do kanałów, wiadomości i obecności.
Oczekiwany wynik: Kanał pokoju, trwałość wiadomości, śledzenie obecności, wskaźniki pisania i testy kanałów.
Scenariusz: Twoja aplikacja ma komponenty, które padają niezależnie. Potrzebujesz prawidłowego nadzoru dla izolacji awarii.
Wskazówka
Zaprojektuj drzewo nadzoru dla aplikacji przetwarzającej dane. (1) Supervisor najwyższego poziomu Application (strategy: :one_for_one): uruchamia Repo, PubSub, Endpoint i WorkerSupervisor. (2) WorkerSupervisor (strategy: :rest_for_one): uruchamia ConnectionPool (GenServer zarządzający połączeniami WebSocket do zewnętrznych źródeł danych), DataProcessor (GenServer, który odbiera dane z połączeń i je przetwarza), MetricsCollector (GenServer, który agreguje metryki i je udostępnia). rest_for_one zapewnia, że jeśli ConnectionPool padnie, DataProcessor i MetricsCollector też się zrestartują, ponieważ od niego zależą. (3) ConnectionPool używa DynamicSupervisor do zarządzania poszczególnymi połączeniami WebSocket — każde połączenie to nadzorowany Task, który ponownie łączy się po awarii z wykładniczym opóźnieniem. (4) Dodaj Registry do nazywania połączeń po ID źródła danych. (5) Dodaj TaskSupervisor dla zadań typu “odpal i zapomnij” (wysyłanie powiadomień, zapis do zewnętrznych API), które nie powinny powodować awarii wywołującego. (6) Zaimplementuj circuit breaker w ConnectionPool: jeśli źródło danych zawiedzie 5 razy w ciągu 1 minuty, przestań się ponownie łączyć na 5 minut. Testuj, że awaria jednego komponentu nie wpływa na rodzeństwo i że strategia restartu działa prawidłowo.
Oczekiwany wynik: Supervisor aplikacji, WorkerSupervisor, ConnectionPool z DynamicSupervisor i testy restartów.
Scenariusz: Twoje wdrożenie kopiuje projekt na serwer i uruchamia mix phx.server. Potrzebujesz prawidłowych releasów.
Wskazówka
Skonfiguruj Mix releases do wdrożenia produkcyjnego. (1) Skonfiguruj sekcję releases w mix.exs z konfiguracją runtime, generowaniem cookie i niestandardowymi argumentami VM (ustaw schedulery na liczbę rdzeni CPU, ustaw limit procesów). (2) Stwórz config/runtime.exs, który odczytuje całą konfigurację ze zmiennych środowiskowych przy starcie releasu: DATABASE_URL, SECRET_KEY_BASE, PHX_HOST, PORT, POOL_SIZE. Waliduj wymagane zmienne i szybko zawiedź z opisowymi błędami. (3) Stwórz skrypt rel/overlays/bin/migrate, który uruchamia migracje Ecto używając releasu: bin/app eval "App.Release.migrate". (4) Stwórz lib/app/release.ex z funkcjami migrate() i rollback(version). (5) Dockerfile: wieloetapowy build używając elixir:1.16-otp-26-alpine do budowania, alpine:3.19 do runtime. Kopiuj tylko tarball releasu. Dodaj health check. Uruchom jako użytkownik nie-root. (6) GitHub Actions: kompilacja i testy, budowanie releasu w Dockerze, push do rejestru, wdrożenie z zerowym przestojem używając rolling update. Testuj, czy release uruchamia się, wykonuje migracje i obsługuje ruch.
Oczekiwany wynik: Konfiguracja releasu, runtime.exs, skrypty migracji, Dockerfile, workflow CI i testy uruchomienia.
Uwaga
Typowe pułapki Elixira:
Wąskie gardła procesów: Jeśli AI wysyła wszystkie wiadomości przez pojedynczy GenServer, staje się on wąskim gardłem. Upewnij się, że operacje bezstanowe są obsługiwane w procesie wywołującym, nie w GenServer.
Zapytania N+1 w Ecto: AI może generować kod, który uzyskuje dostęp do asocjacji bez ich preloadowania, powodując zapytania N+1 z leniwym ładowaniem w produkcji. Zawsze weryfikuj, czy preloady są określone.
Pamięć LiveView: Jeśli AI przechowuje duże zbiory danych w assigns socketa bez temporary_assigns, każdy połączony użytkownik zużywa znaczną ilość pamięci. Używaj temporary_assigns dla list.
Kompletność dopasowania wzorców: Jeśli AI zapomni klauzuli funkcji dla wartości enum, GenServer pada w czasie wykonywania. Upewnij się, że wszystkie wartości enum/statusu są obsługiwane.