25. Oktober 2012

Ruby on Rails Hintergrund Jobs mit Clockwork und Sidekiq

Edit (5.7.2014): Dieser Artikel ist etwas veraltet. Statt dem Gem Clockwork benutzen wir nun Sidetiq. Dieses funktioniert hervorragend mit Sidekiq und ist wesentlich einfacher zu verwalten.

Unsere Rails Applikation muss einige Aufgaben im Hintergrund erledigen. Zum Beispiel ruft sie die Email Accounts von Usern ab und verarbeitet diese. Wir haben dafür eine Möglichst einfache Lösung gesucht und in den beiden Gems Sidekiq und Clockwork gefunden.

Sidekiq ist zuständig für das Abarbeiten von Aufgaben, die es gestellt bekommt, während Clockwork für die regelmässige Verteilung dieser Aufgaben zuständig ist. Weil Clockwork in der jeweiligen Applikation konfiguriert wird, ist die Wartung wesentlich einfacher, als Cronjobs.

Um das Ganze auszutesten schrieb ich eine kleine Applikation, die im Hintergrund Primzahlen vor sich hin berechnet.

Voraussetzung Redis Server

Redis ist ein sehr einfacher, dafür aber hochperformanter Datenbankserver. Sidekiq verwaltet seine Queues darin, weshalb wir Redis zuerst installieren müssen. Auf dem Mac benutzt man dafür den Paketmanager seines Vertrauens.

$ brew install redis

Gemfile

Die folgenden Gems müssen im Gemfile eingetragen werden:

gem 'sidekiq'   # Sidekiq
gem 'sinatra'   # Für die Monitoring Applikation
gem 'slim'      # Ebenfalls die für Monitoring Applikation
gem 'clockwork' # Cron Ersatz

Sidekiq Monitor

Damit wir sehen, was abgeht, machen wir den Sidekiq Monitor über einen Mount verfügbar.

require 'sidekiq/web'
WorkerTestApp::Application.routes.draw do
  ### mount sidekiq engine ###
  mount Sidekiq::Web => '/sidekiq'
end

Worker Klasse

Pro Hintergund-Aufgabe erstellen wir eine Klasse im Verzeichnis app/workers. Weil diese Klassen nur schwer getestet werden können, empfiehlt es sich, den Hauptteil der Logik in die eigentliche Anwendung zu verpacken. Aus Gründen der Einfachheit lasse ich die Berechnung für dieses Beispiel aber den Worker selbst machen. Ich habe bei der Berechnung der Primzahlen absichtlich nicht auf einen möglichst effizienten Algorithmus geachtet. Der Worker soll ja was zu tun haben!

class HeavyWorker
  include Sidekiq::Worker

  def perform(name, count)
    primes = []
    for i in 0..count
      primes << i if isPrime(i)
    end
    puts "#{name} job: From 0 to #{count} there are #{primes.count} prime numbers."
  end

  private

  def isPrime(number)
    if number == 0 or number == 1
      return false
    end

    i = 2
    limit = number / i
    while i < limit
      return false if number % i == 0
      i += 1
      limit = number / i
    end
    return true
  end
end

Jeder Worker muss eine Methode perform haben. Diese wird von Sidekiq aufgerufen. Der Methode können beliebige Argumente übergeben werden. Da diese Daten aber serialisiert in die Redis Queue geschrieben werden, empfiehlt es sich, nur einfache Datentypen zu übergeben. Statt einem ganzen ActiveRecord-Objekt übergibt man also besser nur die ID und ruft diese dann im Worker ab.

Task Scheduling

Nun brauchen wir noch jemanden, der Sidekiq beschäftigt. Und zwar wollen wir pro Minute ein Mal alle Primzahlen von 1 bis 100 wissen. Zusätzlich wollen wir alle 5 Minuten alle Primzahlen von 1 bis 1'000'000 berechnet haben. Das klingt nach Cronjob. Diese sind aber viel zu aufwändig zu pflegen. Deshalb habe ich auf den Clockwork Gem zurückgegriffen. Clockwork läuft als eigener Prozess auf dem/einem Applikationsserver und meldet zu definierten Zeiten, was Sidekiq zu tun hat.

# require boot & environment for a Rails app
require_relative "../config/boot"
require_relative "../config/environment"
require 'clockwork'
module Clockwork
  every(2.minutes, 'huge_prime_numbers.job') do
    HardWorker.perform_async('huge', 10000000)
  end

  every(10.seconds, 'light_prime_numbers.job') do
    HardWorker.perform_async('light', 100)
  end
end

Los geht’s

Das Starten und überwachen der Prozesse überlassen wir normalerweise God. Auf dem Entwicklungsrechner starte ich meine Prozesse aber mit Foreman. Das ist recht bequem. Redis starte ich nicht über Foreman, weil der erstens auf unserem Ubuntu Server als System-Prozess läuft und, zweitens, ich damit eine bessere Kontrolle über die Start- und vor allem Stopreihenfolge habe.

# Procfile
web:    bundle exec rails server
worker: bundle exec sidekiq
clock:  bundle exec clockwork config/clock.rb

Nun kann die Applikation gestartet werden. redis-server wird in einem anderen Terminal ausgeführt.

$ redis-server
$ bundle install
$ foreman start

Die Sidekiq Konsole müsste nun folgendermassen erreichbar sein: http://localhost:3000/sidekiq.

Die Beispielapplikation habe ich auf Github bereitgestellt.