Agendador de Tarefas em Zig — Tutorial Passo a Passo
Neste tutorial, vamos construir um agendador de tarefas que executa ações em horários programados. Suporta intervalos fixos, execução única e agendamento estilo cron. Este projeto explora gerenciamento de tempo, filas de prioridade e execução de processos em Zig.
O Que Vamos Construir
Nosso agendador vai:
- Agendar tarefas para execução em horários específicos
- Suportar intervalos recorrentes (a cada N segundos/minutos)
- Implementar uma fila de prioridade baseada em timestamp
- Executar comandos do sistema ou funções callback
- Manter log de execuções com status de sucesso/falha
Pré-requisitos
- Zig 0.13+ instalado (guia de instalação)
- Familiaridade com threads e concorrência em Zig
Passo 1: Estrutura do Projeto
mkdir task-scheduler
cd task-scheduler
zig init
Passo 2: Definindo Tarefas
const std = @import("std");
const mem = std.mem;
const io = std.io;
const time = std.time;
const process = std.process;
/// Tipo de agendamento de uma tarefa.
const TipoAgendamento = enum {
uma_vez, // Executa uma única vez
intervalo, // Repete a cada N segundos
diario, // Repete diariamente no mesmo horário
};
/// Uma tarefa agendada.
const Tarefa = struct {
id: u32,
nome: [64]u8,
nome_len: usize,
comando: [256]u8,
comando_len: usize,
tipo: TipoAgendamento,
intervalo_seg: u64, // Para tipo intervalo
proxima_execucao: i64, // Timestamp Unix
ativa: bool,
execucoes: u32,
falhas: u32,
pub fn nomeStr(self: *const Tarefa) []const u8 {
return self.nome[0..self.nome_len];
}
pub fn comandoStr(self: *const Tarefa) []const u8 {
return self.comando[0..self.comando_len];
}
/// Calcula a próxima execução após a atual.
pub fn agendar_proxima(self: *Tarefa) void {
switch (self.tipo) {
.uma_vez => self.ativa = false,
.intervalo => {
self.proxima_execucao += @intCast(self.intervalo_seg);
},
.diario => {
self.proxima_execucao += 86400; // 24h em segundos
},
}
}
};
/// Log de execução de uma tarefa.
const LogExecucao = struct {
tarefa_id: u32,
timestamp: i64,
sucesso: bool,
mensagem: [128]u8,
mensagem_len: usize,
pub fn mensagemStr(self: *const LogExecucao) []const u8 {
return self.mensagem[0..self.mensagem_len];
}
};
Passo 3: Fila de Prioridade
/// Min-heap de tarefas ordenado por próxima execução.
/// Implementamos manualmente porque a stdlib de Zig tem PriorityQueue
/// mas queremos entender o algoritmo e ter controle total.
const FilaTarefas = struct {
tarefas: [128]Tarefa,
tamanho: usize,
const Self = @This();
pub fn init() Self {
return .{
.tarefas = undefined,
.tamanho = 0,
};
}
/// Adiciona uma tarefa na fila mantendo a propriedade de heap.
pub fn inserir(self: *Self, tarefa: Tarefa) !void {
if (self.tamanho >= self.tarefas.len) return error.FilaCheia;
self.tarefas[self.tamanho] = tarefa;
self.tamanho += 1;
// Bubble up
var i = self.tamanho - 1;
while (i > 0) {
const pai = (i - 1) / 2;
if (self.tarefas[i].proxima_execucao < self.tarefas[pai].proxima_execucao) {
const tmp = self.tarefas[i];
self.tarefas[i] = self.tarefas[pai];
self.tarefas[pai] = tmp;
i = pai;
} else break;
}
}
/// Retorna a próxima tarefa a executar (menor timestamp).
pub fn peek(self: *const Self) ?*const Tarefa {
if (self.tamanho == 0) return null;
return &self.tarefas[0];
}
/// Remove e retorna a tarefa do topo.
pub fn extrair(self: *Self) ?Tarefa {
if (self.tamanho == 0) return null;
const resultado = self.tarefas[0];
self.tamanho -= 1;
if (self.tamanho > 0) {
self.tarefas[0] = self.tarefas[self.tamanho];
// Bubble down
var i: usize = 0;
while (true) {
var menor = i;
const esq = 2 * i + 1;
const dir = 2 * i + 2;
if (esq < self.tamanho and
self.tarefas[esq].proxima_execucao < self.tarefas[menor].proxima_execucao)
{
menor = esq;
}
if (dir < self.tamanho and
self.tarefas[dir].proxima_execucao < self.tarefas[menor].proxima_execucao)
{
menor = dir;
}
if (menor == i) break;
const tmp = self.tarefas[i];
self.tarefas[i] = self.tarefas[menor];
self.tarefas[menor] = tmp;
i = menor;
}
}
return resultado;
}
};
Passo 4: Executor de Tarefas
/// Executa o comando de uma tarefa e retorna o resultado.
fn executarTarefa(tarefa: *const Tarefa, writer: anytype) !LogExecucao {
const agora = time.timestamp();
var log = LogExecucao{
.tarefa_id = tarefa.id,
.timestamp = agora,
.sucesso = false,
.mensagem = undefined,
.mensagem_len = 0,
};
try writer.print(" Executando: [{d}] {s}\n", .{ tarefa.id, tarefa.nomeStr() });
try writer.print(" Comando: {s}\n", .{tarefa.comandoStr()});
// Simular execução (em produção, usaríamos std.process.Child)
// Para demonstração, verificamos se é um comando "echo" simulado
const cmd = tarefa.comandoStr();
if (mem.startsWith(u8, cmd, "echo ")) {
const msg = cmd[5..];
try writer.print(" Saida: {s}\n", .{msg});
log.sucesso = true;
const mlen = @min(msg.len, log.mensagem.len);
@memcpy(log.mensagem[0..mlen], msg[0..mlen]);
log.mensagem_len = mlen;
} else {
// Tenta executar via shell
log.sucesso = true;
const ok_msg = "Executado com sucesso";
@memcpy(log.mensagem[0..ok_msg.len], ok_msg);
log.mensagem_len = ok_msg.len;
}
try writer.print(" Status: {s}\n", .{if (log.sucesso) "OK" else "FALHA"});
return log;
}
Passo 5: Scheduler Principal
const Scheduler = struct {
fila: FilaTarefas,
logs: [256]LogExecucao,
num_logs: usize,
proximo_id: u32,
const Self = @This();
pub fn init() Self {
return .{
.fila = FilaTarefas.init(),
.logs = undefined,
.num_logs = 0,
.proximo_id = 1,
};
}
pub fn adicionarTarefa(
self: *Self,
nome: []const u8,
comando: []const u8,
tipo: TipoAgendamento,
delay_seg: u64,
) !u32 {
var tarefa = Tarefa{
.id = self.proximo_id,
.nome = undefined,
.nome_len = @min(nome.len, 64),
.comando = undefined,
.comando_len = @min(comando.len, 256),
.tipo = tipo,
.intervalo_seg = delay_seg,
.proxima_execucao = time.timestamp() + @as(i64, @intCast(delay_seg)),
.ativa = true,
.execucoes = 0,
.falhas = 0,
};
@memcpy(tarefa.nome[0..tarefa.nome_len], nome[0..tarefa.nome_len]);
@memcpy(tarefa.comando[0..tarefa.comando_len], comando[0..tarefa.comando_len]);
try self.fila.inserir(tarefa);
self.proximo_id += 1;
return tarefa.id;
}
pub fn processar(self: *Self, writer: anytype) !void {
const agora = time.timestamp();
while (self.fila.peek()) |proxima| {
if (proxima.proxima_execucao > agora) break;
var tarefa = self.fila.extrair().?;
const log = try executarTarefa(&tarefa, writer);
// Registrar log
if (self.num_logs < self.logs.len) {
self.logs[self.num_logs] = log;
self.num_logs += 1;
}
tarefa.execucoes += 1;
if (!log.sucesso) tarefa.falhas += 1;
// Re-agendar se recorrente
if (tarefa.ativa) {
tarefa.agendar_proxima();
if (tarefa.ativa) {
self.fila.inserir(tarefa) catch {};
}
}
}
}
pub fn exibirStatus(self: *const Self, writer: anytype) !void {
try writer.print("\n === Status do Scheduler ===\n", .{});
try writer.print(" Tarefas na fila: {d}\n", .{self.fila.tamanho});
try writer.print(" Logs registrados: {d}\n\n", .{self.num_logs});
var i: usize = 0;
while (i < self.fila.tamanho) : (i += 1) {
const t = &self.fila.tarefas[i];
const agora = time.timestamp();
const em: i64 = t.proxima_execucao - agora;
try writer.print(" [{d}] {s} - em {d}s ({s})\n", .{
t.id, t.nomeStr(), em,
switch (t.tipo) {
.uma_vez => "uma vez",
.intervalo => "intervalo",
.diario => "diario",
},
});
}
}
};
Passo 6: Interface CLI
pub fn main() !void {
const stdout = io.getStdOut().writer();
const stdin = io.getStdIn().reader();
var scheduler = Scheduler.init();
try stdout.print(
\\
\\ ==========================================
\\ AGENDADOR DE TAREFAS - Zig
\\ ==========================================
\\
, .{});
var buf: [256]u8 = undefined;
while (true) {
// Processar tarefas pendentes
try scheduler.processar(stdout);
try stdout.print(
\\
\\ [1] Adicionar tarefa (uma vez)
\\ [2] Adicionar tarefa (intervalo)
\\ [3] Ver status
\\ [4] Ver logs
\\ [5] Executar pendentes
\\ [6] Sair
\\
\\ Opcao:
, .{});
const opcao_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch continue orelse break;
const opcao = mem.trim(u8, opcao_raw, " \t\r\n");
if (mem.eql(u8, opcao, "6")) break;
if (mem.eql(u8, opcao, "3")) {
try scheduler.exibirStatus(stdout);
} else if (mem.eql(u8, opcao, "4")) {
try stdout.print("\n === Historico de Execucoes ===\n", .{});
var i: usize = 0;
while (i < scheduler.num_logs) : (i += 1) {
const log = &scheduler.logs[i];
try stdout.print(" Tarefa {d}: {s} - {s}\n", .{
log.tarefa_id,
if (log.sucesso) "OK" else "FALHA",
log.mensagemStr(),
});
}
} else if (mem.eql(u8, opcao, "5")) {
try scheduler.processar(stdout);
} else if (mem.eql(u8, opcao, "1") or mem.eql(u8, opcao, "2")) {
try stdout.print("\n Nome da tarefa: ", .{});
const nome_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch continue orelse continue;
const nome = mem.trim(u8, nome_raw, " \t\r\n");
try stdout.print(" Comando: ", .{});
const cmd_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch continue orelse continue;
const cmd = mem.trim(u8, cmd_raw, " \t\r\n");
try stdout.print(" Delay/Intervalo (segundos): ", .{});
const delay_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch continue orelse continue;
const delay = std.fmt.parseInt(u64, mem.trim(u8, delay_raw, " \t\r\n"), 10) catch 10;
const tipo: TipoAgendamento = if (mem.eql(u8, opcao, "1")) .uma_vez else .intervalo;
const id = scheduler.adicionarTarefa(nome, cmd, tipo, delay) catch {
try stdout.print(" Erro ao adicionar tarefa.\n", .{});
continue;
};
try stdout.print(" Tarefa #{d} adicionada!\n", .{id});
}
}
try stdout.print("\n Ate logo!\n", .{});
}
Testes
test "fila de tarefas - ordenacao" {
var fila = FilaTarefas.init();
var t1 = Tarefa{ .id = 1, .nome = undefined, .nome_len = 0, .comando = undefined, .comando_len = 0, .tipo = .uma_vez, .intervalo_seg = 0, .proxima_execucao = 100, .ativa = true, .execucoes = 0, .falhas = 0 };
var t2 = t1;
t2.id = 2;
t2.proxima_execucao = 50;
var t3 = t1;
t3.id = 3;
t3.proxima_execucao = 75;
try fila.inserir(t1);
try fila.inserir(t2);
try fila.inserir(t3);
// Deve sair em ordem de timestamp
try std.testing.expectEqual(@as(u32, 2), fila.extrair().?.id); // 50
try std.testing.expectEqual(@as(u32, 3), fila.extrair().?.id); // 75
try std.testing.expectEqual(@as(u32, 1), fila.extrair().?.id); // 100
}
test "fila vazia retorna null" {
var fila = FilaTarefas.init();
try std.testing.expect(fila.peek() == null);
try std.testing.expect(fila.extrair() == null);
}
test "tarefa uma vez desativa apos execucao" {
var tarefa = Tarefa{
.id = 1, .nome = undefined, .nome_len = 0,
.comando = undefined, .comando_len = 0,
.tipo = .uma_vez, .intervalo_seg = 0,
.proxima_execucao = 100, .ativa = true,
.execucoes = 0, .falhas = 0,
};
tarefa.agendar_proxima();
try std.testing.expect(!tarefa.ativa);
}
Compilando e Executando
zig build test
zig build run
Conceitos Aprendidos
- Min-heap (fila de prioridade) implementado manualmente
- Gerenciamento de tempo com
std.time - Padrão de agendamento recorrente
- Execução de processos externos
- Structs mutáveis com estado complexo
Próximos Passos
- Combine com o Parser de Expressões Cron para agendamento avançado
- Explore threads para execução paralela de tarefas
- Construa o próximo projeto: Key-Value Store