Skip to content

Configuration

JASIL never reads environment variables or secret files. The host builds a JasilSettings from whatever source it likes — env vars, a config file, a secrets manager — and installs it once at startup:

import jasil.settings as jasil_settings

jasil_settings.configure(
    jasil_settings.JasilSettings(
        profile=jasil.DeploymentProfile.DISTRIBUTED,
        state_uri="redis://cache:6379/0",
        events_uri="redis://cache:6379/1",
        storage_uri="s3://my-bucket",
        lock_uri="postgres-advisory://",
    )
)

Every component reads the installed settings through get_settings(). Settings are immutable — frozen dataclasses — so one component cannot mutate configuration another has already read.

Calling configure() is optional. With nothing installed, get_settings() returns an all-defaults instance, which is a working single-process deployment.

Shape

Configuration is grouped by concern rather than being one flat list, so enabling durable jobs means reading one small class instead of scanning forty unrelated fields.

Top level — JasilSettings

Field Default Meaning
profile DeploymentProfile.LOCAL The deployment shape. Supplies capability-URI defaults.
web_workers 1 How many web-server worker processes you run. Drives the consistency checks as much as the profile does.
job_worker_mode JobWorkerMode.IN_PROCESS Where durable-job consumers run: in_process (each API process calls start_job_worker()) or standalone (dedicated processes call run_job_worker()). A fact about the shape, not a count: load the same value in every process. Drives the consistency checks; standalone requires jobs.enabled. Parse an environment value with jasil.profile.parse_job_worker_mode.
enforce_deployment_consistency True Refuse to build a platform whose wiring contradicts its topology. False logs the issues as warnings instead.
data_dir "data" Root directory for the local storage backend.
state_uri None memory://, or redis:// / rediss:// / unix://.
storage_uri None local:// or s3://.
events_uri None memory://, or redis:// / rediss:// / unix://.
lock_uri None noop:// or postgres-advisory://.
jobs JobSettings() Durable-job pipeline.
event_log EventLogSettings() Observability trail.
geocoding GeocodingSettings() Reverse-geocoding backend.
mail MailSettings() Outbound mail backend.
network NetworkSettings() Outbound egress.

JobSettings

Field Default Meaning
enabled False Route events through the outbox instead of the bus.
lease_seconds 300 How long a worker may go silent before the reaper may reclaim the jobs it holds. A running worker renews its leases on every heartbeat, so this is not a cap on handler runtime. At least three heartbeat_interval_seconds.
max_runtime_seconds 1800 The longest a single handler is kept leased. Past it the job is abandoned to the reaper, the handler is signalled (jasil.jobs.lease_lost), and the rest of the batch is released. Not below lease_seconds.
batch_size 20 Maximum rows claimed or relayed per pass.
backoff_base_seconds 60 First retry delay; doubles per attempt.
backoff_max_seconds 3600 Ceiling for the exponential backoff.
poll_interval_seconds 5.0 Idle wait between empty polls.
record_attempts True Write one job_attempts row per attempt to run a job: worker, timing, outcome, error. False skips every attempt write and the attempt-derived admin fields read None. See Job attempts.
heartbeat_interval_seconds 15.0 Interval between worker heartbeats. Each beat renews the leases the worker holds and refreshes its registry row. The default stale threshold is three intervals.
max_attempts 5 Attempts before a job is dead-lettered.
retention_days 30 Age at which relayed outbox rows, completed jobs, their attempt history, and stopped/stale worker rows are pruned. A job's history goes after the job row itself is pruned; pending, claimed and dead-lettered jobs keep theirs. <= 0 disables.

EventLogSettings

Field Default Meaning
enabled False Record every event's lifecycle to event_log.
retention_days 30 Age at which trail rows are pruned. <= 0 disables.

Both jobs.enabled and event_log.enabled default to off, because both write to the database. A library should not start writing to your database because you installed it.

GeocodingSettings

Field Default Meaning
provider "" "nominatim", "photon", or "geocode". Anything else disables the capability.
rate_limit 1.0 Maximum requests per second; <= 0 disables throttling.
api_key None Required by geocode.maps.co.
nominatim_host "" Bare host[:port] authority.
nominatim_use_https True
photon_host "" Bare host[:port] authority.
photon_use_https True
user_agent "jasil (ReverseGeocoding)" Nominatim's usage policy requires an identifying value — set your own.

MailSettings

