From 6e6a33ec0b16f629107c6b92f43abfbb5bf6af4c Mon Sep 17 00:00:00 2001 From: Diana Parra Corbacho Date: Fri, 13 Jun 2025 13:47:50 +0200 Subject: [PATCH 01/31] Shovel: use message containers --- deps/amqp10_client/src/amqp10_msg.erl | 41 +++++-- deps/rabbit/src/rabbit_amqp_session.erl | 1 - .../src/rabbit_amqp091_shovel.erl | 82 ++++--------- .../src/rabbit_amqp10_shovel.erl | 109 +++++------------- .../src/rabbit_shovel_behaviour.erl | 11 +- deps/rabbitmq_shovel/test/amqp10_SUITE.erl | 5 +- .../test/amqp10_dynamic_SUITE.erl | 82 ++++++++++--- .../test/amqp10_shovel_SUITE.erl | 8 +- 8 files changed, 165 insertions(+), 174 deletions(-) diff --git a/deps/amqp10_client/src/amqp10_msg.erl b/deps/amqp10_client/src/amqp10_msg.erl index ac8b9f2a4ba9..a7666d11c0f0 100644 --- a/deps/amqp10_client/src/amqp10_msg.erl +++ b/deps/amqp10_client/src/amqp10_msg.erl @@ -270,14 +270,23 @@ new(DeliveryTag, Bin, Settled) when is_binary(Bin) -> Body = [#'v1_0.data'{content = Bin}], new(DeliveryTag, Body, Settled); new(DeliveryTag, Body, Settled) -> % TODO: constrain to amqp types - #amqp10_msg{ - transfer = #'v1_0.transfer'{ - delivery_tag = {binary, DeliveryTag}, - settled = Settled, - message_format = {uint, ?MESSAGE_FORMAT}}, - %% This lib is safe by default. - header = #'v1_0.header'{durable = true}, - body = Body}. + case is_amqp10_body(Body) orelse (not is_list(Body)) of + true -> + #amqp10_msg{ + transfer = #'v1_0.transfer'{ + delivery_tag = {binary, DeliveryTag}, + settled = Settled, + message_format = {uint, ?MESSAGE_FORMAT}}, + %% This lib is safe by default. + header = #'v1_0.header'{durable = true}, + body = Body}; + false -> + Transfer = #'v1_0.transfer'{ + delivery_tag = {binary, DeliveryTag}, + settled = Settled, + message_format = {uint, ?MESSAGE_FORMAT}}, + from_amqp_records([Transfer | Body]) + end. %% @doc Create a new settled amqp10 message using the specified delivery tag %% and body. @@ -462,3 +471,19 @@ uint(B) -> {uint, B}. has_value(undefined) -> false; has_value(_) -> true. + +is_amqp10_body(#'v1_0.amqp_value'{}) -> + true; +is_amqp10_body(List) when is_list(List) -> + lists:all(fun(#'v1_0.data'{}) -> + true; + (_) -> + false + end, List) orelse + lists:all(fun(#'v1_0.amqp_sequence'{}) -> + true; + (_) -> + false + end, List); +is_amqp10_body(_) -> + false. diff --git a/deps/rabbit/src/rabbit_amqp_session.erl b/deps/rabbit/src/rabbit_amqp_session.erl index e9898665061a..40baa3521c6f 100644 --- a/deps/rabbit/src/rabbit_amqp_session.erl +++ b/deps/rabbit/src/rabbit_amqp_session.erl @@ -2457,7 +2457,6 @@ incoming_link_transfer( validate_message_size(PayloadSize, MaxMessageSize), rabbit_msg_size_metrics:observe(?PROTOCOL, PayloadSize), messages_received(Settled), - Mc0 = mc:init(mc_amqp, PayloadBin, #{}), case lookup_target(LinkExchange, LinkRKey, Mc0, Vhost, User, PermCache0) of {ok, X, RoutingKeys, Mc1, PermCache} -> diff --git a/deps/rabbitmq_shovel/src/rabbit_amqp091_shovel.erl b/deps/rabbitmq_shovel/src/rabbit_amqp091_shovel.erl index 53d2cad75ce9..a8456bb6856c 100644 --- a/deps/rabbitmq_shovel/src/rabbit_amqp091_shovel.erl +++ b/deps/rabbitmq_shovel/src/rabbit_amqp091_shovel.erl @@ -12,6 +12,7 @@ -behaviour(rabbit_shovel_behaviour). -include_lib("amqp_client/include/amqp_client.hrl"). +-include_lib("rabbit/include/mc.hrl"). -include("rabbit_shovel.hrl"). -include_lib("kernel/include/logger.hrl"). @@ -34,7 +35,7 @@ ack/3, nack/3, status/1, - forward/4 + forward/3 ]). %% Function references should not be stored on the metadata store. @@ -170,8 +171,8 @@ forward_pending(State) -> case pop_pending(State) of empty -> State; - {{Tag, Props, Payload}, S} -> - S2 = do_forward(Tag, Props, Payload, S), + {{Tag, Mc}, S} -> + S2 = do_forward(Tag, Mc, S), S3 = control_throttle(S2), case is_blocked(S3) of true -> @@ -184,91 +185,50 @@ forward_pending(State) -> end end. -forward(IncomingTag, Props, Payload, State) -> +forward(IncomingTag, Mc, State) -> case is_blocked(State) of true -> %% We are blocked by client-side flow-control and/or %% `connection.blocked` message from the destination %% broker. Simply cache the forward. - PendingEntry = {IncomingTag, Props, Payload}, + PendingEntry = {IncomingTag, Mc}, add_pending(PendingEntry, State); false -> - State1 = do_forward(IncomingTag, Props, Payload, State), + State1 = do_forward(IncomingTag, Mc, State), control_throttle(State1) end. -do_forward(IncomingTag, Props, Payload, +do_forward(IncomingTag, Mc0, State0 = #{dest := #{props_fun := {M, F, Args}, current := {_, _, DstUri}, fields_fun := {Mf, Ff, Argsf}}}) -> SrcUri = rabbit_shovel_behaviour:source_uri(State0), % do publish - Exchange = maps:get(exchange, Props, undefined), - RoutingKey = maps:get(routing_key, Props, undefined), + Exchange = mc:exchange(Mc0), + RoutingKey = case mc:routing_keys(Mc0) of + [RK | _] -> RK; + Any -> Any + end, Method = #'basic.publish'{exchange = Exchange, routing_key = RoutingKey}, Method1 = apply(Mf, Ff, Argsf ++ [SrcUri, DstUri, Method]), - Msg1 = #amqp_msg{props = apply(M, F, Args ++ [SrcUri, DstUri, props_from_map(Props)]), + Mc = mc:convert(mc_amqpl, Mc0), + {Props, Payload} = rabbit_basic_common:from_content(mc:protocol_state(Mc)), + Msg1 = #amqp_msg{props = apply(M, F, Args ++ [SrcUri, DstUri, Props]), payload = Payload}, publish(IncomingTag, Method1, Msg1, State0). -props_from_map(Map) -> - #'P_basic'{content_type = maps:get(content_type, Map, undefined), - content_encoding = maps:get(content_encoding, Map, undefined), - headers = maps:get(headers, Map, undefined), - delivery_mode = maps:get(delivery_mode, Map, undefined), - priority = maps:get(priority, Map, undefined), - correlation_id = maps:get(correlation_id, Map, undefined), - reply_to = maps:get(reply_to, Map, undefined), - expiration = maps:get(expiration, Map, undefined), - message_id = maps:get(message_id, Map, undefined), - timestamp = maps:get(timestamp, Map, undefined), - type = maps:get(type, Map, undefined), - user_id = maps:get(user_id, Map, undefined), - app_id = maps:get(app_id, Map, undefined), - cluster_id = maps:get(cluster_id, Map, undefined)}. - -map_from_props(#'P_basic'{content_type = Content_type, - content_encoding = Content_encoding, - headers = Headers, - delivery_mode = Delivery_mode, - priority = Priority, - correlation_id = Correlation_id, - reply_to = Reply_to, - expiration = Expiration, - message_id = Message_id, - timestamp = Timestamp, - type = Type, - user_id = User_id, - app_id = App_id, - cluster_id = Cluster_id}) -> - lists:foldl(fun({_K, undefined}, Acc) -> Acc; - ({K, V}, Acc) -> Acc#{K => V} - end, #{}, [{content_type, Content_type}, - {content_encoding, Content_encoding}, - {headers, Headers}, - {delivery_mode, Delivery_mode}, - {priority, Priority}, - {correlation_id, Correlation_id}, - {reply_to, Reply_to}, - {expiration, Expiration}, - {message_id, Message_id}, - {timestamp, Timestamp}, - {type, Type}, - {user_id, User_id}, - {app_id, App_id}, - {cluster_id, Cluster_id} - ]). - handle_source(#'basic.consume_ok'{}, State) -> State; handle_source({#'basic.deliver'{delivery_tag = Tag, exchange = Exchange, routing_key = RoutingKey}, #amqp_msg{props = Props0, payload = Payload}}, State) -> - Props = (map_from_props(Props0))#{exchange => Exchange, - routing_key => RoutingKey}, + Content = rabbit_basic_common:build_content(Props0, Payload), + Msg0 = mc:init(mc_amqpl, Content, #{}), + Msg1 = mc:set_annotation(?ANN_ROUTING_KEYS, [RoutingKey], Msg0), + Msg = mc:set_annotation(?ANN_EXCHANGE, Exchange, Msg1), % forward to destination - rabbit_shovel_behaviour:forward(Tag, Props, Payload, State); + rabbit_shovel_behaviour:forward(Tag, Msg, State); handle_source({'EXIT', Conn, Reason}, #{source := #{current := {Conn, _, _}}}) -> diff --git a/deps/rabbitmq_shovel/src/rabbit_amqp10_shovel.erl b/deps/rabbitmq_shovel/src/rabbit_amqp10_shovel.erl index 84a4b7ea0b22..bd9dba0f945b 100644 --- a/deps/rabbitmq_shovel/src/rabbit_amqp10_shovel.erl +++ b/deps/rabbitmq_shovel/src/rabbit_amqp10_shovel.erl @@ -9,6 +9,7 @@ -behaviour(rabbit_shovel_behaviour). +-include_lib("rabbit/include/mc.hrl"). -include("rabbit_shovel.hrl"). -export([ @@ -30,7 +31,7 @@ ack/3, nack/3, status/1, - forward/4 + forward/3 ]). -import(rabbit_misc, [pget/2, pget/3]). @@ -180,10 +181,12 @@ dest_endpoint(#{shovel_type := dynamic, -spec handle_source(Msg :: any(), state()) -> not_handled | state() | {stop, any()}. -handle_source({amqp10_msg, _LinkRef, Msg}, State) -> - Tag = amqp10_msg:delivery_id(Msg), - Payload = amqp10_msg:body_bin(Msg), - rabbit_shovel_behaviour:forward(Tag, #{}, Payload, State); +handle_source({amqp10_msg, _LinkRef, Msg0}, State) -> + Tag = amqp10_msg:delivery_id(Msg0), + [_ | Rest] = amqp10_msg:to_amqp_records(Msg0), + Bin = iolist_to_binary([amqp10_framing:encode_bin(D) || D <- Rest]), + Msg = mc:init(mc_amqp, Bin, #{}), + rabbit_shovel_behaviour:forward(Tag, Msg, State); handle_source({amqp10_event, {connection, Conn, opened}}, State = #{source := #{current := #{conn := Conn}}}) -> State; @@ -256,8 +259,8 @@ handle_dest({amqp10_event, {link, Link, credited}}, %% we have credit so can begin to forward State = State0#{dest => Dst#{link_state => credited, pending => []}}, - lists:foldl(fun ({A, B, C}, S) -> - forward(A, B, C, S) + lists:foldl(fun ({A, B}, S) -> + forward(A, B, S) end, State, lists:reverse(Pend)); handle_dest({amqp10_event, {link, Link, _Evt}}, State= #{dest := #{current := #{link := Link}}}) -> @@ -311,30 +314,29 @@ status(_) -> %% Destination not yet connected ignore. --spec forward(Tag :: tag(), Props :: #{atom() => any()}, - Payload :: binary(), state()) -> +-spec forward(Tag :: tag(), Mc :: mc:state(), state()) -> state() | {stop, any()}. -forward(_Tag, _Props, _Payload, +forward(_Tag, _Mc, #{source := #{remaining := 0}} = State) -> State; -forward(_Tag, _Props, _Payload, +forward(_Tag, _Mc, #{source := #{remaining_unacked := 0}} = State) -> State; -forward(Tag, Props, Payload, +forward(Tag, Mc, #{dest := #{current := #{link_state := attached}, pending := Pend0} = Dst} = State) -> %% simply cache the forward oo - Pend = [{Tag, Props, Payload} | Pend0], + Pend = [{Tag, Mc} | Pend0], State#{dest => Dst#{pending => {Pend}}}; -forward(Tag, Props, Payload, +forward(Tag, Msg0, #{dest := #{current := #{link := Link}, unacked := Unacked} = Dst, ack_mode := AckMode} = State) -> OutTag = rabbit_data_coercion:to_binary(Tag), - Msg0 = new_message(OutTag, Payload, State), - Msg = add_timestamp_header( - State, set_message_properties( - Props, add_forward_headers(State, Msg0))), + Msg1 = mc:protocol_state(mc:convert(mc_amqp, Msg0)), + Records = lists:flatten([amqp10_framing:decode_bin(iolist_to_binary(S)) || S <- Msg1]), + Msg2 = amqp10_msg:new(OutTag, Records, AckMode =/= on_confirm), + Msg = update_amqp10_message(Msg2, mc:exchange(Msg0), mc:routing_keys(Msg0), State), case send_msg(Link, Msg) of ok -> rabbit_shovel_behaviour:decr_remaining_unacked( @@ -363,14 +365,15 @@ send_msg(Link, Msg) -> end end. -new_message(Tag, Payload, #{ack_mode := AckMode, - dest := #{properties := Props, - application_properties := AppProps, - message_annotations := MsgAnns}}) -> - Msg0 = amqp10_msg:new(Tag, Payload, AckMode =/= on_confirm), +update_amqp10_message(Msg0, Exchange, RK, #{dest := #{properties := Props, + application_properties := AppProps0, + message_annotations := MsgAnns}} = State) -> Msg1 = amqp10_msg:set_properties(Props, Msg0), - Msg = amqp10_msg:set_message_annotations(MsgAnns, Msg1), - amqp10_msg:set_application_properties(AppProps, Msg). + Msg2 = amqp10_msg:set_message_annotations(MsgAnns, Msg1), + AppProps = AppProps0#{<<"exchange">> => Exchange, + <<"routing_key">> => RK}, + Msg = amqp10_msg:set_application_properties(AppProps, Msg2), + add_timestamp_header(State, add_forward_headers(State, Msg)). add_timestamp_header(#{dest := #{add_timestamp_header := true}}, Msg) -> P =#{creation_time => os:system_time(milli_seconds)}, @@ -378,58 +381,9 @@ add_timestamp_header(#{dest := #{add_timestamp_header := true}}, Msg) -> add_timestamp_header(_, Msg) -> Msg. add_forward_headers(#{dest := #{cached_forward_headers := Props}}, Msg) -> - amqp10_msg:set_application_properties(Props, Msg); + amqp10_msg:set_application_properties(Props, Msg); add_forward_headers(_, Msg) -> Msg. -set_message_properties(Props, Msg) -> - %% this is effectively special handling properties from amqp 0.9.1 - maps:fold( - fun(content_type, Ct, M) -> - amqp10_msg:set_properties( - #{content_type => to_binary(Ct)}, M); - (content_encoding, Ct, M) -> - amqp10_msg:set_properties( - #{content_encoding => to_binary(Ct)}, M); - (delivery_mode, 2, M) -> - amqp10_msg:set_headers(#{durable => true}, M); - (delivery_mode, 1, M) -> - % by default the durable flag is false - M; - (priority, P, M) when is_integer(P) -> - amqp10_msg:set_headers(#{priority => P}, M); - (correlation_id, Ct, M) -> - amqp10_msg:set_properties(#{correlation_id => to_binary(Ct)}, M); - (reply_to, Ct, M) -> - amqp10_msg:set_properties(#{reply_to => to_binary(Ct)}, M); - (message_id, Ct, M) -> - amqp10_msg:set_properties(#{message_id => to_binary(Ct)}, M); - (timestamp, Ct, M) -> - amqp10_msg:set_properties(#{creation_time => Ct}, M); - (user_id, Ct, M) -> - amqp10_msg:set_properties(#{user_id => Ct}, M); - (headers, Headers0, M) when is_list(Headers0) -> - %% AMPQ 0.9.1 are added as applicatin properties - %% TODO: filter headers to make safe - Headers = lists:foldl( - fun ({K, _T, V}, Acc) -> - case is_amqp10_compat(V) of - true -> - Acc#{to_binary(K) => V}; - false -> - Acc - end - end, #{}, Headers0), - amqp10_msg:set_application_properties(Headers, M); - (Key, Value, M) -> - case is_amqp10_compat(Value) of - true -> - amqp10_msg:set_application_properties( - #{to_binary(Key) => Value}, M); - false -> - M - end - end, Msg, Props). - gen_unique_name(Pre0, Post0) -> Pre = to_binary(Pre0), Post = to_binary(Post0), @@ -440,8 +394,3 @@ bin_to_hex(Bin) -> <<<= 10 -> N -10 + $a; true -> N + $0 end>> || <> <= Bin>>. - -is_amqp10_compat(T) -> - is_binary(T) orelse - is_number(T) orelse - is_boolean(T). diff --git a/deps/rabbitmq_shovel/src/rabbit_shovel_behaviour.erl b/deps/rabbitmq_shovel/src/rabbit_shovel_behaviour.erl index 8f7a890d1698..929a34c5ccf1 100644 --- a/deps/rabbitmq_shovel/src/rabbit_shovel_behaviour.erl +++ b/deps/rabbitmq_shovel/src/rabbit_shovel_behaviour.erl @@ -24,7 +24,7 @@ dest_protocol/1, source_endpoint/1, dest_endpoint/1, - forward/4, + forward/3, ack/3, nack/3, status/1, @@ -82,8 +82,7 @@ -callback ack(Tag :: tag(), Multi :: boolean(), state()) -> state(). -callback nack(Tag :: tag(), Multi :: boolean(), state()) -> state(). --callback forward(Tag :: tag(), Props :: #{atom() => any()}, - Payload :: binary(), state()) -> +-callback forward(Tag :: tag(), Msg :: mc:state(), state()) -> state() | {stop, any()}. -callback status(state()) -> rabbit_shovel_status:shovel_status(). @@ -144,10 +143,10 @@ source_endpoint(#{source := #{module := Mod}} = State) -> dest_endpoint(#{dest := #{module := Mod}} = State) -> Mod:dest_endpoint(State). --spec forward(tag(), #{atom() => any()}, binary(), state()) -> +-spec forward(tag(), mc:state(), state()) -> state() | {stop, any()}. -forward(Tag, Props, Payload, #{dest := #{module := Mod}} = State) -> - Mod:forward(Tag, Props, Payload, State). +forward(Tag, Msg, #{dest := #{module := Mod}} = State) -> + Mod:forward(Tag, Msg, State). -spec ack(tag(), boolean(), state()) -> state(). ack(Tag, Multi, #{source := #{module := Mod}} = State) -> diff --git a/deps/rabbitmq_shovel/test/amqp10_SUITE.erl b/deps/rabbitmq_shovel/test/amqp10_SUITE.erl index 937d37037cd3..3683dad53ac1 100644 --- a/deps/rabbitmq_shovel/test/amqp10_SUITE.erl +++ b/deps/rabbitmq_shovel/test/amqp10_SUITE.erl @@ -118,6 +118,7 @@ amqp10_destination(Config, AckMode) -> receive {amqp10_msg, Receiver, InMsg} -> + ct:pal("GOT ~p", [InMsg]), [<<42>>] = amqp10_msg:body(InMsg), #{content_type := ?UNSHOVELLED, content_encoding := ?UNSHOVELLED, @@ -129,10 +130,12 @@ amqp10_destination(Config, AckMode) -> % creation_time := Timestamp } = amqp10_msg:properties(InMsg), #{<<"routing_key">> := ?TO_SHOVEL, - <<"type">> := ?UNSHOVELLED, + <<"exchange">> := ?EXCHANGE, <<"header1">> := 1, <<"header2">> := <<"h2">> } = amqp10_msg:application_properties(InMsg), + #{<<"x-basic-type">> := ?UNSHOVELLED + } = amqp10_msg:message_annotations(InMsg), #{durable := true} = amqp10_msg:headers(InMsg), ok after ?TIMEOUT -> diff --git a/deps/rabbitmq_shovel/test/amqp10_dynamic_SUITE.erl b/deps/rabbitmq_shovel/test/amqp10_dynamic_SUITE.erl index 639045c76ae7..8870e76b7e8b 100644 --- a/deps/rabbitmq_shovel/test/amqp10_dynamic_SUITE.erl +++ b/deps/rabbitmq_shovel/test/amqp10_dynamic_SUITE.erl @@ -28,7 +28,8 @@ groups() -> autodelete_amqp091_dest_on_publish, simple_amqp10_dest, simple_amqp10_src, - amqp091_to_amqp10_with_dead_lettering + amqp091_to_amqp10_with_dead_lettering, + amqp10_to_amqp091_application_properties ]}, {with_map_config, [], [ simple, @@ -153,6 +154,7 @@ test_amqp10_destination(Config, Src, Dest, Sess, Protocol, ProtocolSrc) -> <<"message-ann-value">>}] end}]), Msg = publish_expect(Sess, Src, Dest, <<"tag1">>, <<"hello">>), + ct:pal("GOT ~p", [Msg]), AppProps = amqp10_msg:application_properties(Msg), ?assertMatch((#{user_id := <<"guest">>, creation_time := _}), @@ -193,6 +195,42 @@ simple_amqp10_src(Config) -> ok end). +amqp10_to_amqp091_application_properties(Config) -> + MapConfig = ?config(map_config, Config), + Src = ?config(srcq, Config), + Dest = ?config(destq, Config), + with_session(Config, + fun (Sess) -> + shovel_test_utils:set_param( + Config, + <<"test">>, [{<<"src-protocol">>, <<"amqp10">>}, + {<<"src-address">>, Src}, + {<<"dest-protocol">>, <<"amqp091">>}, + {<<"dest-queue">>, Dest}, + {<<"add-forward-headers">>, true}, + {<<"dest-add-timestamp-header">>, true}, + {<<"publish-properties">>, + case MapConfig of + true -> #{<<"cluster_id">> => <<"x">>}; + _ -> [{<<"cluster_id">>, <<"x">>}] + end} + ]), + + MsgSent = amqp10_msg:set_application_properties( + #{<<"key">> => <<"value">>}, + amqp10_msg:set_headers( + #{durable => true}, + amqp10_msg:new(<<"tag1">>, <<"hello">>, false))), + + Msg = publish_expect_msg(Sess, Src, Dest, MsgSent), + AppProps = amqp10_msg:application_properties(Msg), + ct:pal("MSG ~p", [Msg]), + + ?assertMatch(#{<<"key">> := <<"value">>}, + AppProps), + ok + end). + change_definition(Config) -> Src = ?config(srcq, Config), Dest = ?config(destq, Config), @@ -257,8 +295,8 @@ autodelete_do(Config, {AckMode, After, ExpSrc, ExpDest}) -> {<<"ack-mode">>, AckMode} ]), await_autodelete(Config, <<"test">>), - expect_count(Session, Dest, <<"hello">>, ExpDest), - expect_count(Session, Src, <<"hello">>, ExpSrc) + expect_count(Session, Dest, ExpDest), + expect_count(Session, Src, ExpSrc) end. autodelete_amqp091_src(Config, {AckMode, After, ExpSrc, ExpDest}) -> @@ -277,8 +315,8 @@ autodelete_amqp091_src(Config, {AckMode, After, ExpSrc, ExpDest}) -> {<<"ack-mode">>, AckMode} ]), await_autodelete(Config, <<"test">>), - expect_count(Session, Dest, <<"hello">>, ExpDest), - expect_count(Session, Src, <<"hello">>, ExpSrc) + expect_count(Session, Dest, ExpDest), + expect_count(Session, Src, ExpSrc) end. autodelete_amqp091_dest(Config, {AckMode, After, ExpSrc, ExpDest}) -> @@ -297,8 +335,8 @@ autodelete_amqp091_dest(Config, {AckMode, After, ExpSrc, ExpDest}) -> {<<"ack-mode">>, AckMode} ]), await_autodelete(Config, <<"test">>), - expect_count(Session, Dest, <<"hello">>, ExpDest), - expect_count(Session, Src, <<"hello">>, ExpSrc) + expect_count(Session, Dest, ExpDest), + expect_count(Session, Src, ExpSrc) end. %%---------------------------------------------------------------------------- @@ -323,6 +361,15 @@ publish(Sender, Tag, Payload) when is_binary(Payload) -> exit(publish_disposition_not_received) end. +publish(Sender, Msg) -> + ok = amqp10_client:send_msg(Sender, Msg), + Tag = amqp10_msg:delivery_tag(Msg), + receive + {amqp10_disposition, {accepted, Tag}} -> ok + after 3000 -> + exit(publish_disposition_not_received) + end. + publish_expect(Session, Source, Dest, Tag, Payload) -> LinkName = <<"dynamic-sender-", Dest/binary>>, {ok, Sender} = amqp10_client:attach_sender_link(Session, LinkName, Source, @@ -330,7 +377,16 @@ publish_expect(Session, Source, Dest, Tag, Payload) -> ok = await_amqp10_event(link, Sender, attached), publish(Sender, Tag, Payload), amqp10_client:detach_link(Sender), - expect_one(Session, Dest, Payload). + expect_one(Session, Dest). + +publish_expect_msg(Session, Source, Dest, Msg) -> + LinkName = <<"dynamic-sender-", Dest/binary>>, + {ok, Sender} = amqp10_client:attach_sender_link(Session, LinkName, Source, + unsettled, unsettled_state), + ok = await_amqp10_event(link, Sender, attached), + publish(Sender, Msg), + amqp10_client:detach_link(Sender), + expect_one(Session, Dest). await_amqp10_event(On, Ref, Evt) -> receive @@ -339,17 +395,17 @@ await_amqp10_event(On, Ref, Evt) -> exit({amqp10_event_timeout, On, Ref, Evt}) end. -expect_one(Session, Dest, Payload) -> +expect_one(Session, Dest) -> LinkName = <<"dynamic-receiver-", Dest/binary>>, {ok, Receiver} = amqp10_client:attach_receiver_link(Session, LinkName, Dest, settled, unsettled_state), ok = amqp10_client:flow_link_credit(Receiver, 1, never), - Msg = expect(Receiver, Payload), + Msg = expect(Receiver), amqp10_client:detach_link(Receiver), Msg. -expect(Receiver, _Payload) -> +expect(Receiver) -> receive {amqp10_msg, Receiver, InMsg} -> InMsg @@ -379,7 +435,7 @@ publish_count(Session, Address, Payload, Count) -> end || I <- lists:seq(1, Count)], amqp10_client:detach_link(Sender). -expect_count(Session, Address, Payload, Count) -> +expect_count(Session, Address, Count) -> {ok, Receiver} = amqp10_client:attach_receiver_link(Session, <<"dynamic-receiver", Address/binary>>, @@ -387,7 +443,7 @@ expect_count(Session, Address, Payload, Count) -> unsettled_state), ok = amqp10_client:flow_link_credit(Receiver, Count, never), [begin - expect(Receiver, Payload) + expect(Receiver) end || _ <- lists:seq(1, Count)], expect_empty(Session, Address), amqp10_client:detach_link(Receiver). diff --git a/deps/rabbitmq_shovel/test/amqp10_shovel_SUITE.erl b/deps/rabbitmq_shovel/test/amqp10_shovel_SUITE.erl index 9550c1b74309..834813fe6aea 100644 --- a/deps/rabbitmq_shovel/test/amqp10_shovel_SUITE.erl +++ b/deps/rabbitmq_shovel/test/amqp10_shovel_SUITE.erl @@ -62,8 +62,8 @@ end_per_testcase(_TestCase, _Config) -> amqp_encoded_data_list(_Config) -> meck:new(rabbit_shovel_behaviour, [passthrough]), meck:expect(rabbit_shovel_behaviour, forward, - fun (_, _, Pay, S) -> - ?assert(erlang:is_binary(Pay)), + fun (_, Msg, S) -> + ?assert(mc:is(Msg)), S end), %% fake some shovel state @@ -83,8 +83,8 @@ amqp_encoded_data_list(_Config) -> amqp_encoded_amqp_value(_Config) -> meck:new(rabbit_shovel_behaviour, [passthrough]), meck:expect(rabbit_shovel_behaviour, forward, - fun (_, _, Pay, S) -> - ?assert(erlang:is_binary(Pay)), + fun (_, Msg, S) -> + ?assert(mc:is(Msg)), S end), %% fake some shovel state From ccd3bde8105388ccaf844b4087dd9534e2fbc4ef Mon Sep 17 00:00:00 2001 From: Diana Parra Corbacho Date: Fri, 20 Jun 2025 09:09:41 +0200 Subject: [PATCH 02/31] amqp10_msg: type spec & code refactor --- deps/amqp10_client/src/amqp10_msg.erl | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/deps/amqp10_client/src/amqp10_msg.erl b/deps/amqp10_client/src/amqp10_msg.erl index a7666d11c0f0..d8e7013ca02b 100644 --- a/deps/amqp10_client/src/amqp10_msg.erl +++ b/deps/amqp10_client/src/amqp10_msg.erl @@ -265,32 +265,29 @@ body_bin(#amqp10_msg{body = #'v1_0.amqp_value'{} = Body}) -> %% A disposition will be notified to the sender by a message of the %% following stucture: %% {amqp10_disposition, {accepted | rejected, DeliveryTag}} --spec new(delivery_tag(), amqp10_body() | binary(), boolean()) -> amqp10_msg(). +-spec new(delivery_tag(), amqp10_body() | binary() | [amqp10_client_types:amqp10_msg_record()], boolean()) -> amqp10_msg(). new(DeliveryTag, Bin, Settled) when is_binary(Bin) -> Body = [#'v1_0.data'{content = Bin}], new(DeliveryTag, Body, Settled); new(DeliveryTag, Body, Settled) -> % TODO: constrain to amqp types + Transfer = #'v1_0.transfer'{ + delivery_tag = {binary, DeliveryTag}, + settled = Settled, + message_format = {uint, ?MESSAGE_FORMAT}}, case is_amqp10_body(Body) orelse (not is_list(Body)) of true -> #amqp10_msg{ - transfer = #'v1_0.transfer'{ - delivery_tag = {binary, DeliveryTag}, - settled = Settled, - message_format = {uint, ?MESSAGE_FORMAT}}, + transfer = Transfer, %% This lib is safe by default. header = #'v1_0.header'{durable = true}, body = Body}; false -> - Transfer = #'v1_0.transfer'{ - delivery_tag = {binary, DeliveryTag}, - settled = Settled, - message_format = {uint, ?MESSAGE_FORMAT}}, from_amqp_records([Transfer | Body]) end. %% @doc Create a new settled amqp10 message using the specified delivery tag %% and body. --spec new(delivery_tag(), amqp10_body() | binary()) -> amqp10_msg(). +-spec new(delivery_tag(), amqp10_body() | binary() | [amqp10_client_types:amqp10_msg_record()]) -> amqp10_msg(). new(DeliveryTag, Body) -> new(DeliveryTag, Body, false). From c8840785d9ac87d747e05f24061b01ed473344dd Mon Sep 17 00:00:00 2001 From: Diana Parra Corbacho Date: Fri, 4 Jul 2025 11:10:56 +0200 Subject: [PATCH 03/31] AMQP10 shovel: make bare message inmutable --- .../src/rabbit_amqp10_shovel.erl | 26 ++++------- .../src/rabbit_shovel_parameters.erl | 9 +++- deps/rabbitmq_shovel/test/amqp10_SUITE.erl | 44 ++++++++++++------- .../test/amqp10_dynamic_SUITE.erl | 22 +++++----- 4 files changed, 55 insertions(+), 46 deletions(-) diff --git a/deps/rabbitmq_shovel/src/rabbit_amqp10_shovel.erl b/deps/rabbitmq_shovel/src/rabbit_amqp10_shovel.erl index bd9dba0f945b..c4e98d42dcf4 100644 --- a/deps/rabbitmq_shovel/src/rabbit_amqp10_shovel.erl +++ b/deps/rabbitmq_shovel/src/rabbit_amqp10_shovel.erl @@ -151,9 +151,9 @@ init_source(State = #{source := #{current := #{link := Link}, init_dest(#{name := Name, shovel_type := Type, dest := #{add_forward_headers := true} = Dst} = State) -> - Props = #{<<"shovelled-by">> => rabbit_nodes:cluster_name(), - <<"shovel-type">> => rabbit_data_coercion:to_binary(Type), - <<"shovel-name">> => rabbit_data_coercion:to_binary(Name)}, + Props = #{<<"x-opt-shovelled-by">> => rabbit_nodes:cluster_name(), + <<"x-opt-shovel-type">> => rabbit_data_coercion:to_binary(Type), + <<"x-opt-shovel-name">> => rabbit_data_coercion:to_binary(Name)}, State#{dest => Dst#{cached_forward_headers => Props}}; init_dest(State) -> State. @@ -336,7 +336,7 @@ forward(Tag, Msg0, Msg1 = mc:protocol_state(mc:convert(mc_amqp, Msg0)), Records = lists:flatten([amqp10_framing:decode_bin(iolist_to_binary(S)) || S <- Msg1]), Msg2 = amqp10_msg:new(OutTag, Records, AckMode =/= on_confirm), - Msg = update_amqp10_message(Msg2, mc:exchange(Msg0), mc:routing_keys(Msg0), State), + Msg = add_timestamp_header(State, add_forward_headers(State, Msg2)), case send_msg(Link, Msg) of ok -> rabbit_shovel_behaviour:decr_remaining_unacked( @@ -365,23 +365,13 @@ send_msg(Link, Msg) -> end end. -update_amqp10_message(Msg0, Exchange, RK, #{dest := #{properties := Props, - application_properties := AppProps0, - message_annotations := MsgAnns}} = State) -> - Msg1 = amqp10_msg:set_properties(Props, Msg0), - Msg2 = amqp10_msg:set_message_annotations(MsgAnns, Msg1), - AppProps = AppProps0#{<<"exchange">> => Exchange, - <<"routing_key">> => RK}, - Msg = amqp10_msg:set_application_properties(AppProps, Msg2), - add_timestamp_header(State, add_forward_headers(State, Msg)). - add_timestamp_header(#{dest := #{add_timestamp_header := true}}, Msg) -> - P =#{creation_time => os:system_time(milli_seconds)}, - amqp10_msg:set_properties(P, Msg); + Anns = #{<<"x-opt-shovelled-timestamp">> => os:system_time(milli_seconds)}, + amqp10_msg:set_message_annotations(Anns, Msg); add_timestamp_header(_, Msg) -> Msg. -add_forward_headers(#{dest := #{cached_forward_headers := Props}}, Msg) -> - amqp10_msg:set_application_properties(Props, Msg); +add_forward_headers(#{dest := #{cached_forward_headers := Anns}}, Msg) -> + amqp10_msg:set_message_annotations(Anns, Msg); add_forward_headers(_, Msg) -> Msg. gen_unique_name(Pre0, Post0) -> diff --git a/deps/rabbitmq_shovel/src/rabbit_shovel_parameters.erl b/deps/rabbitmq_shovel/src/rabbit_shovel_parameters.erl index 2ae9ef6fe55b..4dc8bfc23490 100644 --- a/deps/rabbitmq_shovel/src/rabbit_shovel_parameters.erl +++ b/deps/rabbitmq_shovel/src/rabbit_shovel_parameters.erl @@ -181,9 +181,16 @@ amqp10_dest_validation(_Def, User) -> {<<"dest-address">>, fun rabbit_parameter_validation:binary/2, mandatory}, {<<"dest-add-forward-headers">>, fun rabbit_parameter_validation:boolean/2, optional}, {<<"dest-add-timestamp-header">>, fun rabbit_parameter_validation:boolean/2, optional}, + %% The bare message should be inmutable in the AMQP network. + %% Before RabbitMQ 4.2, we allowed to set application properties, message + %% annotations and any property. This is wrong. + %% From 4.2, the only message modification allowed is the optional + %% addition of forward headers and shovelled timestamp inside message + %% annotations. + %% To avoid breaking existing deployments, the following configuration + %% keys are still accepted but will be ignored. {<<"dest-application-properties">>, fun validate_amqp10_map/2, optional}, {<<"dest-message-annotations">>, fun validate_amqp10_map/2, optional}, - % TODO: restrict to allowed fields {<<"dest-properties">>, fun validate_amqp10_map/2, optional} ]. diff --git a/deps/rabbitmq_shovel/test/amqp10_SUITE.erl b/deps/rabbitmq_shovel/test/amqp10_SUITE.erl index 3683dad53ac1..b5881d31be43 100644 --- a/deps/rabbitmq_shovel/test/amqp10_SUITE.erl +++ b/deps/rabbitmq_shovel/test/amqp10_SUITE.erl @@ -8,6 +8,7 @@ -module(amqp10_SUITE). -include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). -include_lib("amqp_client/include/amqp_client.hrl"). -compile(export_all). @@ -116,27 +117,36 @@ amqp10_destination(Config, AckMode) -> }}, publish(Chan, Msg, ?EXCHANGE, ?TO_SHOVEL), + [NodeA] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename), + Node = atom_to_binary(NodeA), + receive {amqp10_msg, Receiver, InMsg} -> ct:pal("GOT ~p", [InMsg]), [<<42>>] = amqp10_msg:body(InMsg), - #{content_type := ?UNSHOVELLED, - content_encoding := ?UNSHOVELLED, - correlation_id := ?UNSHOVELLED, - user_id := <<"guest">>, - message_id := ?UNSHOVELLED, - reply_to := ?UNSHOVELLED - %% timestamp gets overwritten - % creation_time := Timestamp - } = amqp10_msg:properties(InMsg), - #{<<"routing_key">> := ?TO_SHOVEL, - <<"exchange">> := ?EXCHANGE, - <<"header1">> := 1, - <<"header2">> := <<"h2">> - } = amqp10_msg:application_properties(InMsg), - #{<<"x-basic-type">> := ?UNSHOVELLED - } = amqp10_msg:message_annotations(InMsg), - #{durable := true} = amqp10_msg:headers(InMsg), + Ts = Timestamp * 1000, + ?assertMatch( + #{content_type := ?UNSHOVELLED, + content_encoding := ?UNSHOVELLED, + correlation_id := ?UNSHOVELLED, + user_id := <<"guest">>, + message_id := ?UNSHOVELLED, + reply_to := ?UNSHOVELLED, + %% Message timestamp is no longer overwritten + creation_time := Ts}, + amqp10_msg:properties(InMsg)), + ?assertMatch( + #{<<"header1">> := 1, + <<"header2">> := <<"h2">>}, + amqp10_msg:application_properties(InMsg)), + ?assertMatch( + #{<<"x-basic-type">> := ?UNSHOVELLED, + <<"x-opt-shovel-type">> := <<"static">>, + <<"x-opt-shovel-name">> := <<"test_shovel">>, + <<"x-opt-shovelled-by">> := Node, + <<"x-opt-shovelled-timestamp">> := _}, + amqp10_msg:message_annotations(InMsg)), + ?assertMatch(#{durable := true}, amqp10_msg:headers(InMsg)), ok after ?TIMEOUT -> throw(timeout_waiting_for_deliver1) diff --git a/deps/rabbitmq_shovel/test/amqp10_dynamic_SUITE.erl b/deps/rabbitmq_shovel/test/amqp10_dynamic_SUITE.erl index 8870e76b7e8b..8a47a16feac6 100644 --- a/deps/rabbitmq_shovel/test/amqp10_dynamic_SUITE.erl +++ b/deps/rabbitmq_shovel/test/amqp10_dynamic_SUITE.erl @@ -154,18 +154,20 @@ test_amqp10_destination(Config, Src, Dest, Sess, Protocol, ProtocolSrc) -> <<"message-ann-value">>}] end}]), Msg = publish_expect(Sess, Src, Dest, <<"tag1">>, <<"hello">>), - ct:pal("GOT ~p", [Msg]), AppProps = amqp10_msg:application_properties(Msg), - - ?assertMatch((#{user_id := <<"guest">>, creation_time := _}), - (amqp10_msg:properties(Msg))), - ?assertMatch((#{<<"shovel-name">> := <<"test">>, - <<"shovel-type">> := <<"dynamic">>, <<"shovelled-by">> := _, - <<"app-prop-key">> := <<"app-prop-value">>}), - (AppProps)), + Anns = amqp10_msg:message_annotations(Msg), + %% We no longer add/override properties, application properties or + %% message annotations. Just the forward headers and timestamp as + %% message annotations. The AMQP 1.0 message is inmutable + ?assertNot(maps:is_key(user_id, amqp10_msg:properties(Msg))), + ?assertNot(maps:is_key(<<"app-prop-key">>, AppProps)), ?assertEqual(undefined, maps:get(<<"delivery_mode">>, AppProps, undefined)), - ?assertMatch((#{<<"x-message-ann-key">> := <<"message-ann-value">>}), - (amqp10_msg:message_annotations(Msg))). + ?assertNot(maps:is_key(<<"x-message-ann-key">>, Anns)), + ?assertMatch(#{<<"x-opt-shovel-name">> := <<"test">>, + <<"x-opt-shovel-type">> := <<"dynamic">>, + <<"x-opt-shovelled-by">> := _, + <<"x-opt-shovelled-timestamp">> := _ + }, Anns). simple_amqp10_src(Config) -> MapConfig = ?config(map_config, Config), From 444b5644b2201031ac8c6c039f81bc8715a55f1a Mon Sep 17 00:00:00 2001 From: Diana Parra Corbacho Date: Mon, 23 Jun 2025 16:16:25 +0200 Subject: [PATCH 04/31] Local shovels --- .../src/rabbit_amqp091_shovel.erl | 47 +- .../src/rabbit_amqp10_shovel.erl | 13 +- .../src/rabbit_local_shovel.erl | 637 ++++++++++ .../src/rabbit_shovel_config.erl | 3 +- .../src/rabbit_shovel_parameters.erl | 138 +- .../src/rabbit_shovel_util.erl | 60 +- .../src/rabbit_shovel_worker.erl | 11 +- deps/rabbitmq_shovel/test/amqp10_SUITE.erl | 1 - deps/rabbitmq_shovel/test/local_SUITE.erl | 517 ++++++++ .../test/local_dynamic_SUITE.erl | 1110 +++++++++++++++++ .../test/shovel_test_utils.erl | 33 +- 11 files changed, 2497 insertions(+), 73 deletions(-) create mode 100644 deps/rabbitmq_shovel/src/rabbit_local_shovel.erl create mode 100644 deps/rabbitmq_shovel/test/local_SUITE.erl create mode 100644 deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl diff --git a/deps/rabbitmq_shovel/src/rabbit_amqp091_shovel.erl b/deps/rabbitmq_shovel/src/rabbit_amqp091_shovel.erl index a8456bb6856c..7b3228450b6e 100644 --- a/deps/rabbitmq_shovel/src/rabbit_amqp091_shovel.erl +++ b/deps/rabbitmq_shovel/src/rabbit_amqp091_shovel.erl @@ -7,8 +7,6 @@ -module(rabbit_amqp091_shovel). --define(APP, rabbitmq_shovel). - -behaviour(rabbit_shovel_behaviour). -include_lib("amqp_client/include/amqp_client.hrl"). @@ -58,7 +56,7 @@ parse(_Name, {source, Source}) -> CArgs = proplists:get_value(consumer_args, Source, []), #{module => ?MODULE, uris => proplists:get_value(uris, Source), - resource_decl => decl_fun({source, Source}), + resource_decl => rabbit_shovel_util:decl_fun(?MODULE, {source, Source}), queue => Queue, delete_after => proplists:get_value(delete_after, Source, never), prefetch_count => Prefetch, @@ -74,7 +72,7 @@ parse(Name, {destination, Dest}) -> PropsFun2 = add_timestamp_header_fun(ATH, PropsFun1), #{module => ?MODULE, uris => proplists:get_value(uris, Dest), - resource_decl => decl_fun({destination, Dest}), + resource_decl => rabbit_shovel_util:decl_fun(?MODULE, {destination, Dest}), props_fun => PropsFun2, fields_fun => PubFieldsFun, add_forward_headers => AFH, @@ -544,47 +542,6 @@ props_fun_timestamp_header({M, F, Args}, SrcUri, DestUri, Props) -> rabbit_shovel_util:add_timestamp_header( apply(M, F, Args ++ [SrcUri, DestUri, Props])). -parse_declaration({[], Acc}) -> - Acc; -parse_declaration({[{Method, Props} | Rest], Acc}) when is_list(Props) -> - FieldNames = try rabbit_framing_amqp_0_9_1:method_fieldnames(Method) - catch exit:Reason -> fail(Reason) - end, - case proplists:get_keys(Props) -- FieldNames of - [] -> ok; - UnknownFields -> fail({unknown_fields, Method, UnknownFields}) - end, - {Res, _Idx} = lists:foldl( - fun (K, {R, Idx}) -> - NewR = case proplists:get_value(K, Props) of - undefined -> R; - V -> setelement(Idx, R, V) - end, - {NewR, Idx + 1} - end, {rabbit_framing_amqp_0_9_1:method_record(Method), 2}, - FieldNames), - parse_declaration({Rest, [Res | Acc]}); -parse_declaration({[{Method, Props} | _Rest], _Acc}) -> - fail({expected_method_field_list, Method, Props}); -parse_declaration({[Method | Rest], Acc}) -> - parse_declaration({[{Method, []} | Rest], Acc}). - -decl_fun({source, Endpoint}) -> - case parse_declaration({proplists:get_value(declarations, Endpoint, []), []}) of - [] -> - case proplists:get_value(predeclared, application:get_env(?APP, topology, []), false) of - true -> case proplists:get_value(queue, Endpoint) of - <<>> -> fail({invalid_parameter_value, declarations, {require_non_empty}}); - Queue -> {?MODULE, check_fun, [Queue]} - end; - false -> {?MODULE, decl_fun, []} - end; - Decl -> {?MODULE, decl_fun, [Decl]} - end; -decl_fun({destination, Endpoint}) -> - Decl = parse_declaration({proplists:get_value(declarations, Endpoint, []), []}), - {?MODULE, decl_fun, [Decl]}. - decl_fun(Decl, _Conn, Ch) -> [begin amqp_channel:call(Ch, M) diff --git a/deps/rabbitmq_shovel/src/rabbit_amqp10_shovel.erl b/deps/rabbitmq_shovel/src/rabbit_amqp10_shovel.erl index c4e98d42dcf4..1ab04692c900 100644 --- a/deps/rabbitmq_shovel/src/rabbit_amqp10_shovel.erl +++ b/deps/rabbitmq_shovel/src/rabbit_amqp10_shovel.erl @@ -122,7 +122,7 @@ connect(Name, SndSettleMode, Uri, Postfix, Addr, Map, AttachFun) -> {ok, Sess} = amqp10_client:begin_session(Conn), link(Conn), LinkName = begin - LinkName0 = gen_unique_name(Name, Postfix), + LinkName0 = rabbit_shovel_util:gen_unique_name(Name, Postfix), rabbit_data_coercion:to_binary(LinkName0) end, % needs to be sync, i.e. awaits the 'attach' event as @@ -373,14 +373,3 @@ add_timestamp_header(_, Msg) -> Msg. add_forward_headers(#{dest := #{cached_forward_headers := Anns}}, Msg) -> amqp10_msg:set_message_annotations(Anns, Msg); add_forward_headers(_, Msg) -> Msg. - -gen_unique_name(Pre0, Post0) -> - Pre = to_binary(Pre0), - Post = to_binary(Post0), - Id = bin_to_hex(crypto:strong_rand_bytes(8)), - <
>/binary, Id/binary, <<"_">>/binary, Post/binary>>.
-
-bin_to_hex(Bin) ->
-    <<<= 10 -> N -10 + $a;
-           true  -> N + $0 end>>
-      || <> <= Bin>>.
diff --git a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
new file mode 100644
index 000000000000..d1759379ae5b
--- /dev/null
+++ b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
@@ -0,0 +1,637 @@
+%% This Source Code Form is subject to the terms of the Mozilla Public
+%% License, v. 2.0. If a copy of the MPL was not distributed with this
+%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
+%%
+%% Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
+%%
+
+-module(rabbit_local_shovel).
+
+-behaviour(rabbit_shovel_behaviour).
+
+-include_lib("amqp_client/include/amqp_client.hrl").
+-include_lib("amqp10_common/include/amqp10_types.hrl").
+-include_lib("rabbit/include/mc.hrl").
+-include("rabbit_shovel.hrl").
+
+-export([
+         parse/2,
+         connect_source/1,
+         connect_dest/1,
+         init_source/1,
+         init_dest/1,
+         source_uri/1,
+         dest_uri/1,
+         source_protocol/1,
+         dest_protocol/1,
+         source_endpoint/1,
+         dest_endpoint/1,
+         close_dest/1,
+         close_source/1,
+         handle_source/2,
+         handle_dest/2,
+         ack/3,
+         nack/3,
+         forward/3,
+         status/1
+        ]).
+
+-export([
+         src_decl_exchange/4,
+         decl_queue/4,
+         dest_decl_queue/4,
+         check_queue/4,
+         dest_check_queue/4,
+         decl_fun/3,
+         check_fun/3
+        ]).
+
+-define(QUEUE, lqueue).
+
+-record(pending_ack, {
+                      delivery_tag,
+                      msg_id
+                     }).
+
+parse(_Name, {source, Source}) ->
+    Prefetch = parse_parameter(prefetch_count, fun parse_non_negative_integer/1,
+                               proplists:get_value(prefetch_count, Source,
+                                                   ?DEFAULT_PREFETCH)),
+    Queue = parse_parameter(queue, fun parse_binary/1,
+                            proplists:get_value(queue, Source)),
+    CArgs = proplists:get_value(consumer_args, Source, []),
+    #{module => ?MODULE,
+      uris => proplists:get_value(uris, Source),
+      resource_decl => rabbit_shovel_util:decl_fun(?MODULE, {source, Source}),
+      queue => Queue,
+      delete_after => proplists:get_value(delete_after, Source, never),
+      prefetch_count => Prefetch,
+      consumer_args => CArgs};
+parse(_Name, {destination, Dest}) ->
+    Exchange = parse_parameter(dest_exchange, fun parse_binary/1,
+                               proplists:get_value(dest_exchange, Dest, none)),
+    RK = parse_parameter(dest_exchange_key, fun parse_binary/1,
+                         proplists:get_value(dest_routing_key, Dest, none)),
+    #{module => ?MODULE,
+      uris => proplists:get_value(uris, Dest),
+      resource_decl  => rabbit_shovel_util:decl_fun(?MODULE, {destination, Dest}),
+      exchange => Exchange,
+      routing_key => RK,
+      add_forward_headers => proplists:get_value(add_forward_headers, Dest, false),
+      add_timestamp_header => proplists:get_value(add_timestamp_header, Dest, false)}.
+
+connect_source(State = #{source := Src = #{resource_decl := {M, F, MFArgs},
+                                           queue := QName0,
+                                           uris := [Uri | _]}}) ->
+    QState = rabbit_queue_type:init(),
+    {User, VHost} = get_user_vhost_from_amqp_param(Uri),
+    %% We handle the most recently declared queue to use anonymous functions
+    %% It's usually the channel that does it
+    MRDQ = apply(M, F, MFArgs ++ [VHost, User]),
+    QName = case QName0 of
+                <<>> -> MRDQ;
+                _ -> QName0
+            end,
+    State#{source => Src#{current => #{queue_states => QState,
+                                       next_tag => 1,
+                                       user => User,
+                                       vhost => VHost},
+                          queue => QName},
+           unacked_message_q => ?QUEUE:new()}.
+
+connect_dest(State = #{dest := Dest = #{resource_decl := {M, F, MFArgs},
+                                        uris := [Uri | _]
+                                       },
+                       ack_mode := AckMode}) ->
+    %% Shall we get the user from an URI or something else?
+    {User, VHost} = get_user_vhost_from_amqp_param(Uri),
+    apply(M, F, MFArgs ++ [VHost, User]),
+
+    QState = rabbit_queue_type:init(),
+    case AckMode of
+        on_confirm ->
+            State#{dest => Dest#{current => #{queue_states => QState,
+                                              delivery_id => 1,
+                                              vhost => VHost},
+                                 unacked => #{}}};
+        _ ->
+            State#{dest => Dest#{current => #{queue_states => QState,
+                                              vhost => VHost},
+                                 unacked => #{}}}
+    end.
+
+init_source(State = #{source := #{queue := QName0,
+                                  prefetch_count := Prefetch,
+                                  consumer_args := Args,
+                                  current := #{queue_states := QState0,
+                                               vhost := VHost} = Current} = Src,
+                      name := Name,
+                      ack_mode := AckMode}) ->
+    _Mode = case rabbit_feature_flags:is_enabled('rabbitmq_4.0.0') of
+                true ->
+                    {credited, Prefetch};
+                false ->
+                    {credited, credit_api_v1}
+            end,
+    QName = rabbit_misc:r(VHost, queue, QName0),
+    CTag = consumer_tag(Name),
+    case rabbit_amqqueue:with(
+           QName,
+           fun(Q) ->
+                   SndSettled = case AckMode of
+                                    no_ack -> true;
+                                    on_publish -> false;
+                                    on_confirm -> false
+                               end,
+                   Spec = #{no_ack => SndSettled,
+                            channel_pid => self(),
+                            limiter_pid => none,
+                            limiter_active => false,
+                            mode => {simple_prefetch, Prefetch},
+                            consumer_tag => CTag,
+                            exclusive_consume => false,
+                            args => Args,
+                            ok_msg => undefined,
+                            acting_user => ?SHOVEL_USER},
+                   case remaining(Q, State) of
+                       0 ->
+                          {0, {error, autodelete}};
+                       Remaining ->
+                           {Remaining, rabbit_queue_type:consume(Q, Spec, QState0)}
+                   end
+           end) of
+        {Remaining, {ok, QState}} ->
+            State#{source => Src#{current => Current#{queue_states => QState,
+                                                      consumer_tag => CTag},
+                                  remaining => Remaining,
+                                  remaining_unacked => Remaining}};
+        {0, {error, autodelete}} ->
+            exit({shutdown, autodelete});
+        {_Remaining, {error, Reason}} ->
+            rabbit_log:error(
+              "Shovel '~ts' in vhost '~ts' failed to consume: ~ts",
+              [Name, VHost, Reason]),
+            exit({shutdown, failed_to_consume_from_source});
+        {unlimited, {error, not_implemented, Reason, ReasonArgs}} ->
+            rabbit_log:error(
+              "Shovel '~ts' in vhost '~ts' failed to consume: ~ts",
+              [Name, VHost, io_lib:format(Reason, ReasonArgs)]),
+            exit({shutdown, failed_to_consume_from_source});
+        {error, not_found} ->
+            exit({shutdown, missing_source_queue})
+    end.
+
+init_dest(#{name := Name,
+            shovel_type := Type,
+            dest := #{add_forward_headers := AFH} = Dst} = State) ->
+    case AFH of
+        true ->
+            Props = #{<<"x-opt-shovelled-by">> => rabbit_nodes:cluster_name(),
+                      <<"x-opt-shovel-type">> => rabbit_data_coercion:to_binary(Type),
+                      <<"x-opt-shovel-name">> => rabbit_data_coercion:to_binary(Name)},
+            State#{dest => Dst#{cached_forward_headers => Props}};
+        false ->
+            State
+    end.
+
+source_uri(_State) ->
+    "".
+
+dest_uri(_State) ->
+    "".
+
+source_protocol(_State) ->
+    local.
+
+dest_protocol(_State) ->
+    local.
+
+source_endpoint(#{source := #{queue := Queue,
+                              exchange := SrcX,
+                              routing_key := SrcXKey}}) ->
+    [{src_exchange, SrcX},
+     {src_exchange_key, SrcXKey},
+     {src_queue, Queue}];
+source_endpoint(#{source := #{queue := Queue}}) ->
+    [{src_queue, Queue}];
+source_endpoint(_Config) ->
+    [].
+
+dest_endpoint(#{dest := #{exchange := SrcX,
+                          routing_key := SrcXKey}}) ->
+    [{dest_exchange, SrcX},
+     {dest_exchange_key, SrcXKey}];
+dest_endpoint(#{dest := #{queue := Queue}}) ->
+    [{dest_queue, Queue}];
+dest_endpoint(_Config) ->
+    [].
+      
+close_dest(_State) ->
+    ok.
+
+close_source(#{source := #{current := #{queue_states := QStates0,
+                                        consumer_tag := CTag,
+                                        user := User,
+                                        vhost := VHost},
+                           queue := QName0}}) ->
+    QName = rabbit_misc:r(VHost, queue, QName0),
+    case rabbit_amqqueue:with(
+           QName,
+           fun(Q) ->
+                   rabbit_queue_type:cancel(Q, #{consumer_tag => CTag,
+                                                 reason => remove,
+                                                 user => User#user.username}, QStates0)
+           end) of
+        {ok, _QStates} ->
+            ok;
+        {error, not_found} ->
+            ok;
+        {error, Reason} ->
+            rabbit_log:warning("Local shovel failed to remove consumer ~tp: ~tp",
+                               [CTag, Reason]),
+            ok
+    end;
+close_source(_) ->
+    %% No consumer tag, no consumer to cancel
+    ok.
+
+handle_source(#'basic.ack'{delivery_tag = Seq, multiple = Multiple},
+              State = #{ack_mode := on_confirm}) ->
+    confirm_to_inbound(fun(Tag, Multi, StateX) ->
+                               rabbit_shovel_behaviour:ack(Tag, Multi, StateX)
+                       end, Seq, Multiple, State);
+
+handle_source({queue_event, _, {Type, _, _}}, _State) when Type =:= confirm;
+                                                           Type =:= reject_publish ->
+    not_handled;
+handle_source({queue_event, QRef, Evt}, #{source := Source = #{current := Current = #{queue_states := QueueStates0}}} = State0) ->
+    case rabbit_queue_type:handle_event(QRef, Evt, QueueStates0) of
+        {ok, QState1, Actions} ->
+            State = State0#{source => Source#{current => Current#{queue_states => QState1}}},
+            handle_queue_actions(Actions, State);
+        {eol, Actions} ->
+            _ = handle_queue_actions(Actions, State0),
+            {stop, {inbound_link_or_channel_closure, queue_deleted}};
+        {protocol_error, _Type, Reason, ReasonArgs} ->
+            {stop, list_to_binary(io_lib:format(Reason, ReasonArgs))}
+    end;
+handle_source({{'DOWN', #resource{name = Queue,
+                                  kind = queue,
+                                  virtual_host = VHost}}, _, _, _, _}  ,
+              #{source := #{queue := Queue, current := #{vhost := VHost}}}) ->
+    {stop, {inbound_link_or_channel_closure, source_queue_down}};
+handle_source(_Msg, _State) ->
+    not_handled.
+
+handle_dest({queue_event, _QRef, {confirm, MsgSeqNos, _QPid}},
+            #{ack_mode := on_confirm} = State) ->
+    confirm_to_inbound(fun(Tag, Multi, StateX) ->
+                               rabbit_shovel_behaviour:ack(Tag, Multi, StateX)
+                       end, MsgSeqNos, false, State);
+handle_dest({queue_event, _QRef, {reject_publish, Seq, _QPid}},
+            #{ack_mode := on_confirm} = State) ->
+    confirm_to_inbound(fun(Tag, Multi, StateX) ->
+                               rabbit_shovel_behaviour:nack(Tag, Multi, StateX)
+                       end, Seq, false, State);
+handle_dest({{'DOWN', #resource{name = Queue,
+                                kind = queue,
+                                virtual_host = VHost}}, _, _, _, _}  ,
+            #{dest := #{queue := Queue, current := #{vhost := VHost}}}) ->
+    {stop, {outbound_link_or_channel_closure, dest_queue_down}};
+handle_dest(_Msg, State) ->
+    State.
+
+ack(DeliveryTag, Multiple, State) ->
+    settle(complete, DeliveryTag, Multiple, State).
+
+nack(DeliveryTag, Multiple, State) ->
+    settle(discard, DeliveryTag, Multiple, State).
+
+forward(Tag, Msg0, #{dest := #{current := #{queue_states := QState} = Current,
+                               unacked := Unacked} = Dest,
+                     ack_mode := AckMode} = State0) ->
+    {Options, #{dest := #{current := Current1} = Dest1} = State} =
+        case AckMode of
+            on_confirm  ->
+                DeliveryId = maps:get(delivery_id, Current),
+                Opts = #{correlation => DeliveryId},
+                {Opts, State0#{dest => Dest#{current => Current#{delivery_id => DeliveryId + 1}}}};
+            _ ->
+                {#{}, State0}
+        end,
+    Msg = set_annotations(Msg0, Dest),
+    QNames = route(Msg, Dest),
+    Queues = rabbit_amqqueue:lookup_many(QNames),
+    case rabbit_queue_type:deliver(Queues, Msg, Options, QState) of
+        {ok, QState1, Actions} ->
+            %% TODO handle credit?
+            State1 = State#{dest => Dest1#{current => Current1#{queue_states => QState1}}},
+            #{dest := Dst1} = State2 = rabbit_shovel_behaviour:incr_forwarded(State1),
+            State4 = rabbit_shovel_behaviour:decr_remaining_unacked(
+                       case AckMode of
+                           no_ack ->
+                               rabbit_shovel_behaviour:decr_remaining(1, State2);
+                           on_confirm ->
+                               Correlation = maps:get(correlation, Options),
+                               State2#{dest => Dst1#{unacked => Unacked#{Correlation => Tag}}};
+                           on_publish ->
+                               State3 = rabbit_shovel_behaviour:ack(Tag, false, State2),
+                               rabbit_shovel_behaviour:decr_remaining(1, State3)
+                       end),
+            handle_queue_actions(Actions, State4);
+        {error, Reason} ->
+            exit({shutdown, Reason})
+    end.
+
+set_annotations(Msg, Dest) ->
+    add_routing(add_forward_headers(add_timestamp_header(Msg, Dest), Dest), Dest).
+
+add_timestamp_header(Msg, #{add_timestamp_header := true}) ->
+    mc:set_annotation(<<"x-opt-shovelled-timestamp">>, os:system_time(milli_seconds), Msg);
+add_timestamp_header(Msg, _) ->
+    Msg.
+
+add_forward_headers(Msg, #{cached_forward_headers := Props}) ->
+    maps:fold(fun(K, V, Acc) ->
+                      mc:set_annotation(K, V, Acc)
+              end, Msg, Props);
+add_forward_headers(Msg, _D) ->
+    Msg.
+
+add_routing(Msg0, Dest) ->
+    Msg = case maps:get(exchange, Dest, undefined) of
+              undefined -> Msg0;
+              Exchange -> mc:set_annotation(?ANN_EXCHANGE, Exchange, Msg0)
+          end,
+    case maps:get(routing_key, Dest, undefined) of
+        undefined -> Msg;
+        RK -> mc:set_annotation(?ANN_ROUTING_KEYS, [RK], Msg)
+    end.
+
+status(_) ->
+    running.
+
+%% Internal
+
+parse_parameter(_, _, none) ->
+    none;
+parse_parameter(Param, Fun, Value) ->
+    try
+        Fun(Value)
+    catch
+        _:{error, Err} ->
+            fail({invalid_parameter_value, Param, Err})
+    end.
+
+parse_non_negative_integer(N) when is_integer(N) andalso N >= 0 ->
+    N;
+parse_non_negative_integer(N) ->
+    fail({require_non_negative_integer, N}).
+
+parse_binary(Binary) when is_binary(Binary) ->
+    Binary;
+parse_binary(NotABinary) ->
+    fail({require_binary, NotABinary}).
+
+consumer_tag(Name) ->
+    CTag0 = rabbit_shovel_util:gen_unique_name(Name, "receiver"),
+    rabbit_data_coercion:to_binary(CTag0).
+
+-spec fail(term()) -> no_return().
+fail(Reason) -> throw({error, Reason}).
+
+handle_queue_actions(Actions, State) ->
+    lists:foldl(
+      fun({deliver, _CTag, AckRequired, Msgs}, S0) ->
+              handle_deliver(AckRequired, Msgs, S0);
+         (_, _) ->
+              not_handled
+         %% ({queue_down, QRef}, S0) ->
+         %%      State;
+         %% ({block, QName}, S0) ->
+         %%      State;
+         %% ({unblock, QName}, S0) ->
+         %%      State
+      end, State, Actions).
+
+handle_deliver(AckRequired, Msgs, State) when is_list(Msgs) ->
+    lists:foldl(fun({_QName, _QPid, MsgId, _Redelivered, Mc}, S0) ->
+                        DeliveryTag = next_tag(S0),
+                        S = record_pending(AckRequired, DeliveryTag, MsgId, increase_next_tag(S0)),
+                        rabbit_shovel_behaviour:forward(DeliveryTag, Mc, S)
+               end, State, Msgs).
+
+next_tag(#{source := #{current := #{next_tag := DeliveryTag}}}) ->
+    DeliveryTag.
+
+increase_next_tag(#{source := Source = #{current := Current = #{next_tag := DeliveryTag}}} = State) ->
+    State#{source => Source#{current => Current#{next_tag => DeliveryTag + 1}}}.
+
+record_pending(false, _DeliveryTag, _MsgId, State) ->
+    State;
+record_pending(true, DeliveryTag, MsgId, #{unacked_message_q := UAMQ0} = State) ->
+    UAMQ = ?QUEUE:in(#pending_ack{delivery_tag = DeliveryTag,
+                                  msg_id = MsgId}, UAMQ0),
+    State#{unacked_message_q => UAMQ}.
+
+remaining(_Q, #{source := #{delete_after := never}}) ->
+    unlimited;
+remaining(Q, #{source := #{delete_after := 'queue-length'}}) ->
+    [{messages, Count}] = rabbit_amqqueue:info(Q, [messages]),
+    Count;
+remaining(_Q, #{source := #{delete_after := Count}}) ->
+    Count.
+
+decl_fun(Decl, VHost, User) ->
+    lists:foldr(
+      fun(Method, MRDQ) -> %% keep track of most recently declared queue
+              Reply = rabbit_channel:handle_method(
+                        expand_shortcuts(Method, MRDQ),
+                        none, #{}, none, VHost, User),
+              case {Method, Reply} of
+                  {#'queue.declare'{}, {ok, QName, _, _}} ->
+                      QName#resource.name;
+                  _ ->
+                      MRDQ
+              end
+      end, <<>>, Decl).
+
+expand_shortcuts(#'queue.bind'   {queue = Q, routing_key = K} = M, MRDQ) ->
+    M#'queue.bind'   {queue       = expand_queue_name_shortcut(Q, MRDQ),
+                      routing_key = expand_routing_key_shortcut(Q, K, MRDQ)};
+expand_shortcuts(#'queue.unbind' {queue = Q, routing_key = K} = M, MRDQ) ->
+    M#'queue.unbind' {queue       = expand_queue_name_shortcut(Q, MRDQ),
+                      routing_key = expand_routing_key_shortcut(Q, K, MRDQ)};
+expand_shortcuts(M, _State) ->
+    M.
+
+expand_queue_name_shortcut(<<>>, <<>>) ->
+    exit({shutdown, {not_found, "no previously declared queue"}});
+expand_queue_name_shortcut(<<>>, MRDQ) ->
+    MRDQ;
+expand_queue_name_shortcut(QueueNameBin, _) ->
+    QueueNameBin.
+
+expand_routing_key_shortcut(<<>>, <<>>, <<>>) ->
+    exit({shutdown, {not_found, "no previously declared queue"}});
+expand_routing_key_shortcut(<<>>, <<>>, MRDQ) ->
+    MRDQ;
+expand_routing_key_shortcut(_QueueNameBin, RoutingKey, _) ->
+    RoutingKey.
+
+%% TODO A missing queue stops the shovel but because the error reason
+%% the failed status is not stored. Would not be it more useful to
+%% report it??? This is a rabbit_shovel_worker issues, last terminate
+%% clause
+check_fun(QName, VHost, User) ->
+    Method = #'queue.declare'{queue = QName,
+                              passive = true},
+    decl_fun([Method], VHost, User).
+
+src_decl_exchange(SrcX, SrcXKey, VHost, User) ->
+    Methods = [#'queue.bind'{routing_key = SrcXKey,
+                             exchange    = SrcX},
+               #'queue.declare'{exclusive = true}],
+    decl_fun(Methods, VHost, User).
+
+dest_decl_queue(none, _, _, _) ->
+    ok;
+dest_decl_queue(QName, QArgs, VHost, User) ->
+    decl_queue(QName, QArgs, VHost, User).
+
+decl_queue(QName, QArgs, VHost, User) ->
+    Args = rabbit_misc:to_amqp_table(QArgs),
+    Method = #'queue.declare'{queue = QName,
+                              durable = true,
+                              arguments = Args},
+    decl_fun([Method], VHost, User).
+
+dest_check_queue(none, _, _, _) ->
+    ok;
+dest_check_queue(QName, QArgs, VHost, User) ->
+    check_queue(QName, QArgs, VHost, User).
+
+check_queue(QName, _QArgs, VHost, User) ->
+    Method = #'queue.declare'{queue = QName,
+                              passive = true},
+    decl_fun([Method], VHost, User).
+
+get_user_vhost_from_amqp_param(Uri) ->
+    {ok, AmqpParam} = amqp_uri:parse(Uri),
+    rabbit_log:warning("AMQP PARAM ~p", [AmqpParam]),
+    {Username, Password, VHost} =
+        case AmqpParam of
+            #amqp_params_direct{username = U,
+                                password = P,
+                                virtual_host = V} ->
+                {U, P, V};
+            #amqp_params_network{username = U,
+                                 password = P,
+                                 virtual_host = V} ->
+                {U, P, V}
+        end,
+    case rabbit_access_control:check_user_login(Username, [{password, Password}]) of
+        {ok, User} ->
+            try
+                rabbit_access_control:check_vhost_access(User, VHost, undefined, #{}) of
+                ok ->
+                    {User, VHost}
+            catch
+                exit:#amqp_error{name = not_allowed} ->
+                    exit({shutdown, {access_refused, Username}})
+            end;
+        {refused, Username, _Msg, _Module} ->
+            rabbit_log:error("Local shovel user ~ts was refused access"),
+            exit({shutdown, {access_refused, Username}})
+    end.
+
+settle(Op, DeliveryTag, Multiple, #{unacked_message_q := UAMQ0,
+                             source := #{queue := Queue,
+                                         current := Current = #{queue_states := QState0,
+                                                                consumer_tag := CTag,
+                                                                vhost := VHost}} = Src} = State) ->
+    {Acked, UAMQ} = collect_acks(UAMQ0, DeliveryTag, Multiple),
+    QRef = rabbit_misc:r(VHost, queue, Queue),
+    MsgIds = [Ack#pending_ack.msg_id || Ack <- Acked],
+    case rabbit_queue_type:settle(QRef, Op, CTag, MsgIds, QState0) of
+        {ok, QState1, Actions} ->
+            QState = handle_queue_actions(Actions, QState1),
+            State#{source => Src#{current => Current#{queue_states => QState}},
+                   unacked_message_q => UAMQ};
+        {'protocol_error', Type, Reason, Args} ->
+            rabbit_log:error("Shovel failed to settle ~p acknowledgments with ~tp: ~tp",
+                             [Op, Type, io_lib:format(Reason, Args)]),
+            exit({shutdown, {ack_failed, Reason}})
+    end.
+
+%% From rabbit_channel
+%% Records a client-sent acknowledgement. Handles both single delivery acks
+%% and multi-acks.
+%%
+%% Returns a tuple of acknowledged pending acks and remaining pending acks.
+%% Sorts each group in the youngest-first order (descending by delivery tag).
+%% The special case for 0 comes from the AMQP 0-9-1 spec: if the multiple field is set to 1 (true),
+%% and the delivery tag is 0, this indicates acknowledgement of all outstanding messages (by a client).
+collect_acks(UAMQ, 0, true) ->
+    {lists:reverse(?QUEUE:to_list(UAMQ)), ?QUEUE:new()};
+collect_acks(UAMQ, DeliveryTag, Multiple) ->
+    collect_acks([], [], UAMQ, DeliveryTag, Multiple).
+
+collect_acks(AcknowledgedAcc, RemainingAcc, UAMQ, DeliveryTag, Multiple) ->
+    case ?QUEUE:out(UAMQ) of
+        {{value, UnackedMsg = #pending_ack{delivery_tag = CurrentDT}},
+         UAMQTail} ->
+            if CurrentDT == DeliveryTag ->
+                   {[UnackedMsg | AcknowledgedAcc],
+                    case RemainingAcc of
+                        [] -> UAMQTail;
+                        _  -> ?QUEUE:join(
+                                 ?QUEUE:from_list(lists:reverse(RemainingAcc)),
+                                 UAMQTail)
+                    end};
+               Multiple ->
+                    collect_acks([UnackedMsg | AcknowledgedAcc], RemainingAcc,
+                                 UAMQTail, DeliveryTag, Multiple);
+               true ->
+                    collect_acks(AcknowledgedAcc, [UnackedMsg | RemainingAcc],
+                                 UAMQTail, DeliveryTag, Multiple)
+            end;
+        {empty, UAMQTail} ->
+           {AcknowledgedAcc, UAMQTail}
+    end.
+
+route(_Msg, #{queue := Queue,
+              current := #{vhost := VHost}}) when Queue =/= none ->
+    QName = rabbit_misc:r(VHost, queue, Queue),
+    [QName];
+route(Msg, #{current := #{vhost := VHost}}) ->
+    ExchangeName = rabbit_misc:r(VHost, exchange, mc:exchange(Msg)),
+    Exchange = rabbit_exchange:lookup_or_die(ExchangeName),
+    rabbit_exchange:route(Exchange, Msg, #{return_binding_keys => true}).
+
+remove_delivery_tags(Seq, false, Unacked, 0) ->
+    {maps:remove(Seq, Unacked), 1};
+remove_delivery_tags(Seq, true, Unacked, Count) ->
+    case maps:size(Unacked) of
+        0  -> {Unacked, Count};
+        _ ->
+            maps:fold(fun(K, _V, {Acc, Cnt}) when K =< Seq ->
+                              {maps:remove(K, Acc), Cnt + 1};
+                         (_K, _V, Acc) -> Acc
+                      end, {Unacked, 0}, Unacked)
+    end.
+
+
+confirm_to_inbound(ConfirmFun, SeqNos, Multiple, State)
+  when is_list(SeqNos) ->
+    lists:foldl(fun(Seq, State0) ->
+                        confirm_to_inbound(ConfirmFun, Seq, Multiple, State0)
+                end, State, SeqNos);
+confirm_to_inbound(ConfirmFun, Seq, Multiple,
+                   State0 = #{dest := #{unacked := Unacked} = Dst}) ->
+    #{Seq := InTag} = Unacked,
+    State = ConfirmFun(InTag, Multiple, State0),
+    {Unacked1, Removed} = remove_delivery_tags(Seq, Multiple, Unacked, 0),
+    rabbit_shovel_behaviour:decr_remaining(Removed,
+                                           State#{dest =>
+                                                      Dst#{unacked => Unacked1}}).
diff --git a/deps/rabbitmq_shovel/src/rabbit_shovel_config.erl b/deps/rabbitmq_shovel/src/rabbit_shovel_config.erl
index c03a77c94f2f..0f640bc2c7cd 100644
--- a/deps/rabbitmq_shovel/src/rabbit_shovel_config.erl
+++ b/deps/rabbitmq_shovel/src/rabbit_shovel_config.erl
@@ -13,7 +13,8 @@
 -include("rabbit_shovel.hrl").
 
 resolve_module(amqp091) -> rabbit_amqp091_shovel;
-resolve_module(amqp10) -> rabbit_amqp10_shovel.
+resolve_module(amqp10) -> rabbit_amqp10_shovel;
+resolve_module(local) -> rabbit_local_shovel.
 
 is_legacy(Config) ->
     not proplists:is_defined(source, Config).
diff --git a/deps/rabbitmq_shovel/src/rabbit_shovel_parameters.erl b/deps/rabbitmq_shovel/src/rabbit_shovel_parameters.erl
index 4dc8bfc23490..98b30edbf430 100644
--- a/deps/rabbitmq_shovel/src/rabbit_shovel_parameters.erl
+++ b/deps/rabbitmq_shovel/src/rabbit_shovel_parameters.erl
@@ -86,13 +86,15 @@ internal_owner(Def) ->
 validate_src(Def) ->
     case protocols(Def)  of
         {amqp091, _} -> validate_amqp091_src(Def);
-        {amqp10, _} -> []
+        {amqp10, _} -> [];
+        {local, _} -> validate_local_src(Def)
     end.
 
 validate_dest(Def) ->
     case protocols(Def)  of
         {_, amqp091} -> validate_amqp091_dest(Def);
-        {_, amqp10} -> []
+        {_, amqp10} -> [];
+        {_, local} -> validate_local_dest(Def)
     end.
 
 validate_amqp091_src(Def) ->
@@ -108,6 +110,19 @@ validate_amqp091_src(Def) ->
              ok
      end].
 
+validate_local_src(Def) ->
+    [case pget2(<<"src-exchange">>, <<"src-queue">>, Def) of
+         zero -> {error, "Must specify 'src-exchange' or 'src-queue'", []};
+         one  -> ok;
+         both -> {error, "Cannot specify 'src-exchange' and 'src-queue'", []}
+     end,
+     case {pget(<<"src-delete-after">>, Def, pget(<<"delete-after">>, Def)), pget(<<"ack-mode">>, Def)} of
+         {N, <<"no-ack">>} when is_integer(N) ->
+             {error, "Cannot specify 'no-ack' and numerical 'delete-after'", []};
+         _ ->
+             ok
+     end].
+
 obfuscate_uris_in_definition(Def) ->
   SrcURIs  = get_uris(<<"src-uri">>, Def),
   ObfuscatedSrcURIsDef = pset(<<"src-uri">>, obfuscate_uris(SrcURIs), Def),
@@ -125,6 +140,13 @@ validate_amqp091_dest(Def) ->
          both -> {error, "Cannot specify 'dest-exchange' and 'dest-queue'", []}
      end].
 
+validate_local_dest(Def) ->
+    [case pget2(<<"dest-exchange">>, <<"dest-queue">>, Def) of
+         zero -> ok;
+         one  -> ok;
+         both -> {error, "Cannot specify 'dest-exchange' and 'dest-queue'", []}
+     end].
+
 shovel_validation() ->
     [{<<"internal">>, fun rabbit_parameter_validation:boolean/2, optional},
      {<<"internal_owner">>, fun validate_internal_owner/2, optional},
@@ -132,17 +154,30 @@ shovel_validation() ->
      {<<"ack-mode">>, rabbit_parameter_validation:enum(
                         ['no-ack', 'on-publish', 'on-confirm']), optional},
      {<<"src-protocol">>,
-      rabbit_parameter_validation:enum(['amqp10', 'amqp091']), optional},
+      rabbit_parameter_validation:enum(['amqp10', 'amqp091', 'local']), optional},
      {<<"dest-protocol">>,
-      rabbit_parameter_validation:enum(['amqp10', 'amqp091']), optional}
+      rabbit_parameter_validation:enum(['amqp10', 'amqp091', 'local']), optional}
     ].
 
 src_validation(Def, User) ->
     case protocols(Def)  of
         {amqp091, _} -> amqp091_src_validation(Def, User);
-        {amqp10, _} -> amqp10_src_validation(Def, User)
+        {amqp10, _} -> amqp10_src_validation(Def, User);
+        {local, _} -> local_src_validation(Def, User)
     end.
 
+local_src_validation(_Def, User) ->
+    [
+     {<<"src-uri">>, validate_uri_fun(User), mandatory},
+     {<<"src-exchange">>,     fun rabbit_parameter_validation:binary/2, optional},
+     {<<"src-exchange-key">>, fun rabbit_parameter_validation:binary/2, optional},
+     {<<"src-queue">>, fun rabbit_parameter_validation:binary/2, optional},
+     {<<"src-queue-args">>,   fun validate_queue_args/2, optional},
+     {<<"src-consumer-args">>, fun validate_consumer_args/2, optional},
+     {<<"src-prefetch-count">>, fun rabbit_parameter_validation:number/2, optional},
+     {<<"src-delete-after">>, fun validate_delete_after/2, optional},
+     {<<"src-predeclared">>,  fun rabbit_parameter_validation:boolean/2, optional}
+    ].
 
 amqp10_src_validation(_Def, User) ->
     [
@@ -173,7 +208,8 @@ dest_validation(Def0, User) ->
     Def = rabbit_data_coercion:to_proplist(Def0),
     case protocols(Def)  of
         {_, amqp091} -> amqp091_dest_validation(Def, User);
-        {_, amqp10} -> amqp10_dest_validation(Def, User)
+        {_, amqp10} -> amqp10_dest_validation(Def, User);
+        {_, local} -> local_dest_validation(Def, User)
     end.
 
 amqp10_dest_validation(_Def, User) ->
@@ -209,6 +245,17 @@ amqp091_dest_validation(_Def, User) ->
      {<<"dest-predeclared">>,  fun rabbit_parameter_validation:boolean/2, optional}
     ].
 
+local_dest_validation(_Def, User) ->
+    [{<<"dest-uri">>,        validate_uri_fun(User), mandatory},
+     {<<"dest-exchange">>,   fun rabbit_parameter_validation:binary/2,optional},
+     {<<"dest-exchange-key">>,fun rabbit_parameter_validation:binary/2,optional},
+     {<<"dest-queue">>,      fun rabbit_parameter_validation:amqp091_queue_name/2,optional},
+     {<<"dest-queue-args">>, fun validate_queue_args/2, optional},
+     {<<"dest-add-forward-headers">>, fun rabbit_parameter_validation:boolean/2,optional},
+     {<<"dest-add-timestamp-header">>, fun rabbit_parameter_validation:boolean/2,optional},
+     {<<"dest-predeclared">>,  fun rabbit_parameter_validation:boolean/2, optional}
+    ].
+
 validate_uri_fun(User) ->
     fun (Name, Term) -> validate_uri(Name, Term, User) end.
 
@@ -322,7 +369,8 @@ parse({VHost, Name}, ClusterName, Def) ->
 parse_source(Def) ->
     case protocols(Def) of
         {amqp10, _} -> parse_amqp10_source(Def);
-        {amqp091, _} -> parse_amqp091_source(Def)
+        {amqp091, _} -> parse_amqp091_source(Def);
+        {local, _} -> parse_local_source(Def)
     end.
 
 parse_dest(VHostName, ClusterName, Def, SourceHeaders) ->
@@ -330,7 +378,9 @@ parse_dest(VHostName, ClusterName, Def, SourceHeaders) ->
         {_, amqp10} ->
             parse_amqp10_dest(VHostName, ClusterName, Def, SourceHeaders);
         {_, amqp091} ->
-            parse_amqp091_dest(VHostName, ClusterName, Def, SourceHeaders)
+            parse_amqp091_dest(VHostName, ClusterName, Def, SourceHeaders);
+        {_, local} ->
+            parse_local_dest(VHostName, ClusterName, Def, SourceHeaders)
     end.
 
 parse_amqp10_dest({_VHost, _Name}, _ClusterName, Def, SourceHeaders) ->
@@ -407,6 +457,35 @@ parse_amqp091_dest({VHost, Name}, ClusterName, Def, SourceHeaders) ->
                                                     AddTimestampHeader]}
                 }, Details).
 
+parse_local_dest({_VHost, _Name}, _ClusterName, Def, _SourceHeaders) ->
+    Mod       = rabbit_local_shovel,
+    DestURIs  = deobfuscated_uris(<<"dest-uri">>,      Def),
+    DestX     = pget(<<"dest-exchange">>,     Def, none),
+    DestXKey  = pget(<<"dest-exchange-key">>, Def, none),
+    DestQ     = pget(<<"dest-queue">>,        Def, none),
+    DestQArgs = pget(<<"dest-queue-args">>,   Def, #{}),
+    GlobalPredeclared = proplists:get_value(predeclared, application:get_env(?APP, topology, []), false),
+    Predeclared = pget(<<"dest-predeclared">>, Def, GlobalPredeclared),
+    DestDeclFun = case Predeclared of
+        true -> {Mod, dest_check_queue, [DestQ, DestQArgs]};
+        false -> {Mod, dest_decl_queue, [DestQ, DestQArgs]}
+    end,
+
+    AddHeaders = pget(<<"dest-add-forward-headers">>, Def, false),
+    AddTimestampHeader = pget(<<"dest-add-timestamp-header">>, Def, false),
+    %% Details are only used for status report in rabbitmqctl, as vhost is not
+    %% available to query the runtime parameters.
+    Details = maps:from_list([{K, V} || {K, V} <- [{exchange, DestX},
+                                                   {routing_key, DestXKey},
+                                                   {queue, DestQ}],
+                                        V =/= none]),
+    maps:merge(#{module => rabbit_local_shovel,
+                 uris => DestURIs,
+                 resource_decl => DestDeclFun,
+                 add_forward_headers => AddHeaders,
+                 add_timestamp_header => AddTimestampHeader
+                }, Details).
+
 fields_fun(X, Key, _SrcURI, _DestURI, P0) ->
     P1 = case X of
              none -> P0;
@@ -496,6 +575,49 @@ parse_amqp091_source(Def) ->
                   consumer_args => SrcCArgs
                  }, Details), DestHeaders}.
 
+parse_local_source(Def) ->
+    %% TODO add exchange source back
+    Mod      = rabbit_local_shovel,
+    SrcURIs  = deobfuscated_uris(<<"src-uri">>, Def),
+    SrcX     = pget(<<"src-exchange">>,Def, none),
+    SrcXKey  = pget(<<"src-exchange-key">>, Def, <<>>),
+    SrcQ     = pget(<<"src-queue">>, Def, none),
+    SrcQArgs = pget(<<"src-queue-args">>,   Def, #{}),
+    SrcCArgs = rabbit_misc:to_amqp_table(pget(<<"src-consumer-args">>, Def, [])),
+    GlobalPredeclared = proplists:get_value(predeclared, application:get_env(?APP, topology, []), false),
+    Predeclared = pget(<<"src-predeclared">>, Def, GlobalPredeclared),
+    {SrcDeclFun, Queue, DestHeaders} =
+    case SrcQ of
+        none -> {{Mod, src_decl_exchange, [SrcX, SrcXKey]}, <<>>,
+                 [{<<"src-exchange">>,     SrcX},
+                  {<<"src-exchange-key">>, SrcXKey}]};
+        _ -> case Predeclared of
+                false ->
+                    {{Mod, decl_queue, [SrcQ, SrcQArgs]},
+                        SrcQ, [{<<"src-queue">>, SrcQ}]};
+                true ->
+                    {{Mod, check_queue, [SrcQ, SrcQArgs]},
+                        SrcQ, [{<<"src-queue">>, SrcQ}]}
+            end
+    end,
+    DeleteAfter = pget(<<"src-delete-after">>, Def,
+                       pget(<<"delete-after">>, Def, <<"never">>)),
+    PrefetchCount = pget(<<"src-prefetch-count">>, Def,
+                         pget(<<"prefetch-count">>, Def, 1000)),
+    %% Details are only used for status report in rabbitmqctl, as vhost is not
+    %% available to query the runtime parameters.
+    Details = maps:from_list([{K, V} || {K, V} <- [{exchange, SrcX},
+                                                   {routing_key, SrcXKey}],
+                                        V =/= none]),
+    {maps:merge(#{module => Mod,
+                  uris => SrcURIs,
+                  resource_decl => SrcDeclFun,
+                  queue => Queue,
+                  delete_after => opt_b2a(DeleteAfter),
+                  prefetch_count => PrefetchCount,
+                  consumer_args => SrcCArgs
+                 }, Details), DestHeaders}.
+
 src_decl_exchange(SrcX, SrcXKey, _Conn, Ch) ->
     Ms = [#'queue.declare'{exclusive = true},
           #'queue.bind'{routing_key = SrcXKey,
diff --git a/deps/rabbitmq_shovel/src/rabbit_shovel_util.erl b/deps/rabbitmq_shovel/src/rabbit_shovel_util.erl
index e33ec1058804..5541041093b2 100644
--- a/deps/rabbitmq_shovel/src/rabbit_shovel_util.erl
+++ b/deps/rabbitmq_shovel/src/rabbit_shovel_util.erl
@@ -11,12 +11,15 @@
          add_timestamp_header/1,
          delete_shovel/3,
          restart_shovel/2,
-         get_shovel_parameter/1]).
+         get_shovel_parameter/1,
+         gen_unique_name/2,
+         decl_fun/2]).
 
 -include_lib("rabbit_common/include/rabbit_framing.hrl").
 -include_lib("rabbit_common/include/rabbit.hrl").
 -include_lib("kernel/include/logger.hrl").
 
