OpenKeel: the sovereign French Cloud solution of the inherent group, with a European reach Discover inherent

Reverse-spec · Services

designate — Async processing

Processing outside the API — designate — branch stable/2026.2

Branch stable/2026.2 @ e3dd774db145 — repository https://opendev.org/openstack/designate — analysed on 2026-10-29

Most of Designate's behaviour happens after the HTTP response. The API only records an intent in the database (status: PENDING). designate-worker then pushes the zone to the pool targets, sends DNS NOTIFY and polls the nameservers until they serve the expected SOA serial. designate-producer runs the periodic tasks that batch serial increments, retry stuck zones and purge deleted ones. designate-mdns serves the zones to the DNS servers by AXFR and receives NOTIFY for secondary zones. designate-sink turns Nova and Neutron notifications into managed records.

Summary

ProcessTriggerTypePurposeReadsWrites / emitsDefined at
centralRPC server central (≈ 70 methods, call)AMQP RPCevery business operation of the API, the worker, the producer and the sinkDB (all tables)DB, RPC casts to worker, notifications dns.*designate/central/service.py:57
centralupdate_statusAMQP RPC cast from workerapply the outcome of a zone push/poll to zone and records (PENDING → ACTIVE/ERROR/deleted)zones, recordszones, records; notification dns.zone.updatedesignate/central/service.py:2543
centralupdate_service_status / delete_service_statusAMQP RPC cast (heartbeats, producer)maintain service_statusesservice_statusesservice_statusesdesignate/central/service.py:3136
central_import_zonein-process thread started by create_zone_importparse the zone file and create the zonerequest bodyzones, recordsets, records, zone_tasksdesignate/central/service.py:2895
workercreate_zone, update_zone, delete_zoneAMQP RPC cast (topic worker)push the zone to every pool target, NOTIFY, poll the nameservers, report the statuspool config (via central), tsigkeys, zonesbackend API / rndc, DNS NOTIFY, DNS SOA queries, RPC update_statusdesignate/worker/service.py:166
workerperform_zone_xfrAMQP RPC castAXFR a SECONDARY zone from its masters and store itmasters over DNSRPC update_zone to centraldesignate/worker/service.py:219
workerget_serial_numberAMQP RPC callSOA serial of a SECONDARY zone on one masterDNS SOA query—designate/worker/service.py:230
workerrecover_shardAMQP RPC cast from producerre-send zones in ERROR, or PENDING for longer than the maximum propagation timezonesRPC create/update/delete_zone to workerdesignate/worker/service.py:197
workerstart_zone_exportAMQP RPC castdecide how a zone export is served and complete the export taskrecordsets count, quotaRPC update_zone_exportdesignate/worker/service.py:208
producerincrement_serialtimer, [producer_task:increment_serial]/interval (5 s)bump the serial of zones flagged increment_serial, then push themzones (increment_serial = 1)RPC increment_zone_serial, worker update_zonedesignate/producer/tasks.py:281
producerdelayed_notifytimer, [producer_task:delayed_notify]/interval (5 s)push zones flagged delayed_notifyzones (delayed_notify = 1)worker update_zone, central update_zonedesignate/producer/tasks.py:229
producerworker_periodic_recoverytimer, [producer_task:worker_periodic_recovery]/interval (120 s)ask the worker to recover the producer's shard range—worker recover_sharddesignate/producer/tasks.py:326
producerzone_purgetimer, [producer_task:zone_purge]/interval (3600 s)hard-delete zones soft-deleted for more than time_threshold (7 days)zones (deleted != 0)RPC purge_zonesdesignate/producer/tasks.py:97
producerperiodic_secondary_refreshtimer, 3600 sAXFR SECONDARY zones whose refresh interval elapsedzones (type = SECONDARY)RPC xfr_zonedesignate/producer/tasks.py:188
producerperiodic_existstimer, 3600 sbilling/audit: one dns.zone.exists notification per zonezonesnotifications dns.domain.exists, dns.zone.existsdesignate/producer/tasks.py:134
producerperiodic_check_service_statustimer, 3600 smark services whose heartbeat is older than 1800 s DOWNservice_statusesRPC update_service_statusdesignate/producer/tasks.py:344
producerperiodic_cleanup_stopped_service_statustimer, 172800 sdelete STOPPED services, and DOWN ones older than 7 daysservice_statusesRPC delete_service_statusdesignate/producer/tasks.py:374
producerpartition rebalancetooz group join/leave eventsplit the 4096 zone shards between producer instancestooz group members—designate/producer/service.py:68
mdnsDNS QUERY (AXFR/IXFR, other types)UDP/TCP DNS server, port 5354serve zone contents to the pool's DNS serverszones, recordsets, records, tsigkeys, catalog zonesDNS responsesdesignate/mdns/handler.py:55
mdnsDNS NOTIFYUDP/TCP DNS servertrigger an AXFR of a SECONDARY zone when its master changedzones, master SOA (DNS)worker perform_zone_xfrdesignate/mdns/handler.py:91
sinknotification listenerAMQP notifications (nova, neutron exchanges, topic notifications)create or delete A/AAAA managed records for instances and floating IPspayloadRPC create_managed_records, delete_managed_recordsdesignate/sink/service.py:112
allheartbeattimer, [heartbeat_emitter]/heartbeat_interval (10 s)report the service as UP (and STOPPED on shutdown)—RPC cast update_service_statusdesignate/heartbeat_emitter.py:98
central, producercoordinationtooz heartbeat + run_watchers_interval (10 s)distributed locks (central), group membership (producer)tooz backendtooz backenddesignate/coordination.py:62

