Describe the bug
Enabling firehose tracing on a vhost is a non-atomic read-modify-write of the single
shared rabbit app-env list trace_vhosts (rabbit_trace:update_config/1):
update_config(Fun) ->
VHosts0 = vhosts_with_tracing_enabled(), %% READ
VHosts = Fun(VHosts0), %% MODIFY
application:set_env(rabbit, ?TRACE_VHOSTS, VHosts), %% WRITE (replaces whole list)
...
When tracing is enabled on multiple vhosts concurrently, two callers READ the same stale
list and the second WRITE overwrites the first. Vhosts are silently dropped from
trace_vhosts even though every enable returns ok; their tracer never activates and
never recovers. All enable paths funnel through here (rabbitmqctl trace_on,
PUT /api/vhosts {"tracing": true}, the management UI), so all are affected.
Source: https://github.com/rabbitmq/rabbitmq-server/blob/main/deps/rabbit/src/rabbit_trace.erl
Versions
- RabbitMQ: 4.3.2 (code path unchanged on
main)
- Erlang/OTP: 27
- OS / image: official
rabbitmq:4.3.2-management Docker image
- Client: none — reproduced with
rabbitmqctl only
Suggested fix
Serialize all trace_vhosts toggles so the read-modify-write is atomic (e.g. route
start/1/stop/1 through a single gen_server, or guard update_config/1 with a node
lock). Happy to submit a PR if you confirm the preferred approach.
Reproduction steps
On a default single-node RabbitMQ broker (e.g. the official rabbitmq:4.3.2 image,
no extra config), create N vhosts, then enable tracing on all of them simultaneously
and count how many actually land in trace_vhosts. This rabbitmqctl eval spawns one
Erlang process per vhost, releases them from a barrier at once, and returns the count of
survivors:
# create 50 vhosts
for i in $(seq 1 50); do rabbitmqctl add_vhost "v$i"; done
# enable tracing on all 50 concurrently from inside the broker, then count survivors
rabbitmqctl eval '
VHs=[list_to_binary("v"++integer_to_list(I))||I<-lists:seq(1,50)],
P=self(),
Ps=[spawn(fun()->receive go->ok end,catch rabbit_trace:start(V),P!{done,self()} end)||V<-VHs],
[X!go||X<-Ps],
[receive{done,_}->ok end||_<-Ps],
{ok,En}=application:get_env(rabbit,trace_vhosts),
length([V||V<-En,lists:member(V,VHs)]).'
Expected: 50 — every successfully-enabled vhost is present in trace_vhosts.
Actual: far fewer. On RabbitMQ 4.3.2 / OTP 27 this printed 13 on the first attempt —
37 of 50 enables were lost.
Expected behavior
Enabling tracing on a vhost is durable and independent of concurrency: if N enable
operations each return ok, all N vhosts should be present in trace_vhosts and have an
active tracer. Concurrent enables on different vhosts must not lose each other's updates.
Additional context
Root cause is a classic lost update: rabbit_trace:update_config/1 does
get_env -> modify -> set_env on the shared rabbit/trace_vhosts list with no
serialization, so concurrent callers overwrite each other.
- Affects all enable entry points, since they all call
rabbit_trace:start/1 ->
update_config/1: rabbitmqctl trace_on, PUT /api/vhosts {"tracing": true} (HTTP API),
and the management UI checkbox.
- The dropped vhost stays broken until tracing is explicitly toggled again for it — the
per-channel cache (rabbit_trace:init/1) is only refreshed when tracing toggles.
- Originally hit in an automated test harness that provisions a fresh vhost with tracing per
test; reduced to the minimal rabbitmqctl eval reproduction above.
- Suggested fix: serialize toggles through a single
gen_server (or a node-global lock) so
the read-modify-write is atomic.
Describe the bug
Enabling firehose tracing on a vhost is a non-atomic read-modify-write of the single
shared
rabbitapp-env listtrace_vhosts(rabbit_trace:update_config/1):When tracing is enabled on multiple vhosts concurrently, two callers READ the same stale
list and the second WRITE overwrites the first. Vhosts are silently dropped from
trace_vhostseven though every enable returnsok; their tracer never activates andnever recovers. All enable paths funnel through here (
rabbitmqctl trace_on,PUT /api/vhosts {"tracing": true}, the management UI), so all are affected.Source: https://github.com/rabbitmq/rabbitmq-server/blob/main/deps/rabbit/src/rabbit_trace.erl
Versions
main)rabbitmq:4.3.2-managementDocker imagerabbitmqctlonlySuggested fix
Serialize all
trace_vhoststoggles so the read-modify-write is atomic (e.g. routestart/1/stop/1through a singlegen_server, or guardupdate_config/1with a nodelock). Happy to submit a PR if you confirm the preferred approach.
Reproduction steps
On a default single-node RabbitMQ broker (e.g. the official
rabbitmq:4.3.2image,no extra config), create N vhosts, then enable tracing on all of them simultaneously
and count how many actually land in
trace_vhosts. Thisrabbitmqctl evalspawns oneErlang process per vhost, releases them from a barrier at once, and returns the count of
survivors:
Expected:
50— every successfully-enabled vhost is present intrace_vhosts.Actual: far fewer. On RabbitMQ 4.3.2 / OTP 27 this printed
13on the first attempt —37 of 50 enables were lost.
Expected behavior
Enabling tracing on a vhost is durable and independent of concurrency: if N enable
operations each return
ok, all N vhosts should be present intrace_vhostsand have anactive tracer. Concurrent enables on different vhosts must not lose each other's updates.
Additional context
Root cause is a classic lost update:
rabbit_trace:update_config/1doesget_env -> modify -> set_env on the shared
rabbit/trace_vhostslist with noserialization, so concurrent callers overwrite each other.
rabbit_trace:start/1->update_config/1:rabbitmqctl trace_on,PUT /api/vhosts {"tracing": true}(HTTP API),and the management UI checkbox.
per-channel cache (
rabbit_trace:init/1) is only refreshed when tracing toggles.test; reduced to the minimal
rabbitmqctl evalreproduction above.gen_server(or a node-global lock) sothe read-modify-write is atomic.