juspay / juspay/invokr

Discussion: standardize CRON jobs on UTC and reconsider the per-job `timezone` parameter

Open
#39 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
2
Forks
0
Avg merge
1d 23h
Merged PRs (30d)
10

Description

## Summary

We want to discuss standardizing CRON schedules on **UTC** and removing (or deprecating) the per-job `timezone` parameter. The cron expression should always be interpreted as UTC, and we explicitly **do not** want to rewrite/offset the cron string itself to encode a timezone.

This came out of #38, which fixed a deserialization failure on `starts_at`/`ends_at` and surfaced that the `timezone` parameter isn't doing what it appears to.

## Current state

The per-job `timezone` (`cron_timezone`) is effectively cosmetic today, and in a misleading way:

1. **Firing is delegated to pg_cron.** `db::jobs::build_cron_command` + `cron.schedule(name, cron_expression, command)` (`crates/common/src/db/jobs.rs`) hand the raw `cron_expression` to pg_cron. pg_cron evaluates it in its own **cluster-global** timezone (`cron.timezone`, default GMT) — it never sees the per-job `cron_timezone`.
2. **`timezone` is used in exactly one place:** the API handler's `compute_next_cron` (`crates/api/src/handlers/jobs.rs`), purely to populate `cron_next_run_at` at create/update time.
3. **`cron_next_run_at` is write-once.** The worker never recomputes it (no references to `cron_next_run_at` in `crates/worker`), so it's a stale snapshot after the first tick.

Net effect: a job with `timezone: Asia/Kolkata`, cron `0 9 * * *` **reports** `next_run_at` as 09:00 IST but **actually fires** at 09:00 UTC (14:30 IST). The parameter promises local-time scheduling we don't deliver.

## Proposed direction

- Treat the cron expression as **UTC always**.
- **Do not** rewrite the cron string to bake in an offset (e.g. turning `0 9 * * *` into `30 3 * * *`). String rewriting is brittle — it breaks across DST transitions, day/month rollovers, and field ranges/steps (`0 9-17 * * MON-FRI`), and it makes the stored expression no longer match what the user typed.
- Use `starts_at`/`ends_at` as absolute **UTC** window bounds (this is already how `cron_ends_at` gates firing in pg_cron).
- Deprecate the `timezone` parameter explicitly rather than silently: it currently sets an expectation (local-time, DST-aware recurrence) the system does not honor.

### Trade-off to acknowledge

`timezone` and `starts_at`/`ends_at` are **not** substitutes for each other:

- `timezone` would define *what wall-clock the recurring cron fields mean* and is inherently **DST-aware** for a recurring rule ("09:00 local every day" as the UTC offset shifts twice a year).
- `starts_at`/`ends_at` are two absolute instants bounding *when the schedule is active*. You cannot reconstruct a DST-aware recurring rule from two instants.

So going UTC-only is a deliberate **capability cut**: we lose DST-correct local recurring schedules (e.g. "09:00 business hours in New York"). The decision here is that we'd rather have a simple, honest, consistent UTC model than carry a half-implemented timezone feature. Users who need local times convert to UTC at author time and accept fixed-offset behavior.

## Scope of work (for discussion, not committing yet)

- [ ] Decide: hard-remove `timezone` vs. deprecate-and-ignore (with a clear API warning/docs note).
- [ ] Stop requiring `timezone` for CRON jobs in `handlers::jobs::create` (currently returns `400` when absent).
- [ ] Recompute / stop persisting a misleading `cron_next_run_at` (compute in UTC, and ideally refresh it on tick, or drop it as a stored field).
- [ ] Interpret `starts_at`/`ends_at` as UTC instants consistently (already the case after #38; confirm and document).
- [ ] Migration for existing jobs that carry a non-UTC `cron_timezone` (leave as metadata? backfill? document behavior change).
- [ ] Update the dashboard job form (`crates/dashboard/src/pages/workspace_detail.rs`) and API docs/smithy model to reflect UTC-only semantics.

### Alternative (rejected for now)

Make `timezone` authoritative — honor it in pg_cron (per-schedule timezone support / session TZ) and recompute next-runs DST-aware. Strictly more capable, but more moving parts and contrary to the "keep cron UTC, don't format the cron string" preference. Captured here so the trade-off is on record.

## References

- PR #38 — datetime-local deserialization fix that surfaced this.
- `crates/common/src/db/jobs.rs` — `build_cron_command`, `register_pg_cron`.
- `crates/api/src/handlers/jobs.rs` — `compute_next_cron`, CRON create/update path.
- `crates/common/src/models/job.rs` — `CreateJob` / `UpdateJob` (`timezone`, `starts_at`, `ends_at`).

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.