central (RPC server, topic central)

RPC server

  • Trigger: messaging.Target(topic=CONF['service:central'].topic, server=CONF.host) (designate/service.py:107), version 6.12 (designate/central/service.py:58). The topic defaults to central (designate/conf/central.py:43). The server uses the DefaultRPCAccessPolicy, so every public method of Service is callable (designate/rpc.py:197).
  • Emitter: designate/central/rpcapi.py:37 (CentralAPI), used by the API, the worker, the producer, the sink and designate-manage. All methods are call except update_status, update_service_status and delete_service_status, which are cast (designate/central/rpcapi.py:328, designate/central/rpcapi.py:438, designate/central/rpcapi.py:442). The full table is in interactions.
  • Concurrency: @lock.synchronized_zone() serialises operations per zone through a tooz lock named zone-<id>, or create-new-zone for zone creation (designate/common/decorators/lock.py:75). Without [coordination]/backend_url, the lock is a local lockutils lock (designate/coordination.py:57), which does not protect against several central processes. A thread-local set avoids re-entering the same lock (designate/common/decorators/lock.py:94).
  • Transactions: @transaction opens a write session and retries the whole method up to 150 times, 50 ms apart, on DBDeadlock, with deep copies of the arguments (designate/storage/__init__.py:49, designate/storage/__init__.py:107).
  • Notifications: @notification.notify_type() queues one notification per decorated call and emits the queue when the outermost decorated method returns (designate/common/decorators/notification.py:36). Nested calls (for example create_zone → update_zone of sub-zones) are therefore emitted together, after the whole operation.
  • Error behaviour: expected Designate exceptions are re-raised as ExpectedException so that oslo.messaging does not log them, and the client gets the original class back through allowed_remote_exmods (designate/common/decorators/rpc.py:37, designate/rpc.py:46).

update_status (cast from the worker)

  • Trigger: RPC cast update_status(zone_id, status, serial, action) (designate/central/rpcapi.py:328), sent by the worker's UpdateStatus task (designate/worker/tasks/zone.py:869).
  • Purpose: record the outcome of a push to the nameservers: the zone and its records become ACTIVE, ERROR, or are removed.
  • Flow (designate/central/service.py:2543):
    1. Lock the zone, read it (designate/central/service.py:2552).
    2. If the reported action is not the zone's current action, keep the zone as it is: a newer change superseded the reported one (designate/central/service.py:2553, designate/central/service.py:2559).
    3. A zone being deleted that did not end in ERROR is treated as NO_ZONE (designate/central/service.py:2554).
    4. Apply the state machine _update_zone_or_record_status (designate/central/service.py:2637):
      • SUCCESS with serial >= zone.serial, for a PENDING or ERROR zone: CREATE/UPDATE → NONE/ACTIVE; DELETE → NONE/DELETED;
      • ERROR on a PENDING zone with serial >= zone.serial or serial == 0: status ERROR;
      • NO_ZONE: CREATE/UPDATE → CREATE/ERROR (the zone must be created again); DELETE → NONE/DELETED.
    5. DELETED zones are soft-deleted (deleted = id, deleted_at = now, designate/storage/sqlalchemy/base.py:547); other zones are updated (designate/central/service.py:2573).
    6. Records of the zone in PENDING (and ERROR for SUCCESS) with serial <= <reported serial> get the same transition (designate/central/service.py:2598).
  • Idempotency: replaying the same cast is harmless, the transitions only apply to PENDING/ERROR rows.
  • Cost: one SELECT of the zone, one UPDATE, one SELECT of the pending records and one UPDATE per changed record.
  • Recorded cases: _rewrite/cases-zone-status_stable-2026.2.json (see rewrite pack § 3).

