Repository navigation
feat: finished Job runs ship as a windowed journal (ADR 0029) - #57
Merged
Merged
Conversation
Usage rollups systematically misrepresent short-lived workloads. A Job that runs ninety seconds inside an hour-long window reports a tiny average, and covered_nanoseconds says the coverage was low without saying that the workload was a batch run that started, worked and finished. That gap lands where it costs most: CronJob requests are routinely set from the peak run and left, so a batch fleet is where overprovisioning accumulates quietly. `job_runs` is the denominator. Windowed and superseding like the other two journals, for ADR 0021's reason — a fleet of CronJobs can finish hundreds of runs an hour, and one file per run would put the spool's file count under the cluster's control. Each record carries the run's instants, its outcome and failure reason, its pod success/failure counts, and the declared parallelism, completions and backoffLimit: without the last two, "three succeeded" is unreadable, and without the pod counts a run that retried twice looks free. Runs are deliberately NOT baselined on first observation, which reverses what the plan said. ADR 0020 §5 baselines restart counters because a counter carries no time; a Job carries startTime, completionTime and its condition's transition time, so an already-finished run files itself in the window where it actually finished. That is ADR 0021's distinction, and it means a freshly started agent picks up the cluster's recent batch history correctly dated instead of discarding it. The burst is bounded by the cluster's own retention — three successful and one failed Job per CronJob by default. A run's Job is filtered on four objects rather than three: namespace, namespace annotation, the owning CronJob (ADR 0028's workload step), then the Job's own annotations or its pod template's. The last is what keeps this honest — before ADR 0028 the documented opt-out was a pod-template annotation, so a customer who used it has excluded pods, and reading only the Job object would ship facts about a workload whose pods the agent refuses to measure. Template metadata only; the env/args/command beneath it are never read. Job counters are kept apart from pod counters: one Job makes many pods, so a shared number would answer neither question. No new RBAC and no new informer — the Job handler rides the watch already running for owner resolution. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Slice C1b. Adds the payload kind
job_runs; nothing existing changes shape.Why
Usage rollups systematically misrepresent short-lived workloads. A Job that runs ninety seconds inside an hour-long window reports a tiny average, because the average is over the window and the workload existed for a fortieth of it. ADR 0013's
covered_nanosecondssays the coverage was low; it does not say the workload was a batch run that started, did its work, and finished.That gap lands where it costs most. CronJob requests are routinely set from the peak run and then left, so a batch fleet is where overprovisioning accumulates quietly — and it is the one workload class the agent measures but cannot explain.
What ships
job_runs· sourcejournal· key (window start, window length) · supersedes — the same shape ascontainer_restartsandpod_disruptions, for ADR 0021's reason: a fleet of CronJobs can finish hundreds of runs an hour, and one file per run would put the spool's file count under the cluster's control.Per record: namespace, the scheduling CronJob (or the Job itself when bare), the run's own name,
started_at/finished_at, result, failure reason, the run's pod success/failure counts, and the declaredparallelism/completions/backoffLimit.The last two groups are there because without them the outcome is unreadable. A run that succeeded with
failed: 2retried twice, and those retries consumed resources nothing else in the protocol reports. "Three succeeded" is a complete run againstcompletions: 3and a third of one againstcompletions: 9.Failure reasons come from Kubernetes' vocabulary —
BackoffLimitExceededandDeadlineExceededare different capacity facts — and the free-textmessagebeside them is never read (ADR 0020 §6).Reversal from the plan: no baseline
The plan said to baseline already-terminal Jobs on the ADR 0020 §5 precedent. Building it showed the precedent does not transfer.
Restarts are baselined because a restart counter carries no time, so reporting a pre-existing count would file history in whichever window happens to be open at startup. A Job carries
status.startTime,status.completionTimeand its condition's transition time — nothing has to be invented, and an already-finished run files itself in the window where it actually finished. This is the distinction ADR 0021 drew for disruptions.So a freshly started agent picks up the cluster's recent batch history, correctly dated, instead of discarding it. The burst is bounded by the cluster's own retention: three successful and one failed Job per CronJob by default, less where
ttlSecondsAfterFinishedis set.Four filter steps, not three
Namespace allow/deny → the namespace's annotation → the owning CronJob's annotation (ADR 0028's workload step) → the Job's own annotations or its pod template's.
That last one is what keeps the payload honest. Before ADR 0028 the documented way to opt a workload out was an annotation in its pod template, so a customer who did that has excluded pods — and if
job_runsread only the Job object, the agent would ship facts about a workload whose pods it refuses to measure. Collected and sent is worse than collected and dropped.Template metadata only. The
env,argsandcommandbeneath it are never read, on a Job no less than on a pod.Job counters are kept apart from pod counters: one Job makes many pods, so a shared
observednumber would answer neither question.Cost
No new RBAC and no new informer. The Job handler rides the watch already running for owner resolution.
Changes
docs/adr/0029-job-runs.mdinternal/collector/jobs.go—JobRun, the terminal-condition read, dedup by UID,admitJobinternal/collector/filter.go—AdmitJob,ExcludedByObjectAnnotation, five Job countersinternal/collector/pods.go— the handler on the existing Job informerinternal/journal/jobs.go— the windowed accumulatorinternal/sink/— payload,WriteJobRuns, registry row, goldencmd/agent/main.go— wiring, flusher, coverage logdocs/security.md— payload table, the Job field-minimization rule, the RBAC why, and whatjob_runsnamesdocs/plans/job-and-rollout-facts.md— C1a ticked, C1b's reporting rule corrected in placeVerification
make testandmake lintpass.Golden covers the three cases a consumer must tell apart: a run that succeeded after retries, one that exhausted its backoff limit, and a bare Job that failed before starting (no
started_at— omitted rather than rendered as the epoch).Collector tests run the real Job informer against a fake cluster: instants and outcome come from the cluster not the observation, a running Job is not a run, a pod-template opt-out excludes the run, and a CronJob opt-out excludes its runs. The pod-template step was negative-tested by removing it —
TestPodTemplateOptOutExcludesTheRunfails on both the report and the counter.Journal tests cover filing by finish instant (including a run three hours old), idempotent re-observation, unfinished runs, and window closing.
🤖 Generated with Claude Code