dizzy.engine.mp¶
dizzy.engine.mp
¶
The multiprocess engine's scheduling shell — Dramatiq/Redis workers, pool-routed.
dizzy.engine.mp — a scheduling shell around an app it knows nothing about.
Install it with the extra that carries its broker — DIZZY is not on a package
index, so name the source: dizzy[mp] @ git+https://github.com/PNNL/dizzy.
Same library, different engine: the app's build_engine is reused
verbatim; the only difference is what's passed in — a command queue whose
put() publishes to the broker instead of an in-proc queue, and telemetry
sinks that publish to Redis pub/sub instead of the in-proc hub. Every
command — policy cascades included — flows through the broker. At-least-once
delivery stands (Dramatiq redelivers on worker death); the confluent
projections absorb the duplicates.
Nothing in this file names a command, an event, or an environment field.
Everything app-specific arrives through dizzy.engine.ports.HostApp, which
the worker resolves from $DIZZY_HOST_APP (module:attr) at boot:
- the app's topology is a
FeatGraph— the.feat.yamlread, sorun_commandlooks its command class up in the DECLARED graph rather than a hand-maintained dict; build_runtimebuilds this process's engine (the app's wiring, its env hydration, its telemetry sinks — a sink that must cross the process boundary is simply one the app closed overpublishwith);routesresolves command → (pool, message options);origin_for/on_command_donecarry the app's own causality (e.g. tool-call identity) across a dispatch without this shell knowing what a tool call is;otelsupplies tracing, defaulting to a no-op.
Pool routing: the app's routes() shapes the fleet — every dispatch
resolves to a pool and lands on that pool's queue (a pool's time_limit_ms
rides the message as its dramatiq option). Producers can enqueue to any pool
queue; CONSUMPTION is what's capability-gated — a worker process serves the
pools named in $DIZZY_POOLS (default default, declared at import so
--queues can select them). Start non-default fleets with
--queues <pool>, after checking the node can actually serve it. A bare
dramatiq dizzy.engine.mp consumes only the default queue — the uniform
fleet, unchanged.
The Redis TELEMETRY_CHANNEL carries records discriminated by kind; this
shell emits two of them and the app is free to publish its own:
- engine bus records (kind: procedure|projection|policy|event|telemetry) — self-observation, landing in the server's BUS ring;
{"kind": "job", "id", "status", "error"?}— broker-job lifecycle, so the server's command-queue ledger (and its SSE tab) tracks worker runs;{"kind": "worker", ...}— worker logs, which ARE telemetry here.
Run a worker fleet (after just redis-up):
dramatiq dizzy.engine.mp --processes 4 --threads 1
NOTE: run workers with --threads 1 — the per-process emitted-event
collection assumes one command in flight per process.
Dispatch from any process:
from dizzy.engine.mp import dispatch_by_name
dispatch_by_name("create_log_entry", {"body": "hi", ...})
Config: $DIZZY_HOST_APP (required — the app manifest), $DIZZY_REDIS_URL (default redis://localhost:6379/0), $DIZZY_POOLS (pools this worker serves).
BrokerCommandQueue
¶
The CommandQueue port: a policy's dispatch becomes a broker message.
Correlation on a dispatch is the APP's to decide: origin_for sees the
event currently draining and the command going out, and returns whatever
string should ride the message (the st shell's _ToolOriginQueue rule,
expressed app-side).
Source code in dizzy/src/dizzy/engine/mp/__init__.py
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 | |
app()
¶
Resolve $DIZZY_HOST_APP once per process. Lazy on purpose: a producer that only enqueues still pays for the app's imports otherwise, and the dramatiq CLI imports this module before the app's sys.path is settled.
Source code in dizzy/src/dizzy/engine/mp/__init__.py
96 97 98 99 100 101 102 103 | |
set_app(host_app)
¶
Inject the manifest directly (tests, embedding hosts).
Source code in dizzy/src/dizzy/engine/mp/__init__.py
106 107 108 109 | |
send_routed(command_name, payload_json, origin='', job_id=None)
¶
Every broker dispatch funnels here: resolve the command's pool from the app's route table and enqueue on that pool's queue with its message options. Cached — pool wiring is startup config, a manifest edit means restart.
Source code in dizzy/src/dizzy/engine/mp/__init__.py
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | |
publish(record)
¶
One channel for all worker→server telemetry; never load-bearing. Records published inside a recording span carry its trace_id, so the server's BUS ring (and the ledger's job rows) can link back to traces.
Source code in dizzy/src/dizzy/engine/mp/__init__.py
166 167 168 169 170 171 172 173 174 175 176 177 178 179 | |
run_command(command_name, payload_json, origin='', job_id=None, carrier=None)
¶
The uniform unit of work: claim → build engine → run_command → ack.
origin/job_id are correlation, not routing: origin carries whatever
identity the app's origin_for encoded, job_id ties the run back to the
server's ledger row. carrier is the producer's W3C trace context.
Source code in dizzy/src/dizzy/engine/mp/__init__.py
308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 | |
dispatch_by_name(command_name, payload)
¶
Producer-side dispatch: enqueue without building the library.
Source code in dizzy/src/dizzy/engine/mp/__init__.py
376 377 378 | |