Por que os drivers importam
Em um bot de processo único, cada job agendado roda exatamente uma vez porque existe apenas uma instância do scheduler. Quando você escala para vários shards ou processos, o mesmo job cron pode disparar ao mesmo tempo em cada instância, causando execuções duplicadas. O sistema de drivers resolve isso ao introduzir um mecanismo de lock que garante que apenas uma instância adquira o direito de executar determinado job a cada disparo.
Os drivers são plugáveis. Você configura o driver uma vez na config de schedule, e o framework cuida do restante de forma transparente. Os métodos dos seus jobs não precisam saber qual driver está ativo. Isso significa que você pode desenvolver localmente com MemoryDriver e trocar para RedisScheduleDriver em produção sem mudar nenhum código de serviço.
O modelo de lock é otimista. Quando um job dispara, o driver tenta adquirir um lock usando o nome do job como chave. Se o lock for adquirido, o job roda. Se outro processo já estiver com o lock, o job é silenciosamente ignorado naquela instância. O lock tem um TTL configurável que é liberado automaticamente após a janela esperada de execução.
Drivers disponíveis
| Driver | Package | Mecanismo de lock | Melhor uso |
|---|---|---|---|
| MemoryDriver | @spraxium/schedule | Sempre adquire, sem operação real | Bots de processo único e desenvolvimento local |
| RedisScheduleDriver | @spraxium/schedule | Redis SET NX + Lua release | Produção com múltiplos shards e múltiplos processos |
Configurando drivers
O MemoryDriver padrão não exige nenhuma configuração. Ele sempre adquire o lock, então cada job roda sempre que dispara. Esse é o comportamento correto para bots de processo único.
Em ambientes distribuídos, troque para RedisScheduleDriver, passando as opções de conexão do Redis e um lock TTL opcional. O driver usa SET key NX EX ttl para adquirir locks de forma atômica e um script Lua para liberá-los com segurança, sem condições de corrida.
import { defineSchedule } from '@spraxium/schedule';
export default defineSchedule({
timezone: 'UTC',
});API de ScheduleService
O ScheduleService é um provider injetável que oferece controle total, em runtime, sobre os jobs registrados. Injete-o em qualquer serviço, command handler ou controller para pausar, retomar, destruir ou disparar jobs manualmente pelo nome. O serviço é exportado pelo ScheduleModule global, então fica disponível em todos os módulos sem imports explícitos.
Todo método recebe o name do job, conforme definido nas opções do decorator. Se você não definir um nome explícito, o framework usará um nome gerado a partir da classe e do método. Nomes explícitos são fortemente recomendados para qualquer job que você pretenda administrar em runtime.
| Método | Retorno | Descrição |
|---|---|---|
| pause(name) | Sem valor de retorno | Interrompe um job em execução para que ele possa ser retomado depois |
| resume(name) | Sem valor de retorno | Retoma um job pausado anteriormente |
| destroy(name) | Sem valor de retorno | Remove permanentemente um job e seu timer |
| get(name) | JobStatus ou null | Recupera um job registrado pelo nome |
| getAll() | Lista de entradas JobStatus | Lista todos os jobs registrados com seu estado |
| runNow(name) | Promise sem valor | Executa imediatamente um job fora do cronograma normal |
Usando ScheduleService em um command
Um caso de uso prático para ScheduleService é criar comandos administrativos que permitem a donos de guild ou administradores do bot controlar tarefas em segundo plano sem reiniciar o bot. O exemplo abaixo mostra um prefix command handler que pausa ou retoma um job pelo nome. O mesmo padrão funciona com slash commands ou endpoints de dashboard.
import { Ctx, PrefixArg, PrefixCommandHandler } from '@spraxium/common';
import { ScheduleService } from '@spraxium/schedule';
import type { Message } from 'discord.js';
import { ScheduleAdminCommand } from '../commands/schedule-admin.command';
@PrefixCommandHandler(ScheduleAdminCommand)
export class ScheduleAdminHandler {
constructor(private readonly scheduleService: ScheduleService) {}
async handle(
@Ctx() message: Message,
@PrefixArg('action') action: string,
@PrefixArg('jobName') jobName: string,
): Promise<void> {
const job = this.scheduleService.get(jobName);
if (!job) {
await message.reply(`Job \`${jobName}\` not found.`);
return;
}
if (action === 'pause') {
this.scheduleService.pause(jobName);
await message.reply(`Job \`${jobName}\` paused.`);
} else if (action === 'resume') {
this.scheduleService.resume(jobName);
await message.reply(`Job \`${jobName}\` resumed.`);
} else if (action === 'run') {
await this.scheduleService.runNow(jobName);
await message.reply(`Job \`${jobName}\` triggered manually.`);
}
}
}