shadow boots exporter.main/main now: the browser pool starts through the system, with reload hooks on its dev metadata and the old app.core reload path commented out. The entry installs the process handlers (idempotent, on start rather than on load) and enables info logging for the exporter tree, without which the default level filters everything below fatal. Service params go unqualified: the top-level key already scopes the component. AI-assisted-by: muse-spark-1.3-contributor
Exporter
Node process that renders shapes, frames and files to bitmap, SVG and PDF. It
is a consumer: it polls the backend's :exporter job queue on Redis and
drives each export through the backend's management API — claim, report the
milestones, and settle (complete-job multipart with the artifact, or
fail-job on error). There is no HTTP surface of its own and nothing is
persisted on this side: the row of the job, its cancel button and its result
all live in the backend.
How a job runs
- The backend dispatcher pushes
[job-id, scheduled-at](JSON) intopenpot.worker.queue:<tenant>:exporter. - One of the
Kpollers pops it and owns that connection for the whole run: the blocking pop is both the semaphore of the slot and the backpressure, so the process never runs more exports than pollers.KisPENPOT_EXPORTER_WORKER_CONCURRENCY(default 2, and the browser and wasm pools size themselves to the same figure). - The poller claims the job over the management API and, on
:run, mints a render session (create-job-session) through which every asset fetch happens as the owner of the job. - The milestones are reported as beats (
:preparing,:rendering,:packaging, with theobjectsandpagescounters). A watchdog re-sends the last one every second, whatever the run is doing. - It settles with
complete-job(the artifact rides multipart, the session is closed in the same step) orfail-job. The settle never rejects: a worker that cannot talk leaves the row to the backend's lease GC.
Cancellation
A cancellation reaches the worker through the beats: a skip answer means the
backend no longer listens (the row is terminal), so the runner stops between
units of work; mid-Skia the watchdog terminates the leased render worker. The
row was already cancelled by the backend's cancel-job.
Configuration
| Variable | Default | Description |
|---|---|---|
PENPOT_PUBLIC_URI |
— | The frontend the assets render through |
PENPOT_INTERNAL_URI |
public-uri |
Same network's frontend; the management API too |
PENPOT_REDIS_URI |
redis://redis/0 |
The queue and the rest of redis |
PENPOT_TENANT |
default |
Queue prefix (workspace name in devenv) |
PENPOT_EXPORTER_SHARED_KEY |
— | The shared key; derived from the secret otherwise |
PENPOT_EXPORTER_JOB_TTL |
3600 |
Lifetime of the temp files a job owns, in seconds |
PENPOT_EXPORTER_WORKER_CONCURRENCY |
2 |
K: pollers, running exports, pool sizes |
PENPOT_WASM_WORKER_POOL_MIN |
1 |
Render workers kept warm; clamped to the max |
PENPOT_WASM_WORKER_IDLE_TIMEOUT |
300 |
Silence before a worker is terminated, in seconds |
PENPOT_WASM_WORKER_IMAGE_CACHE_SIZE |
134217728 |
Per-worker image cache budget, in bytes |
Inspecting the backend
Nothing about a job is stored here: get-job in the backend answers the full
row, and its app.debug/admin surfaces render the same records.