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.