Table of Contents
Wstęp
Tutorial składa się z dwóch cześci:
Kod (nowszy i lepszy ;) ) dostępny na githubie.
Ze względu na dużą popularność wpisu dot. "naprawy" linków z Facebooka (tutaj), postanowiłem dołączyć wam drugą część poradnika. Ta część dotyczyć będzie cachowania wyników - cobyśmy za często nie odpytywali Facebooka o linki. Dlaczego lepiej jest cachować wyniki, niż każdorazowo odpytywać FB? Z dwóch powodów:
- czas - przede wszystkim - czas potrzebny na odpytanie serwera Facebooka o potrzebne dane, a następnie zwrócenie tego klientowi, zajmuje zbyt wiele czasu. Dużo szybciej jest wyciągnąć i zwrócić pojedynczy rekord z bazy,
- logi - Facebook na pewno nie będzie zadowolony (i nie przepuści po pewnym czasie) z tego, że codziennie nasze konto odpytuje np. 30 - 40 tys razy o strony z filmami. Uznają to za dziwne zachowanie i po prostu zbanują konto lub co gorsza IP.
Czas "życia" adresu z Facebooka to od 24 do 36 godzin. Aby ciągłość usług była stabilna, załóżmy że cache będzie działał przez 30 minut. Da nam to odpytywanie Facebooka - maksymalnie co 30 minut na filmik (a nie przy każdym żądaniu). Dodatkowo, kiedy nasz cache się przedawni, nie odpytamy od razu o nowy URL - najpierw sprawdzimy czy stary już umarł, wysyłając zapytanie o nagłówek pod URL filmu. Jeśli dostaniemy odpowiedź 200 - tzn, że link wciąż jest aktywny i nie musimy go zmieniać.
Takie podejście zapewni nam uptime na poziomie (w najgorszym przypadku): 100%-(30/1440)*100%= 97.9% czasu. A to i tak zakładając wariant, że url z FB zmieni się dokładnie po naszym odpytaniu, przez co cache będzie serwował przez całe 30 minut zły URL. Myślę, że spokojnie można przyjąć uptime na poziomie 98.5%-99%.
Ustawienia
Aby było nam łatwiej, wszystkie ustawienia będziemy przechowywać w jednym miejscu. Stwórzmy sobie plik lib/settings.rb, w którym w stałej SETTINGS będziemy przechowywać co nam trzeba:
SETTINGS = { :db_user => 'user_bazy', :db_adapter => "mysql", :db_host => "mysql5", :db_database => "baza", :db_pass => 'haslo', :db_socket => '/tmp/mysql.sock', :fb_login => 'mail@do.facebooka.pl', :fb_pass => 'haslo-do-fb', }
Baza danych
Do przechowywania danych wykorzystamy bazę MySQLa. Stworzymy sobie plik db_connector.rb, który tak jak poprzedni (i wszystkie następne oprócz main.rb) będzie przechowywany w katalogu lib/. W pliku tym będziemy trzymać kod odpowiedzialny za łączenie się z bazą danych oraz naszą "pseudo" migrację - generującą tabelę z odpowiednimi kolumnami. Warto zwrócić uwagę na to, że mimo wykorzystania ActiveRecord - ORMa z Railsów - nie wykorzystujemy całego frameworka. Sam ORM nam w zupełności wystarczy.
ActiveRecord::Base.establish_connection( :adapter => SETTINGS[:db_adapter], :host => SETTINGS[:db_host], :database => SETTINGS[:db_database], :username => SETTINGS[:db_user], :password => SETTINGS[:db_pass], :socket => SETTINGS[:db_socket] ) unless Video.table_exists? ActiveRecord::Base.connection.create_table(:videos) do |t| t.column :video_id, :string t.column :name, :string t.column :url, :string, :default => nil t.column :views, :integer, :default => 1 t.column :cached_at, :datetime end end
Warto zwrócić uwagę na ten warunek:
unless Video.table_exists?
dzięki któremu migracja zostanie wykonana tylko jeśli tabela z danymi nie istnieje.
Model filmu wideo
Mamy już gdzie trzymać nasze dane. Pora stworzyć model pojedyńczego filmiku z FB. Model nasz zawierać będzie cztery metody:
- url=(new_url) - przypisanie nowego adresu filmiku z Facebooka,
- working? - metoda zwracająca true/false zależnie od tego czy filmik działa czy też nie,
- url_working? - metoda sprawdzająca nagłówek odpowiedzi z żądania pod adresem URL filmu,
- (private) set_cached_at_time - ustawiająca podczas tworzenia instancji czas cachowania na teraz.
Zacznijmy od szkieletu modelu:
require 'net/http' require 'uri' class Video < ActiveRecord::Base # 30 minut CACHE_TIME = 60*30 before_create :set_cached_at_time # Gwarancja, ze jeden filmik bedzie mial jeden wpis w bazie validates_uniqueness_of :video_id validate :working? end
Metody
1. url=
def url=(new_url) self.errors.clear self.cached_at = Time.now super new_url end
Część z was może zapytać, dlaczego czyszczę błędy (self.errors.clear)? Czyszczę je, ponieważ przypisanie nowego URLa następuje wtedy i tylko wtedy, gdy stary był niepoprawny i nie przechodził walidacji. Nowy z założenia przechodzi, tak więc błąd starego URLa musi zostać "zapomniany".
2. working?
def working? v = false if self.cached_at >= Time.now - CACHE_TIME && self.url.length > 10 v = true else v = url_working? self.cached_at = Time.now end self.errors.add(:cached_at, 'Cache ulegl przedawnieniu') unless v v end
W tej metodzie sprawdzamy czy URL jest poprawny. Jeśli jest "młody" (czyli nowszy niż 30 minut) i jego długość jest większa niż 10 (to tak dla pewności ;) ), tzn. że jest prawdziwy (w 99% procentach przypadków - patrz wstęp). W przeciwnym razie - sprawdzamy nagłówek odpowiedzi z żądania wysłanego pod link, i jeśli jest poprawny - to ok, jeśli nie - to wrzucamy błąd do tablicy błędów naszej instancji modelu. Uważniejsze osoby mogą zapytać, dlaczego ustawiamy cache na nowy - mimo, że URL się nie zmienił. Ustawiamy tak, ponieważ zmiana URLa nastąpi na pewno jeśli ten link był niepoprawny. Nastąpi ona jednak w pliku main.rb.
3. url_working?
def url_working? begin host = URI.parse(self.url).host http = Net::HTTP.new(host) headers = http.head(self.url) rescue return true end if headers.code == "200" true else false end end
Zasadniczo sposób działania tej metody opisałem już wyżej, ale tak dla pewności:
- Wysyłamy żądanie nagłówka pod adres naszego filmiku
- Jeśli coś poszło nie tak - zakładamy, że URL nie jest poprawny
- Jeśli dostaliśmy nagłówek to go sprawdzamy
- Jeśli 200 - to znaczy że URL jest poprawny (true)
- Jeśli cokolwiek innego (404, 401, 403, 500, itd) - tzn. że już "umarł" (false)
4. set_cached_at_time
def set_cached_at_time self.cached_at = Time.now end
Tutaj nie ma co tłumaczyć.
To by było na tyle jeśli chodzi o nasz model Video. Pozostaje nam jeszcze tylko plik main.rb.
Połączenie wszystkiego w całość
Na początek zaincludujmy pliki z katalogu lib/ oraz inne niezbędne nam biblioteki:
# coding: utf-8 def require_local(file) require File.join(File.dirname(__FILE__), "/lib/#{file}") end require 'rubygems' require_local 'settings' require_local 'facebook_bot' require 'active_record' require_local 'video' require_local 'db_connector'
Zwróćcie uwagę, że dołączamy naszego FacebookBota z pierwszej części tego poradnika. Jednak jest to wersja lekko zmodyfikowana w stos. do wersji z części pierwszej. Różnica jest zasadniczo taka, że ta "nowa" wersja bota, oprócz URLa filmu pobiera także jego nazwę, dlatego radzę pobrać sobie paczkę ze źródełkami lub plik z botem z mojego githuba.
Dalej pobieramy z parametrów ID filmu oraz opcję "force" której użycie wymusi regenerację linku wprost z Facebooka, niezależnie od wartości cache'a:
video_id = ARGV[0] force = false || ARGV[1] == 'true'
Dalej cała główna logika tej aplikacji:
video = Video.find_by_video_id(video_id) # Jesli taki plik istnieje i jest poprawny i żądanie nie jest wymuszone # to zwróć link if video && video.valid? && !force puts 'Cache' puts video.url else # Jesli link był niepoprawny lub jest to nowy filmik to zainicjuj FBbota # i pobierz z FB URL oraz nazwę filmiku fb = FacebookBot.new(SETTINGS[:fb_login], SETTINGS[:fb_pass]) url = fb.video_url(video_id) name = fb.video_name(video_id) # Jesli video istniało tylko URL wygasł to przypisz nowy URL i nazwę # (nazwe na wypadek gdyby ulegla zmianie) if video video.url = url video.name = name else # Jesli filmiku nie bylo to go utworz video = Video.new( :video_id => video_id, :url => url, :name => name ) end puts 'Nowe wywolanie' puts video.url end # Podbij ilosc wyswietlen video.views+=1 # I Zapisz zmiany w modelu video.save
Tyle. Od teraz mamy w pełni działający program umożliwiający "regenerację" linków do plików video z Facebooka. Dzięki cachowaniu w bazie danych ilość odpytań dla popularnych filmików znacznie spada.
Tutorial składa się z dwóch cześci:
Kod (nowszy i lepszy ;) ) dostępny na githubie.
Leave a Reply