+-define(APP, rabbitmq_shovel).
 -define(ROUTING_HEADER, <<"x-shovelled">>).
 -define(TIMESTAMP_HEADER, <<"x-shovelled-timestamp">>).
 
@@ -99,3 +102,58 @@ get_shovel_parameter({VHost, ShovelName}) ->
     rabbit_runtime_parameters:lookup(VHost, <<"shovel">>, ShovelName);
 get_shovel_parameter(ShovelName) ->
     rabbit_runtime_parameters:lookup(<<"/">>, <<"shovel">>, ShovelName).
+
+gen_unique_name(Pre0, Post0) ->
+    Pre = rabbit_data_coercion:to_binary(Pre0),
+    Post = rabbit_data_coercion:to_binary(Post0),
+    Id = bin_to_hex(crypto:strong_rand_bytes(8)),
+    <
>/binary, Id/binary, <<"_">>/binary, Post/binary>>.
+
+bin_to_hex(Bin) ->
+    <<<= 10 -> N -10 + $a;
+           true  -> N + $0 end>>
+      || <> <= Bin>>.
+
+decl_fun(Mod, {source, Endpoint}) ->
+    case parse_declaration({proplists:get_value(declarations, Endpoint, []), []}) of
+        [] ->
+            case proplists:get_value(predeclared, application:get_env(?APP, topology, []), false) of
+                true -> case proplists:get_value(queue, Endpoint) of
+                            <<>> -> fail({invalid_parameter_value, declarations, {require_non_empty}});
+                            Queue -> {Mod, check_fun, [Queue]}
+                        end;
+                false -> {Mod, decl_fun, []}
+            end;
+        Decl -> {Mod, decl_fun, [Decl]}
+    end;
+decl_fun(Mod, {destination, Endpoint}) ->
+    Decl = parse_declaration({proplists:get_value(declarations, Endpoint, []), []}),
+    {Mod, decl_fun, [Decl]}.
+
+parse_declaration({[], Acc}) ->
+    Acc;
+parse_declaration({[{Method, Props} | Rest], Acc}) when is_list(Props) ->
+    FieldNames = try rabbit_framing_amqp_0_9_1:method_fieldnames(Method)
+                 catch exit:Reason -> fail(Reason)
+                 end,
+    case proplists:get_keys(Props) -- FieldNames of
+        []            -> ok;
+        UnknownFields -> fail({unknown_fields, Method, UnknownFields})
+    end,
+    {Res, _Idx} = lists:foldl(
+                    fun (K, {R, Idx}) ->
+                            NewR = case proplists:get_value(K, Props) of
+                                       undefined -> R;
+                                       V         -> setelement(Idx, R, V)
+                                   end,
+                            {NewR, Idx + 1}
+                    end, {rabbit_framing_amqp_0_9_1:method_record(Method), 2},
+                    FieldNames),
+    parse_declaration({Rest, [Res | Acc]});
+parse_declaration({[{Method, Props} | _Rest], _Acc}) ->
+    fail({expected_method_field_list, Method, Props});
+parse_declaration({[Method | Rest], Acc}) ->
+    parse_declaration({[{Method, []} | Rest], Acc}).
+
+-spec fail(term()) -> no_return().
+fail(Reason) -> throw({error, Reason}).
diff --git a/deps/rabbitmq_shovel/src/rabbit_shovel_worker.erl b/deps/rabbitmq_shovel/src/rabbit_shovel_worker.erl
index 368bb60ec622..df7bda6191e3 100644
--- a/deps/rabbitmq_shovel/src/rabbit_shovel_worker.erl
+++ b/deps/rabbitmq_shovel/src/rabbit_shovel_worker.erl
@@ -52,6 +52,9 @@ init([Type, Name, Config0]) ->
                      Config0;
                 dynamic ->
                     ClusterName = rabbit_nodes:cluster_name(),
