Skip to content

Commit 35f36f4

Browse files
committed
Add per-node ingress byte tracking to rabbit_fifo v9
Implement the OSS infrastructure for ingress-dependent quorum queue leader rebalancing. This adds: - Per-node enqueue byte counters in the replicated state machine, updated deterministically on every enqueue via apply_enqueue/6 - Exposure of cumulative totals via overview/1, propagated to all replicas on every tick - Aux state leaky integrators (via ra_li) to compute smoothed per-node ingress rates, updated on handle_tick - A get_ingress_rates aux query that returns bytes/second per node, callable locally without cross-node coordination The new field ingress_bytes_by_node defaults to #{} and is never read by OSS, so pre-v9 members in mixed clusters are unaffected. The aux state version is bumped to aux_v5 with an upgrade clause for rolling restarts. Decay time is configurable via persistent_term. Tests added to verify accumulation, snapshot persistence, v8→v9 conversion, and rate decay behavior under controlled tick timing.
1 parent 10cca06 commit 35f36f4

6 files changed

Lines changed: 177 additions & 41 deletions

File tree

deps/rabbit/src/rabbit_fifo.erl

Lines changed: 93 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,7 @@
203203
delayed_op/0]).
204204

205205
-spec init(config()) -> state().
206-
init(#{name := Name,
207-
queue_resource := Resource} = Conf) ->
206+
init(#{name := Name, queue_resource := Resource} = Conf) ->
208207
update_config(Conf, #?STATE{cfg = #cfg{name = Name,
209208
resource = Resource}}).
210209