Field Default Meaning
uri None memory://, smtp://host[:port] (STARTTLS, port 587) or smtps://host[:port] (implicit TLS, port 465). Unset disables mail: send raises MailNotConfiguredError. No userinfo, path or query. The host is a DNS name or an IP literal. memory:// is refused in a multi-process deployment.
secure True Verified TLS. False allows cleartext smtp:// and only to a host that resolves exclusively to private, loopback or link-local addresses (re-checked on every send, and the connection is pinned to the checked addresses, tried in order; the shared address space 100.64.0.0/10 used by carrier-grade NAT and Tailscale is not private for this purpose), with no username / password; anything else, including memory://, fails build_platform.
from_address "" "addr" or "Name <addr>". Required for the SMTP schemes. Independent of username.
username None SMTP login. Must be set together with password.
password None SMTP password. Hidden from repr.
ehlo_hostname None Name announced in EHLO. Defaults to the machine's hostname; set a fully qualified one for a relay that rejects a bare name.

Host names follow RFC 1123, so a name with an underscore (some Docker Compose service names) is refused for uri and ehlo_hostname; use an IP literal or a network alias without one.

A bad value fails build_platform with ValueError; no connection is attempted at startup, so a mail server that is down does not block boot. With secure = False the host is resolved at startup to confirm it is private, so that one case does need a working DNS resolver to boot.

NetworkSettings

Field Default Meaning
ssrf_allowed_hosts () Hostnames and CIDRs exempt from the SSRF address denylist.

Capability URIs

Each capability resolves its backend by URI scheme, independently of the profile:

Capability Schemes
state_uri memory://, redis://, rediss://, unix://
storage_uri local://, local://<path>, s3://<bucket>
events_uri memory://, redis://, rediss://, unix://
lock_uri noop://, postgres-advisory://

An unrecognised scheme raises ValueError at startup. Failing to start beats silently running on the wrong backend.

Profile defaults

Leaving a URI unset falls back to the profile's default:

Profile state storage events lock
local memory:// local:// memory:// noop://
distributed required required required required
custom required required required required

Non-local profiles refuse to guess

A Redis host or bucket name cannot be inferred, and defaulting to a process-local backend across replicas would mean each one silently keeping its own copy of state that is supposed to be shared. So an unset URI under distributed or custom raises ValueError at startup rather than starting a deployment that looks healthy and is not.

Host integration

Three seams the host wires once at startup, in this order:

import jasil.correlation as correlation
import jasil.orm as jasil_orm
import jasil.settings as jasil_settings

# 1. The ORM: you own the base and the engine.
jasil_orm.map_models(Base)
jasil_orm.configure_sessionmaker(sessionmaker(bind=engine))

# 2. Settings.
jasil_settings.configure(jasil_settings.JasilSettings(...))

# 3. Optional: where the correlation id comes from.
correlation.configure_provider(my_middleware.get_request_id)

map_models must run before any JASIL model module is imported — importing one beforehand raises a RuntimeError telling you so.

In practice that constrains two modules only:

Module Safe to import at module scope?
jasil.jobs.crud, jasil.event_log.crud No — import them inside a function, or after map_models has run.
everything else, including jasil.publisher, jasil.retention, jasil.jobs.service, jasil.container, jasil.deps, jasil.testing Yes, from anywhere.

That split matters because jasil.publisher is imported by every module that publishes an event, deep in your import graph and long before any startup hook runs. It defers its model imports so from jasil.publisher import publish at the top of a domain module always works.

Databases

JASIL runs its whole suite against SQLite, PostgreSQL and MySQL on every change, so all three are supported rather than merely expected to work. JASIL never creates the engine — whichever you hand it through configure_sessionmaker is the one it uses.

What differs between them is handled internally, but two consequences are worth knowing:

Consequence
FOR UPDATE SKIP LOCKED Available on PostgreSQL, MySQL 8.0.1+ and MariaDB 10.6+. Where it is missing, concurrent workers and relayers fall back to a compare-and-set claim plus the idempotent fan-out, which is correct but does more redundant work.
Timestamp precision MySQL's DATETIME stores whole seconds and rounds, so a persisted timestamp can differ from the one written by up to half a second, in either direction. Lease expiry, backoff and retention are all interval comparisons measured in seconds or days, so this does not affect them — but do not compare a timestamp JASIL persisted for equality with the value you passed in.

Logging

There is nothing to configure. JASIL uses logging.getLogger(__name__) throughout, so records propagate to whatever handlers you have already configured. Structured fields arrive via extra.

Correlation ids

With no provider installed, jasil.correlation uses a module-local context variable you can set yourself:

correlation.set_correlation_id(request_id)

A provider that raises is treated as "no id" rather than propagating — a correlation id is diagnostic metadata and must never break publishing.