Le Triumvirat "Solid" en Action
Bienvenue dans ce quatrième article dans la série sur le Triumvirat "Solid" dans Rails 8 ! Et on fini en beauté en développant ensemble une mini applications : un système de notifications en temps réel.
On va également parler de cas d'usage réels et de recommandations
- Comment optimiser en production
- Comment déployer en production
- Comment adresser les problèmes les plus courants
N'hésitez pas à parcourir les précédents articles pour reprendre ou consolider vos connaissances sur le sujet avant d'attaquer cette mini-application :
- "Solid Cache" : Ne plus dépendre de Redis pour le cache dans Rails
- "Solid Cable" : Des websockets et du temps réel simplement et sans prise de tête
- "Solid Queue" : Des taches de fonds à la sauce Rails
- Le triumvirat appliqué à un projet complet
Accrochez ça va être du solide :)
Le Triumvirat dans un projet complet Rails 8
Construisons une mini-application qui utilise les trois technologies Solid ensemble : un système de notifications en temps réel avec cache.
L'Application : Notifications en Temps Réel
Fonctionnalités :
- Créer des notifications pour les utilisateurs
- Les afficher en temps réel via WebSocket
- Cacher le nombre de notifications non lues
- Traiter les notifications en fond pour envoyer des emails
Structure du Projet
# vérifiez que vous avez Rails 8
rails -v
> 8.0.0.1
# Créez un nouveau projet
rails new solid_notifications
cd solid_notifications
rails db:create
Configurez cable.yml pour utiliser solid_cable au lieu de async en développement.
# Async adapter only works within the same process, so for manually triggering cable updates from a console,
# and seeing results in the browser, you must do so from the web console (running inside the dev process),
# not a terminal started via bin/rails console! Add "console" to any action or any ERB template view
# to make the web console appear.
development:
adapter: solid_cable
test:
adapter: test
production:
adapter: solid_cable
connects_to:
database:
writing: cable
polling_interval: 0.1.seconds
message_retention: 1.day
En développement il faut ajouter le schéma de la base de donnée solid_cable dans la base de donnée. En production, on aura une base de donnée dédiée donc pas besoin.
bin/rails db:schema:load SCHEMA=db/cable_schema.rb
Modèles
rails g model User name:string email:string
rails g model Notification user:references title:string content:text read:boolean
rails db:migrate
# app/models/user.rb
class User < ApplicationRecord
has_many :notifications, dependent: :destroy
def unread_notifications_count
# Utiliser Solid Cache pour éviter de compter à chaque fois
Rails.cache.fetch("user_#{id}_unread_count", expires_in: 5.minutes) do
notifications.where(read: [false, nil]).count
end
end
def mark_notification_as_read(notification_id)
notification = notifications.find(notification_id)
notification.update(read: true)
# Invalider le cache
Rails.cache.delete("user_#{id}_unread_count")
end
end
# app/models/notification.rb
class Notification < ApplicationRecord
belongs_to :user
after_create_commit do
# 1. Broadcaster via Solid Cable
broadcast_notification
# 2. Enqueuer un job via Solid Queue pour envoyer l'email
NotificationEmailJob.perform_later(self.id)
# 3. Invalider le cache Solid Cache
Rails.cache.delete("user_#{user_id}_unread_count")
end
# Diffuser les changements d'état (ex: lu/non lu)
after_update_commit :broadcast_read_update, if: :saved_change_to_read?
private
def broadcast_notification
broadcast_prepend_to(
"user_#{user_id}_notifications",
target: "notifications-list",
partial: "notifications/notification",
locals: { notification: self }
)
# Broadcaster aussi le nouveau compte
broadcast_replace_to(
"user_#{user_id}_notifications",
target: "notifications-count",
partial: "notifications/count",
locals: { count: user.notifications.where(read: [false, nil]).count }
)
end
def broadcast_read_update
# Remplacer l'élément de notification pour refléter l'état "lu"
broadcast_replace_to(
"user_#{user_id}_notifications",
target: ActionView::RecordIdentifier.dom_id(self),
partial: "notifications/notification",
locals: { notification: self }
)
# Mettre à jour le compteur
broadcast_replace_to(
"user_#{user_id}_notifications",
target: "notifications-count",
partial: "notifications/count",
locals: { count: user.notifications.where(read: [false, nil]).count }
)
end
end
Job de Fond (Solid Queue)
# app/jobs/notification_email_job.rb
class NotificationEmailJob < ApplicationJob
queue_as :mailers
retry_on StandardError, wait: :exponentially_longer, attempts: 5
def perform(notification_id)
notification = Notification.find(notification_id)
# Simuler l'envoi d'email (remplacer par votre mailer)
Rails.logger.info "Sending email for notification #{notification_id} to #{notification.user.email}"
# UserMailer.notification_email(notification).deliver_now
# Simuler un délai d'envoi
sleep(2)
Rails.logger.info "Email sent successfully for notification #{notification_id}"
end
end
Canal WebSocket (Solid Cable)
# app/channels/notifications_channel.rb
class NotificationsChannel < ApplicationCable::Channel
def subscribed
# S'abonner au stream personnel de l'utilisateur
stream_from "user_#{current_user.id}_notifications"
end
def unsubscribed
# Cleanup
end
end
Contrôleurs
# app/controllers/notifications_controller.rb
class NotificationsController < ApplicationController
before_action :set_current_user # Simuler l'authentification
def index
# Utiliser Solid Cache pour cacher la liste
@notifications = Rails.cache.fetch("user_#{current_user.id}_notifications", expires_in: 1.minute) do
current_user.notifications.order(created_at: :desc).limit(50).to_a
end
@unread_count = current_user.unread_notifications_count
end
def create
@notification = current_user.notifications.build(notification_params)
if @notification.save
# Le broadcast et le job sont gérés automatiquement par le callback
head :ok
else
render json: { errors: @notification.errors }, status: :unprocessable_entity
end
end
def mark_as_read
current_user.mark_notification_as_read(params[:id])
head :ok
end
private
def notification_params
params.require(:notification).permit(:title, :content)
end
def set_current_user
# Simuler un utilisateur connecté (remplacer par votre système d'auth)
@current_user = User.first || User.create(name: "Test User", email: "test@example.com")
end
def current_user
@current_user
end
helper_method :current_user
end
Vues
<!-- app/views/notifications/index.html.erb -->
<div class="notifications-container">
<%= turbo_stream_from "user_#{current_user.id}_notifications" %>
<div class="header">
<h1>Mes Notifications</h1>
<%= render "notifications/count", count: @unread_count %>
</div>
<!-- Formulaire de test pour créer une notification -->
<div class="create-notification">
<%= form_with(
model: Notification.new,
url: notifications_path
) do |form| %>
<%= form.text_field :title, placeholder: "Titre" %>
<%= form.text_area :content, placeholder: "Contenu" %>
<%= form.submit "Créer une notification (test)" %>
<% end %>
</div>
<!-- Liste des notifications -->
<div id="notifications-list">
<%= render @notifications %>
</div>
</div>
<!-- app/views/notifications/_notification.html.erb -->
<div class="notification <%= 'unread' unless notification.read %>" id="<%= dom_id(notification) %>">
<div class="notification-content">
<h3><%= notification.title %></h3>
<p><%= notification.content %></p>
class="time"><%= time_ago_in_words(notification.created_at) %> ago</span>
</div>
<% unless notification.read %>
<%= button_to "Marquer comme lu",
mark_as_read_notification_path(notification),
method: :patch,
form: { data: { turbo_stream: true } } %>
<% end %>
</div>
<!-- app/views/notifications/_count.html.erb -->
<span id="notifications-count" class="badge <%= 'has-unread' if count > 0 %>">
<%= count %> non lues
</span>
Routes
# config/routes.rb
Rails.application.routes.draw do
resources :notifications, only: [:index, :create] do
member do
patch :mark_as_read
end
end
root "notifications#index"
end
CSS (optionnel, pour le style)
/* app/assets/stylesheets/notifications.css */
.notifications-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.badge {
padding: 5px 15px;
background: #e0e0e0;
border-radius: 20px;
font-size: 14px;
}
.badge.has-unread {
background: #ff5252;
color: white;
}
.notification {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
margin-bottom: 15px;
display: flex;
justify-content: space-between;
align-items: center;
}
.notification.unread {
border-left: 4px solid #2196F3;
background: #f0f8ff;
}
.notification h3 {
margin: 0 0 10px 0;
font-size: 18px;
}
.notification p {
margin: 0 0 5px 0;
color: #666;
}
.time {
font-size: 12px;
color: #999;
}
button {
background: #2196F3;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #1976D2;
}
.create-notification {
background: #f5f5f5;
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
}
.create-notification input,
.create-notification textarea {
width: 100%;
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
Tester l'Application
- Démarrer le serveur :
- Ouvrir deux fenêtres de navigateur sur http://localhost:3000
- Dans la première fenêtre, créer une notification avec le formulaire
- Observer la magie :✅ La notification apparaît instantanément dans les deux fenêtres (Solid Cable)
- ✅ Le compteur se met à jour en temps réel (Solid Cable)
- ✅ Dans les logs, vous verrez le job d'email en cours (Solid Queue)
- ✅ Si vous rafraîchissez, la liste et le compteur sont rapides (Solid Cache)
- Marquer une notification comme lue :Le badge se met à jour
- Le cache est invalidé automatiquement
Ce qui se passe en coulisse
Quand vous créez une notification :
- Le modèle est sauvegardé dans PostgreSQL
- Le callback after_create_commit se déclenche :Solid Cable : Broadcast de la notification via WebSocket (table solid_cable_messages)
- Solid Queue : Enqueue du job d'email (table solid_queue_jobs)
- Solid Cache : Invalidation du cache (suppression dans solid_cache_entries)
- Les clients connectés reçoivent le message WebSocket et Turbo Stream met à jour le DOM
- Le worker Solid Queue récupère le job et envoie l'email en fond
- Les prochaines visites utilisent le cache pour afficher rapidement la liste
Cas d'Usage Réels et Recommandations
Quand Utiliser le Triumvirat Solid ?
Le triumvirat Solid est parfait pour :
✅ Startups et Projets Side
- Budget limité (pas besoin de Redis)
- Infrastructure simple
- Déploiement rapide
- Moins de services à maintenir
✅ Applications Petites à Moyennes
- < 10,000 utilisateurs actifs
- < 1,000 jobs par heure
- < 500 connexions WebSocket simultanées
- Trafic modéré et prévisible
✅ Projets où la Simplicité Prime
- Équipe de développement réduite
- Pas de DevOps dédié
- Applications internes d'entreprise
- MVPs et prototypes
✅ Hébergement sur Plateformes Simples
- Heroku, Render, Fly.io
- Pas de configuration réseau complexe
- Un seul serveur ou scaling horizontal simple
Quand Passer à Redis/Sidekiq ?
Envisagez de migrer vers des solutions Redis si :
❌ Performance Critique
- Latence WebSocket < 50ms requise
10,000 jobs par heure
1,000 connexions WebSocket simultanées
- Cache avec des millions d'entrées
❌ Fonctionnalités Avancées Nécessaires
- Pub/Sub complexe
- Sorted sets, HyperLogLog
- Lua scripting
- Structures de données Redis spécifiques
❌ Scaling Massif
- Application distribuée sur plusieurs datacenters
- Microservices nécessitant un message broker
- Besoin de sharding avancé
Architecture Hybride : Le Meilleur des Deux Mondes
Vous pouvez aussi mixer les approches :
# config/environments/production.rb
# Solid Cache pour la plupart des choses
config.cache_store = :solid_cache_store
# Mais Redis pour des données ultra-critiques
REDIS_CACHE = ActiveSupport::Cache::RedisCacheStore.new(
url: ENV['REDIS_URL']
)
# Dans votre code
# Cache normal
Rails.cache.fetch('user_profile') { ... }
# Cache critique avec Redis
REDIS_CACHE.fetch('critical_data') { ... }
Optimisations et Bonnes Pratiques
Solid Cache
1. Choisir les Bonnes Durées d'Expiration
# ❌ Mauvais : trop court, cache peu utile
Rails.cache.fetch('products', expires_in: 10.seconds) { ... }
# ✅ Bon : équilibré selon la fréquence de mise à jour
Rails.cache.fetch('products', expires_in: 15.minutes) { ... }
# ✅ Bon : données rarement modifiées
Rails.cache.fetch('static_config', expires_in: 1.day) { ... }
2. Utiliser des Clés Descriptives
# ❌ Mauvais : clé vague
Rails.cache.fetch('data') { ... }
# ✅ Bon : clé descriptive et versionnée
Rails.cache.fetch("user_#{user.id}_dashboard_v2") { ... }
3. Invalidation Intelligente
# app/models/product.rb
class Product < ApplicationRecord
after_save :clear_related_caches
private
def clear_related_caches
Rails.cache.delete('all_products')
Rails.cache.delete("product_#{id}")
Rails.cache.delete("category_#{category_id}_products")
end
end
4. Cache Fragments dans les Vues
<!-- ❌ Mauvais : cache trop large -->
<% cache 'entire_page' do %>
<%= render @products %>
<%= render 'user_panel' %>
<% end %>
<!-- ✅ Bon : cache granulaire -->
<% cache 'product_list' do %>
<%= render @products %>
<% end %>
<% cache ['user_panel', current_user.id, current_user.updated_at] do %>
<%= render 'user_panel' %>
<% end %>
5. Monitoring de la Taille du Cache
# lib/tasks/cache_maintenance.rake
namespace :cache do
desc "Report cache statistics"
task stats: :environment do
total_entries = SolidCache::Entry.count
total_size = SolidCache::Entry.sum(:byte_size) / 1.megabyte
puts "Total entries: #{total_entries}"
puts "Total size: #{total_size.round(2)} MB"
# Nettoyer si trop grand
if total_size > 1024 # > 1GB
puts "Cache too large, cleaning old entries..."
SolidCache::Entry.where('created_at < ?', 7.days.ago).delete_all
end
end
end
Solid Cable
1. Limiter les Broadcasts
# ❌ Mauvais : broadcast à chaque changement
class Product < ApplicationRecord
after_update :broadcast_update
end
# ✅ Bon : broadcaster uniquement si nécessaire
class Product < ApplicationRecord
after_update :broadcast_update, if: :should_broadcast?
private
def should_broadcast?
saved_change_to_price? || saved_change_to_stock?
end
end
2. Batching des Messages
# ❌ Mauvais : 100 broadcasts pour 100 produits
products.each do |product|
product.broadcast_update
end
# ✅ Bon : 1 broadcast global
broadcast_replace_to(
'products_list',
target: 'products',
partial: 'products/list',
locals: { products: products }
)
3. Déconnexion Propre
// ❌ Mauvais : ne jamais se désabonner
let subscription = consumer.subscriptions.create("ChatChannel", {})
// ✅ Bon : se désabonner quand on quitte la page
export default class extends Controller {
connect() {
this.subscription = consumer.subscriptions.create("ChatChannel", {})
}
disconnect() {
if (this.subscription) {
this.subscription.unsubscribe()
}
}
}
4. Polling Interval Adapté
# config/cable.yml
# ❌ Trop fréquent : surcharge inutile
production:
adapter: solid_cable
polling_interval: 0.01.seconds # Toutes les 10ms !
# ✅ Équilibré pour la plupart des cas
production:
adapter: solid_cable
polling_interval: 0.1.seconds # Toutes les 100ms
# ✅ Pour du temps réel moins critique
production:
adapter: solid_cable
polling_interval: 0.5.seconds # Toutes les 500ms
Solid Queue
1. Idempotence des Jobs
Vos jobs doivent être idempotents : s'ils s'exécutent plusieurs fois, le résultat doit être le même.
# ❌ Mauvais : pas idempotent
class IncrementCounterJob < ApplicationJob
def perform(user_id)
user = User.find(user_id)
user.increment!(:counter) # Si exécuté 2 fois, counter += 2 !
end
end
# ✅ Bon : idempotent
class UpdateCounterJob < ApplicationJob
def perform(user_id)
user = User.find(user_id)
new_count = user.posts.count # Recalcule à chaque fois
user.update(counter: new_count)
end
end
2. Timeouts Appropriés
class LongRunningJob < ApplicationJob
# ❌ Mauvais : pas de timeout, peut bloquer
def perform
loop do
# Code qui pourrait tourner indéfiniment
end
end
end
# ✅ Bon : avec timeout
class LongRunningJob < ApplicationJob
def perform
Timeout.timeout(5.minutes) do
# Code avec limite de temps
end
rescue Timeout::Error
Rails.logger.error "Job timeout after 5 minutes"
raise
end
end
3. Queues Dédiées par Type
# config/solid_queue.yml
production:
workers:
# Jobs critiques : haute priorité
- queues: critical
threads: 5
processes: 2
polling_interval: 0.1
# Emails : beaucoup de threads (I/O)
- queues: mailers
threads: 10
processes: 2
polling_interval: 1
# Rapports : peu de threads (CPU)
- queues: reports
threads: 2
processes: 1
polling_interval: 5
# Utiliser les bonnes queues
class UrgentJob < ApplicationJob
queue_as :critical
end
class NewsletterJob < ApplicationJob
queue_as :mailers
end
class MonthlyReportJob < ApplicationJob
queue_as :reports
end
4. Logging Détaillé
class ImportJob < ApplicationJob
def perform(file_id)
file = ImportFile.find(file_id)
Rails.logger.info "Starting import of file #{file_id}"
start_time = Time.current
begin
result = process_file(file)
duration = Time.current - start_time
Rails.logger.info "Completed import of file #{file_id} in #{duration}s. #{result[:count]} records imported"
rescue => e
Rails.logger.error "Failed import of file #{file_id}: #{e.class} - #{e.message}"
Rails.logger.error e.backtrace.join("\n")
raise
end
end
end
5. Utiliser les Priorités
# Priorité par défaut : jobs FIFO
RegularJob.perform_later
# Haute priorité (nombre plus bas = plus prioritaire)
UrgentJob.set(priority: 0).perform_later
# Basse priorité
CleanupJob.set(priority: 100).perform_later
Déploiement en Production
Configuration des Bases de Données
Option 1 : Bases Séparées (Recommandé)
# config/database.yml
production:
primary:
<<: *default
database: mon_app_production
username: mon_app
password: <%= ENV["DATABASE_PASSWORD"] %>
cache:
<<: *default
database: mon_app_cache_production
username: mon_app
password: <%= ENV["DATABASE_PASSWORD"] %>
migrations_paths: db/cache_migrate
queue:
<<: *default
database: mon_app_queue_production
username: mon_app
password: <%= ENV["DATABASE_PASSWORD"] %>
migrations_paths: db/queue_migrate
cable:
<<: *default
database: mon_app_cable_production
username: mon_app
password: <%= ENV["DATABASE_PASSWORD"] %>
Avantages :
- Isolation totale
- Pas d'impact des jobs sur les requêtes utilisateurs
- Scaling indépendant
Inconvénient :
- Plus de bases à gérer
Option 2 : Base Unique (Plus Simple)
# config/database.yml
production:
primary:
<<: *default
database: mon_app_production
cache:
<<: *default
database: mon_app_production
migrations_paths: db/cache_migrate
queue:
<<: *default
database: mon_app_production
migrations_paths: db/queue_migrate
cable:
<<: *default
database: mon_app_production
Avantages :
- Configuration ultra-simple
- Une seule base à gérer
- Coûts réduits
Inconvénient :
- Contention possible sous forte charge
Recommandation : Commencez avec une base unique. Migrez vers des bases séparées si vous rencontrez des problèmes de performance.
Avec Kamal (Recommandé pour Rails 8)
# config/deploy.yml
service: mon_app
image: mon_app/mon_app
servers:
web:
- 192.168.1.100
job:
hosts:
- 192.168.1.101
cmd: bin/jobs
env:
secret:
- RAILS_MASTER_KEY
- DATABASE_URL
- CACHE_DATABASE_URL
- QUEUE_DATABASE_URL
- CABLE_DATABASE_URL
Déployer :
Avec Docker + Docker Compose
# docker-compose.yml
version: '3.8'
services:
web:
build: .
command: bundle exec rails server -b 0.0.0.0
ports:
- "3000:3000"
environment:
DATABASE_URL: postgresql://postgres:password@db:5432/mon_app_production
depends_on:
- db
worker:
build: .
command: bundle exec bin/jobs
environment:
DATABASE_URL: postgresql://postgres:password@db:5432/mon_app_production
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Variables d'Environnement Importantes
# .env.production
RAILS_ENV=production
RAILS_MASTER_KEY=your_master_key_here
# Database
DATABASE_URL=postgresql://user:pass@host:5432/app_production
# Si bases séparées
CACHE_DATABASE_URL=postgresql://user:pass@host:5432/app_cache_production
QUEUE_DATABASE_URL=postgresql://user:pass@host:5432/app_queue_production
CABLE_DATABASE_URL=postgresql://user:pass@host:5432/app_cable_production
# Solid Queue
SOLID_QUEUE_IN_PUMA=true # Si vous voulez utiliser le plugin Puma
# Monitoring (optionnel)
SENTRY_DSN=your_sentry_dsn
Monitoring et Alertes
1. Surveiller les Queues
# lib/tasks/monitoring.rake
namespace :monitoring do
desc "Check queue health"
task queue_health: :environment do
pending_jobs = SolidQueue::Job.count
failed_jobs = SolidQueue::FailedExecution.count
if pending_jobs > 1000
alert("High pending jobs count: #{pending_jobs}")
end
if failed_jobs > 100
alert("High failed jobs count: #{failed_jobs}")
end
end
def alert(message)
# Envoyer une alerte (Slack, email, etc.)
Rails.logger.error("[ALERT] #{message}")
end
end
Avec cron :
# Vérifier toutes les 5 minutes
*/5 * * * * cd /var/www/mon_app && bundle exec rake monitoring:queue_health
2. Surveiller le Cache
namespace :monitoring do
desc "Check cache health"
task cache_health: :environment do
cache_size = SolidCache::Entry.sum(:byte_size) / 1.gigabyte.to_f
if cache_size > 10 # > 10GB
alert("Cache size is large: #{cache_size.round(2)} GB")
end
# Vérifier les performances
start = Time.current
Rails.cache.read('health_check_key')
duration = Time.current - start
if duration > 0.1 # > 100ms
alert("Cache read is slow: #{(duration * 1000).round(2)}ms")
end
end
end
3. Logs Structurés
# config/environments/production.rb
config.log_formatter = proc do |severity, datetime, progname, msg|
{
timestamp: datetime.iso8601,
severity: severity,
program: progname,
message: msg
}.to_json + "\n"
end
Troubleshooting : Problèmes Courants
Solid Cache
Problème : "Cache très lent"
Diagnostic :
# Mesurer la vitesse du cache
require 'benchmark'
time = Benchmark.realtime do
Rails.cache.read('test_key')
end
puts "Cache read took #{(time * 1000).round(2)}ms"
Solutions :
- Vérifier que les index sont bien créés
- Augmenter la RAM de la base de données
- Utiliser un SSD plus rapide
- Vérifier que max_size n'est pas dépassé
Problème : "Table solid_cache_entries trop grande"
# Voir la taille
SolidCache::Entry.sum(:byte_size) / 1.gigabyte # En GB
# Solution 1 : Réduire max_size dans config/cache.yml
# Solution 2 : Nettoyer manuellement
SolidCache::Entry.where('created_at < ?', 7.days.ago).delete_all
Solid Cable
Problème : "Les messages ne sont pas reçus"
Vérifications :
- Vérifier que Solid Cable est bien utilisé :
# config/cable.yml
development:
adapter: solid_cable # Pas "async" !
- Vérifier la connexion WebSocket dans la console navigateur :
// Dans la console du navigateur
App.cable.connection.isActive() // Doit retourner true
- Vérifier les logs Rails pour les erreurs de broadcast
Solution : Souvent, c'est un problème de configuration cable.yml ou de migration manquante.
Problème : "Latence élevée"
Diagnostic :
# Réduire le polling_interval
production:
adapter: solid_cable
polling_interval: 0.05.seconds # Plus fréquent (50ms au lieu de 100ms)
Attention : Un polling trop fréquent augmente la charge base de données !
Solid Queue
Problème : "Jobs ne s'exécutent pas"
Vérifications :
- Le worker tourne-t-il ?
- Y a-t-il des jobs en attente ?
SolidQueue::Job.count # Devrait augmenter si jobs ne s'exécutent pas
- Vérifier les logs du worker :
tail -f log/production.log | grep SolidQueue
Solutions communes :
- Redémarrer le worker : bin/jobs
- Vérifier la configuration solid_queue.yml
- Vérifier les migrations sont bien appliquées
Problème : "Jobs échouent en boucle"
# Voir les jobs échoués
failed = SolidQueue::FailedExecution.all
failed.each do |f|
puts "Job: #{f.job_class}"
puts "Error: #{f.error}"
puts "Executions: #{f.executions}"
puts "---"
end
Solutions :
- Corriger le bug dans le job
- Augmenter le nombre de retry
- Ajouter discard_on pour certaines erreurs
- Supprimer les jobs irrécupérables :
SolidQueue::FailedExecution.where('executions > 10').delete_all
Problème : "Table solid_queue_jobs trop grande"
# Voir le nombre de jobs
SolidQueue::Job.count
# Nettoyer les jobs terminés depuis longtemps
SolidQueue::Job.finished_before(7.days.ago).delete_all
Prévention :
# Tâche cron quotidienne
# lib/tasks/cleanup.rake
namespace :cleanup do
desc "Clean old finished jobs"
task old_jobs: :environment do
deleted = SolidQueue::Job.finished_before(7.days.ago).delete_all
puts "Deleted #{deleted} old jobs"
end
end
Migration depuis Redis/Sidekiq
Vous avez déjà une app avec Redis et Sidekiq et voulez migrer vers le triumvirat Solid ? Voici comment.
Migration du Cache (Redis → Solid Cache)
Étape 1 : Installer Solid Cache
bundle add solid_cache
bin/rails solid_cache:install
rails db:migrate
Étape 2 : Basculer progressivement
# config/environments/production.rb
# Garder les deux pendant la transition
PRIMARY_CACHE = ActiveSupport::Cache::SolidCacheStore.new
REDIS_CACHE = ActiveSupport::Cache::RedisCacheStore.new(url: ENV['REDIS_URL'])
# Utiliser Solid Cache par défaut
config.cache_store = :solid_cache_store
Dans votre code, vous pouvez tester en parallèle :
# Écrire dans les deux
Rails.cache.write('key', value)
REDIS_CACHE.write('key', value)
# Lire de Solid Cache, fallback sur Redis
value = Rails.cache.read('key') || REDIS_CACHE.read('key')
Étape 3 : Monitoring
Surveillez les performances pendant quelques jours. Si tout va bien, retirez Redis.
Migration des Jobs (Sidekiq → Solid Queue)
Étape 1 : Installation
bundle add solid_queue
bin/rails solid_queue:install
rails db:migrate
Étape 2 : Adapter les Jobs
Les jobs Active Job fonctionnent directement, mais vérifiez les spécificités Sidekiq :
# Avant (Sidekiq)
class MyJob
include Sidekiq::Job
sidekiq_options retry: 5, queue: 'critical'
def perform(user_id)
# ...
end
end
# Après (Solid Queue)
class MyJob < ApplicationJob
queue_as :critical
retry_on StandardError, attempts: 5
def perform(user_id)
# ...
end
end
Étape 3 : Cohabitation temporaire
Vous pouvez faire tourner les deux en parallèle :
# config/application.rb
config.active_job.queue_adapter = :sidekiq # Par défaut Sidekiq
# Pour certains jobs spécifiques
class TestJob < ApplicationJob
self.queue_adapter = :solid_queue # Ce job utilise Solid Queue
def perform
# ...
end
end
Étape 4 : Migration progressive
- Migrez d'abord les jobs non-critiques
- Surveillez les performances
- Migrez les jobs critiques
- Une fois stable, retirez Sidekiq
Migration d'Action Cable
# config/cable.yml
# Avant
production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") %>
# Après
production:
adapter: solid_cable
connects_to:
database:
writing: cable
polling_interval: 0.1.seconds
Redémarrez l'app et testez les fonctionnalités temps réel.
Conclusion : L'Avenir de Rails
Le triumvirat Solid (Cache, Cable, Queue) représente bien plus qu'un simple changement technique dans Rails 8. C'est une philosophie : celle de rendre le développement Rails plus simple, plus accessible, et plus économique.
Ce que nous avons appris
Dans cet article, nous avons exploré en profondeur :
- Solid Cache : Un système de cache basé sur la base de données qui rivalise avec Redis pour la plupart des applications, tout en offrant une capacité de stockage bien supérieure
- Solid Cable : Une solution de WebSocket qui élimine le besoin de Redis pour Action Cable, rendant le temps réel accessible à tous
- Solid Queue : Un gestionnaire de jobs de fond performant et fiable, sans dépendance externe
Les avantages clés du triumvirat
- ✅ Simplicité : Une seule technologie (votre base de données) au lieu de trois ou quatre
- ✅ Coûts réduits : Pas besoin de services externes coûteux
- ✅ Déploiement facile : Moins de configuration, moins de maintenance
- ✅ Performance correcte : Suffisant pour 80% des applications Rails
- ✅ Approche moderne : Tire parti des SSD rapides et des bases de données évoluées
Quand adopter le triumvirat Solid ?
Oui, adoptez-le si :
- Vous démarrez un nouveau projet Rails 8
- Vous êtes une startup ou une petite équipe
- Vous voulez réduire la complexité
- Votre application n'a pas besoin de performance extrême
Peut-être, selon le contexte :
- Vous migrez une application existante
- Vous avez des besoins de performance élevés mais pas extrêmes
- Vous voulez d'abord tester avant de vous engager
Non, gardez vos solutions actuelles si :
- Vous avez des millions d'utilisateurs
- La latence doit être < 50ms
- Vous traitez des dizaines de milliers de jobs par minute
- Vous utilisez des fonctionnalités Redis avancées
L'approche progressive
Vous n'êtes pas obligé de tout adopter d'un coup ! Commencez par un composant :
- Solid Cache d'abord : Le plus facile à adopter
- Solid Queue ensuite : Quand vous êtes à l'aise avec le cache
- Solid Cable enfin : Si vous avez besoin de temps réel
Ressources pour aller plus loin
- Documentation officielle Rails : guides.rubyonrails.org
- GitHub Solid Cache : github.com/rails/solid_cache
- GitHub Solid Cable : github.com/rails/solid_cable
- GitHub Solid Queue : github.com/rails/solid_queue
- Blog de DHH : Annonces et philosophie derrière Rails 8
Mot de la fin
Le triumvirat Solid n'est pas parfait pour tous les cas d'usage, mais il représente une avancée majeure pour la communauté Rails. Il rend le développement web moderne accessible à tous, sans sacrifier la qualité ou les fonctionnalités.
Que vous soyez développeur junior ou expérimenté, j'espère que cet article vous a donné une compréhension claire et pratique de Solid Cache, Solid Cable et Solid Queue. Ces trois technologies vont probablement devenir la norme pour les applications Rails dans les années à venir.
Alors n'hésitez plus : lancez-vous, expérimentez, et construisez des applications Rails modernes avec le triumvirat Solid !
Avez-vous des questions ? N'hésitez pas à consulter la documentation officielle ou à rejoindre la communauté Rails pour obtenir de l'aide.
Bon coding ! 🚀