Ruby, Sidekiq et Crystal

Publié le 25 juillet 2019 par Hugo Fabre | back - ruby

Cet article est publié sous licence CC BY-NC-SA

Dans le cadre d’une application web il nous est tous déjà arrivé de devoir effectuer une tâche assez longue en asynchrone pour ne pas gêner le flux de notre application. En Ruby la solution la plus populaire est d’utiliser l’outil Sidekiq.

Sidekiq est un outil développé en Ruby qui rend la création et l’utilisation de tâche asynchrone vraiment simple à intégrer dans une application existante. Pour fonctionner, Sidekiq gère une queue de tâches dans un Redis sous un format spécifique.

jason-d--XvhrIC1Mhc-unsplash

Une petite touche d’exotisme avec Crystal

Ce que l’on sait moins, c’est que le développeur derrière Sidekiq, Mike Perham, a réimplémenté son outil en Crystal. Certes il y a quelques fonctionnalités en moins, mais ce qui est intéressant c’est que l’objectif a été de garder une compatibilité dans la manière de gérer la queue de tâches dans Redis. Par conséquent il est en théorie très simple de rajouter à la file une demande d’exécution qui sera ensuite exécutée par un worker développé en Crystal.

Pour rappel, Crystal est un langage de programmation compilé dont la syntaxe est très inspirée de Ruby. Bien qu’il n’y ait pas de volonté de maintenir une compatibilité, en pratique sur du code simple c’est souvent le cas.

L’idée de cet article est donc de présenter une preuve de concept d’une application Ruby qui ferait appel à un worker Crystal.

Pourquoi ?

Ici mon point de départ a été la découverte de la compatibilité entre Sidekiq Ruby et Sidekiq Crystal. Je me suis donc demandé dans quelle situation on pourrait en tirer avantage. La réponse est assez simple : on a besoin d’effectuer un traitement lourd en calcul dans une application Ruby sans rajouter une couche de complexité qui demanderait une grande adaptation aux développeurs (par exemple une extension native en C ou en Rust) et sans avoir un worker très long et gourmand en mémoire (typiquement pour de la génération de statistiques). Comme toute solution technique à un problème donné, il y a des avantages et des inconvénients.

Les points positifs

Ici on permet à une application Ruby de tirer parti d’un langage performant et moins gourmand en mémoire. Sidekiq est un outil que tout le monde a l’habitude d’utiliser et le code du worker en Crystal sera facilement compréhensible pour un développeur Ruby. D’ailleurs dans le cadre de cet article le code du worker est totalement utilisable en Ruby comme en Crystal (bien sûr c’est un exemple très simple).

Les points négatifs

Il ne sera pas forcément facile de faire revenir le résultat de notre worker dans notre application principale. On peut imaginer certaines solutions (WebSocket, requête HTTP ou même se connecter depuis le worker à la base de données de l’application, ou encore par e-mail si l’on n’a pas besoin d’un retour dans l’application) mais toutes ne sont pas adaptées à toutes les situations.

De plus Crystal n’ayant toujours pas de version stable on peut ne pas souhaiter l’intégrer à une application qui tourne en production.

En bref

Pour moi il s’agit clairement d’une solution à garder sous le coude car elle peut présenter de gros avantages, mais uniquement dans certaines situations pour le moment.

L’implémentation

Eh bien en fait, même si le processus n’est pas documenté, l’implémentation reste finalement très simple. Avant toute chose, il y a quelques outils à installer à l’avance. Nous aurons besoin de Redis, Crystal et Ruby.

Ensuite il nous faudra un Gemfile pour l’application Ruby, celui-ci sera très simple :

# Gemfile

source 'https://rubygems.org'

gem 'sidekiq'

Et un fichier shard.yml (plus ou moins l’équivalent du Gemfile pour une application Crystal) presque aussi simple :

# shard.yml

name: worker
version: 0.1.0

authors:
  - vous <vous@email.com>

targets:
  crystal:
    main: worker.cr

dependencies:
  sidekiq:
    github: mperham/sidekiq.cr
    branch: master

crystal: 0.27.2

license: MIT

On peut ensuite installer toutes nos dépendances :

