Category: Software

Ruby, Mongoid and memory leaks – Identity map problem

Where is my memory?

Recently, when browsing large dataset from MongoDB using Padrino and Thin, Ruby started to have memory leaks. After each request it grew approximately 2-5 MB.

I've started debugging by putting following line in my action, to see memory usage increase per request:

puts 'RAM USAGE: ' + `pmap #{Process.pid} | tail -1`[10,40].strip

Results:

RAM USAGE: 796156K
RAM USAGE: 798284K
RAM USAGE: 798824K
RAM USAGE: 799088K
RAM USAGE: 799900K
RAM USAGE: 799900K
RAM USAGE: 812044K
RAM USAGE: 816152K
RAM USAGE: 816292K
RAM USAGE: 816836K
RAM USAGE: 818956K
RAM USAGE: 819088K
RAM USAGE: 830572K
RAM USAGE: 884604K
RAM USAGE: 887648K
RAM USAGE: 892800K
RAM USAGE: 897160K
RAM USAGE: 906960K

As you can see it grows rapidly. When looking at htop things get even worse:

88,4 MB
93,9 MB
97,5 MB
99,2 MB
109,4 MB
113,4 MB
122,7 MB
127,1 MB
...
1,2 GB!

It was definitely too much! Memory consumption reached it's limits and everything slowed down.

I knew that it had something to do with this line:

@analyses = Analysis.finished.page(params[:page] ||= 1).per(10)

Kaminari?

At the beginning I've suspected Kaminari and its pagination engine, however it is just a more complex layer covering some scopes. To check this I've removed Kaminari:

@analyses = Analysis.finished.skip(((params[:page] ||= 1)-1)*10).limit(10)

Unfortunately nothing good happened and memory consumption kept growing with same speed. Interesting is that, when I've turned off all MongoDB indexes:

db.collection1.dropIndexes()
db.collection2.dropIndexes()
...
db.collectionN.dropIndexes()

memory usage grew much slower than before. So WTF?

Identity map!

Finally I've discovered damn source of my problem. It was identity map in Mongoid. What is identity map?

The identity map in Mongoid is a current aid to assist with excessive database queries in relations, and is necessary for eager loading to work. (...) When a document is now loaded from the database, is is automatically added to the identity map by it's class and id. Subsequent request for that document by it's id will not hit the database, but rather pull the document back from the identity map itself. It's primary function in this capacity is to aid in cutting down queries for belongs_to relations when iterating over the parents.

Seems like identity map was never cleared (or it has a memory leak bug in it). Adding:

use Rack::Mongoid::Middleware::IdentityMap

didn't help at all so I've just turned identity map off:

mongo.identity_map_enabled = false

and everything went back to normal. Interesting thing is that identity map in ActiveRecord is by default turned off in Rails because it's known to cause similar problems.

Poprawne linki z Facebooka dla JWPlayera – wygasające linki – część I

Wstęp

Tutorial składa się z dwóch cześci:

  1. FacebookBot z wykorzystaniem Mechanize
  2. Cachowanie odpytań

Kod (nowszy i lepszy ;) ) dostępny na githubie.

Część z Was na pewno osadza swoje filmiki z konta Facebook także na swoich stronach www. Niektórzy robią to z pomocą odtwarzacza dostarczonego przez Facebooka, inni korzystają np. z JWPlayera. Niestety ostatnimi czasy, Facebook zauważył, że staje się bardzo fajną platformą hostingową dla filmów wszelakich (i to w jakości HD!). Zjadało im to (i zjada ;) ) gigantyczne ilości zasobów - przede wszystkim łącza. Jeśli osadzasz pliki wideo przez ich odtwarzacz - to nie ma problemu - FB ma z tego korzyści (mają swoje logo na filmiku, itd). Korzystając jednak z JWPlayera - nie uda ci się ta sztuczka. Linki bezpośrednie do plików MP4 są zmieniane co 24-36 godzin, w skutek czego umieszczanie ich "w" odtwarzaczu nie ma sensu. Na szczęście da się to bardzo łatwo rozwiązać, jak zawsze w... Rubym :)

Mechanize

Napiszemy prostego bota - który wchodzi na Facebooka, loguje się i sprawdza URL filmu. Warto dodać cacheowanie, tak aby nie odpytywać FB za każdym razem o to samo. Rozbudowę tego narzędzia pozostawię jednak Wam. Wracając do sedna sprawy. Aby napisać tego bota, skorzystamy z Mechanize. Mechanize jest biblioteką stworzoną do łatwej integracji ze stronami wszelakimi. Umożliwia przesyłanie formularzy, odwiedzanie stron, zapewnia obsługę cookies, ssl-a, itp, itd. To głównie dzięki niemu, będziemy mogli odświeżać linki do plików MP4 z FB.

Zanim jednak to zrobimy, musimy dodać do Mechanize pewną małą poprawkę. Domyślnie nie pozwala wyszukiwać formularzy po ID a nam taka metoda się przyda. Tak więc:

class Mechanize::Page
  def form_id(formId)
    formContents = (self/:form).find { |elem| elem['id'] == formId }
    if formContents then return Mechanize::Form.new(formContents) end
  end
end

Tyle :) A teraz pora na FacebookBota.

