Skip to content

Commit 479f654

Browse files
committed
QQ: record consumer metrics on the Ra leader, not the consumer's node
consumer_created/updated/deleted rows were recorded on the consuming channel's node, unlike other queue metrics which live on the Ra leader. Cross-node updates went through an erpc:cast that silently dropped on a disconnected node, leaving stale rows with no retry. Move ownership to the leader: the checkout apply clause and state_enter(leader/follower, ...) now write/clear these rows locally, since mod_call effects only ever run on the current leader and must never crash it. Also fan out a one-off cleanup on the v9 machine version bump, to remove stale rows any node may have written under the old scheme.
1 parent c34ccc9 commit 479f654

3 files changed

Lines changed: 176 additions & 67 deletions

File tree

deps/rabbit/src/rabbit_fifo.erl

Lines changed: 94 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,21 @@ update_config(Conf, State) ->
260260
apply(Meta, {machine_version, FromVersion, ToVersion}, VXState) ->
261261
%% machine version upgrades cant be done in apply_
262262
State = convert(Meta, FromVersion, ToVersion, VXState),
263-
{State, ok, [{aux, {dlx, setup}}]};
263+
Effects = case ToVersion of
264+
9 ->
265+
%% v9 moved consumer metrics ownership from the
266+
%% consuming channel's node to the current Ra leader;
267+
%% clear out any stale rows earlier versions may have
268+
%% left on other nodes and repopulate authoritatively
269+
#?STATE{cfg = #cfg{resource = QRes}} = State,
270+
[{mod_call, rabbit_quorum_queue,
271+
consumer_metrics_v9_upgrade,
272+
[QRes, consumer_metrics_rows(State)]},
273+
{aux, {dlx, setup}}];
274+
_ ->
275+
[{aux, {dlx, setup}}]
276+
end,
277+
{State, ok, Effects};
264278
apply(#{system_time := Ts} = Meta, Cmd,
265279
#?STATE{reclaimable_bytes = ReclBytes} = State) ->
266280
%% Add estimated reclaimable bytes.
@@ -492,7 +506,23 @@ apply_(#{index := Idx} = Meta,
492506
{Consumer, State1} = update_consumer(Meta, ConsumerKey, ConsumerId,
493507
ConsumerMeta, Spec, Priority,
494508
Timeout, State0),
495-
{State2, Effs} = activate_next_consumer(State1, []),
509+
WasActive = is_active(ConsumerKey, State1),
510+
{State2, Effs0} = activate_next_consumer(State1, []),
511+
{Active, ActivityStatus} = consumer_activity_status(ConsumerKey, State2),
512+
Effs = case {WasActive, Active} of
513+
{false, true} ->
514+
%% activate_next_consumer/2 has already emitted the
515+
%% metrics effect as part of promoting this consumer
516+
Effs0;
517+
_ ->
518+
%% record (or refresh) the consumer metrics on the
519+
%% leader; needed here as activate_next_consumer/2 does
520+
%% not emit anything for the competing consumer strategy
521+
%% nor for a consumer that stays/becomes non-active
522+
[consumer_metrics_effect(State2, Consumer, Active,
523+
ActivityStatus)
524+
| Effs0]
525+
end,
496526

497527
%% reply with a consumer infos
498528
Reply = {ok, consumer_info(ConsumerKey, Consumer, State2)},
@@ -1055,7 +1085,7 @@ state_enter(leader,
10551085
waiting_consumers = WaitingConsumers,
10561086
cfg = #cfg{resource = QRes,
10571087
dead_letter_handler = DLH},
1058-
dlx = DlxState}) ->
1088+
dlx = DlxState} = State) ->
10591089
% return effects to monitor all current consumers and enqueuers
10601090
Pids = lists:usort(maps:keys(Enqs)
10611091
++ [P || ?CONSUMER_PID(P) <- maps:values(Cons)]
@@ -1064,7 +1094,12 @@ state_enter(leader,
10641094
Nots = [{send_msg, P, leader_change, ra_event} || P <- Pids],
10651095
NodeMons = lists:usort([{monitor, node, node(P)} || P <- Pids]),
10661096
NotifyDecs = notify_decorators_startup(QRes),
1067-
Effects = Mons ++ Nots ++ NodeMons ++ [NotifyDecs],
1097+
%% (re-)populate the local consumer metrics table with the current
1098+
%% consumers of this queue: this node is now authoritative for them
1099+
ConsumerMetrics = {mod_call, rabbit_quorum_queue,
1100+
bulk_update_consumer_metrics,
1101+
[QRes, consumer_metrics_rows(State)]},
1102+
Effects = Mons ++ Nots ++ NodeMons ++ [NotifyDecs, ConsumerMetrics],
10681103

10691104
case DLH of
10701105
at_least_once ->
@@ -1073,6 +1108,18 @@ state_enter(leader,
10731108
ok
10741109
end,
10751110
Effects;
1111+
state_enter(follower, #?STATE{cfg = #cfg{resource = QRes,
1112+
dead_letter_handler = DLH},
1113+
dlx = DlxState}) ->
1114+
%% this node is no longer authoritative for this queue's consumer
1115+
%% metrics, drop the locally cached rows
1116+
case DLH of
1117+
at_least_once ->
1118+
ensure_worker_terminated(DlxState);
1119+
_ ->
1120+
ok
1121+
end,
1122+
[{mod_call, rabbit_quorum_queue, delete_local_consumer_metrics, [QRes]}];
10761123
state_enter(eol, #?STATE{enqueuers = Enqs,
10771124
consumers = Cons0,
10781125
waiting_consumers = WaitingConsumers0}) ->
@@ -1786,21 +1833,56 @@ cancel_consumer(Meta, ConsumerKey,
17861833
end
17871834
end.
17881835

1789-
consumer_update_active_effects(#?STATE{cfg = #cfg{resource = QName}} = State,
1836+
%% is this consumer currently entitled to receive deliveries and what its
1837+
%% externally-visible activity status is, given the queue's consumer strategy
1838+
consumer_activity_status(_ConsumerKey,
1839+
#?STATE{cfg = #cfg{consumer_strategy = competing}}) ->
1840+
{true, up};
1841+
consumer_activity_status(ConsumerKey,
1842+
#?STATE{cfg = #cfg{consumer_strategy = single_active}} = State) ->
1843+
case is_active(ConsumerKey, State) of
1844+
true -> {true, single_active};
1845+
false -> {false, waiting}
1846+
end.
1847+
1848+
consumer_metrics_effect(#?STATE{cfg = #cfg{resource = QName}},
1849+
#consumer{cfg = #consumer_cfg{meta = Meta,
1850+
pid = CPid,
1851+
tag = CTag}},
1852+
Active, ActivityStatus) ->
1853+
Ack = maps:get(ack, Meta, undefined),
1854+
Prefetch = maps:get(prefetch, Meta, undefined),
1855+
Args = maps:get(args, Meta, []),
1856+
{mod_call, rabbit_quorum_queue, update_consumer_handler,
1857+
[QName, {CTag, CPid}, false, Ack, Prefetch, Active, ActivityStatus, Args]}.
1858+
1859+
%% metrics rows for every consumer of this queue, active or waiting, used to
1860+
%% (re-)populate the local consumer metrics table when this node becomes leader
1861+
consumer_metrics_rows(#?STATE{consumers = Cons,
1862+
waiting_consumers = WaitingConsumers} = State) ->
1863+
[consumer_metrics_row(ConsumerKey, Consumer, State)
1864+
|| {ConsumerKey, Consumer} <- maps:to_list(Cons) ++ WaitingConsumers].
1865+
1866+
consumer_metrics_row(ConsumerKey,
1867+
#consumer{cfg = #consumer_cfg{pid = Pid,
1868+
tag = Tag,
1869+
meta = Meta}},
1870+
State) ->
1871+
{Active, ActivityStatus} = consumer_activity_status(ConsumerKey, State),
1872+
{Pid, Tag, maps:get(ack, Meta, undefined), maps:get(prefetch, Meta, undefined),
1873+
Active, ActivityStatus, maps:get(args, Meta, [])}.
1874+
1875+
consumer_update_active_effects(State,
17901876
#consumer{cfg = #consumer_cfg{meta = Meta,
17911877
pid = CPid,
17921878
tag = CTag,
17931879
credit_mode = Mode},
17941880
delivery_count = DeliveryCount,
17951881
credit = Credit,
1796-
drain = Drain},
1882+
drain = Drain} = Consumer,
17971883
Active, ActivityStatus, Effects0) ->
1798-
Ack = maps:get(ack, Meta, undefined),
1799-
Prefetch = maps:get(prefetch, Meta, undefined),
1800-
Args = maps:get(args, Meta, []),
1801-
Effects = [{mod_call, rabbit_quorum_queue, update_consumer_handler,
1802-
[QName, {CTag, CPid}, false, Ack, Prefetch,
1803-
Active, ActivityStatus, Args]} | Effects0],
1884+
Effects = [consumer_metrics_effect(State, Consumer, Active, ActivityStatus)
1885+
| Effects0],
18041886
case Mode of
18051887
{credited, _} when map_get(link_state_properties, Meta) =:= true ->
18061888
Avail = case Active of