+                     %% TODO It could handle errors while parsing
+                     %% (i.e. missing predeclared queues) and stop nicely
+                     %% without long stacktraces
                     {ok, Conf} = rabbit_shovel_parameters:parse(Name,
                                                                 ClusterName,
                                                                 Config0),
@@ -103,10 +106,14 @@ handle_cast(init_shovel, State = #state{config = Config}) ->
                [human_readable_name(maps:get(name, Config2))]),
     State1 = State#state{config = Config2},
     ok = report_running(State1),
-    {noreply, State1}.
+    {noreply, State1};
+handle_cast(Msg, State) ->
+    handle_msg(Msg, State).
 
+handle_info(Msg, State) ->
+    handle_msg(Msg, State).
 
-handle_info(Msg, State = #state{config = Config, name = Name}) ->
+handle_msg(Msg, State = #state{config = Config, name = Name}) ->
     case rabbit_shovel_behaviour:handle_source(Msg, Config) of
         not_handled ->
             case rabbit_shovel_behaviour:handle_dest(Msg, Config) of
diff --git a/deps/rabbitmq_shovel/test/amqp10_SUITE.erl b/deps/rabbitmq_shovel/test/amqp10_SUITE.erl
index b5881d31be43..10712eae75d5 100644
--- a/deps/rabbitmq_shovel/test/amqp10_SUITE.erl
+++ b/deps/rabbitmq_shovel/test/amqp10_SUITE.erl
@@ -122,7 +122,6 @@ amqp10_destination(Config, AckMode) ->
 
     receive
         {amqp10_msg, Receiver, InMsg} ->
-            ct:pal("GOT ~p", [InMsg]),
             [<<42>>] = amqp10_msg:body(InMsg),
             Ts = Timestamp * 1000,
             ?assertMatch(
diff --git a/deps/rabbitmq_shovel/test/local_SUITE.erl b/deps/rabbitmq_shovel/test/local_SUITE.erl
new file mode 100644
index 000000000000..41aacca1651e
--- /dev/null
+++ b/deps/rabbitmq_shovel/test/local_SUITE.erl
@@ -0,0 +1,517 @@
+%% This Source Code Form is subject to the terms of the Mozilla Public
+%% License, v. 2.0. If a copy of the MPL was not distributed with this
+%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
+%%
+%% Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
+%%
+
+-module(local_SUITE).
+
+-include_lib("amqp_client/include/amqp_client.hrl").
+-include_lib("common_test/include/ct.hrl").
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("rabbitmq_ct_helpers/include/rabbit_assert.hrl").
+
+-compile(export_all).
+
+-define(EXCHANGE,    <<"test_exchange">>).
+-define(TO_SHOVEL,   <<"to_the_shovel">>).
+-define(FROM_SHOVEL, <<"from_the_shovel">>).
+-define(UNSHOVELLED, <<"unshovelled">>).
+-define(SHOVELLED,   <<"shovelled">>).
+-define(TIMEOUT,     1000).
+
+all() ->
+    [
+      {group, tests}
+    ].
+
+groups() ->
+    [
+      {tests, [], [
+          local_destination_no_ack,
+          local_destination_on_publish,
+          local_destination_on_confirm,
+          local_destination_forward_headers_amqp10,
+          local_destination_forward_headers_amqp091,
+          local_destination_no_forward_headers_amqp10,
+          local_destination_timestamp_header_amqp10,
+          local_destination_timestamp_header_amqp091,
+          local_destination_no_timestamp_header_amqp10,
+          local_source_no_ack,
+          local_source_on_publish,
+          local_source_on_confirm,
+          local_source_anonymous_queue
+        ]}
+    ].
+
+%% -------------------------------------------------------------------
+%% Testsuite setup/teardown.
+%% -------------------------------------------------------------------
+
+init_per_suite(Config) ->
+    {ok, _} = application:ensure_all_started(amqp10_client),
+    rabbit_ct_helpers:log_environment(),
+    Config1 = rabbit_ct_helpers:set_config(Config, [
+        {rmq_nodename_suffix, ?MODULE}
+      ]),
+    rabbit_ct_helpers:run_setup_steps(Config1,
+      rabbit_ct_broker_helpers:setup_steps() ++
+      rabbit_ct_client_helpers:setup_steps() ++
+      [fun stop_shovel_plugin/1]).
+
+end_per_suite(Config) ->
+    application:stop(amqp10_client),
+    rabbit_ct_helpers:run_teardown_steps(Config,
+      rabbit_ct_client_helpers:teardown_steps() ++
+      rabbit_ct_broker_helpers:teardown_steps()).
+
+init_per_group(_, Config) ->
+    Config.
+
+end_per_group(_, Config) ->
+    Config.
+
+init_per_testcase(Testcase, Config) ->
+    rabbit_ct_helpers:testcase_started(Config, Testcase).
+
+end_per_testcase(Testcase, Config) ->
+    rabbit_ct_broker_helpers:rpc(Config, 0, ?MODULE, delete_queues, [[<<"source-queue">>, <<"dest-queue">>]]),
+    rabbit_ct_helpers:testcase_finished(Config, Testcase).
+
+stop_shovel_plugin(Config) ->
+    ok = rabbit_ct_broker_helpers:rpc(Config, 0,
+                                      application, stop, [rabbitmq_shovel]),
+    Config.
+
+%% -------------------------------------------------------------------
+%% Testcases.
+%% -------------------------------------------------------------------
+
+%% TODO test errors when queue has not been predeclared, it just crashes
+%% with a case_clause right now
+
+local_destination_no_ack(Config) ->
+    local_destination(Config, no_ack).
+
+local_destination_on_publish(Config) ->
+    local_destination(Config, on_publish).
+
+local_destination_on_confirm(Config) ->
+    local_destination(Config, on_confirm).
+
+local_destination(Config, AckMode) ->
+    TargetQ =  <<"dest-queue">>,
+    ok = setup_local_destination_shovel(Config, TargetQ, AckMode, []),
+    {Conn, Receiver} = attach_receiver(Config, TargetQ),
+    Chan = rabbit_ct_client_helpers:open_channel(Config, 0),
+    Timestamp = erlang:system_time(millisecond),
+    Msg = #amqp_msg{payload = <<42>>,
+                    props = #'P_basic'{delivery_mode = 2,
+                                       headers = [{<<"header1">>, long, 1},
+                                                  {<<"header2">>, longstr, <<"h2">>}],
+                                       content_encoding = ?UNSHOVELLED,
+                                       content_type = ?UNSHOVELLED,
+                                       correlation_id = ?UNSHOVELLED,
+                                       %% needs to be guest here
+                                       user_id = <<"guest">>,
+                                       message_id = ?UNSHOVELLED,
+                                       reply_to = ?UNSHOVELLED,
+                                       timestamp = Timestamp,
+                                       type = ?UNSHOVELLED
+                                      }},
+    publish(Chan, Msg, ?EXCHANGE, ?TO_SHOVEL),
+
+    receive
+        {amqp10_msg, Receiver, InMsg} ->
+            [<<42>>] = amqp10_msg:body(InMsg),
+            #{content_type := ?UNSHOVELLED,
+              content_encoding := ?UNSHOVELLED,
+              correlation_id := ?UNSHOVELLED,
+              user_id := <<"guest">>,
+              message_id := ?UNSHOVELLED,
+              reply_to := ?UNSHOVELLED
+             } = amqp10_msg:properties(InMsg),
+            #{<<"header1">> := 1,
+              <<"header2">> := <<"h2">>
+             } = amqp10_msg:application_properties(InMsg),
+            #{<<"x-basic-type">> := ?UNSHOVELLED
+             } = amqp10_msg:message_annotations(InMsg),
+            #{durable := true} = amqp10_msg:headers(InMsg),
+            ok
+    after ?TIMEOUT ->
+              throw(timeout_waiting_for_deliver1)
+    end,
+
+
+    ?awaitMatch([[_, <<"1">>, <<"0">>],
+                 [<<"dest-queue">>, <<"1">>, <<"0">>]],
+                lists:sort(
+                  rabbit_ct_broker_helpers:rabbitmqctl_list(
+                    Config, 0,
+                    ["list_queues", "name", "consumers", "messages", "--no-table-headers"])),
+                30000),
+
+    [{test_shovel, static, {running, _Info}, _Metrics, _Time}] =
+        rabbit_ct_broker_helpers:rpc(Config, 0,
+          rabbit_shovel_status, status, []),
+    detach_receiver(Conn, Receiver),
+    rabbit_ct_client_helpers:close_channel(Chan).
+
+local_destination_forward_headers_amqp10(Config) ->
+    TargetQ = <<"dest-queue">>,
+    ok = setup_local_destination_shovel(Config, TargetQ, on_publish,
+                                        [{add_forward_headers, true}]),
+    {Conn, Receiver} = attach_receiver(Config, TargetQ),
+    Chan = rabbit_ct_client_helpers:open_channel(Config, 0),
+
+    Msg = #amqp_msg{props = #'P_basic'{}},
+    publish(Chan, Msg, ?EXCHANGE, ?TO_SHOVEL),
+
+    [NodeA] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+    Node = atom_to_binary(NodeA),
+
+    receive
+        {amqp10_msg, Receiver, InMsg} ->
+            ?assertMatch(#{<<"x-opt-shovelled-by">> := Node,
+                           <<"x-opt-shovel-type">> := <<"static">>,
+                           <<"x-opt-shovel-name">> := <<"test_shovel">>},
+                         amqp10_msg:message_annotations(InMsg))
+    after ?TIMEOUT ->
+              throw(timeout_waiting_for_deliver1)
+    end,
+
+    detach_receiver(Conn, Receiver),
+    rabbit_ct_client_helpers:close_channel(Chan).
+
+local_destination_forward_headers_amqp091(Config) ->
+    %% Check that we can consume with 0.9.1 or 1.0 and no properties are
+    %% lost in translation
+    TargetQ = <<"dest-queue">>,
+    ok = setup_local_destination_shovel(Config, TargetQ, on_publish,
+                                        [{add_forward_headers, true}]),
+    Chan = rabbit_ct_client_helpers:open_channel(Config, 0),
+    CTag = consume(Chan, TargetQ, true),
+
+    Msg = #amqp_msg{props = #'P_basic'{}},
+    publish(Chan, Msg, ?EXCHANGE, ?TO_SHOVEL),
+
+    [NodeA] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+    Node = atom_to_binary(NodeA),
+    ExpectedHeaders = lists:sort(
+                        [{<<"x-opt-shovelled-by">>, longstr, Node},
+                         {<<"x-opt-shovel-type">>, longstr, <<"static">>},
+                         {<<"x-opt-shovel-name">>, longstr, <<"test_shovel">>}]),
+    receive
+        {#'basic.deliver'{consumer_tag = CTag},
+         #amqp_msg{props = #'P_basic'{headers = Headers}}} ->
+            ?assertMatch(ExpectedHeaders,
+                         lists:sort(Headers))
+    after ?TIMEOUT -> throw(timeout_waiting_for_deliver1)
+    end,
+
+    rabbit_ct_client_helpers:close_channel(Chan).
+
+local_destination_no_forward_headers_amqp10(Config) ->
+    TargetQ =  <<"dest-queue">>,
+    ok = setup_local_destination_shovel(Config, TargetQ, on_publish,
+                                        [{add_forward_headers, false}]),
+    {Conn, Receiver} = attach_receiver(Config, TargetQ),
+    Chan = rabbit_ct_client_helpers:open_channel(Config, 0),
+
+    Msg = #amqp_msg{props = #'P_basic'{}},
+    publish(Chan, Msg, ?EXCHANGE, ?TO_SHOVEL),
+
+    receive
+        {amqp10_msg, Receiver, InMsg} ->
+            Anns = amqp10_msg:message_annotations(InMsg),
+            ?assertNot(maps:is_key(<<"x-opt-shovelled-by">>, Anns)),
+            ?assertNot(maps:is_key(<<"x-opt-shovel-type">>, Anns)),
+            ?assertNot(maps:is_key(<<"x-opt-shovel-name">>, Anns)),
+            ok
+    after ?TIMEOUT ->
+              throw(timeout_waiting_for_deliver1)
+    end,
+
+    detach_receiver(Conn, Receiver),
+    rabbit_ct_client_helpers:close_channel(Chan).
+
+local_destination_timestamp_header_amqp10(Config) ->
+    TargetQ = <<"dest-queue">>,
+    ok = setup_local_destination_shovel(Config, TargetQ, on_publish,
+                                        [{add_timestamp_header, true}]),
+    {Conn, Receiver} = attach_receiver(Config, TargetQ),
+    Chan = rabbit_ct_client_helpers:open_channel(Config, 0),
+
+    Msg = #amqp_msg{props = #'P_basic'{}},
+    publish(Chan, Msg, ?EXCHANGE, ?TO_SHOVEL),
+
+    receive
+        {amqp10_msg, Receiver, InMsg} ->
+            ?assertMatch(#{<<"x-opt-shovelled-timestamp">> := _},
+                         amqp10_msg:message_annotations(InMsg))
+    after ?TIMEOUT ->
+              throw(timeout_waiting_for_deliver1)
+    end,
+
+    detach_receiver(Conn, Receiver),
+    rabbit_ct_client_helpers:close_channel(Chan).
+
+local_destination_timestamp_header_amqp091(Config) ->
+    TargetQ = <<"dest-queue">>,
+    ok = setup_local_destination_shovel(Config, TargetQ, on_publish,
+                                        [{add_timestamp_header, true}]),
+    Chan = rabbit_ct_client_helpers:open_channel(Config, 0),
+    CTag = consume(Chan, TargetQ, true),
+
+    Msg = #amqp_msg{props = #'P_basic'{}},
+    publish(Chan, Msg, ?EXCHANGE, ?TO_SHOVEL),
+
+    receive
+        {#'basic.deliver'{consumer_tag = CTag},
+         #amqp_msg{props = #'P_basic'{headers = Headers}}} ->
+            ?assertMatch([{<<"x-opt-shovelled-timestamp">>, long, _}],
+                         Headers)
+    after ?TIMEOUT -> throw(timeout_waiting_for_deliver1)
+    end,
+
+    rabbit_ct_client_helpers:close_channel(Chan).
+
+local_destination_no_timestamp_header_amqp10(Config) ->
+    TargetQ =  <<"dest-queue">>,
+    ok = setup_local_destination_shovel(Config, TargetQ, on_publish,
+                                        [{add_timestamp_header, false}]),
+    {Conn, Receiver} = attach_receiver(Config, TargetQ),
+    Chan = rabbit_ct_client_helpers:open_channel(Config, 0),
+
+    Msg = #amqp_msg{props = #'P_basic'{}},
+    publish(Chan, Msg, ?EXCHANGE, ?TO_SHOVEL),
+
+    receive
+        {amqp10_msg, Receiver, InMsg} ->
+            Anns = amqp10_msg:message_annotations(InMsg),
+            ?assertNot(maps:is_key(<<"x-opt-shovelled-timestamp">>, Anns))
+    after ?TIMEOUT ->
+              throw(timeout_waiting_for_deliver1)
+    end,
+
+    detach_receiver(Conn, Receiver),
+    rabbit_ct_client_helpers:close_channel(Chan).
+
+local_source_no_ack(Config) ->
+    local_source(Config, no_ack).
+
+local_source_on_publish(Config) ->
+    local_source(Config, on_publish).
+
+local_source_on_confirm(Config) ->
+    local_source(Config, on_confirm).
+
+local_source(Config, AckMode) ->
+    SourceQ =  <<"source-queue">>,
+    DestQ =  <<"dest-queue">>,
+    ok = setup_local_source_shovel(Config, SourceQ, DestQ, AckMode),
+    Chan = rabbit_ct_client_helpers:open_channel(Config, 0),
+    CTag = consume(Chan, DestQ, AckMode =:= no_ack),
+    Msg = #amqp_msg{payload = <<42>>,
+                    props = #'P_basic'{delivery_mode = 2,
+                                       content_type = ?UNSHOVELLED}},
+    % publish to source
+    publish(Chan, Msg, <<>>, SourceQ),
+
+    receive
+        {#'basic.deliver'{consumer_tag = CTag, delivery_tag = AckTag},
+         #amqp_msg{payload = <<42>>,
+                   props = #'P_basic'{headers = [{<<"x-shovelled">>, _, _},
+                                                 {<<"x-shovelled-timestamp">>,
+                                                  long, _}]}}} ->
+            case AckMode of
+                no_ack -> ok;
+                _      -> ok = amqp_channel:call(
+                                 Chan, #'basic.ack'{delivery_tag = AckTag})
+            end
+    after ?TIMEOUT -> throw(timeout_waiting_for_deliver1)
+    end,
+
+    QueuesAndConsumers = lists:sort([[<<"source-queue">>,<<"1">>,<<"0">>],
+                                     [<<"dest-queue">>,<<"1">>,<<"0">>]]),
+    ?awaitMatch(QueuesAndConsumers,
+                lists:sort(
+                  rabbit_ct_broker_helpers:rabbitmqctl_list(
+                    Config, 0,
+                    ["list_queues", "name", "consumers", "messages", "--no-table-headers"])),
+                30000),
+
+    [{test_shovel, static, {running, _Info}, _Metrics, _Time}] =
+        rabbit_ct_broker_helpers:rpc(Config, 0,
+          rabbit_shovel_status, status, []),
+    rabbit_ct_client_helpers:close_channel(Chan).
+
+local_source_anonymous_queue(Config) ->
+    DestQ =  <<"dest-queue">>,
+    ok = setup_local_server_named_shovel(Config, DestQ, no_ack),
+    Chan = rabbit_ct_client_helpers:open_channel(Config, 0),
+    CTag = consume(Chan, DestQ, true),
+    Msg = #amqp_msg{payload = <<42>>,
+                    props = #'P_basic'{delivery_mode = 2,
+                                       content_type = ?UNSHOVELLED}},
+    % publish to source
+    publish(Chan, Msg, <<"amq.fanout">>, <<>>),
+
+    receive
+        {#'basic.deliver'{consumer_tag = CTag},
+         #amqp_msg{payload = <<42>>,
+                   props = #'P_basic'{}}} ->
+            ok
+    after ?TIMEOUT -> throw(timeout_waiting_for_deliver1)
+    end,
+
+    rabbit_ct_client_helpers:close_channel(Chan).
+
+%%
+%% Internal
+%%
+setup_local_source_shovel(Config, SourceQueue, DestQueue, AckMode) ->
+    Hostname = ?config(rmq_hostname, Config),
+    Port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp),
+    Shovel = [{test_shovel,
+               [{source,
+                 [{uris, [rabbit_misc:format("amqp://~ts:~b/%2f?heartbeat=5",
+                                             [Hostname, Port])]},
+                  {protocol, local},
+                  {queue, SourceQueue},
+                  {declarations,
+                   [{'queue.declare', [{queue, SourceQueue}, auto_delete]}]}
+                  ]
+                },
+                {destination,
+                 [{uris, [rabbit_misc:format("amqp://~ts:~b/%2f?heartbeat=5",
+                                             [Hostname, Port])]},
+                  {declarations,
+                   [{'queue.declare', [{queue, DestQueue}, auto_delete]}]},
+                  {publish_fields, [{exchange, <<>>},
+                                    {routing_key, DestQueue}]},
+                  {publish_properties, [{delivery_mode, 2},
+                                        {content_type,  ?SHOVELLED}]},
+                  {add_forward_headers, true},
+                  {add_timestamp_header, true}]},
+                {queue, <<>>},
+                {ack_mode, AckMode}
+               ]}],
+    ok = rabbit_ct_broker_helpers:rpc(Config, 0, ?MODULE, setup_shovel,
+                                      [Shovel]).
+
+setup_local_destination_shovel(Config, Queue, AckMode, Dest) ->
+    Hostname = ?config(rmq_hostname, Config),
+    Port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp),
+    Shovel = [{test_shovel,
+               [{source,
+                 [{uris, [rabbit_misc:format("amqp://~ts:~b/%2f?heartbeat=5",
+                                             [Hostname, Port])]},
+                  {declarations,
+                   [{'queue.declare', [exclusive, auto_delete]},
+                    {'exchange.declare', [{exchange, ?EXCHANGE}, auto_delete]},
+                    {'queue.bind', [{queue, <<>>}, {exchange, ?EXCHANGE},
+                                    {routing_key, ?TO_SHOVEL}]}]},
+                  {queue, <<>>}]},
+                {destination,
+                 [{protocol, local},
+                  {declarations,
+                   [{'queue.declare', [{queue, Queue}, auto_delete]}]},
+                  {uris, [rabbit_misc:format("amqp://~ts:~b",
+                                             [Hostname, Port])]},
+                  {dest_exchange, <<>>},
+                  {dest_routing_key, Queue}] ++ Dest
+                },
+                {ack_mode, AckMode}]}],
+    ok = rabbit_ct_broker_helpers:rpc(Config, 0, ?MODULE, setup_shovel,
+                                      [Shovel]).
+
+setup_local_server_named_shovel(Config, DestQueue, AckMode) ->
+    Hostname = ?config(rmq_hostname, Config),
+    Port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp),
+    Shovel = [{test_shovel,
+               [{source,
+                 [{uris, [rabbit_misc:format("amqp://~ts:~b/%2f?heartbeat=5",
+                                             [Hostname, Port])]},
+                  {protocol, local},
+                  {queue, <<>>},
+                  {declarations,
+                   ['queue.declare',
+                    {'queue.bind', [
+                                    {exchange, <<"amq.fanout">>},
+                                    {queue,    <<>>}
+                                   ]}]}
+                 ]
+                },
+                {destination,
+                 [{protocol, local},
+                  {declarations,
+                   [{'queue.declare', [{queue, DestQueue}, auto_delete]}]},
+                  {uris, [rabbit_misc:format("amqp://~ts:~b",
+                                             [Hostname, Port])]},
+                  {dest_exchange, <<>>},
+                  {dest_routing_key, DestQueue}]},
+                {ack_mode, AckMode}
+               ]}],
+    ok = rabbit_ct_broker_helpers:rpc(Config, 0, ?MODULE, setup_shovel,
+                                      [Shovel]).
+
+setup_shovel(ShovelConfig) ->
+    _ = application:stop(rabbitmq_shovel),
+    application:set_env(rabbitmq_shovel, shovels, ShovelConfig, infinity),
+    ok = application:start(rabbitmq_shovel),
+    await_running_shovel(test_shovel).
+
+await_running_shovel(Name) ->
+    case [N || {N, _, {running, _}, _, _}
+                      <- rabbit_shovel_status:status(),
+                         N =:= Name] of
+        [_] -> ok;
+        _   -> timer:sleep(100),
+               await_running_shovel(Name)
+    end.
+
+consume(Chan, Queue, NoAck) ->
+    #'basic.consume_ok'{consumer_tag = CTag} =
+        amqp_channel:subscribe(Chan, #'basic.consume'{queue = Queue,
+                                                      no_ack = NoAck,
+                                                      exclusive = false},
+                               self()),
+    receive
+        #'basic.consume_ok'{consumer_tag = CTag} -> ok
+    after ?TIMEOUT -> throw(timeout_waiting_for_consume_ok)
+    end,
+    CTag.
+
+publish(Chan, Msg, Exchange, RoutingKey) ->
+    ok = amqp_channel:call(Chan, #'basic.publish'{exchange = Exchange,
+                                                  routing_key = RoutingKey},
+                           Msg).
+
+delete_queues(Qs) when is_list(Qs) ->
+    (catch lists:foreach(fun delete_testcase_queue/1, Qs)).
+
+delete_testcase_queue(Name) ->
+    QName = rabbit_misc:r(<<"/">>, queue, Name),
+    case rabbit_amqqueue:lookup(QName) of
+        {ok, Q} ->
+            {ok, _} = rabbit_amqqueue:delete(Q, false, false, <<"dummy">>);
+        _ ->
+            ok
+    end.
+
+attach_receiver(Config, TargetQ) ->
+    Hostname = ?config(rmq_hostname, Config),
+    Port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp),
+    {ok, Conn} = amqp10_client:open_connection(Hostname, Port),
+    {ok, Sess} = amqp10_client:begin_session(Conn),
+    {ok, Receiver} = amqp10_client:attach_receiver_link(Sess,
+                                                        <<"amqp-destination-receiver">>,
+                                                        TargetQ, settled, unsettled_state),
+    ok = amqp10_client:flow_link_credit(Receiver, 5, never),
+    {Conn, Receiver}.
+
+detach_receiver(Conn, Receiver) ->
+    amqp10_client:detach_link(Receiver),
+    amqp10_client:close_connection(Conn).
diff --git a/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl b/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl
new file mode 100644
index 000000000000..5fa27f4a7870
--- /dev/null
+++ b/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl
@@ -0,0 +1,1110 @@
+%% This Source Code Form is subject to the terms of the Mozilla Public
+%% License, v. 2.0. If a copy of the MPL was not distributed with this
+%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
+%%
+%% Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
+%%
+
+-module(local_dynamic_SUITE).
+
+-include_lib("amqp_client/include/amqp_client.hrl").
+-include_lib("common_test/include/ct.hrl").
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("rabbitmq_ct_helpers/include/rabbit_assert.hrl").
+
+-compile(export_all).
+
+-define(PARAM, <<"test">>).
+
+all() ->
+    [
+      {group, tests}
+    ].
+
+groups() ->
+    [
+     {tests, [], [
+                  local_to_local_opt_headers,
+                  local_to_local_queue_dest,
+                  local_to_local_original_dest,
+                  local_to_local_exchange_dest,
+                  local_to_local_missing_exchange_dest,
+                  local_to_local_predeclared_src,
+                  local_to_local_predeclared_quorum_src,
+                  local_to_local_predeclared_stream_first_offset_src,
+                  local_to_local_predeclared_stream_last_offset_src,
+                  local_to_local_missing_predeclared_src,
+                  local_to_local_exchange_src,
+                  local_to_local_queue_args_src,
+                  local_to_local_queue_args_dest,
+                  local_to_local_predeclared_dest,
+                  local_to_local_predeclared_quorum_dest,
+                  local_to_local_missing_predeclared_dest,
+                  local_to_local_queue_status,
+                  local_to_local_exchange_status,
+                  local_to_local_queue_and_exchange_src_fails,
+                  local_to_local_queue_and_exchange_dest_fails,
+                  local_to_local_delete_after_never,
+                  local_to_local_delete_after_queue_length,
+                  local_to_local_delete_after_queue_length_zero,
+                  local_to_local_delete_after_number,
+                  local_to_local_no_ack,
+                  local_to_local_quorum_no_ack,
+                  local_to_local_stream_no_ack,
+                  local_to_local_dest_stream_no_ack,
+                  local_to_local_on_publish,
+                  local_to_local_quorum_on_publish,
+                  local_to_local_stream_on_publish,
+                  local_to_local_on_confirm,
+                  local_to_local_quorum_on_confirm,
+                  local_to_local_stream_on_confirm,
+                  local_to_local_reject_publish,
+                  local_to_amqp091,
+                  local_to_amqp10,
+                  amqp091_to_local,
+                  amqp10_to_local,
+                  local_to_local_delete_src_queue,
+                  local_to_local_delete_dest_queue,
+                  local_to_local_vhost_access,
+                  local_to_local_user_access
+                 ]}
+    ].
+
+%% -------------------------------------------------------------------
+%% Testsuite setup/teardown.
+%% -------------------------------------------------------------------
+
+init_per_suite(Config0) ->
+    {ok, _} = application:ensure_all_started(amqp10_client),
+    rabbit_ct_helpers:log_environment(),
+    Config1 = rabbit_ct_helpers:set_config(Config0, [
+        {rmq_nodename_suffix, ?MODULE}
+      ]),
+    rabbit_ct_helpers:run_setup_steps(Config1,
+      rabbit_ct_broker_helpers:setup_steps() ++
+      rabbit_ct_client_helpers:setup_steps()).
+
+end_per_suite(Config) ->
+    application:stop(amqp10_client),
+    rabbit_ct_helpers:run_teardown_steps(Config,
+      rabbit_ct_client_helpers:teardown_steps() ++
+      rabbit_ct_broker_helpers:teardown_steps()).
+
+init_per_group(_, Config) ->
+    Config.
+
+end_per_group(_, Config) ->
+    Config.
+
+init_per_testcase(Testcase, Config0) ->
+    SrcQ = list_to_binary(atom_to_list(Testcase) ++ "_src"),
+    DestQ = list_to_binary(atom_to_list(Testcase) ++ "_dest"),
+    DestQ2 = list_to_binary(atom_to_list(Testcase) ++ "_dest2"),
+    VHost = list_to_binary(atom_to_list(Testcase) ++ "_vhost"),
+    Config = [{srcq, SrcQ}, {destq, DestQ}, {destq2, DestQ2},
+              {alt_vhost, VHost} | Config0],
+    rabbit_ct_helpers:testcase_started(Config, Testcase).
+
+end_per_testcase(Testcase, Config) ->
+    shovel_test_utils:clear_param(Config, ?PARAM),
+    rabbit_ct_broker_helpers:rpc(Config, 0, ?MODULE, delete_all_queues, []),
+    _ = rabbit_ct_broker_helpers:delete_vhost(Config, ?config(alt_vhost, Config)),
+    rabbit_ct_helpers:testcase_finished(Config, Testcase).
+
+%% -------------------------------------------------------------------
+%% Testcases.
+%% -------------------------------------------------------------------
+
+local_to_local_opt_headers(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    with_session(Config,
+      fun (Sess) ->
+              shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"local">>},
+                                           {<<"src-queue">>, Src},
+                                           {<<"dest-protocol">>, <<"local">>},
+                                           {<<"dest-queue">>, Dest},
+                                           {<<"dest-add-forward-headers">>, true},
+                                           {<<"dest-add-timestamp-header">>, true}
+                                          ]),
+              Msg = publish_expect(Sess, Src, Dest, <<"tag1">>, <<"hello">>),
+              ?assertMatch(#{<<"x-opt-shovel-name">> := ?PARAM,
+                             <<"x-opt-shovel-type">> := <<"dynamic">>,
+                             <<"x-opt-shovelled-by">> := _,
+                             <<"x-opt-shovelled-timestamp">> := _},
+                           amqp10_msg:message_annotations(Msg))
+      end).
+
+local_to_local_queue_dest(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    with_session(Config,
+      fun (Sess) ->
+             shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"local">>},
+                                           {<<"src-queue">>, Src},
+                                           {<<"dest-protocol">>, <<"local">>},
+                                           {<<"dest-queue">>, Dest}
+                                          ]),
+              _ = publish_expect(Sess, Src, Dest, <<"tag1">>, <<"hello">>)
+      end).
+
+local_to_local_original_dest(Config) ->
+    %% Publish with the original routing keys, but use a different vhost
+    %% to avoid a loop (this is a single-node test).
+    Src = ?config(srcq, Config),
+    Dest = Src,
+    AltVHost = ?config(alt_vhost, Config),
+    ok = rabbit_ct_broker_helpers:add_vhost(Config, AltVHost),
+    ok = rabbit_ct_broker_helpers:set_full_permissions(Config, <<"guest">>, AltVHost),
+    declare_queue(Config, AltVHost, Dest),
+    with_session(
+      Config,
+      fun (Sess) ->
+              SrcUri = shovel_test_utils:make_uri(Config, 0, <<"%2F">>),
+              DestUri = shovel_test_utils:make_uri(Config, 0, AltVHost),
+              ok = rabbit_ct_broker_helpers:rpc(
+                     Config, 0, rabbit_runtime_parameters, set,
+                     [<<"/">>, <<"shovel">>, ?PARAM, [{<<"src-uri">>,  SrcUri},
+                                                      {<<"dest-uri">>, [DestUri]},
+                                                      {<<"src-protocol">>, <<"local">>},
+                                                      {<<"src-queue">>, Src},
+                                                      {<<"dest-protocol">>, <<"local">>}],
+                      none]),
+              shovel_test_utils:await_shovel(Config, 0, ?PARAM),
+              _ = publish(Sess, Src, Dest, <<"tag1">>, <<"hello">>)
+      end),
+    with_session(Config, AltVHost,
+                 fun (Sess) ->
+                         expect_one(Sess, Dest)
+                 end).
+
+local_to_local_exchange_dest(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    AltExchange = <<"alt-exchange">>,
+    RoutingKey = <<"funky-routing-key">>,
+    declare_exchange(Config, <<"/">>, AltExchange),
+    declare_and_bind_queue(Config, <<"/">>, AltExchange, Dest, RoutingKey),
+    with_session(Config,
+      fun (Sess) ->
+             shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"local">>},
+                                           {<<"src-queue">>, Src},
+                                           {<<"dest-protocol">>, <<"local">>},
+                                           {<<"dest-exchange">>, AltExchange},
+                                           {<<"dest-exchange-key">>, RoutingKey}
+                                          ]),
+              _ = publish_expect(Sess, Src, Dest, <<"tag1">>, <<"hello">>)
+      end).
+
+local_to_local_missing_exchange_dest(Config) ->
+    Src = ?config(srcq, Config),
+    AltExchange = <<"alt-exchange">>,
+    RoutingKey = <<"funky-routing-key">>,
+    %% If the destination exchange doesn't exist, it succeeds to start
+    %% the shovel. Just messages will not be routed
+    shovel_test_utils:set_param(Config, ?PARAM,
+                                [{<<"src-protocol">>, <<"local">>},
+                                 {<<"src-queue">>, Src},
+                                 {<<"dest-protocol">>, <<"local">>},
+                                 {<<"dest-exchange">>, AltExchange},
+                                 {<<"dest-exchange-key">>, RoutingKey}
+                                ]).
+
+local_to_local_predeclared_src(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    declare_queue(Config, <<"/">>, Src),
+    with_session(Config,
+      fun (Sess) ->
+             shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"local">>},
+                                           {<<"src-queue">>, Src},
+                                           {<<"src-predeclared">>, true},
+                                           {<<"dest-protocol">>, <<"local">>},
+                                           {<<"dest-queue">>, Dest}
+                                          ]),
+              _ = publish_expect(Sess, Src, Dest, <<"tag1">>, <<"hello">>)
+      end).
+
+local_to_local_predeclared_quorum_src(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    declare_queue(Config, <<"/">>, Src, [{<<"x-queue-type">>, longstr, <<"quorum">>}]),
+    with_session(Config,
+      fun (Sess) ->
+             shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"local">>},
+                                           {<<"src-queue">>, Src},
+                                           {<<"src-predeclared">>, true},
+                                           {<<"dest-protocol">>, <<"local">>},
+                                           {<<"dest-queue">>, Dest}
+                                          ]),
+              _ = publish_expect(Sess, Src, Dest, <<"tag1">>, <<"hello">>)
+      end).
+
+local_to_local_predeclared_stream_first_offset_src(Config) ->
+    %% TODO test this in static
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    declare_queue(Config, <<"/">>, Src, [{<<"x-queue-type">>, longstr, <<"stream">>}]),
+    with_session(Config,
+      fun (Sess) ->
+              publish_many(Sess, Src, Dest, <<"tag1">>, 20),
+              shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"local">>},
+                                           {<<"src-queue">>, Src},
+                                           {<<"src-predeclared">>, true},
+                                           {<<"src-consumer-args">>,  #{<<"x-stream-offset">> => <<"first">>}},
+                                           {<<"dest-protocol">>, <<"local">>},
+                                           {<<"dest-queue">>, Dest}
+                                          ]),
+              expect_many(Sess, Dest, 20),
+              expect_none(Sess, Dest),
+              _ = publish_expect(Sess, Src, Dest, <<"tag1">>, <<"hello">>)
+      end).
+
+local_to_local_predeclared_stream_last_offset_src(Config) ->
+    %% TODO test this in static
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    declare_queue(Config, <<"/">>, Src, [{<<"x-queue-type">>, longstr, <<"stream">>}]),
+    with_session(Config,
+      fun (Sess) ->
+              publish_many(Sess, Src, Dest, <<"tag1">>, 20),
+              shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"local">>},
+                                           {<<"src-queue">>, Src},
+                                           {<<"src-predeclared">>, true},
+                                           {<<"src-consumer-args">>,  #{<<"x-stream-offset">> => <<"last">>}},
+                                           {<<"dest-protocol">>, <<"local">>},
+                                           {<<"dest-queue">>, Dest}
+                                          ]),
+              %% Deliver last
+              expect_many(Sess, Dest, 1),
+              expect_none(Sess, Dest),
+              _ = publish_expect(Sess, Src, Dest, <<"tag1">>, <<"hello">>)
+      end).
+
+local_to_local_missing_predeclared_src(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    shovel_test_utils:set_param_nowait(Config, ?PARAM,
+                                       [{<<"src-protocol">>, <<"local">>},
+                                        {<<"src-queue">>, Src},
+                                        {<<"src-predeclared">>, true},
+                                        {<<"dest-protocol">>, <<"local">>},
+                                        {<<"dest-queue">>, Dest}
+                                       ]),
+    shovel_test_utils:await_no_shovel(Config, ?PARAM),
+    %% The shovel parameter is only deleted when 'delete-after'
+    %% is used. In any other failure, the shovel should
+    %% remain and try to restart
+    ?assertNotMatch(
+       not_found,
+       rabbit_ct_broker_helpers:rpc(
+         Config, 0, rabbit_runtime_parameters, lookup,
+         [<<"/">>, <<"shovel">>, ?PARAM])).
+
+local_to_local_exchange_src(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    with_session(Config,
+      fun (Sess) ->
+             shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"local">>},
+                                           {<<"src-exchange">>, <<"amq.direct">>},
+                                           {<<"src-exchange-key">>, Src},
+                                           {<<"dest-protocol">>, <<"local">>},
+                                           {<<"dest-queue">>, Dest}
+                                          ]),
+              Target = <<"/exchange/amq.direct/", Src/binary>>,
+              _ = publish_expect(Sess, Target, Dest, <<"tag1">>, <<"hello">>)
+      end).
+
+local_to_local_queue_args_src(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    shovel_test_utils:set_param(Config, ?PARAM,
+                                [{<<"src-protocol">>, <<"local">>},
+                                 {<<"src-queue">>, Src},
+                                 {<<"src-queue-args">>, #{<<"x-queue-type">> => <<"quorum">>}},
+                                 {<<"dest-protocol">>, <<"local">>},
+                                 {<<"dest-queue">>, Dest}
+                                ]),
+    Expected = lists:sort([[Src, <<"quorum">>], [Dest, <<"classic">>]]),
+    ?assertMatch(Expected,
+                 lists:sort(rabbit_ct_broker_helpers:rabbitmqctl_list(
+                              Config, 0,
+                              ["list_queues", "name", "type", "--no-table-headers"]))).
+
+local_to_local_queue_args_dest(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    shovel_test_utils:set_param(Config, ?PARAM,
+                                [{<<"src-protocol">>, <<"local">>},
+                                 {<<"src-queue">>, Src},
+                                 {<<"dest-protocol">>, <<"local">>},
+                                 {<<"dest-queue">>, Dest},
+                                 {<<"dest-queue-args">>, #{<<"x-queue-type">> => <<"quorum">>}}
+                                ]),
+    Expected = lists:sort([[Dest, <<"quorum">>], [Src, <<"classic">>]]),
+    ?assertMatch(Expected,
+                 lists:sort(rabbit_ct_broker_helpers:rabbitmqctl_list(
+                              Config, 0,
+                              ["list_queues", "name", "type", "--no-table-headers"]))).
+
+local_to_local_predeclared_dest(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    declare_queue(Config, <<"/">>, Dest),
+    with_session(Config,
+      fun (Sess) ->
+             shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"local">>},
+                                           {<<"src-queue">>, Src},
+                                           {<<"dest-predeclared">>, true},
+                                           {<<"dest-protocol">>, <<"local">>},
+                                           {<<"dest-queue">>, Dest}
+                                          ]),
+              _ = publish_expect(Sess, Src, Dest, <<"tag1">>, <<"hello">>)
+      end).
+
+local_to_local_predeclared_quorum_dest(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    declare_queue(Config, <<"/">>, Dest, [{<<"x-queue-type">>, longstr, <<"quorum">>}]),
+    with_session(Config,
+      fun (Sess) ->
+             shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"local">>},
+                                           {<<"src-queue">>, Src},
+                                           {<<"dest-predeclared">>, true},
+                                           {<<"dest-protocol">>, <<"local">>},
+                                           {<<"dest-queue">>, Dest}
+                                          ]),
+              _ = publish_expect(Sess, Src, Dest, <<"tag1">>, <<"hello">>)
+      end).
+
+local_to_local_missing_predeclared_dest(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    shovel_test_utils:set_param_nowait(
+      Config, ?PARAM, [{<<"src-protocol">>, <<"local">>},
+                       {<<"src-queue">>, Src},
+                       {<<"dest-predeclared">>, true},
+                       {<<"dest-protocol">>, <<"local">>},
+                       {<<"dest-queue">>, Dest}
+                      ]),
+    shovel_test_utils:await_no_shovel(Config, ?PARAM),
+    %% The shovel parameter is only deleted when 'delete-after'
+    %% is used. In any other failure, the shovel should
+    %% remain and try to restart
+    ?assertNotMatch(
+       not_found,
+       rabbit_ct_broker_helpers:rpc(
+         Config, 0, rabbit_runtime_parameters, lookup,
+         [<<"/">>, <<"shovel">>, ?PARAM])).
+
+local_to_local_queue_status(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    shovel_test_utils:set_param(Config, ?PARAM,
+                                [{<<"src-protocol">>, <<"local">>},
+                                 {<<"src-queue">>, Src},
+                                 {<<"dest-protocol">>, <<"local">>},
+                                 {<<"dest-queue">>, Dest}
+                                ]),
+    Status = rabbit_ct_broker_helpers:rpc(Config, 0,
+                                          rabbit_shovel_status, status, []),
+    ?assertMatch([{_, dynamic, {running, _}, _, _}], Status),
+    [{_, dynamic, {running, Info}, _, _}] = Status,
+    ?assertMatch(<<"local">>, proplists:get_value(src_protocol, Info)),
+    ?assertMatch(<<"local">>, proplists:get_value(dest_protocol, Info)),
+    ?assertMatch(Src, proplists:get_value(src_queue, Info)),
+    ?assertMatch(Dest, proplists:get_value(dest_queue, Info)),
+    ok.
+
+local_to_local_exchange_status(Config) ->
+    DefExchange = <<"amq.direct">>,
+    RK1 = <<"carrots">>,
+    AltExchange = <<"amq.fanout">>,
+    RK2 = <<"bunnies">>,
+    shovel_test_utils:set_param(Config, ?PARAM,
+                                [{<<"src-protocol">>, <<"local">>},
+                                 {<<"src-exchange">>, DefExchange},
+                                 {<<"src-exchange-key">>, RK1},
+                                 {<<"dest-protocol">>, <<"local">>},
+                                 {<<"dest-exchange">>, AltExchange},
+                                 {<<"dest-exchange-key">>, RK2}
+                                ]),
+    Status = rabbit_ct_broker_helpers:rpc(Config, 0,
+                                          rabbit_shovel_status, status, []),
+    ?assertMatch([{_, dynamic, {running, _}, _, _}], Status),
+    [{_, dynamic, {running, Info}, _, _}] = Status,
+    ?assertMatch(<<"local">>, proplists:get_value(src_protocol, Info)),
+    ?assertMatch(<<"local">>, proplists:get_value(dest_protocol, Info)),
+    ?assertMatch(match, re:run(proplists:get_value(src_queue, Info),
+                               "amq\.gen.*", [{capture, none}])),
+    ?assertMatch(DefExchange, proplists:get_value(src_exchange, Info)),
+    ?assertMatch(RK1, proplists:get_value(src_exchange_key, Info)),
+    ?assertMatch(AltExchange, proplists:get_value(dest_exchange, Info)),
+    ?assertMatch(RK2, proplists:get_value(dest_exchange_key, Info)),
+    ok.
+
+local_to_local_queue_and_exchange_src_fails(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    %% Setting both queue and exchange for source fails
+    try
+        shovel_test_utils:set_param(Config, ?PARAM,
+                                    [{<<"src-protocol">>, <<"local">>},
+                                     {<<"src-queue">>, Src},
+                                     {<<"src-exchange">>, <<"amq.direct">>},
+                                     {<<"src-exchange-key">>, <<"bunnies">>},
+                                     {<<"dest-protocol">>, <<"local">>},
+                                     {<<"dest-queue">>, Dest}
+                                    ]),
+        throw(unexpected_success)
+    catch
+        _:{badmatch, {error_string, Reason}} ->
+            ?assertMatch(match, re:run(Reason, "Validation failed", [{capture, none}]))
+    end.
+
+local_to_local_queue_and_exchange_dest_fails(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    %% Setting both queue and exchange for dest fails
+    try
+        shovel_test_utils:set_param(Config, ?PARAM,
+                                    [{<<"src-protocol">>, <<"local">>},
+                                     {<<"src-queue">>, Src},
+                                     {<<"dest-protocol">>, <<"local">>},
+                                     {<<"dest-queue">>, Dest},
+                                     {<<"dest-exchange">>, <<"amq.direct">>},
+                                     {<<"dest-exchange-key">>, <<"bunnies">>}
+                                    ]),
+        throw(unexpected_success)
+    catch
+        _:{badmatch, {error_string, Reason}} ->
+            ?assertMatch(match, re:run(Reason, "Validation failed", [{capture, none}]))
+    end.
+
+local_to_local_delete_after_never(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    with_session(Config,
+      fun (Sess) ->
+             shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"local">>},
+                                           {<<"src-queue">>, Src},
+                                           {<<"dest-protocol">>, <<"local">>},
+                                           {<<"dest-queue">>, Dest}
+                                          ]),
+              publish_many(Sess, Src, Dest, <<"tag1">>, 20),
+              expect_many(Sess, Dest, 20)
+      end).
+
+local_to_local_delete_after_queue_length_zero(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    declare_queue(Config, <<"/">>, Src),
+    shovel_test_utils:set_param_nowait(Config, ?PARAM,
+                                       [{<<"src-protocol">>, <<"local">>},
+                                        {<<"src-predeclared">>, true},
+                                        {<<"src-queue">>, Src},
+                                        {<<"src-delete-after">>, <<"queue-length">>},
+                                        {<<"dest-protocol">>, <<"local">>},
+                                        {<<"dest-queue">>, Dest}
+                                       ]),
+    shovel_test_utils:await_no_shovel(Config, ?PARAM),
+    %% The shovel parameter is only deleted when 'delete-after'
+    %% is used. In any other failure, the shovel should
+    %% remain and try to restart
+    ?assertMatch(not_found, rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_runtime_parameters, lookup, [<<"/">>, <<"shovel">>, ?PARAM])).
+
+local_to_local_delete_after_queue_length(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    declare_queue(Config, <<"/">>, Src),
+    with_session(Config,
+      fun (Sess) ->
+              publish_many(Sess, Src, Dest, <<"tag1">>, 18),
+              shovel_test_utils:set_param_nowait(Config, ?PARAM,
+                                                 [{<<"src-protocol">>, <<"local">>},
+                                                  {<<"src-predeclared">>, true},
+                                                  {<<"src-queue">>, Src},
+                                                  {<<"src-delete-after">>, <<"queue-length">>},
+                                                  {<<"dest-protocol">>, <<"local">>},
+                                                  {<<"dest-queue">>, Dest}
+                                                 ]),
+              shovel_test_utils:await_no_shovel(Config, ?PARAM),
+              %% The shovel parameter is only deleted when 'delete-after'
+              %% is used. In any other failure, the shovel should
+              %% remain and try to restart
+              expect_many(Sess, Dest, 18),
+              ?assertMatch(not_found, rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_runtime_parameters, lookup, [<<"/">>, <<"shovel">>, ?PARAM])),
+              publish_many(Sess, Src, Dest, <<"tag1">>, 5),
+              expect_none(Sess, Dest)
+      end).
+
+local_to_local_delete_after_number(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    with_session(Config,
+      fun (Sess) ->
+              publish_many(Sess, Src, Dest, <<"tag1">>, 5),
+              shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"local">>},
+                                           {<<"src-queue">>, Src},
+                                           {<<"src-delete-after">>, 10},
+                                           {<<"dest-protocol">>, <<"local">>},
+                                           {<<"dest-queue">>, Dest}
+                                          ]),
+              expect_many(Sess, Dest, 5),
+              publish_many(Sess, Src, Dest, <<"tag1">>, 10),
+              expect_many(Sess, Dest, 5),
+              ?assertMatch(not_found, rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_runtime_parameters, lookup, [<<"/">>, <<"shovel">>, ?PARAM])),
+              expect_none(Sess, Dest)
+      end).
+
+local_to_local_no_ack(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    with_session(Config,
+      fun (Sess) ->
+             shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"local">>},
+                                           {<<"src-queue">>, Src},
+                                           {<<"dest-protocol">>, <<"local">>},
+                                           {<<"dest-queue">>, Dest},
+                                           {<<"ack-mode">>, <<"no-ack">>}
+                                          ]),
+              _ = publish_expect(Sess, Src, Dest, <<"tag1">>, <<"hello">>)
+      end).
+
+local_to_local_quorum_no_ack(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    VHost = <<"/">>,
+    declare_queue(Config, VHost, Src, [{<<"x-queue-type">>, longstr, <<"quorum">>}]),
+    declare_queue(Config, VHost, Dest, [{<<"x-queue-type">>, longstr, <<"quorum">>}]),
+    with_session(Config,
+      fun (Sess) ->
+             shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"local">>},
+                                           {<<"src-predeclared">>, true},
+                                           {<<"src-queue">>, Src},
+                                           {<<"dest-protocol">>, <<"local">>},
+                                           {<<"dest-predeclared">>, true},
+                                           {<<"dest-queue">>, Dest},
+                                           {<<"ack-mode">>, <<"no-ack">>}
+                                          ]),
+              _ = publish_expect(Sess, Src, Dest, <<"tag1">>, <<"hello">>)
+      end).
+
+local_to_local_stream_no_ack(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    VHost = <<"/">>,
+    declare_queue(Config, VHost, Src, [{<<"x-queue-type">>, longstr, <<"stream">>}]),
+    declare_queue(Config, VHost, Dest, [{<<"x-queue-type">>, longstr, <<"stream">>}]),
+    shovel_test_utils:set_param_nowait(Config, ?PARAM,
+                                    [{<<"src-protocol">>, <<"local">>},
+                                     {<<"src-predeclared">>, true},
+                                     {<<"src-queue">>, Src},
+                                     {<<"dest-protocol">>, <<"local">>},
+                                     {<<"dest-predeclared">>, true},
+                                     {<<"dest-queue">>, Dest},
+                                     {<<"ack-mode">>, <<"no-ack">>}
+                                    ]),
+    %% Streams require consumer acknowledgments
+    shovel_test_utils:await_no_shovel(Config, ?PARAM),
+    %% The shovel parameter is only deleted when 'delete-after'
+    %% is used. In any other failure, the shovel should
+    %% remain and try to restart
+    ?awaitMatch([{_Name, dynamic, {terminated, _}, _, _}],
+                rabbit_ct_broker_helpers:rpc(Config, 0,
+                                             rabbit_shovel_status, status, []),
+                30000),
+    ?assertNotMatch(
+       not_found,
+       rabbit_ct_broker_helpers:rpc(
+         Config, 0, rabbit_runtime_parameters, lookup,
+         [VHost, <<"shovel">>, ?PARAM])).
+
+local_to_local_dest_stream_no_ack(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    declare_queue(Config, <<"/">>, Dest, [{<<"x-queue-type">>, longstr, <<"stream">>}]),
+    with_session(Config,
+      fun (Sess) ->
+              shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"local">>},
+                                           {<<"src-queue">>, Src},
+                                           {<<"dest-protocol">>, <<"local">>},
+                                           {<<"dest-predeclared">>, true},
+                                           {<<"dest-queue">>, Dest},
+                                           {<<"ack-mode">>, <<"no-ack">>}
+                                          ]),
+              Receiver = subscribe(Sess, Dest),
+              publish_many(Sess, Src, Dest, <<"tag1">>, 10),
+              ?awaitMatch([{_Name, dynamic, {running, _}, #{forwarded := 10}, _}],
+                          rabbit_ct_broker_helpers:rpc(Config, 0,
+                                                       rabbit_shovel_status, status, []),
+                          30000),
+              _ = expect(Receiver, 10, []),
+              amqp10_client:detach_link(Receiver)
+      end).
+
+local_to_local_on_confirm(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    with_session(Config,
+      fun (Sess) ->
+             shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"local">>},
+                                           {<<"src-queue">>, Src},
+                                           {<<"dest-protocol">>, <<"local">>},
+                                           {<<"dest-queue">>, Dest},
+                                           {<<"ack-mode">>, <<"on-confirm">>}
+                                          ]),
+              _ = publish_expect(Sess, Src, Dest, <<"tag1">>, <<"hello">>)
+      end).
+
+local_to_local_quorum_on_confirm(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    VHost = <<"/">>,
+    declare_queue(Config, VHost, Src, [{<<"x-queue-type">>, longstr, <<"quorum">>}]),
+    declare_queue(Config, VHost, Dest, [{<<"x-queue-type">>, longstr, <<"quorum">>}]),
+    with_session(Config,
+      fun (Sess) ->
+             shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"local">>},
+                                           {<<"src-predeclared">>, true},
+                                           {<<"src-queue">>, Src},
+                                           {<<"dest-protocol">>, <<"local">>},
+                                           {<<"dest-predeclared">>, true},
+                                           {<<"dest-queue">>, Dest},
+                                           {<<"ack-mode">>, <<"on-confirm">>}
+                                          ]),
+              _ = publish_expect(Sess, Src, Dest, <<"tag1">>, <<"hello">>)
+      end).
+
+local_to_local_stream_on_confirm(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    VHost = <<"/">>,
+    declare_queue(Config, VHost, Src, [{<<"x-queue-type">>, longstr, <<"stream">>}]),
+    declare_queue(Config, VHost, Dest, [{<<"x-queue-type">>, longstr, <<"stream">>}]),
+    with_session(Config,
+      fun (Sess) ->
+             shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"local">>},
+                                           {<<"src-predeclared">>, true},
+                                           {<<"src-queue">>, Src},
+                                           {<<"dest-protocol">>, <<"local">>},
+                                           {<<"dest-predeclared">>, true},
+                                           {<<"dest-queue">>, Dest},
+                                           {<<"ack-mode">>, <<"on-confirm">>}
+                                          ]),
+              Receiver = subscribe(Sess, Dest),
+              publish_many(Sess, Src, Dest, <<"tag1">>, 10),
+              ?awaitMatch([{_Name, dynamic, {running, _}, #{forwarded := 10}, _}],
+                          rabbit_ct_broker_helpers:rpc(Config, 0,
+                                                       rabbit_shovel_status, status, []),
+                          30000),
+              _ = expect(Receiver, 10, []),
+              amqp10_client:detach_link(Receiver)
+      end).
+
+local_to_local_on_publish(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    with_session(Config,
+      fun (Sess) ->
+             shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"local">>},
+                                           {<<"src-queue">>, Src},
+                                           {<<"dest-protocol">>, <<"local">>},
+                                           {<<"dest-queue">>, Dest},
+                                           {<<"ack-mode">>, <<"on-publish">>}
+                                          ]),
+              _ = publish_expect(Sess, Src, Dest, <<"tag1">>, <<"hello">>)
+      end).
+
+local_to_local_quorum_on_publish(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    VHost = <<"/">>,
+    declare_queue(Config, VHost, Src, [{<<"x-queue-type">>, longstr, <<"quorum">>}]),
+    declare_queue(Config, VHost, Dest, [{<<"x-queue-type">>, longstr, <<"quorum">>}]),
+    with_session(Config,
+      fun (Sess) ->
+             shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"local">>},
+                                           {<<"src-predeclared">>, true},
+                                           {<<"src-queue">>, Src},
+                                           {<<"dest-protocol">>, <<"local">>},
+                                           {<<"dest-predeclared">>, true},
+                                           {<<"dest-queue">>, Dest},
+                                           {<<"ack-mode">>, <<"on-publish">>}
+                                          ]),
+              _ = publish_expect(Sess, Src, Dest, <<"tag1">>, <<"hello">>)
+      end).
+
+local_to_local_stream_on_publish(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    VHost = <<"/">>,
+    declare_queue(Config, VHost, Src, [{<<"x-queue-type">>, longstr, <<"stream">>}]),
+    declare_queue(Config, VHost, Dest, [{<<"x-queue-type">>, longstr, <<"stream">>}]),
+    with_session(Config,
+      fun (Sess) ->
+             shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"local">>},
+                                           {<<"src-predeclared">>, true},
+                                           {<<"src-queue">>, Src},
+                                           {<<"dest-protocol">>, <<"local">>},
+                                           {<<"dest-predeclared">>, true},
+                                           {<<"dest-queue">>, Dest},
+                                           {<<"ack-mode">>, <<"on-publish">>}
+                                          ]),
+              Receiver = subscribe(Sess, Dest),
+              publish_many(Sess, Src, Dest, <<"tag1">>, 10),
+              ?awaitMatch([{_Name, dynamic, {running, _}, #{forwarded := 10}, _}],
+                          rabbit_ct_broker_helpers:rpc(Config, 0,
+                                                       rabbit_shovel_status, status, []),
+                          30000),
+              _ = expect(Receiver, 10, []),
+              amqp10_client:detach_link(Receiver)
+      end).
+
+local_to_local_reject_publish(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    declare_queue(Config, <<"/">>, Dest, [{<<"x-max-length">>, long, 1},
+                                          {<<"x-overflow">>, longstr, <<"reject-publish">>}
+                                         ]),
+    with_session(
+      Config,
+      fun (Sess) ->
+              publish_many(Sess, Src, Dest, <<"tag1">>, 5),
+              shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"local">>},
+                                           {<<"src-queue">>, Src},
+                                           {<<"dest-protocol">>, <<"local">>},
+                                           {<<"dest-predeclared">>, true},
+                                           {<<"dest-queue">>, Dest},
+                                           {<<"ack-mode">>, <<"on-confirm">>}
+                                          ]),
+              expect_many(Sess, Dest, 1),
+              expect_none(Sess, Dest)
+      end).
+
+local_to_amqp091(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    with_session(Config,
+      fun (Sess) ->
+             shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"local">>},
+                                           {<<"src-queue">>, Src},
+                                           {<<"dest-protocol">>, <<"amqp091">>},
+                                           {<<"dest-queue">>, Dest}
+                                          ]),
+              _ = publish_expect(Sess, Src, Dest, <<"tag1">>, <<"hello">>)
+      end).
+
+local_to_amqp10(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    with_session(Config,
+      fun (Sess) ->
+             shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"local">>},
+                                           {<<"src-queue">>, Src},
+                                           {<<"dest-protocol">>, <<"amqp10">>},
+                                           {<<"dest-address">>, Dest}
+                                          ]),
+              _ = publish_expect(Sess, Src, Dest, <<"tag1">>, <<"hello">>)
+      end).
+
+amqp091_to_local(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    with_session(Config,
+      fun (Sess) ->
+             shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"amqp091">>},
+                                           {<<"src-queue">>, Src},
+                                           {<<"dest-protocol">>, <<"local">>},
+                                           {<<"dest-queue">>, Dest}
+                                          ]),
+              _ = publish_expect(Sess, Src, Dest, <<"tag1">>, <<"hello">>)
+      end).
+
+amqp10_to_local(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    with_session(Config,
+      fun (Sess) ->
+             shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"amqp10">>},
+                                           {<<"src-address">>, Src},
+                                           {<<"dest-protocol">>, <<"local">>},
+                                           {<<"dest-queue">>, Dest}
+                                          ]),
+              _ = publish_expect(Sess, Src, Dest, <<"tag1">>, <<"hello">>)
+      end).
+
+local_to_local_delete_src_queue(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    with_session(Config,
+      fun (Sess) ->
+             shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"local">>},
+                                           {<<"src-queue">>, Src},
+                                           {<<"dest-protocol">>, <<"local">>},
+                                           {<<"dest-queue">>, Dest}
+                                          ]),
+              _ = publish_expect(Sess, Src, Dest, <<"tag1">>, <<"hello">>),
+              ?awaitMatch([{_Name, dynamic, {running, _}, #{forwarded := 1}, _}],
+                          rabbit_ct_broker_helpers:rpc(Config, 0,
+                                                       rabbit_shovel_status, status, []),
+                          30000),
+              rabbit_ct_broker_helpers:rpc(Config, 0, ?MODULE, delete_queue,
+                                           [Src, <<"/">>]),
+              ?awaitMatch([{_Name, dynamic, {terminated,source_queue_down}, _, _}],
+                          rabbit_ct_broker_helpers:rpc(Config, 0,
+                                                       rabbit_shovel_status, status, []),
+                          30000)
+      end).
+
+local_to_local_delete_dest_queue(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    with_session(Config,
+      fun (Sess) ->
+             shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"local">>},
+                                           {<<"src-queue">>, Src},
+                                           {<<"dest-protocol">>, <<"local">>},
+                                           {<<"dest-queue">>, Dest}
+                                          ]),
+              _ = publish_expect(Sess, Src, Dest, <<"tag1">>, <<"hello">>),
+              ?awaitMatch([{_Name, dynamic, {running, _}, #{forwarded := 1}, _}],
+                          rabbit_ct_broker_helpers:rpc(Config, 0,
+                                                       rabbit_shovel_status, status, []),
+                          30000),
+              rabbit_ct_broker_helpers:rpc(Config, 0, ?MODULE, delete_queue,
+                                           [Dest, <<"/">>]),
+              ?awaitMatch([{_Name, dynamic, {terminated, dest_queue_down}, _, _}],
+                          rabbit_ct_broker_helpers:rpc(Config, 0,
+                                                       rabbit_shovel_status, status, []),
+                          30000)
+      end).
+
+local_to_local_vhost_access(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    AltVHost = ?config(alt_vhost, Config),
+    ok = rabbit_ct_broker_helpers:add_vhost(Config, AltVHost),
+    Uri = shovel_test_utils:make_uri(Config, 0, AltVHost),
+    ok = rabbit_ct_broker_helpers:rpc(
+           Config, 0, rabbit_runtime_parameters, set,
+           [<<"/">>, <<"shovel">>, ?PARAM, [{<<"src-uri">>,  Uri},
+                                            {<<"dest-uri">>, [Uri]},
+                                            {<<"src-protocol">>, <<"local">>},
+                                            {<<"src-queue">>, Src},
+                                            {<<"dest-protocol">>, <<"local">>},
+                                            {<<"dest-queue">>, Dest}],
+            none]),
+    shovel_test_utils:await_no_shovel(Config, ?PARAM).
+
+local_to_local_user_access(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    Uri = shovel_test_utils:make_uri(
+            Config, 0, <<"guest">>, <<"forgotmypassword">>, <<"%2F">>),
+    ok = rabbit_ct_broker_helpers:rpc(
+           Config, 0, rabbit_runtime_parameters, set,
+           [<<"/">>, <<"shovel">>, ?PARAM, [{<<"src-uri">>,  Uri},
+                                            {<<"dest-uri">>, [Uri]},
+                                            {<<"src-protocol">>, <<"local">>},
+                                            {<<"src-queue">>, Src},
+                                            {<<"dest-protocol">>, <<"local">>},
+                                            {<<"dest-queue">>, Dest}],
+            none]),
+    shovel_test_utils:await_no_shovel(Config, ?PARAM).
+
+%%----------------------------------------------------------------------------
+with_session(Config, Fun) ->
+    with_session(Config, <<"/">>, Fun).
+
+with_session(Config, VHost, Fun) ->
+    Hostname = ?config(rmq_hostname, Config),
+    Port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp),
+    Cfg = #{address => Hostname,
+            port => Port,
+            sasl => {plain, <<"guest">>, <<"guest">>},
+            hostname => <<"vhost:", VHost/binary>>},
+    {ok, Conn} = amqp10_client:open_connection(Cfg),
+    {ok, Sess} = amqp10_client:begin_session(Conn),
+    Fun(Sess),
+    amqp10_client:close_connection(Conn),
+    ok.
+
+publish(Sender, Tag, Payload) when is_binary(Payload) ->
+    Headers = #{durable => true},
+    Msg = amqp10_msg:set_headers(Headers,
+                                 amqp10_msg:new(Tag, Payload, false)),
+    ok = amqp10_client:send_msg(Sender, Msg),
+    receive
+        {amqp10_disposition, {accepted, Tag}} -> ok
+    after 3000 ->
+              exit(publish_disposition_not_received)
+    end.
+
+publish(Session, Source, Dest, Tag, Payload) ->
+    LinkName = <<"dynamic-sender-", Dest/binary>>,
+    {ok, Sender} = amqp10_client:attach_sender_link(Session, LinkName, Source,
+                                                    unsettled, unsettled_state),
+    ok = await_amqp10_event(link, Sender, attached),
+    publish(Sender, Tag, Payload),
+    amqp10_client:detach_link(Sender).
+
+publish_expect(Session, Source, Dest, Tag, Payload) ->
+    publish(Session, Source, Dest, Tag, Payload),
+    expect_one(Session, Dest).
+
+publish_many(Session, Source, Dest, Tag, N) ->
+    [publish(Session, Source, Dest, Tag, integer_to_binary(Payload))
+     || Payload <- lists:seq(1, N)].
+
+await_amqp10_event(On, Ref, Evt) ->
+    receive
+        {amqp10_event, {On, Ref, Evt}} -> ok
+    after 5000 ->
+          exit({amqp10_event_timeout, On, Ref, Evt})
+    end.
+
+expect_one(Session, Dest) ->
+    LinkName = <<"dynamic-receiver-", Dest/binary>>,
+    {ok, Receiver} = amqp10_client:attach_receiver_link(Session, LinkName,
+                                                        Dest, settled,
+                                                        unsettled_state),
+    ok = amqp10_client:flow_link_credit(Receiver, 1, never),
+    Msg = expect(Receiver),
+    amqp10_client:detach_link(Receiver),
+    Msg.
+
+expect_none(Session, Dest) ->
+    LinkName = <<"dynamic-receiver-", Dest/binary>>,
+    {ok, Receiver} = amqp10_client:attach_receiver_link(Session, LinkName,
+                                                        Dest, settled,
+                                                        unsettled_state),
+    ok = amqp10_client:flow_link_credit(Receiver, 1, never),
+    receive
+        {amqp10_msg, Receiver, _} ->
+            throw(unexpected_msg)
+    after 4000 ->
+            ok
+    end,
+    amqp10_client:detach_link(Receiver).
+
+subscribe(Session, Dest) ->
+    LinkName = <<"dynamic-receiver-", Dest/binary>>,
+    {ok, Receiver} = amqp10_client:attach_receiver_link(Session, LinkName,
+                                                        Dest, settled,
+                                                        unsettled_state),
+    ok = amqp10_client:flow_link_credit(Receiver, 10, 1),
+    Receiver.
+
+expect_many(Session, Dest, N) ->
+    LinkName = <<"dynamic-receiver-", Dest/binary>>,
+    {ok, Receiver} = amqp10_client:attach_receiver_link(Session, LinkName,
+                                                        Dest, settled,
+                                                        unsettled_state),
+    ok = amqp10_client:flow_link_credit(Receiver, 10, 1),
+    Msgs = expect(Receiver, N, []),
+    amqp10_client:detach_link(Receiver),
+    Msgs.
+
+expect(_, 0, Acc) ->
+    Acc;
+expect(Receiver, N, Acc) ->
+    receive
+        {amqp10_msg, Receiver, InMsg} ->
+            expect(Receiver, N - 1, [amqp10_msg:body(InMsg) | Acc])
+    after 4000 ->
+            throw({timeout_in_expect_waiting_for_delivery, N, Acc})
+    end.
+
+expect(Receiver) ->
+    receive
+        {amqp10_msg, Receiver, InMsg} ->
+            InMsg
+    after 4000 ->
+            throw(timeout_in_expect_waiting_for_delivery)
+    end.
+
+declare_queue(Config, VHost, QName) ->
+    declare_queue(Config, VHost, QName, []).
+
+declare_queue(Config, VHost, QName, Args) ->
+    Conn = rabbit_ct_client_helpers:open_unmanaged_connection(Config, 0, VHost),
+    {ok, Ch} = amqp_connection:open_channel(Conn),
+    ?assertEqual(
+       {'queue.declare_ok', QName, 0, 0},
+       amqp_channel:call(
+         Ch, #'queue.declare'{queue = QName, durable = true, arguments = Args})),
+    rabbit_ct_client_helpers:close_channel(Ch),
+    rabbit_ct_client_helpers:close_connection(Conn).
+
+declare_and_bind_queue(Config, VHost, Exchange, QName, RoutingKey) ->
+    Conn = rabbit_ct_client_helpers:open_unmanaged_connection(Config, 0, VHost),
+    {ok, Ch} = amqp_connection:open_channel(Conn),
+    ?assertEqual(
+       {'queue.declare_ok', QName, 0, 0},
+       amqp_channel:call(
+         Ch, #'queue.declare'{queue = QName, durable = true,
+                              arguments = [{<<"x-queue-type">>, longstr, <<"classic">>}]})),
+    ?assertMatch(
+       #'queue.bind_ok'{},
+       amqp_channel:call(Ch, #'queue.bind'{
+                                queue = QName,
+                                exchange = Exchange,
+                                routing_key = RoutingKey
+                               })),
+    rabbit_ct_client_helpers:close_channel(Ch),
+    rabbit_ct_client_helpers:close_connection(Conn).
+
+declare_exchange(Config, VHost, Exchange) ->
+    Conn = rabbit_ct_client_helpers:open_unmanaged_connection(Config, 0, VHost),
+    {ok, Ch} = amqp_connection:open_channel(Conn),
+    ?assertMatch(
+       #'exchange.declare_ok'{},
+       amqp_channel:call(Ch, #'exchange.declare'{exchange = Exchange})),
+    rabbit_ct_client_helpers:close_channel(Ch),
+    rabbit_ct_client_helpers:close_connection(Conn).
+
+delete_all_queues() ->
+    Queues = rabbit_amqqueue:list(),
+    lists:foreach(
+      fun(Q) ->
+              {ok, _} = rabbit_amqqueue:delete(Q, false, false, <<"dummy">>)
+      end, Queues).
+
+delete_queue(Name, VHost) ->
+    QName = rabbit_misc:r(VHost, queue, Name),
+    case rabbit_amqqueue:lookup(QName) of
+        {ok, Q} ->
+            {ok, _} = rabbit_amqqueue:delete(Q, false, false, <<"dummy">>);
+        _ ->
+            ok
+    end.
diff --git a/deps/rabbitmq_shovel/test/shovel_test_utils.erl b/deps/rabbitmq_shovel/test/shovel_test_utils.erl
index b3593c4d9984..3bd07a79a924 100644
--- a/deps/rabbitmq_shovel/test/shovel_test_utils.erl
+++ b/deps/rabbitmq_shovel/test/shovel_test_utils.erl
@@ -13,7 +13,9 @@
          shovels_from_status/0, shovels_from_status/1,
          get_shovel_status/2, get_shovel_status/3,
          restart_shovel/2,
-         await/1, await/2, clear_param/2, clear_param/3, make_uri/2]).
+         await/1, await/2, clear_param/2, clear_param/3, make_uri/2,
+         make_uri/3, make_uri/5,
+         await_shovel1/4, await_no_shovel/2]).
 
 make_uri(Config, Node) ->
     Hostname = ?config(rmq_hostname, Config),
