Ensure – naprawdę gwarantuje wykonanie

Ostatnio przepisywałem skrypt do zbierania statystyk ruchu ze switchy po snmp w celu pobrania ilości oktetów, które przeleciały przez jego interfejsy. Na końcu aktualizuję pliki RRD, w których te informacje zapisuje. Niby prosta rzecz, ale trzeba obsłużyć bardzo wiele wyjątków (błąd połączenia do bazy danych, timeout switcha, wywalenie się jakiegoś wątku itp. itd.).

Skrypt jest odpalany co minutę z crona i zazwyczaj wyrabia się w przeciągu 10 – 15 sekund. Czasem jednak wystąpi jakiś fackup (timeouty do switcha, timeout do bazy danych, load na maszynie itp.), więc taki skrypt może wykonywać się dłużej. W przypadku wykonywania skryptu dłużej niż minuta pojawia się problem , że cron może odpalić drugą instancję, która również może mieć w/w opóźnienia. Jest to sytuacja, do której nie możemy dopuścić.

begin
  SCRIPT_NAME = "update_stats"

  if File.exist? "tmp/#{SCRIPT_NAME}.pid"
    Logger.instance.warning "Another instance is running. QUIT"
    exit
  else
    File.open("tmp/#{SCRIPT_NAME}.pid", "w") do |file|
      file.puts Process.pid
    end
  end
  ...
  ...

rescue => e
  puts "**** FATAL ERROR ****"
  puts
  puts e.message
  puts
  puts e.backtrace.join("\n")
  puts
  puts "**** END ****"

ensure
  FileUtils.rm_f "tmp/#{SCRIPT_NAME}.pid"
end

Jak to działa? Skrypt podczas startu tworzy plik z pidem. Teraz jeśli odpalimy drugą instancję skryptu, podczas gdy inna również działa dostaniemy komunikat z błędem i skrypt zostanie zakończony. Można by pomyśleć, że wszystko jest ok, ale nie do końca.

Problemem jest słowo kluczowe ensure. Jak wiemy ensure gwarantuje nam wykonanie się kodu w przypadku wystąpienia wyjątku np. możemy zamknąć plik lub połączenie do bazy danych. Dotychczas jednak nie wiedziałem, że jeśli w bloku kodu begin …. ensure …. end wywołamy metodę Kernel#exit to ensure też zostanie wykonane. Poniżej przykładowy kawałek kodu:

begin
  Kernel.exit
ensure
  puts "Hello World"
end

Teraz gdy wykonamy ten skrypt dostaniemy w wyniku:

[y3ti@Macintosh:~/tmp]$ ruby cos.rb
Hello World

… ale co to ma do mojego skryptu do odpytywania switchy? Ano ma to, że ensure zawsze się wykona! W kodzie zostanie wykonana metoda Kernel#exit, a następnie zostanie wykonany blok ensure, a tym samym zostanie usunięty plik z pidem! Teraz, gdy opalimy jeszcze raz skrypt (a inna instancja będzie działać) to skrypt nie zakończy swojego działania, tylko będzie sobie śmiało działał, przez co będziemy mieć dwa działające skrypty!

Pytanie jak poprawić nasz pierwszy kawałek kodu, aby nie pozwolić na usunięcie pida? Poniżej kod:

begin
  other_istance_running = false
  SCRIPT_NAME = "update_stats"

  if File.exist? "tmp/#{SCRIPT_NAME}.pid"
    Logger.instance.warning "Another instance is running. QUIT"
    other_istance_running = true
    exit
  else
    File.open("tmp/#{SCRIPT_NAME}.pid", "w") do |file|
      file.puts Process.pid
    end
  end
  ...
  ...

rescue => e
  puts "**** FATAL ERROR ****"
  puts
  puts e.message
  puts
  puts e.backtrace.join("\n")
  puts
  puts "**** END ****"

ensure
  FileUtils.rm_f "tmp/#{SCRIPT_NAME}.pid" unless other_istance_running
end

You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

2 Comments »

 
 

Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>