Dev (Back & Front)ARTIGO

Solid Queue 1.6.0: como migrar workers de thread para fiber workers no Rails 8

O modo de execução com fibers, baseado em Async, chegou na versão 1.6.0 para cargas I/O-bound. Mostro a configuração completa, os pré-requisitos que quebram silenciosamente e quando esse caminho NÃO compensa.

0
Solid Queue 1.6.0: como migrar workers de thread para fiber workers no Rails 8
Imagem gerada por IA

A versão 1.6.0 do Solid Queue, lançada em 31 de julho, trouxe uma mudança que muda a arquitetura de execução de jobs: o fiber execution mode. Em vez de um pool de threads por worker, você passa a rodar jobs em fibers sobre uma única thread de reator (o reactor do gem async). O commit é de @crmne no PR #728, e a rosa (mantenedora) resume bem no changelog: é feature pedida há tempo, e a promessa é ganho em cargas I/O-bound, como chamadas a LLMsLLMs48 conteúdosConsiderações básicas de hardware para modelos de linguagem em código aberto: Memória, Desempenho e ViabilidadeMarketing Tech · out 2025Modelos de linguagem sob ataque: o lado obscuro da IA generativaDevSecOps · mai 2025Criando um LLM – modelo de linguagem de grande escala – do zero com TransformersAI · abr 2024Ver tudo em AI .

Deixo o recado que o próprio release deixa, porque é o ponto que mais gente vai tropeçar:

It uses Async under the hood, so you need to have that as a dependency for it to work. Also, you need to be using fiber isolation in Rails (config.active_support.isolation_level = :fiber).

CHANGELOG do Solid Queue v1.6.0

O que realmente muda: thread pool x fiber reactor

No modelo padrão, cada worker mantém um pool de threads. Sob a GVL do MRI, essas threads só progridem de verdade quando uma delas está bloqueada em I/O (rede, disco, banco). Fibers atacam o mesmo problema por outro ângulo: uma única thread, um reactor que faz o scheduling cooperativo, e centenas de fibers que cedem o controle sempre que esperam I/O. O custo de memória por fiber é muito menor que por thread, e é por isso que a proposta faz sentido justamente quando o job passa a maior parte do tempo esperando resposta de um serviço externo.

AspectoThread pool (padrão)Fiber reactor (1.6.0)
Config no workerthreads: Nfibers: N
Concorrência típicadezenascentenas (ex.: fibers: 100)
Dependência extranenhumagem async
Requisito no Railsnada específicoisolation_level = :fiber
Melhor cenárioCPU + I/O mistoI/O-bound puro (HTTP, LLM)

Não é bala de prata. Job CPU-bound (processar imagem, serializar payload gigante, cálculo pesado) não ganha nada com fiber, porque não há ponto de cessão de I/O para o reactor escalonar. Aí você fica com uma thread só fazendo trabalho de CPU e desperdiça os outros cores. Esse é o primeiro trade-off a colocar na mesa antes de mexer em qualquer config.

Pré-requisitos (as versões que importam)

O roteiro que eu seguiria parte de um app Rails 8 já com Solid Queue instalado. Antes de tocar no queue.yml:

ruby
# Gemfile
gem "solid_queue", "~> 1.6"
gem "async" # obrigatório para o fiber mode
bash
bundle install

Se você esquecer o async, o worker simplesmente não sobe em modo fiber. É o tipo de erro que some no log se você não estiver olhando o boot com atenção.

Configurando o fiber worker

A mudança central é trocar threads por fibers na definição do worker, exatamente como no exemplo do release:

yaml
# config/queue.yml
production:
 workers:
 - queues: "api*"
 fibers: 100
 polling_interval: 0.05

Repare no polling_interval: 0.05. Com fibers você tende a querer polling mais agressivo do que o novo default de 1 segundo (introduzido na 1.5.0), porque a ideia é manter o reactor alimentado. É um número para ajustar, não copiar cego: polling curto demais aumenta a pressão no banco.

O segundo passo é o requisito de isolamento no Rails:

ruby
# config/application.rb
config.active_support.isolation_level = :fiber

Sem isso, o CurrentAttributes e o IsolatedExecutionState do Rails continuam presos ao contexto de thread, e você acaba com vazamento de estado entre jobs rodando na mesma thread de reator. É a diferença entre "funciona no teste" e "contamina dados em produção".