@@ -21,6 +23,18 @@ make_uri(Config, Node) ->
     list_to_binary(lists:flatten(io_lib:format("amqp://~ts:~b",
                                                [Hostname, Port]))).
 
+make_uri(Config, Node, VHost) ->
+    Hostname = ?config(rmq_hostname, Config),
+    Port = rabbit_ct_broker_helpers:get_node_config(Config, Node, tcp_port_amqp),
+    list_to_binary(lists:flatten(io_lib:format("amqp://~ts:~b/~ts",
+                                               [Hostname, Port, VHost]))).
+
+make_uri(Config, Node, User, Password, VHost) ->
+    Hostname = ?config(rmq_hostname, Config),
+    Port = rabbit_ct_broker_helpers:get_node_config(Config, Node, tcp_port_amqp),
+    list_to_binary(lists:flatten(io_lib:format("amqp://~ts:~ts@~ts:~b/~ts",
+                                               [User, Password, Hostname, Port, VHost]))).
+
 set_param(Config, Name, Value) ->
     set_param_nowait(Config, 0, 0, Name, Value),
     await_shovel(Config, 0, Name).
@@ -53,13 +67,26 @@ await_shovel(Config, Node, Name, ExpectedState) ->
     rabbit_ct_broker_helpers:rpc(Config, Node,
       ?MODULE, await_shovel1, [Config, Name, ExpectedState]).
 
