java-topology/defects/rabbitmq/patch/rmq-0005-export-binding-qnames-sets.md

3.3 KiB
Raw Blame History

UNDF: UNDF-2026-000000473

rmq-0005: rabbit_mgmt_wm_definitions export_binding O(B×Q) → O(B+Q)

Location

deps/rabbitmq_management/src/rabbit_mgmt_wm_definitions.erl Lines 5458 (all_definitions/2) and 115120 (vhost_definitions/2)

Severity

MEDIUM — triggered by GET /api/definitions (HTTP API export). Automation tools, monitoring pipelines, and GitOps workflows hit this endpoint on a schedule. In large deployments it serialises the entire broker definition and becomes a CPU hotspot.

Description

all_definitions/2 builds QNames as a plain list of {Name, VHost} tuples, then calls export_binding(B, QNames) for every binding B in a list comprehension. Inside export_binding/2, lists:member({Dest, VHost}, QNames) performs a linear scan of all queue-name tuples for every binding processed.

With B bindings and Q queues this is O(B × Q). In a busy broker (e.g., a topic exchange with 10 k routing keys matched against 1 k queues) the export scans 10 M tuple comparisons per API call. If called every 15 seconds by a monitoring tool, this is 666 k redundant comparisons/second sustained.

Root Cause

%% all_definitions/2 — deps/rabbitmq_management/src/rabbit_mgmt_wm_definitions.erl
Qs     = [Q || Q <- rabbit_mgmt_wm_queues:basic(ReqData), export_queue(Q)],
QNames = [{pget(name, Q), pget(vhost, Q)} || Q <- Qs],     %% ← plain list
Bs     = [B || B <- rabbit_mgmt_wm_bindings:basic(ReqData),
               export_binding(B, QNames)],                  %% ← O(B) calls

%% export_binding/2 — same file
export_binding(Binding, Qs) ->
    ...
    ( (DestType =:= queue andalso lists:member({Dest, VHost}, Qs))  %% ← O(Q) scan
    ...

vhost_definitions/2 has the identical pattern at lines 115120 using QNames.

Fix

Convert QNames to an ordsets:ordset() (or sets:set() with {version, 2}) before the binding comprehension. Pass the set to export_binding/2 and use sets:is_element/2 in place of lists:member/2.

%% all_definitions/2 — fixed
Qs      = [Q || Q <- rabbit_mgmt_wm_queues:basic(ReqData), export_queue(Q)],
QNames  = [{pget(name, Q), pget(vhost, Q)} || Q <- Qs],
QNamesS = sets:from_list(QNames, [{version, 2}]),          %% ← O(Q log Q) once
Bs      = [B || B <- rabbit_mgmt_wm_bindings:basic(ReqData),
               export_binding(B, QNamesS)],                 %% ← O(B) calls

%% export_binding/2 — fixed
export_binding(Binding, Qs) ->
    ...
    ( (DestType =:= queue andalso sets:is_element({Dest, VHost}, Qs))  %% ← O(1)
    ...

The same change applies to vhost_definitions/2.

Complexity

Before After
Build QNames O(Q) O(Q) + O(Q log Q) set build
Per-binding check O(Q) scan O(1) set lookup
Total export O(B × Q) O(Q log Q + B)

At B=10 k bindings, Q=1 k queues: 10 M ops → 14 k ops (714x improvement). At B=100 k bindings, Q=10 k queues: 10^9 ops → 133 k ops (7500x improvement).

Context

GET /api/definitions is the standard RabbitMQ backup/export endpoint. It is called:

  • By rabbitmqctl export_definitions
  • By monitoring pipelines (Datadog, Prometheus exporters)
  • By GitOps automation to detect config drift

Slow exports block the HTTP listener and increase memory pressure from large response bodies held in flight while CPU-bound scan is underway.