Article écrit par Hugo Fabre
Dans le cadre d’une application web, il nous est tous déjà arrivés 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.
Une petite touche d’exotisme avec Crystal
Ce que nous savons 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 : nous avons 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, nous permettons à 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. Nous pouvons 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, nous pouvons 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 :
gt; bundle install
gt; 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 :
gt; 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é :
gt; export PKG_CONFIG_PATH="/usr/local/opt/openssl/lib/pkgconfig"
On lance Redis, puis notre worker, et enfin notre application :
gt; redis-server
gt; ./worker
gt; 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, nous ne pouvons 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 :
gt; bundle exec sidekiq -r ./worker.rb
gt; 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 sommes 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.
Nous nous quittons 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.