-await_shovel1(_Config, Name, ExpectedState) ->
+await_shovel1(Config, Name, ExpectedState) ->
+    await_shovel1(Config, Name, ExpectedState, 30_000).
+
+await_shovel1(_Config, Name, ExpectedState, Timeout) ->
     Ret = await(fun() ->
                   Status = shovels_from_status(ExpectedState),
                   lists:member(Name, Status)
-          end, 30_000),
+          end, Timeout),
     Ret.
 
+await_no_shovel(Config, Name) ->
+    try
+        rabbit_ct_broker_helpers:rpc(Config, 0, ?MODULE, await_shovel1,
+                                     [Config, Name, running, 10_000]),
+        throw(unexpected_success)
+    catch
+        _:{exception, {await_timeout, false}, _} ->
+            ok
+    end.
+
 shovels_from_status() ->
     shovels_from_status(running).
 

From 6fce51ddff54ce681885afeddf2a16d1e80df072 Mon Sep 17 00:00:00 2001
From: Diana Parra Corbacho 
Date: Wed, 16 Jul 2025 17:24:15 +0200
Subject: [PATCH 05/31] WIP

---
 .../src/rabbit_local_shovel.erl               | 20 ++++++++++---------
 .../src/rabbit_shovel_worker.erl              |  1 +
 .../test/local_dynamic_SUITE.erl              |  3 +++
 3 files changed, 15 insertions(+), 9 deletions(-)

diff --git a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
index d1759379ae5b..f1210531313b 100644
--- a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
+++ b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
@@ -127,12 +127,13 @@ init_source(State = #{source := #{queue := QName0,
                                                vhost := VHost} = Current} = Src,
                       name := Name,
                       ack_mode := AckMode}) ->
-    _Mode = case rabbit_feature_flags:is_enabled('rabbitmq_4.0.0') of
-                true ->
-                    {credited, Prefetch};
-                false ->
-                    {credited, credit_api_v1}
-            end,
+    %% Should it just use v2 ?
+    Mode = case rabbit_feature_flags:is_enabled('rabbitmq_4.0.0') of
+               true ->
+                   {credited, Prefetch};
+               false ->
+                   {credited, credit_api_v1}
+           end,
     QName = rabbit_misc:r(VHost, queue, QName0),
     CTag = consumer_tag(Name),
     case rabbit_amqqueue:with(
@@ -147,7 +148,7 @@ init_source(State = #{source := #{queue := QName0,
                             channel_pid => self(),
                             limiter_pid => none,
                             limiter_active => false,
-                            mode => {simple_prefetch, Prefetch},
+                            mode => Mode, %%{simple_prefetch, Prefetch},
                             consumer_tag => CTag,
                             exclusive_consume => false,
                             args => Args,
@@ -160,7 +161,9 @@ init_source(State = #{source := #{queue := QName0,
                            {Remaining, rabbit_queue_type:consume(Q, Spec, QState0)}
                    end
            end) of
-        {Remaining, {ok, QState}} ->
+        {Remaining, {ok, QState1}} ->
+            {ok, QState, Actions} = rabbit_queue_type:credit(QName, CTag, 100, Prefetch, false, QState1),
+            %% TODO handle actions
             State#{source => Src#{current => Current#{queue_states => QState,
                                                       consumer_tag => CTag},
                                   remaining => Remaining,
@@ -518,7 +521,6 @@ check_queue(QName, _QArgs, VHost, User) ->
 
 get_user_vhost_from_amqp_param(Uri) ->
     {ok, AmqpParam} = amqp_uri:parse(Uri),
-    rabbit_log:warning("AMQP PARAM ~p", [AmqpParam]),
     {Username, Password, VHost} =
         case AmqpParam of
             #amqp_params_direct{username = U,
diff --git a/deps/rabbitmq_shovel/src/rabbit_shovel_worker.erl b/deps/rabbitmq_shovel/src/rabbit_shovel_worker.erl
index df7bda6191e3..21616144317d 100644
--- a/deps/rabbitmq_shovel/src/rabbit_shovel_worker.erl
+++ b/deps/rabbitmq_shovel/src/rabbit_shovel_worker.erl
@@ -114,6 +114,7 @@ handle_info(Msg, State) ->
     handle_msg(Msg, State).
 
 handle_msg(Msg, State = #state{config = Config, name = Name}) ->
+    rabbit_log:warning("HANDLING MESSAGE ~p", [Msg]),
     case rabbit_shovel_behaviour:handle_source(Msg, Config) of
         not_handled ->
             case rabbit_shovel_behaviour:handle_dest(Msg, Config) of
diff --git a/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl b/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl
index 5fa27f4a7870..8ca52fba33a0 100644
--- a/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl
+++ b/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl
@@ -91,6 +91,9 @@ end_per_suite(Config) ->
       rabbit_ct_broker_helpers:teardown_steps()).
 
 init_per_group(_, Config) ->
+    [Node] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+    ok = rabbit_ct_broker_helpers:enable_feature_flag(
+           Config, [Node], 'rabbitmq_4.0.0'),
     Config.
 
 end_per_group(_, Config) ->

From acbd4b465bc1f31e38a625b8353eb7544cea7678 Mon Sep 17 00:00:00 2001
From: Diana Parra Corbacho 
Date: Thu, 17 Jul 2025 07:42:17 +0200
Subject: [PATCH 06/31] Local shovel: use queue name to filter source/dest
 messages

---
 deps/rabbitmq_shovel/src/rabbit_local_shovel.erl | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
index f1210531313b..88baa10f14ca 100644
--- a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
+++ b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
@@ -264,10 +264,12 @@ handle_source(#'basic.ack'{delivery_tag = Seq, multiple = Multiple},
                                rabbit_shovel_behaviour:ack(Tag, Multi, StateX)
                        end, Seq, Multiple, State);
 
-handle_source({queue_event, _, {Type, _, _}}, _State) when Type =:= confirm;
-                                                           Type =:= reject_publish ->
-    not_handled;
-handle_source({queue_event, QRef, Evt}, #{source := Source = #{current := Current = #{queue_states := QueueStates0}}} = State0) ->
+handle_source({queue_event, #resource{name = Queue,
+                                      kind = queue,
+                                      virtual_host = VHost} = QRef, Evt},
+              #{source := Source = #{queue := Queue,
+                                     current := Current = #{queue_states := QueueStates0,
+                                                            vhost := VHost}}} = State0) ->
     case rabbit_queue_type:handle_event(QRef, Evt, QueueStates0) of
         {ok, QState1, Actions} ->
             State = State0#{source => Source#{current => Current#{queue_states => QState1}}},

From c8127ea3abef41bd810dd61c18feb62728e24826 Mon Sep 17 00:00:00 2001
From: Diana Parra Corbacho 
Date: Thu, 17 Jul 2025 08:25:47 +0200
Subject: [PATCH 07/31] Local shovel: Initialise delivery count for credit

---
 deps/rabbitmq_shovel/src/rabbit_local_shovel.erl | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
index 88baa10f14ca..88867aaf7450 100644
--- a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
+++ b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
@@ -47,6 +47,10 @@
         ]).
 
 -define(QUEUE, lqueue).
+%% "Note that, despite its name, the delivery-count is not a count but a
+%% sequence number initialized at an arbitrary point by the sender."
+%% See rabbit_amqp_session.erl
+-define(INITIAL_DELIVERY_COUNT, 16#ff_ff_ff_ff - 4).
 
 -record(pending_ack, {
                       delivery_tag,
@@ -162,7 +166,7 @@ init_source(State = #{source := #{queue := QName0,
                    end
            end) of
         {Remaining, {ok, QState1}} ->
-            {ok, QState, Actions} = rabbit_queue_type:credit(QName, CTag, 100, Prefetch, false, QState1),
+            {ok, QState, Actions} = rabbit_queue_type:credit(QName, CTag, ?INITIAL_DELIVERY_COUNT, Prefetch, false, QState1),
             %% TODO handle actions
             State#{source => Src#{current => Current#{queue_states => QState,
                                                       consumer_tag => CTag},

From 860eb6c499ea350ea6257d38947bcfd62dfe3b40 Mon Sep 17 00:00:00 2001
From: Diana Parra Corbacho 
Date: Fri, 18 Jul 2025 11:03:06 +0200
Subject: [PATCH 08/31] Local shovel: fix initial delivery count and state
 handling

---
 .../src/rabbit_local_shovel.erl               | 33 +++++++++----------
 .../src/rabbit_shovel_worker.erl              |  1 -
 .../test/local_dynamic_SUITE.erl              | 33 ++-----------------
 3 files changed, 17 insertions(+), 50 deletions(-)

diff --git a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
index 88867aaf7450..0da9e29ec7ff 100644
--- a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
+++ b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
@@ -131,10 +131,10 @@ init_source(State = #{source := #{queue := QName0,
                                                vhost := VHost} = Current} = Src,
                       name := Name,
                       ack_mode := AckMode}) ->
-    %% Should it just use v2 ?
+    %% TODO put this shovel behind the rabbitmq_4.0.0 feature flag
     Mode = case rabbit_feature_flags:is_enabled('rabbitmq_4.0.0') of
                true ->
-                   {credited, Prefetch};
+                   {credited, ?INITIAL_DELIVERY_COUNT};
                false ->
                    {credited, credit_api_v1}
            end,
@@ -152,7 +152,7 @@ init_source(State = #{source := #{queue := QName0,
                             channel_pid => self(),
                             limiter_pid => none,
                             limiter_active => false,
-                            mode => Mode, %%{simple_prefetch, Prefetch},
+                            mode => Mode,
                             consumer_tag => CTag,
                             exclusive_consume => false,
                             args => Args,
@@ -168,10 +168,11 @@ init_source(State = #{source := #{queue := QName0,
         {Remaining, {ok, QState1}} ->
             {ok, QState, Actions} = rabbit_queue_type:credit(QName, CTag, ?INITIAL_DELIVERY_COUNT, Prefetch, false, QState1),
             %% TODO handle actions
-            State#{source => Src#{current => Current#{queue_states => QState,
-                                                      consumer_tag => CTag},
-                                  remaining => Remaining,
-                                  remaining_unacked => Remaining}};
+            State2 = State#{source => Src#{current => Current#{queue_states => QState,
+                                                               consumer_tag => CTag},
+                                           remaining => Remaining,
+                                           remaining_unacked => Remaining}},
+            handle_queue_actions(Actions, State2);
         {0, {error, autodelete}} ->
             exit({shutdown, autodelete});
         {_Remaining, {error, Reason}} ->
@@ -262,12 +263,6 @@ close_source(_) ->
     %% No consumer tag, no consumer to cancel
     ok.
 
