Pular para o conteúdo

Agendador cron

Ler uma expressão cron, dizer quando ela roda — e recusar a que nunca roda.

Cron é uma linguagem pequena que quase todo sistema usa e quase ninguém lê direito: */15 9-18 * * 1-5 é “a cada 15 minutos, em horário comercial, nos dias úteis”. Este projeto escreve o parser, expande cada campo para os valores concretos e responde as próximas execuções.

PeçaO que ela exercita
parser de campo*, */n, a-b, a,b,c
faixas por campoo minuto vai de 0 a 59; o dia da semana, de 0 a 6
mensagem com o campo“hora 25 fora de 0-23”
as próximas execuçõeso que o usuário quer ver antes de salvar

Estrutura#

text
agendador/
  src/
    cron.df        ler, casa, proximas
    tarefas.df     o catalogo de jobs
  tests/
forge.toml
[project]
name = "agendador"
version = "0.1.0"
description = "Agendador com cron"
entry = "src/main.df"
dataforge = ">=1.1"

[dependencies]

[scripts]
start = "run src/main.df"
test = "test tests/"

O núcleo#

Este bloco roda sozinho — copie para um arquivo e rode dataforge run. Ele termina com assert, e é assim que esta página é conferida a cada build.

src/cron.df
steady CAMPOS := [["minuto", 0, 59], ["hora", 0, 23], ["dia", 1, 31], ["mes", 1, 12], ["semana", 0, 6]]

action expandir(texto, nome, menor, maior):
    valores := []
    cycle parte in texto.split(","):
        passo := 1
        faixa := parte
        given "/" in parte:
            faixa, p := parte.split("/")
            passo := int(p)
            given passo smaller_eq 0:
                trigger $"{nome}: o passo precisa ser positivo"
        given faixa is "*":
            a := menor
            b := maior
        orif "-" in faixa:
            ia, ib := faixa.split("-")
            a := int(ia)
            b := int(ib)
        otherwise:
            a := int(faixa)
            b := a
        given a smaller menor or b bigger maior or a bigger b:
            trigger $"{nome} {faixa} fora de {menor}-{maior}"
        cycle v from a to b step passo:
            valores.append(v)
    yield sorted(unique(valores))

action ler(expressao):
    partes := expressao.trim().split(" ") >> sift p: p is not ""
    given len(partes) is not 5:
        trigger $"uma expressao cron tem 5 campos, esta tem {len(partes)}"
    regra := {}
    cycle i in range(0, 5):
        c := CAMPOS[i]
        regra[c[0]] := expandir(partes[i], c[0], c[1], c[2])
    yield regra

action casa(regra, m, h, dia, mes, semana):
    yield m in regra["minuto"] and h in regra["hora"] and dia in regra["dia"] and mes in regra["mes"] and semana in regra["semana"]

r := ler("*/15 9-18 * * 1-5")
assert r["minuto"] is [0, 15, 30, 45]
assert len(r["hora"]) is 10
assert casa(r, 30, 10, 21, 9, 1)
assert not casa(r, 30, 10, 20, 9, 0)

monitor:
    ler("0 25 * * *")
    assert no
handle Error as e:
    out e.message
    assert "hora 25" in e.message

O teste#

No projeto, a regra mora em src/ e o teste a importa pelo caminho relativo — dataforge test tests/ descobre o arquivo sozinho.

tests/nucleo_test.df
adopt ../src/cron as C

crucible "cron":
    trial "lista com virgula":
        expect C.ler("0,30 * * * *")["minuto"] is [0, 30]

    trial "quatro campos e recusado":
        expect(lambda => C.ler("* * * *")).to_raise()

As decisões#

DecisãoSem ela
expandir para os valores concretoscada pergunta “casa?” reinterpreta o texto
a faixa de cada campo0 25 * * * é aceito e nunca roda
a mensagem nomeia o campo“expressão inválida”, e a pessoa conta os espaços
unique + sorted1,1,2 roda duas vezes no minuto 1

Para ir além#

  • Rodar no horário, numa thread só: Arcane.Laco.
  • No banco, com pg_cron: Em produção.
  • O dia do mês e da semana juntos é um ou no cron clássico — decida e documente.

Volte para todos os tipos de projeto.