dizzy.engine¶
dizzy.engine
¶
The DIZZY runtime kit's engine layer — how a declared feature gets RUN.
dizzy generate turns a .feat.yaml into schemas, contracts and stubs.
This package is the other half: the machinery that schedules those elements at
runtime, and — crucially — does so without knowing what they are.
Five pieces here, plus a scheduling shell per execution model:
loop—Engine: the control loop. Command -> procedure -> events -> projections -> policies -> commands, with the read-model commit boundary between the fold and the dispatch. It is keyed by generated class, so it names nothing.store—EventStoreoverdagstore, a content-addressed event DAG: the truth an engine appends to before anything else runs.rebuild/replicate— the two things you can do with a stream besides run it forward: refold it into the read models (the recoverability test), and pull a peer's facts and fold those through the same projections. Both take the projection runners as an argument, so neither knows a feature.registry—FeatGraph: the feat file read into an app's topology, with every declared command and event resolved to its generated pydantic class by DIZZY's naming convention. This is what makes a shell generic. The feat already declares everything; a shell that hard-codes any of it has copied the design out of the artifact, which is the one thing DIZZY exists to prevent.ports—HostApp/ShellServices/Runtimeand theCommandQueue/TelemetryBusprotocols: the seam through which everything app-specific reaches a shell. An app publishes oneHostApp; a shell resolves it from$DIZZY_HOST_APPand needs nothing else.
Scheduling shells (installed via extras, so a host pays only for the one it runs):
dizzy.engine.st— single process: a durable sqlite command queue with atomic claim + lanes, and an in-process telemetry ring. Stdlib only.dizzy.engine.mp— a fleet: Dramatiq/Redis workers, pool-routed, with telemetry over Redis pub/sub. Needsdizzy[mp].
The shells differ ONLY in scheduling — who holds the command queue, who runs the workers, where telemetry lands. The engine they drive and the wiring that binds a feature to it are shared, and both shells execute them verbatim.
They do NOT, however, claim the same semantics, and that difference is
deliberate rather than incidental. Because the engine hands every
policy-dispatched command to the shell's queue, the shell owns the command
phase — so it is the shell, not the engine, that decides how many commands run
at once. st drains one lane in one process and is sequentially consistent:
one legal interleaving, which is DIZZY's defined semantics. mp runs N
workers under at-least-once delivery and is knowingly weaker, relying on
confluent projections to absorb the reordering and the duplicates. A host picks
the shell whose guarantee it needs.
The engine reads and writes read models only through the runners the wiring
registers, so it carries no ORM: dizzy.engine costs pyyaml and pydantic and
nothing else. The mp shell's broker dependencies stay behind its extra, so
importing dizzy.engine never drags in a broker.
Engine
¶
One process's control loop over a registered topology.
Source code in dizzy/src/dizzy/engine/loop.py
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 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 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 | |
command_queue = command_queue
instance-attribute
¶
External (shell-owned) command queue — where dispatches go.
store = store
instance-attribute
¶
Append-only event store: the truth.
commit = commit
instance-attribute
¶
Called once per event, after its projections fold and before its
policies dispatch. None means the app has no read-model
transaction to close (see the ordering rule above).
current_event = None
instance-attribute
¶
The event whose policies are currently running, for wiring that needs to correlate a dispatch back to its cause.
projection_runners()
¶
The event class -> [(name, runner)] map, as registered.
This is exactly what :func:dizzy.engine.rebuild.rebuild and
:func:dizzy.engine.replicate.fold_envelopes take, so a host that has
already wired an engine does not wire the data loop a second time to
refold or to replicate — one registration, three triggers.
Source code in dizzy/src/dizzy/engine/loop.py
110 111 112 113 114 115 116 117 118 | |
emit_event(event)
¶
A procedure emitted an event -> onto the local event queue.
Source code in dizzy/src/dizzy/engine/loop.py
122 123 124 | |
dispatch_command(command)
¶
A policy dispatched a command -> onto the external queue.
Source code in dizzy/src/dizzy/engine/loop.py
126 127 128 | |
discard_pending_events()
¶
Drop events emitted but not yet drained.
A shell calls this after a failed command so un-appended events cannot leak into the NEXT command on a long-lived process.
Source code in dizzy/src/dizzy/engine/loop.py
130 131 132 133 134 135 136 | |
run_command(command)
¶
Run one command to quiescence: its procedure, then every event that cascades from it. Returns when the local event queue is empty; any command a policy dispatched is by then on the external queue.
Source code in dizzy/src/dizzy/engine/loop.py
211 212 213 214 215 216 217 218 219 220 221 222 223 | |
CommandQueue
¶
Bases: Protocol
Where a policy's dispatch goes. The engine holds one of these.
Source code in dizzy/src/dizzy/engine/ports.py
39 40 41 42 43 44 45 | |
HostApp
dataclass
¶
Everything a scheduling shell needs to run an app it knows nothing about.
Source code in dizzy/src/dizzy/engine/ports.py
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 | |
routes = dict
class-attribute
instance-attribute
¶
command name -> (pool, broker message options). Unlisted commands go to the default pool. Where the route table COMES from — a manifest, a config file, a constant — is the app's business, not a shell's.
origin_for = _no_origin
class-attribute
instance-attribute
¶
(current_event, command_being_dispatched) -> correlation string, or None for the default. Lets an app thread its own causality (e.g. a tool call's identity) through a dispatch without the shell knowing those names.
on_command_done = _no_hook
class-attribute
instance-attribute
¶
(origin, status, detail, emitted) after a command finishes — the app's place to close out whatever origin referred to.
Returning TRUTHY on a failure means "handled": the app turned the failure into a fact, so the shell must not also let the broker retry the side effect. A falsy return re-raises, keeping at-least-once delivery.
span_attrs = _no_attrs
class-attribute
instance-attribute
¶
origin -> extra tracing attributes. Whatever an app encodes in an origin string is the app's to decode, but a shell that simply dropped it would make traces unsearchable by the app's own identifiers — so the decoding gets a door rather than being deleted.
resolve(spec=None)
staticmethod
¶
Load the app manifest named by spec or $DIZZY_HOST_APP.
Spec form is module:attr; attr may be a HostApp or a
zero-argument callable returning one (the usual choice — it defers the
app's imports to worker-boot time).
Source code in dizzy/src/dizzy/engine/ports.py
223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 | |
NullOtel
¶
Satisfies the tracing surface a shell uses, doing nothing.
Source code in dizzy/src/dizzy/engine/ports.py
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | |
Runtime
dataclass
¶
One process's live engine, as the app built it.
Source code in dizzy/src/dizzy/engine/ports.py
159 160 161 162 163 164 165 166 167 168 169 170 | |
session = None
class-attribute
instance-attribute
¶
The read-model session, when there is one — the shell rolls it back after a failed command so a partial fold can't leak into the next.
refresh = lambda: None
class-attribute
instance-attribute
¶
Re-hydrate mutable environment before each command (secrets can change
under a long-lived worker). Derive the field list from
graph.environment rather than listing it.
ShellServices
dataclass
¶
The shell's side of the contract, passed to build_runtime.
The app builds its Engine around these: dispatches go to command_queue, observations to publish. Telemetry sinks are the app's to construct — a sink that must cross the process boundary is just one that closes over publish, which keeps the shell ignorant of the app's payload shapes.
Source code in dizzy/src/dizzy/engine/ports.py
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | |
observer = _noop_observer
class-attribute
instance-attribute
¶
The SHELL's event observer, which the app MUST call from whatever observer it passes to its engine builder.
The engine takes exactly one observer, so an app that installs its own
without chaining this one silently unplugs the shell: mp collects the
events a command emitted here, and on_command_done receives that list.
Dropping it degrades every result to "no events emitted" rather than
failing, which is why it is stated as a requirement and not a nicety.
Use :func:chain_observers if you have nothing app-specific to add.
TelemetryBus
¶
Bases: Protocol
Host-level observation — never events, never load-bearing.
Source code in dizzy/src/dizzy/engine/ports.py
48 49 50 51 52 | |
FeatGraph
¶
An app's declared topology, with its generated classes resolved.
Construct with :meth:load. Cheap to hold; every resolution is cached.
Source code in dizzy/src/dizzy/engine/registry.py
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 | |
environment
property
¶
Environment field names — what a shell must re-hydrate per command.
Derived, so adding an env shape to the feat needs no shell change.
telemetry
property
¶
Telemetry sink names — the ports a shell may re-route as transport.
commands
cached
property
¶
Command name -> generated pydantic class.
events
cached
property
¶
Event name -> generated pydantic class.
names(section)
¶
The names the feat declares in section, in feat order.
Source code in dizzy/src/dizzy/engine/registry.py
207 208 209 | |
entry(section, name)
¶
One declaration, normalized to a dict.
A bare string is a description-only entry (how the feat spells most commands); a NULL value is a declared-but-unwritten entry, which is normal while drafting — it is present, just empty.
Source code in dizzy/src/dizzy/engine/registry.py
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 | |
command_name(command)
¶
The feat name of a command INSTANCE (or class).
Source code in dizzy/src/dizzy/engine/registry.py
283 284 285 286 | |
validate_registered(registered)
¶
Assert an app's registered elements are exactly what the feat declares.
registered maps a topology section to the names the app actually
wired. Replaces the hand-maintained _REGISTERED literal: the feat
side is read, so only the app's own wiring must be reported.
Source code in dizzy/src/dizzy/engine/registry.py
294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 | |
Envelope
dataclass
¶
One appended fact, as the stream knows it.
Source code in dizzy/src/dizzy/engine/store.py
62 63 64 65 66 67 68 69 70 71 72 73 74 75 | |
EventStore
¶
Content-addressed event store. Path from arg > $DIZZY_STORE_PATH > default.
Source code in dizzy/src/dizzy/engine/store.py
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 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 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | |
event_classes
property
¶
The feat's event map, resolved on first use.
Deferred because appending needs no classes: a worker that only writes the stream should not pay to import the generated definitions package.
__init__(path=None, event_classes=None, graph=None)
¶
event_classes maps feat event name -> class, for
:meth:reconstruct_event. Omit both it and graph and the store reads
the ambient feat file when (and only when) something first reconstructs.
Source code in dizzy/src/dizzy/engine/store.py
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | |
append(event, ingested_at=None)
¶
Append one event and return its envelope.
ingested_at is stamped NOW unless supplied — a caller supplies it
only when replaying or replicating an already-stamped fact.
Source code in dizzy/src/dizzy/engine/store.py
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | |
iterate()
¶
Yield all envelopes in canonical (topological) order.
Source code in dizzy/src/dizzy/engine/store.py
149 150 151 152 153 154 155 156 157 158 159 160 161 | |
add_replicated(event)
¶
Ingest a dagstore event fetched from a peer, returning its envelope.
The hash is verified on arrival by the DAG, and every parent must already be present — replication delivers ancestry first.
Source code in dizzy/src/dizzy/engine/store.py
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | |
raw_event(event_id)
¶
The stored dagstore event, wrapper payload and all — what a peer asks for over the wire. Raises KeyError if absent.
Source code in dizzy/src/dizzy/engine/store.py
188 189 190 191 192 | |
reconstruct_event(envelope)
¶
Rebuild the event instance from an envelope.
Source code in dizzy/src/dizzy/engine/store.py
202 203 204 | |
chain_observers(*observers)
¶
Compose observers into the one the engine accepts, in order.
Source code in dizzy/src/dizzy/engine/ports.py
122 123 124 125 126 127 128 129 | |
null_app(build_runtime, feat_path=None)
¶
The minimal HostApp: a feat file and a way to build the engine.
Source code in dizzy/src/dizzy/engine/ports.py
270 271 272 273 274 | |
camel_case(name)
¶
classify_image -> ClassifyImage.
LinkML's camelcase semantics (that generator produced the classes, so
this must match it, not merely resemble it): split on non-word runs and
underscores, upper the first character of each part, keep the rest.
Source code in dizzy/src/dizzy/engine/registry.py
63 64 65 66 67 68 69 70 | |
check_name(name, section, feat_name)
¶
Reject a declared name that does not survive the round trip.
camel_case splits on any non-word run, so classify-image,
classifyImage and classify_image all collapse to ClassifyImage.
Left unchecked, a typo'd feat entry resolves to its NEIGHBOUR's class and
the reverse lookup (which routes commands) silently maps it back to the
wrong name — the stale-generation check would pass on a broken feat. A
name is only well formed if snake_case(camel_case(name)) == name.
Also catches YAML's scalar keys: 123: or on: parse to int/bool and
would otherwise die inside re with no mention of the feat file.
Source code in dizzy/src/dizzy/engine/registry.py
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | |
find_feat(start=None)
¶
Locate the app's feat file.
$DIZZY_FEAT_PATH wins. Otherwise walk up from start (default: the
working directory) looking for exactly one *.feat.yaml. This is how a
worker boots knowing only where it is — no app import required.
Source code in dizzy/src/dizzy/engine/registry.py
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | |
graph(feat_path=None, def_package=DEFAULT_DEF_PACKAGE)
¶
Process-wide FeatGraph cache — a worker parses its feat once.
Keyed on the RESOLVED path, not on nothing: an earlier version cached a
single graph, so once any caller had built one, a later $DIZZY_FEAT_PATH
(or a different cwd, since discovery walks up) silently handed back the
first caller's feat. Re-resolving per call is two env reads; parsing is
what the cache is for.
Source code in dizzy/src/dizzy/engine/registry.py
321 322 323 324 325 326 327 328 329 330 331 332 333 334 | |
reset_graph()
¶
Drop the cache — for tests that rewrite a feat file in place.
Source code in dizzy/src/dizzy/engine/registry.py
337 338 339 | |
snake_case(name)
¶
ClassifyImage -> classify_image — the inverse of camel_case.
Source code in dizzy/src/dizzy/engine/registry.py
73 74 75 | |
reconstruct_event(envelope, event_classes)
¶
Rebuild the event instance an envelope stands for.
event_classes is a feat-name -> class map; FeatGraph.events is one.
Source code in dizzy/src/dizzy/engine/store.py
78 79 80 81 82 83 84 85 86 87 88 89 90 | |