-handle_source(#'basic.ack'{delivery_tag = Seq, multiple = Multiple},
-              State = #{ack_mode := on_confirm}) ->
-    confirm_to_inbound(fun(Tag, Multi, StateX) ->
-                               rabbit_shovel_behaviour:ack(Tag, Multi, StateX)
-                       end, Seq, Multiple, State);
-
 handle_source({queue_event, #resource{name = Queue,
                                       kind = queue,
                                       virtual_host = VHost} = QRef, Evt},
@@ -413,8 +408,10 @@ handle_queue_actions(Actions, State) ->
     lists:foldl(
       fun({deliver, _CTag, AckRequired, Msgs}, S0) ->
               handle_deliver(AckRequired, Msgs, S0);
-         (_, _) ->
-              not_handled
+         (Action, S0) ->
+              %% TODO handle credit_reply
+              rabbit_log:warning("ACTION NOT HANDLED ~p", [Action]),
+              S0
          %% ({queue_down, QRef}, S0) ->
          %%      State;
          %% ({block, QName}, S0) ->
@@ -563,9 +560,9 @@ settle(Op, DeliveryTag, Multiple, #{unacked_message_q := UAMQ0,
     MsgIds = [Ack#pending_ack.msg_id || Ack <- Acked],
     case rabbit_queue_type:settle(QRef, Op, CTag, MsgIds, QState0) of
         {ok, QState1, Actions} ->
-            QState = handle_queue_actions(Actions, QState1),
-            State#{source => Src#{current => Current#{queue_states => QState}},
-                   unacked_message_q => UAMQ};
+            State#{source => Src#{current => Current#{queue_states => QState1}},
+                   unacked_message_q => UAMQ},
+            handle_queue_actions(Actions, State);
         {'protocol_error', Type, Reason, Args} ->
             rabbit_log:error("Shovel failed to settle ~p acknowledgments with ~tp: ~tp",
                              [Op, Type, io_lib:format(Reason, Args)]),
diff --git a/deps/rabbitmq_shovel/src/rabbit_shovel_worker.erl b/deps/rabbitmq_shovel/src/rabbit_shovel_worker.erl
index 21616144317d..df7bda6191e3 100644
--- a/deps/rabbitmq_shovel/src/rabbit_shovel_worker.erl
+++ b/deps/rabbitmq_shovel/src/rabbit_shovel_worker.erl
@@ -114,7 +114,6 @@ handle_info(Msg, State) ->
     handle_msg(Msg, State).
 
 handle_msg(Msg, State = #state{config = Config, name = Name}) ->
-    rabbit_log:warning("HANDLING MESSAGE ~p", [Msg]),
     case rabbit_shovel_behaviour:handle_source(Msg, Config) of
         not_handled ->
             case rabbit_shovel_behaviour:handle_dest(Msg, Config) of
diff --git a/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl b/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl
index 8ca52fba33a0..b321012090b4 100644
--- a/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl
+++ b/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl
@@ -51,7 +51,6 @@ groups() ->
                   local_to_local_no_ack,
                   local_to_local_quorum_no_ack,
                   local_to_local_stream_no_ack,
-                  local_to_local_dest_stream_no_ack,
                   local_to_local_on_publish,
                   local_to_local_quorum_on_publish,
                   local_to_local_stream_on_publish,
@@ -611,42 +610,14 @@ local_to_local_quorum_no_ack(Config) ->
 local_to_local_stream_no_ack(Config) ->
     Src = ?config(srcq, Config),
     Dest = ?config(destq, Config),
-    VHost = <<"/">>,
-    declare_queue(Config, VHost, Src, [{<<"x-queue-type">>, longstr, <<"stream">>}]),
-    declare_queue(Config, VHost, Dest, [{<<"x-queue-type">>, longstr, <<"stream">>}]),
-    shovel_test_utils:set_param_nowait(Config, ?PARAM,
-                                    [{<<"src-protocol">>, <<"local">>},
-                                     {<<"src-predeclared">>, true},
-                                     {<<"src-queue">>, Src},
-                                     {<<"dest-protocol">>, <<"local">>},
-                                     {<<"dest-predeclared">>, true},
-                                     {<<"dest-queue">>, Dest},
-                                     {<<"ack-mode">>, <<"no-ack">>}
-                                    ]),
-    %% Streams require consumer acknowledgments
-    shovel_test_utils:await_no_shovel(Config, ?PARAM),
-    %% The shovel parameter is only deleted when 'delete-after'
-    %% is used. In any other failure, the shovel should
-    %% remain and try to restart
-    ?awaitMatch([{_Name, dynamic, {terminated, _}, _, _}],
-                rabbit_ct_broker_helpers:rpc(Config, 0,
-                                             rabbit_shovel_status, status, []),
-                30000),
-    ?assertNotMatch(
-       not_found,
-       rabbit_ct_broker_helpers:rpc(
-         Config, 0, rabbit_runtime_parameters, lookup,
-         [VHost, <<"shovel">>, ?PARAM])).
-
-local_to_local_dest_stream_no_ack(Config) ->
-    Src = ?config(srcq, Config),
-    Dest = ?config(destq, Config),
+    declare_queue(Config, <<"/">>, Src, [{<<"x-queue-type">>, longstr, <<"stream">>}]),
     declare_queue(Config, <<"/">>, Dest, [{<<"x-queue-type">>, longstr, <<"stream">>}]),
     with_session(Config,
       fun (Sess) ->
               shovel_test_utils:set_param(Config, ?PARAM,
                                           [{<<"src-protocol">>, <<"local">>},
                                            {<<"src-queue">>, Src},
+                                           {<<"src-predeclared">>, true},
                                            {<<"dest-protocol">>, <<"local">>},
                                            {<<"dest-predeclared">>, true},
                                            {<<"dest-queue">>, Dest},

From a112aa521402f6a2f315201304829a2f422c0f90 Mon Sep 17 00:00:00 2001
From: Diana Parra Corbacho 
Date: Fri, 18 Jul 2025 15:09:43 +0200
Subject: [PATCH 09/31] Local shovels: set link credit

---
 deps/rabbitmq_shovel/src/rabbit_local_shovel.erl | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
index 0da9e29ec7ff..a4dc236203ec 100644
--- a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
+++ b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
@@ -51,6 +51,7 @@
 %% sequence number initialized at an arbitrary point by the sender."
 %% See rabbit_amqp_session.erl
 -define(INITIAL_DELIVERY_COUNT, 16#ff_ff_ff_ff - 4).
+-define(DEFAULT_MAX_LINK_CREDIT, 170).
 
 -record(pending_ack, {
                       delivery_tag,
@@ -125,7 +126,6 @@ connect_dest(State = #{dest := Dest = #{resource_decl := {M, F, MFArgs},
     end.
 
 init_source(State = #{source := #{queue := QName0,
-                                  prefetch_count := Prefetch,
                                   consumer_args := Args,
                                   current := #{queue_states := QState0,
                                                vhost := VHost} = Current} = Src,
@@ -138,6 +138,8 @@ init_source(State = #{source := #{queue := QName0,
                false ->
                    {credited, credit_api_v1}
            end,
+    MaxLinkCredit = application:get_env(
+                      rabbit, max_link_credit, ?DEFAULT_MAX_LINK_CREDIT),
     QName = rabbit_misc:r(VHost, queue, QName0),
     CTag = consumer_tag(Name),
     case rabbit_amqqueue:with(
@@ -166,7 +168,7 @@ init_source(State = #{source := #{queue := QName0,
                    end
            end) of
         {Remaining, {ok, QState1}} ->
-            {ok, QState, Actions} = rabbit_queue_type:credit(QName, CTag, ?INITIAL_DELIVERY_COUNT, Prefetch, false, QState1),
+            {ok, QState, Actions} = rabbit_queue_type:credit(QName, CTag, ?INITIAL_DELIVERY_COUNT, MaxLinkCredit, false, QState1),
             %% TODO handle actions
             State2 = State#{source => Src#{current => Current#{queue_states => QState,
                                                                consumer_tag => CTag},

From f381993db07a653f6f6235f1c99fa88b44f529cf Mon Sep 17 00:00:00 2001
From: Diana Parra Corbacho 
Date: Sun, 20 Jul 2025 13:41:27 +0200
Subject: [PATCH 10/31] Local shovels: renew credit

---
 .../src/rabbit_local_shovel.erl               | 43 ++++++++++++++++---
 .../test/local_dynamic_SUITE.erl              | 33 +++++++++++---
 2 files changed, 64 insertions(+), 12 deletions(-)

diff --git a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
index a4dc236203ec..f156bcf09973 100644
--- a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
+++ b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
@@ -138,8 +138,7 @@ init_source(State = #{source := #{queue := QName0,
                false ->
                    {credited, credit_api_v1}
            end,
-    MaxLinkCredit = application:get_env(
-                      rabbit, max_link_credit, ?DEFAULT_MAX_LINK_CREDIT),
+    MaxLinkCredit = max_link_credit(),
     QName = rabbit_misc:r(VHost, queue, QName0),
     CTag = consumer_tag(Name),
     case rabbit_amqqueue:with(
@@ -173,7 +172,9 @@ init_source(State = #{source := #{queue := QName0,
             State2 = State#{source => Src#{current => Current#{queue_states => QState,
                                                                consumer_tag => CTag},
                                            remaining => Remaining,
-                                           remaining_unacked => Remaining}},
+                                           remaining_unacked => Remaining,
+                                           delivery_count => ?INITIAL_DELIVERY_COUNT,
+                                           credit => MaxLinkCredit}},
             handle_queue_actions(Actions, State2);
         {0, {error, autodelete}} ->
             exit({shutdown, autodelete});
@@ -410,8 +411,10 @@ handle_queue_actions(Actions, State) ->
     lists:foldl(
       fun({deliver, _CTag, AckRequired, Msgs}, S0) ->
               handle_deliver(AckRequired, Msgs, S0);
-         (Action, S0) ->
+         ({credit_reply, _, _, _, _, _}, S0) ->
               %% TODO handle credit_reply
+              S0;
+         (Action, S0) ->
               rabbit_log:warning("ACTION NOT HANDLED ~p", [Action]),
               S0
          %% ({queue_down, QRef}, S0) ->
@@ -426,8 +429,8 @@ handle_deliver(AckRequired, Msgs, State) when is_list(Msgs) ->
     lists:foldl(fun({_QName, _QPid, MsgId, _Redelivered, Mc}, S0) ->
                         DeliveryTag = next_tag(S0),
                         S = record_pending(AckRequired, DeliveryTag, MsgId, increase_next_tag(S0)),
-                        rabbit_shovel_behaviour:forward(DeliveryTag, Mc, S)
-               end, State, Msgs).
+                        sent_pending_delivery(rabbit_shovel_behaviour:forward(DeliveryTag, Mc, S))
+                end, State, Msgs).
 
 next_tag(#{source := #{current := #{next_tag := DeliveryTag}}}) ->
     DeliveryTag.
@@ -642,3 +645,31 @@ confirm_to_inbound(ConfirmFun, Seq, Multiple,
     rabbit_shovel_behaviour:decr_remaining(Removed,
                                            State#{dest =>
                                                       Dst#{unacked => Unacked1}}).
+
+sent_pending_delivery(#{source := #{current := #{consumer_tag := CTag,
+                                                 vhost := VHost,
+                                                 queue_states := QState0
+                                                 } = Current,
+                                    delivery_count := DeliveryCount0,
+                                    credit := Credit0,
+                                    queue := QName0} = Src} = State0) ->
+    %% TODO add check for credit request in-flight
+    QName = rabbit_misc:r(VHost, queue, QName0),
+    DeliveryCount = serial_number:add(DeliveryCount0, 1),
+    Credit = max(0, Credit0 - 1),
+    {ok, QState, Actions} = case Credit =:= 0 of
+                                true ->
+                                    rabbit_queue_type:credit(
+                                      QName, CTag, DeliveryCount, max_link_credit(),
+                                      false, QState0);
+                                false ->
+                                    {ok, QState0, []}
+                            end,
+    State = State0#{source => Src#{current => Current#{queue_states => QState},
+                                   credit => Credit,
+                                   delivery_count => DeliveryCount
+                                  }},
+    handle_queue_actions(Actions, State).
+
+max_link_credit() ->
+    application:get_env(rabbit, max_link_credit, ?DEFAULT_MAX_LINK_CREDIT).
diff --git a/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl b/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl
index b321012090b4..d497159758ea 100644
--- a/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl
+++ b/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl
@@ -65,7 +65,8 @@ groups() ->
                   local_to_local_delete_src_queue,
                   local_to_local_delete_dest_queue,
                   local_to_local_vhost_access,
-                  local_to_local_user_access
+                  local_to_local_user_access,
+                  local_to_local_credit_flow
                  ]}
     ].
 
@@ -916,6 +917,21 @@ local_to_local_user_access(Config) ->
             none]),
     shovel_test_utils:await_no_shovel(Config, ?PARAM).
 
+local_to_local_credit_flow(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    with_session(Config,
+      fun (Sess) ->
+             shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"local">>},
+                                           {<<"src-queue">>, Src},
+                                           {<<"dest-protocol">>, <<"local">>},
+                                           {<<"dest-queue">>, Dest}
+                                          ]),
+              publish_many(Sess, Src, Dest, <<"tag1">>, 500),
+              expect_many(Sess, Dest, 500)
+      end).
+
 %%----------------------------------------------------------------------------
 with_session(Config, Fun) ->
     with_session(Config, <<"/">>, Fun).
@@ -944,12 +960,17 @@ publish(Sender, Tag, Payload) when is_binary(Payload) ->
               exit(publish_disposition_not_received)
     end.
 
-publish(Session, Source, Dest, Tag, Payload) ->
+publish(Session, Source, Dest, Tag, Payloads) ->
     LinkName = <<"dynamic-sender-", Dest/binary>>,
     {ok, Sender} = amqp10_client:attach_sender_link(Session, LinkName, Source,
                                                     unsettled, unsettled_state),
     ok = await_amqp10_event(link, Sender, attached),
-    publish(Sender, Tag, Payload),
+    case is_list(Payloads) of
+        true ->
+            [publish(Sender, Tag, Payload) || Payload <- Payloads];
+        false ->
+            publish(Sender, Tag, Payloads)
+    end,
     amqp10_client:detach_link(Sender).
 
 publish_expect(Session, Source, Dest, Tag, Payload) ->
@@ -957,14 +978,14 @@ publish_expect(Session, Source, Dest, Tag, Payload) ->
     expect_one(Session, Dest).
 
 publish_many(Session, Source, Dest, Tag, N) ->
-    [publish(Session, Source, Dest, Tag, integer_to_binary(Payload))
-     || Payload <- lists:seq(1, N)].
+    Payloads = [integer_to_binary(Payload) || Payload <- lists:seq(1, N)],
+    publish(Session, Source, Dest, Tag, Payloads).
 
 await_amqp10_event(On, Ref, Evt) ->
     receive
         {amqp10_event, {On, Ref, Evt}} -> ok
     after 5000 ->
-          exit({amqp10_event_timeout, On, Ref, Evt})
+            exit({amqp10_event_timeout, On, Ref, Evt})
     end.
 
 expect_one(Session, Dest) ->

From c3ec85fafb85760684f78068829c15ffc1dee681 Mon Sep 17 00:00:00 2001
From: Diana Parra Corbacho 
Date: Sun, 20 Jul 2025 17:12:00 +0200
Subject: [PATCH 11/31] Local shovel: finish credit flow handling

This implementation intentionally ignores the
drain property of AMQP 1.0 flow control because
local shovels does not use this feature, and no
external component can "enable" it, so we can
simply ignore it.

Pair: @dcorbacho.

Co-authored-by: Diana Parra Corbacho 
Co-authored-by: Michael Klishin 
---
 .../src/rabbit_local_shovel.erl               | 121 ++++++++++++++----
 1 file changed, 99 insertions(+), 22 deletions(-)

diff --git a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
index f156bcf09973..1d9334a611e6 100644
--- a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
+++ b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
@@ -58,6 +58,12 @@
                       msg_id
                      }).
 
+%% This is a significantly reduced version of its rabbit_amqp_session counterpart.
+%% Local shovels always use the maximum credit allowed.
+-record(credit_req, {
+  delivery_count :: sequence_no()
+}).
+
 parse(_Name, {source, Source}) ->
     Prefetch = parse_parameter(prefetch_count, fun parse_non_negative_integer/1,
                                proplists:get_value(prefetch_count, Source,
@@ -168,13 +174,16 @@ init_source(State = #{source := #{queue := QName0,
            end) of
         {Remaining, {ok, QState1}} ->
             {ok, QState, Actions} = rabbit_queue_type:credit(QName, CTag, ?INITIAL_DELIVERY_COUNT, MaxLinkCredit, false, QState1),
-            %% TODO handle actions
             State2 = State#{source => Src#{current => Current#{queue_states => QState,
                                                                consumer_tag => CTag},
                                            remaining => Remaining,
                                            remaining_unacked => Remaining,
                                            delivery_count => ?INITIAL_DELIVERY_COUNT,
-                                           credit => MaxLinkCredit}},
+                                           queue_delivery_count => ?INITIAL_DELIVERY_COUNT,
+                                           credit => MaxLinkCredit,
+                                           queue_credit => MaxLinkCredit,
+                                           at_least_one_credit_req_in_flight => true,
+                                           stashed_credit_req => none}},
             handle_queue_actions(Actions, State2);
         {0, {error, autodelete}} ->
             exit({shutdown, autodelete});
@@ -331,7 +340,6 @@ forward(Tag, Msg0, #{dest := #{current := #{queue_states := QState} = Current,
     Queues = rabbit_amqqueue:lookup_many(QNames),
     case rabbit_queue_type:deliver(Queues, Msg, Options, QState) of
         {ok, QState1, Actions} ->
-            %% TODO handle credit?
             State1 = State#{dest => Dest1#{current => Current1#{queue_states => QState1}}},
             #{dest := Dst1} = State2 = rabbit_shovel_behaviour:incr_forwarded(State1),
             State4 = rabbit_shovel_behaviour:decr_remaining_unacked(
@@ -411,9 +419,8 @@ handle_queue_actions(Actions, State) ->
     lists:foldl(
       fun({deliver, _CTag, AckRequired, Msgs}, S0) ->
               handle_deliver(AckRequired, Msgs, S0);
-         ({credit_reply, _, _, _, _, _}, S0) ->
-              %% TODO handle credit_reply
-              S0;
+         ({credit_reply, _, _, _, _, _} = Action, S0) ->
+              handle_credit_reply(Action, S0);
          (Action, S0) ->
               rabbit_log:warning("ACTION NOT HANDLED ~p", [Action]),
               S0
@@ -429,7 +436,7 @@ handle_deliver(AckRequired, Msgs, State) when is_list(Msgs) ->
     lists:foldl(fun({_QName, _QPid, MsgId, _Redelivered, Mc}, S0) ->
                         DeliveryTag = next_tag(S0),
                         S = record_pending(AckRequired, DeliveryTag, MsgId, increase_next_tag(S0)),
-                        sent_pending_delivery(rabbit_shovel_behaviour:forward(DeliveryTag, Mc, S))
+                        sent_delivery(rabbit_shovel_behaviour:forward(DeliveryTag, Mc, S))
                 end, State, Msgs).
 
 next_tag(#{source := #{current := #{next_tag := DeliveryTag}}}) ->
@@ -559,14 +566,14 @@ settle(Op, DeliveryTag, Multiple, #{unacked_message_q := UAMQ0,
                              source := #{queue := Queue,
                                          current := Current = #{queue_states := QState0,
                                                                 consumer_tag := CTag,
-                                                                vhost := VHost}} = Src} = State) ->
+                                                                vhost := VHost}} = Src} = State0) ->
     {Acked, UAMQ} = collect_acks(UAMQ0, DeliveryTag, Multiple),
     QRef = rabbit_misc:r(VHost, queue, Queue),
     MsgIds = [Ack#pending_ack.msg_id || Ack <- Acked],
     case rabbit_queue_type:settle(QRef, Op, CTag, MsgIds, QState0) of
         {ok, QState1, Actions} ->
-            State#{source => Src#{current => Current#{queue_states => QState1}},
-                   unacked_message_q => UAMQ},
+            State = State0#{source => Src#{current => Current#{queue_states => QState1}},
+                            unacked_message_q => UAMQ},
             handle_queue_actions(Actions, State);
         {'protocol_error', Type, Reason, Args} ->
             rabbit_log:error("Shovel failed to settle ~p acknowledgments with ~tp: ~tp",
@@ -646,30 +653,100 @@ confirm_to_inbound(ConfirmFun, Seq, Multiple,
                                            State#{dest =>
                                                       Dst#{unacked => Unacked1}}).
 
-sent_pending_delivery(#{source := #{current := #{consumer_tag := CTag,
-                                                 vhost := VHost,
-                                                 queue_states := QState0
-                                                 } = Current,
-                                    delivery_count := DeliveryCount0,
-                                    credit := Credit0,
-                                    queue := QName0} = Src} = State0) ->
-    %% TODO add check for credit request in-flight
+sent_delivery(#{source := #{current := #{consumer_tag := CTag,
+                                         vhost := VHost,
+                                         queue_states := QState0
+                                        } = Current,
+                            delivery_count := DeliveryCount0,
+                            credit := Credit0,
+                            queue_delivery_count := QDeliveryCount0,
+                            queue_credit := QCredit0,
+                            at_least_one_credit_req_in_flight := HaveCreditReqInFlight,
+                            queue := QName0} = Src,
+                dest := #{unacked := Unacked}} = State0) ->
     QName = rabbit_misc:r(VHost, queue, QName0),
     DeliveryCount = serial_number:add(DeliveryCount0, 1),
     Credit = max(0, Credit0 - 1),
-    {ok, QState, Actions} = case Credit =:= 0 of
+    QDeliveryCount = serial_number:add(QDeliveryCount0, 1),
+    QCredit = max(0, QCredit0 - 1),
+    MaxLinkCredit = max_link_credit(),
+    GrantLinkCredit = grant_link_credit(HaveCreditReqInFlight, Credit, MaxLinkCredit, maps:size(Unacked)),
+    Src1 = case HaveCreditReqInFlight andalso GrantLinkCredit of
+      true ->
+        Req = #credit_req {
+          delivery_count = DeliveryCount
+        },
+        maps:put(stashed_credit_req, Req, Src);
+      false ->
+        Src
+    end,
+    {ok, QState, Actions} = case GrantLinkCredit of
                                 true ->
                                     rabbit_queue_type:credit(
                                       QName, CTag, DeliveryCount, max_link_credit(),
                                       false, QState0);
-                                false ->
+                                _ ->
                                     {ok, QState0, []}
                             end,
-    State = State0#{source => Src#{current => Current#{queue_states => QState},
+    CreditReqInFlight = case GrantLinkCredit of
+                            true -> true;
+                            false -> HaveCreditReqInFlight
+                        end,
+    State = State0#{source => Src1#{current => Current#{queue_states => QState},
                                    credit => Credit,
-                                   delivery_count => DeliveryCount
+                                   delivery_count => DeliveryCount,
+                                   queue_credit => QCredit,
+                                   queue_delivery_count => QDeliveryCount,
+                                   at_least_one_credit_req_in_flight => CreditReqInFlight
                                   }},
     handle_queue_actions(Actions, State).
 
 max_link_credit() ->
     application:get_env(rabbit, max_link_credit, ?DEFAULT_MAX_LINK_CREDIT).
+
+grant_link_credit(true = _HaveCreditReqInFlight, _Credit, _MaxLinkCredit, _NumUnconfirmed) ->
+    false;
+grant_link_credit(false = _HaveCreditReqInFlight, Credit, MaxLinkCredit, NumUnconfirmed) ->
+    Credit =< MaxLinkCredit div 2 andalso
+    NumUnconfirmed < MaxLinkCredit.
+
+%% Drain is ignored because local shovels do not use it.
+handle_credit_reply({credit_reply, CTag, DeliveryCount, Credit, _Available, _Drain},
+                    #{source := #{credit := CCredit,
+                                  queue_delivery_count := QDeliveryCount,
+                                  stashed_credit_req := StashedCreditReq,
+                                  queue := QName0,
+                                  current := Current = #{queue_states := QState0,
+                                                         vhost := VHost}} = Src} = State0) ->
+    %% Assertion: Our (receiver) delivery-count should be always
+    %% in sync with the delivery-count of the sending queue.
+    QDeliveryCount = DeliveryCount,
+    case StashedCreditReq of
+        #credit_req{delivery_count = StashedDeliveryCount} ->
+          MaxLinkCredit = max_link_credit(),
+          QName = rabbit_misc:r(VHost, queue, QName0),
+          {ok, QState, Actions} = rabbit_queue_type:credit(QName, CTag, StashedDeliveryCount,
+            MaxLinkCredit, false, QState0),
+          State = State0#{source => Src#{queue_credit => MaxLinkCredit,
+            at_least_one_credit_req_in_flight => true,
+            stashed_credit_req => none,
+            current => Current#{queue_states => QState}}},
+          handle_queue_actions(Actions, State);
+        none when Credit =:= 0 andalso
+                  CCredit > 0 ->
+            MaxLinkCredit = max_link_credit(),
+            QName = rabbit_misc:r(VHost, queue, QName0),
+            {ok, QState, Actions} = rabbit_queue_type:credit(QName, CTag, DeliveryCount, MaxLinkCredit, false, QState0),
+            State = State0#{source => Src#{queue_credit => MaxLinkCredit,
+                                           at_least_one_credit_req_in_flight => true,
+                                           current => Current#{queue_states => QState}}},
+            handle_queue_actions(Actions, State);
+        none ->
+            %% Although we (the receiver) usually determine link credit, we set here
+            %% our link credit to what the queue says our link credit is (which is safer
+            %% in case credit requests got applied out of order in quorum queues).
+            %% This should be fine given that we asserted earlier that our delivery-count is
+            %% in sync with the delivery-count of the sending queue.
+            State0#{source => Src#{queue_credit => Credit,
+                                   at_least_one_credit_req_in_flight => false}}
+    end.

From 085c0ccc7087615809f075eb99877a90680f3ece Mon Sep 17 00:00:00 2001
From: Diana Parra Corbacho 
Date: Tue, 22 Jul 2025 09:31:46 +0200
Subject: [PATCH 12/31] Local shovels: remove rabbit_log and switch to LOG_
 macros

---
 .../src/rabbit_local_shovel.erl               | 22 +++++++++----------
 1 file changed, 11 insertions(+), 11 deletions(-)

diff --git a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
index 1d9334a611e6..2ec562899bd3 100644
--- a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
+++ b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
@@ -11,6 +11,7 @@
 
 -include_lib("amqp_client/include/amqp_client.hrl").
 -include_lib("amqp10_common/include/amqp10_types.hrl").
+-include_lib("kernel/include/logger.hrl").
 -include_lib("rabbit/include/mc.hrl").
 -include("rabbit_shovel.hrl").
 
