Retour à l'accueil

Async-background : tâches en arrière-plan en Ruby sans Redis ni Postgres

Async-background — gem pour Ruby, fournissant des tâches en arrière-plan sans dépendances externes. Résout les problèmes de gestion des ressources et de fiabilité. Adapté aux applications sur Async et Falcon.

Comment exécuter des tâches en arrière-plan en Ruby sans Redis ? La solution async-background
Advertisement 728x90

Async-background : un système léger de jobs en arrière-plan pour Ruby sans dépendances externes

Les développeurs d'applications Ruby ont souvent besoin d'exécuter des jobs en arrière-plan, mais les solutions prêtes à l'emploi comme Sidekiq nécessitent une infrastructure supplémentaire. La nouvelle gem async-background offre une alternative qui s'exécute sur Async sans Redis ni Postgres, avec un contrôle fin des ressources.

Pourquoi les solutions existantes étaient insuffisantes

Sidekiq est généralement utilisé pour les jobs en arrière-plan en Ruby, mais il nécessite Redis. Pour les petits projets avec seulement une poignée de jobs, c'est excessif : un conteneur supplémentaire dans docker-compose, un autre point de défaillance, et des coûts d'infrastructure. De plus, Sidekiq ne permet pas une distribution flexible de la charge entre les workers — un rapport nocturne lourd peut ralentir le traitement des e-mails des utilisateurs.

Les autres options ont aussi leurs inconvénients :

Google AdInline article slot
  • whenever — génère un crontab système, sans file d'attente ni intégration à l'app.
  • rufus-scheduler — s'exécute dans des threads séparés, causant des changements de contexte et des problèmes de duplication dans les apps Async avec plusieurs processus.
  • good_job et solid_queue — liés à ActiveRecord et Postgres. Cela crée des risques : les jobs en arrière-plan concurrencent la charge transactionnelle pour les ressources de la base de données, potentiellement ralentissant l'app principale.

Ainsi, il manquait une solution combinant plannings cron, intervalles et file d'attente dynamique sans dépendances externes, avec un contrôle sur l'utilisation des ressources.

Principes de conception clés

L'auteur de la gem a défini six principes fondamentaux :

  • Boucle d'événements unique. Le scheduler s'exécute dans la même boucle réactive que l'app, sans threads séparés.
  • Zéro infrastructure. La file est stockée sur disque (SQLite en mode WAL ou un fichier dans tmp/), pas de Redis ni Postgres.
  • Dépendances optionnelles. Les fonctionnalités supplémentaires (comme les métriques) sont ajoutées via des gems optionnelles.
  • Sécurité multi-processus sans coordinateur. Assure qu'un job s'exécute dans un seul processus lors de l'utilisation de Falcon (forks).
  • Persistance comme assurance, vitesse comme mécanisme séparé. La fiabilité vient du stockage SQLite, la vitesse des réveils via socket UNIX.
  • Utilisation gérée des workers. Les développeurs peuvent assigner des jobs à des workers spécifiques via variables d'environnement et config YAML.

Ces principes ont permis de créer une gem qui s'intègre parfaitement dans les apps Async existantes sans infrastructure supplémentaire.

Google AdInline article slot

Fonctionnalités architecturales

MinHeap au lieu de ticks de polling

Au lieu d'une boucle avec sleep 1 (réveil toutes les secondes pour vérifier tous les jobs), un tas minimum binaire (MinHeap) est utilisé. Cela permet :

  • Récupération du prochain job en O(1) avec peek.
  • Mise à jour du tas après exécution d'un job en O(log n) avec replace_top.
  • Sommeil précis jusqu'au prochain événement, économisant les ressources CPU.

L'implémentation ne fait que 74 lignes et n'a pas de dépendances externes.

Deux échelles de temps

Pour les jobs basés sur intervalles (ex. every: 60), des horloges monotones (CLOCK_MONOTONIC) sont utilisées pour éviter les problèmes de synchronisation horaire (NTP). Pour les jobs cron (ex. cron: "0 3 *"), l'heure murale (Time.now) est utilisée car le planning est lié au temps calendaire.

Google AdInline article slot

En code, cela donne :

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

Lors de l'utilisation de Falcon (qui fork), les jobs doivent s'exécuter dans un seul processus. Le sharding déterministe y pourvoit : Zlib.crc32(name) % total_workers + 1 est calculé pour le nom du job. S'il correspond à l'index du worker courant, le job est chargé.

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

Cela élimine le besoin de verrous distribués.

Skip-on-Overlap

Si un job dure plus longtemps que son intervalle (ex. 90 secondes pour un intervalle de 60), la nouvelle instance est sautée. Au lieu de cela, le job courant est journalisé, sauté, et replannifié pour le prochain créneau :

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

File d'attente : SQLite + Socket UNIX

La file dynamique utilise deux mécanismes :

  • SQLite — source de vérité. Les jobs sont persistés sur disque pour la fiabilité.
  • Socket UNIX — pour notifications instantanées. Après ajout d'un job en base, un signal est envoyé via le socket pour que le worker le traite immédiatement.

Le polling toutes les 5 secondes sert de secours en cas de perte de signal.

Récupération des pannes

Si un worker plante en plein job, le statut reste "running". Au redémarrage, Queue::Store#recover trouve ces enregistrements et les remet dans la file.

Points clés à retenir

  • Pas de dépendances sur services externes. Async-background repose uniquement sur le système de fichiers, simplifiant le déploiement.
  • Contrôle des ressources. Isoler les workers pour différents types de jobs (cron, file) prévient la contention des ressources.
  • Fiabilité. Les jobs ne sont pas perdus grâce à la persistance SQLite et aux mécanismes de récupération.
  • Flexibilité. Config YAML et variables d'environnement facilitent l'ajustement de la distribution des jobs.
  • Performance. Min-heap et sockets UNIX offrent faible latence et utilisation efficace du CPU.

— Editorial Team

Advertisement 728x90

Lire ensuite