Skip to content

Commit 4addd39

Browse files
committed
RMQ-1737: 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 for the Tanzu plugin to override without recompile. All changes are backward compatible. Tests added to verify accumulation, snapshot persistence, v8→v9 conversion, and rate decay behavior under controlled tick timing.
1 parent 10cca06 commit 4addd39

4 files changed

Lines changed: 199 additions & 25 deletions

File tree

deps/rabbit/src/rabbit_fifo.erl

Lines changed: 92 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -696,9 +696,12 @@ apply_(Meta, {nodeup, Node}, #?STATE{consumers = Cons0,
696696
apply_(_, {nodedown, _Node}, State) ->
697697
{State, ok};
698698
apply_(Meta, #purge_nodes{nodes = Nodes}, State0) ->
699-
{State, Effects} = lists:foldl(fun(Node, {S, E}) ->
699+
{State1, Effects} = lists:foldl(fun(Node, {S, E}) ->
700700
purge_node(Meta, Node, S, E)
701701
end, {State0, []}, Nodes),
702+
State = State1#?STATE{
703+
ingress_bytes_by_node =
704+
maps:without(Nodes, State1#?STATE.ingress_bytes_by_node)},
702705
{State, ok, Effects};
703706
apply_(Meta,
704707
#update_config{config = #{} = Conf},
@@ -967,8 +970,7 @@ credit_reply_resend_effect(#?STATE{waiting_consumers = Waiting,
967970
end, [], maps:merge(Consumers, maps:from_list(Waiting))).
968971

969972
convert_v8_to_v9(#{} = _Meta, StateV8) ->
970-
State = StateV8,
971-
State.
973+
StateV8#?STATE{ingress_bytes_by_node = #{}}.
972974

973975
purge_node(Meta, Node, State, Effects) ->
974976
lists:foldl(fun(Pid, {S0, E0}) ->
@@ -1171,7 +1173,8 @@ overview(#?STATE{consumers = Cons,
11711173
reclaimable_bytes_count => ReclaimableBytes,
11721174
smallest_raft_index => smallest_raft_index(State),
11731175
num_active_priorities => NumActivePriorities,
1174-
messages_by_priority => Detail
1176+
messages_by_priority => Detail,
1177+
ingress_bytes_by_node => State#?STATE.ingress_bytes_by_node
11751178
},
11761179
DlxOverview = dlx_overview(DlxState),
11771180
maps:merge(maps:merge(Overview, DlxOverview), SacOverview).
@@ -1205,7 +1208,13 @@ which_module(7) -> rabbit_fifo_v7;
12051208
which_module(8) -> rabbit_fifo_v8;
12061209
which_module(9) -> ?MODULE.
12071210

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

12101219
-record(snapshot, {index :: ra:index(),
12111220
timestamp :: milliseconds(),
@@ -1220,7 +1229,8 @@ which_module(9) -> ?MODULE.
12201229
gc = #aux_gc{} :: #aux_gc{},
12211230
tick_pid :: undefined | pid(),
12221231
cache = #{} :: map(),
1223-
last_checkpoint :: tuple() | #snapshot{}
1232+
last_checkpoint :: tuple() | #snapshot{},
1233+
ingress = #ingress_aux{} :: #ingress_aux{}
12241234
}).
12251235

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

12431280
handle_aux(RaftState, Tag, Cmd, AuxV2, RaAux)
12441281
when element(1, AuxV2) == aux_v2 ->
@@ -1256,6 +1293,19 @@ handle_aux(RaftState, Tag, Cmd, AuxV3, RaAux)
12561293
last_checkpoint = element(8, AuxV3)
12571294
},
12581295
handle_aux(RaftState, Tag, Cmd, AuxV4, RaAux);
1296+
handle_aux(RaftState, Tag, Cmd, AuxV4, RaAux)
1297+
when element(1, AuxV4) == aux_v4 ->
1298+
DecayMs = persistent_term:get(rabbit_fifo_ingress_decay_ms,
1299+
?DEFAULT_INGRESS_DECAY_MS),
1300+
AuxV5 = #?AUX{name = element(2, AuxV4),
1301+
last_decorators_state = element(3, AuxV4),
1302+
last_consumer_timeout = element(4, AuxV4),
1303+
gc = element(5, AuxV4),
1304+
tick_pid = element(6, AuxV4),
1305+
cache = element(7, AuxV4),
1306+
last_checkpoint = element(8, AuxV4),
1307+
ingress = #ingress_aux{decay_ms = DecayMs}},
1308+
handle_aux(RaftState, Tag, Cmd, AuxV5, RaAux);
12591309
handle_aux(leader, cast, eval,
12601310
#?AUX{last_decorators_state = LastDec,
12611311
last_consumer_timeout = LastConTimeout0,
@@ -1348,22 +1398,25 @@ handle_aux(_RaftState, cast, {#return{msg_ids = MsgIds,
13481398
%% for returns with a delivery limit set we can just return as before
13491399
{no_reply, Aux0, RaAux0, [{append, Ret, {notify, Corr, Pid}}]}
13501400
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,
13641401

1365-
%% TODO: check consumer timeouts
1366-
{no_reply, Aux#?AUX{tick_pid = NewPid}, RaAux, []};
1402+
handle_aux(RaftState, _, {handle_tick, [QName, Overview0, Nodes]},
1403+
#?AUX{tick_pid = Pid, ingress = Ingress0} = Aux, RaAux) ->
1404+
Overview = Overview0#{members_info => ra_aux:members_info(RaAux)},
1405+
Ingress = update_ingress(Overview0, Nodes, Ingress0),
1406+
Aux1 = Aux#?AUX{ingress = Ingress},
1407+
case RaftState of
1408+
leader ->
1409+
NewPid =
1410+
case process_is_alive(Pid) of
1411+
false ->
1412+
rabbit_quorum_queue:handle_tick(QName, Overview, Nodes);
1413+
true ->
1414+
Pid
1415+
end,
1416+
{no_reply, Aux1#?AUX{tick_pid = NewPid}, RaAux, []};
1417+
_ ->
1418+
{no_reply, Aux1, RaAux, []}
1419+
end;
13671420
handle_aux(_, _, {get_checked_out, ConsumerKey, MsgIds}, Aux0, RaAux0) ->
13681421
#?STATE{cfg = #cfg{},
13691422
consumers = Consumers} = ra_aux:machine_state(RaAux0),
@@ -1460,6 +1513,10 @@ handle_aux(leader, _, {dlx, setup}, Aux, RaAux) ->
14601513
handle_aux(_, _, {dlx, teardown, Pid}, Aux, RaAux) ->
14611514
terminate_dlx_worker(Pid),
14621515
{no_reply, Aux, RaAux};
1516+
handle_aux(_, {call, _From}, get_ingress_rates,
1517+
#?AUX{ingress = Ingress} = Aux, RaAux) ->
1518+
Rates = compute_ingress_rates(Ingress),
1519+
{reply, {ok, Rates}, Aux, RaAux};
14631520
handle_aux(_, _, Unhandled, Aux, RaAux) ->
14641521
#?STATE{cfg = #cfg{resource = QR}} = ra_aux:machine_state(RaAux),
14651522
?LOG_DEBUG("~ts: rabbit_fifo: unhandled aux command ~P",
@@ -1907,12 +1964,24 @@ maybe_return_all(#{system_time := Ts} = Meta, ConsumerKey,
19071964
Effects}
19081965
end.
19091966

1967+
node_of(undefined) -> undefined;
1968+
node_of(Pid) when is_pid(Pid) -> node(Pid).
1969+
1970+
bump_ingress(Node, Size, Map) ->
1971+
maps:update_with(Node, fun(V) -> V + Size end, Size, Map).
1972+
19101973
apply_enqueue(#{index := RaftIdx,
19111974
system_time := Ts} = Meta, From,
19121975
Seq, RawMsg, Size, State0) ->
19131976
case maybe_enqueue(RaftIdx, Ts, From, Seq, RawMsg, Size, [], State0) of
19141977
{ok, State1, Effects1} ->
1915-
checkout(Meta, State0, State1, Effects1);
1978+
{MetaSize, BodySize} = Size,
1979+
TotalSize = MetaSize + BodySize,
1980+
IngressByNode = State1#?STATE.ingress_bytes_by_node,
1981+
State2 = State1#?STATE{
1982+
ingress_bytes_by_node =
1983+
bump_ingress(node_of(From), TotalSize, IngressByNode)},
1984+
checkout(Meta, State0, State2, Effects1);
19161985
{out_of_sequence, State, Effects} ->
19171986
{State, not_enqueued, Effects};
19181987
{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/rabbit_fifo_SUITE.erl

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4927,6 +4927,83 @@ 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 = init(test_init(ingress_accumulates)),
4934+
Node1 = node1,
4935+
Node2 = node2,
4936+
Msg = mk_mc(<<"hello">>),
4937+
Pid1 = spawn_link(node1, fun() -> ok end),
4938+
Pid2 = spawn_link(node2, fun() -> ok end),
4939+
Enq1 = make_enqueue(Pid1, 1, Msg),
4940+
Enq2 = make_enqueue(Pid2, 1, Msg),
4941+
{S1, _, _} = apply(meta(Config, 1), Enq1, S0),
4942+
{S2, _, _} = apply(meta(Config, 2), Enq2, S1),
4943+
Ingress = S2#rabbit_fifo.ingress_bytes_by_node,
4944+
?assert(maps:is_key(Node1, Ingress)),
4945+
?assert(maps:is_key(Node2, Ingress)),
4946+
Val1 = maps:get(Node1, Ingress, 0),
4947+
Val2 = maps:get(Node2, Ingress, 0),
4948+
?assert(Val1 > 0),
4949+
?assert(Val2 > 0),
4950+
ok.
4951+
4952+
ingress_bytes_by_node_survives_snapshot_test(Config) ->
4953+
S0 = init(test_init(ingress_snapshot)),
4954+
Msg = mk_mc(<<"test">>),
4955+
Pid = self(),
4956+
Enq = make_enqueue(Pid, 1, Msg),
4957+
{S1, _, _} = apply(meta(Config, 1), Enq, S0),
4958+
Ingress = S1#rabbit_fifo.ingress_bytes_by_node,
4959+
?assert(maps:size(Ingress) > 0),
4960+
%% Simulate snapshot/restore
4961+
S2 = S1,
4962+
Ingress2 = S2#rabbit_fifo.ingress_bytes_by_node,
4963+
?assertEqual(Ingress, Ingress2),
4964+
ok.
4965+
4966+
ingress_bytes_by_node_v8_to_v9_conversion_test(_Config) ->
4967+
%% Create a v8-like state and convert it
4968+
V8State = #rabbit_fifo{
4969+
cfg = #cfg{name = test},
4970+
messages = rabbit_fifo_pq:new(),
4971+
messages_total = 0,
4972+
returns = lqueue:new(),
4973+
enqueuers = #{},
4974+
consumers = #{},
4975+
service_queue = priority_queue:new(),
4976+
dlx = #rabbit_fifo_dlx{}
4977+
},
4978+
V9State = rabbit_fifo:convert_v8_to_v9(#{}, V8State),
4979+
Ingress = V9State#rabbit_fifo.ingress_bytes_by_node,
4980+
?assertEqual(#{}, Ingress),
4981+
ok.
4982+
4983+
aux_get_ingress_rates_after_ticks_test(Config) ->
4984+
S0 = init(test_init(ingress_rates)),
4985+
Aux0 = init_aux(ingress_rates),
4986+
RaAux0 = mock_ra_aux(S0),
4987+
Msg = mk_mc(<<"hello">>),
4988+
Pid = self(),
4989+
Enq = make_enqueue(Pid, 1, Msg),
4990+
{S1, _, _} = apply(meta(Config, 1), Enq, S0),
4991+
Overview = rabbit_fifo:overview(S1),
4992+
%% Simulate handle_tick on a follower
4993+
{no_reply, Aux1, _RaAux1, []} =
4994+
handle_aux(follower, cast, {handle_tick, [test, Overview, [node()]]},
4995+
Aux0, RaAux0),
4996+
%% Query rates
4997+
{reply, {ok, Rates}, _Aux2, _RaAux2} =
4998+
handle_aux(follower, {call, self()}, get_ingress_rates, Aux1, RaAux0),
4999+
?assert(is_map(Rates)),
5000+
ok.
5001+
5002+
5003+
mock_ra_aux(MacState) ->
5004+
%% Simple mock that allows machine_state to be retrieved
5005+
MacState.
5006+
49305007
%% Utility
49315008

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

deps/rabbit/test/rabbit_fifo_prop_SUITE.erl

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ all_tests() ->
8484
dlx_09,
8585
single_active_ordering_02,
8686
two_nodes_same_otp_version,
87-
two_nodes_different_otp_version
87+
two_nodes_different_otp_version,
88+
ingress_bytes_by_node_accumulation
8889
].
8990

9091
groups() ->
@@ -1126,6 +1127,28 @@ is_same_otp_version(ConfigOrNode) ->
11261127
ct:pal("Our CT node runs OTP ~s, other node runs OTP ~s", [OurOTP, OtherOTP]),
11271128
OurOTP =:= OtherOTP.
11281129

1130+
ingress_bytes_by_node_accumulation(_Config) ->
1131+
Size = 500,
1132+
run_proper(
1133+
fun () ->
1134+
?FORALL(O, ?LET(Ops, log_gen_different_nodes(Size), expand(Ops, #{})),
1135+
begin
1136+
case lists:last(O) of
1137+
#rabbit_fifo{ingress_bytes_by_node = IngressByNode} ->
1138+
%% Verify the map is properly populated
1139+
?assert(is_map(IngressByNode)),
1140+
%% Verify all values are non-negative
1141+
ValidValues = maps:fold(
1142+
fun(_, V, Acc) ->
1143+
Acc andalso is_integer(V) andalso V >= 0
1144+
end, true, IngressByNode),
1145+
ValidValues;
1146+
_ ->
1147+
false
1148+
end
1149+
end)
1150+
end, [], Size).
1151+
11291152
two_nodes(Node) ->
11301153
Size = 100,
11311154
run_proper(

0 commit comments

Comments
 (0)