@@ -188,12 +189,12 @@ init_source(State = #{source := #{queue := QName0,
         {0, {error, autodelete}} ->
             exit({shutdown, autodelete});
         {_Remaining, {error, Reason}} ->
-            rabbit_log:error(
-              "Shovel '~ts' in vhost '~ts' failed to consume: ~ts",
-              [Name, VHost, Reason]),
+            ?LOG_ERROR(
+               "Shovel '~ts' in vhost '~ts' failed to consume: ~ts",
+               [Name, VHost, Reason]),
             exit({shutdown, failed_to_consume_from_source});
         {unlimited, {error, not_implemented, Reason, ReasonArgs}} ->
-            rabbit_log:error(
+            ?LOG_ERROR(
               "Shovel '~ts' in vhost '~ts' failed to consume: ~ts",
               [Name, VHost, io_lib:format(Reason, ReasonArgs)]),
             exit({shutdown, failed_to_consume_from_source});
@@ -267,8 +268,8 @@ close_source(#{source := #{current := #{queue_states := QStates0,
         {error, not_found} ->
             ok;
         {error, Reason} ->
-            rabbit_log:warning("Local shovel failed to remove consumer ~tp: ~tp",
-                               [CTag, Reason]),
+            ?LOG_WARNING("Local shovel failed to remove consumer ~tp: ~tp",
+                         [CTag, Reason]),
             ok
     end;
 close_source(_) ->
@@ -421,8 +422,7 @@ handle_queue_actions(Actions, State) ->
               handle_deliver(AckRequired, Msgs, S0);
          ({credit_reply, _, _, _, _, _} = Action, S0) ->
               handle_credit_reply(Action, S0);
-         (Action, S0) ->
-              rabbit_log:warning("ACTION NOT HANDLED ~p", [Action]),
+         (_Action, S0) ->
               S0
          %% ({queue_down, QRef}, S0) ->
          %%      State;
@@ -558,7 +558,7 @@ get_user_vhost_from_amqp_param(Uri) ->
                     exit({shutdown, {access_refused, Username}})
             end;
         {refused, Username, _Msg, _Module} ->
-            rabbit_log:error("Local shovel user ~ts was refused access"),
+            ?LOG_ERROR("Local shovel user ~ts was refused access"),
             exit({shutdown, {access_refused, Username}})
     end.
 
@@ -576,8 +576,8 @@ settle(Op, DeliveryTag, Multiple, #{unacked_message_q := UAMQ0,
                             unacked_message_q => UAMQ},
             handle_queue_actions(Actions, State);
         {'protocol_error', Type, Reason, Args} ->
-            rabbit_log:error("Shovel failed to settle ~p acknowledgments with ~tp: ~tp",
-                             [Op, Type, io_lib:format(Reason, Args)]),
+            ?LOG_ERROR("Shovel failed to settle ~p acknowledgments with ~tp: ~tp",
+                       [Op, Type, io_lib:format(Reason, Args)]),
             exit({shutdown, {ack_failed, Reason}})
     end.
 

From ae6a36a7b227328ca9ca079a39d6934ab198d2ee Mon Sep 17 00:00:00 2001
From: Diana Parra Corbacho 
Date: Tue, 22 Jul 2025 09:49:59 +0200
Subject: [PATCH 13/31] Local shovels: remove unused parameter

Fixes a dialyzer failure
---
 .../src/rabbit_local_shovel.erl               | 40 ++++++-------------
 1 file changed, 13 insertions(+), 27 deletions(-)

diff --git a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
index 2ec562899bd3..d149a419dd6a 100644
--- a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
+++ b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
@@ -302,14 +302,14 @@ handle_source(_Msg, _State) ->
 
 handle_dest({queue_event, _QRef, {confirm, MsgSeqNos, _QPid}},
             #{ack_mode := on_confirm} = State) ->
-    confirm_to_inbound(fun(Tag, Multi, StateX) ->
-                               rabbit_shovel_behaviour:ack(Tag, Multi, StateX)
-                       end, MsgSeqNos, false, State);
+    confirm_to_inbound(fun(Tag, StateX) ->
+                               rabbit_shovel_behaviour:ack(Tag, false, StateX)
+                       end, MsgSeqNos, State);
 handle_dest({queue_event, _QRef, {reject_publish, Seq, _QPid}},
             #{ack_mode := on_confirm} = State) ->
-    confirm_to_inbound(fun(Tag, Multi, StateX) ->
-                               rabbit_shovel_behaviour:nack(Tag, Multi, StateX)
-                       end, Seq, false, State);
+    confirm_to_inbound(fun(Tag, StateX) ->
+                               rabbit_shovel_behaviour:nack(Tag, false, StateX)
+                       end, Seq, State);
 handle_dest({{'DOWN', #resource{name = Queue,
                                 kind = queue,
                                 virtual_host = VHost}}, _, _, _, _}  ,
@@ -626,32 +626,18 @@ route(Msg, #{current := #{vhost := VHost}}) ->
     Exchange = rabbit_exchange:lookup_or_die(ExchangeName),
     rabbit_exchange:route(Exchange, Msg, #{return_binding_keys => true}).
 
-remove_delivery_tags(Seq, false, Unacked, 0) ->
-    {maps:remove(Seq, Unacked), 1};
-remove_delivery_tags(Seq, true, Unacked, Count) ->
-    case maps:size(Unacked) of
-        0  -> {Unacked, Count};
-        _ ->
-            maps:fold(fun(K, _V, {Acc, Cnt}) when K =< Seq ->
-                              {maps:remove(K, Acc), Cnt + 1};
-                         (_K, _V, Acc) -> Acc
-                      end, {Unacked, 0}, Unacked)
-    end.
-
-
-confirm_to_inbound(ConfirmFun, SeqNos, Multiple, State)
+confirm_to_inbound(ConfirmFun, SeqNos, State)
   when is_list(SeqNos) ->
     lists:foldl(fun(Seq, State0) ->
-                        confirm_to_inbound(ConfirmFun, Seq, Multiple, State0)
+                        confirm_to_inbound(ConfirmFun, Seq, State0)
                 end, State, SeqNos);
-confirm_to_inbound(ConfirmFun, Seq, Multiple,
+confirm_to_inbound(ConfirmFun, Seq,
                    State0 = #{dest := #{unacked := Unacked} = Dst}) ->
     #{Seq := InTag} = Unacked,
-    State = ConfirmFun(InTag, Multiple, State0),
-    {Unacked1, Removed} = remove_delivery_tags(Seq, Multiple, Unacked, 0),
-    rabbit_shovel_behaviour:decr_remaining(Removed,
-                                           State#{dest =>
-                                                      Dst#{unacked => Unacked1}}).
+    State = ConfirmFun(InTag, State0),
+    Unacked1 = maps:remove(Seq, Unacked),
+    rabbit_shovel_behaviour:decr_remaining(
+      1, State#{dest => Dst#{unacked => Unacked1}}).
 
 sent_delivery(#{source := #{current := #{consumer_tag := CTag,
                                          vhost := VHost,

From 2e2dc4b2dd43c1cbe43126423ec775d8e81898ff Mon Sep 17 00:00:00 2001
From: Diana Parra Corbacho 
Date: Tue, 22 Jul 2025 10:02:46 +0200
Subject: [PATCH 14/31] Shovel tests: ignore nodename

CI uses a different hostname
---
 deps/rabbitmq_shovel/test/amqp10_SUITE.erl |  5 +----
 deps/rabbitmq_shovel/test/local_SUITE.erl  | 15 ++++-----------
 2 files changed, 5 insertions(+), 15 deletions(-)

diff --git a/deps/rabbitmq_shovel/test/amqp10_SUITE.erl b/deps/rabbitmq_shovel/test/amqp10_SUITE.erl
index 10712eae75d5..bb8a118a8f28 100644
--- a/deps/rabbitmq_shovel/test/amqp10_SUITE.erl
+++ b/deps/rabbitmq_shovel/test/amqp10_SUITE.erl
@@ -117,9 +117,6 @@ amqp10_destination(Config, AckMode) ->
                                       }},
     publish(Chan, Msg, ?EXCHANGE, ?TO_SHOVEL),
 
-    [NodeA] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
-    Node = atom_to_binary(NodeA),
-
     receive
         {amqp10_msg, Receiver, InMsg} ->
             [<<42>>] = amqp10_msg:body(InMsg),
@@ -142,7 +139,7 @@ amqp10_destination(Config, AckMode) ->
                #{<<"x-basic-type">> := ?UNSHOVELLED,
                  <<"x-opt-shovel-type">> := <<"static">>,
                  <<"x-opt-shovel-name">> := <<"test_shovel">>,
-                 <<"x-opt-shovelled-by">> := Node,
+                 <<"x-opt-shovelled-by">> := _,
                  <<"x-opt-shovelled-timestamp">> := _},
                amqp10_msg:message_annotations(InMsg)),
             ?assertMatch(#{durable := true}, amqp10_msg:headers(InMsg)),
diff --git a/deps/rabbitmq_shovel/test/local_SUITE.erl b/deps/rabbitmq_shovel/test/local_SUITE.erl
index 41aacca1651e..f12c53f83512 100644
--- a/deps/rabbitmq_shovel/test/local_SUITE.erl
+++ b/deps/rabbitmq_shovel/test/local_SUITE.erl
@@ -168,12 +168,9 @@ local_destination_forward_headers_amqp10(Config) ->
     Msg = #amqp_msg{props = #'P_basic'{}},
     publish(Chan, Msg, ?EXCHANGE, ?TO_SHOVEL),
 
-    [NodeA] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
-    Node = atom_to_binary(NodeA),
-
     receive
         {amqp10_msg, Receiver, InMsg} ->
-            ?assertMatch(#{<<"x-opt-shovelled-by">> := Node,
+            ?assertMatch(#{<<"x-opt-shovelled-by">> := _,
                            <<"x-opt-shovel-type">> := <<"static">>,
                            <<"x-opt-shovel-name">> := <<"test_shovel">>},
                          amqp10_msg:message_annotations(InMsg))
@@ -196,16 +193,12 @@ local_destination_forward_headers_amqp091(Config) ->
     Msg = #amqp_msg{props = #'P_basic'{}},
     publish(Chan, Msg, ?EXCHANGE, ?TO_SHOVEL),
 
-    [NodeA] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
-    Node = atom_to_binary(NodeA),
-    ExpectedHeaders = lists:sort(
-                        [{<<"x-opt-shovelled-by">>, longstr, Node},
-                         {<<"x-opt-shovel-type">>, longstr, <<"static">>},
-                         {<<"x-opt-shovel-name">>, longstr, <<"test_shovel">>}]),
     receive
         {#'basic.deliver'{consumer_tag = CTag},
          #amqp_msg{props = #'P_basic'{headers = Headers}}} ->
-            ?assertMatch(ExpectedHeaders,
+            ?assertMatch([{<<"x-opt-shovel-name">>, longstr, <<"test_shovel">>},
+                          {<<"x-opt-shovel-type">>, longstr, <<"static">>},
+                          {<<"x-opt-shovelled-by">>, longstr, _}],
                          lists:sort(Headers))
     after ?TIMEOUT -> throw(timeout_waiting_for_deliver1)
     end,

From b5a2dac758bfd6edc7fbe5aa27cda618485682b8 Mon Sep 17 00:00:00 2001
From: Diana Parra Corbacho 
Date: Wed, 23 Jul 2025 16:40:51 +0200
Subject: [PATCH 15/31] Local shovels: grant credit after confirming

---
 .../src/rabbit_local_shovel.erl               | 73 ++++++++++---------
 1 file changed, 39 insertions(+), 34 deletions(-)

diff --git a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
index d149a419dd6a..f6a2cfcaf818 100644
--- a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
+++ b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
@@ -558,7 +558,7 @@ get_user_vhost_from_amqp_param(Uri) ->
                     exit({shutdown, {access_refused, Username}})
             end;
         {refused, Username, _Msg, _Module} ->
-            ?LOG_ERROR("Local shovel user ~ts was refused access"),
+            ?LOG_ERROR("Local shovel user ~ts was refused access", [Username]),
             exit({shutdown, {access_refused, Username}})
     end.
 
@@ -628,9 +628,10 @@ route(Msg, #{current := #{vhost := VHost}}) ->
 
 confirm_to_inbound(ConfirmFun, SeqNos, State)
   when is_list(SeqNos) ->
-    lists:foldl(fun(Seq, State0) ->
-                        confirm_to_inbound(ConfirmFun, Seq, State0)
-                end, State, SeqNos);
+    State1 = lists:foldl(fun(Seq, State0) ->
+                                 confirm_to_inbound(ConfirmFun, Seq, State0)
+                         end, State, SeqNos),
+    maybe_grant_or_stash_credit(State1);
 confirm_to_inbound(ConfirmFun, Seq,
                    State0 = #{dest := #{unacked := Unacked} = Dst}) ->
     #{Seq := InTag} = Unacked,
@@ -639,37 +640,47 @@ confirm_to_inbound(ConfirmFun, Seq,
     rabbit_shovel_behaviour:decr_remaining(
       1, State#{dest => Dst#{unacked => Unacked1}}).
 
-sent_delivery(#{source := #{current := #{consumer_tag := CTag,
-                                         vhost := VHost,
-                                         queue_states := QState0
-                                        } = Current,
-                            delivery_count := DeliveryCount0,
+sent_delivery(#{source := #{delivery_count := DeliveryCount0,
                             credit := Credit0,
                             queue_delivery_count := QDeliveryCount0,
-                            queue_credit := QCredit0,
-                            at_least_one_credit_req_in_flight := HaveCreditReqInFlight,
-                            queue := QName0} = Src,
-                dest := #{unacked := Unacked}} = State0) ->
-    QName = rabbit_misc:r(VHost, queue, QName0),
+                            queue_credit := QCredit0} = Src
+               } = State0) ->
     DeliveryCount = serial_number:add(DeliveryCount0, 1),
     Credit = max(0, Credit0 - 1),
     QDeliveryCount = serial_number:add(QDeliveryCount0, 1),
     QCredit = max(0, QCredit0 - 1),
+    State = State0#{source => Src#{credit => Credit,
+                                   delivery_count => DeliveryCount,
+                                   queue_credit => QCredit,
+                                   queue_delivery_count => QDeliveryCount
+                                  }},
+    maybe_grant_or_stash_credit(State).
+
+maybe_grant_or_stash_credit(#{source := #{queue := QName0,
+                                    credit := Credit,
+                                    delivery_count := DeliveryCount,
+                                    at_least_one_credit_req_in_flight := HaveCreditReqInFlight,
+                                    current := #{consumer_tag := CTag,
+                                                 vhost := VHost,
+                                                 queue_states := QState0} = Current
+                                   } = Src,
+                        dest := #{unacked := Unacked}} = State0) ->
     MaxLinkCredit = max_link_credit(),
-    GrantLinkCredit = grant_link_credit(HaveCreditReqInFlight, Credit, MaxLinkCredit, maps:size(Unacked)),
+    GrantLinkCredit = grant_link_credit(Credit, MaxLinkCredit, maps:size(Unacked)),
     Src1 = case HaveCreditReqInFlight andalso GrantLinkCredit of
-      true ->
-        Req = #credit_req {
-          delivery_count = DeliveryCount
-        },
-        maps:put(stashed_credit_req, Req, Src);
-      false ->
-        Src
-    end,
-    {ok, QState, Actions} = case GrantLinkCredit of
+               true ->
+                   Req = #credit_req {
+                            delivery_count = DeliveryCount
+                           },
+                   maps:put(stashed_credit_req, Req, Src);
+               false ->
+                   Src
+           end,
+    {ok, QState, Actions} = case (GrantLinkCredit and not HaveCreditReqInFlight) of
                                 true ->
+                                    QName = rabbit_misc:r(VHost, queue, QName0),
                                     rabbit_queue_type:credit(
-                                      QName, CTag, DeliveryCount, max_link_credit(),
+                                      QName, CTag, DeliveryCount, MaxLinkCredit,
                                       false, QState0);
                                 _ ->
                                     {ok, QState0, []}
@@ -679,20 +690,14 @@ sent_delivery(#{source := #{current := #{consumer_tag := CTag,
                             false -> HaveCreditReqInFlight
                         end,
     State = State0#{source => Src1#{current => Current#{queue_states => QState},
-                                   credit => Credit,
-                                   delivery_count => DeliveryCount,
-                                   queue_credit => QCredit,
-                                   queue_delivery_count => QDeliveryCount,
-                                   at_least_one_credit_req_in_flight => CreditReqInFlight
-                                  }},
+                                    at_least_one_credit_req_in_flight => CreditReqInFlight
+                                   }},
     handle_queue_actions(Actions, State).
 
 max_link_credit() ->
     application:get_env(rabbit, max_link_credit, ?DEFAULT_MAX_LINK_CREDIT).
 
-grant_link_credit(true = _HaveCreditReqInFlight, _Credit, _MaxLinkCredit, _NumUnconfirmed) ->
-    false;
-grant_link_credit(false = _HaveCreditReqInFlight, Credit, MaxLinkCredit, NumUnconfirmed) ->
+grant_link_credit(Credit, MaxLinkCredit, NumUnconfirmed) ->
     Credit =< MaxLinkCredit div 2 andalso
     NumUnconfirmed < MaxLinkCredit.
 

From 30f67e0f7abc82b887c186c895fe5b1cedccca8c Mon Sep 17 00:00:00 2001
From: Michael Klishin 
Date: Wed, 23 Jul 2025 15:18:04 -0700
Subject: [PATCH 16/31] local_dynamic_SUITE: await credit for publishing links

This elimiantes a race condition between the destination
granting the sender link credit and the rest of what
the test does.

Note: the amqp_utils module in server core cannot be easily
moved to, say, rabbit_ct_helpers because it combines
two kinds of helpers that belong to two of our
CT helper subprojects.

So we've copied two small functions from it for
the needs of this suite.
---
 .../test/local_dynamic_SUITE.erl              | 13 ++++-----
 .../test/shovel_test_utils.erl                | 28 ++++++++++++++++++-
 2 files changed, 33 insertions(+), 8 deletions(-)

diff --git a/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl b/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl
index d497159758ea..6783847dd7e9 100644
--- a/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl
+++ b/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl
@@ -14,6 +14,8 @@
 
 -compile(export_all).
 
+-import(shovel_test_utils, [await_amqp10_event/3, await_credit/1]).
+
 -define(PARAM, <<"test">>).
 
 all() ->
@@ -106,6 +108,7 @@ init_per_testcase(Testcase, Config0) ->
     VHost = list_to_binary(atom_to_list(Testcase) ++ "_vhost"),
     Config = [{srcq, SrcQ}, {destq, DestQ}, {destq2, DestQ2},
               {alt_vhost, VHost} | Config0],
+
     rabbit_ct_helpers:testcase_started(Config, Testcase).
 
 end_per_testcase(Testcase, Config) ->
@@ -953,6 +956,8 @@ publish(Sender, Tag, Payload) when is_binary(Payload) ->
     Headers = #{durable => true},
     Msg = amqp10_msg:set_headers(Headers,
                                  amqp10_msg:new(Tag, Payload, false)),
+    %% N.B.: this function does not attach a link and does not
+    %%       need to use await_credit/1
     ok = amqp10_client:send_msg(Sender, Msg),
     receive
         {amqp10_disposition, {accepted, Tag}} -> ok
@@ -965,6 +970,7 @@ publish(Session, Source, Dest, Tag, Payloads) ->
     {ok, Sender} = amqp10_client:attach_sender_link(Session, LinkName, Source,
                                                     unsettled, unsettled_state),
     ok = await_amqp10_event(link, Sender, attached),
+    ok = await_credit(Sender),
     case is_list(Payloads) of
         true ->
             [publish(Sender, Tag, Payload) || Payload <- Payloads];
@@ -981,13 +987,6 @@ publish_many(Session, Source, Dest, Tag, N) ->
     Payloads = [integer_to_binary(Payload) || Payload <- lists:seq(1, N)],
     publish(Session, Source, Dest, Tag, Payloads).
 
-await_amqp10_event(On, Ref, Evt) ->
-    receive
-        {amqp10_event, {On, Ref, Evt}} -> ok
-    after 5000 ->
-            exit({amqp10_event_timeout, On, Ref, Evt})
-    end.
-
 expect_one(Session, Dest) ->
     LinkName = <<"dynamic-receiver-", Dest/binary>>,
     {ok, Receiver} = amqp10_client:attach_receiver_link(Session, LinkName,
diff --git a/deps/rabbitmq_shovel/test/shovel_test_utils.erl b/deps/rabbitmq_shovel/test/shovel_test_utils.erl
index 3bd07a79a924..e0e26b570725 100644
--- a/deps/rabbitmq_shovel/test/shovel_test_utils.erl
+++ b/deps/rabbitmq_shovel/test/shovel_test_utils.erl
@@ -13,7 +13,8 @@
          shovels_from_status/0, shovels_from_status/1,
          get_shovel_status/2, get_shovel_status/3,
          restart_shovel/2,
-         await/1, await/2, clear_param/2, clear_param/3, make_uri/2,
+         await/1, await/2, await_amqp10_event/3, await_credit/1,
+         clear_param/2, clear_param/3, make_uri/2,
          make_uri/3, make_uri/5,
          await_shovel1/4, await_no_shovel/2]).
 
@@ -87,6 +88,31 @@ await_no_shovel(Config, Name) ->
             ok
     end.
 
+flush(Prefix) ->
+  receive
+    Msg ->
+      ct:log("~p flushed: ~p~n", [Prefix, Msg]),
+      flush(Prefix)
+  after 1 ->
+    ok
+  end.
+
+await_credit(Sender) ->
+  receive
+    {amqp10_event, {link, Sender, credited}} ->
+      ok
+  after 5_000 ->
+      flush("await_credit timed out"),
+      ct:fail(credited_timeout)
+  end.
+
+await_amqp10_event(On, Ref, Evt) ->
+    receive
+        {amqp10_event, {On, Ref, Evt}} -> ok
+    after 5_000 ->
+        exit({amqp10_event_timeout, On, Ref, Evt})
+    end.
+
 shovels_from_status() ->
     shovels_from_status(running).
 

From a855146b996248f7df36b7a8cc6cad2da1612cd8 Mon Sep 17 00:00:00 2001
From: Diana Parra Corbacho 
Date: Thu, 24 Jul 2025 11:11:32 +0200
Subject: [PATCH 17/31] Local shovel: handle destination queue events

---
 .../src/rabbit_local_shovel.erl               | 52 ++++++++++++-------
 .../test/local_dynamic_SUITE.erl              |  7 ++-
 2 files changed, 37 insertions(+), 22 deletions(-)

diff --git a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
index f6a2cfcaf818..c47f6b5996a4 100644
--- a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
+++ b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
@@ -300,16 +300,19 @@ handle_source({{'DOWN', #resource{name = Queue,
 handle_source(_Msg, _State) ->
     not_handled.
 
-handle_dest({queue_event, _QRef, {confirm, MsgSeqNos, _QPid}},
-            #{ack_mode := on_confirm} = State) ->
-    confirm_to_inbound(fun(Tag, StateX) ->
-                               rabbit_shovel_behaviour:ack(Tag, false, StateX)
-                       end, MsgSeqNos, State);
-handle_dest({queue_event, _QRef, {reject_publish, Seq, _QPid}},
-            #{ack_mode := on_confirm} = State) ->
-    confirm_to_inbound(fun(Tag, StateX) ->
-                               rabbit_shovel_behaviour:nack(Tag, false, StateX)
-                       end, Seq, State);
+handle_dest({queue_event, QRef, Evt},
+            #{ack_mode := on_confirm,
+              dest := Dest = #{current := Current = #{queue_states := QueueStates0}}} = State0) ->
+    case rabbit_queue_type:handle_event(QRef, Evt, QueueStates0) of
+        {ok, QState1, Actions} ->
+            State = State0#{dest => Dest#{current => Current#{queue_states => QState1}}},
+            handle_dest_queue_actions(Actions, State);
+        {eol, Actions} ->
+            _ = handle_dest_queue_actions(Actions, State0),
+            {stop, {outbound_link_or_channel_closure, queue_deleted}};
+        {protocol_error, _Type, Reason, ReasonArgs} ->
+            {stop, list_to_binary(io_lib:format(Reason, ReasonArgs))}
+    end;
 handle_dest({{'DOWN', #resource{name = Queue,
                                 kind = queue,
                                 virtual_host = VHost}}, _, _, _, _}  ,
@@ -424,12 +427,6 @@ handle_queue_actions(Actions, State) ->
               handle_credit_reply(Action, S0);
          (_Action, S0) ->
               S0
-         %% ({queue_down, QRef}, S0) ->
-         %%      State;
-         %% ({block, QName}, S0) ->
-         %%      State;
-         %% ({unblock, QName}, S0) ->
-         %%      State
       end, State, Actions).
 
 handle_deliver(AckRequired, Msgs, State) when is_list(Msgs) ->
@@ -445,6 +442,22 @@ next_tag(#{source := #{current := #{next_tag := DeliveryTag}}}) ->
 increase_next_tag(#{source := Source = #{current := Current = #{next_tag := DeliveryTag}}} = State) ->
     State#{source => Source#{current => Current#{next_tag => DeliveryTag + 1}}}.
 
+handle_dest_queue_actions(Actions, State) ->
+    lists:foldl(
+      fun({settled, _QName, MsgSeqNos}, S0) ->
+              maybe_grant_or_stash_credit(
+                confirm_to_inbound(fun(Tag, StateX) ->
+                                           rabbit_shovel_behaviour:ack(Tag, false, StateX)
+                                   end, MsgSeqNos, S0));
+         ({rejected, _QName, MsgSeqNos}, S0) ->
+              maybe_grant_or_stash_credit(
+                confirm_to_inbound(fun(Tag, StateX) ->
+                                           rabbit_shovel_behaviour:nack(Tag, false, StateX)
+                                   end, MsgSeqNos, S0));
+         (_Action, S0) ->
+              S0
+      end, State, Actions).
+
 record_pending(false, _DeliveryTag, _MsgId, State) ->
     State;
 record_pending(true, DeliveryTag, MsgId, #{unacked_message_q := UAMQ0} = State) ->
@@ -628,10 +641,9 @@ route(Msg, #{current := #{vhost := VHost}}) ->
 
 confirm_to_inbound(ConfirmFun, SeqNos, State)
   when is_list(SeqNos) ->
-    State1 = lists:foldl(fun(Seq, State0) ->
-                                 confirm_to_inbound(ConfirmFun, Seq, State0)
-                         end, State, SeqNos),
-    maybe_grant_or_stash_credit(State1);
+    lists:foldl(fun(Seq, State0) ->
+                        confirm_to_inbound(ConfirmFun, Seq, State0)
+                end, State, SeqNos);
 confirm_to_inbound(ConfirmFun, Seq,
                    State0 = #{dest := #{unacked := Unacked} = Dst}) ->
     #{Seq := InTag} = Unacked,
diff --git a/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl b/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl
index 6783847dd7e9..62fea906a1ae 100644
--- a/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl
+++ b/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl
@@ -546,12 +546,15 @@ local_to_local_delete_after_queue_length(Config) ->
                                                   {<<"dest-protocol">>, <<"local">>},
                                                   {<<"dest-queue">>, Dest}
                                                  ]),
-              shovel_test_utils:await_no_shovel(Config, ?PARAM),
               %% The shovel parameter is only deleted when 'delete-after'
               %% is used. In any other failure, the shovel should
               %% remain and try to restart
               expect_many(Sess, Dest, 18),
-              ?assertMatch(not_found, rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_runtime_parameters, lookup, [<<"/">>, <<"shovel">>, ?PARAM])),
+              ?awaitMatch(not_found, rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_runtime_parameters, lookup, [<<"/">>, <<"shovel">>, ?PARAM]), 30_000),
+              ?awaitMatch([],
+                          rabbit_ct_broker_helpers:rpc(Config, 0,
+                                                       rabbit_shovel_status, status, []),
+                          30_000),
               publish_many(Sess, Src, Dest, <<"tag1">>, 5),
               expect_none(Sess, Dest)
       end).

From a0d42ea31dd3cbba275e38838f48a67d3be51889 Mon Sep 17 00:00:00 2001
From: Diana Parra Corbacho 
Date: Thu, 24 Jul 2025 14:01:21 +0200
Subject: [PATCH 18/31] Local shovels: remove unused prefetch count

---
 deps/rabbitmq_shovel/src/rabbit_local_shovel.erl      | 4 ----
 deps/rabbitmq_shovel/src/rabbit_shovel_parameters.erl | 4 ----
 2 files changed, 8 deletions(-)

diff --git a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
index c47f6b5996a4..a66ab36ac256 100644
--- a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
+++ b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
@@ -66,9 +66,6 @@
 }).
 
 parse(_Name, {source, Source}) ->
-    Prefetch = parse_parameter(prefetch_count, fun parse_non_negative_integer/1,
-                               proplists:get_value(prefetch_count, Source,
-                                                   ?DEFAULT_PREFETCH)),
     Queue = parse_parameter(queue, fun parse_binary/1,
                             proplists:get_value(queue, Source)),
     CArgs = proplists:get_value(consumer_args, Source, []),
@@ -77,7 +74,6 @@ parse(_Name, {source, Source}) ->
       resource_decl => rabbit_shovel_util:decl_fun(?MODULE, {source, Source}),
       queue => Queue,
       delete_after => proplists:get_value(delete_after, Source, never),
-      prefetch_count => Prefetch,
       consumer_args => CArgs};
 parse(_Name, {destination, Dest}) ->
     Exchange = parse_parameter(dest_exchange, fun parse_binary/1,
diff --git a/deps/rabbitmq_shovel/src/rabbit_shovel_parameters.erl b/deps/rabbitmq_shovel/src/rabbit_shovel_parameters.erl
index 98b30edbf430..06a7a223621e 100644
--- a/deps/rabbitmq_shovel/src/rabbit_shovel_parameters.erl
+++ b/deps/rabbitmq_shovel/src/rabbit_shovel_parameters.erl
@@ -174,7 +174,6 @@ local_src_validation(_Def, User) ->
      {<<"src-queue">>, fun rabbit_parameter_validation:binary/2, optional},
      {<<"src-queue-args">>,   fun validate_queue_args/2, optional},
      {<<"src-consumer-args">>, fun validate_consumer_args/2, optional},
-     {<<"src-prefetch-count">>, fun rabbit_parameter_validation:number/2, optional},
      {<<"src-delete-after">>, fun validate_delete_after/2, optional},
      {<<"src-predeclared">>,  fun rabbit_parameter_validation:boolean/2, optional}
     ].
@@ -602,8 +601,6 @@ parse_local_source(Def) ->
     end,
     DeleteAfter = pget(<<"src-delete-after">>, Def,
                        pget(<<"delete-after">>, Def, <<"never">>)),
-    PrefetchCount = pget(<<"src-prefetch-count">>, Def,
-                         pget(<<"prefetch-count">>, Def, 1000)),
     %% Details are only used for status report in rabbitmqctl, as vhost is not
     %% available to query the runtime parameters.
     Details = maps:from_list([{K, V} || {K, V} <- [{exchange, SrcX},
@@ -614,7 +611,6 @@ parse_local_source(Def) ->
                   resource_decl => SrcDeclFun,
                   queue => Queue,
                   delete_after => opt_b2a(DeleteAfter),
-                  prefetch_count => PrefetchCount,
                   consumer_args => SrcCArgs
                  }, Details), DestHeaders}.
 

From be061cd54b3fe566ed1bc63e62a6a42a5ade7f80 Mon Sep 17 00:00:00 2001
From: Diana Parra Corbacho 
Date: Mon, 28 Jul 2025 17:17:33 +0200
Subject: [PATCH 19/31] Local shovel: fix credit handling order

---
 .../src/rabbit_local_shovel.erl               | 18 ++++++-----
 .../test/local_dynamic_SUITE.erl              | 30 ++++++++++++++++++-
 2 files changed, 39 insertions(+), 9 deletions(-)

diff --git a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
index a66ab36ac256..9a730ad1875c 100644
--- a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
+++ b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
@@ -353,7 +353,7 @@ forward(Tag, Msg0, #{dest := #{current := #{queue_states := QState} = Current,
                                State3 = rabbit_shovel_behaviour:ack(Tag, false, State2),
                                rabbit_shovel_behaviour:decr_remaining(1, State3)
                        end),
-            handle_queue_actions(Actions, State4);
+            handle_dest_queue_actions(Actions, State4);
         {error, Reason} ->
             exit({shutdown, Reason})
     end.
@@ -426,11 +426,13 @@ handle_queue_actions(Actions, State) ->
       end, State, Actions).
 
 handle_deliver(AckRequired, Msgs, State) when is_list(Msgs) ->
-    lists:foldl(fun({_QName, _QPid, MsgId, _Redelivered, Mc}, S0) ->
-                        DeliveryTag = next_tag(S0),
-                        S = record_pending(AckRequired, DeliveryTag, MsgId, increase_next_tag(S0)),
-                        sent_delivery(rabbit_shovel_behaviour:forward(DeliveryTag, Mc, S))
-                end, State, Msgs).
+    maybe_grant_or_stash_credit(
+      lists:foldl(
+        fun({_QName, _QPid, MsgId, _Redelivered, Mc}, S0) ->
+                DeliveryTag = next_tag(S0),
+                S = record_pending(AckRequired, DeliveryTag, MsgId, increase_next_tag(S0)),
+                rabbit_shovel_behaviour:forward(DeliveryTag, Mc, sent_delivery(S))
+        end, State, Msgs)).
 
 next_tag(#{source := #{current := #{next_tag := DeliveryTag}}}) ->
     DeliveryTag.
@@ -450,6 +452,7 @@ handle_dest_queue_actions(Actions, State) ->
                 confirm_to_inbound(fun(Tag, StateX) ->
                                            rabbit_shovel_behaviour:nack(Tag, false, StateX)
                                    end, MsgSeqNos, S0));
+         %% TODO handle {block, QName}
          (_Action, S0) ->
               S0
       end, State, Actions).
@@ -661,8 +664,7 @@ sent_delivery(#{source := #{delivery_count := DeliveryCount0,
                                    delivery_count => DeliveryCount,
                                    queue_credit => QCredit,
                                    queue_delivery_count => QDeliveryCount
-                                  }},
-    maybe_grant_or_stash_credit(State).
+                                  }}.
 
 maybe_grant_or_stash_credit(#{source := #{queue := QName0,
                                     credit := Credit,
diff --git a/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl b/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl
index 62fea906a1ae..20e6e8d1122e 100644
--- a/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl
+++ b/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl
@@ -68,7 +68,8 @@ groups() ->
                   local_to_local_delete_dest_queue,
                   local_to_local_vhost_access,
                   local_to_local_user_access,
-                  local_to_local_credit_flow
+                  local_to_local_credit_flow,
+                  local_to_local_stream_credit_flow
                  ]}
     ].
 
@@ -938,6 +939,33 @@ local_to_local_credit_flow(Config) ->
               expect_many(Sess, Dest, 500)
       end).
 
+local_to_local_stream_credit_flow(Config) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    VHost = <<"/">>,
+    declare_queue(Config, VHost, Src, [{<<"x-queue-type">>, longstr, <<"stream">>}]),
+    declare_queue(Config, VHost, Dest, [{<<"x-queue-type">>, longstr, <<"stream">>}]),
+    with_session(Config,
+      fun (Sess) ->
+             shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"local">>},
+                                           {<<"src-queue">>, Src},
+                                           {<<"src-predeclared">>, true},
+                                           {<<"dest-protocol">>, <<"local">>},
+                                           {<<"dest-queue">>, Dest},
+                                           {<<"dest-predeclared">>, true}
+                                          ]),
+
+              Receiver = subscribe(Sess, Dest),
+              publish_many(Sess, Src, Dest, <<"tag1">>, 500),
+              ?awaitMatch([{_Name, dynamic, {running, _}, #{forwarded := 500}, _}],
+                          rabbit_ct_broker_helpers:rpc(Config, 0,
+                                                       rabbit_shovel_status, status, []),
+                          30000),
+              _ = expect(Receiver, 500, []),
+              amqp10_client:detach_link(Receiver)
+      end).
+
 %%----------------------------------------------------------------------------
 with_session(Config, Fun) ->
     with_session(Config, <<"/">>, Fun).

From 414da5d14635fd4a0b5a4ba312df678c67c3d94d Mon Sep 17 00:00:00 2001
From: Michael Klishin 
Date: Mon, 28 Jul 2025 11:33:56 -0400
Subject: [PATCH 20/31] local_dynamic_SUITE: ignore two expected crash reports
 in the logs

---
 deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl b/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl
index 20e6e8d1122e..cdca22296d14 100644
--- a/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl
+++ b/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl
@@ -81,7 +81,13 @@ init_per_suite(Config0) ->
     {ok, _} = application:ensure_all_started(amqp10_client),
     rabbit_ct_helpers:log_environment(),
     Config1 = rabbit_ct_helpers:set_config(Config0, [
-        {rmq_nodename_suffix, ?MODULE}
+        {rmq_nodename_suffix, ?MODULE},
+      {ignored_crashes, [
+          "server_initiated_close,404",
+          "writer,send_failed,closed",
+          "source_queue_down",
+          "dest_queue_down"
+        ]}
       ]),
     rabbit_ct_helpers:run_setup_steps(Config1,
       rabbit_ct_broker_helpers:setup_steps() ++

From 78167f0fb7ae19facab1266e95521c9ad1e1a841 Mon Sep 17 00:00:00 2001
From: Diana Parra Corbacho 
Date: Tue, 29 Jul 2025 10:47:02 +0200
Subject: [PATCH 21/31] Local shovel: more tests

---
 .../test/local_dynamic_SUITE.erl              | 70 +++++++++++++++++--
 1 file changed, 64 insertions(+), 6 deletions(-)

diff --git a/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl b/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl
index cdca22296d14..1c8f90fffd80 100644
--- a/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl
+++ b/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl
@@ -68,8 +68,15 @@ groups() ->
                   local_to_local_delete_dest_queue,
                   local_to_local_vhost_access,
                   local_to_local_user_access,
-                  local_to_local_credit_flow,
-                  local_to_local_stream_credit_flow
+                  local_to_local_credit_flow_on_confirm,
+                  local_to_local_credit_flow_on_publish,
+                  local_to_local_credit_flow_no_ack,
+                  local_to_local_quorum_credit_flow_on_confirm,
+                  local_to_local_quorum_credit_flow_on_publish,
+                  local_to_local_quorum_credit_flow_no_ack,
+                  local_to_local_stream_credit_flow_on_confirm,
+                  local_to_local_stream_credit_flow_on_publish,
+                  local_to_local_stream_credit_flow_no_ack
                  ]}
     ].
 
@@ -930,7 +937,16 @@ local_to_local_user_access(Config) ->
             none]),
     shovel_test_utils:await_no_shovel(Config, ?PARAM).
 
-local_to_local_credit_flow(Config) ->
+local_to_local_credit_flow_on_confirm(Config) ->
+    local_to_local_credit_flow(Config, <<"on-confirm">>).
+
+local_to_local_credit_flow_on_publish(Config) ->
+    local_to_local_credit_flow(Config, <<"on-publish">>).
+
+local_to_local_credit_flow_no_ack(Config) ->
+    local_to_local_credit_flow(Config, <<"no-ack">>).
+
+local_to_local_credit_flow(Config, AckMode) ->
     Src = ?config(srcq, Config),
     Dest = ?config(destq, Config),
     with_session(Config,
@@ -939,13 +955,53 @@ local_to_local_credit_flow(Config) ->
                                           [{<<"src-protocol">>, <<"local">>},
                                            {<<"src-queue">>, Src},
                                            {<<"dest-protocol">>, <<"local">>},
-                                           {<<"dest-queue">>, Dest}
+                                           {<<"dest-queue">>, Dest},
+                                           {<<"ack-mode">>, AckMode}
+                                          ]),
+              publish_many(Sess, Src, Dest, <<"tag1">>, 500),
+              expect_many(Sess, Dest, 500)
+      end).
+
+local_to_local_quorum_credit_flow_on_confirm(Config) ->
+    local_to_local_quorum_credit_flow(Config, <<"on-confirm">>).
+
+local_to_local_quorum_credit_flow_on_publish(Config) ->
+    local_to_local_quorum_credit_flow(Config, <<"on-publish">>).
+
+local_to_local_quorum_credit_flow_no_ack(Config) ->
+    local_to_local_quorum_credit_flow(Config, <<"no-ack">>).
+
+local_to_local_quorum_credit_flow(Config, AckMode) ->
+    Src = ?config(srcq, Config),
+    Dest = ?config(destq, Config),
+    VHost = <<"/">>,
+    declare_queue(Config, VHost, Src, [{<<"x-queue-type">>, longstr, <<"quorum">>}]),
+    declare_queue(Config, VHost, Dest, [{<<"x-queue-type">>, longstr, <<"quorum">>}]),
+    with_session(Config,
+      fun (Sess) ->
+             shovel_test_utils:set_param(Config, ?PARAM,
+                                          [{<<"src-protocol">>, <<"local">>},
+                                           {<<"src-queue">>, Src},
+                                           {<<"src-predeclared">>, true},
+                                           {<<"dest-protocol">>, <<"local">>},
+                                           {<<"dest-queue">>, Dest},
+                                           {<<"dest-predeclared">>, true},
+                                           {<<"ack-mode">>, AckMode}
                                           ]),
               publish_many(Sess, Src, Dest, <<"tag1">>, 500),
               expect_many(Sess, Dest, 500)
       end).
 
-local_to_local_stream_credit_flow(Config) ->
+local_to_local_stream_credit_flow_on_confirm(Config) ->
+    local_to_local_stream_credit_flow(Config, <<"on-confirm">>).
+
+local_to_local_stream_credit_flow_on_publish(Config) ->
+    local_to_local_stream_credit_flow(Config, <<"on-publish">>).
+
+local_to_local_stream_credit_flow_no_ack(Config) ->
+    local_to_local_stream_credit_flow(Config, <<"no-ack">>).
+
+local_to_local_stream_credit_flow(Config, AckMode) ->
     Src = ?config(srcq, Config),
     Dest = ?config(destq, Config),
     VHost = <<"/">>,
@@ -959,7 +1015,8 @@ local_to_local_stream_credit_flow(Config) ->
                                            {<<"src-predeclared">>, true},
                                            {<<"dest-protocol">>, <<"local">>},
                                            {<<"dest-queue">>, Dest},
-                                           {<<"dest-predeclared">>, true}
+                                           {<<"dest-predeclared">>, true},
+                                           {<<"ack-mode">>, AckMode}
                                           ]),
 
               Receiver = subscribe(Sess, Dest),
@@ -972,6 +1029,7 @@ local_to_local_stream_credit_flow(Config) ->
               amqp10_client:detach_link(Receiver)
       end).
 
+
 %%----------------------------------------------------------------------------
 with_session(Config, Fun) ->
     with_session(Config, <<"/">>, Fun).

From bb5e1d95d9d31c20d83721a468c838aebf569837 Mon Sep 17 00:00:00 2001
From: Diana Parra Corbacho 
Date: Tue, 29 Jul 2025 11:59:38 +0200
Subject: [PATCH 22/31] Local shovels: handle credit on sender side

---
 .../src/rabbit_local_shovel.erl               | 53 ++++++++-----------
 .../test/local_dynamic_SUITE.erl              | 14 ++---
 2 files changed, 30 insertions(+), 37 deletions(-)

diff --git a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
index 9a730ad1875c..a8a1f7df1d53 100644
--- a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
+++ b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
@@ -318,10 +318,10 @@ handle_dest(_Msg, State) ->
     State.
 
 ack(DeliveryTag, Multiple, State) ->
-    settle(complete, DeliveryTag, Multiple, State).
+    maybe_grant_or_stash_credit(settle(complete, DeliveryTag, Multiple, State)).
 
 nack(DeliveryTag, Multiple, State) ->
-    settle(discard, DeliveryTag, Multiple, State).
+    maybe_grant_or_stash_credit(settle(discard, DeliveryTag, Multiple, State)).
 
 forward(Tag, Msg0, #{dest := #{current := #{queue_states := QState} = Current,
                                unacked := Unacked} = Dest,
@@ -398,11 +398,6 @@ parse_parameter(Param, Fun, Value) ->
             fail({invalid_parameter_value, Param, Err})
     end.
 
-parse_non_negative_integer(N) when is_integer(N) andalso N >= 0 ->
-    N;
-parse_non_negative_integer(N) ->
-    fail({require_non_negative_integer, N}).
-
 parse_binary(Binary) when is_binary(Binary) ->
     Binary;
 parse_binary(NotABinary) ->
@@ -431,8 +426,8 @@ handle_deliver(AckRequired, Msgs, State) when is_list(Msgs) ->
         fun({_QName, _QPid, MsgId, _Redelivered, Mc}, S0) ->
                 DeliveryTag = next_tag(S0),
                 S = record_pending(AckRequired, DeliveryTag, MsgId, increase_next_tag(S0)),
-                rabbit_shovel_behaviour:forward(DeliveryTag, Mc, sent_delivery(S))
-        end, State, Msgs)).
+                rabbit_shovel_behaviour:forward(DeliveryTag, Mc, S)
+        end, sent_delivery(State, length(Msgs)), Msgs)).
 
 next_tag(#{source := #{current := #{next_tag := DeliveryTag}}}) ->
     DeliveryTag.
@@ -443,15 +438,13 @@ increase_next_tag(#{source := Source = #{current := Current = #{next_tag := Deli
 handle_dest_queue_actions(Actions, State) ->
     lists:foldl(
       fun({settled, _QName, MsgSeqNos}, S0) ->
-              maybe_grant_or_stash_credit(
-                confirm_to_inbound(fun(Tag, StateX) ->
-                                           rabbit_shovel_behaviour:ack(Tag, false, StateX)
-                                   end, MsgSeqNos, S0));
+              confirm_to_inbound(fun(Tag, StateX) ->
+                                         rabbit_shovel_behaviour:ack(Tag, false, StateX)
+                                 end, MsgSeqNos, S0);
          ({rejected, _QName, MsgSeqNos}, S0) ->
-              maybe_grant_or_stash_credit(
-                confirm_to_inbound(fun(Tag, StateX) ->
-                                           rabbit_shovel_behaviour:nack(Tag, false, StateX)
-                                   end, MsgSeqNos, S0));
+              confirm_to_inbound(fun(Tag, StateX) ->
+                                         rabbit_shovel_behaviour:nack(Tag, false, StateX)
+                                 end, MsgSeqNos, S0);
          %% TODO handle {block, QName}
          (_Action, S0) ->
               S0
@@ -646,25 +639,25 @@ confirm_to_inbound(ConfirmFun, SeqNos, State)
 confirm_to_inbound(ConfirmFun, Seq,
                    State0 = #{dest := #{unacked := Unacked} = Dst}) ->
     #{Seq := InTag} = Unacked,
-    State = ConfirmFun(InTag, State0),
     Unacked1 = maps:remove(Seq, Unacked),
-    rabbit_shovel_behaviour:decr_remaining(
-      1, State#{dest => Dst#{unacked => Unacked1}}).
+    State = rabbit_shovel_behaviour:decr_remaining(
+              1, State0#{dest => Dst#{unacked => Unacked1}}),
+    ConfirmFun(InTag, State).
 
 sent_delivery(#{source := #{delivery_count := DeliveryCount0,
                             credit := Credit0,
                             queue_delivery_count := QDeliveryCount0,
                             queue_credit := QCredit0} = Src
-               } = State0) ->
-    DeliveryCount = serial_number:add(DeliveryCount0, 1),
-    Credit = max(0, Credit0 - 1),
-    QDeliveryCount = serial_number:add(QDeliveryCount0, 1),
-    QCredit = max(0, QCredit0 - 1),
-    State = State0#{source => Src#{credit => Credit,
-                                   delivery_count => DeliveryCount,
-                                   queue_credit => QCredit,
-                                   queue_delivery_count => QDeliveryCount
-                                  }}.
+               } = State0, NumMsgs) ->
+    DeliveryCount = serial_number:add(DeliveryCount0, NumMsgs),
+    Credit = max(0, Credit0 - NumMsgs),
+    QDeliveryCount = serial_number:add(QDeliveryCount0, NumMsgs),
+    QCredit = max(0, QCredit0 - NumMsgs),
+    State0#{source => Src#{credit => Credit,
+                           delivery_count => DeliveryCount,
+                           queue_credit => QCredit,
+                           queue_delivery_count => QDeliveryCount
+                          }}.
 
 maybe_grant_or_stash_credit(#{source := #{queue := QName0,
                                     credit := Credit,
diff --git a/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl b/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl
index 1c8f90fffd80..d6242d818574 100644
--- a/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl
+++ b/deps/rabbitmq_shovel/test/local_dynamic_SUITE.erl
@@ -958,8 +958,8 @@ local_to_local_credit_flow(Config, AckMode) ->
                                            {<<"dest-queue">>, Dest},
                                            {<<"ack-mode">>, AckMode}
                                           ]),
-              publish_many(Sess, Src, Dest, <<"tag1">>, 500),
-              expect_many(Sess, Dest, 500)
+              publish_many(Sess, Src, Dest, <<"tag1">>, 1000),
+              expect_many(Sess, Dest, 1000)
       end).
 
 local_to_local_quorum_credit_flow_on_confirm(Config) ->
@@ -988,8 +988,8 @@ local_to_local_quorum_credit_flow(Config, AckMode) ->
                                            {<<"dest-predeclared">>, true},
                                            {<<"ack-mode">>, AckMode}
                                           ]),
-              publish_many(Sess, Src, Dest, <<"tag1">>, 500),
-              expect_many(Sess, Dest, 500)
+              publish_many(Sess, Src, Dest, <<"tag1">>, 1000),
+              expect_many(Sess, Dest, 1000)
       end).
 
 local_to_local_stream_credit_flow_on_confirm(Config) ->
@@ -1020,12 +1020,12 @@ local_to_local_stream_credit_flow(Config, AckMode) ->
                                           ]),
 
               Receiver = subscribe(Sess, Dest),
