Category: Rails

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.

Rails3 + mały middleware z kontrolerem

Ostatnimi dniami wzięło mnie na poprawianie moich repo na githubie. Jednym z pluginów do poprawy był mój niewielki Custom Errors Handler, który ma za zadanie generować odpowiedni szablon błędu zależnie od miejsca w którym wystąpił błąd. Dzięki temu, np. moduł administracji może mieć inne 404 niż moduł użytkowników.

Cały kod składa się z dwóch klas:

  • MyActionDispatch::ShowExceptions
  • CustomErrorsHandlerController

Pierwsza z nich to middleware odpowiedzialny za przejmowanie obsługi wyświetlania wyjątków, drugi to kontroler który ma wygenerować widok.

W cały kod wgłębiać się nie będę (jest dostępny na moim githubie), opiszę jednak proces dodawania middlewaru z poziomu gemu dedykowanego dla Railsów. Dawniej był plik init.rb, w którym można było najzwyczajniej nadpisać domyślny ActionDispatch::ShowExceptions. Takie podejście w Railsach3 jednak nie przejdzie (chociaż póki co, jako plugin działa ;) ). Aby móc nasz middleware wykorzystać zamiast domyślnego, posłużymy się metodą swap middleware'u (o innym metodach z rodziny middleware'u przeczytasz tutaj).

Zamiana jest całkiem prosta:

  class Railtie < Rails::Railtie
    initializer "app.insert_my_errors_handler" do |app|
      app.config.middleware.swap ActionDispatch::ShowExceptions, 
        MyActionDispatch::ShowExceptions
    end
  end

Podajemy co zamieniamy i na co, pamiętając przy tym, że init.rb już nie jest wywoływany, więc ten kod powinien trafić do /lib/nazwa_gema.rb.

Pamiętajmy też, że jeżeli chcemy mieć kompletnie niezależny kontroler - nie powinien on dziedziczyć po ApplicationControlerze, ale raczej bezpośrednio po ActionController::Base, dzięki czemu nie będzie zależny od filtrów nałożonych na ApplicationController.

Copyright © 2026 Closer to Code

Theme by Anders NorenUp ↑