deps/rabbit/src/rabbit_quorum_queue.erl

Lines changed: 70 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@
3434
deliver/3]).
3535
-export([dead_letter_publish/5]).
3636
-export([cluster_state/1, status/1, status/2]).
37-
-export([update_consumer_handler/8, update_consumer/9]).
38-
-export([cancel_consumer_handler/2, cancel_consumer/3]).
37+
-export([update_consumer_handler/8]).
38+
-export([cancel_consumer_handler/2]).
39+
-export([bulk_update_consumer_metrics/2, delete_local_consumer_metrics/1]).
40+
-export([consumer_metrics_v9_upgrade/2]).
3941
-export([become_leader/2, handle_tick/3, spawn_deleter/1]).
4042
-export([rpc_delete_metrics/1,
4143
key_metrics_rpc/1]).
@@ -461,29 +463,70 @@ single_active_consumer_on(Q) ->
461463
QArguments = amqqueue:get_arguments(Q),
462464
table_lookup(QArguments, <<"x-single-active-consumer">>, false).
463465

466+
%% these mod_call effects are always applied on the current Ra leader
467+
%% (see ra_server_proc:handle_effect/5), which is where quorum queue
468+
%% consumer metrics are recorded, so no cross-node dispatch is needed
464469
update_consumer_handler(QName, {ConsumerTag, ChPid}, Exclusive, AckRequired,
465470
Prefetch, Active, ActivityStatus, Args) ->
466-
catch rabbit_queue_type_util:local_or_remote_handler(ChPid, ?MODULE, update_consumer,
467-
[QName, ChPid, ConsumerTag,
468-
Exclusive, AckRequired,
469-
Prefetch, Active,
470-
ActivityStatus, Args]).
471-
472-
update_consumer(QName, ChPid, ConsumerTag, Exclusive, AckRequired, Prefetch,
473-
Active, ActivityStatus, Args) ->
474471
catch rabbit_core_metrics:consumer_updated(ChPid, ConsumerTag, Exclusive,
475-
AckRequired,
476-
QName, Prefetch, Active,
477-
ActivityStatus, Args).
472+
AckRequired, QName, Prefetch,
473+
Active, ActivityStatus, Args).
478474

