Tag: Ruby

Błąd “Errno::EMFILE: Too many open files” w testach jednostkowych

Dzisiaj pisząc kolejną porcję testów do Senpuu v5, po odpaleniu ich pod RCovem i Rubym 1.8.7 napotkałem na taki oto błąd:

Errno::EMFILE: Too many open files

Błąd ten związany jest z ilością otwartych jednocześnie plików (co ciekawe nie wystąpił podczas odpalania testów pod 1.9.2). Okazuje się, że tego typu wywołania:

file = File.new(File.join(Rails.root, "sciezka/do/pliku"), 'rb')

nie są nigdy zamykane, więc otwierając przykładowo 100 plików, żaden z nich nigdy nie zostanie zamknięty (przy zamykaniu aplikacji zostana). Z czasem ilość otwartych plików narasta i po pewnym czasie następuje zgłoszenie wyżej wspomnianego wyjątku. Rozwiązanie jest nader proste: zamykajmy otwierane pliki :)

W przypadku aplikacji jest to dość proste (robimy coś na pliku, kończymy i zamykamy). Jak jednak zapanować nad otwartymi plikami w testach? Przyznam szczerze, że jestem zbyt dużym leniem aby pamiętać o zamykaniu każdego pliku otworzonego w każdym teście. Z tego względu warto zbudować sobie prosty garbage collector który zajmie się tym za nas. Jak to zrobić pokaże na przykładzie podpięcia takiego kodu pod ActiveSupport::TestCase. Nic nie stoi na przeszkodzie aby tę metodę stosować także dla innych frameworków jak np. RSpec.

W metodze setup deklarujemy zmienną instancyjną, która przechowywać będzie nasze otwarte pliki:

  def setup
    super
    # Jakis inny kod ...
    @opened_files = []
  end

W metodzie teardown zamykamy wszystko:

  def teardown
    super
    # Jakis inny kod
    @opened_files.each { |f| f.close }
  end

I ostatnia z metod - metoda otwierająca pliki i zapamiętująca je w naszej zmiennej:

  def open_file(file = nil)
    return nil if filename.nil?
    file = File.new(File.join(Rails.root, file), 'rb')
    @opened_files << file
    file
  end

Od teraz, korzystając z metody open_file nie musimy się martwić otwartymi plikami. Zostaną automatycznie zamknięte podczas wywołania metody teardown.

Ruby, DRMAA, DRMAA4Ruby, Sun Grid + Monte Carlo integration

Wstęp

Przyjęło się, że wykonywanie obliczeń z pomocą gridów jest domeną języków C, C++ i Javy. To samo tyczy się zarządzania gridem. Okazuje się jednak, że z powodzenie można do tego wykorzystać także Rubiego. Jako przykład wykorzystamy całkowanie metodą Monte Carlo, zaś zarządzać gridem będziemy za pośrednictwem DRMAA'y oraz biblioteki DRMAA4Ruby.

DRMAA4Ruby

DRMAA4Ruby jest to interfejs do języka Ruby - który jest bindingiem do DRMAA'y dla C. Dzięki niemu możemy w bardzo prosty sposób rozdzielać zadania na mniejsze fragmenty i kolejkować je na gridzie.

DRMAA4Ruby ma jednak także swoje wady - nowa wersja tej biblioteki nie jest kompatybilna z wciąż popularną wersją Rubiego - 1.8.7. Występują także problemy z odnalezieniem i użyciem modułu libdrmaa.so. Na szczęście - oba te problemy nie są trudne do rozwiązania.

DRMAA4Ruby znajdziemy tutaj. W katalogu lib/ znajdują się pliki:

  • drmaa.rb
  • original-drmaa.rb

Jeśli korzystasz z Rubiego w wersji 1.8.7 (lub starszej), zamiast ładować drmaa.rb musisz ładować original-drmaa.rb.

Drugim z problemów jest błąd podczas ładowania pliku libdrmaa.so. Ruby nie może go namierzyć. Aby to zmienić, wystarczy podać dokładną ścieżkę do tegoż pliku (można to zrobić używając polecenia find w powłoce). Podmiany ścieżki na bezwzględną dokonujemy w linijce 191 (dla original-drmaa.rb):


@libdrmaa = DL.dlopen('/sciezka/bezwzgledna/libdrmaa.so')

Po tym zabiegu biblioteka powinna ładować się (i działać bez problemu).

Monte Carlo

Nie będę tutaj opisywał jak dokładnie działa całkowanie metodą Monte Carlo - zainteresowanych odsyłam do wikipedii. Poniżej przedstawię jednak kod realizujący taką metodę całkowania (oczywiście w Rubym):


xp = ARGV[0].to_f
xk = ARGV[1].to_f
parts = ARGV[2].to_i
part = ENV['SGE_TASK_ID'].to_i
STEPS=10_000

step = (xk-xp)/parts

p = xp+step*(part-1)
k = p+step

xp = p; xk = k

f = lambda { |x| x*x+3 }

dx=(xk-xp).to_f
s = 0

STEPS.times { s += f.call(xp+rand*dx) }
s = dx*s/STEPS

print s

