Processing outside the API — heat — branch stable/2026.2
Branch
stable/2026.2@5872396e548c(27.0.1) — repository https://opendev.org/openstack/heat — analysed on 2026-10-29
Almost everything Heat does happens in heat-engine, after the API has answered. This file covers the engine's RPC servers, the convergence engine (the default, [DEFAULT]/convergence_engine = True, heat/common/config.py:247), the legacy engine still used for stacks created before convergence, the timers, and the processing triggered by signals and software deployments. The RPC frames themselves are in 03.
Summary
| Process | Trigger | Type | Purpose | Reads | Writes / emits | Defined at |
|---|---|---|---|---|---|---|
| heat-engine | 54 methods of EngineService | RPC call received, topic engine, server = [DEFAULT]/host (version 1.36) | serve the APIs (create, update, delete, show…) | all tables | stacks, resources, events; casts check_resource | heat/engine/service.py:405 |
| heat-engine | check_resource | RPC cast received, topic engine_worker (version 1.9), any engine | process one node of the convergence graph | resource, stack, raw_template, sync_point | resource row (lock, state), sync points, next casts, stack state | heat/engine/worker.py:202 |
| heat-engine | check_resource_delete_snapshot | RPC cast, topic engine_worker (1.7) | delete one resource's part of a snapshot | snapshot, resource_snapshot | service backups deleted, snapshot rows | heat/engine/worker.py:170 |
| heat-engine | cancel_check_resource | RPC cast, topic engine_worker, server = engine id (1.3) | stop this engine's workers of a stack | — | THREAD_CANCEL into the in-process message queues | heat/engine/worker.py:369 |
| heat-engine | listening | RPC call, topic heat-engine-listener, server = engine id | liveness probe ("is the engine that holds the lock alive?") | — | — | heat/engine/service.py:386 |
| heat-engine | stop_stack, send | RPC call, topic heat-engine-listener, server = engine id | legacy engine: stop or message a stack's threads on the engine holding its lock | — | thread cancellation | heat/engine/service.py:394, heat/engine/service.py:399 |
| heat-engine | service_manage_report | timer, every [DEFAULT]/periodic_interval (60 s) | heartbeat of this engine in the service table | service | service.updated_at | heat/engine/service.py:476, heat/engine/service.py:2523 |
| heat-engine | service_manage_cleanup | once at start | delete service rows of this host that missed 3 heartbeats | service | service (soft delete) | heat/engine/service.py:473, heat/engine/service.py:2550 |
| heat-engine | reset_stack_status | once at start (thread) | mark legacy stacks left IN_PROGRESS by a dead engine as FAILED | stack, stack_lock | stack and resources FAILED, lock released | heat/engine/service.py:478, heat/engine/service.py:2568 |
| heat-engine | resource signal | thread started by the resource_signal RPC | run handle_signal of a wait condition, deployment, scaling policy… | resource, stack | resource data/metadata, nested updates | heat/engine/service.py:1995-2004 |
| heat-engine | stack timeout | checked at the start of every check_resource | fail a traversal older than timeout_mins | stack.updated_at/created_at | stack FAILED "Timed out" | heat/engine/check_resource.py:335-337 |
| heat-engine | deployment polling | on each show_software_deployment call | pull a deployment's signal from Swift or Zaqar | Swift object / Zaqar queue | deployment status | heat/engine/service_software_config.py:257-271 |
| heat-engine | notifications | every stack state change | usage notifications orchestration.stack.<action>.<start|end|error> | — | oslo.messaging notification | heat/engine/notification/stack.py:18-39 |
client_call_found: three EngineService methods are public but called by no client in Heat (reset_stack_status, service_manage_report, service_manage_cleanup), and two WorkerService methods (stop_traversal, stop_all_workers) — oslo.messaging's DefaultRPCAccessPolicy (heat/common/messaging.py:122) exposes every public method of an endpoint, so anything that can publish on the engine/engine_worker topics can invoke them ([_rewrite/rpc-contract_stable-2026.2.json](_rewrite/rpc-contract_stable-2026.2.json)).
1. Engine processes and identities
heat-engine(heat/cmd/engine.py) forks[DEFAULT]/num_engine_workersprocesses (defaultmax(4, CPU count)), each running oneEngineServicewith the threading backend of oslo.service (no eventlet on this branch).- At
start()each process generates a random engine id (UUID4,heat/common/service_utils.py:80-81) and starts three RPC servers (heat/engine/service.py:445-478):enginetopic, server[DEFAULT]/host: the API ↔ engine contract;engine_workertopic, server = engine id: the convergence workers (heat/engine/worker.py:85-97);heat-engine-listenertopic, server = engine id: liveness and legacy thread control (heat/engine/service.py:369-374).
- Locks (stack lock, resource lock) store the engine id. A lock is stale when the engine that holds it does not answer a
listeningcall within[DEFAULT]/engine_life_check_timeout(2 s) (heat/rpc/listener_client.py:36-48). There is no lease or TTL: liveness is only this RPC probe. - The database
max_overflowis raised toexecutor_thread_pool_sizeso that every RPC thread can get a connection (heat/engine/service.py:480-490).
2. Convergence engine
2.1 Data model
| Item | Where | Meaning |
|---|---|---|
| traversal id | stack.current_traversal (heat/db/models.py:143) | UUID of the operation in progress; changes for each create/update/delete/rollback/check/suspend/resume/snapshot; for a snapshot it is the snapshot id (heat/engine/service.py:2226-2228). '' after a failed traversal (heat/engine/stack.py:2406-2407). |
| graph | stack.current_deps (heat/db/models.py:145) | {"edges": [[requirer, required|null], ...]}; each node is [resource_id, is_update] or, for snapshot nodes, [snapshot_id, false, "snapshot"] (heat/engine/stack.py:1512-1516, heat/engine/stack.py:89-93). An edge [X, Y] means "X needs Y done first". |
update node (id, true) | create or update resource id to the current template | |
cleanup node (id, false) | delete resource id if it is not part of the current template (old resources, replaced resources) | |
| sync point | sync_point table, PK (entity_id, traversal_id, is_update) (heat/db/models.py:82-101) | one row per graph node and one for the stack (entity_id = stack id, is_update = true), holding the data received from predecessors (input_data), the failures (extra_data), and an atomic_key for compare-and-swap |
| resource lock | resource.engine_id + resource.atomic_key (heat/db/models.py:295-296) | the engine working on the resource; every write is UPDATE … WHERE id=? AND engine_id=<expected> AND atomic_key=<n>, setting atomic_key = n+1 (heat/db/api.py:536-546) |
| template of a resource | resource.current_template_id | the raw_template the resource was last created/updated with; a cleanup node deletes the resource only if this differs from the traversal's template (heat/engine/resource.py:1990) |
| dependencies of a resource | resource.requires (JSON list of resource ids, sorted descending) | used to build cleanup edges of the next traversal (heat/engine/stack.py:1644-1647) |
| replacement | resource.replaces / resource.replaced_by | a replacement is a new row; the old row stays until its cleanup node deletes it |
2.2 Starting a traversal: Stack.converge_stack
heat/engine/stack.py:1445-1504, then _converge_create_or_update in a thread (heat/engine/stack.py:1507-1566):
- For CREATE/UPDATE/ROLLBACK/DELETE/RESTORE/ADOPT the stack definition switches to the new template (
prev_raw_template_idkeeps the old one, except for CREATE/ADOPT); CHECK/SUSPEND/RESUME/SNAPSHOT keep the current template (heat/engine/stack.py:1449-1462). action,IN_PROGRESS, reason "Stack <ACTION> started", and a new traversal id are stored withUPDATE stack … WHERE current_traversal = <previous>(heat/engine/stack.py:1477-1493,heat/db/api.py:1003-1028). If another request changed the traversal in between, nothing happens (logged warning). This compare-and-swap is what makes concurrent updates safe without a stack lock.- The previous traversal's sync points are deleted (
heat/engine/stack.py:1496-1498): workers still running for it will find their sync point missing and stop propagating. - (thread)
_update_or_store_resources: for each resource of the new template, pick the best existing row of that name — score −30 if FAILED, −50 if DELETE, −1 if replaced, +5 if its template is the previous one, +10 if it is the current one (rollback), ties broken by last change (heat/engine/stack.py:1587-1617); create anINIT_COMPLETErow when none exists (heat/engine/stack.py:1619-1632). _compute_convg_dependencies(heat/engine/stack.py:1634-1666):- the template's dependency graph translated to update nodes
(row id, true); - for every existing row: a cleanup node
(id, false); edgescleanup(req) → cleanup(id)for eachrequires(a resource is deleted after what depends on it),cleanup(replaces) → cleanup(id), andcleanup(id) → update(id)when the row is also updated (clean up after update); - for DELETE, one independent snapshot node per snapshot of the stack.
- the template's dependency graph translated to update nodes
- The edges are stored in
current_deps(with the traversal check),pre_convergeruns (DELETE: cancel the old workers), async_pointis created for each node and for the stack (heat/engine/stack.py:1534-1541). - The leaves (nodes with no requirement) get a
check_resourcecast each, update nodes after cleanup nodes; no leaf →mark_completeimmediately (heat/engine/stack.py:1543-1566).
2.3 Processing a node: check_resource
Received by any engine (engine_worker topic, no server: round-robin), heat/engine/worker.py:202-248:
- Deserialise
data({"input_data": {...}}, tuple keys restored,heat/engine/sync_point.py:83-117) intoNodeDataper predecessor (heat/engine/node_data.py:114-124): id, name, reference id, attributes, action, status, uuid of each resource this one depends on. Resource.load(heat/engine/resource.py:323-383): the resource row; the stack with the latest definition; but the resource is instantiated from the definition of its owncurrent_template_idwhen it differs (so updates compare old and new properties); a type change is honoured only if the new type is a valid substitute.- If
current_traversalof the message is no longer the stack's: re-trigger (_retrigger_replaced,heat/engine/worker.py:154-166) — a replacement not needed by the new traversal is marked DELETE so it gets purged, and its predecessor is re-triggered in the new traversal. - Otherwise
CheckResource.check(heat/engine/check_resource.py:327-369):- stack timed out (
time_elapsed > timeout_secs, computed fromupdated_atorcreated_at) →mark_failed('Timed out')and stop (heat/engine/check_resource.py:335-337,heat/engine/stack.py:2444-2458); - update node:
create_convergenceif the resource isINIT,update_convergenceotherwise,check/resume/snapshot_convergencefor those stack actions, nothing for SUSPEND (heat/engine/check_resource.py:517-532); - cleanup node:
delete_convergence(deletes only ifcurrent_template_id≠ traversal template; anINITrow is just deleted from the database),suspend_convergencefor SUSPEND, nothing for CHECK/RESUME/SNAPSHOT (heat/engine/check_resource.py:535-545,heat/engine/resource.py:1976-2001); - each operation holds the resource lock for its whole duration:
state_set(<ACTION>, IN_PROGRESS, lock=LOCK_ACQUIRE)writesengine_id = <this engine>only ifengine_id IS NULL AND atomic_key = <n>; the finalstate_set(COMPLETE|FAILED, lock=LOCK_RELEASE)writesengine_id = NULLonly ifengine_id = <this engine>(heat/engine/resource.py:861-932,heat/engine/resource.py:2159-2183). UPDATE and DELETE retry the acquisitionclient_retry_limit(2) times with a random wait ≤ 2 s (heat/engine/resource.py:878-898).
- stack timed out (
- On success,
_initiate_propagate_resource(heat/engine/check_resource.py:231-303):- a replacement impersonates the node of the resource it replaces (
heat/engine/check_resource.py:238-245); - for each node that requires this one (
deps.required_by), cleanup nodes first: the data sent is the resource'snode_data().as_dict()(attributes resolved once and forwarded) for update nodes, or the id of the resource (or its replacement) for cleanup nodes; propagate_check_resource→sync_point.syncon the dependant's sync point; when all its predecessors have reported, the dependant gets its owncheck_resourcecast with the accumulatedinput_data(heat/engine/check_resource.py:479-500);- resolved attributes are stored in
resource_properties_data(attr_data_id) forshow_stack/outputs (heat/engine/check_resource.py:278-284,heat/engine/resource.py:2369-2386); check_stack_complete: if this node is a root of the graph, it reports to the stack's sync point; when all roots have reported,mark_complete(heat/engine/check_resource.py:447-476).
- a replacement impersonates the node of the resource it replaces (
- Replacement (
UpdateReplaceraised by the update):make_replacementinserts the new row (replaces = old id,INIT_COMPLETE,requires) and setsreplaced_byon the old row in one transaction, conditional on the old row'satomic_keyandengine_id IS NULL(heat/engine/resource.py:385-437,heat/db/api.py:287-309); then acheck_resourcecast for the new row in the same traversal (heat/engine/check_resource.py:130-152). The old row is deleted later by its cleanup node.
2.4 Sync points
heat/engine/sync_point.py:132-207:
update_sync_pointis an optimistic loop: read the row, merge the new predecessor data intoinput_data(and failures /skip_propagateintoextra_data), thenUPDATE sync_point SET input_data=?, extra_data=?, atomic_key = n+1 WHERE entity_id=? AND traversal_id=? AND is_update=? AND atomic_key = n(heat/db/api.py:2120-2135); retry until one row is updated, with exponential random jitter (tenacity.wait_random_exponential(max=60), multiplier 10 ms per expected concurrent writer, capped at 10 s). There is no retry limit.- A missing sync point (deleted by a newer traversal) raises
EntityNotFound(entity="Sync Point")(heat/engine/sync_point.py:46-55): the sender reloads the stack and re-triggers itself into the new traversal if needed (heat/engine/check_resource.py:288-303). synccalls the propagation callback whenpredecessors − keys(input_data)is empty (heat/engine/sync_point.py:198-207). Because every predecessor writes its own key, a node becomes ready exactly once — unless two predecessors' final writes both observe "ready": the CAS onatomic_keyprevents that (only one update succeeds peratomic_keyvalue, and the second re-reads).- Serialisation of keys: tuple keys of the graph (
ConvergenceNode, 3 fields) are stored as the Python repr of the tuple, prefixed withtuple:— for example"tuple:(42, True, 'resource')"— and parsed back withast.literal_eval(heat/engine/sync_point.py:74-102). The same encoding applies to nested dict keys (attribute paths inattrs). A non-Python engine must emit and parse exactly this repr (True/False, single quotes,', 'separators). extra_dataholds{"extra_data": {"resource_failures": {"<resource name or id>": "<reason>"}, "skip_propagate": true}}, used by the CHECK action to keep traversing after failures and report them all at the end (heat/engine/check_resource.py:96-117,heat/engine/check_resource.py:461-472).
2.5 End of a traversal
mark_complete(heat/engine/stack.py:2362-2380):state_set(<ACTION>, COMPLETE)guarded by the traversal id; SNAPSHOT also writes the snapshot data; thenpurge_db.mark_failed(heat/engine/stack.py:2334-2360):state_set(<ACTION>, FAILED, reason); if rollback is enabled (disable_rollback = false) and the action is CREATE/ADOPT/UPDATE/RESTORE, start a ROLLBACK traversal to the previous template (or an empty template after a failed create) (heat/engine/stack.py:1568-1585); otherwisepurge_db.purge_db(heat/engine/stack.py:2392-2442): delete theDELETE_COMPLETEresource rows and their attribute data, setcurrent_traversal = ''if FAILED (so late workers stop), dropprev_raw_template_id(and the template) after success, delete the traversal's sync points, and onDELETE_COMPLETEdelete the trust/credentials and soft-delete the stack row.
2.6 Concurrency and cancellation
- New update during an update: allowed; the new traversal id makes the old one's sync points disappear. A worker of the old traversal that finishes a resource re-triggers the node in the new traversal (
heat/engine/check_resource.py:119-124,heat/engine/check_resource.py:207-229). A worker that cannot lock a resource (UpdateInProgress) checks whether the holder is alive: if not, it steals the lock (setsengine_id = NULL), marks the resource FAILED "Worker went down during resource <ACTION>" and re-casts itself; if no one holds it and the template is unchanged, it simply retries; otherwise it callshandle_preemptand lets the other traversal finish (heat/engine/check_resource.py:60-94,heat/engine/check_resource.py:177-187). - Cancel (
cancel_update/cancel_without_rollback): rollback = a ROLLBACK traversal; without rollback =stop_traversal: new traversal id, stack<ACTION>_FAILED"Stack <ACTION> cancelled", sync points deleted, the same on every in-progress nested stack (heat/engine/worker.py:112-133,heat/engine/worker.py:378-404). In-flight resource operations finish normally. - Delete during an operation:
stop_traversal, thenstop_all_workerscastscancel_check_resourceto each engine holding a resource lock of the stack (fromSELECT DISTINCT engine_id FROM resource WHERE stack_id=? AND engine_id IS NOT NULL,heat/db/api.py:374-380) and waits up to 3 × 5 s for the locks to clear (heat/engine/worker.py:413-442). The receiving engine putsTHREAD_CANCELin the message queue of each running worker; the worker raisesCancelOperationat its next progress check (heat/engine/check_resource.py:503-514), the resource endsFAILED"<ACTION> aborted". - Engine death mid-traversal: nothing restarts the traversal by itself. The resources it held keep
engine_idset; the stack staysIN_PROGRESSuntil the stack timeout is noticed by some latercheck_resource, or until the user issues an update/delete (the new traversal steals stale locks as above).reset_stack_statusat engine start only handles legacy stacks (convergence = False,heat/engine/service.py:2568-2575);heat-manage reset_stack_status <id>forcesIN_PROGRESS → FAILED, clearsengine_id, hook data and the stack lock (heat/db/api.py:1054-1099). - Idempotency: a re-delivered
check_resourcefor a node already done finds the resource'scurrent_template_idequal to the traversal template and does nothing but propagate again; the sync-point merge is idempotent (same key, same data). A replayed cast of an older traversal is ignored (step 3 of § 2.3).
2.7 Nested stacks
Resources of type OS::Heat::Stack-like template resources, OS::Heat::ResourceGroup, AutoScalingGroup… are StackResources: they create a child stack through the engine RPC — create_stack/update_stack with a pre-stored template_id and the parent's owner_id, nested_depth + 1, user_creds_id and stack_user_project_id (heat/engine/resources/stack_resource.py:300-336, heat/engine/resources/stack_resource.py:487-533), delete_stack or abandon_stack for deletion (heat/engine/resources/stack_resource.py:570-600) — and poll the child's state with show_stack in check_create_complete (heat/engine/resources/stack_resource.py:407-410). The RPC goes to any engine: the child's traversal is independent of the parent's. The child runs its own traversal; max_nested_stack_depth (5) bounds the depth. stop_traversal and stop_all_workers act on all descendants (stack_get_all_by_root_owner_id).
2.8 Cost
- RPC: one cast per graph node per traversal (plus re-triggers); the payload of each cast carries the resolved attributes of all predecessors.
- SQL per node: load resource, stack, template (twice when the resource uses an older template), lock write, state writes, one event insert per state change, the attribute store, and for each dependant at least two sync-point statements (
SELECT, conditionalUPDATE) — more under contention. - A wide graph (hundreds of resources sharing one dependant) turns the dependant's sync point into a hot row: every predecessor retries its CAS with jitter.
3. Legacy engine (stacks with convergence = False)
Stacks created by old releases, or with convergence_engine = False, run in one thread of the engine that took the stack lock (stack_lock row, heat/engine/stack_lock.py:52-107): create/update/delete/suspend/resume/check/snapshot/restore all go through ThreadGroupManager.start_with_lock (heat/engine/service.py:194-259). The lock is released (and the final state persisted) when the thread ends (heat/engine/service.py:233-253); if the release itself fails the engine exits (sys.exit(-1)). A lock held by a dead engine is stolen when the next operation finds it stale. heat-manage migrate_convergence_1 <stack id> converts a COMPLETE legacy stack tree to convergence (heat/engine/service.py:2480-2521, heat/engine/stack.py:2460-2486).
4. Timers and startup tasks
service_manage_report (heat-engine)
- Trigger:
ThreadGroup.add_timer(periodic_interval)(60 s), first run at start (heat/engine/service.py:476-477). - Purpose: register the engine in
serviceand keepupdated_atfresh, so thatGET /servicesreports itup(within 2 ×report_interval). - Data: first run
INSERT service (host, hostname, binary='heat-engine', engine_id, topic, report_interval), thenUPDATE service SET deleted_at = NULL(theupdated_atcolumn is bumped by the ORM) (heat/engine/service.py:2523-2548). - Errors: logged, retried next period. One row per engine process; rows of stopped engines are deleted at
stop()(heat/engine/service.py:523-527).
service_manage_cleanup (heat-engine, at start)
heat/engine/service.py:2550-2566: soft-deletes this host's service rows whose updated_at is older than 3 × periodic_interval.
reset_stack_status (heat-engine, at start)
heat/engine/service.py:2568-2613: for each legacy stack IN_PROGRESS (nested included), try the stack lock without retry; if acquired (holder dead), reload and run reset_stack_and_resources_in_progress("Engine went down during stack <ACTION>"): IN_PROGRESS resources → FAILED, stack → <ACTION>_FAILED, previous template restored for updates (heat/engine/stack.py:1702-1718). Stacks locked by a live engine are skipped (ActionInProgress).
5. Signals, wait conditions and software deployments
5.1 Signal sources
Transport (signal_transport property) | URL / channel given to the instance | Who authenticates | Arrives at |
|---|---|---|---|
CFN_SIGNAL (default for deployments, [DEFAULT]/default_deployment_signal_transport, heat/common/config.py:280) | pre-signed EC2 URL …/v1/signal/<arn> (or /waitcondition/) on heat-api-cfn | EC2 keypair of a stack-domain user, checked by Keystone ec2tokens | resource_signal RPC (heat/api/cfn/v1/signal.py:40-50) |
HEAT_SIGNAL | …/v1/<stack-user project>/stacks/<name>/<id>/resources/<res>/signal on heat-api, plus auth_url, user_id, password, project_id, domain_id of a stack-domain user | Keystone password auth of that user (heat_stack_user role) | resource_signal RPC |
TEMP_URL_SIGNAL | Swift TempURL PUT on an object in a container named after the stack | none (Swift temp URL) | polled: wait condition check_create_complete, deployment show (heat/engine/service_software_config.py:164-208) |
ZAQAR_SIGNAL | Zaqar queue id + stack-domain user credentials | Keystone | polled from Zaqar |
NO_SIGNAL | — | — | the resource completes without waiting |
The URLs and credentials are generated by SignalResponder (heat/engine/resources/signal_responder.py:100-336): the stack-domain user is created on demand in the stack's project of the heat domain with role [DEFAULT]/heat_stack_user_role (heat/engine/clients/os/keystone/heat_keystoneclient.py:344-395), its password is stored encrypted in resource_data (password, redacted), EC2 keys in resource_data (access_key, secret_key).
5.2 resource_signal processing
heat/engine/service.py:1953-2004:
- Load the stack with the stored context (
use_stored_context=True: the trust of the stack owner, not the caller's token), find the resource. _signal_check_actionand_signal_check_hook(heat/engine/resource.py:2457-2486): a signal that only clears a hook ({"unset_hook": "pre-create"}) is handled even during an operation.- If the resource has
handle_signal, runrsrc.signal(details)in a thread of the engine that received the call; otherwise synchronously.sync_call=True(CFN wait condition) forces synchronous handling and returns the resource metadata. - When the signal changes metadata other resources use (wait condition data), every created resource's metadata and node data are refreshed (
heat/engine/service.py:1966-1978). - Errors: exceptions in the thread are logged; the HTTP caller already got 200. Validation errors (unsupported action, hidden hook) are returned synchronously (400).
- Concurrency: signals and convergence writes on the same resource are serialised by the
atomic_keyCAS;make_replacementretries when a signal bumped it (heat/engine/resource.py:407-425).
5.3 Software deployment metadata push
Whenever a deployment is created, updated with a new config, or deleted, _push_metadata_software_deployments (heat/engine/service_software_config.py:109-162):
- finds the server's resource row by physical id (
nova_instance = server_id); - sets
rsrc_metadata.deployments= the configs of all the server's deployments, conditional on the row'satomic_key(retry on conflict,ConcurrentTransaction); PUTs the JSON metadata to the server's Swift TempURL (metadata_put_url, timeout[DEFAULT]/metadata_put_timeout, 60 s) and/or posts it to its Zaqar queue;- bumps
atomic_keyonce more when a TempURL is used. In-instance agents read it withPOLL_SERVER_CFN(CFNDescribeStackResource),POLL_SERVER_HEAT(GET …/resources/<server>/metadata),POLL_TEMP_URLorZAQAR_MESSAGE(heat/engine/resources/server_base.py:186-200; default[DEFAULT]/default_software_config_transport = POLL_SERVER_CFN,heat/common/config.py:259).
5.4 signal_software_deployment
heat/engine/service_software_config.py:302-361: a deployment IN_PROGRESS receives {"deploy_status_code", "deploy_stdout", "deploy_stderr", <outputs>}; a non-zero status code, or an output flagged error_output, makes it FAILED with a reason listing them; otherwise COMPLETE "Outputs received". Signals to a deployment not IN_PROGRESS are ignored.
6. Hooks and lifecycle plugins
- Breakpoints (
resource_registry.resources.<name>.hooks: [pre-create, pre-update, pre-delete, post-create, …]in the environment) make the resource wait, storing aresource_datarow named after the hook, until a signal{"unset_hook": "<hook>"}clears it (heat/engine/resource.py:533-553,heat/engine/resource.py:2464-2486).reset_stack_statusdeletes those rows (heat/db/api.py:1074-1084). - Stack lifecycle plugins (
heat.stack_lifecycle_pluginsentry point, empty on this branch,setup.cfg) and event sinks (heat.event_sinks: onlyzaqar-queue) are called on stack actions and events (heat/engine/stack.py:1071,dispatch_event).
7. Errors that never reach the client
They end up as statuses and events (see 02 § asynchronous errors): Resource CREATE failed: <exception> (heat/engine/check_resource.py:188-194), Timed out, Worker went down during resource <ACTION>, <ACTION> aborted (user triggered cancel), Stack <ACTION> cancelled, Engine went down during stack <ACTION>, Stack status manually reset, Resource CHECK failed: name: reason; ..., Snapshot deletion failed: ….
Source: reverse-spec-heat/05-async-processing_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.