Skip to content

Adota Solid Queue como fila persistente em produção - #87

Merged
Hirley merged 1 commit into
mainfrom
claude-hirley/solid-queue
Sep 1, 2026
Merged

Hirley merged 1 commit into
mainfrom
claude-hirley/solid-queue

Conversation

@Hirley

@Hirley Hirley commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Closes #77. Fecha o achado de alta prioridade da avaliação técnica: a fila vivia na memória do processo web.

Não havia queue_adapter configurado em lugar nenhum, então valia o default do Rails — :async. Todo restart, deploy ou OOM descartava em silêncio o que ainda não tinha rodado, incluindo as retentativas de webhook que o #86 acabou de introduzir e que, por definição, ficam pendentes por algum tempo.

A prova, com o compose de verdade

Specs não cobrem isto — o adapter em teste é o :test. Então verifiquei subindo a stack:

1. worker parado          → job enfileirado pelo web → 1 pendente no banco
2. docker compose restart web  (simula um deploy)    → 1 pendente ainda lá
3. worker de volta                                    → 0 pendentes, finished_at preenchido

O passo 2 é o ponto: com :async, aquele job teria desaparecido.

Decisões

Mesmo banco da aplicação, e não o banco separado que o instalador do Solid Queue assume. Multi-banco obrigaria a reescrever o config/database.yml inteiro — incluindo o caminho de DATABASE_URL, que é o que o Railway injeta — para resolver um problema de escala que este projeto não tem. O db/queue_schema.rb da gem virou uma migration comum, com o motivo registrado nela.

Produção usa :solid_queue; desenvolvimento continua no :async. Exigir um segundo processo para bin/rails server quebraria o fluxo de setup que o README documenta. A durabilidade importa onde há deploy.

