callbacki przed save

Co jest złego w takim kawałku kodu:

class User
  def before_create
    self.active = true
    self.blacklisted = false
  end
end

Z pozoru nic. Ale spróbujmy teraz stworzyć użytkownika - za każdym razem dostaniemy ActiveRecord::RecordNotSaved.

Dlaczego tak się dzieje? Po prostu wartością zwracaną przez metodę (jeśli nie jest jawnie wywoływany return) jest wartość ostatniego wyrażenia - w tym przypadku false. A ponieważ callback before_create oprócz możliwości inicjalizacji służy też jako filtr, więc wartość false oznacza, iż obiekt nie zostanie zapisany.

Jak w łatwy sposób zmienić omawianą metodę, żeby zadziałała poprawnie? Wystarczy zamienić linie:

class User
  def before_create
    self.blacklisted = false
    self.active = true
  end
end

A żeby nie mieć problemów w przyszłości warto wyrobić sobie nawyk dodawania return true na końcu callbacków.

Posted by Marek Janukowicz Mon, 10 Mar 2008 22:59:00 GMT


Plugin: timed_fragment_cache

Cache i kwestie związane z jego utrzymaniem i ekspiracją nie są zapewne obce żadnemu programiście Rails mającemu na swoim koncie jakąkolwiek aplikację “produkcyjną”. Jak to wyczytałem gdzieś w sieci: “W programowaniu są tylko dwa poważne problemy: nazywanie zmiennych i ekspiracja cache” (może trochę przekręcam cytat, ale sens był na pewno właśnie taki). Na szczęście istnieje plugin timed_fragment_cache, który znacznie ułatwia to trudne zagadnienie.

Plugina instalujemy wydając polecenie:

script/plugin install http://svn.livsey.org/plugins/timed_fragment_cache

Co nam on daje? Przede wszystkim pozwala na określenie czasu, po jakim dany fragment automatycznie się wyekspiruje. Dzięki temu nie musimy się martwić, czy ręcznie wyekspirowaliśmy fragment we wszystkich miejscach w kodzie, w których jego zawartość może się zmienić. Czasową ekspirację robi się bardzo prosto:

  <% cache 'costam', 1.hour.from_now do %>
    ...
  <% end %>

Jak łatwo się domyślić, fragment ‘costam’ zostanie wyekspirowany po godzinie.

Wygodne? Wygodne. Proste? Proste. Ale jak uważny czytelnik zapewne zauważył, ten prosty blok kodu nie rozwiązuje bynajmniej całkowicie problemu z cache’owaniem fragmentów. Załóżmy, że uzupełniamy wykropkowany fragment:

  <% cache 'costam', 1.hour.from_now do %>
     <% @posts.each do |post| %>
        <%= @post.title %><br/>
     <% end %>
  <% end %>

a w kontrolerze inicjujemy @posts następująco:

  @posts = Post.find( :all )

Przykład może niezbyt wydumany, ale za to życiowy :) Problem polega oczywiście na tym, że nawet wtedy, kiedy fragment jest w dalszym ciągu w cache, zmienna @posts jest niepotrzebnie inicjowana (=> jest przeprowadzane potencjalnie kosztowne zapytanie bazodanowe). Na szczęście omawiany plugin w tej sytuacji przychodzi nam z pomocą - inkryminowaną linię kodu w kontrolerze zastępujemy przez:

  when_fragment_expired 'costam', 1.hour.from_now do
    @posts = Post.find( :all )
  end

Dzięki temu zmienna @posts będzie inicjowana tylko wtedy, kiedy fragment zostanie wyekspirowany i trzeba go ponownie wyrenderować.

Ważne: jeśli używamy when_fragment_expired, to w wywołaniu cache w widoku nie podajemy czasu ekspiracji. Jeśli nie będziemy o tym pamiętać, to narazimy się na trudno wykrywalne błędy związane z minimalnymi różnicami czasowymi między wykonaniem akcji kontrolera a renderowaniem widoku.

Na koniec bezczelnie się pochwalę - autor plugina dodał mojego patcha, który pozwala na bezproblemowe działanie plugina przy wielu serwerach Mongrel (patrz moje wcześniejsze artykuły nt. współbieżności). To chyba wyjaśnia, dlaczego ten plugin omawiam jako pierwszy :)

Posted by Marek Janukowicz Tue, 26 Feb 2008 18:09:00 GMT


Współbieżność - testowanie

W poprzednim poscie opisałem typowe problemy współbieżnościowe i zaproponowałem ich rozwiązania, natomiast teraz chciałbym uzupełnić ten opis o sposoby testowania pod kątem współbieżności w sposób jak najbardziej zgodny z filozofią Rails.

Na początku zastrzeżenie - opisuję coś, co napisałem sam na własne (tzn. tworzonego projektu) potrzeby. Na pewno wiele elementów dałoby się ulepszyć i może uda mi się z tego zrobić kiedyś plugina, ale na razie mój “system testowania współbieżnego” pozostaje w pierwszej, oryginalnej wersji zgodnie z zasadą, że prowizorki są najbardziej trwałe :)

