"Pega trabalho, processa, repete." O loop de um Worker Service parece a coisa mais simples do mundo — até ele ir para produção e queimar 100% de CPU sem ter nada para fazer, ou cair inteiro porque um job lançou uma exceção. O loop trivial é uma ilusão.
Neste post você vai percorrer as três armadilhas clássicas ao projetar esse loop — busy waiting, falhas mal tratadas e estado de job inconsistente — e ver a correção idiomática de cada uma. Spoiler: o lugar do try-catch e o lugar do decremento do contador fazem toda a diferença.
O ponto de partida: o padrão "recuperar → processar → repetir"
Quase todo worker segue o mesmo esqueleto. Ele recupera algum trabalho, processa, e repete:
while (!stoppingToken.IsCancellationRequested)
{
var job = await jobProcessor.GetNextJob();
await jobProcessor.ProcessJob(job);
await Task.Delay(loopInterval, stoppingToken);
}
Isso é frequentemente descrito como a fundação de qualquer sistema de processamento em background. Mas, sozinho, não é suficiente para produção. Vamos ver onde ele quebra.
Armadilha 1: busy waiting queimando CPU à toa
O primeiro problema é o busy waiting: quando não há trabalho disponível, o loop continua girando e consumindo CPU desnecessariamente. Você está pagando por ciclos de processador para... processar null.
A correção mais simples é só processar quando realmente existe um job:
var job = await jobProcessor.GetNextJob();
if (job is not null)
{
await jobProcessor.ProcessJob(job);
}
await Task.Delay(loopInterval, stoppingToken);
Repare: não estamos introduzindo um delay novo — já temos o Task.Delay do fim do loop. Estamos apenas evitando processar à toa quando não há nada. Sem job, o worker dá sua soneca habitual e volta a checar.
A versão melhor: verifique quantos jobs existem
Se você consegue saber quantos jobs estão pendentes, dá para ser mais esperto e processá-los em lote — garantindo que só trabalha quando há trabalho:
var pendingJobs = await jobProcessor.GetTotalPendingJobs();
if (pendingJobs > 0)
{
var job = await jobProcessor.GetNextJob();
// processa...
}
Contar jobs costuma ser uma operação barata (o tamanho de uma coleção ou de uma fila, por exemplo), então o overhead é baixo.
Mas atenção ao contexto — essa é a nuance que quase ninguém comenta:
- Múltiplos workers compartilhando a fila: use uma checagem condicional simples. Se há 42 jobs e você pega um, não sabe quantos sobraram (outro worker pode ter pegado) — você teria que verificar de novo a cada vez.
- Você é o único worker (a fila vive dentro do próprio serviço): aí não há risco da contagem mudar por baixo dos panos. Você pode processar em batch até esgotar:
var pendingJobs = await jobProcessor.GetTotalPendingJobs();
while (pendingJobs > 0)
{
var job = await jobProcessor.GetNextJob();
await jobProcessor.ProcessJob(job);
pendingJobs--;
}
await Task.Delay(loopInterval, stoppingToken);
A ideia: você acorda, processa os 42 jobs, dorme por dois segundos, volta e checa se surgiu mais alguma coisa. Esse modelo te deixa acelerar quando há trabalho acumulado — mas só é seguro se você for o único consumidor daquela fila. Com vários serviços competindo pelo mesmo conjunto centralizado de jobs, não use essa abordagem.
Armadilha 2: uma falha que derruba o worker inteiro
Aqui está a regra que você nunca pode violar: uma única falha no processamento jamais pode derrubar o worker inteiro.
Se um job lança uma exceção e você não trata, o loop morre e o processo cai — junto com todos os outros jobs que processariam normalmente. A solução básica é um try-catch ao redor do processamento específico, registrando a exceção:
while (pendingJobs > 0)
{
var job = await jobProcessor.GetNextJob();
try
{
await jobProcessor.ProcessJob(job);
}
catch (Exception ex)
{
logger.LogError(ex, "Falha ao processar o job");
}
pendingJobs--; // FORA do try — sempre decrementa
}
Como não há UI em um worker, o log é a sua única fonte de verdade. Capturar a exceção e logá-la é o que te permite ver, depois, que um job específico falhou — enquanto o resto continua rodando.
O detalhe fatal: onde fica o decremento
Olhe de novo para o exemplo acima. O pendingJobs-- está fora do try. Isso é intencional e crítico.
Se você colocar o decremento dentro do try, e o job sempre falha, você nunca decrementa — e o loop fica preso para sempre com a mesma contagem, ou o contador sai de sincronia. Por isso: o try envolve só o trabalho que você quer proteger contra falha; o controle do loop fica de fora. Seja cuidadoso com essa fronteira.
Armadilha 3: estado de job inconsistente
A terceira armadilha é mais sutil: conforme processa, o worker precisa rastrear o estado do job — pendente, em progresso, completo, falho.
O padrão geral é marcar o job ao longo do ciclo de vida:
var job = await jobProcessor.GetNextJob();
try
{
await jobProcessor.MarkJobInProgress(job);
await jobProcessor.ProcessJob(job);
await jobProcessor.MarkJobComplete(job);
}
catch (Exception ex)
{
logger.LogError(ex, "Falha ao processar o job");
await jobProcessor.MarkJobFailed(job);
}
A ideia é simples: recuperou o job → marca como em progresso → processa → marca como completo. Se falhar, marca como falho no catch, mantendo o estado consistente. Você precisa ser bem cuidadoso ao atualizar esse estado, justamente para que uma falha não deixe um job pendurado em "em progresso" para sempre.
Não esqueça: respeite sempre o cancellation token
Por mais que pareça repetitivo, vale insistir: sempre respeite o CancellationToken. Poder sinalizar que algo foi cancelado é uma prática crucial em qualquer worker. Repare que ele aparece tanto na condição do while quanto no Task.Delay dos exemplos — é assim que o worker desliga de forma graciosa quando a aplicação está parando.
Conclusão
O loop "pega, processa, repete" é enganosamente simples. Em produção, ele exige três cuidados: não queimar CPU em busy waiting, isolar falhas para que um job ruim não derrube o worker e rastrear o estado do job de forma consistente — tudo isso respeitando o cancellation token. A mentalidade certa: um worker não é "só mais um loop em background", é um componente de sistema de longa duração que precisa se comportar de forma previsível.
Revise o loop do seu worker hoje: o try-catch está ao redor só do processamento? O decremento está fora dele? Se respondeu "não" para alguma, você já encontrou a próxima refatoração. No próximo post, vamos tirar o worker do localhost e colocá-lo para rodar em produção.

Top comments (0)