479475
cancel_consumer_handler(QName, {ConsumerTag, ChPid}) ->
480-
catch rabbit_queue_type_util:local_or_remote_handler(ChPid, ?MODULE, cancel_consumer,
481-
[QName, ChPid, ConsumerTag]).
482-
483-
cancel_consumer(QName, ChPid, ConsumerTag) ->
484476
catch rabbit_core_metrics:consumer_deleted(ChPid, ConsumerTag, QName),
485477
rabbit_queue_type_util:notify_consumer_deleted(ChPid, ConsumerTag, QName, ?INTERNAL_USER).
486478

479+
%% called when this node becomes the Ra leader for QName: the previous
480+
%% leader's local rows (if any) were already dropped when it stepped down
481+
%% (see delete_local_consumer_metrics/1), but bulk-replace defensively so
482+
%% this table is never a mix of two leader terms' data
483+
%%
484+
%% this mod_call effect (like all mod_call effects) is not protected by
485+
%% ra_server_proc, so a crash here would take down the whole Ra server;
486+
%% the consumer_created table may also legitimately not exist, e.g. in
487+
%% rabbit_fifo_int_SUITE which runs bare ra clusters without core_metrics
488+
bulk_update_consumer_metrics(QName, ConsumerMetrics) ->
489+
catch bulk_update_consumer_metrics0(QName, ConsumerMetrics).
490+
491+
bulk_update_consumer_metrics0(QName, ConsumerMetrics) ->
492+
delete_local_consumer_metrics(QName),
493+
%% quorum queues don't support exclusive consumers
494+
[rabbit_core_metrics:consumer_created(ChPid, ConsumerTag, false,
495+
AckRequired, QName, Prefetch,
496+
Active, ActivityStatus, Args)
497+
|| {ChPid, ConsumerTag, AckRequired, Prefetch, Active,
498+
ActivityStatus, Args} <- ConsumerMetrics],
499+
ok.
500+
501+
delete_local_consumer_metrics(QName) ->
502+
catch ets:match_delete(consumer_created,
503+
{{QName, '_', '_'}, '_', '_', '_', '_', '_', '_'}),
504+
ok.
505+
506+
%% one-off cleanup run when a queue's machine version crosses into v9: prior
507+
%% versions wrote consumer_created from the consuming channel's node instead
508+
%% of the Ra leader's, so any node in the cluster could be holding a stale
509+
%% or duplicate row for this queue; wipe them all and let the current
510+
%% leader (the only node this mod_call effect runs on) repopulate
511+
%% authoritatively
512+
%%
513+
%% see bulk_update_consumer_metrics/2 for why this must never crash
514+
consumer_metrics_v9_upgrade(QName, ConsumerMetrics) ->
515+
catch consumer_metrics_v9_upgrade0(QName, ConsumerMetrics).
516+
517+
consumer_metrics_v9_upgrade0(QName, ConsumerMetrics) ->
518+
case rabbit_amqqueue:lookup(QName) of
519+
{ok, Q} when ?is_amqqueue(Q) ->
520+
Nodes = get_nodes(Q),
521+
_ = [_ = erpc_call(Node, ?MODULE, delete_local_consumer_metrics,
522+
[QName], ?RPC_TIMEOUT)
523+
|| Node <- Nodes, Node =/= node()],
524+
ok;
525+
_ ->
526+
ok
527+
end,
528+
bulk_update_consumer_metrics(QName, ConsumerMetrics).
529+
487530
become_leader(_QName, _Name) ->
488531
%% noop now as we instead rely on the promt tick_timeout + repair to update
489532
%% the meta data store after a leader change
@@ -1254,38 +1297,16 @@ consume(Q, Spec, QState0) when ?amqqueue_is_quorum(Q) ->
12541297
%% in credit_reply
12551298
link_state_properties => true},
12561299
case rabbit_fifo_client:checkout(ConsumerTag, Mode, ConsumerMeta, QState0) of
1257-
{ok, Infos, QState} ->
1258-
%% this info key was added in QQ v8
1259-
IsSac = maps:get(consumer_strategy, Infos, competing) == single_active,
1260-
case IsSac orelse single_active_consumer_on(Q) of
1261-
true ->
1262-
ActivityStatus = case Infos of
1263-
#{is_active := true} ->
1264-
single_active;
1265-
_ ->
1266-
waiting
1267-
end,
1268-
rabbit_core_metrics:consumer_created(ChPid, ConsumerTag,
1269-
ExclusiveConsume,
1270-
AckRequired, QName,
1271-
Prefetch,
1272-
ActivityStatus == single_active,
1273-
ActivityStatus, Args),
1274-
rabbit_queue_type_util:notify_consumer_created(
1275-
ChPid, ConsumerTag, ExclusiveConsume, AckRequired,
1276-
QName, Prefetch, Args, none, ActingUser),
1277-
{ok, QState};
1278-
false ->
1279-
rabbit_core_metrics:consumer_created(ChPid, ConsumerTag,
1280-
ExclusiveConsume,
1281-
AckRequired, QName,
1282-
Prefetch, true,
1283-
up, Args),
1284-
rabbit_queue_type_util:notify_consumer_created(
1285-
ChPid, ConsumerTag, ExclusiveConsume, AckRequired,
1286-
QName, Prefetch, Args, none, ActingUser),
1287-
{ok, QState}
1288-
end;
1300+
{ok, _Infos, QState} ->
1301+
%% the consumer_created/updated rabbit_core_metrics rows for this
1302+
%% consumer are recorded on the Ra leader as part of applying the
1303+
%% checkout command (see rabbit_fifo:consumer_metrics_effect/4),
1304+
%% not here, as this code may run on a different node than the
1305+
%% leader
1306+
rabbit_queue_type_util:notify_consumer_created(
1307+
ChPid, ConsumerTag, ExclusiveConsume, AckRequired,
1308+
QName, Prefetch, Args, none, ActingUser),
1309+
{ok, QState};
12891310
Err ->
12901311
consume_error(Err, QName)
12911312
end.