FacebookBot

Nasz bot będzie miał łącznie 4 metody:

  1. Initialize - inicjalizacja bota
  2. Login - logowanie do Facebooka
  3. Video_Url - pobranie URLa pliku wideo
  4. (priv) get_url - wyodrębnienie samego urla pliku ze strony z linkiem

Szkielet klasy będzie więc wyglądał tak:

require 'rubygems'
require 'mechanize'
require 'uri'
require 'cgi'
require 'time'

class FacebookBot
  # Strona główna Facebooka
  FB_URL = "http://www.facebook.com/"
  # Nasza przeglądarka i system :)
  USER_AGENT = 'Linux Firefox'

  def initialize(email, pass)
  end

  def login
  end

  def video_url(video_id)
  end

  private

  def get_url(url)
  end
end

Inicjalizacja naszego bota składa się z zapamiętania e-maila i hasła, utworzenia obiektu Mechanize do eksploracji FB oraz próby zalogowania na nasze konto:

  def initialize(email, pass)
    @email, @pass = email, pass

    @agent = Mechanize.new
    @agent.user_agent_alias = USER_AGENT

    @cookies = File.dirname(__FILE__) + "/../cookies-" + @email + ".yml"
    if (File.file?(@cookies))
      @agent.cookie_jar.load(@cookies)
    end

    self.login
  end

Wartym omówienia jest ten fragment:

    @cookies = File.dirname(__FILE__) + "/../cookies-" + @email + ".yml"
    if (File.file?(@cookies))
      @agent.cookie_jar.load(@cookies)
    end

Zmienna @cookies trzyma ciastka sesji od Facebooka na dysku. Dlaczego jest o katalog wyżej (../) niż sam plik? Ponieważ plik z botem wrzucimy do 'lib/' a plik z ciastkiem będziemy chcieli mieć w katalogu głównym naszej małej aplikacji. Tak więc, ustalamy sobie ścieżkę, sprawdzamy czy plik już istnieje i jeśli tak jest, to ładujemy jego zawartość do kontenera na ciacho - tak żeby można się było przedstawiać nim w Facebooku. Następnie podejmujemy próbę logowania.

Pora na logowanie:

  def login
    page = @agent.get(FB_URL)

    if (loginf = page.form_id("login_form"))
      loginf.set_fields(:email => @email, :pass => @pass)
      page = @agent.submit(loginf, loginf.buttons.first)
    end

    @agent.cookie_jar.save_as(@cookies)

    body = page.root.to_html
    @uid = %r{\\"user\\":(\d+),\\"hide\\"}.match(body)[1]
    @post_form_id = %r{<input type="hidden" id="post_form_id" name="post_form_id" value="([^"]+)}.match(body)[1]
  end

Logowanie przebiega w następujący sposób:

  1. Próbujemy przejść na stronę główną FB.
  2. Jeśli się to udało - tzn że nasze ciastka były i były prawidłowe
  3. Jeśli nie, to wypełniamy formularz logowania i klikamy na "Zaloguj"
  4. Zapamiętujemy uaktualnione ciastka
  5. Przechodzimy na stronę główną i zapamiętujemy id usera (UID)
  6. Zapamiętujemy klucz do przesyłania żądań typu POST

Prawda że proste? :)

Video_url:

def video_url(id)
    begin
      pa = @agent.get("#{FB_URL}video/video.php?v=#{id}")
      pr = pa.body
      get_url(pr)
    rescue
      "Niepoprawne ID pliku wideo lub wideo nie jest publiczne."
    end
end

Pobieramy stronę pliku wideo, wyciągamy link do MP4 i zwracamy. Jeśli coś pójdzie nie tak, to zwracamy info że wideo jest nieprawidłowe lub nie jest publiczne.

Samo parsowanie przebiega w sposób trochę "brutalny" - nie chciało mi się bawić i myśleć to wyciągnąłem tak niezbyt elegancko:

private

def get_url(url)
	url = url.scan(/addVariable\(\"highqual_src\",\s\"http.+\"\)/ix).first
	url = url.split(')').first.gsub('\u00253A', ':')
	url = url.gsub('\u00252F', '/')
	url = url.gsub('\u00253F', '?')
	url = url.gsub('\u00253D', '=')
	url = url.gsub('\u002526', '&')
	url = "http://#{url.split('http://')[1]}".split('"').first
	CGI.unescapeHTML(url)
end

Grunt że działa.
Oto jak działa całość:

  fb = FacebookBot.new('moj@mail.pl', 'moje_tajne_haslo')
  p  fb.video_url(ID_PLIKU_VIDEO)

Warto dodać do tego system cacheowanie, tak aby nie odpytywać FB za każdym razem o to samo!

Źródełko :)

W związku z szerokim zainteresowaniem, udostępniam prosty interfejs: www.fbc.mensfeld.pl. Linki do odcinków buduje się tak:

www.fbc.mensfeld.pl/ID-ODCINKA.mp4

Tutorial składa się z dwóch cześci:

  1. FacebookBot z wykorzystaniem Mechanize
  2. Cachowanie odpytań

Kod (nowszy i lepszy ;) ) dostępny na githubie.

Copyright © 2026 Closer to Code

Theme by Anders NorenUp ↑