Files
base/docs/cron-to-tasks.md
zeekay 1c40839ccb refactor(cron): route every scheduled-job path through tools/tasks
tools/cron is now a thin alias over tools/tasks. The legacy ticker +
crontab-matching implementation is deleted; the public surface (New,
Add/MustAdd, Remove, RemoveAll, Jobs, HasJob, Total, Start, Stop,
SetInterval, SetTimezone, HasStarted) is preserved so no callsite
changes. app.Cron() and app.Tasks() share a single *tasks.Client, giving
one obvious way to schedule work.

- tools/tasks/client.go gains the primitives the cron shim needs
  (Remove, RemoveAll, HasJob, Total, Schedules, Run, PauseAll, ResumeAll,
  ActiveTickerCount) plus a richer internal scheduleEntry so remote vs
  local ticking are distinguishable.
- cron.Cron.Stop() now pauses local tickers while preserving the
  registry, matching the legacy semantics expected by ResetBootstrapState
  and the admin API.
- core/base.go builds app.cron via cron.NewFromTasks(app.tasks) so both
  accessors feed the same state.
- apis/cron.go keeps the /crons list + run-now behaviour; lookup now
  uses HasJob() for an O(1) precheck.

Tests rewritten around the shim (TestCronDelegatesToTasks,
TestCronAppFacadeLocal, TestCronAppFacadeRemoteFallback,
TestCronNewFromTasksShared). Legacy 1-minute-tick assertions
(TestCronStartStop) are SKIPped with a pointer to their replacement;
crontab parser tests (tools/cron/schedule_test.go) still pass — the
parser stays as a validation primitive for settings_model.go.

docs/cron-to-tasks.md covers the migration path for services.
2026-04-22 15:14:45 -07:00

2.2 KiB

Cron → Tasks migration

tools/cron is now a thin shim over tools/tasks. Every scheduled job registered through app.Cron() runs via a single *tasks.Client — durable Temporal workflows when TASKS_URL is set, local goroutine tickers otherwise.

What didn't change

  • app.Cron().Add(id, expr, fn) keeps its signature. Existing callers compile and run without edits. Services like BD, ATS, TA, etc. need no code changes.
  • cron.NewSchedule(expr) is still the cron-expression validator used by core/settings_model.go for Backups.Cron validation.
  • GET /crons and POST /crons/{id} admin endpoints keep the same JSON shape ([{"id": "...", "expression": "..."}]) and run-now semantics.

What changed

  • app.Cron() and app.Tasks() share one underlying *tasks.Client. Jobs registered through either surface are visible to both.
  • There is no global 1-minute ticker any more — every schedule has its own goroutine (local mode) or Temporal schedule (durable mode). Sub-minute expressions work natively (e.g. "30s", "*/30 * * * * *").
  • cron.Cron.Stop() now pauses tickers without dropping the registry. cron.Cron.Start() resumes them. This matches the previous semantics from the ticker-based implementation.
  • cron.Cron.SetInterval() is a no-op. The global tick cadence no longer exists — each schedule's duration/expression is authoritative.
  • cron.Cron.SetTimezone() is accepted but only the expression-evaluation timezone is propagated; durable schedules run in the tasks server's timezone (UTC by default).

Call-site rule going forward

Use app.Tasks() for new code:

app.Tasks().Add("settlement", "30s", fn)          // recurring, duration
app.Tasks().Add("daily-cleanup", "0 3 * * *", fn) // recurring, cron
app.Tasks().Now("webhook.deliver", payload)        // one-shot

app.Cron() stays forever as the alias — no "deprecated in v2" churn.

Environment switches

  • TASKS_URL unset → pure local goroutine tickers (dev/test default).
  • TASKS_URL=http://tasks.hanzo.ai → durable schedules on Hanzo Tasks.
  • TASKS_ZAP=host:port → ZAP binary transport preferred over HTTP.

If the remote server is unreachable, schedules transparently fall back to local tickers so the app never stops firing jobs.