Cron no Magento 2: configuração, jobs customizados e troubleshooting
Boa parte do que mantém uma loja Magento 2 funcionando acontece no cron: indexers em modo schedule, e-mails de pedido enviados de forma assíncrona, geração de sitemap, cotações de moeda, regras de preço de catálogo, limpeza de cache e queue consumers. Quando o cron para, a loja continua servindo páginas, então ninguém percebe por horas ou dias. Aí os preços ficam errados, os e-mails somem e as tabelas de changelog ficam enormes. Este guia explica como o cron funciona no Magento Open Source e no Adobe Commerce 2.4.8, como adicionar seus próprios jobs e como descobrir por que um job não está rodando.
Como o cron do Magento funciona
Existe exatamente uma entrada no crontab do sistema, e ela roda a cada minuto:
* * * * * /usr/bin/php /var/www/html/bin/magento cron:run 2>&1 | grep -v "Ran jobs by schedule" >> /var/www/html/var/log/magento.cron.log
Você pode instalá-la para o usuário atual com bin/magento cron:install e removê-la com bin/magento cron:remove. Use o binário do PHP e o usuário do sistema que rodam a aplicação web, para que as permissões de arquivo em var/ e generated/ continuem consistentes.
Cada cron:run faz duas coisas para cada cron group:
- Gera linhas de agendamento na tabela
cron_schedulepara os próximos minutos, com base na expressão cron de cada job. - Executa as linhas que estão no horário e depois as marca como
success,erroroumissed.
Ou seja, o crontab do sistema só dispara o scheduler do próprio Magento. A agenda de verdade fica no XML e no banco de dados.
Escrevendo um job de cron customizado
Um job é uma classe com um método, declarado em etc/crontab.xml:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Cron:etc/crontab.xsd">
<group id="default">
<job name="acme_feed_export" instance="Acme\Feed\Cron\ExportFeed" method="execute">
<schedule>15 */2 * * *</schedule>
</job>
</group>
</config>
<?php
declare(strict_types=1);
namespace Acme\Feed\Cron;
use Acme\Feed\Model\FeedExporter;
use Psr\Log\LoggerInterface;
class ExportFeed
{
public function __construct(
private readonly FeedExporter $exporter,
private readonly LoggerInterface $logger
) {
}
public function execute(): void
{
try {
$this->exporter->exportAll();
} catch (\Throwable $e) {
$this->logger->error('Feed export failed: ' . $e->getMessage(), ['exception' => $e]);
throw $e;
}
}
}
Relançar a exceção é proposital. Assim o Magento marca a linha do agendamento como error e guarda a mensagem, que é onde você vai olhar primeiro quando algo quebrar. Engolir exceções faz execuções com falha parecerem sucesso.
Mantenha a classe de cron enxuta. Ela deve chamar um service que também funcione a partir de um comando CLI, para você poder rodar a mesma lógica manualmente ao depurar. O guia de comando CLI customizado mostra como embrulhar o mesmo service em um comando.
Agendamentos configuráveis
Em vez de uma expressão fixa, um job pode ler seu agendamento da configuração do sistema com config_path:
<job name="acme_feed_export" instance="Acme\Feed\Cron\ExportFeed" method="execute">
<config_path>acme_feed/general/cron_expr</config_path>
</job>
Adicione um campo no system.xml para esse path e os admins poderão mudar o agendamento sem deploy. Valide o valor, porque uma expressão inválida significa que o job nunca é agendado.
Cron groups
Os jobs pertencem a groups. O core usa default, index e mais alguns adicionados por módulos. Cada group tem suas configurações em etc/cron_groups.xml:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Cron:etc/cron_groups.xsd">
<group id="acme_feed">
<schedule_generate_every>1</schedule_generate_every>
<schedule_ahead_for>4</schedule_ahead_for>
<schedule_lifetime>2</schedule_lifetime>
<history_cleanup_every>10</history_cleanup_every>
<history_success_lifetime>60</history_success_lifetime>
<history_failure_lifetime>4320</history_failure_lifetime>
<use_separate_process>1</use_separate_process>
</group>
</config>
Os valores são em minutos. O schedule_lifetime é o mais importante para troubleshooting: se um job pendente não começar dentro desse número de minutos depois do horário agendado, ele é marcado como missed. O use_separate_process roda o group em um processo PHP próprio, para que um group lento não atrase os outros.
Vale ter um group separado para jobs longos. Uma exportação de feed que leva 20 minutos no group default atrasa tudo que está nesse group, inclusive jobs que enviam e-mails. Dar a ela um group próprio isola o problema. As configurações do group também podem ser alteradas no admin em Stores > Configuration > Advanced > System > Cron (Scheduled Tasks).
Você pode rodar um único group manualmente, o que ajuda durante os testes:
bin/magento cron:run --group=acme_feed
Cron e queue consumers
Muitas funcionalidades assíncronas no 2.4 usam queue consumers: operações bulk de REST, atualizações em massa de atributos pelo grid de produtos, exportação de dados e várias tarefas de inventário. Por padrão, o cron inicia os consumers pelo job consumers_runner. Você controla isso em app/etc/env.php:
'cron_consumers_runner' => [
'cron_run' => true,
'max_messages' => 10000,
'consumers' => [],
],
Um array consumers vazio significa todos os consumers. Em servidores onde os consumers são gerenciados por Supervisor ou systemd, coloque cron_run como false para o cron não iniciar duplicatas. Para trabalho pesado de fila, um consumer de longa duração é melhor do que um job de cron. O artigo sobre consumers do RabbitMQ entra nessa configuração.
Troubleshooting
A maioria dos problemas de cron pode ser diagnosticada pela tabela cron_schedule:
SELECT job_code, status, COUNT(*) AS total, MAX(scheduled_at) AS last_scheduled
FROM cron_schedule
GROUP BY job_code, status
ORDER BY job_code, status;
SELECT job_code, scheduled_at, executed_at, finished_at, messages
FROM cron_schedule
WHERE status = 'error'
ORDER BY scheduled_at DESC
LIMIT 20;
Nenhuma linha
O crontab do sistema não está rodando ou roda com o usuário errado. Confira o crontab -l do usuário web e olhe o var/log/magento.cron.log. Verifique também se o binário do PHP CLI no crontab é da versão certa, já que servidores costumam ter várias.
As linhas ficam pending
Os agendamentos são criados, mas nada os executa. Causas comuns são um job demorado segurando o lock do group, um erro fatal de PHP em um job que mata o processo, ou um schedule_lifetime curto demais para a frequência real do cron. Procure jobs presos em running com um executed_at antigo. Se o processo não existe mais, essas linhas não se recuperam sozinhas.
Muitas linhas missed
O cron rodou tarde demais. O servidor pode estar sobrecarregado, o crontab pode não estar rodando a cada minuto ou outro group está bloqueando. Mover jobs longos para um group próprio com use_separate_process normalmente resolve.
Seu job não está na tabela
O XML pode estar inválido, o módulo desativado, o cache de config desatualizado, ou o valor de config_path vazio ou inválido. Limpe o cache de config e confira se o job aparece depois do próximo cron:run.
Os indexers ficam para trás
Se os indexers estão em modo schedule e o cron não está saudável, as tabelas de changelog continuam crescendo. O bin/magento indexer:status mostra o backlog. Mais sobre isso em o artigo sobre modos de indexer.
Bons hábitos para jobs de cron
- Faça jobs idempotentes. Um job pode rodar duas vezes depois de um restart ou de uma execução manual.
- Processe o trabalho em lotes e pare depois de um limite de tempo, deixando o resto para a próxima execução.
- Registre início, fim e contagens em nível info, e falhas em nível error, em um arquivo de log dedicado.
- Não dependa de contexto de store ou de design a menos que você o configure explicitamente. O cron roda na área
crontab, sem request e sem sessão. - Monitore o cron por fora. Uma checagem simples que avisa quando a linha de sucesso mais recente em
cron_scheduleé mais antiga que alguns minutos pega a maioria das quedas cedo.
FAQ
Como rodo um único job de cron do Magento manualmente?
O Magento Open Source não tem um comando nativo para um job só. Ou você roda o group dele com cron:run --group, ou chama o mesmo service a partir de um comando CLI customizado, ou usa uma ferramenta de terceiros como o n98-magerun2.
Por que meus jobs de cron aparecem como missed?
Eles não começaram dentro de schedule_lifetime minutos do horário agendado. Geralmente o cron não roda a cada minuto, o servidor está sobrecarregado ou um job longo no mesmo group está bloqueando.
Posso desativar um job de cron do core?
Não com uma flag. O jeito usual é sobrescrever o agendamento no crontab.xml do seu módulo com uma expressão que na prática nunca bate, e documentar por que você fez isso.
Os queue consumers devem rodar pelo cron?
Funciona para lojas pequenas. Para filas pesadas, rode os consumers como processos de longa duração no Supervisor ou systemd e coloque cron_run como false no env.php.
Atendo como freelancer em Magento e Adobe Commerce. Se o cron ou os jobs em background não são confiáveis na sua loja, fale comigo.
Artigos relacionados
Indexers do Magento 2: Update on Save vs Update by Schedule
Como funcionam os modos de indexer do Magento 2, por que lojas em produção usam Update by Schedule, como mview e changelogs funcionam e como manter o cron saudável.
Salable quantity errada no Magento 2? Como funcionam as reservations
Por que a salable quantity do Magento 2 difere da quantity, como as inventory reservations funcionam e como achar e corrigir inconsistências com a CLI nativa.
Erros comuns do Magento 2 e como corrigir
Correções para erros clássicos do Magento 2: Area code is not set, Class does not exist, Invalid Form Key, indexers travados, unserialize e 404 em estáticos.
Magento 2 Catalog Price Rules não aplicam? Como funciona a indexação
Como as catalog price rules do Magento 2 são indexadas, por que uma regra não aparece na storefront, o cron diário, timezones e como depurar preços de regra com SQL.