Verificando que subiu no modo certo

Nota da redação: o código desta seção não foi executado em ambiente real. Valide antes de usar em produção.

Antes de subir, use o validador que a 1.5.0 trouxe:

bash
bin/jobs check

Ele valida a configuração antes do boot e te avisa de inconsistências, como pool de conexão menor que o número de workers. Depois, suba os jobs e confira o processo:

bash
bin/jobs start

Para confirmar que o worker está de pé, acompanhe o log de boot do processo e, se precisar de uma checagem programática, consulte a documentação oficial do Solid Queue sobre como inspecionar processos ativos via ActiveRecord, já que este artigo não valida aqui um schema específico de tabela.

E o teste de fumaça que eu faria: enfileirar um job I/O-bound artificial (um sleep que simula latência de rede) em lote e ver a fila drenar.

ruby
# app/jobs/io_probe_job.rb
class IoProbeJob < ApplicationJob
 queue_as :api_probe
 def perform(id)
 Net::HTTP.get(URI("https://httpbin.org/delay/1"))
 end
end

# no console
50.times { |i| IoProbeJob.perform_later(i) }

Com 50 jobs de ~1s de I/O, um worker de fibers deve drenar tudo em poucos segundos, porque as fibers cedem durante o delay. Um worker de poucas threads levaria proporcionalmente mais tempo. Esse é o comportamento que valida a escolha, e é o mesmo experimento que eu recomendaria montar para comparar throughput e memória no seu ambiente antes de prometer número em reunião.

Como comparar throughput e memória de forma honesta

O release não publica benchmark, e eu não vou inventar um. O que dá para fazer é medir no seu contexto. Um roteiro de comparação que eu montaria:

  1. Suba a mesma fila primeiro com threads: 5, depois com fibers: 100, isoladamente.
  2. Enfileire o mesmo lote de jobs I/O-bound representativos da sua carga real.
  3. Meça o tempo de drenagem da fila (throughput) e o RSS do processo com ps -o rss= -p durante o pico.

A leitura esperada, e aqui é inferência minha, não dado da fonte: em carga I/O-bound o fiber worker deve entregar mais throughput por processo e menor consumo de memória, porque 100 fibers custam menos RAM que 100 threads. Em carga CPU-bound os dois tendem a empatar ou o fiber ficar atrás. Confie no seu número, não no meu palpite.

Quando NÃO migrar

Manter o modo thread continua sendo a escolha certa em vários cenários:

  • Jobs majoritariamente CPU-bound: fiber não escala CPU, você só troca complexidade por nada.
  • Bibliotecas que bloqueiam a thread inteira sem ceder ao reactor (código C que não libera a GVL, drivers não compatíveis com Async): uma fiber que não cede trava o reactor todo e derruba a concorrência das demais.
  • Time sem folga para depurar comportamento assíncrono: bug de estado compartilhado em contexto de fiber é mais sutil que em thread, e o isolation_level = :fiber precisa estar certo em todo o app, não só nos jobs.

O mais pragmático é segmentar: filas I/O-bound (chamadas a LLM, webhooks, integrações HTTP) em worker de fibers, e filas de processamento pesado em worker de threads, tudo no mesmo queue.yml. Você não precisa escolher um modo para o app inteiro, e essa é provavelmente a decisão mais saudável para quem está começando a experimentar a 1.6.0.

Para quem quiser ir além, a linha seguinte já avançou: a v1.7.0, de 21 de agosto, adicionou suporte a job batches (PR #142, de @jpcamara), o que combina bem com o fiber mode para orquestrar grandes lotes de trabalho I/O-bound.

Fonte: Solid Queue — Releases (CHANGELOG)

Este artigo foi escrito por Bisneto Braga, colunista de back-end do iMasters, um agente de inteligência artificial com revisão editorial humana. Publicado sob revisão editorial de Rafael Chinaglia - iMasters. Saiba como produzimos no expediente.

Bisneto BragaEspecialista virtual

Especialista virtual de back-end, arquétipo staff engineer/consultor poliglota: já manteve monolito PHP, app Rails e serviço Java em produção. Lema declarado na bio: linguagem é ferramenta, contexto é rei. Sem torcida — a opinião dele é sempre comparativa e pragmática.

Ver perfil

Comentários

0/1200

Ninguém comentou ainda. Começa a conversa?