Zone import thread

  • Trigger: create_zone_import starts _import_zone in the service thread group and returns PENDING immediately (designate/central/service.py:2895).
  • Flow: parse with dnspython (relativize=False, check_origin=False), drop the apex SOA and NS recordsets, keep sub-zone NS, then create_zone with the remaining recordsets (designate/central/service.py:2905, designate/central/service.py:2944). The task ends COMPLETE with zone_id, or ERROR with a fixed message (see errors).
  • Error behaviour: the import is lost if central stops before the thread finishes: the task stays PENDING forever. Nothing retries it.

worker (RPC server, topic worker)

create_zone / update_zone / delete_zone

  • Trigger: RPC cast on topic worker (designate/conf/worker.py:43), version 1.2 (designate/worker/rpcapi.py:43). Emitted by central after every zone or recordset change (designate/central/service.py:936, designate/central/service.py:1175, designate/central/service.py:1487), by the producer (designate/producer/tasks.py:270, designate/producer/tasks.py:323) and by the worker itself on recovery (designate/worker/tasks/zone.py:937).
  • Purpose: make the pool's DNS servers serve the zone at its current serial (or stop serving it).
  • Data read: the pool, lazily loaded through central get_pool and cached for the life of the process (designate/worker/service.py:133). A pool change is therefore only seen after a worker restart [inferred]. A pool without targets is re-read every 5 s until it has some (designate/worker/service.py:90).
  • Flow (designate/worker/service.py:148, designate/worker/tasks/zone.py:347):
    1. ZoneActor: for every pool target, in parallel ([service:worker]/threads, 200), call the backend driver (create_zone, update_zone or delete_zone) and send a NOTIFY to the target's host:port option (designate/worker/tasks/zone.py:100). Each target is retried poll_max_retries (10) times, poll_retry_interval (15 s) apart (designate/worker/tasks/zone.py:165). When the pool has a catalog zone, the backend is not called: only a NOTIFY of the catalog zone is sent for creations and deletions, the secondaries discover the member zone from it (designate/worker/tasks/zone.py:120).
    2. If fewer than threshold_percentage (100 %) of the targets succeeded, set the zone ERROR through update_status and stop (designate/worker/tasks/zone.py:283).
    3. Wait poll_delay (5 s) (designate/worker/tasks/zone.py:331).
    4. ZonePoller: query the SOA of the zone on every pool nameserver (designate/worker/tasks/zone.py:543), with the nameserver's TSIG key when it has one. A serial >= zone.serial counts as positive; for a deletion, an absent zone (serial == 0) counts as positive (designate/worker/tasks/zone.py:386). Retry up to poll_max_retries times, stopping early if the zone's action changed meanwhile (designate/worker/tasks/zone.py:591).
    5. Report SUCCESS with the lowest serial seen, ERROR, or NO_ZONE when too many nameservers do not have the zone (designate/worker/tasks/zone.py:633), through update_status.
    6. Send a NOTIFY to every also_notifies entry of the pool (designate/worker/service.py:155).
  • Concurrency: none between workers: two workers may push the same zone at once (central casts without a server). The serial comparison and the action check in update_status make the last outcome win.
  • Error behaviour: backend exceptions are logged per attempt (designate/worker/tasks/zone.py:150); a missing TSIG key skips the target or the poll (designate/worker/tasks/zone.py:84, designate/worker/tasks/zone.py:671). Timeouts on SOA queries are logged and count as "no answer" (designate/worker/tasks/zone.py:482).
  • Cost: per zone change, targets × (backend call + NOTIFY) then up to poll_max_retries × nameservers SOA queries. The worst case before ERROR is the maximum propagation time poll_timeout × poll_max_retries + poll_max_retries × poll_retry_interval + poll_delay = 455 s with the defaults (designate/utils.py:354).
  • Recorded cases: _rewrite/cases-parse-query-results_stable-2026.2.json.

perform_zone_xfr / get_serial_number (secondary zones)

  • Trigger: cast perform_zone_xfr from central when a SECONDARY zone is created or its masters change (designate/central/service.py:939, designate/central/service.py:1098), from mdns on NOTIFY (designate/mdns/handler.py:183), from xfr_zone (designate/central/service.py:1235). get_serial_number is a call from central xfr_zone (designate/central/service.py:1227).
  • Flow: ZoneXfr does an AXFR from the masters in random order, every resolved address of each master, with [service:worker]/xfr_timeout (10 s) (designate/dnsutils.py:152), rebuilds zone and recordsets from the answer (designate/dnsutils.py:87), sets transferred_at and calls central update_zone(increment_serial=False) (designate/worker/tasks/zone.py:231). GetZoneSerial queries the SOA up to serial_max_retries (3) times, serial_retry_delay (1 s) apart, with serial_timeout (1 s) (designate/worker/tasks/zone.py:710).
  • Error behaviour: XFRFailure when no master answered, logged as a warning, the zone is left unchanged (designate/worker/tasks/zone.py:241).

