Posted by Piotr Sarnacki
Fri, 03 Oct 2008 10:37:00 GMT
W railsach od jakiegoś czasu można używać named_scope. Jest to bardzo fajny mechanizm umożliwiający łatwiejsze skonstruowanie zapytania używając wcześniej zdefiniowanych metod. Wcześniej był dostępny jako plugin has_finder.
Wygląda to mniej więcej tak:
named_scope :active, :conditions => { :active => true }
named_scope :paid, :conditions => { :paid => true }
named_scope :recent, lambda { { :conditions => ['created_at >= ?', 1.week.ago] } }
Po zdefiniowaniu takich sope’ów (macie pomysł jak to spolszczyć?) można ich używać w ten sposób (zakładam, że scope’y zostały zadeklarowane w modelu Product):
Product.active.paid.recent
Jak można łatwo zauważyć, scope’y można łączyć w łańcuchy. Dzięki takiej konstrukcji otrzymujemy prosty i treściwy kod, który łatwo zrozumieć. Teraz coś lekko trudniejszego:
named_scope :in_category, lambda { |category| { :conditions => { :category_id => category } } }
# i użycie
Product.active.recent.in_category(category)
O co chodzi? Jako 2 argument można przekazać lambdę, która zostanie wywołana przy użyciu scope’a i powinna zwrócić Hash.
Ale to nie są jedyne zastosowania scope’ów. Można do nich przekazać też inne parametry, które może przyjąć metoda find.
Na przykład order, offset i limit:
named_scope :order, lambda { |*args| { :order => args.first || "created_at DESC" } }
named_scope :offset lambda { |offset| { :offset => offset } }
named_scope :limit, lambda { |*args| { :limit => args.first || 10 } }
# i użycie:
Product.active.order("id DESC").offset(10).limit
W pierwszym i trzecim scope’ie zastosowałem trick, który pozwala na użycie zdefiniowanej wartości domyślnej w razie gdy żadna nie zostanie podana. `*args` pakuje argumenty do tablicy. Jeżeli args.first zwróci nil, to znaczy, że tablica jest pusta, czyli nie zostały podane żadne argumenty i trzeba zaaplikować wartość domyślną.
Coś jeszcze? Fajnie by było, żeby można było sortować nie tylko po wartościach z jednej tablicy… a do tego wypadałoby użyć joins/include. Nic prostszego.
named_scope :joins, lambda { |joins| { :joins => joins } }
# a teraz można tak:
Product.active.joins(:user).order('users.email')
How cool is that?
No i na koniec coś co ostatecznie przekonało mnie, że named_scope jest genialnym wynalazkiem. Jeżeli ktoś robił kiedyś formularze, w których można wybrać kilka opcji i na ich podstawie trzeba zbudować zapytanie, zapewne wie, że jest to nieco upierdliwe. Używając `named_scopes` można to zrobić bardzo łatwo.
Ryan Bates napisał plugin scope_builder, dzięki któremu można to zrobić jeszcze łatwiej. Przypuśćmy, że mamy formę, w której można zaznaczyć kilka pól i po ich wysłaniu należy na ich podstawie pobrać odpowiednie rekordy z bazy.
# przygotowujemy listę parametrów, które użytkownik może ustawić
parameters = [:active, :paid, :recent, :title]
# Tworzymy scope
@products = Product.scope_builder
# teraz można sprawdzić, które pola zostały zazanczone
parameters.each do |param|
@products.send(param) if params[param]
end
# na koniec można na przykład dodać paginację
@products = @products.paginate(:per_page => 10, :page => params[:id])
Dla mnie genialne. :) Można dzięki temu łatwiej tworzyć zaawansowane wyszukiwanie, sortowanie i inne tego typu rzeczy.
Oczywiście najlepiej jest zamknąć taki kawałek kodu jako metodę modelu, zgodnie ze skinny controller, fat model, ale to pozostawię jako ćwiczenie ;-)
Posted in Ruby on Rails, Programowanie | Tags named_scope, rails, ruby | 6 comments | no trackbacks
Posted by Piotr Sarnacki
Tue, 17 Jun 2008 18:50:00 GMT
Dzisiaj opowiem wam bajeczkę o tym jak mod_passenger zeswatał ze sobą Apache’a i Railsy. :)
Jeżeli ktoś sądzi, że z tego związku nic nie będzie, to rzucam kilka zalet:
- nie trzeba bawić się w konfigurację proxy i martwić o zajęte porty
- nie trzeba monitorować mongreli (ja używałem do tego goda)
- ogólna prostota użycia
- dzięki ruby enterprise można zaoszczędzić 33% pamięci
- upload buffering, czyli nie musimy się martwić o upload dużych plików – pliki są przesyłane do aplikacji railsowej dopiero, gdy zostaną w całości uploadowane na serwer
- fair load balancing – zapytania są wysyłane do procesów, które mają najmniej klientów w kolejce. żeby uzyskać coś takiego używając nginxa trzeba było instalować dodatkowy plugin
- liczba instancji aplikacji nie jest sztywno określone. używając nginxa trzeba było przewidzieć jaka liczba serwerów railsów będzie potrzebna dla danej aplikacji, jeżeli na dany serwis wchodziło mało osób niewykorzystane serwery zjadały pamięć. mod_passenger uruchamia kolejne instancje w miarę potrzeb.
Dodatkowo napisałem apache upload progress module , dzięki czemu z mod_passengerem można sklecić pasek postępu wysyłania plików na serwer (forma dokładnie taka sama jak z Nginx Upload Progress, więc jeżeli ktoś go używał, to nawet nic nie będzie musiał zmieniać w skryptach).
Mod Passenger
Aktualnie najnowszą wersją mod_passengera jest 1.9.1 (RC2), instrukcja instalacji jest na blogu phusion.
Po odpaleniu instalatora użytkownik jest prowadzony za rączkę, więc nikt nie powinien mieć probleów – lubię takie podejście, nie muszę się zastanawiać nad tym co i jak mam zrobić i kopiować i wklejać kolejnych komend.
Mod passenger sprawuje się na serwerze bardzo fajnie, do tej pory nie miałem żadnych problemów. Żeby zrestartować aplikację rails wystarczy utworzyć plik tmp/restart.txt w katalogu aplikacji.
Po instalacji można jeszcze dodać w configu apacha linijkę:
PassengerMaxPoolSize X
Zamiast X wstawiamy maksymalną liczbę instancji aplikacji odpalonych na raz. W dokumentacji twierdzą, że dla VPSa z 256 megabajtami pamięci ram dobrą wartością będzie tutaj 2, a dla serwera dedykowanego z 2GB ramu 30. Większa liczba aplikacji to więcej zajętego ramu, ale też więcej requestów do obsłużenia w danej chwili.
Ruby Enterprise Edition
O ile o mod passengerze jest dużo informacji, to o ruby enterprise edition jeszcze niewiele. Według niektórych nazwa jest nietrafiona i niefajna, niektórym nie podoba się, że phusion chce się na REE wypromować.
Mi to w zasadzie wszystko jedno jak się chłopaki z Phusion promują, kto na tym zarobi i jaka jest nazwa, o ile ta wersja będzie dobrze działała i rzeczywiście zmniejszała zużycie pamięci.
Żeby zainstalować ruby enterprise edition należy ściągnąć paczkę (najnowszą wersję paczki można znaleźć
tutaj):
wget http://rubyforge.org/frs/download.php/38777/ruby-enterprise-1.8.6-20080623.tar.gz
Następnie ją rozpakować:
tar -zxvf ruby-enterprise-1.8.6-20080623.tar.gz
i uruchomić installer:
./ruby-enterprise-1.8.6-20080623/installer
Tyle mówi opis na stronie
REE, ale to jeszcze nie wszystko. Na początek można stworzyć dowiązanie symboliczne dla
REE:
ln -sf /opt/ruby-enterprise-1.8.6-20080623/bin/ruby /usr/bin/ruby-enterprise
Użytkownicy gentoo muszą również skopiować plik auto_gem, który automatycznie jest w tej dystrybucji dodawany do
RUBYOPT:
cp /usr/lib/ruby/site_ruby/auto_gem.rb /opt/ruby-enterprise-1.8.6-20080623/lib/ruby/site_ruby
Po zainstalowaniu rubiego instaluje się też kilka gemów, ale to tylko podstawowe – resztę trzeba zainstalować samemu. Oczywiście można odpalić gem list i instalować kolejne gemy w danych wersjach, ale od czego mamy rubiego. Dosłownie w 2 minuty napisałem prosty skrypcik, który wyciąga nazwy i wersje gemów po czym instaluje je dla Ruby Enterprise Edition:
`gem list`.split("\n").each do |line|
if line =~ /([0-9a-zA-Z_\-]*) \((.*)\)/
gem, versions = $1, $2.split(", ")
versions.each do |version|
puts "Installing #{gem}, version: #{version}"
puts `ruby-enterprise /opt/ruby-enterprise-1.8.6-20080623/bin/gem install #{gem} -y --version '#{version}' 2>&1`
end
end
end
Teraz wystarczy dodać w configu apacha linijkę:
RailsRuby /opt/ruby-enterprise-1.8.6-20080623/bin/ruby
I możemy cieszyć się naszym własnym wypicowanym apachem z mod_passengerem w korporacyjnej wersji ;-)
Posted in Ruby on Rails | Tags apache, enterprise, instalacja, mod_passenger, ruby | 11 comments | no trackbacks
Posted by Piotr Sarnacki
Sat, 05 Apr 2008 05:31:00 GMT
Jakiś czas temu pojawiły się wzmianki o hotruby (tylko wtedy nie miałem czasu o tym napisać). Krótko mówiąc jest to “implementacja rubiego w javascripcie”. Możemy używać rubiego w przeglądarce i we flashu.
Kod rubiego jest wysyłany do skompilowania przez skrypt cgi, skrypt cgi kompiluje go do bytecodu, po czym javascript wykonuje ów bytecode.
Na stronie HotRuby jest wzmianka o tym, że składnia języka jest w większości zaimplementowana (na pewno nie ma wyjątków), ale jak na razie większość funkcji i bibliotek nie. Na razie nie wiem do czego mogłoby mi się takie połączenie przydać, ale patrząc na dema łatwo można zauważyć, że całkiem fajne rzeczy da się stworzyć z pomocą hotrubiego. Oby tak dalej.
Posted in Inne | Tags hotruby, javascript, ruby | 3 comments | no trackbacks
Posted by Piotr Sarnacki
Thu, 31 Jan 2008 09:09:00 GMT
Jakiś czas temu pisałem ,że mogą szykować się zmiany w deploymencie railsów. Dzisiaj przeczytałem, że Ezra Zygmuntowicz zatrudnił szóstego programistę, którego zadaniem będzie praca nad mod_rubinius dla serwerów Nginx i Apache. Więcej w podcaście z udziałem Ezry
Mały cytat:
More interesting things from the podcast:
- In like one night Evan Phoenix implemented a multiple Rubinius VM running in single OS process in native threads and passing each other messages like it happens in Erlang). This can lead to a great solution to shared hostings and Ruby deployment problem David Hansson wrote about recently.
- Rubinius may support native code compilation along side with bytecode that Rubinius VM can run. Sounds interesting. Ryan Davis is working on Rubinius at Engine Yard and his Ruby2C experiments may be useful.
Posted in Ruby on Rails | Tags deployment, ezra, mod_rubinius, on, rails, rubinius, ruby, zygmuntowicz | 2 comments | 1 trackback
Posted by Piotr Sarnacki
Tue, 08 Jan 2008 10:06:00 GMT
Ruby posiada bardzo fajną właściwość nazwaną otwartymi klasami. Znaczy to tyle, że jeżeli nikt nie zamroził danej klasy/metody, można w dowolnym miejscu w kodzie nadpisać ją klasę, metodą, dodać nowe metody. Można tak nadpisać nawet klasy wbudowane w język!
Na przykład taki kod (och jak ja lubię ten przykład):
class Fixnum
alias old_plus +
def +(other)
(self.old_plus other) % 7
end
end
4+4 #=> 1
4+4=1 ? nie tego się spodziewaliśmy. A właściwie nie tego się spodziewali ludzie, którzy nie rozumieją jeszcze powyższego przykładu (celowo dodałem słowo jeszcze, będę na tyle cywilizowanym i miłym człowiekiem, że spróbuję wytłumaczyć o co chodzi).
Liczby całkowite są klasy Fixnum. Powyższy kod modyfikuje tą klasę. Najpierw tworzony jest alias dla metody +, żeby można było jej później używać, a następnie owa metoda zostaje nadpisana w taki sposób, żeby zwracała wynik dodawania modulo7
Do napisania tego artykułu natchnął mnie Daniel Owsiański pisząc o zjawisku roboczo nazwanym version lock-in. Daniel ma oczywiście dużo racji i moja paranoja, o której pisałem u niego w komentarzach jest objawem przewrażliwienia mojej mózgoczaszki w pewnych kwestiach. Są jednak wypadki, w których naprawdę warto zachować zgodność z nowymi wersjami. Pomaga tutaj powyższa właściwość języka Ruby. Pisał o tym kiedyś autor bloga Err the Blog w kontekście rozszerzania możliwości pluginów.
Często w rozmowach o Ruby on Rails na różnych listach dyskusyjnych można usłyszeć, że gdy chcemy coś zmienić w danej metodzie, najlepiej przekopiować kod metody, nadpisać ją, zmienić to co trzeba i voilla. Ale nie tędy droga panie i panowie :)
Wyglądałoby to mniej więcej tak. Chcemy na przykład nadpisać metodę find. Wchodzimy na
Rails API, znajdujemy ActiveRecord::Base#find, wklejamy kod w modelu i zmieniamy:
#w modelu:
def self.find(*args)
#jakiś zmodyfikowany kod finda
end
Jakie minusy ma takie podejście? Gdy mamy zainstalowaną dużą liczbę pluginów nigdy nie wiadomo czy któryś z nich nie nadpisuje już metody find i wtedy nadpisując ją stracimy funkcjonalność dodaną przez plugin. Smuteczek. Poza tym gdy zmieni się kod metody find w samym frameworku również u nas będziemy musieli go zmienić.
Jak to zrobić Ruby Way™? Przypuśćmy, że chcemy się popastwić nad wspomnianą metodą find:
#w modelu:
# metoda find jest metodą klasy
class << self
alias :old_find :find
def find(*args)
args[1] ||= {}
args[1][:conditions] ||= {}
args[1][:conditions] = [args[1][:conditions]] if args[1][:conditions].is_a?(String)
case args[1][:conditions]
when Hash:
args[1][:conditions].merge!(:deleted => false)
when Array:
if args[1][:conditions][0].strip.blank?
args[1][:conditions][0] = "deleted = ?"
else
args[1][:conditions][0] = ["(#{args[1][:conditions][0]})", "deleted = ?"].join(' AND ')
end
args[1][:conditions] << false
end
old_find(*args)
end
end
Powyższy kod dodaje do conditions warunek “deleted = false”, po czym wywołuje metodę find z tak zmodyfikowanymi argumentami. Czasami trzeba jednak namieszać coś w kodzie metody. Można wtedy dodać dodatkowy argument. Następnie dajemy ifa – jeżeli argument zwraca true wykonujemy zmodyfikowany kod, a jeżeli nie, wykonujemy kod oryginalnej metody.
Dzięki takiemu podejściu możemy w miarę łatwo upgradować pluginy i Railsy bez większego stresu :)
Posted in Ruby on Rails | Tags hacking, klasy, otwarte, ruby, rubyonrails, tips | 4 comments | no trackbacks
Posted by Piotr Sarnacki
Sun, 06 Jan 2008 09:23:00 GMT
Jak donosi Ruby Inside, wypuszczony został ostatnio serwer szybszy od mongrela.
Chodzi o Thin, czyli połączenie EventMachine, Racka (interfejs do komunikacji z serwerem) i Mongrela (a dokładniej jego bibliotek do parsowania). Instalacja i użycie jest równie łatwe co użycie samego mongrela. `gem install thin`, a później w katalogu aplikacji `thin start`. Jedna z moich aplikacji wyciągała na mongrelu 110req/s, a na thinie około 150req/s (ab -c 10 -n 100). Różnica całkiem spora. Warto się przesiąść
Sprawdziłem też ostatnio sphinxa. O sphinxie pisał już Jarosław Zabiełło i bardzo mnie powyższym artykułem zainteresował. Do tej pory używałem ferreta, ale odpalanie nie do końca stabilnych i niezbyt szybkich serwerów dla każdej aplikacji było trochę uciążliwe. Ezra Zygmuntowicz, twórca frameworka Merb wypowiadał się, że miał dość duże kłopoty ze stabilnością ferreta, które skończyły się po przejściu na sphinxa.
Instalacja opisana jest w dokumentacji, więc nie będę powtarzał :) Mogę dodać jedynie, że czasami używając bazy postgresql trzeba ręcznie dodać language poleceniem `createlang plpgsql nazwa_bazy` (chodzi o nowy język proceduralny dla postgresa). Serwer odpalony przez sphinxa jest bardzo wydajny i zajmuje kilkadziesiąt razy mniejsze ilości pamięci operacyjnej. Jedynym minusem jest to, że nie ma niektórych opcji, które posiada ferret – jeżeli ktoś ich potrzebuje proponuję sprawdzenie Solr.
Zapomniałem wspomnieć, że ultrasphinx działa z bazami postgresql >= 8.2
Posted in Ruby on Rails | Tags ferret, mongrel, ruby, rubyonrails, sphinx, thin | no comments | no trackbacks
Posted by Piotr Sarnacki
Sat, 05 Jan 2008 09:40:00 GMT
Wpadłem jakiś czas temu na bardzo fajną aplikację wspomagającą deployment Ruby on Rails. god.rb monitoruje procesy i w razie potrzeby (out of memory, zbyt dużo pożartego cpu, pad serwera) restartuje je. Config, jak i sam god oczywiście, jest napisany w Ruby’im, więc można sobie nieco ułatwić pracę (wspominałem kiedyś, że nigdy nie miałem dość samozaparcia, żeby się nauczyć pisać skrypty powłoki pod linuxem?).
Aby go zainstalować należy jako root (pod ubuntu sudo) wykonać polecenie:
gem install god
Na stronie goda są podane różne configi, ale nie zakładają one scenariusza: “sporo aplikacji, taki sam config, jak to zrobić automatycznie?”. Pomyślałem sobie, że napiszę prosty config, który będzie pobierał listę aplikacji Rails z pliku i dodawał mongrele do monitorowania. Gdy na serwerze siedzi więcej aplikacji głupio byłoby kopiować config dla każdej z nich i ręcznie ustawiać numery portów i katalogi.
Pełny config z komentarzami wyjaśniającymi o co kaman (
wersja tekstowa):
# run with: god -c /path/to/config.god
require 'yaml'
# otwieramy plik ze spisem aplikacji i robimy z nich tablicę
rails_apps = File.open("/etc/god/applications", "r").readlines.collect { |app| app.strip }
for rails_app in rails_apps
# załadujmy ustawienia danej aplikacji
# korzystać będziemy z konfiguracji dla clusterów
config = YAML::load(File.open(File.join(rails_app, "config/mongrel_cluster.yml")))
config["servers"] ||= 1 # domyślnie startujemy jeden serwer
# teraz stawiamy mongrela dla każdego portu
((config["port"].to_i)..(config["port"].to_i+config["servers"].to_i-1)).each do |port|
God.watch do |w|
pid_file = File.join(rails_app, config["pid_file"].gsub(/\.pid$/, ".#{port}.pid"))
w.name = "#{rails_app[/[^\/]*$/]}-mongrel-#{port}"
# dodajemy mongrele danej aplikacji do grupy o nazwie takiej samej jak nazwa katalogu
w.group = "#{rails_app[/[^\/]*$/]}"
w.interval = 30.seconds # default
w.start = "mongrel_rails start -c #{rails_app} -p #{port} \
-P #{pid_file} -e #{config['environment']} -d"
w.stop = "mongrel_rails stop -P #{pid_file}"
w.restart = "mongrel_rails restart -P #{pid_file}"
w.start_grace = 10.seconds
w.restart_grace = 10.seconds
w.pid_file = File.join(pid_file)
w.behavior(:clean_pid_file)
w.start_if do |start|
start.condition(:process_running) do |c|
c.interval = 5.seconds
c.running = false
end
end
w.restart_if do |restart|
restart.condition(:memory_usage) do |c|
c.above = 150.megabytes
c.times = [3, 5] # 3 out of 5 intervals
end
restart.condition(:cpu_usage) do |c|
c.above = 50.percent
c.times = 5
end
end
# lifecycle
w.lifecycle do |on|
on.condition(:flapping) do |c|
c.to_state = [:start, :restart]
c.times = 5
c.within = 5.minute
c.transition = :unmonitored
c.retry_in = 10.minutes
c.retry_times = 5
c.retry_within = 2.hours
end
end
end
end
end
puts "kuniec"
Aplikacje w pliku, który jest odczytywany na początku to ścieżki wpisane w kolejnych liniach. gdy wystartujemy goda poleceniem `god -c /sciezka/do/configu` uruchomią się mongrele dla wszystkich aplikacji. Ważne: powyższy config korzysta z konfiguracji stworzonych dla clusterów. Aby ją stworzyć należy wykonać w katalogu aplikacji:
mongrel_rails cluster::configure -e production -p 8000 -N 2
Powyższa komenda wygeneruje plik `mongrel_cluster.yml` w katalogu config aplikacji Ruby on Rails. Zostanie ustawione środowisko production, port 8000 i 2 serwery (na portach 8000 i 8001).
Żeby szczęście było pełne i niczym niezmącone można jeszcze pokusić się o napisanie skryptu startowego init.d. Oczywiście w ruby’im. A jakie to proste pokazywał kiedyś Jarek Zebiełło.
W razie wątpliwości pytajcie o szczegóły w komentarzach.
Posted in Ruby on Rails | Tags deployment, god, monitoring, ruby, rubyonrails | no comments | 1 trackback
Posted by Piotr Sarnacki
Sat, 01 Sep 2007 12:40:00 GMT
Zdarza mi się ostatnimi czasy przeczytać komentarz, że książki programistyczne są już niepotrzebne, bo i tak wszystko jest w necie. A nawet, cytuję: “w książkach to jest naćkane, jak chcesz konkretów to nie kupuj książek”.
Całkiem głupie to ;-)
Dla mnie książki programistyczne są niezastąpione. I najchętniej kupiłbym sobie połowę pozycji dostępnych na rynku. Po czym prawdopodobnie 1/3 bym nie przeczytał, ale to już inna kwestia. Ogólnie rzecz biorąc większości rzeczy zawartych w książkach można pewnie nauczyć się samemu, z lekką pomocą wujka Googla. Ale o ile przyjemniej przeczytać jak to robią profesjonaliści (o ile nikt nie da się nabrać niektórym profesjonalistom inaczej)
Po takim wstępie nie mogę się nie pochwalić, że jakiś czas temu kupiłem wspólnie z ludźmi od nowego projektu (na razie nie zapeszam. za jakiś czas pewnie coś napiszę) parę bardzo fajnych książek.
- Don’t make me think – genialna książka o usability. Bardzo krótka i treściwa. Autor starał się napisać książkę, którą będzie się dało przeczytać w parę godzin. I muszę przyznać, że bardzo dobrze mu to wyszło. Idealna dla ludzi, którzy nie są specami w tej kwestii, a chcą poprawić użyteczność swoich stron.
- Agile development with Rails – Pozycja obowiązkowa dla uczących się RoR. Co prawda teraz niewiele mi się przyda, ale czułbym się źle jakby jej nie kupił. Korzystałem dość sporo z ebooka z amule’a. No i przyda się bliźniemu, który właśnie zaczyna się uczyć RoR.
- Ruby for Rails – jeszcze nie przeczytałem, ale po przekartkowaniu zapowiada się bardzo ciekawie. Napiszę coś więcej jak wreszcie się za nią wezmę ;-)
- The Ruby Way – bardzo dobra książka. Jestem właśnie w trakcie czytania. Podaje bardzo dużo rozwiązań często spotykanych problemów zgddnie z “ruby way”. Ponad 600 stron esencji programowania ;-)
- JavaScript: The Definitive Guide – niektórzy narzekają, że książka bardziej zbliżona jest do dokumentacji niż do książki “do nauki”. Dla mnie to plus. Javascripta znam całkiem dobrze. Pewnie dzięki temu, że w poprzednim stuleciu, kiedy o bibliotekach takich jak jQuery nikt nie słyszał, bawiłem się w pisanie różnego rodzaju latających warstw, wysuwanych menu i innych arkanoidów (nawet fajnie się grało ;-). W tym momencie potrzebuję właśnie takiej dokumentacji, do której mogę spojrzeć podczas pisania i zobaczyć jakie metody udostępnia dana klasa, albo jak powinien się zachowywać jakiś kod. Jeżeli ktoś chce pisać coś więcej niż efekty z użyciem jQuery (czy innego script.aculo.us), to naprawdę zachęcam do kupna.
Podejrzewam, że gdyby początkujący programiści więcej inwestowali w wiedzę książkową to byłoby dużo mniej partactwa. Szczególnie, że książki związane z Rubim i RoR oprócz nauki programowania próbują od początku wpajać dobre praktyki programistyczne. I koniecznie trzeba przeczytać Pragmatycznego programistę :)
Posted in Programowanie | Tags javascript, książki, programowanie, rails, ruby, rubyonrails | no comments
Posted by Piotr Sarnacki
Fri, 22 Dec 2006 13:30:00 GMT
Pastwiłem się ostatnio nad pewną aplikacją wykonując proces zwany, jakże trafnie, optymalizacją. Prawdopodobnie możnaby jeszcze dużo z niej wycisnąć, ale nie lubię się za bardzo przemęczać, a nie ma już teraz części, która jakoś znacznie wyróżniałaby się czasem wykonywania.
Ruby on Rails daje nam bardzo proste mechanizmy obsługujące cachowanie treści (page, action i fragment caching). W kilku słowach:
- page caching - zapisywanie całych stron - podczas pobierania serwer nawet nie tyka railsów, serwowany jest statyczny plik
- action caching - podobne do page caching, z tym że framework jest wywoływany - przydatne jeżeli chcemy mieć dostęp do filtrów (autentykacja, autoryzacja, pewnie jakieśinne acje ;-) )
- fragment caching - zapisywane są fragmenty stron - najmniejszy skok szybkości, ale może się przydać przy dynamicznych stronach, możemy wtedy skeszować tylko te fragmenty, które nie podlegają częstym zmianom
Postanowiłem zacząć od fragment cachingu. Elementem, który generował się najdłużej, nadal było menu. Rekurancja i bardzo dużo iteracji nie jest tym, co tygryski lubią najbardziej ;-)
Menu tworzy metodarender_menu(). Zcachujmy ją więc:
<% cache do %>
<%= render_menu(3, nil, nil) %>
<% end %>
W katalogu tmp/cache (w naszej apliakcji) stworzył się plik: localhost.3000/zycie.cache (zycie to jedna z sekcji). Fajnie. Powstaje tylko jeden mały problem - fragment cachowany jest w pliku z layoutem, więc nie jest przypisany do żadnej akcji. Co jeżeli będziemy chcieli zcachować inny fragment? Z pomocą przychodzi możliwość określenia miejsca przechowywania:
<% cache(:controller => 'abstract', :action => 'menu', :part => 'zycie') do %>
<%= render_menu(3, nil, nil) %>
<% end %>
Kontroler i akcja określane w tym miejscu nie są w żaden sposób powiązane z kontrolerami w naszej aplikacji. Dzięki temu możemy dowolnie określić miejsce zapisania fragmentu (co później się jeszcze przyda). Po odświeżeniu strony pojawił się plik:
tmp/cache/localhost.3006/zycie/abstract/menu.part=zycie.cache
Przed kontrolerem widać jeszcze swoisty namespace określany w routes.rb na podstawie argumentu section w adresie (w tym wypadku jest to sekcja “zycie”). Jeżeli nie ma w aplikacji podobnych myków, w ścieżce nie będzie w ogóle części zycie.
A teraz czas ładowania się strony:
- z generowaniem się menu:
Completed in 1.52972 (0 reqs/sec) | Rendering: 1.50603 (98%) | DB: 0.01315 (0%) | 200 OK (czas jest dość długi, bo moja machina nie jest najwyższej klasy, a odpalonych jest parę aplikacji)
- z menu odczytanym z fragmentu:
Completed in 0.04565 (21 reqs/sec) | Rendering: 0.01936 (42%) | DB: 0.01816 (39%) | 200 OK
Skok szybkości jest ogromny. Jest to też największy dział w serwisie. Dla innej “sekcji” różnica jest mniejsza, ale cały czas bardzo duża:
- z generowanym menu:
Completed in 0.27923 (3 reqs/sec) | Rendering: 0.21727 (77%) | DB: 0.03056 (10%) | 200 OK
- z pamięci cache:
Completed in 0.03424 (29 reqs/sec) | Rendering: 0.01521 (44%) | DB: 0.01059 (30%) | 200 OK
Bingo! :)
Teraz wystarczy tylko zapewnić czyszczenie pamięci podczas modyfikacji. Przeznaczona jest do tego funkcja expire_fragment. W kontrolerze modyfikującym menu dodałem prywatną metodę expire_menu, w której czyszczę fragment:
def expire_menu
expire_fragment(%r{/#{section_name(params[:section])}/abstract/menu/*})
end
Skorzystałem tutaj z możliwości użycia wyrażeń regularnych. Przy określeniu położenia pliku (expire_fragment(:controller => 'abstract', :action => 'menu', :part => 'zycie')) coś się gryzło z owymi sekcjami, o których pisałem na początku - fragment nie chciał się usuwać. Prawdopodobnie w większości przypadków nie trzeba uciekać do takich rozwiązań. Wyrażenia regularne mają jeszcze tą zaletę, że możemy w razie potrzeby wyczyścić wiele fragmentów na raz.
Teraz wystarczy wywołać metodę dla akcji, które modyfikują menu:
after_filter :expire_menu, :only => ['create', 'update', 'destroy', 'move_up', 'move_down']
W tym wypadku sprawa była ułatwiona, gdyż treść, którą chciałem skeszować była generowana w helperze. Co jeżeli chcemy zapisać na przykład listę artykułów? Mamy zapewne jakąś pętlę for article in @articles, którą opatulimy metodą cache. I ok. Ale w kontrolerze nadal pobiera się lista artykułów, która przy odczytywaniu skeszowanej treści nie będzie do niczego potrzebna. W takich wypadkach powinno się używać metody read_fragment. Pobiera ona te same argumenty co expire_fragment i zwraca true jeżeli fragment istnieje. Czyli wszystko można załatwić jest krótkim kodem:
@articles = Article.find(:all) unless read_fragment(:controller => 'articles', :action => 'list')
Jest coraz lepiej, ale moja sadystyczna natura nie pozwala mi przedwcześnie zakończyć znęcania sie nad aplikacją ;-)
Dodałem jeszcze page cahing - daje on największy wzrost prędkości. Dlaczego nie zrobiłem tylko page cachingu olewając fragment caching? Przecież na skeszowanej stronie i tak menu nie będzie się generowało. Odpowiedź jest prosta. Moje menu zapisze się teraz do pliku przy pierwszym wywołaniu dowolnej strony z layoutem default. A całe strony będą się cachowały tylko z danymi argumentami - czyli dla każdego z 250 artykułów i kilkudziesięciu newsów oddzielnie. Ale o tym już w następnej notce :)
Update: Uważajcie na niektóre teksty w sieci ;) Nie twierdzę, że moje rozwiązanie jest jedyne i słuszne, ale wpadłem dzisiaj przypdakiem na posta, w którym autor twierdzi, że trzeba wrzucić do cron’a skrypt, który będzie przeładowywał fragment cache co kilka minut i raczej nie keszować akcji z dużą ilością pobieranych danych. Czyli po prostu nie wie do czego służą metody read_fragment i expire_fragment. I jeszcze taki śmieszny cytacik z końca owego posta :)
Conclusion
Rails has a bunch of built-in page caching mechanisms, but they aren’t THAT useful out of the box. You need to tweak and play around to get what you need, and most of the time you will NOT use the simple solutions.
For our large scale sites we still love the event-driven memcached approaches.
Zastanawiam się co to za large scale sites :)
Posted in Ruby on Rails | Tags caching, on, optymalizacja, rails, ruby | no comments