@@ -696,9 +695,12 @@ apply_(Meta, {nodeup, Node}, #?STATE{consumers = Cons0,
696695
apply_(_, {nodedown, _Node}, State) ->
697696
{State, ok};
698697
apply_(Meta, #purge_nodes{nodes = Nodes}, State0) ->
699-
{State, Effects} = lists:foldl(fun(Node, {S, E}) ->
698+
{State1, Effects} = lists:foldl(fun(Node, {S, E}) ->
700699
purge_node(Meta, Node, S, E)
701700
end, {State0, []}, Nodes),
701+
State = State1#?STATE{
702+
ingress_bytes_by_node =
703+
maps:without(Nodes, State1#?STATE.ingress_bytes_by_node)},
702704
{State, ok, Effects};
703705
apply_(Meta,
704706
#update_config{config = #{} = Conf},
@@ -967,8 +969,7 @@ credit_reply_resend_effect(#?STATE{waiting_consumers = Waiting,
967969
end, [], maps:merge(Consumers, maps:from_list(Waiting))).
968970

969971
convert_v8_to_v9(#{} = _Meta, StateV8) ->
970-
State = StateV8,
971-
State.
972+
erlang:append_element(StateV8, #{}).
972973

973974
purge_node(Meta, Node, State, Effects) ->
974975
lists:foldl(fun(Pid, {S0, E0}) ->
@@ -1171,7 +1172,8 @@ overview(#?STATE{consumers = Cons,
11711172
reclaimable_bytes_count => ReclaimableBytes,
11721173
smallest_raft_index => smallest_raft_index(State),
11731174
num_active_priorities => NumActivePriorities,
1174-
messages_by_priority => Detail
1175+
messages_by_priority => Detail,
1176+
ingress_bytes_by_node => State#?STATE.ingress_bytes_by_node
11751177
},
11761178
DlxOverview = dlx_overview(DlxState),
11771179
maps:merge(maps:merge(Overview, DlxOverview), SacOverview).
@@ -1205,7 +1207,13 @@ which_module(7) -> rabbit_fifo_v7;
12051207
which_module(8) -> rabbit_fifo_v8;
12061208
which_module(9) -> ?MODULE.
12071209

1208-
-define(AUX, aux_v4).
1210+
-define(AUX, aux_v5).
1211+
-define(DEFAULT_INGRESS_DECAY_MS, 60_000).
1212+
1213+
-record(ingress_aux,
1214+
{last_totals = #{} :: #{node() | undefined => non_neg_integer()},
1215+
estimators = #{} :: #{node() | undefined => ra_li:state()},
1216+
decay_ms = ?DEFAULT_INGRESS_DECAY_MS :: pos_integer()}).
12091217

12101218
-record(snapshot, {index :: ra:index(),
12111219
timestamp :: milliseconds(),
@@ -1220,7 +1228,8 @@ which_module(9) -> ?MODULE.
12201228
gc = #aux_gc{} :: #aux_gc{},
12211229
tick_pid :: undefined | pid(),
12221230
cache = #{} :: map(),
1223-
last_checkpoint :: tuple() | #snapshot{}
1231+
last_checkpoint :: tuple() | #snapshot{},
1232+
ingress = #ingress_aux{} :: #ingress_aux{}
12241233
}).
12251234

12261235
init_aux(Name) when is_atom(Name) ->
@@ -1234,11 +1243,38 @@ init_aux(Name) when is_atom(Name) ->
12341243
?SNAP_MIN_RECLAIMABLE_B}),
12351244
Range = max(1, SnapMinReclaimable - ?SNAP_MIN_RECLAIMABLE_LOW_B),
12361245
MinReclaimable = ?SNAP_MIN_RECLAIMABLE_LOW_B + rand:uniform(Range),
1246+
DecayMs = persistent_term:get(rabbit_fifo_ingress_decay_ms,
1247+
?DEFAULT_INGRESS_DECAY_MS),
12371248
#?AUX{name = Name,
12381249
last_checkpoint = #snapshot{index = 0,
12391250
timestamp = erlang:system_time(millisecond),
12401251
messages_total = 0,
1241-
min_reclaimable = MinReclaimable}}.
1252+
min_reclaimable = MinReclaimable},
1253+
ingress = #ingress_aux{decay_ms = DecayMs}}.
1254+
1255+
update_ingress(Overview, Nodes, #ingress_aux{last_totals = LastTotals,
1256+
estimators = Estimators0,
1257+
decay_ms = DecayMs} = Ingress) ->
1258+
NewTotals = maps:get(ingress_bytes_by_node, Overview, #{}),
1259+
Ts = erlang:monotonic_time(millisecond),
1260+
Estimators1 =
1261+
maps:fold(fun(Node, NewTotal, Est) ->
1262+
Delta = NewTotal - maps:get(Node, LastTotals, 0),
1263+
Li0 = maps:get(Node, Est, ra_li:new(DecayMs)),
1264+
Li1 = ra_li:update(Delta, Ts, Li0),
1265+
Est#{Node => Li1}
1266+
end, Estimators0, NewTotals),
1267+
ActiveNodes = sets:from_list(Nodes, [{version, 2}]),
1268+
Estimators = maps:filter(fun(Node, _) ->
1269+
Node =:= undefined orelse
1270+
sets:is_element(Node, ActiveNodes)
1271+
end, Estimators1),
1272+
Ingress#ingress_aux{last_totals = NewTotals,
1273+
estimators = Estimators}.
1274+
1275+
compute_ingress_rates(#ingress_aux{estimators = Estimators}) ->
1276+
Ts = erlang:monotonic_time(millisecond),
1277+
maps:map(fun(_Node, Li) -> ra_li:rate(Ts, Li) end, Estimators).
12421278

12431279
handle_aux(RaftState, Tag, Cmd, AuxV2, RaAux)
12441280
when element(1, AuxV2) == aux_v2 ->
@@ -1256,6 +1292,19 @@ handle_aux(RaftState, Tag, Cmd, AuxV3, RaAux)
12561292
last_checkpoint = element(8, AuxV3)
12571293
},
12581294
handle_aux(RaftState, Tag, Cmd, AuxV4, RaAux);
1295+
handle_aux(RaftState, Tag, Cmd, AuxV4, RaAux)
1296+
when element(1, AuxV4) == aux_v4 ->
1297+
DecayMs = persistent_term:get(rabbit_fifo_ingress_decay_ms,
1298+
?DEFAULT_INGRESS_DECAY_MS),
1299+
AuxV5 = #?AUX{name = element(2, AuxV4),
1300+
last_decorators_state = element(3, AuxV4),
1301+
last_consumer_timeout = element(4, AuxV4),
1302+
gc = element(5, AuxV4),
1303+
tick_pid = element(6, AuxV4),
1304+
cache = element(7, AuxV4),
1305+
last_checkpoint = element(8, AuxV4),
1306+
ingress = #ingress_aux{decay_ms = DecayMs}},
1307+
handle_aux(RaftState, Tag, Cmd, AuxV5, RaAux);
12591308
handle_aux(leader, cast, eval,
12601309
#?AUX{last_decorators_state = LastDec,
12611310
last_consumer_timeout = LastConTimeout0,
@@ -1348,22 +1397,25 @@ handle_aux(_RaftState, cast, {#return{msg_ids = MsgIds,
13481397
%% for returns with a delivery limit set we can just return as before
13491398
{no_reply, Aux0, RaAux0, [{append, Ret, {notify, Corr, Pid}}]}
13501399
end;
1351-
handle_aux(leader, _, {handle_tick, [QName, Overview0, Nodes]},
1352-
#?AUX{tick_pid = Pid} = Aux, RaAux) ->
1353-
Overview = Overview0#{members_info => ra_aux:members_info(RaAux)},
1354-
NewPid =
1355-
case process_is_alive(Pid) of
1356-
false ->
1357-
%% No active TICK pid
1358-
%% this function spawns and returns the tick process pid
1359-
rabbit_quorum_queue:handle_tick(QName, Overview, Nodes);
1360-
true ->
1361-
%% Active TICK pid, do nothing
1362-
Pid
1363-
end,
13641400

1365-
%% TODO: check consumer timeouts
1366-
{no_reply, Aux#?AUX{tick_pid = NewPid}, RaAux, []};
1401+
handle_aux(RaftState, _, {handle_tick, [QName, Overview0, Nodes]},
1402+
#?AUX{tick_pid = Pid, ingress = Ingress0} = Aux, RaAux) ->
1403+
Overview = Overview0#{members_info => ra_aux:members_info(RaAux)},
1404+
Ingress = update_ingress(Overview0, Nodes, Ingress0),
1405+
Aux1 = Aux#?AUX{ingress = Ingress},
1406+
case RaftState of
1407+
leader ->
1408+
NewPid =
1409+
case process_is_alive(Pid) of
1410+
false ->
1411+
rabbit_quorum_queue:handle_tick(QName, Overview, Nodes);
1412+
true ->
1413+
Pid
1414+
end,
1415+
{no_reply, Aux1#?AUX{tick_pid = NewPid}, RaAux, []};
1416+
_ ->
1417+
{no_reply, Aux1, RaAux, []}
1418+
end;
13671419
handle_aux(_, _, {get_checked_out, ConsumerKey, MsgIds}, Aux0, RaAux0) ->
13681420
#?STATE{cfg = #cfg{},
13691421
consumers = Consumers} = ra_aux:machine_state(RaAux0),
@@ -1460,6 +1512,10 @@ handle_aux(leader, _, {dlx, setup}, Aux, RaAux) ->
14601512
handle_aux(_, _, {dlx, teardown, Pid}, Aux, RaAux) ->
14611513
terminate_dlx_worker(Pid),
14621514
{no_reply, Aux, RaAux};
1515+
handle_aux(_, {call, _From}, get_ingress_rates,
1516+
#?AUX{ingress = Ingress} = Aux, RaAux) ->
1517+
Rates = compute_ingress_rates(Ingress),
1518+
{reply, {ok, Rates}, Aux, RaAux};
14631519
handle_aux(_, _, Unhandled, Aux, RaAux) ->
14641520
#?STATE{cfg = #cfg{resource = QR}} = ra_aux:machine_state(RaAux),
14651521
?LOG_DEBUG("~ts: rabbit_fifo: unhandled aux command ~P",
@@ -1907,12 +1963,24 @@ maybe_return_all(#{system_time := Ts} = Meta, ConsumerKey,
19071963
Effects}
19081964
end.
19091965

1966+
node_of(undefined) -> undefined;
1967+
node_of(Pid) when is_pid(Pid) -> node(Pid).
1968+
1969+
bump_ingress(Node, Size, Map) ->
1970+
maps:update_with(Node, fun(V) -> V + Size end, Size, Map).
1971+
19101972
apply_enqueue(#{index := RaftIdx,
19111973
system_time := Ts} = Meta, From,
19121974
Seq, RawMsg, Size, State0) ->
19131975
case maybe_enqueue(RaftIdx, Ts, From, Seq, RawMsg, Size, [], State0) of
19141976
{ok, State1, Effects1} ->
1915-
checkout(Meta, State0, State1, Effects1);
1977+
{MetaSize, BodySize} = Size,
1978+
TotalSize = MetaSize + BodySize,
1979+
IngressByNode = State1#?STATE.ingress_bytes_by_node,
1980+
State2 = State1#?STATE{
1981+
ingress_bytes_by_node =
1982+
bump_ingress(node_of(From), TotalSize, IngressByNode)},
1983+
checkout(Meta, State0, State2, Effects1);
19161984
{out_of_sequence, State, Effects} ->
19171985
{State, not_enqueued, Effects};
19181986
{duplicate, State, Effects} ->

deps/rabbit/src/rabbit_fifo.hrl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,12 @@
295295
last_active :: option(non_neg_integer()),
296296
msg_cache :: option({ra:index(), raw_msg()}),
297297
%% delayed retry messages awaiting redelivery
298-
delayed = #delayed{} :: #delayed{}
298+
delayed = #delayed{} :: #delayed{},
299+
%% bytes enqueued, accumulated per originating node since the
300+
%% creation of this state machine. Monotonically non-decreasing
301+
%% on every node (under leader replication). Used by leader
302+
%% migration tooling to estimate per-node ingress.
303+
ingress_bytes_by_node = #{} :: #{node() | undefined => non_neg_integer()}
299304
}).
300305

301306
-type config() :: #{name := atom(),

deps/rabbit/test/quorum_queue_SUITE.erl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4670,7 +4670,8 @@ purge(Config) ->
46704670

46714671
{'queue.purge_ok', 2} = amqp_channel:call(Ch, #'queue.purge'{queue = QQ}),
46724672

4673-
?assertEqual([0], dirty_query([Server], RaName, fun rabbit_fifo:query_messages_total/1)).
4673+
?assertMatch(#{num_messages := 0}, machine_overview({RaName, Server})),
4674+
ok.
46744675

46754676
peek(Config) ->
46764677
[Server | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),

deps/rabbit/test/rabbit_fifo_SUITE.erl

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4030,7 +4030,7 @@ machine_version_test(Config) ->
40304030
consumers = #{3 := #consumer{cfg = #consumer_cfg{priority = 0}}},
40314031
service_queue = S,
40324032
messages = Msgs}, ok,
4033-
[_|_]} = apply(meta(Config, Idx), {machine_version, 7, 8}, S1),
4033+
[_|_]} = apply(meta(Config, Idx), {machine_version, 7, 9}, S1),
40344034

40354035
?assertEqual(1, rabbit_fifo_pq:len(Msgs)),
40364036
?assert(priority_queue:is_queue(S)),
@@ -4058,16 +4058,16 @@ machine_version_waiting_consumer_test(Config) ->
40584058
#consumer_cfg{priority = 0}}},
40594059
service_queue = S,
40604060
messages = Msgs}, ok, _} = apply(meta(Config, Idx),
4061-
{machine_version, 7, 8}, S1),
4061+
{machine_version, 7, 9}, S1),
40624062
%% validate message conversion to lqueue
40634063
?assertEqual(0, rabbit_fifo_pq:len(Msgs)),
40644064
?assert(priority_queue:is_queue(S)),
40654065
?assertEqual(1, priority_queue:len(S)),
40664066
ok.
40674067

4068-
convert_v7_to_v8_test(Config) ->
4068+
convert_v7_to_v9_test(Config) ->
40694069
ConfigV7 = [{machine_version, 7} | Config],
4070-
ConfigV8 = [{machine_version, 8} | Config],
4070+
ConfigV9 = [{machine_version, 9} | Config],
40714071

40724072
EPid = test_util:fake_pid(node()),
40734073
Pid1 = test_util:fake_pid(node()),
@@ -4092,7 +4092,7 @@ convert_v7_to_v8_test(Config) ->
40924092
{StateV7, _} = run_log(rabbit_fifo_v7, ConfigV7, Init, Entries,
40934093
fun (_) -> true end),
40944094
{#rabbit_fifo{consumers = Consumers}, ok, _} =
4095-
apply(meta(ConfigV8, ?LINE), {machine_version, 7, 8}, StateV7),
4095+
apply(meta(ConfigV9, ?LINE), {machine_version, 7, 9}, StateV7),
40964096

40974097
?assertMatch(#consumer{status = {suspected_down, up}},
40984098
maps:get(Cid1, Consumers)),
@@ -4927,6 +4927,36 @@ query_single_active_consumer_consumer_info_test(Config) ->
49274927
ok.
49284928

49294929

4930+
%% Ingress tracking tests
4931+
4932+
ingress_bytes_by_node_accumulates_on_enqueue_test(Config) ->
4933+
S0 = test_init(ingress_accumulates),
4934+
Msg = mk_mc(<<"hello">>),
4935+
Pid = self(),
4936+
Enq = make_enqueue(Pid, 1, Msg),
4937+
{S1, _, _} = apply(meta(Config, 1, 0, {notify, 1, Pid}), Enq, S0),
4938+
Ingress = S1#rabbit_fifo.ingress_bytes_by_node,
4939+
?assert(maps:get(node(Pid), Ingress, 0) > 0),
4940+
ok.
4941+
4942+
ingress_bytes_by_node_survives_snapshot_test(Config) ->
4943+
S0 = test_init(ingress_snapshot),
4944+
Msg = mk_mc(<<"test">>),
4945+
Pid = self(),
4946+
Enq = make_enqueue(Pid, 1, Msg),
4947+
{S1, _, _} = apply(meta(Config, 1, 0, {notify, 1, Pid}), Enq, S0),
4948+
Ingress = S1#rabbit_fifo.ingress_bytes_by_node,
4949+
?assert(maps:size(Ingress) > 0),
4950+
%% Simulate snapshot/restore via serialization round-trip
4951+
S2 = binary_to_term(term_to_binary(S1)),
4952+
Ingress2 = S2#rabbit_fifo.ingress_bytes_by_node,
4953+
?assertEqual(Ingress, Ingress2),
4954+
ok.
4955+
4956+
4957+
%% Ingress tracking tests end
4958+
4959+
49304960
%% Utility
49314961

49324962
init(Conf) -> rabbit_fifo:init(Conf).

deps/rabbit/test/rabbit_fifo_dlx_integration_SUITE.erl

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -655,8 +655,8 @@ reject_publish_max_length_target_quorum_queue(Config) ->
655655
amqp_channel:call(Ch, #'basic.get'{queue = TargetQ}),
656656
30000)
657657
end || N <- lists:seq(1,4)],
658-
eventually(?_assertMatch([{0, _}],
659-
dirty_query([Server], RaName, fun rabbit_fifo:query_stat_dlx/1)), 500, 10),
658+
eventually(?_assertMatch(#{num_discarded := 0},
659+
machine_overview({RaName, Server}))),
660660
?assertEqual(4, counted(messages_dead_lettered_expired_total, Config)),
661661
eventually(?_assertEqual(4, counted(messages_dead_lettered_confirmed_total, Config))).
662662

@@ -706,8 +706,8 @@ reject_publish_down_target_quorum_queue(Config) ->
706706
sets:add_element(Msg, S)
707707
end, sets:new([{version, 2}]), lists:seq(1, 50)),
708708
?assertEqual(50, sets:size(Received)),
709-
eventually(?_assertMatch([{0, _}],
710-
dirty_query([Server], RaName, fun rabbit_fifo:query_stat_dlx/1)), 500, 10),
709+
eventually(?_assertMatch(#{num_discarded := 0},
710+
machine_overview({RaName, Server}))),
711711
?assertEqual(50, counted(messages_dead_lettered_expired_total, Config)),
712712
eventually(?_assertEqual(50, counted(messages_dead_lettered_confirmed_total, Config))).
713713

@@ -999,3 +999,11 @@ counted(Metric, Config) ->
999999
metric(Metric, Counters) ->
10001000
Metrics = maps:get(#{queue_type => rabbit_quorum_queue, dead_letter_strategy => at_least_once}, Counters),
10011001
maps:get(Metric, Metrics).
1002+
1003+
machine_overview(ServerId) when is_tuple(ServerId) ->
1004+
case ra:member_overview(ServerId) of
1005+
{ok, #{machine := Mac}, _} ->
1006+
Mac;
1007+
Err ->
1008+
Err
1009+
end.

0 commit comments

Comments
 (0)