deps/rabbit/test/rabbit_fifo_SUITE.erl

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1231,6 +1231,7 @@ return_auto_checked_out_test(Config) ->
12311231
{State1, #{key := CKey,
12321232
next_msg_id := MsgId},
12331233
[_Monitor,
1234+
{mod_call, rabbit_quorum_queue, update_consumer_handler, _},
12341235
{log_ext, [1], _Fun1, _} ]} = checkout(Config, ?LINE, Cid, 1, State0),
12351236
% return should include another delivery
12361237
{State2, _, Effects} = apply(meta(Config, 3),
@@ -1246,7 +1247,7 @@ return_auto_checked_out_test(Config) ->
12461247

12471248
{State4, #{key := CKey2,
12481249
next_msg_id := MsgId3},
1249-
[_, {log_ext, [1], _Fun3, _} ]} = checkout(Config, ?LINE, Cid, 1, State3),
1250+
[_, _, {log_ext, [1], _Fun3, _} ]} = checkout(Config, ?LINE, Cid, 1, State3),
12501251

12511252
[{_, {_, #{delivery_count := 1,
12521253
acquired_count := 2}}}]
@@ -1262,6 +1263,7 @@ requeue_test(Config) ->
12621263
{State1, #{key := CKey,
12631264
next_msg_id := MsgId},
12641265
[_Monitor,
1266+
{mod_call, rabbit_quorum_queue, update_consumer_handler, _},
12651267
{log_ext, [1], _Fun, _}]} = checkout(Config, ?LINE, Cid, 1, State0),
12661268

