Zurück zur Startseite

Async-background: Hintergrundaufgaben in Ruby ohne Redis und Postgres

Async-background — Gem für Ruby, das Hintergrundaufgaben ohne externe Abhängigkeiten bereitstellt. Löst Probleme mit Ressourcenmanagement und Zuverlässigkeit. Geeignet für Anwendungen auf Async und Falcon.

Wie führt man Hintergrundaufgaben in Ruby ohne Redis aus? Die async-background-Lösung
Advertisement 728x90

# Async-background: Ein leichtgewichtiges Hintergrund-Job-System für Ruby ohne externe Abhängigkeiten

Entwickler von Ruby-Anwendungen müssen häufig Hintergrundjobs ausführen, doch fertige Lösungen wie Sidekiq erfordern zusätzliche Infrastruktur. Das neue Gem async-background bietet eine Alternative, die auf Async läuft – ohne Redis oder Postgres – und eine feingranulare Kontrolle über Ressourcen ermöglicht.

Warum bestehende Lösungen zu kurz greifen

Sidekiq wird typischerweise für Hintergrundjobs in Ruby verwendet, erfordert aber Redis. Für kleine Projekte mit nur wenigen Jobs ist das Overkill: ein zusätzlicher Container in docker-compose, ein weiterer Ausfallpunkt und Infrastrukturkosten. Zudem erlaubt Sidekiq keine flexible Lastverteilung unter Workers – ein schwerer nächtlicher Bericht kann die Verarbeitung von Benutzer-E-Mails verlangsamen.

Andere Optionen haben ebenfalls Nachteile:

Google AdInline article slot
  • whenever — erzeugt eine System-Crondatei, ohne Queue oder App-Integration.
  • rufus-scheduler — läuft in separaten Threads, was Kontextwechsel und Duplikationsprobleme in Async-Apps mit mehreren Prozessen verursacht.
  • good_job und solid_queue — gebunden an ActiveRecord und Postgres. Das birgt Risiken: Hintergrundjobs konkurrieren mit transaktionaler Last um Datenbankressourcen und können die Hauptanwendung verlangsamen.

Es bestand daher Bedarf an einer Lösung, die Cron-Zeitpläne, Intervalle und eine dynamische Queue ohne externe Abhängigkeiten kombiniert, ergänzt um Kontrolle über die Ressourcennutzung.

Wichtige Designprinzipien

Der Autor des Gems hat sechs Kernprinzipien formuliert:

  • Einzelne Event-Loop. Der Scheduler läuft in derselben reaktiven Schleife wie die App, ohne separate Threads.
  • Keine Infrastruktur. Die Queue wird auf Disk gespeichert (SQLite im WAL-Modus oder eine Datei in tmp/), kein Redis oder Postgres.
  • Optionale Abhängigkeiten. Zusätzliche Features (z. B. Metriken) werden über optionale Gems hinzugefügt.
  • Multi-Process-Sicherheit ohne Koordinator. Stellt sicher, dass ein Job bei Verwendung von Falcon (Forks) nur in einem Prozess läuft.
  • Persistenz als Versicherung, Geschwindigkeit als separater Mechanismus. Zuverlässigkeit durch SQLite-Speicherung, Geschwindigkeit durch UNIX-Socket-Wakeups.
  • Gesteuerte Worker-Nutzung. Entwickler können Jobs über Umgebungsvariablen und YAML-Konfig spezifischen Workers zuweisen.

Diese Prinzipien ermöglichen ein Gem, das nahtlos in bestehende Async-Apps integriert wird, ohne zusätzliche Infrastruktur.

Google AdInline article slot

Architekturmerkmale

MinHeap statt Polling-Ticks

Statt einer Schleife mit sleep 1 (die jede Sekunde aufwacht, um alle Jobs zu prüfen), wird ein binärer Min-Heap (MinHeap) verwendet. Das ermöglicht:

  • Abruf des nächsten Jobs in O(1)-Zeit mit peek.
  • Aktualisierung des Heaps nach Job-Ausführung in O(log n)-Zeit mit replace_top.
  • Präzises Schlafen bis zum nächsten Event, was CPU-Ressourcen spart.

Die Implementierung umfasst nur 74 Zeilen und hat keine externen Abhängigkeiten.

Zwei Zeitskalen

Für intervallbasierte Jobs (z. B. every: 60) werden monotonische Uhren (CLOCK_MONOTONIC) verwendet, um Probleme mit Zeitsynchronisation (NTP) zu vermeiden. Für Cron-Jobs (z. B. cron: "0 3 *") wird Wanduhrenzeit (Time.now) genutzt, da der Zeitplan an die Kalenderzeit gebunden ist.

Google AdInline article slot

Im Code sieht das so aus:

next_run_at = if task_config[:interval]
  now + jitter + task_config[:interval]
else
  now_wall = Time.now
  wall_wait = task_config[:cron].next_time(now_wall).to_f - now_wall.to_f
  now + jitter + [wall_wait, MIN_SLEEP_TIME].max
end

Sharding via CRC32

Bei Verwendung von Falcon (das forkt) darf ein Job nur in einem Prozess laufen. Deterministisches Sharding erreicht das: Zlib.crc32(name) % total_workers + 1 wird für den Job-Namen berechnet. Passt es zum Index des aktuellen Workers, wird der Job geladen.

assigned = config['worker']&.to_i || ((Zlib.crc32(name) % total_workers) + 1)
next unless assigned == worker_index

Das macht verteilte Locks überflüssig.

Skip-on-Overlap

Läuft ein Job länger als sein Intervall (z. B. 90 Sekunden bei 60-Sekunden-Intervall), wird die neue Instanz übersprungen. Stattdessen wird der aktuelle Job protokolliert, übersprungen und für den nächsten Slot neu geplant:

if entry.running
  logger.warn('Async::Background') { "#{entry.name}: skipped, previous run still active" }
  metrics.job_skipped(entry)
  entry.reschedule(monotonic_now)
  heap.replace_top(entry)
  next
end

Queue: SQLite + UNIX Socket

Die dynamische Queue nutzt zwei Mechanismen:

  • SQLite — Quelle der Wahrheit. Jobs werden für Zuverlässigkeit auf Disk persistiert.
  • UNIX Socket — für sofortige Benachrichtigungen. Nach Hinzufügen eines Jobs zur DB wird ein Signal über den Socket gesendet, damit der Worker ihn unverzüglich verarbeitet.

Polling alle 5 Sekunden dient als Backup bei Signalverlust.

Fehlerwiederherstellung

Stürzt ein Worker mitten in einem Job ab, bleibt der Status „running“. Beim Neustart findet Queue::Store#recover solche Einträge und gibt sie zurück in die Queue.

Wichtige Erkenntnisse

  • Keine Abhängigkeiten von externen Diensten. Async-background basiert nur auf dem Dateisystem und vereinfacht den Deployment-Prozess.
  • Ressourcenkontrolle. Isolierung von Workers für verschiedene Job-Typen (Cron, Queue) verhindert Ressourcenkonflikte.
  • Zuverlässigkeit. Jobs gehen nicht verloren dank SQLite-Persistenz und Wiederherstellungsmechanismen.
  • Flexibilität. YAML- und Umgebungsvariablen-Konfig erleichtern die Anpassung der Job-Verteilung.
  • Performance. Min-Heap und UNIX-Sockets sorgen für niedrige Latenz und effiziente CPU-Nutzung.

— Editorial Team

Advertisement 728x90

Weiterlesen