Asynchronous processing — masakari — branch stable/2026.2
Branch
stable/2026.2@ae41528e1ebc— repository https://opendev.org/openstack/masakari — analysed on 2026-10-28
All the non-HTTP behaviour lives in masakari-engine (masakari/cmd/engine.py). It runs one oslo.messaging RPC server on topic [DEFAULT]/masakari_topic = ha_engine (masakari/conf/engine.py:20), with server = [DEFAULT]/host (masakari/service.py:91), the eventlet executor (masakari/rpc.py:147) and the endpoint MasakariManager. Its periodic tasks are scheduled by an oslo.service dynamic timer (masakari/service.py:103). The engine has no heartbeat, no service table and no leader election: every engine instance consumes the same queue and runs every periodic task.
| Process | Trigger | Type | Purpose | Reads | Writes / emits | Defined at |
|---|---|---|---|---|---|---|
| masakari-engine | process_notification | RPC cast received (topic ha_engine, version 1.0) | run the recovery workflow of a new notification | hosts, failover_segments, notifications; Nova | notifications.status, hosts.on_maintenance/reserved, vmoves; taskflow logbook; Nova actions; versioned notifications | masakari/engine/manager.py:335 |
| masakari-engine | get_notification_recovery_workflow_details | RPC call received (version 1.1) | return the progress of each task of a notification's workflow | hosts, failover_segments, taskflow flowdetails/atomdetails | — | masakari/engine/manager.py:420 |
| masakari-engine | _process_unfinished_notifications | SQL polling ([DEFAULT]/process_unfinished_notifications_interval, 120 s) | retry notifications in error, and new ones whose cast was lost | notifications WHERE status IN ('error','new') AND deleted=0 | as process_notification, then error → failed | masakari/engine/manager.py:356 |
| masakari-engine | _check_expired_notifications | SQL polling ([DEFAULT]/check_expired_notifications_interval, 600 s) | give up notifications older than notifications_expired_interval (24 h) | notifications WHERE status IN ('running','error','new') | status = 'failed' | masakari/engine/manager.py:394 |
| masakari-engine | _wait_for_evacuation_confirmation | state polling of Nova (verify_interval 1 s, timeout wait_period_after_evacuation 90 s) | confirm that an evacuated instance runs elsewhere | Nova GET /servers/{id} | vmove status | masakari/engine/drivers/taskflow/host_failure.py:216 |
| masakari-engine | _wait_for_stop_confirmation | state polling of Nova (1 s, wait_period_after_power_off 180 s) | confirm that an instance evacuated from stopped/error is stopped again | Nova GET /servers/{id} | — | masakari/engine/drivers/taskflow/host_failure.py:171 |
| masakari-engine | _wait_for_power_off, _wait_for_active | state polling of Nova (1 s; 180 s / 60 s) | instance recovery: stopped, then active | Nova GET /servers/{id} | — | masakari/engine/drivers/taskflow/instance_failure.py:87, masakari/engine/drivers/taskflow/instance_failure.py:151 |
| masakari-engine | _wait_for_disable | state polling of Nova (1 s, wait_period_after_service_update 180 s) | confirm the compute service is disabled | Nova GET /os-services | — | masakari/engine/drivers/taskflow/process_failure.py:65 |
Periodic tasks start after a random delay of 0–[DEFAULT]/periodic_fuzzy_delay (60 s) and run when [DEFAULT]/periodic_enable is true (default, masakari/conf/service.py:37). The two task intervals are read at import time (spacing=CONF.…, masakari/engine/manager.py:354), so a configuration reload does not change them.
process_notification (masakari-engine)
- Trigger: RPC cast
process_notification(notification=<Notification 1.3>), version 1.0, emitted by masakari-api after theINSERT(masakari/engine/rpcapi.py:48). Exchangeopenstack(default control exchange), queueha_engine, round-robin between engines. - Emitter:
NotificationAPI._create_notification(masakari/ha/api.py:333). - Purpose: turn a failure report into recovery actions on Nova.
- Flow:
- Load the source host. If its segment is disabled:
status = ignored,message = "Recovery aboart: the segment HA is disabled.", then raiseFailoverSegmentDisabled(logged by oslo.messaging,masakari/engine/manager.py:339). - Emit
notification.process.start(masakari/engine/manager.py:328). - Take a process-local lock named after the source host UUID (blocking,
masakari/engine/manager.py:274,masakari/utils.py:243): notifications of the same host are serialised within one engine only. - Re-read the notification. If the message says
newbut the database no longer does, skip it (bug 1773132,masakari/engine/manager.py:288). This protects against a replayed message, but not against two engines receiving two different messages for the same row [inferred]. status = running, then dispatch on the type (below), then write the final status (masakari/engine/manager.py:322) and emitnotification.process.end(or.errorwith the exception and traceback).
- Load the source host. If its segment is disabled:
- Concurrency and idempotency:
- Two engines can process two notifications of the same host at the same time. The API refuses a duplicate only within 180 s with an identical payload, and the lock is per process.
- Retries replay the whole workflow, because each run creates a new taskflow flow detail in the same logbook (
masakari/engine/drivers/taskflow/base.py:124). Nothing is resumed.
- Error behaviour: the cast has no reply.
MasakariExceptionsubclasses becomeerror; any other exception escapes and leaves the rowrunninguntil it expires [inferred].
COMPUTE_HOST: _handle_notification_type_host (masakari/engine/manager.py:174)
| Condition | Result |
|---|---|
host_status missing | ignored (masakari/engine/manager.py:180) |
host_status ≠ NORMAL (for example UNKNOWN: masakari-monitors could not confirm the power-off by IPMI) | ignored, so that instances are never evacuated from a node that may still be running (masakari/engine/manager.py:185) |
event = STARTED | finished, nothing done (masakari/engine/manager.py:194) |
event = STOPPED | recovery below |
other event | ignored (unreachable through the API: the payload schema only allows STARTED/STOPPED) |
Recovery of a stopped host:
- Set the host
on_maintenance = true, andreserved = falseif it was a reserved host (masakari/engine/manager.py:207). - If the segment's method is not
auto, list its reserved hosts that are not on maintenance (masakari/engine/manager.py:223), **in the default ordercreated_at desc, id desc**. - Run the method (
masakari/engine/drivers/taskflow/driver.py:134):auto: auto flow;reserved_host: RH flow, and with an empty list →ReservedHostsUnavailable(masakari/engine/drivers/taskflow/driver.py:68);auto_priority: auto flow, and on any exception other thanSkipHostRecoveryException→ RH flow (masakari/engine/drivers/taskflow/driver.py:83);rh_priority: RH flow, then auto flow on failure (masakari/engine/drivers/taskflow/driver.py:109).
The auto flow is a taskflow linear flow pre → main → post whose tasks are loaded by stevedore from [taskflow_driver_recovery_flows]/host_auto_failure_recovery_tasks (default pre: disable_compute_service_task, main: prepare_HA_enabled_instances_task, post: evacuate_instances_task, masakari/conf/engine_driver.py:128). The RH flow (host_rh_failure_recovery_tasks, default pre: disable_compute_service_task, main: prepare_HA_enabled_instances_task, evacuate_instances_task) wraps main in a ParameterizedForEach retry over reserved_host_list, so the main tasks are re-run with the next reserved host when one fails (masakari/engine/drivers/taskflow/host_failure.py:537).
The three tasks:
- DisableComputeServiceTask (
masakari/engine/drivers/taskflow/host_failure.py:44): Nova disablesnova-computeon the host with reason[host_failure]/service_disable_reason("Masakari detected host failed."), then **sleepswait_period_after_service_update(180 s)** "until nova recognizes the node down". Masakari never forces the service down (forced_downis not used), so Nova accepts the evacuation only once its own service group sees the node down [inferred from Nova's evacuate preconditions]. - PrepareHAEnabledInstancesTask (
masakari/engine/drivers/taskflow/host_failure.py:64):- List all servers of the host (
GET /servers/detail?all_tenants=true&host=<name>, one page only: nolimit, so Nova'smax_limit(1000) caps it,masakari/compute/nova.py:147). - Keep those with metadata
[host_failure]/ha_enabled_instance_metadata_key(HA_Enabled) true. Skiperrorinstances whenignore_instances_in_error_state. Append the others whenevacuate_all_instances(default true). - With nothing left, raise
SkipHostRecoveryException→finished. - Otherwise create one
vmovesrow per instance (pending,evacuation).
- List all servers of the host (
- EvacuateInstancesTask (
masakari/engine/drivers/taskflow/host_failure.py:153):- Read the vmoves still
pending, in the default ordercreated_at desc, id desc. Since the HA-enabled instances were inserted first, non-HA instances are evacuated first, against the help text ofevacuate_all_instances("First preference will be given to those instances which have …True"). Observed inhost-failure-auto. - With a reserved host: optionally add it to every aggregate that contains the failed host (
[host_failure]/add_reserved_host_to_aggregate; a Nova 409 is ignored), enable its compute service, and set itreserved = falsein Masakari. This runs under a non-blocking, process-local lock named after the reserved host (masakari/engine/drivers/taskflow/host_failure.py:458). - Evacuate in a green thread pool of
[DEFAULT]/host_failure_recovery_threads(3). For each instance (_evacuate_and_confirm,masakari/engine/drivers/taskflow/host_failure.py:193):GETthe server twice. Lock it unless it is already locked.- Normalise its state:
- a
vm_stateother thanactive/stopped/error→os-resetStatetoerror; - an
activeinstance with a pendingtask_state→ reset toerrorand do not stop it afterwards; - a
resizedinstance that is not shut down → do not stop it afterwards.
- a
- vmove
ongoing,POST evacuate(withhostonly in the RH flow; Nova chooses otherwise). - Poll until the instance is on another host (and
vm_stateback to the old value, orerror→active). - Instances that were not
activeare stopped again (os-stop, then poll untilstopped), and an instance that was inerroris reset toerroragain. - vmove
succeededwithdest_host. Any exception or timeout → vmovefailedwith the message. - In all cases set
end_timeand unlock; an unlock failure is only logged (masakari/engine/drivers/taskflow/host_failure.py:341).
- Re-read all vmoves of the notification. Any
failed, or any stillpending/ongoing→HostRecoveryFailureException(masakari/engine/drivers/taskflow/host_failure.py:424).
- Read the vmoves still
**Defect (observed, scenario host-failure-auto-priority-fallback).** The check of step 4 counts the vmoves of earlier attempts too. Under auto_priority, the auto attempt leaves a failed vmove. The RH fallback runs PrepareHAEnabledInstancesTask again (a second vmove for the same instance) and evacuates the instance successfully to the reserved host, but the notification still ends error ("Failed to evacuate instances 'X,X'"). The same applies to rh_priority, to the ParameterizedForEach retry of the next reserved host, and to the periodic retry of an error notification. In the observed run, the periodic retry later found no instance left on the host and ended finished.
VM: _handle_notification_type_instance (masakari/engine/manager.py:130)
- Only these
(event, vir_domain_event)pairs are processed (masakari/engine/instance_events.py:22):(LIFECYCLE, STOPPED_FAILED),(QEMU_GUEST_AGENT_ERROR, STOPPED_FAILED)and(IO_ERROR, IO_ERROR_REPORT). Every other pair →ignored. - The instance flow (
instance_failure_recovery_tasks, default stop → start → confirm active):- StopInstanceTask (
masakari/engine/drivers/taskflow/instance_failure.py:40): skip it if it is not HA-enabled and[instance_failure]/process_all_instancesis false (→finished);paused/rescued→ignored;resized→ reset toactive; thenos-stop(a Nova 409 is tolerated if the instance isstopped) and poll. - StartInstanceTask:
os-startifstopped, otherwiseInstanceRecoveryFailureException. - ConfirmInstanceActiveTask: poll until
active.
- StopInstanceTask (
- The flow works on the source host only: it does not check that the instance is still on the notifying host. It also starts an instance that was deliberately stopped: an
IO_ERRORon astoppedHA instance ends with the instanceactive(observed,instance-failure:vm-io-error).
PROCESS: _handle_notification_type_process (masakari/engine/manager.py:67)
STARTED→finished, nothing done.STOPPED→ the host is set **on_maintenance = truefirst** (masakari/engine/manager.py:90), then:nova-compute→ disable the compute service unless it is already disabled (reason[process_failure]/service_disable_reason), and poll untildisabled;- any other process name →
SkipProcessRecoveryException→finished, with the host left on maintenance: later host failures of that node are refused with 409 (observed,process-failure/libvirtd).
get_notification_recovery_workflow_details (masakari-engine)
- Trigger: RPC call, version 1.1, from
GET /v1/notifications/{id}at microversion ≥ 1.1 (masakari/engine/rpcapi.py:53). - Flow: open the taskflow persistence backend, read the logbook named after the notification UUID, and list its flows and atoms. Then order the atoms by the configured task list of the notification type and method (
masakari/engine/drivers/taskflow/driver.py:258), and return each atom that storedprogress_detailsin itsmeta(masakari/engine/drivers/taskflow/driver.py:349). - Defect: the comparison
recovery_method in ['AUTO_PRIORITY', 'RH_PRIORITY'](masakari/engine/drivers/taskflow/driver.py:319) uses upper-case values while the database storesauto_priority/rh_priority, so the "which flow ran" detection never applies.auto_priorityis then ordered by the auto task list. With the default lists the RH tasks have the same names, so the effect is limited to reordering when operators customise the lists [inferred]. - Error: any exception →
MasakariExceptionreturned to the API → 500.
_process_unfinished_notifications (masakari-engine)
- Trigger: every
process_unfinished_notifications_interval(120 s) in every engine. - Data read:
SELECT * FROM notifications WHERE deleted = 0 AND status IN ('error','new') ORDER BY created_at DESC, id DESC(masakari/db/sqlalchemy/api.py:496), with no limit. - Flow: for each row:
- If it is
error, ornewwithgenerated_timeolder thanretry_notification_new_status_interval(60 s, mutable), run_process_notificationdirectly. This bypasses the "segment disabled" check ofprocess_notification[inferred frommasakari/engine/manager.py:370]. - Re-read the row; if it is still
error, set it tofailed(masakari/engine/manager.py:376).
- If it is
- Concurrency: no lock across engines. Two engines can retry the same notification at the same time, and the per-host lock is per process. The guard at
masakari/engine/manager.py:288does not stop it, because the in-memory status is alreadyerror/new[inferred]. With N engines, a failed host recovery can be replayed N times. - Cost: one query, plus one full workflow per retried row, sequentially inside the periodic task. A host flow sleeps at least 180 s in
DisableComputeServiceTask, so a backlog delays the other periodic task.
_check_expired_notifications (masakari-engine)
- Trigger: every 600 s in every engine.
- Data read:
notifications WHERE status IN ('running','error','new'). Each row whosegenerated_time(sender clock) is older thannotifications_expired_interval(86400 s) is set tofailed(masakari/engine/manager.py:404). Arunningworkflow that is still alive is markedfailedwithout being stopped [inferred].
Maintenance commands (not triggers, run by the operator)
masakari-manage db sync/db version(masakari/cmd/manage.py:67).masakari-manage db purge --age_in_days N --max_rows M: hard-deletes soft-deleted segments and hosts older than N days, andnotificationsthat arefinished/failed/ignoredwithupdated_atolder than N days (masakari/db/sqlalchemy/api.py:752). **vmovesrows are never purged**: they are soft-deleted only bynotification_delete(masakari/db/sqlalchemy/api.py:623), which no API or engine path calls, and the purge selects other tables ondeleted_at(masakari/db/sqlalchemy/api.py:791). The purge then hard-deletes the notifications, leaving orphan vmoves.masakari-status upgrade check(masakari/cmd/status.py:26).
Source: reverse-spec-masakari/05-async-processing_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.