O worker espera o web ficar saudável, não só o banco. É o entrypoint do web que aplica as migrations; subir antes faria o worker procurar tabelas que ainda não existem. E ele não roda migration nenhuma de propósito — dois processos preparando o mesmo banco ao mesmo tempo é corrida, não redundância. (Isso encosta na #82, que trata de tirar as migrations do boot do web; aqui só garanti que o worker não piora o quadro.)

Build e ambiente saem de âncoras YAML compartilhadas. Duas listas separadas divergiriam na primeira variável nova que alguém esquecesse de copiar, e o sintoma seria um job se comportando diferente da tela — caro de diagnosticar.

Três detalhes que valem o comentário que receberam

  • O HEALTHCHECK da imagem faz curl na porta 3000. Faz sentido para o web, não para o worker, que não serve HTTP — desligado no serviço, senão ficaria eternamente unhealthy por não responder algo que nem deveria responder.
  • config/recurring.yml existe vazio só para o worker parar de avisar que não encontrou o arquivo. Ruído permanente num log é ruído que se aprende a ignorar.
  • As duas migrations do Solid Queue são schema da gem copiado, não código nosso: entraram na exclusão do RuboCop pelo mesmo critério já aplicado ao db/schema.rb. Sem isso são 287 ofensas, e reformatá-las faria o próximo solid_queue:update virar conflito.

Efeito colateral que vale declarar

O Gemfile.lock ganhou a plataforma x86_64-linux, resultado de resolver as gems dentro do container. É uma melhoria — o lockfile passa a descrever a plataforma onde a aplicação de fato roda, e era inclusive uma das recomendações da avaliação. O bundle lock --add-platform do Dockerfile continua ali como rede de segurança, agora como no-op.

Verificação

resultado
RuboCop 105 arquivos, 0 offenses
Zeitwerk check All is good!
RSpec 375 exemplos, 0 falhas
docker compose up --build três serviços saudáveis, na ordem certa
Job de ponta a ponta enfileirado pelo web, executado pelo worker, 0 falhas
Durabilidade sobrevive a restart do web com o worker parado

O que continua em aberto

O adapter de desenvolvimento segue :async, então lá os jobs ainda se perdem — documentado no README, e é o trade-off deliberado acima. E a #82 (migrations acopladas ao boot do web) fica mais visível agora que há dois processos: hoje o worker depende do web ter subido para que o banco esteja pronto.

🤖 Generated with Claude Code

Closes #77.

Nao havia queue_adapter configurado em lugar nenhum, entao valia o
default do Rails: :async, que guarda a fila na memoria do processo web.
Todo restart, deploy ou OOM descartava em silencio o que ainda nao tinha
rodado — inclusive as retentativas de webhook agendadas com backoff pelo
PR anterior, que por definicao ficam pendentes por algum tempo.

Producao passa a usar :solid_queue, gravando nas tabelas solid_queue_*
do proprio PostgreSQL. Desenvolvimento segue com :async, pra que
`bin/rails server` sozinho continue funcionando sem exigir um segundo
processo; teste segue com :test.

Mesmo banco da aplicacao, e nao o banco separado que o instalador da gem
assume. Multiplos bancos obrigariam a reescrever config/database.yml
inteiro, incluindo o caminho de DATABASE_URL que o Railway injeta, pra
resolver um problema de escala que este projeto nao tem. Por isso o
db/queue_schema.rb da gem virou uma migration comum.

O worker e um servico separado no docker-compose.yml, rodando bin/jobs.
Sobe so depois de o web ficar saudavel: e o entrypoint do web que aplica
as migrations, e o worker precisa das tabelas existindo. Ele nao roda
migration nenhuma de proposito — dois processos preparando o mesmo banco
ao mesmo tempo e corrida, nao redundancia. Build e ambiente saem de
ancoras YAML compartilhadas, pra que web e worker nao divirjam na
primeira variavel nova que alguem esquecer de copiar.

Detalhes que ficaram registrados no codigo:

  * o HEALTHCHECK da imagem faz curl na porta 3000, o que nao faz sentido
    pro worker — desligado no servico, senao ele ficaria eternamente
    "unhealthy" por nao responder algo que nem deveria responder;
  * config/recurring.yml existe vazio so pra o worker parar de avisar que
    nao encontrou o arquivo, o que viraria ruido permanente no log;
  * as duas migrations do Solid Queue sao schema da gem copiado, nao
    codigo nosso: entram na exclusao do RuboCop pelo mesmo criterio ja
    aplicado ao db/schema.rb. Reformata-las faria o proximo
    `solid_queue:update` virar conflito.

O Gemfile.lock ganhou a plataforma x86_64-linux, efeito de resolver as
gems dentro do container Linux. E uma melhoria: o lockfile passa a
descrever a plataforma onde a aplicacao de fato roda, e o
`bundle lock --add-platform` do Dockerfile continua ali como rede de
seguranca.

Verificado com o compose de verdade, e nao so com specs: job enfileirado
pelo web com o worker PARADO sobreviveu a um restart do web e foi
executado quando o worker voltou — exatamente o que o :async nao fazia.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Hirley
Hirley merged commit 87e07a9 into main Sep 1, 2026
3 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in @Hirley's task_keeper_api Sep 1, 2026
Hirley added a commit that referenced this pull request Sep 1, 2026
O smoke test do commit anterior falhou na primeira execução com
"container task_keeper_api-worker-1 exited (126)" — 126 é "comando
encontrado, mas não executável".

bin/jobs entrou no repositório como 100644, enquanto todos os outros
scripts de bin/ são 100755. O arquivo foi criado no Windows na #77, e o
sistema de arquivos de lá não tem bit de execução, então o Git registrou
o modo sem ele. Um `docker build` nesta máquina não percebe: o contexto
vem do disco, onde tudo parece executável — foi por isso que a
verificação da #77 passou aqui. Um checkout num runner Linux respeita o
modo do índice, e aí o worker não sobe.

Ou seja: a imagem publicada em main a partir da #87 builda perfeitamente
e não tem worker. Como o web responde normalmente e a falha é do outro
processo, o sintoma seria "os jobs não saem da fila" — exatamente o
modo de falha silencioso que a #77 veio eliminar.

Duas camadas, mesmo critério já usado para o CRLF:

  * `git update-index --chmod=+x bin/jobs` corrige a causa, no índice;
  * `chmod +x bin/*` no Dockerfile, ao lado do `sed` que normaliza LF,
    protege a imagem do próximo script criado no Windows.

A armadilha foi para o CLAUDE.md, junto com outra que apareceu na
verificação: exportar MSYS_NO_PATHCONV=1 no shell inteiro faz o curl do
Git Bash falhar com "(23)" em `-o /dev/null`, o que parece erro da
aplicação e não é.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Fila de jobs em memória: trabalho se perde em restart ou deploy

1 participant