Kod nie jest skomplikowany, a dla nas ważne jest to jak go odpalamy. Przyjmuje on 3 parametry jawne:

  • początek przedziału całkowania
  • koniec przedziału całkowania
  • ilość części na jakie będziemy dzielić nasze całe całkowanie

Co znaczy "ilość części"? Musimy pamiętać, że obliczenia wykonywane będą na gridzie, z tego względu musimy nasze zadanie rozbić na subzadania. Każde subzadanie będzie liczyć "kawałek" całki, po czym całość zsumujemy. Nasz kod ma jeszcze jeden parametr - brany ze zmiennej środowiskowej SGE_TASK_ID. Tym parametrem jest informacja który kawałek ma liczyć dana instancja programu. Po ustawieniu zmiennych nie pozostaje nam nic innego jak policzyć i wyświetlić wynik. To by było na tyle jeśli chodzi o samo zadanie wywoływane na gridzie. Nie ma tu wielkiej filozofii - wystarczy pamiętać o tym, że duże zadanie rozbijamy na mniejsze subtaski które są względem siebie niezależne (mogą funkcjonować autonomicznie).

DRMAA + Ruby + JobTemplate

Pora stworzyć kod który doda do grida nasze zadanie, a następnie zbierze wyniki i wyświetli. Na początek wymagane biblioteki i dwie stałe:


$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
$LOAD_PATH.unshift(File.dirname(__FILE__))

require 'original-drmaa.rb'

STD_PATH = "/home/path/"
SOURCE_PATH = "/home/path/"

  • STD_PATH - gdzie ma domyślnie zapisywać wyniki obliczeń z poszczególnych instancji zadania
  • SOURCE_PATH - gdzie znajduje się nasze zadanie które chcemy uruchomić

Dalej zdefiniujemy sobie JobTemplate - który odwzorowuje nasze zadanie w gridzie:

class Carlos < DRMAA::JobTemplate
  def initialize
	  super
	  self.command = "/usr/bin/ruby"
	  self.arg = ["#{SOURCE_PATH}/monte_carlo.rb", 0, 100, 10]
	  self.stdout = ":#{STD_PATH}/"
	  self.join = true
	  end
end

Warto zauważyć, że jako polecenie nie podajemy pliku z kodem ale ścieżkę do Rubiego. Plik z kodem stanowi pierwszy parametr (da się to obejść całkiem prosto - jak? Pozostawiam to wam).
Następnie podajemy 3 parametry:

  1. o - początek przedziału całki
  2. 100 - koniec przedziału
  3. 10 - ilość subzadań na gridzie

Uruchamiamy nasze zadanie (opis metod znajduje się na stronie DRMAA'y - można skorzystać z opisów do Javy, ponieważ interfejs jest praktycznie ten sam):

session = DRMAA::Session.new

jobs = []
session.run_bulk(Carlos.new, 1, 10).each { |job| jobs << job }

Zanim przejdziemy do zbierania i sumowania wyników, dla przypomnienia wspomnę, że wyniki z subzadań zapisywane są przez DRMAA'ę - tak więc nie musimy się martwić o ich zapis, tak długo, jak długo drukujemy coś na ekranie. DRMAA numeruje pliki w mniej więcej taki sposób:

  1. Nazwa polecenia (u nas ruby)
  2. .o (tak - kropka i literka "o")
  3. ID zadania
  4. . (kropka)
  5. task ID

Np: ruby.o15.61

Wiedząc o tym i majac w tablicy jobs informacje o kolejnych taskach - możemy poiterować pliki i odczytać z nich wyniki. Następnie wystarczy suma i zwrócenie wyniku całościowego:

result = 0
session.wait_each{ |info|
	if ! info.wifexited?
		puts "failed: " + info.job
	else
		file_name = "#{STD_PATH}/ruby.o#{info.job}"
		begin
			File.open(file_name) do |inl|
				while (line = inl.gets)
					result += line.to_f
				end
			end
			File.delete(file_name)
		rescue
		end
	end
}
print result

I to by było na tyle - z pomocą tego kodu możemy policzyć na gridzie całkę metodą Monte Carlo. Całość kodu managera:

$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
$LOAD_PATH.unshift(File.dirname(__FILE__))

require 'original-drmaa.rb'

STD_PATH = "/home/path"
SOURCE_PATH = "/home/path"

class Carlos < DRMAA::JobTemplate
  def initialize
	  super
	  self.command = "/usr/bin/ruby"
	  self.arg = ["#{SOURCE_PATH}/monte_carlo.rb", 0, 100, 10]
	  self.stdout = ":#{STD_PATH}/"
	  self.join = true
	  end
end


session = DRMAA::Session.new

jobs = []
session.run_bulk(Carlos.new, 1, 10).each { |job| jobs << job }

result = 0
session.wait_each{ |info|
	if ! info.wifexited?
		puts "failed: " + info.job
	else
		file_name = "#{STD_PATH}/ruby.o#{info.job}"
		begin
			File.open(file_name) do |inl|
				while (line = inl.gets)
					result += line.to_f
				end
			end
			File.delete(file_name)
		rescue
		end
	end
}
print result

Copyright © 2025 Closer to Code

Theme by Anders NorenUp ↑