Agendador de Tarefas em Zig — Tutorial Passo a Passo

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

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

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.