Resource Monitoring - ptweezy/cronstable GitHub Wiki
Enable monitorResources to record a job's total CPU time (user + system), sampled peak resident memory, and optional CPU%/RSS chart series. cronstable samples the whole process tree, including child processes, with psutil.
The run record carries the numbers to every surface that already reports a run: the web dashboard, the terminal dashboard, the HTTP API, Prometheus, statsd, failure reports, and the durable run ledger.
It is observability only. Monitoring never changes a run's success/failure verdict, never delays or crashes a job, and is off by default. A per-run sampling task is spawned only when it is on. To cap a run's CPU, memory, or process count, see resource limits.
psutil is a core dependency. If it is unavailable, monitoring reports no data and the job continues.
monitorResources is a per-job option. To enable it fleet-wide, set it once under a defaults: block:
jobs:
- name: nightly-model-refresh
command: python -m models.refresh
schedule: "0 4 * * *"
monitorResources: trueThe bool form samples with the defaults: a 1s cadence and a 240-point chart series per run. DAG tasks accept the option too (see DAG tasks later).
Use a boolean or a map of optional settings:
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Turn sampling on. Writing the map form at all opts in, so enabled exists to keep a tuned map in place while switched off. |
interval |
float | 1.0 |
Seconds between process-tree samples. Minimum 0.1. Each sample walks the process table, so a sub-100ms cadence would busy-loop, and violating the bound is a load-time ConfigError. A shorter interval catches sharper RSS spikes at the cost of more wakeups. Total CPU is cumulative and re-read every sample, so it converges regardless of the interval. |
history |
int | 240 |
Chart-series points kept per run. 0 keeps the summary numbers only. Must be between 0 and 2000 (load-time ConfigError otherwise), bounding what one run adds to a durable ledger record. |
monitorResources:
interval: 0.5 # seconds between samples (default 1.0, minimum 0.1)
history: 240 # chart points kept per run (default 240; 0 = summary only)The option merges normally under defaults: and is not part of the job-set ID fingerprint. Turning monitoring on or off never changes a job's identity. The per-job option row and the numeric validation table also appear in the configuration reference.
The monitor attaches to the launched child's pid immediately after launch, takes a first reading right away (so even a short run has a chance of a sample), then polls on the configured interval. It takes one final opportunistic reading when the run ends.
-
The whole tree is accounted. Each sample reads the child and all of its descendants. CPU time is tracked per tree member. A member that exits has its last reading banked before it is forgotten, so sequential children (
sh -c 'a; b') accumulate instead of plateauing. For an uncapped run, the only CPU that escapes accounting entirely is a child that spawns and exits within a single sampling gap. -
A capped run's CPU total is exact. When the job also sets
resourceLimitsand the run is inside its own cgroup (Linux) or Job Object (Windows), each sample takes the run's members from that container instead of walking the process tree, and the CPU totals come from the kernel's accounting for the container. They include children that started and exited between samples, and processes that left the tree.max_rss_bytesis the sampled RSS sum, as for any run. On Linux, while cronstable's cpu controller is on, which takes a job with acpucap and a delegated cpu controller (as withDelegate=yes), every run has a cgroup of its own (see CPU caps and priority), so an uncapped run is sampled this way too. A run that never got into its container is sampled by walking its process tree. Withverify, a capped run's figures end before its check starts. -
Peak RSS is a sampled high-water mark. The recorded maximum is the highest resident-set sum observed across samples, so a spike narrower than the sampling gap can be missed. A shorter
intervalnarrows that window. -
Best-effort, never fatal. Every psutil interaction is guarded. A process that exits mid-sample, a platform that denies the read, or psutil raising anything at all yields whatever was captured so far. A run where nothing could be sampled carries no resource stats (
resourcesisnullin the API). It is never an error. - Short runs have fewer samples. Measurements are less precise when a run finishes between samples.
A finished monitored run records a summary object: cpu_user_seconds, cpu_system_seconds, cpu_total_seconds, max_rss_bytes, and samples (how many times the tree was successfully read). While the run is still going, the live readings are cpu_seconds (cumulative), cpu_percent (usage since the previous sample; can exceed 100 across multiple cores), and rss_bytes (the tree's current resident memory).
Alongside the summary numbers, each monitored run records a [t, cpu%, rss] point per sample: the data behind the dashboard's Resources charts.
After a run exceeds its history cap, the series is downsampled in place. Adjacent buckets merge with the mean CPU% but the peak RSS (the memory spikes people monitor for are never averaged away), and the effective resolution halves, so a run of any length stays within history points with uniform bucket widths: a few KB even for a days-long run. history: 0 disables the series and keeps the summary only.
The series is embedded in the durable run record's resources.series, so charts survive restarts. It is deliberately excluded from the polled /jobs and /jobs/{name}/runs payloads. The dedicated GET /jobs/{name}/resources endpoint serves it instead, and the dashboard fetches that endpoint lazily when you open a job's Resources tab, never on the poll loop.
| Surface | What appears |
|---|---|
| Web Dashboard | Live CPU/memory chips on a running job's row and drawer; per-run CPU and peak-memory columns and stats in the drawer's History tab. The drawer's Resources tab charts the live instance, the recorded profile of recent runs, and per-run trend strips; for an unmonitored job it points at the config instead of an empty chart, and for an unmonitored job with resource limits it lists the recent runs' cap records. |
| Terminal Dashboard | Live CPU/memory chips on monitored rows, a resources view in the job drawer, and a node resources panel. |
| HTTP API |
running_resources (live, summed over running instances) on GET /jobs. A per-run resources object plus windowed CPU/RSS aggregates on GET /jobs/{name}/runs. The chart-grade series on GET /jobs/{name}/resources. |
| Prometheus |
cronstable_job_cpu_seconds_total{job_name, mode}, cronstable_job_peak_rss_bytes, cronstable_job_last_run_cpu_seconds, and cronstable_job_last_run_max_rss_bytes; emitted only after the job has a monitored run. A state: store makes the two cumulative families restart-durable. |
| statsd | A <prefix>.cpu timer and a <prefix>.max_rss gauge appended to a monitored run's stop datagram. An unmonitored job's datagram is unchanged. |
| Reporting |
cpu_seconds / cpu_user_seconds / cpu_system_seconds / max_rss_bytes template variables and CRONSTABLE_CPU_SECONDS / CRONSTABLE_MAX_RSS_BYTES in the shell reporter's environment, so a failure page can say how big the run was when it died. |
| Durable State | The run ledger record carries the resources object, chart series included, so stats and charts survive restarts. Retention follows the ledger's state.maxRunsPerJob pruning. |
| MCP | The read-only cron_get_job_resources tool (observe toolset) returns the same live and per-run series as GET /jobs/{name}/resources. |
DAG tasks accept monitorResources with the same schema, but surface the result differently. A finished task instance's summary usage lands in the resources object of its task record inside the durable dag_run document (returned by GET /dags/{name}/runs/{run_key}). A task with a statsd sink sends the same cpu / max_rss lines.
Task instances are ephemeral and do not appear in the per-job Prometheus families on GET /metrics.
Three adjacent surfaces report the node's load rather than a job's. None of them requires monitorResources:
-
GET /nodesamples the serving host's live CPU/memory (plus the daemon's own footprint) fresh per request and drives the dashboard header's node meter. It is container-aware: under a cgroup v2 limit the numbers describe the daemon's own slice, not the host. -
GET /node/historyserves a background-sampled CPU/memory ring (every 5s, keeping the last hour, by default) charted behind the header meter. Configure or disable it withweb.nodeHistory. -
cluster.observabilityshares each node's whole-node CPU/memory across a cluster, so the dashboard's cluster panel and fleet view show where the load is.
- Configuration Reference: the per-job option table and load-time numeric validation.
- Resource Limits: per-run CPU, memory, and process caps that the kernel enforces.
- HTTP Control API: the endpoints that carry the numbers, with response examples.
- Metrics with Prometheus: the resource metric families and their restart-durability.
-
Metrics with statsd: the exact wire format of the
cpu/max_rsslines. - Reporting (Mail, Sentry, Shell, Webhook, Push, Event Log): the resource template and environment variables.
- Durable State: the run ledger the summary and series persist into.
-
Orchestration and DAGs:
monitorResourceson DAG tasks. -
Clustering and Leader Election: the
cluster.observabilityfleet-wide node stats.