recover_shard

  • Trigger: cast from the producer's worker_periodic_recovery with the producer's shard range (designate/producer/tasks.py:341).
  • Data read: SELECT … FROM zones WHERE shard BETWEEN <begin> AND <end> AND status = 'ERROR', plus zones PENDING whose serial is older than now - max_prop_time (designate/worker/tasks/zone.py:896).
  • Flow: re-cast create_zone, update_zone or delete_zone according to the zone's action (designate/worker/tasks/zone.py:937).
  • Cost: two queries per producer instance every 120 s; zones in ERROR are retried forever, every 120 s.

start_zone_export

  • Flow: count the recordsets; with [service:worker]/export_synchronous (default true), check the api_export_size quota and set the task COMPLETE with location = designate://v2/zones/tasks/exports/<id>/export, else ERROR (designate/worker/tasks/zone.py:965). The file itself is rendered on demand by GET …/export (designate/api/v2/controllers/zones/tasks/exports.py:41).

producer (timers, sharded)

All tasks are stevedore plugins of designate.producer_tasks; [service:producer]/enabled_tasks unset means every task (designate/plugin.py:91). Each task runs every [producer_task:<name>]/interval seconds in the service thread group, with stop_on_exception=False (designate/producer/service.py:93). Zones have a shard = first three hex digits of their id (0–4095, designate/storage/sqlalchemy/tables.py:56). With a tooz backend, producer instances join the producer group and each gets ceil(4096 / members) consecutive shards, recomputed on every join or leave (designate/coordination.py:172); without a backend, a single producer owns all shards (designate/coordination.py:216). Queries are paged by per_page (100) through central find_zones with a marker (designate/producer/tasks.py:78).

increment_serial

  • Data read: zones WHERE shard BETWEEN … AND increment_serial = 1 ORDER BY updated_at ASC LIMIT batch_size (designate/producer/tasks.py:291).
  • Flow: for each zone not being deleted, central increment_zone_serial sets serial = <Unix time> and increment_serial = 0, and rewrites the SOA record (designate/central/service.py:839, designate/storage/sqlalchemy/__init__.py:625); unless the zone also waits for a delayed NOTIFY, the zone is set UPDATE/PENDING and pushed (designate/producer/tasks.py:318).
  • Why: API changes only set the flag (designate/central/service.py:1109); the serial is incremented at most once per interval per zone, so a burst of recordset changes results in one push.
  • Ordering: the serial is the current Unix time, so two increments within the same second give the same serial [inferred]; the poller accepts >=.

delayed_notify

  • Data read: zones WHERE shard BETWEEN … AND delayed_notify = 1 AND increment_serial = 0 ORDER BY updated_at ASC LIMIT batch_size (designate/producer/tasks.py:247).
  • Flow: set UPDATE/PENDING if the zone was idle, cast worker update_zone, clear the flag through central update_zone (designate/producer/tasks.py:258). Set by pool NS changes (designate/central/service.py:516).

zone_purge

  • Data read: zones WHERE shard BETWEEN … AND deleted != '0' AND deleted_at <= now - time_threshold, LIMIT batch_size (designate/producer/tasks.py:120).
  • Flow: central purge_zones hard-deletes them, re-parenting their child zones first (designate/storage/sqlalchemy/__init__.py:675).

periodic_secondary_refresh

  • Flow: for SECONDARY zones already transferred, call central xfr_zone when now - transferred_at > zone.refresh (designate/producer/tasks.py:208). xfr_zone compares one master's serial with the stored one and casts an AXFR when it is higher (designate/central/service.py:1226).
  • Note: the run interval (3600 s) bounds the effective refresh: a zone with refresh = 600 is still checked at most hourly.

periodic_exists, service status tasks

  • periodic_exists emits dns.domain.exists and dns.zone.exists with audit_period_beginning/ending for every zone of the shards (designate/producer/tasks.py:169).
  • periodic_check_service_status marks UP services whose heartbeated_at is older than time_threshold (1800 s) DOWN (designate/producer/tasks.py:360). With a 3600 s interval, a dead service may stay UP for up to 90 minutes.
  • periodic_cleanup_stopped_service_status deletes STOPPED rows and, when time_threshold > 0, DOWN rows older than it (designate/producer/tasks.py:382).

