Conducteur.

scheduled refresher — no cron, just a loop simulated
What this demonstrates scheduled work on a platform with no cron

Central Station deploys Deployments. There is no CronJob, no Job, and nowhere in the console or the CLI to say “run this every five minutes”. So scheduled work on this platform means a long-lived process with a loop inside it — and everything a CronJob would have given you for free becomes something you write yourself.

This app is that loop, with the schedule on screen. A second app, conducteur-worker, does the actual work: it refreshes NS disruption data every 60 seconds and holds it in memory. It is Internal Only, so no browser can reach it — this page reads it over internal cluster DNS at a URL Central Station injected. Every number below is served from that cache, so refreshing this page as often as you like generates zero upstream requests. That is the whole reason to refresh on a schedule instead of on demand.

The countdownRuns land on wall-clock boundaries, not “60s after the last one finished”. The cadence never drifts, however long the work takes.
Run nowThe same loop, so a manual run cannot overlap a scheduled one. This platform’s stand-in for kubectl create job --from=cronjob.
Force failureWatch retries back off 60s → 120s → 240s to a ceiling, then snap straight back onto the schedule when it recovers.
MissedBoundaries that passed while a run was still going. Silently absorbing those is how an “every 30s” job quietly becomes an every-few-minutes job.

The schedule

The loop, live. Everything here would be a field in a CronJob spec on a platform that had one — the table on the right maps each of them to the code that replaces it.

—
until next refresh

Run history

What a CronJob would leave behind as completed Pods, minus having to go and find them. Run 1 always says startup: the pod refreshes immediately and reports unready until the cache is primed, so it never serves an empty page.

#StartedTriggerTookItemsOutcome

Cached data

Live NS disruptions, or a simulator when no API key is set — the badge in the header says which, rather than putting a LIVE label on invented data. This is the cache every reader shares; it goes stale, loudly, rather than blank when the upstream is down.

…

What a CronJob would have done for you

Each row is a field you no longer get, and the code that replaces it. All of it is in worker/scheduler.go, about 300 lines.

CronJob fieldReplaced by
schedule:interval + wall-clock alignment on a fixed epoch
concurrencyPolicy: Forbidone goroutine — overlap is impossible, not guarded
activeDeadlineSeconds:a per-run context deadline
startingDeadlineSeconds:missed-boundary accounting
backoffLimit:exponential backoff to a ceiling
successfulJobsHistoryLimit:a 20-entry ring buffer
kubectl create job --from=the Run now button, through the same loop
one run per clusternothing — pinned to a single replica, see below
The last row is the one that cannot be fixed from inside the app. Two replicas would run two schedulers and refresh twice, silently, while both pods look healthy. Real leader election needs a coordination.k8s.io/Lease and RBAC to go with it, and Central Station grants an app neither — so this is pinned to one replica and says singleton_assumed in its own status rather than pretending otherwise.

Platform

What Central Station injected into this pod, read from the running process rather than from what we hoped it did.

Internal Only is not isolation. The worker has no ingress, so no browser can reach it — but this cluster runs with network-policy: none, so any pod in any namespace still can. See deploy/network-policy-gap.md.