tako docs guide reference api
GitHub

tako.Cron — scheduled handlers

A cron is a func(Ctx) error run on a schedule, declared like everything else — a ticker locally, a Cloud Scheduler job in the cloud (T2), one declaration for both.

Declare

var Cleanup = tako.Cron("every 24h", cleanupOrphans, tako.CronConfig{Singleflight: true})

Cleanup is a tako.Handler — the value you hand to CronMissed, CronFailed and app.AdvanceCron.

The schedule must be a string constant (analyzer-enforced), in either form: "every <Go duration>" (every 10m, every 6h, every 24h) or a 5-field unix-cron expression ("0 3 * * *", "0 9-17 * * 1-5"). Both must map onto Cloud Scheduler's grammar — an every interval that unix-cron cannot express (every 7m, every 90s) is refused at analysis time with the mapping rule. The analyzer, the runtime, and the provisioner share one parser, so a schedule that analyzes runs locally AND provisions.

CronConfig.Singleflight wraps the handler in the §4.4 lease (lock.md), so overlapping or double-fired runs collapse to one execution — across all instances, not just one process. The wrap happens at declaration, so every trigger — dev ticker, AdvanceCron, Cloud Scheduler — inherits it. CronConfig.Profile and CronConfig.Footprint (footprint.md) shape the HOSTING service, the escape hatch a cron-only package would otherwise lack.

Use

The handler is plain: do the work, return nil or an error. Cron handlers run inside the package's app service. Gallery's daily cleanup deletes never-completed uploads (examples/gallery/app.go); failures are narrated and the next run stays on schedule — there is no retry of a failed run, the schedule itself is the retry.

Timezone and deadline

var Cleanup = tako.Cron("0 9 * * *", cleanupOrphans,
	tako.CronConfig{
		Singleflight: true,
		Timezone:     "America/Mexico_City",
		Deadline:     10 * time.Minute,
	})

Schedules run in UTC unless CronConfig.Timezone says otherwise — nine o'clock means nine o'clock somewhere, and the zone follows daylight saving. The name is an IANA zone, checked against the tz database at analysis time, so a misspelling is a build error rather than a 3am surprise.

CronConfig.Deadline caps one run (default 180s, range 15s–30m) before Cloud Scheduler abandons the attempt. There is no retry config: the schedule IS the retry policy for an idempotent cron. Work that needs longer than 30 minutes wants a job.

Local

tako dev runs each cron on a real ticker and narrates it:

⏰ cron Cleanup: every 24h
⏰ cron Cleanup ok (312ms)
⏰ cron Cleanup failed: <err> — next run on schedule

Runs are serialized per cron in-process — a tick landing mid-run is skipped with a ⏰ … run still in progress — tick skipped line. With CronConfig.Singleflight the collapse is cross-instance and lease-based, with its own narration — the feature working, never an error:

↻ cron cleanupOrphans skipped — an overlapping run holds the lease

Cloud

Every cron provisions a Cloud Scheduler job (+ create scheduler job <svc>-cron-<name>) firing POST /_tako/api/cron/<name> on the service with an OIDC token minted as the service's own SA. The route trusts nobody: it verifies the Google-signed token's audience (this service) and identity (that SA) — injected at deploy as TAKO_CRON_AUDIENCE/TAKO_CRON_INVOKER — so the route is safe even on a public service, and locally it just refuses strangers (the ticker drives dev). In-process tickers stay off in the cloud: one schedule, not one per instance. A failed run answers 500 and the schedule itself is the retry; Scheduler's attempt deadline (180s) bounds a run — put longer work behind a job the cron triggers. Scheduler bills per job at trivial rates; destroy deletes the jobs with everything else.

Test it

Immediately after a fresh deploy, a cron that fires can get a 403 from Cloud Run's front door while the Scheduler job's invoker grant propagates — a minute or two, self-healing, and the next scheduled run is the recovery. See the cloud story.

The cron's identity is its variable. var Cleanup = tako.Cron(...) deploys as the Scheduler job for /_tako/api/cron/Cleanup, leases as cron-Cleanup under CronConfig.Singleflight, and is fired in a test as app.AdvanceCron(Cleanup) — one name, from one place. Renaming the variable renames the cron; renaming its function does not.

Upgrading from a tako that keyed the singleflight lease on the handler function's name: during the one deploy that rolls the change out, old and new instances hold different keys, so a single overlapping run can fire twice. Afterwards it is stable.

Tests own scheduled time: app.AdvanceCron(Cleanup) fires one run synchronously, by handler function name. Singleflight is testable by occupying the cron's lease — named cron-<handler> — and firing:

go func() {
    done <- tako.Lock("cron-cleanupOrphans").Do(ctx, func(tako.Ctx) error {
        close(holding)
        <-release
        return nil
    })
}()
<-holding
app.AdvanceCron(Cleanup) // collapses into the held run

(examples/gallery/coordination_test.go)

Errors you'll meet

Not yet