Warto stworzyć osobny katalog na testy współbieżności, ja swój nazwałem test/concurrent.W tym katalogu tworzymy bazową klasę dla testów, np. ConcurrentTest:

    require 'test/unit/ui/console/testrunner'

    class ConcurrentTest < Test::Unit::TestCase

      def concurrent( count )
        saved_config = ActiveRecord::Base.remove_connection
        (1..count).collect { |i|
          fork do
              ActiveRecord::Base.establish_connection( saved_config )
              yield i
          end
        }.each {|process_id| Process.waitpid process_id}
      end

      def self.run
        Test::Unit::UI::Console::TestRunner.run(self)
      end
    end

Jeśli chcemy korzystać z istniejącego test helpera, to dodajemy na początku pliku linię require File.dirname( __FILE__ ) + '/../test_helper'.

Myślę, że kod jest zrozumiały - w samym teście wywołujemy metodę concurrent_test zadając jej ilość współbieżnych procesów, a metoda ta forkuje odpowiednią ich ilość i dla każdego wykonuje blok. Bardzo ważne jest prawidłowe zarządzanie połączeniami do bazy danych - jeśli nie będziemy tworzyli osobnego połączenia dla każdego procesu potomnego, to ten, który zakończy pierwszy nieźle namiesza zamykając połączenie używane przez pozostałe.

Teraz przykład testu:

    #!/usr/bin/env ruby
    require File.dirname( __FILE__ ) + '/concurrent_test_helper'

    class PostTest < ConcurrentTest

      fixtures :posts

      def test_view_count
        count = 10
        post = Post.find 1
        assert_equal 0, post.view_count
        concurrent( count ) do
          post = Post.find 1
          sleep 1
          post.increase_view_count
        end
        post = Post.find 1
        assert_equal count, post.view_count
      end

    end

    PostTest.run

Dziesięciokrotnie zwiększamy licznik odsłon posta (w 10 współbieżnych procesach), a następnie sprawdzamy, czy wartość licznika wynosi 10. Prawda, że proste?

Wątpliwości może budzić użycie funkcji sleep - otóż jedną z najbardziej wkurzających cech błędów związanych ze współbieżnością jest trudność ich powtórzenia. W podanym wyżej przykładzie trzeba by liczyć na to, że któryś proces wykona find i increase_view_count naprzemiennie z innym, a nie że każdy z procesów wykona po kolei obie te metody, a dopiero potem pozwoli się wykonać następnemu. Użycie sleep praktycznie gwarantuje nam najpierw wykonanie find przez wszystkie procesy, a dopiero potem increase_view_count, co (przy założeniu, że increase_view_count nie dba o współbieżność) pozwoli nam na zreprodukowanie błędu za każdym razem.

Nie w każdym przypadku umieszczenie sleep w ciele testu pozwoli na odtworzenie błędu. Czasami trzeba tę funkcję wywołać w inkryminowanej metodzie albo nawet w procedurze bazodanowej (dla Postgresa byłoby to pg_sleep). Pamiętajcie u usunięciu tego wywołania po zakończeniu testowania!

Teraz wystarczy odpalić nasz plik z testem i czekać na wynik.

Integracja

Możemy zintegrować nasze współbieżne testy nieco bardziej z infrastrukturą testową Rails. W tym celu tworzymy następującego taska Rake:

    namespace :test do

      task :concurrency => 'db:test:prepare' do
        Dir.glob( "test/concurrent/*test.rb" ).each { |test| system test }
      end
    end

I teraz możemy odpalić wszystkie nasze testy używając rake test:concurrency.

Mam jeszcze taki pomysł, żeby wyprodukować ładny raport dot. ilości błędów (taki jak podaje rake test) i użyć kodu wyjścia programu w celu podliczenia ilości testów, które oblały, ale na razie pozostawiam to jako ćwiczenie dla czytelnika :)

Uwagi

  • jako że aplikacje Rails nie oszczędzają specjalnie pamięci, a my odpalamy ich np. 10 naraz, należy zaopatrzyć się w odpowiednią ilość RAM. Moja praktyka pokazuje, że przy 10 współbieżnych procesach laptop z 1GB RAM potrafi swapować (oczywiście uruchomione były też różne inne programy) - specjalnie do testów współbieżnych dorzuciłem jeszcze 1GB :)
  • ilość współbieżnych procesów można zdefiniować tak: minimalna ilość, która pozwala na w miarę regularne odtworzenie błędu. W powyższym przykładzie właściwie wystarczyłoby uruchomić 2 procesy, ale czasami i 10 to za mało.
  • jeśli błąd nie chce się pojawić mimo wielokrotnego uruchamiania testów, to należy jeszcze raz dokładnie przeanalizować kod: aplikacji, że potwierdzić, że błąd naprawdę może wystąpić i testu, by sprawdzić, że stworzone zostały odpowiednie warunki do jego wystąpienia (patrz uwaga nt. sleep). Pamiętajcie, że błędy “współbieżnościowe” są mało deterministyczne i kilkukrotny bezbłędny przebieg testów może świadczyć zarówno o poprawności programu jak i niskiej jakości testów
  • zamiast testować ponownie podobne kawałki kodu (np. licznik odsłon posta, licznik odsłon artykułów użytkownika, licznik odsłon komentarza) lepiej jest użyć sprawdzonego kawałka kodu jako wzorca, a czas przeznaczony na testowanie poświęcić na przetestowanie innego przypadku

Posted by Marek Janukowicz Mon, 11 Feb 2008 21:14:00 GMT