12671269
[{MsgId, {H1, _}}] = rabbit_fifo:get_checked_out(CKey, MsgId, MsgId, State1),
@@ -2096,8 +2098,9 @@ single_active_consumer_state_enter_leader_include_waiting_consumers_test(Config)
20962098
ct:pal("Efx ~p", [Effects]),
20972099
%% 2 effects for each consumer process (channel process),
20982100
%% 1 effect for the node,
2099-
%% 1 for decorators
2100-
?assertEqual(2 * 3 + 1 + 1, length(Effects)).
2101+
%% 1 for decorators,
2102+
%% 1 for the bulk consumer metrics resync
2103+
?assertEqual(2 * 3 + 1 + 1 + 1, length(Effects)).
21012104

21022105
single_active_consumer_state_enter_eol_include_waiting_consumers_test(Config) ->
21032106
Resource = rabbit_misc:r("/", queue, ?FUNCTION_NAME_B),
@@ -2480,10 +2483,13 @@ single_active_consumer_priority_test(Config) ->
24802483
assert_update_consumer_handler_state_transition(C2, Resource, true, single_active, lists:nth(2, ModCalls)),
24812484
%% C1 should transition to waiting
24822485
assert_update_consumer_handler_state_transition(C1, Resource, false, waiting, lists:nth(3, ModCalls)),
2483-
%% C3 is added as single_active
2484-
assert_update_consumer_handler_state_transition(C3, Resource, true, single_active, lists:nth(4, ModCalls)),
2486+
%% C3 is added as waiting (the active consumer, C2, still has a message
2487+
%% checked out so cannot be pre-empted immediately)
2488+
assert_update_consumer_handler_state_transition(C3, Resource, false, waiting, lists:nth(4, ModCalls)),
2489+
%% C3 is promoted to single_active once C2's message is settled
2490+
assert_update_consumer_handler_state_transition(C3, Resource, true, single_active, lists:nth(5, ModCalls)),
24852491
%% C2 should transition as waiting
2486-
assert_update_consumer_handler_state_transition(C2, Resource, false, waiting, lists:nth(5, ModCalls)),
2492+
assert_update_consumer_handler_state_transition(C2, Resource, false, waiting, lists:nth(6, ModCalls)),
24872493

24882494
ok.
24892495

0 commit comments

Comments
 (0)