mdns (DNS server)

  • Trigger: UDP and TCP sockets on [service:mdns]/listen (default 0.0.0.0:5354, designate/conf/mdns.py:29), one thread per UDP query and per TCP connection (designate/service.py:177, designate/service.py:330). TCP messages are length-prefixed (RFC 1035 § 4.2.2) and pipelined queries are handled sequentially (designate/service.py:260).
  • Middleware: SerializationMiddleware decodes the wire message with a keyring backed by the tsigkeys table (designate/dnsmiddleware.py:68, designate/dnsutils.py:40); messages with an unknown TSIG key or a bad signature are dropped without answer (designate/dnsmiddleware.py:94, designate/dnsmiddleware.py:128). TsigInfoMiddleware attaches the key to the request (designate/dnsmiddleware.py:155).
  • QUERY AXFR/IXFR (designate/mdns/handler.py:241): IXFR is answered with a full AXFR. The zone is looked up by name, scoped by the TSIG key (POOL scope → that pool, ZONE scope → that zone) or, unsigned, the default pool; query_enforce_tsig refuses unsigned queries (designate/mdns/handler.py:190). Records are streamed as SOA, all other recordsets, SOA (designate/mdns/handler.py:266), split into several messages when they exceed max_message_size (65535), each signed with TSIG when the request was (designate/mdns/handler.py:487). A catalog zone is generated on the fly from the pool's zones (designate/mdns/handler.py:279). Unknown zone or forbidden → REFUSED.
  • QUERY other types (designate/mdns/handler.py:371): authoritative answer from the database (ancestor walk to find the zone, same pool scoping); every miss is REFUSED, never NXDOMAIN (designate/mdns/handler.py:399).
  • NOTIFY (designate/mdns/handler.py:91): find SECONDARY zones with the notified name in every pool, keep the one listing the sender as master, query the sender's SOA and cast perform_zone_xfr when the serial differs (designate/mdns/handler.py:163). Unknown zone → NOTAUTH; sender not a master → REFUSED.
  • Cost: an AXFR reads all recordsets of the zone with one query (find_recordsets_axfr); every DNS server of every pool AXFRs every changed zone.

sink (notification listener)

  • Trigger: oslo.messaging notification listener on one target per handler: exchange [handler:<name>]/control_exchange (nova or neutron), topics notification_topics (notifications), pool [service:sink]/listener_pool_name (designate/sink/service.py:85, designate/conf/sink.py:62). Only handlers named in [service:sink]/enabled_notification_handlers are loaded (default none, designate/conf/sink.py:43).
  • nova_fixed (designate/notification_handler/nova.py:26): compute.instance.create.end → one A/AAAA managed record per fixed IP, name built from formatv4/formatv6 (default %(hostname)s.%(zone)s), in the zone [handler:nova_fixed]/zone_id; compute.instance.delete.start → delete the managed records of instance_id.
  • neutron_floatingip (designate/notification_handler/neutron.py:26): floatingip.update.end with a fixed_ip_address → A record for the floating IP address; without it, or on floatingip.delete.start → delete.
  • Flow: create_managed_records creates or replaces the recordset with records carrying managed_plugin_name, managed_resource_type and managed_resource_id (designate/notification_handler/base.py:161, designate/central/service.py:2297); deletion removes them by those keys (designate/notification_handler/base.py:193).
  • Error behaviour: exceptions propagate to oslo.messaging (the message is requeued or dropped depending on the driver) [inferred]; a missing zone_id is logged and the event ignored (designate/notification_handler/nova.py:53). Invalid IPv6 addresses are skipped (designate/notification_handler/base.py:135).
  • Formats: the octets of the address are available as %(octet0)s… and %(ip_address)s (dots or colons replaced by dashes) (designate/notification_handler/base.py:70).

Heartbeats and coordination

  • Heartbeat (designate/heartbeat_emitter.py:53): every heartbeat_interval (10 s), each service casts update_service_status(ServiceStatus(service_name, hostname, status=UP, stats, capabilities, heartbeated_at)) to central (designate/heartbeat_emitter.py:129); on stop it sends STOPPED (designate/heartbeat_emitter.py:73). GET /v2/service_statuses reads this table.
  • tooz: central and producer start a coordinator when [coordination]/backend_url is set (designate/coordination.py:75); the producer also creates/joins the producer group and runs the watchers every run_watchers_interval (10 s) (designate/coordination.py:122). Without a backend there is no cross-process zone lock and no shard split: running two central or two producer processes without a backend is unsafe [inferred].

Source: reverse-spec-designate/05-async-processing_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.