Job Set ID - ptweezy/cronstable GitHub Wiki

Job-set ID

The job-set ID is a deterministic, order-independent fingerprint of selected fields in each configured job's effective configuration. Replicas with matching values for those fields produce the same id, even if host-specific settings or secret values differ. The id lets you compare configurations across replicas and detect drift; it does not describe which jobs are currently executing.

The id is the agreement key for cluster peer attestation. You can also use it without clustering: print it in a deploy script, compare it across a fleet through metrics, or read it off the dashboard header. The fingerprint is computed in cronstable/fingerprint.py.

An id is a v1: scheme prefix plus 64 hex characters:

v1:b834d7565aee0da50cd017f666651a5ba3b2e6b161daf0cb6e430f23f51ce90b

On this page: What the id covers · No secret material · Scheme versioning and platform caveats · Where it surfaces · The per-job digest

What the id covers

The id uses selected fields from every job's effective (post-merge) configuration, with the included and excluded fields described below. It has these properties:

  • Independent of job order, of how the jobs are split across included files, and of whether a setting is written inline or hoisted into a defaults block: the fingerprint is taken over the merged result only. Numeric spellings are normalized, so an inline killTimeout: 30 and the same value inherited from defaults fingerprint identically even where one parses as a float.
  • Equivalent schedule spellings match. The minute:/hour: object form collapses to the same crontab line as the equivalent string form, and runs of whitespace in a string schedule are collapsed, so a reformatted crontab line fingerprints the same (see schedules and time zones).
  • user / group are fingerprinted as configured (for example, www-data), not as the resolved numeric uid/gid, which can differ host to host.
  • It covers the shared job settings used to compare replicas. These include the fields below, with values normalized and secrets redacted:
    • name, command (a shell string and an argv list are kept distinct).
    • The normalized schedule.
    • shell, concurrencyPolicy, clusterPolicy, captureStdout / captureStderr, streamPrefix, saveLimit, maxLineLength.
    • The effective scheduling time zone: the resolved utc / timezone frame, so behaviorally identical spellings match.
    • enabled, onlyIfLastSucceeded, failsWhen.
    • onFailure / onPermanentFailure / onSuccess, including the retry and reporting policy, with secret values redacted.
    • The sorted names of environment variables.
    • executionTimeout, killTimeout, statsd, user, group.
    • concurrencyScope when set to cluster.
    • priority when set to anything but normal: the level, never the nice value or priority class a platform resolves it to, so a Windows replica and a Linux one still agree.
    • verify, and pool with poolSlots / queuePriority / queueTimeout, when set.
    • resourceLimits when the job has caps: the normalized CPU count, memory in bytes, process count, and required, so memory: 512M and memory: 536870912 match, and a map whose caps are all max counts as unset.

Deliberately not part of the identity:

  • The catch-up trio (onMissed, startingDeadlineSeconds, catchupJitterSeconds) and the archival pair (archiveOutput, redactArchivedSecrets), which are restart-time or observability-only, node-local behavior.
  • workingDirectory, for the same reason environment values are excluded. It is a per-host path, and a fleet can legitimately run the same logical job from D:\jobs on a Windows replica and /srv/jobs on a Linux one, which must not read as permanent drift.
  • sla / onLate, monitorResources, secrets, stateAllowedScopes, and the env_file path (its variable names still count).
  • Environment variable values and inline secret values (next section).
  • Everything outside the job definitions, in particular the cluster section itself. The peer list, distribution, and the rest of the coordination config never move the id.

Fields added after the v1 scheme shipped enter the identity only when they are set away from their default (concurrencyScope, listed earlier, is one), so upgrading cronstable never changes the id of an existing configuration.

Because the id is taken over effective config, it also reflects environment-variable interpolation: a ${VAR} in a fingerprinted string field is expanded before the fingerprint is computed, so a config that interpolates per-environment values yields a different id per environment by design. That is correct: the deployments really are running different job sets. If you need one id to compare across environments, keep interpolated variables out of fingerprinted job fields.

No secret material

The id is logged at startup and served on a possibly unauthenticated HTTP endpoint, so it must never embed secret material:

  • Inline reporting secrets are redacted. A Sentry DSN, mail password, or webhook URL or header value written as a literal value: is replaced by a placeholder before hashing. Whether and how a secret is configured (value / fromFile / fromEnvVar, and the file path or environment variable name) is still part of the identity. The literal secret never is.
  • Only the names of environment variables are hashed, never their values: environment variables commonly carry secrets, and a per-host value (for example, from env_file) would otherwise make identical configs fingerprint differently across hosts. The name set does include names contributed by env_file, so replicas must ship environment files with the same variable names (only the values may differ per host).

Two consequences: the id is safe to log and serve, and rotating a secret or changing an environment value does not change it.

Scheme versioning and platform caveats

The scheme is versioned: the v1: prefix is part of the emitted id and folded into the hash, so ids are only ever comparable within one scheme version. A cluster peer reporting a different scheme is marked drifted immediately, with no debounce, because such ids are not comparable (see per-peer status).

Because the fingerprint is over effective config, it also reflects platform-dependent defaults: the default shell is /bin/sh on POSIX and cmd.exe on Windows. Unless shell is set explicitly, the same YAML fingerprints differently across platforms. Compare instances running on the same platform, which is what replicas are.

Where it surfaces

  • CLI: cronstable --job-set-id prints the id to stdout and exits, for scripts and deploy checks. See the command-line reference.

    $ cronstable -c /etc/cronstable.d --job-set-id
    v1:b834d7565aee0da50cd017f666651a5ba3b2e6b161daf0cb6e430f23f51ce90b
  • HTTP: GET /job-set-id returns it as text/plain, or as a JSON object that also carries the job count under Accept: application/json.

  • Logs: Job set id: v1:… (N jobs) is logged at INFO once at startup, and again whenever a config reload changes the id.

  • Web dashboard: the header shows a chip with the first 12 hex characters (#b834d7565aee). Its tooltip carries the full id, and a click copies it (see web dashboard).

  • Terminal dashboard: the header bar shows the first characters of the id, and the command palette has a Copy job-set ID action (see terminal dashboard).

  • Metrics: the cronstable_job_set_info{job_set_id} info gauge. Compare it across a fleet to spot config drift (see metrics with Prometheus).

  • Cluster: the id is the job_set_id field on GET /cluster and the agreement key exchanged on the mTLS /peer endpoint. On the lease backends, the persisted @reboot "already ran" records are scoped to the current id, so a changed job set re-arms leader-gated @reboot one-shots. What the cluster does with the id is covered in clustering and leader election.

The per-job digest

The set id is built from per-job digests. Each job's canonical identity is serialized to a canonical JSON form and SHA-256 hashed. The per-job digests are then sorted, which neutralizes order. The sorted list is hashed together with the scheme version to yield the final v1:<64 hex> id. An empty job set yields a stable, well-defined id.

The per-job digest is also used on its own: durable state records that must not outlive a job's definition (pending retries, @reboot markers, and in-flight run records) are stamped with the owning job's digest and invalidated when that job's fingerprinted settings change. This limits invalidation to the affected job; using the whole-set ID would drop every pending record whenever any job's fingerprint changed.

See also

⚠️ **GitHub.com Fallback** ⚠️