-              publish_many(Sess, Src, Dest, <<"tag1">>, 500),
-              ?awaitMatch([{_Name, dynamic, {running, _}, #{forwarded := 500}, _}],
+              publish_many(Sess, Src, Dest, <<"tag1">>, 1000),
+              ?awaitMatch([{_Name, dynamic, {running, _}, #{forwarded := 1000}, _}],
                           rabbit_ct_broker_helpers:rpc(Config, 0,
                                                        rabbit_shovel_status, status, []),
                           30000),
-              _ = expect(Receiver, 500, []),
+              _ = expect(Receiver, 1000, []),
               amqp10_client:detach_link(Receiver)
       end).
 

From 8c9f79fb36fa88c3ee720b8eb39a13952996db5d Mon Sep 17 00:00:00 2001
From: Diana Parra Corbacho 
Date: Wed, 30 Jul 2025 15:02:45 +0200
Subject: [PATCH 23/31] Shovel management: add local shovels

---
 .../priv/www/js/shovel.js                     | 12 ++-
 .../priv/www/js/tmpl/dynamic-shovels.ejs      | 98 ++++++++++++++++++-
 .../test/http_SUITE.erl                       | 33 ++++++-
 3 files changed, 140 insertions(+), 3 deletions(-)

diff --git a/deps/rabbitmq_shovel_management/priv/www/js/shovel.js b/deps/rabbitmq_shovel_management/priv/www/js/shovel.js
index ee46e8562ff2..496c7876eb00 100644
--- a/deps/rabbitmq_shovel_management/priv/www/js/shovel.js
+++ b/deps/rabbitmq_shovel_management/priv/www/js/shovel.js
@@ -42,17 +42,27 @@ dispatcher_add(function(sammy) {
             //remove fields not required by the selected protocol
             if (this.params['src-protocol'] == 'amqp10') {
                 remove_params_with(this, 'amqp091-src');
+                remove_params_with(this, 'local-src');
+            } else if (this.params['src-protocol'] == 'amqp091') {
+                remove_params_with(this, 'amqp10-src');
+                remove_params_with(this, 'local-src');
             } else {
                 remove_params_with(this, 'amqp10-src');
+                remove_params_with(this, 'amqp091-src');
             }
             if (this.params['dest-protocol'] == 'amqp10') {
                 remove_params_with(this, 'amqp091-dest');
+                remove_params_with(this, 'local-dest');
+            } else if (this.params['dest-protocol'] == 'amqp091'){
+                remove_params_with(this, 'amqp10-dest');
+                remove_params_with(this, 'local-dest');
             } else {
+                remove_params_with(this, 'amqp091-dest');
                 remove_params_with(this, 'amqp10-dest');
             }
 
             var trimProtoPrefix = function (x) {
-                if(x.startsWith('amqp10-') || x.startsWith('amqp091-')) {
+                if(x.startsWith('amqp10-') || x.startsWith('amqp091-') || x.startsWith('local-')) {
                     return x.substr(x.indexOf('-') + 1, x.length);
                 }
                 return x;
diff --git a/deps/rabbitmq_shovel_management/priv/www/js/tmpl/dynamic-shovels.ejs b/deps/rabbitmq_shovel_management/priv/www/js/tmpl/dynamic-shovels.ejs
index 979bd420bf6f..2e8e1fb81c31 100644
--- a/deps/rabbitmq_shovel_management/priv/www/js/tmpl/dynamic-shovels.ejs
+++ b/deps/rabbitmq_shovel_management/priv/www/js/tmpl/dynamic-shovels.ejs
@@ -95,6 +95,7 @@
             
             
+            
           
         
         
@@ -214,6 +264,7 @@
             
             
+            
           
         
         
diff --git a/deps/rabbitmq_shovel_management/test/http_SUITE.erl b/deps/rabbitmq_shovel_management/test/http_SUITE.erl
index 12bf2345d4da..28cee8bdcd4c 100644
--- a/deps/rabbitmq_shovel_management/test/http_SUITE.erl
+++ b/deps/rabbitmq_shovel_management/test/http_SUITE.erl
@@ -32,6 +32,7 @@ groups() ->
                   start_and_get_a_dynamic_amqp091_shovel_with_publish_properties,
                   start_and_get_a_dynamic_amqp091_shovel_with_missing_publish_properties,
                   start_and_get_a_dynamic_amqp091_shovel_with_empty_publish_properties,
+                  start_and_get_a_dynamic_local_shovel,
                   create_and_delete_a_dynamic_shovel_that_successfully_connects,
                   create_and_delete_a_dynamic_shovel_that_fails_to_connect
                  ]},
@@ -212,6 +213,20 @@ start_and_get_a_dynamic_amqp091_shovel_with_empty_publish_properties(Config) ->
 
     ok.
 
+start_and_get_a_dynamic_local_shovel(Config) ->
+    remove_all_dynamic_shovels(Config, <<"/">>),
+    Name = rabbit_data_coercion:to_binary(?FUNCTION_NAME),
+    ID = {<<"/">>, Name},
+    await_shovel_removed(Config, ID),
+
+    declare_local_shovel(Config, Name),
+    await_shovel_startup(Config, ID),
+    Sh = get_shovel(Config, Name),
+    ?assertEqual(Name, maps:get(name, Sh)),
+    delete_shovel(Config, Name),
+
+    ok.
+
 start_static_shovels(Config) ->
     http_put(Config, "/users/admin",
 	     #{password => <<"admin">>, tags => <<"administrator">>}, ?CREATED),
@@ -455,6 +470,22 @@ declare_amqp091_shovel_with_publish_properties(Config, Name, Props) ->
             }
         }, ?CREATED).
 
+declare_local_shovel(Config, Name) ->
+    Port = integer_to_binary(
+        rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp)),
+    http_put(Config, io_lib:format("/parameters/shovel/%2f/~ts", [Name]),
+        #{
+            value => #{
+                <<"src-protocol">> => <<"local">>,
+                <<"src-uri">> => <<"amqp://localhost:", Port/binary>>,
+                <<"src-queue">>  => <<"local.src.test">>,
+                <<"src-delete-after">> => <<"never">>,
+                <<"dest-protocol">> => <<"local">>,
+                <<"dest-uri">> => <<"amqp://localhost:", Port/binary>>,
+                <<"dest-queue">> => <<"local.dest.test">>
+            }
+        }, ?CREATED).
+
 await_shovel_startup(Config, Name) ->
     await_shovel_startup(Config, Name, 10_000).
 
@@ -480,4 +511,4 @@ does_shovel_exist(Config, Name) ->
     case lookup_shovel_status(Config, Name) of
         not_found -> false;
         _Found    -> true
-    end.
\ No newline at end of file
+    end.

From 0b1aefd4ae483db1d6750c522c37f0a7c75a1e16 Mon Sep 17 00:00:00 2001
From: Diana Parra Corbacho 
Date: Tue, 12 Aug 2025 14:59:44 +0200
Subject: [PATCH 24/31] Local shovels: move unacked_message_q inside source
 config

---
 .../src/rabbit_local_shovel.erl               | 26 ++++++++++---------
 1 file changed, 14 insertions(+), 12 deletions(-)

diff --git a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
index a8a1f7df1d53..6328b132e914 100644
--- a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
+++ b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
@@ -103,9 +103,9 @@ connect_source(State = #{source := Src = #{resource_decl := {M, F, MFArgs},
     State#{source => Src#{current => #{queue_states => QState,
                                        next_tag => 1,
                                        user => User,
-                                       vhost => VHost},
-                          queue => QName},
-           unacked_message_q => ?QUEUE:new()}.
+                                       vhost => VHost,
+                                       unacked_message_q => ?QUEUE:new()},
+                          queue => QName}}.
 
 connect_dest(State = #{dest := Dest = #{resource_decl := {M, F, MFArgs},
                                         uris := [Uri | _]
@@ -452,10 +452,11 @@ handle_dest_queue_actions(Actions, State) ->
 
 record_pending(false, _DeliveryTag, _MsgId, State) ->
     State;
-record_pending(true, DeliveryTag, MsgId, #{unacked_message_q := UAMQ0} = State) ->
+record_pending(true, DeliveryTag, MsgId,
+               #{source := Src = #{current := Current = #{unacked_message_q := UAMQ0}}} = State) ->
     UAMQ = ?QUEUE:in(#pending_ack{delivery_tag = DeliveryTag,
                                   msg_id = MsgId}, UAMQ0),
-    State#{unacked_message_q => UAMQ}.
+    State#{source => Src#{current => Current#{unacked_message_q => UAMQ}}}.
 
 remaining(_Q, #{source := #{delete_after := never}}) ->
     unlimited;
@@ -567,18 +568,19 @@ get_user_vhost_from_amqp_param(Uri) ->
             exit({shutdown, {access_refused, Username}})
     end.
 
-settle(Op, DeliveryTag, Multiple, #{unacked_message_q := UAMQ0,
-                             source := #{queue := Queue,
-                                         current := Current = #{queue_states := QState0,
-                                                                consumer_tag := CTag,
-                                                                vhost := VHost}} = Src} = State0) ->
+settle(Op, DeliveryTag, Multiple,
+       #{source := #{queue := Queue,
+                     current := Current = #{queue_states := QState0,
+                                            consumer_tag := CTag,
+                                            unacked_message_q := UAMQ0,
+                                            vhost := VHost}} = Src} = State0) ->
     {Acked, UAMQ} = collect_acks(UAMQ0, DeliveryTag, Multiple),
     QRef = rabbit_misc:r(VHost, queue, Queue),
     MsgIds = [Ack#pending_ack.msg_id || Ack <- Acked],
     case rabbit_queue_type:settle(QRef, Op, CTag, MsgIds, QState0) of
         {ok, QState1, Actions} ->
-            State = State0#{source => Src#{current => Current#{queue_states => QState1}},
-                            unacked_message_q => UAMQ},
+            State = State0#{source => Src#{current => Current#{queue_states => QState1,
+                                                               unacked_message_q => UAMQ}}},
             handle_queue_actions(Actions, State);
         {'protocol_error', Type, Reason, Args} ->
             ?LOG_ERROR("Shovel failed to settle ~p acknowledgments with ~tp: ~tp",

From e9d767b84b949a27eaf7542004008c1064fb50dc Mon Sep 17 00:00:00 2001
From: Diana Parra Corbacho 
Date: Wed, 13 Aug 2025 12:19:36 +0200
Subject: [PATCH 25/31] Local shovels: fix credit flow

---
 .../src/rabbit_local_shovel.erl               | 20 ++++++-------------
 1 file changed, 6 insertions(+), 14 deletions(-)

diff --git a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
index 6328b132e914..ac414bc25e55 100644
--- a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
+++ b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
@@ -176,9 +176,7 @@ init_source(State = #{source := #{queue := QName0,
                                            remaining => Remaining,
                                            remaining_unacked => Remaining,
                                            delivery_count => ?INITIAL_DELIVERY_COUNT,
-                                           queue_delivery_count => ?INITIAL_DELIVERY_COUNT,
                                            credit => MaxLinkCredit,
-                                           queue_credit => MaxLinkCredit,
                                            at_least_one_credit_req_in_flight => true,
                                            stashed_credit_req => none}},
             handle_queue_actions(Actions, State2);
@@ -647,18 +645,12 @@ confirm_to_inbound(ConfirmFun, Seq,
     ConfirmFun(InTag, State).
 
 sent_delivery(#{source := #{delivery_count := DeliveryCount0,
-                            credit := Credit0,
-                            queue_delivery_count := QDeliveryCount0,
-                            queue_credit := QCredit0} = Src
+                            credit := Credit0} = Src
                } = State0, NumMsgs) ->
     DeliveryCount = serial_number:add(DeliveryCount0, NumMsgs),
     Credit = max(0, Credit0 - NumMsgs),
-    QDeliveryCount = serial_number:add(QDeliveryCount0, NumMsgs),
-    QCredit = max(0, QCredit0 - NumMsgs),
     State0#{source => Src#{credit => Credit,
-                           delivery_count => DeliveryCount,
-                           queue_credit => QCredit,
-                           queue_delivery_count => QDeliveryCount
+                           delivery_count => DeliveryCount
                           }}.
 
 maybe_grant_or_stash_credit(#{source := #{queue := QName0,
@@ -709,7 +701,7 @@ grant_link_credit(Credit, MaxLinkCredit, NumUnconfirmed) ->
 %% Drain is ignored because local shovels do not use it.
 handle_credit_reply({credit_reply, CTag, DeliveryCount, Credit, _Available, _Drain},
                     #{source := #{credit := CCredit,
-                                  queue_delivery_count := QDeliveryCount,
+                                  delivery_count := QDeliveryCount,
                                   stashed_credit_req := StashedCreditReq,
                                   queue := QName0,
                                   current := Current = #{queue_states := QState0,
@@ -723,7 +715,7 @@ handle_credit_reply({credit_reply, CTag, DeliveryCount, Credit, _Available, _Dra
           QName = rabbit_misc:r(VHost, queue, QName0),
           {ok, QState, Actions} = rabbit_queue_type:credit(QName, CTag, StashedDeliveryCount,
             MaxLinkCredit, false, QState0),
-          State = State0#{source => Src#{queue_credit => MaxLinkCredit,
+          State = State0#{source => Src#{credit => MaxLinkCredit,
             at_least_one_credit_req_in_flight => true,
             stashed_credit_req => none,
             current => Current#{queue_states => QState}}},
@@ -733,7 +725,7 @@ handle_credit_reply({credit_reply, CTag, DeliveryCount, Credit, _Available, _Dra
             MaxLinkCredit = max_link_credit(),
             QName = rabbit_misc:r(VHost, queue, QName0),
             {ok, QState, Actions} = rabbit_queue_type:credit(QName, CTag, DeliveryCount, MaxLinkCredit, false, QState0),
-            State = State0#{source => Src#{queue_credit => MaxLinkCredit,
+            State = State0#{source => Src#{credit => MaxLinkCredit,
                                            at_least_one_credit_req_in_flight => true,
                                            current => Current#{queue_states => QState}}},
             handle_queue_actions(Actions, State);
@@ -743,6 +735,6 @@ handle_credit_reply({credit_reply, CTag, DeliveryCount, Credit, _Available, _Dra
             %% in case credit requests got applied out of order in quorum queues).
             %% This should be fine given that we asserted earlier that our delivery-count is
             %% in sync with the delivery-count of the sending queue.
-            State0#{source => Src#{queue_credit => Credit,
+            State0#{source => Src#{credit => Credit,
                                    at_least_one_credit_req_in_flight => false}}
     end.

From 07a085365e48e547d8903d5e73e7d29f5c35a0dc Mon Sep 17 00:00:00 2001
From: Diana Parra Corbacho 
Date: Wed, 13 Aug 2025 12:59:40 +0200
Subject: [PATCH 26/31] Local shovels: optimisations

---
 .../src/rabbit_local_shovel.erl               | 78 +++++++++----------
 1 file changed, 39 insertions(+), 39 deletions(-)

diff --git a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
index ac414bc25e55..e171ad575b9d 100644
--- a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
+++ b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
@@ -100,35 +100,44 @@ connect_source(State = #{source := Src = #{resource_decl := {M, F, MFArgs},
                 <<>> -> MRDQ;
                 _ -> QName0
             end,
+    Queue = rabbit_misc:r(VHost, queue, QName),
     State#{source => Src#{current => #{queue_states => QState,
                                        next_tag => 1,
                                        user => User,
                                        vhost => VHost,
                                        unacked_message_q => ?QUEUE:new()},
-                          queue => QName}}.
+                          queue => QName,
+                          queue_r => Queue}}.
 
 connect_dest(State = #{dest := Dest = #{resource_decl := {M, F, MFArgs},
-                                        uris := [Uri | _]
-                                       },
+                                        uris := [Uri | _]},
                        ack_mode := AckMode}) ->
     %% Shall we get the user from an URI or something else?
     {User, VHost} = get_user_vhost_from_amqp_param(Uri),
     apply(M, F, MFArgs ++ [VHost, User]),
 
     QState = rabbit_queue_type:init(),
-    case AckMode of
-        on_confirm ->
-            State#{dest => Dest#{current => #{queue_states => QState,
-                                              delivery_id => 1,
-                                              vhost => VHost},
-                                 unacked => #{}}};
-        _ ->
-            State#{dest => Dest#{current => #{queue_states => QState,
-                                              vhost => VHost},
-                                 unacked => #{}}}
-    end.
+    maybe_add_dest_queue(
+      case AckMode of
+          on_confirm ->
+              State#{dest => Dest#{current => #{queue_states => QState,
+                                                delivery_id => 1,
+                                                vhost => VHost},
+                                   unacked => #{}}};
+          _ ->
+              State#{dest => Dest#{current => #{queue_states => QState,
+                                                vhost => VHost},
+                                   unacked => #{}}}
+      end).
+
+maybe_add_dest_queue(State = #{dest := Dest = #{queue := QName,
+                                                current := #{vhost := VHost}}}) ->
+    Queue = rabbit_misc:r(VHost, queue, QName),
+    State#{dest => Dest#{queue_r => Queue}};
+maybe_add_dest_queue(State) ->
+    State.
 
-init_source(State = #{source := #{queue := QName0,
+init_source(State = #{source := #{queue_r := QName,
                                   consumer_args := Args,
                                   current := #{queue_states := QState0,
                                                vhost := VHost} = Current} = Src,
@@ -142,7 +151,6 @@ init_source(State = #{source := #{queue := QName0,
                    {credited, credit_api_v1}
            end,
     MaxLinkCredit = max_link_credit(),
-    QName = rabbit_misc:r(VHost, queue, QName0),
     CTag = consumer_tag(Name),
     case rabbit_amqqueue:with(
            QName,
@@ -176,6 +184,7 @@ init_source(State = #{source := #{queue := QName0,
                                            remaining => Remaining,
                                            remaining_unacked => Remaining,
                                            delivery_count => ?INITIAL_DELIVERY_COUNT,
+                                           max_link_credit => MaxLinkCredit,
                                            credit => MaxLinkCredit,
                                            at_least_one_credit_req_in_flight => true,
                                            stashed_credit_req => none}},
@@ -246,10 +255,8 @@ close_dest(_State) ->
 
 close_source(#{source := #{current := #{queue_states := QStates0,
                                         consumer_tag := CTag,
-                                        user := User,
-                                        vhost := VHost},
-                           queue := QName0}}) ->
-    QName = rabbit_misc:r(VHost, queue, QName0),
+                                        user := User},
+                           queue_r := QName}}) ->
     case rabbit_amqqueue:with(
            QName,
            fun(Q) ->
@@ -567,13 +574,12 @@ get_user_vhost_from_amqp_param(Uri) ->
     end.
 
 settle(Op, DeliveryTag, Multiple,
-       #{source := #{queue := Queue,
+       #{source := #{queue_r := QRef,
                      current := Current = #{queue_states := QState0,
                                             consumer_tag := CTag,
-                                            unacked_message_q := UAMQ0,
-                                            vhost := VHost}} = Src} = State0) ->
+                                            unacked_message_q := UAMQ0}
+                    } = Src} = State0) ->
     {Acked, UAMQ} = collect_acks(UAMQ0, DeliveryTag, Multiple),
-    QRef = rabbit_misc:r(VHost, queue, Queue),
     MsgIds = [Ack#pending_ack.msg_id || Ack <- Acked],
     case rabbit_queue_type:settle(QRef, Op, CTag, MsgIds, QState0) of
         {ok, QState1, Actions} ->
@@ -622,10 +628,9 @@ collect_acks(AcknowledgedAcc, RemainingAcc, UAMQ, DeliveryTag, Multiple) ->
            {AcknowledgedAcc, UAMQTail}
     end.
 
-route(_Msg, #{queue := Queue,
-              current := #{vhost := VHost}}) when Queue =/= none ->
-    QName = rabbit_misc:r(VHost, queue, Queue),
-    [QName];
+route(_Msg, #{queue_r := QueueR,
+              queue := Queue}) when Queue =/= none ->
+    [QueueR];
 route(Msg, #{current := #{vhost := VHost}}) ->
     ExchangeName = rabbit_misc:r(VHost, exchange, mc:exchange(Msg)),
     Exchange = rabbit_exchange:lookup_or_die(ExchangeName),
@@ -653,16 +658,15 @@ sent_delivery(#{source := #{delivery_count := DeliveryCount0,
                            delivery_count => DeliveryCount
                           }}.
 
-maybe_grant_or_stash_credit(#{source := #{queue := QName0,
+maybe_grant_or_stash_credit(#{source := #{queue_r := QName,
                                     credit := Credit,
+                                    max_link_credit := MaxLinkCredit,
                                     delivery_count := DeliveryCount,
                                     at_least_one_credit_req_in_flight := HaveCreditReqInFlight,
                                     current := #{consumer_tag := CTag,
-                                                 vhost := VHost,
                                                  queue_states := QState0} = Current
                                    } = Src,
                         dest := #{unacked := Unacked}} = State0) ->
-    MaxLinkCredit = max_link_credit(),
     GrantLinkCredit = grant_link_credit(Credit, MaxLinkCredit, maps:size(Unacked)),
     Src1 = case HaveCreditReqInFlight andalso GrantLinkCredit of
                true ->
@@ -675,7 +679,6 @@ maybe_grant_or_stash_credit(#{source := #{queue := QName0,
            end,
     {ok, QState, Actions} = case (GrantLinkCredit and not HaveCreditReqInFlight) of
                                 true ->
-                                    QName = rabbit_misc:r(VHost, queue, QName0),
                                     rabbit_queue_type:credit(
                                       QName, CTag, DeliveryCount, MaxLinkCredit,
                                       false, QState0);
@@ -701,18 +704,17 @@ grant_link_credit(Credit, MaxLinkCredit, NumUnconfirmed) ->
 %% Drain is ignored because local shovels do not use it.
 handle_credit_reply({credit_reply, CTag, DeliveryCount, Credit, _Available, _Drain},
                     #{source := #{credit := CCredit,
+                                  max_link_credit := MaxLinkCredit,
                                   delivery_count := QDeliveryCount,
                                   stashed_credit_req := StashedCreditReq,
-                                  queue := QName0,
-                                  current := Current = #{queue_states := QState0,
-                                                         vhost := VHost}} = Src} = State0) ->
+                                  queue_r := QName,
+                                  current := Current = #{queue_states := QState0}
+                                 } = Src} = State0) ->
     %% Assertion: Our (receiver) delivery-count should be always
     %% in sync with the delivery-count of the sending queue.
     QDeliveryCount = DeliveryCount,
     case StashedCreditReq of
         #credit_req{delivery_count = StashedDeliveryCount} ->
-          MaxLinkCredit = max_link_credit(),
-          QName = rabbit_misc:r(VHost, queue, QName0),
           {ok, QState, Actions} = rabbit_queue_type:credit(QName, CTag, StashedDeliveryCount,
             MaxLinkCredit, false, QState0),
           State = State0#{source => Src#{credit => MaxLinkCredit,
@@ -722,8 +724,6 @@ handle_credit_reply({credit_reply, CTag, DeliveryCount, Credit, _Available, _Dra
           handle_queue_actions(Actions, State);
         none when Credit =:= 0 andalso
                   CCredit > 0 ->
-            MaxLinkCredit = max_link_credit(),
-            QName = rabbit_misc:r(VHost, queue, QName0),
             {ok, QState, Actions} = rabbit_queue_type:credit(QName, CTag, DeliveryCount, MaxLinkCredit, false, QState0),
             State = State0#{source => Src#{credit => MaxLinkCredit,
                                            at_least_one_credit_req_in_flight => true,

From edf0e3c1ffdea818d46ffc257c345ac81253bc1b Mon Sep 17 00:00:00 2001
From: Diana Parra Corbacho 
Date: Wed, 13 Aug 2025 13:54:52 +0200
Subject: [PATCH 27/31] Local shovel: remove unused clause

---
 deps/rabbitmq_shovel/src/rabbit_local_shovel.erl | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
index e171ad575b9d..223e46cb8931 100644
--- a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
+++ b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
@@ -598,10 +598,6 @@ settle(Op, DeliveryTag, Multiple,
 %%
 %% Returns a tuple of acknowledged pending acks and remaining pending acks.
 %% Sorts each group in the youngest-first order (descending by delivery tag).
-%% The special case for 0 comes from the AMQP 0-9-1 spec: if the multiple field is set to 1 (true),
-%% and the delivery tag is 0, this indicates acknowledgement of all outstanding messages (by a client).
-collect_acks(UAMQ, 0, true) ->
-    {lists:reverse(?QUEUE:to_list(UAMQ)), ?QUEUE:new()};
 collect_acks(UAMQ, DeliveryTag, Multiple) ->
     collect_acks([], [], UAMQ, DeliveryTag, Multiple).
 

From 02fcbc0dc5e288ab3d4a5a8a84b10c9236889e74 Mon Sep 17 00:00:00 2001
From: Diana Parra Corbacho 
Date: Wed, 13 Aug 2025 14:13:12 +0200
Subject: [PATCH 28/31] Local shovels: optimisation

---
 deps/rabbitmq_shovel/src/rabbit_local_shovel.erl | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
index 223e46cb8931..489cbbafdbc2 100644
--- a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
+++ b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
@@ -579,8 +579,7 @@ settle(Op, DeliveryTag, Multiple,
                                             consumer_tag := CTag,
                                             unacked_message_q := UAMQ0}
                     } = Src} = State0) ->
-    {Acked, UAMQ} = collect_acks(UAMQ0, DeliveryTag, Multiple),
-    MsgIds = [Ack#pending_ack.msg_id || Ack <- Acked],
+    {MsgIds, UAMQ} = collect_acks(UAMQ0, DeliveryTag, Multiple),
     case rabbit_queue_type:settle(QRef, Op, CTag, MsgIds, QState0) of
         {ok, QState1, Actions} ->
             State = State0#{source => Src#{current => Current#{queue_states => QState1,
@@ -603,10 +602,11 @@ collect_acks(UAMQ, DeliveryTag, Multiple) ->
 
 collect_acks(AcknowledgedAcc, RemainingAcc, UAMQ, DeliveryTag, Multiple) ->
     case ?QUEUE:out(UAMQ) of
-        {{value, UnackedMsg = #pending_ack{delivery_tag = CurrentDT}},
+        {{value, UnackedMsg = #pending_ack{delivery_tag = CurrentDT,
+                                           msg_id = Id}},
          UAMQTail} ->
             if CurrentDT == DeliveryTag ->
-                   {[UnackedMsg | AcknowledgedAcc],
+                   {[Id | AcknowledgedAcc],
                     case RemainingAcc of
                         [] -> UAMQTail;
                         _  -> ?QUEUE:join(
@@ -614,7 +614,7 @@ collect_acks(AcknowledgedAcc, RemainingAcc, UAMQ, DeliveryTag, Multiple) ->
                                  UAMQTail)
                     end};
                Multiple ->
-                    collect_acks([UnackedMsg | AcknowledgedAcc], RemainingAcc,
+                    collect_acks([Id | AcknowledgedAcc], RemainingAcc,
                                  UAMQTail, DeliveryTag, Multiple);
                true ->
                     collect_acks(AcknowledgedAcc, [UnackedMsg | RemainingAcc],

From 3349321c585e4468e684cc709cf7a28e2dbcb055 Mon Sep 17 00:00:00 2001
From: Diana Parra Corbacho 
Date: Wed, 13 Aug 2025 19:30:19 +0200
Subject: [PATCH 29/31] Local shovels: fix credit flow

---
 deps/rabbitmq_shovel/src/rabbit_local_shovel.erl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
index 489cbbafdbc2..8644b8c61f08 100644
--- a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
+++ b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
@@ -719,7 +719,7 @@ handle_credit_reply({credit_reply, CTag, DeliveryCount, Credit, _Available, _Dra
             current => Current#{queue_states => QState}}},
           handle_queue_actions(Actions, State);
         none when Credit =:= 0 andalso
-                  CCredit > 0 ->
+                  CCredit >= 0 ->
             {ok, QState, Actions} = rabbit_queue_type:credit(QName, CTag, DeliveryCount, MaxLinkCredit, false, QState0),
             State = State0#{source => Src#{credit => MaxLinkCredit,
                                            at_least_one_credit_req_in_flight => true,

From 382fac3e348c7c38ac705976bb455eec6a95f2a9 Mon Sep 17 00:00:00 2001
From: Diana Parra Corbacho 
Date: Thu, 14 Aug 2025 12:39:26 +0200
Subject: [PATCH 30/31] Local shovels: remove stashed credit request

---
 .../src/rabbit_local_shovel.erl               | 67 +++++++------------
 1 file changed, 25 insertions(+), 42 deletions(-)

diff --git a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
index 8644b8c61f08..73335b6fff14 100644
--- a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
+++ b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
@@ -186,8 +186,7 @@ init_source(State = #{source := #{queue_r := QName,
                                            delivery_count => ?INITIAL_DELIVERY_COUNT,
                                            max_link_credit => MaxLinkCredit,
                                            credit => MaxLinkCredit,
-                                           at_least_one_credit_req_in_flight => true,
-                                           stashed_credit_req => none}},
+                                           at_least_one_credit_req_in_flight => true}},
             handle_queue_actions(Actions, State2);
         {0, {error, autodelete}} ->
             exit({shutdown, autodelete});
@@ -323,10 +322,10 @@ handle_dest(_Msg, State) ->
     State.
 
 ack(DeliveryTag, Multiple, State) ->
-    maybe_grant_or_stash_credit(settle(complete, DeliveryTag, Multiple, State)).
+    maybe_grant_credit(settle(complete, DeliveryTag, Multiple, State)).
 
 nack(DeliveryTag, Multiple, State) ->
-    maybe_grant_or_stash_credit(settle(discard, DeliveryTag, Multiple, State)).
+    maybe_grant_credit(settle(discard, DeliveryTag, Multiple, State)).
 
 forward(Tag, Msg0, #{dest := #{current := #{queue_states := QState} = Current,
                                unacked := Unacked} = Dest,
@@ -426,7 +425,7 @@ handle_queue_actions(Actions, State) ->
       end, State, Actions).
 
 handle_deliver(AckRequired, Msgs, State) when is_list(Msgs) ->
-    maybe_grant_or_stash_credit(
+    maybe_grant_credit(
       lists:foldl(
         fun({_QName, _QPid, MsgId, _Redelivered, Mc}, S0) ->
                 DeliveryTag = next_tag(S0),
@@ -654,25 +653,17 @@ sent_delivery(#{source := #{delivery_count := DeliveryCount0,
                            delivery_count => DeliveryCount
                           }}.
 
-maybe_grant_or_stash_credit(#{source := #{queue_r := QName,
-                                    credit := Credit,
-                                    max_link_credit := MaxLinkCredit,
-                                    delivery_count := DeliveryCount,
-                                    at_least_one_credit_req_in_flight := HaveCreditReqInFlight,
-                                    current := #{consumer_tag := CTag,
-                                                 queue_states := QState0} = Current
-                                   } = Src,
-                        dest := #{unacked := Unacked}} = State0) ->
-    GrantLinkCredit = grant_link_credit(Credit, MaxLinkCredit, maps:size(Unacked)),
-    Src1 = case HaveCreditReqInFlight andalso GrantLinkCredit of
-               true ->
-                   Req = #credit_req {
-                            delivery_count = DeliveryCount
-                           },
-                   maps:put(stashed_credit_req, Req, Src);
-               false ->
-                   Src
-           end,
+maybe_grant_credit(#{source := #{queue_r := QName,
+                                 credit := Credit,
+                                 max_link_credit := MaxLinkCredit,
+                                 delivery_count := DeliveryCount,
+                                 at_least_one_credit_req_in_flight := HaveCreditReqInFlight,
+                                 current := #{consumer_tag := CTag,
+                                              queue_states := QState0,
+                                              unacked_message_q := Q} = Current
+                                } = Src,
+                     dest := #{unacked := Unacked}} = State0) ->
+    GrantLinkCredit = grant_link_credit(Credit, MaxLinkCredit, ?QUEUE:len(Q)),
     {ok, QState, Actions} = case (GrantLinkCredit and not HaveCreditReqInFlight) of
                                 true ->
                                     rabbit_queue_type:credit(
@@ -685,9 +676,9 @@ maybe_grant_or_stash_credit(#{source := #{queue_r := QName,
                             true -> true;
                             false -> HaveCreditReqInFlight
                         end,
-    State = State0#{source => Src1#{current => Current#{queue_states => QState},
-                                    at_least_one_credit_req_in_flight => CreditReqInFlight
-                                   }},
+    State = State0#{source => Src#{current => Current#{queue_states => QState},
+                                   at_least_one_credit_req_in_flight => CreditReqInFlight
+                                  }},
     handle_queue_actions(Actions, State).
 
 max_link_credit() ->
@@ -702,30 +693,22 @@ handle_credit_reply({credit_reply, CTag, DeliveryCount, Credit, _Available, _Dra
                     #{source := #{credit := CCredit,
                                   max_link_credit := MaxLinkCredit,
                                   delivery_count := QDeliveryCount,
-                                  stashed_credit_req := StashedCreditReq,
                                   queue_r := QName,
-                                  current := Current = #{queue_states := QState0}
+                                  current := Current = #{queue_states := QState0,
+                                                         unacked_message_q := Q}
                                  } = Src} = State0) ->
     %% Assertion: Our (receiver) delivery-count should be always
     %% in sync with the delivery-count of the sending queue.
     QDeliveryCount = DeliveryCount,
-    case StashedCreditReq of
-        #credit_req{delivery_count = StashedDeliveryCount} ->
-          {ok, QState, Actions} = rabbit_queue_type:credit(QName, CTag, StashedDeliveryCount,
-            MaxLinkCredit, false, QState0),
-          State = State0#{source => Src#{credit => MaxLinkCredit,
-            at_least_one_credit_req_in_flight => true,
-            stashed_credit_req => none,
-            current => Current#{queue_states => QState}}},
-          handle_queue_actions(Actions, State);
-        none when Credit =:= 0 andalso
-                  CCredit >= 0 ->
-            {ok, QState, Actions} = rabbit_queue_type:credit(QName, CTag, DeliveryCount, MaxLinkCredit, false, QState0),
+    case grant_link_credit(CCredit, MaxLinkCredit, ?QUEUE:len(Q)) of
+        true ->
+            {ok, QState, Actions} = rabbit_queue_type:credit(QName, CTag, QDeliveryCount,
+                                                             MaxLinkCredit, false, QState0),
             State = State0#{source => Src#{credit => MaxLinkCredit,
                                            at_least_one_credit_req_in_flight => true,
                                            current => Current#{queue_states => QState}}},
             handle_queue_actions(Actions, State);
-        none ->
+        false ->
             %% Although we (the receiver) usually determine link credit, we set here
             %% our link credit to what the queue says our link credit is (which is safer
             %% in case credit requests got applied out of order in quorum queues).

From 2763d3ed1af156995005294c9877f649037706f2 Mon Sep 17 00:00:00 2001
From: Diana Parra Corbacho 
Date: Thu, 14 Aug 2025 15:24:02 +0200
Subject: [PATCH 31/31] Local shovels: single acks

For some reason, multiple acknowledgments are really slow when using credit flow v2
---
 .../src/rabbit_local_shovel.erl               | 26 ++++++++++---------
 1 file changed, 14 insertions(+), 12 deletions(-)

diff --git a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
index 73335b6fff14..d3f98e5b2ac7 100644
--- a/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
+++ b/deps/rabbitmq_shovel/src/rabbit_local_shovel.erl
@@ -574,21 +574,23 @@ get_user_vhost_from_amqp_param(Uri) ->
 
 settle(Op, DeliveryTag, Multiple,
        #{source := #{queue_r := QRef,
-                     current := Current = #{queue_states := QState0,
-                                            consumer_tag := CTag,
+                     current := Current = #{consumer_tag := CTag,
                                             unacked_message_q := UAMQ0}
                     } = Src} = State0) ->
     {MsgIds, UAMQ} = collect_acks(UAMQ0, DeliveryTag, Multiple),
-    case rabbit_queue_type:settle(QRef, Op, CTag, MsgIds, QState0) of
-        {ok, QState1, Actions} ->
-            State = State0#{source => Src#{current => Current#{queue_states => QState1,
-                                                               unacked_message_q => UAMQ}}},
-            handle_queue_actions(Actions, State);
-        {'protocol_error', Type, Reason, Args} ->
-            ?LOG_ERROR("Shovel failed to settle ~p acknowledgments with ~tp: ~tp",
-                       [Op, Type, io_lib:format(Reason, Args)]),
-            exit({shutdown, {ack_failed, Reason}})
-    end.
+    State = State0#{source => Src#{current => Current#{unacked_message_q => UAMQ}}},
+    lists:foldl(
+      fun(MsgId, #{source := Src0 = #{current := Current0 = #{queue_states := QState0}}} = St0) ->
+              case rabbit_queue_type:settle(QRef, Op, CTag, [MsgId], QState0) of
+                  {ok, QState1, Actions} ->
+                      St = St0#{source => Src0#{current => Current0#{queue_states => QState1}}},
+                      handle_queue_actions(Actions, St);
+                  {'protocol_error', Type, Reason, Args} ->
+                      ?LOG_ERROR("Shovel failed to settle ~p acknowledgments with ~tp: ~tp",
+                                 [Op, Type, io_lib:format(Reason, Args)]),
+                      exit({shutdown, {ack_failed, Reason}})
+              end
+      end, State, MsgIds).
 
 %% From rabbit_channel
 %% Records a client-sent acknowledgement. Handles both single delivery acks