Configuration¶
imbi-scheduler reads its settings from the environment. Everything specific to
this service is prefixed IMBI_SCHEDULER_; the stores and the JWT secret are
shared with the rest of the platform and keep their platform-wide names.
Required¶
| Variable | Purpose |
|---|---|
IMBI_AUTH_JWT_SECRET |
Verifies the bearer tokens presented to the API |
POSTGRES_URL |
Task definitions, trigger state, and the run leases |
CLICKHOUSE_URL |
Run history (imbi.scheduler_runs) |
IMBI_SCHEDULER_SA_CLIENT_ID |
The scheduler's own service-account client id |
IMBI_SCHEDULER_SA_CLIENT_SECRET |
Its client secret |
IMBI_API_URL |
imbi-api's public URL — see Reaching imbi-api |
IMBI_INTERNAL_API_URL |
Where the scheduler connects to imbi-api — see Reaching imbi-api |
Without the service-account credentials the service still starts and still
schedules, but no api target can resolve a principal, so every such firing is
recorded as skipped. The container entrypoint therefore refuses to start in
scheduler mode without them.
The account itself is seeded. imbi-api setup — or imbi-api
setup-service-accounts on an install that predates it — creates the
imbi-scheduler service account, gives it the seeded imbi-scheduler role, and
either adopts the credential these two variables name or generates one and
prints it once. Set them before running setup and they are what the account
authenticates with; leave them unset and setup tells you what to set them to.
Set both or neither. A client id without its secret cannot be verified and a secret without its id names nothing to verify it against, so half a pair is refused rather than quietly replaced by a generated credential the service does not hold.
In all mode the entrypoint provisions the pair itself when neither variable is
set, so a single-container deployment runs the scheduler with no credential setup
at all. Setting only one there is the same error, and the entrypoint reports it
and leaves the scheduler out.
The seeded role grants scheduled_task:* plus every :read — enough to manage
the schedule and to inspect what a task reads, and deliberately not enough to
write anything else. A task whose api target writes needs that permission
added to the imbi-scheduler role first; until then it gets a 403.
Reaching imbi-api¶
Two variables, deliberately distinct:
| Variable | What it is | Example |
|---|---|---|
IMBI_INTERNAL_API_URL |
Where to connect. In-cluster, bare origin. | http://imbi-api:8000 |
IMBI_API_URL |
imbi-api's public URL, whose path is the prefix it mounts its routes under. | https://imbi.example.com/api |
imbi-api mounts every router under the path component of its public URL, while
the internal URL is a bare origin. The scheduler joins the two: with the values
above it calls http://imbi-api:8000/api/.... Setting only the internal URL
means every request omits the prefix and 404s — including the token request,
whose 404 becomes an IdentityError, so every api-target firing is recorded
as skipped rather than failed. Both are required.
Optional¶
| Variable | Default | Purpose |
|---|---|---|
IMBI_SCHEDULER_API_PREFIX |
/api |
Path the task and run routes are mounted under. /status is never prefixed. |
IMBI_SCHEDULER_SCHEMA |
scheduler |
Postgres schema holding task definitions |
IMBI_SCHEDULER_GATEWAY_URL |
http://localhost:8003 |
imbi-gateway base URL for gateway targets |
IMBI_SCHEDULER_SA_SLUG |
imbi-scheduler |
The service account's slug |
IMBI_SCHEDULER_MAX_CONCURRENT_RUNS |
20 |
Per-process ceiling on runs in flight |
IMBI_SCHEDULER_CONSECUTIVE_SKIPS_LIMIT |
5 |
Consecutive skipped runs before the task is disabled |
IMBI_SCHEDULER_CONSECUTIVE_NO_EFFECT_LIMIT |
5 |
Consecutive no-effect runs before a warning is logged |
IMBI_SCHEDULER_POLL_INTERVAL |
30 |
Upper bound in seconds on the engine's sleep |
MAX_CONCURRENT_RUNS is not bounded by the connection pool, and does not need
to be. The engine holds two of POSTGRES_MAX_POOL_SIZE (default 10) for the
life of the process — a LISTEN for task changes and another for cancels — but
a firing only takes a connection for short round trips and holds none across the
HTTP call, since neither the executor nor the identity resolver touches
Postgres. Twenty concurrent runs against eight free connections queue briefly on
checkout; capping the ceiling at the pool size would throttle throughput for
nothing.
POLL_INTERVAL is a ceiling, not a cadence. A change to the schedule arrives as
a NOTIFY and wakes the loop immediately; the interval only bounds how long a
missed notification can delay a firing. It costs latency, never a run.
The prefix, and why it is a setting¶
The all-in-one image's Caddyfile mounts this service with handle_path, which
strips /scheduler before the request arrives, so a caller reaching
/scheduler/api/tasks hits /api/tasks here and the default is correct.
Under Okteto the endpoints pass the path through instead, so there the prefix
has to include it (IMBI_SCHEDULER_API_PREFIX=/scheduler/api). A relative value
is refused at startup rather than mounting the routes somewhere unreachable.
Health¶
/status is unprefixed and stays that way, so a probe does not move when the
API is relocated. Both the container and the Helm chart read it directly on the
pod rather than through the reverse proxy.
Running more than one replica¶
Safe, and the point of the design. A due firing is claimed with
FOR UPDATE SKIP LOCKED, so concurrent replicas receive disjoint task sets and
each occurrence is claimed by exactly one replica. That is a guarantee about
the claim, not about delivery: the claim commits before the outbound call, so a
replica lost in between drops the occurrence rather than duplicating it.
Execution is therefore at-most-once and best-effort — see
ADR 0001. Per-task concurrency
(execution.max_running_instances) is enforced across replicas with a Postgres
advisory lock over a short-lived lease row.
Cancellation is broadcast with pg_notify, so cancelling a run reaches whichever
replica is holding the HTTP call open and interrupts it, not just the replica
that received the request.