Skip to content

Commit 494131c

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 494131c

6 files changed

Lines changed: 184 additions & 43 deletions

File tree

deps/rabbit/src/rabbit_fifo.erl

Lines changed: 100 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -203,10 +203,14 @@
203203
delayed_op/0]).
204204

205205
-spec init(config()) -> state().
206-
init(#{name := Name,
207-
queue_resource := Resource} = Conf) ->
208-
update_config(Conf, #?STATE{cfg = #cfg{name = Name,
209-
resource = Resource}}).
206+
init(Conf) ->
207+
case Conf of
208+
#{name := Name, queue_resource := Resource} ->
209+
update_config(Conf, #?STATE{cfg = #cfg{name = Name,
210+
resource = Resource}});
211+
_ ->
212+
erlang:error({bad_init_arg, Conf})
213+
end.
210214

211215
update_config(Conf, State) ->
212216
DLH = maps:get(dead_letter_handler, Conf, undefined),
@@ -696,9 +700,12 @@ apply_(Meta, {nodeup, Node}, #?STATE{consumers = Cons0,
696700
apply_(_, {nodedown, _Node}, State) ->
697701
{State, ok};
698702
apply_(Meta, #purge_nodes{nodes = Nodes}, State0) ->
699-
{State, Effects} = lists:foldl(fun(Node, {S, E}) ->
703+
{State1, Effects} = lists:foldl(fun(Node, {S, E}) ->
700704
purge_node(Meta, Node, S, E)
701705
end, {State0, []}, Nodes),
706+
State = State1#?STATE{
707+
ingress_bytes_by_node =
708+
maps:without(Nodes, State1#?STATE.ingress_bytes_by_node)},
702709
{State, ok, Effects};
703710
apply_(Meta,
704711
#update_config{config = #{} = Conf},
@@ -967,8 +974,7 @@ credit_reply_resend_effect(#?STATE{waiting_consumers = Waiting,
967974
end, [], maps:merge(Consumers, maps:from_list(Waiting))).
968975

969976
convert_v8_to_v9(#{} = _Meta, StateV8) ->
970-
State = StateV8,
971-
State.
977+
erlang:append_element(StateV8, #{}).
972978

973979
purge_node(Meta, Node, State, Effects) ->
974980
lists:foldl(fun(Pid, {S0, E0}) ->
@@ -1171,7 +1177,8 @@ overview(#?STATE{consumers = Cons,
11711177
reclaimable_bytes_count => ReclaimableBytes,
11721178
smallest_raft_index => smallest_raft_index(State),
11731179
num_active_priorities => NumActivePriorities,
1174-
messages_by_priority => Detail
1180+
messages_by_priority => Detail,
1181+
ingress_bytes_by_node => State#?STATE.ingress_bytes_by_node
11751182
},
11761183
DlxOverview = dlx_overview(DlxState),
11771184
maps:merge(maps:merge(Overview, DlxOverview), SacOverview).
@@ -1205,7 +1212,13 @@ which_module(7) -> rabbit_fifo_v7;
12051212
which_module(8) -> rabbit_fifo_v8;
12061213
which_module(9) -> ?MODULE.
12071214

1208-
-define(AUX, aux_v4).
1215+
-define(AUX, aux_v5).
1216+
-define(DEFAULT_INGRESS_DECAY_MS, 60_000).
1217+
1218+
-record(ingress_aux,
1219+
{last_totals = #{} :: #{node() | undefined => non_neg_integer()},
1220+
estimators = #{} :: #{node() | undefined => ra_li:state()},
1221+
decay_ms = ?DEFAULT_INGRESS_DECAY_MS :: pos_integer()}).
12091222

12101223
-record(snapshot, {index :: ra:index(),
12111224
timestamp :: milliseconds(),
@@ -1220,7 +1233,8 @@ which_module(9) -> ?MODULE.
12201233
gc = #aux_gc{} :: #aux_gc{},
12211234
tick_pid :: undefined | pid(),
12221235
cache = #{} :: map(),
1223-
last_checkpoint :: tuple() | #snapshot{}
1236+
last_checkpoint :: tuple() | #snapshot{},
1237+
ingress = #ingress_aux{} :: #ingress_aux{}
12241238
}).
12251239

12261240
init_aux(Name) when is_atom(Name) ->
@@ -1234,11 +1248,38 @@ init_aux(Name) when is_atom(Name) ->
12341248
?SNAP_MIN_RECLAIMABLE_B}),
12351249
Range = max(1, SnapMinReclaimable - ?SNAP_MIN_RECLAIMABLE_LOW_B),
12361250
MinReclaimable = ?SNAP_MIN_RECLAIMABLE_LOW_B + rand:uniform(Range),
1251+
DecayMs = persistent_term:get(rabbit_fifo_ingress_decay_ms,
1252+
?DEFAULT_INGRESS_DECAY_MS),
12371253
#?AUX{name = Name,
12381254
last_checkpoint = #snapshot{index = 0,
12391255
timestamp = erlang:system_time(millisecond),
12401256
messages_total = 0,
1241-
min_reclaimable = MinReclaimable}}.
1257+
min_reclaimable = MinReclaimable},
1258+
ingress = #ingress_aux{decay_ms = DecayMs}}.
1259+
1260+
update_ingress(Overview, Nodes, #ingress_aux{last_totals = LastTotals,
1261+
estimators = Estimators0,
1262+
decay_ms = DecayMs} = Ingress) ->
1263+
NewTotals = maps:get(ingress_bytes_by_node, Overview, #{}),
1264+
Ts = erlang:monotonic_time(millisecond),
1265+
Estimators1 =
1266+
maps:fold(fun(Node, NewTotal, Est) ->
1267+
Delta = NewTotal - maps:get(Node, LastTotals, 0),
1268+
Li0 = maps:get(Node, Est, ra_li:new(DecayMs)),
1269+
Li1 = ra_li:update(Delta, Ts, Li0),
1270+
Est#{Node => Li1}
1271+
end, Estimators0, NewTotals),
1272+
ActiveNodes = sets:from_list(Nodes, [{version, 2}]),
1273+
Estimators = maps:filter(fun(Node, _) ->
1274+
Node =:= undefined orelse
1275+
sets:is_element(Node, ActiveNodes)
1276+
end, Estimators1),
1277+
Ingress#ingress_aux{last_totals = NewTotals,
1278+
estimators = Estimators}.
1279+
1280+
compute_ingress_rates(#ingress_aux{estimators = Estimators}) ->
1281+
Ts = erlang:monotonic_time(millisecond),
1282+
maps:map(fun(_Node, Li) -> ra_li:rate(Ts, Li) end, Estimators).
12421283

12431284
handle_aux(RaftState, Tag, Cmd, AuxV2, RaAux)
12441285
when element(1, AuxV2) == aux_v2 ->
@@ -1256,6 +1297,19 @@ handle_aux(RaftState, Tag, Cmd, AuxV3, RaAux)
12561297
last_checkpoint = element(8, AuxV3)
12571298
},
12581299
handle_aux(RaftState, Tag, Cmd, AuxV4, RaAux);
1300+
handle_aux(RaftState, Tag, Cmd, AuxV4, RaAux)
1301+
when element(1, AuxV4) == aux_v4 ->
1302+
DecayMs = persistent_term:get(rabbit_fifo_ingress_decay_ms,
1303+
?DEFAULT_INGRESS_DECAY_MS),
1304+
AuxV5 = #?AUX{name = element(2, AuxV4),
1305+
last_decorators_state = element(3, AuxV4),
1306+
last_consumer_timeout = element(4, AuxV4),
1307+
gc = element(5, AuxV4),
1308+
tick_pid = element(6, AuxV4),
1309+
cache = element(7, AuxV4),
1310+
last_checkpoint = element(8, AuxV4),
1311+
ingress = #ingress_aux{decay_ms = DecayMs}},
1312+
handle_aux(RaftState, Tag, Cmd, AuxV5, RaAux);
12591313
handle_aux(leader, cast, eval,
12601314
#?AUX{last_decorators_state = LastDec,
12611315
last_consumer_timeout = LastConTimeout0,
@@ -1348,22 +1402,25 @@ handle_aux(_RaftState, cast, {#return{msg_ids = MsgIds,
13481402
%% for returns with a delivery limit set we can just return as before
13491403
{no_reply, Aux0, RaAux0, [{append, Ret, {notify, Corr, Pid}}]}
13501404
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,
13641405

1365-
%% TODO: check consumer timeouts
1366-
{no_reply, Aux#?AUX{tick_pid = NewPid}, RaAux, []};
1406+
handle_aux(RaftState, _, {handle_tick, [QName, Overview0, Nodes]},
1407+
#?AUX{tick_pid = Pid, ingress = Ingress0} = Aux, RaAux) ->
1408+
Overview = Overview0#{members_info => ra_aux:members_info(RaAux)},
1409+
Ingress = update_ingress(Overview0, Nodes, Ingress0),
1410+
Aux1 = Aux#?AUX{ingress = Ingress},
1411+
case RaftState of
1412+
leader ->
1413+
NewPid =
1414+
case process_is_alive(Pid) of
1415+
false ->
1416+
rabbit_quorum_queue:handle_tick(QName, Overview, Nodes);
1417+
true ->
1418+
Pid
1419+
end,
1420+
{no_reply, Aux1#?AUX{tick_pid = NewPid}, RaAux, []};
1421+
_ ->
1422+
{no_reply, Aux1, RaAux, []}
1423+
end;
13671424
handle_aux(_, _, {get_checked_out, ConsumerKey, MsgIds}, Aux0, RaAux0) ->
13681425
#?STATE{cfg = #cfg{},
13691426
consumers = Consumers} = ra_aux:machine_state(RaAux0),
@@ -1460,6 +1517,10 @@ handle_aux(leader, _, {dlx, setup}, Aux, RaAux) ->
14601517
handle_aux(_, _, {dlx, teardown, Pid}, Aux, RaAux) ->
14611518
terminate_dlx_worker(Pid),
14621519
{no_reply, Aux, RaAux};
1520+
handle_aux(_, {call, _From}, get_ingress_rates,
1521+
#?AUX{ingress = Ingress} = Aux, RaAux) ->
1522+
Rates = compute_ingress_rates(Ingress),
1523+
{reply, {ok, Rates}, Aux, RaAux};
14631524
handle_aux(_, _, Unhandled, Aux, RaAux) ->
14641525
#?STATE{cfg = #cfg{resource = QR}} = ra_aux:machine_state(RaAux),
14651526
?LOG_DEBUG("~ts: rabbit_fifo: unhandled aux command ~P",
@@ -1907,12 +1968,24 @@ maybe_return_all(#{system_time := Ts} = Meta, ConsumerKey,
19071968
Effects}
19081969
end.
19091970

1971+
node_of(undefined) -> undefined;
1972+
node_of(Pid) when is_pid(Pid) -> node(Pid).
1973+
1974+
bump_ingress(Node, Size, Map) ->
1975+
maps:update_with(Node, fun(V) -> V + Size end, Size, Map).
1976+
19101977
apply_enqueue(#{index := RaftIdx,
19111978
system_time := Ts} = Meta, From,
19121979
Seq, RawMsg, Size, State0) ->
19131980
case maybe_enqueue(RaftIdx, Ts, From, Seq, RawMsg, Size, [], State0) of
19141981
{ok, State1, Effects1} ->
1915-
checkout(Meta, State0, State1, Effects1);
1982+
{MetaSize, BodySize} = Size,
1983+
TotalSize = MetaSize + BodySize,
1984+
IngressByNode = State1#?STATE.ingress_bytes_by_node,
1985+
State2 = State1#?STATE{
1986+
ingress_bytes_by_node =
1987+
bump_ingress(node_of(From), TotalSize, IngressByNode)},
1988+
checkout(Meta, State0, State2, Effects1);
19161989
{out_of_sequence, State, Effects} ->
19171990
{State, not_enqueued, Effects};
19181991
{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 Tanzu's
302+
%% leader-migration plugin 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)