$> bundle install
$> shards install

Et enfin le code. Le calcul que nous allons réaliser et un simple calcul de factoriel tout droit sortie de Rosetta Code.

Le worker :

# worker.cr

require "sidekiq"
require "sidekiq/cli"

class FactorialWorker
  include Sidekiq::Worker

  def perform(n : Int32)
    (2...n).each { |i| n *= i }
    n.zero? ? 1 : n
  end
end


# See https://github.com/mperham/sidekiq.cr/wiki/Configuration
ENV["LOCAL_REDIS"] = "redis://localhost:6379/8"
ENV["REDIS_PROVIDER"] = "LOCAL_REDIS"

cli = Sidekiq::CLI.new
server = cli.configure do |config|
  # middleware would be added here
end

cli.run(server)

L’application Ruby :

# app.rb

require 'sidekiq'

redis_config = { url: 'redis://localhost:6379/8' }

Sidekiq.configure_client do |config|
  config.redis = redis_config
end

Sidekiq::Client.push('class' => 'FactorialWorker', 'args' =>[100_000])

On compile ensuite notre worker :

$> crystal build --release worker.cr

Sous MacOS j’ai rencontré une erreur du linker lors de la compilation qui ne trouvait pas OpenSSL via pkg-config :

ld: library not found for -lssl (this usually means you need to install the development package for libssl)
clang: error: linker command failed with exit code 1 (use -v to see invocation)

Comme j’avais déjà installé OpenSSL via Homebrew j’ai simplement utilisé brew info openssl qui m’a indiqué la variable d’environnement à exporter. Dans mon cas j’ai utilisé :

$> export PKG_CONFIG_PATH="/usr/local/opt/openssl/lib/pkgconfig"

On lance Redis, puis notre worker, et enfin notre application :

$> redis-server
$> ./worker
$> bundle exec ruby app.rb

Et on observe le résultat :

8728 TID-20vrgqo  JID=ef3eda4298c177869ea0dde7 INFO: Start
8728 TID-20vrgqo  JID=ef3eda4298c177869ea0dde7 INFO: Done: 0.000108 sec

Plutôt simple non ?

Comme tout développeur digne de ce nom on ne peut pas résister à faire un petit benchmark ~qui n’a pas de sens~, je vous propose donc un worker ruby pour comparer :

# worker.rb

require "sidekiq"

class FactorialWorker
  include Sidekiq::Worker

  def perform(n)
    (2...n).each { |i| n *= i }
    n.zero? ? 1 : n
  end
end

Sidekiq.configure_server do |config|
  config.redis = { url: 'redis://localhost:6379/8' }
end

On note que même si l’api de Sidekiq est différente le code métier est exactement le même. Ensuite on lance notre worker Ruby (sans oublier de couper celui en Crystal avant) et on relance notre application :

$> bundle exec sidekiq -r ./worker.rb
$> bundle exec ruby app.rb

Et voilà le résultat sur ma machine :

9384 TID-ouyqxa944 FactorialWorker JID-19031fc5db6010f45415f771 INFO: start
9384 TID-ouyqxa944 FactorialWorker JID-19031fc5db6010f45415f771 INFO: done: 6.656 sec

Conclusion

Comme promis l’implémentation est vraiment simple et le code de notre worker est facilement compréhensible et maintenable par un développeur Ruby sans besoin de monter en compétences sur un nouveau langage. Évidemment pour respecter le contexte de preuve de concept nous somme restés sur une utilisation et une implémentation très simples. Je vous invite vivement à tester par vous-même et nous faire part de vos retours.

On se quitte sur deux liens qui pourraient vous être utile pour vos recherches : le seul article que j’ai trouvé qui parle du sujet (en chinois mais heureusement le code est très compréhensible et il est toujours possible d’utiliser son outil de traduction préféré au besoin) de plus celui-ci propose une intégration dans une application Rails et enfin le readme de la version Crystal de Sidekiq qui liste les fonctionnalités manquantes par rapport à la version originale et explique les choix notamment sur les différences d’api entre les deux versions.


L’équipe Synbioz.
Libres d’être ensemble.