summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile2
-rw-r--r--rabbitmq-components.mk3
-rw-r--r--src/dtree.erl205
-rw-r--r--src/rabbit_amqqueue.erl4
-rw-r--r--src/rabbit_amqqueue_process.erl19
-rw-r--r--src/rabbit_channel.erl123
-rw-r--r--src/rabbit_msg_store.erl73
-rw-r--r--src/rabbit_msg_store_gc.erl16
-rw-r--r--src/rabbit_policies.erl2
-rw-r--r--src/rabbit_quorum_queue.erl50
-rw-r--r--src/unconfirmed_messages.erl280
-rw-r--r--test/confirms_rejects_SUITE.erl168
-rw-r--r--test/dead_lettering_SUITE.erl33
-rw-r--r--test/priority_queue_SUITE.erl24
-rw-r--r--test/queue_parallel_SUITE.erl27
-rw-r--r--test/quorum_queue_SUITE.erl11
-rw-r--r--test/simple_ha_SUITE.erl23
17 files changed, 739 insertions, 324 deletions
diff --git a/Makefile b/Makefile
index 4e61332513..f8a5c8cad9 100644
--- a/Makefile
+++ b/Makefile
@@ -135,7 +135,7 @@ endef
LOCAL_DEPS = sasl mnesia os_mon inets compiler public_key crypto ssl syntax_tools
BUILD_DEPS = rabbitmq_cli syslog
-DEPS = ranch lager rabbit_common ra sysmon_handler stdout_formatter
+DEPS = ranch lager rabbit_common ra sysmon_handler stdout_formatter recon observer_cli
TEST_DEPS = rabbitmq_ct_helpers rabbitmq_ct_client_helpers amqp_client meck proper
dep_syslog = git https://github.com/schlagert/syslog 3.4.5
diff --git a/rabbitmq-components.mk b/rabbitmq-components.mk
index 3a8d9a5a4f..0ff2ad0253 100644
--- a/rabbitmq-components.mk
+++ b/rabbitmq-components.mk
@@ -117,7 +117,8 @@ dep_lager = hex 3.6.10
dep_prometheus = git https://github.com/deadtrickster/prometheus.erl v4.3.0
dep_ra = git https://github.com/rabbitmq/ra.git master
dep_ranch = hex 1.7.1
-dep_recon = hex 2.4.0
+dep_recon = hex 2.5.0
+dep_observer_cli = hex 1.5.0
dep_stdout_formatter = hex 0.2.2
dep_sysmon_handler = hex 1.1.0
diff --git a/src/dtree.erl b/src/dtree.erl
deleted file mode 100644
index 6f4d102c10..0000000000
--- a/src/dtree.erl
+++ /dev/null
@@ -1,205 +0,0 @@
-%% The contents of this file are subject to the Mozilla Public License
-%% Version 1.1 (the "License"); you may not use this file except in
-%% compliance with the License. You may obtain a copy of the License
-%% at https://www.mozilla.org/MPL/
-%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and
-%% limitations under the License.
-%%
-%% The Original Code is RabbitMQ.
-%%
-%% The Initial Developer of the Original Code is GoPivotal, Inc.
-%% Copyright (c) 2007-2019 Pivotal Software, Inc. All rights reserved.
-%%
-
-%% A dual-index tree.
-%%
-%% Entries have the following shape:
-%%
-%% +----+--------------------+---+
-%% | PK | SK1, SK2, ..., SKN | V |
-%% +----+--------------------+---+
-%%
-%% i.e. a primary key, set of secondary keys, and a value.
-%%
-%% There can be only one entry per primary key, but secondary keys may
-%% appear in multiple entries.
-%%
-%% The set of secondary keys must be non-empty. Or, to put it another
-%% way, entries only exist while their secondary key set is non-empty.
-
--module(dtree).
-
--export([empty/0, insert/4, take/3, take/2, take_one/2, take_all/2, drop/2,
- is_defined/2, is_empty/1, smallest/1, size/1]).
-
-%%----------------------------------------------------------------------------
-
--export_type([?MODULE/0]).
-
--opaque ?MODULE() :: {gb_trees:tree(), gb_trees:tree()}.
-
--type pk() :: any().
--type sk() :: any().
--type val() :: any().
--type kv() :: {pk(), val()}.
-
-%%----------------------------------------------------------------------------
-
--spec empty() -> ?MODULE().
-
-empty() -> {gb_trees:empty(), gb_trees:empty()}.
-
-%% Insert an entry. Fails if there already is an entry with the given
-%% primary key.
-
--spec insert(pk(), [sk()], val(), ?MODULE()) -> ?MODULE().
-
-insert(PK, [], V, {P, S}) ->
- %% dummy insert to force error if PK exists
- _ = gb_trees:insert(PK, {gb_sets:empty(), V}, P),
- {P, S};
-insert(PK, SKs, V, {P, S}) ->
- {gb_trees:insert(PK, {gb_sets:from_list(SKs), V}, P),
- lists:foldl(fun (SK, S0) ->
- case gb_trees:lookup(SK, S0) of
- {value, PKS} -> PKS1 = gb_sets:insert(PK, PKS),
- gb_trees:update(SK, PKS1, S0);
- none -> PKS = gb_sets:singleton(PK),
- gb_trees:insert(SK, PKS, S0)
- end
- end, S, SKs)}.
-
-%% Remove the given secondary key from the entries of the given
-%% primary keys, returning the primary-key/value pairs of any entries
-%% that were dropped as the result (i.e. due to their secondary key
-%% set becoming empty). It is ok for the given primary keys and/or
-%% secondary key to not exist.
-
--spec take([pk()], sk(), ?MODULE()) -> {[kv()], ?MODULE()}.
-
-take(PKs, SK, {P, S}) ->
- case gb_trees:lookup(SK, S) of
- none -> {[], {P, S}};
- {value, PKS} -> TakenPKS = gb_sets:from_list(PKs),
- PKSInter = gb_sets:intersection(PKS, TakenPKS),
- PKSDiff = gb_sets_difference (PKS, PKSInter),
- {KVs, P1} = take2(PKSInter, SK, P),
- {KVs, {P1, case gb_sets:is_empty(PKSDiff) of
- true -> gb_trees:delete(SK, S);
- false -> gb_trees:update(SK, PKSDiff, S)
- end}}
- end.
-
-%% Remove the given secondary key from all entries, returning the
-%% primary-key/value pairs of any entries that were dropped as the
-%% result (i.e. due to their secondary key set becoming empty). It is
-%% ok for the given secondary key to not exist.
-
--spec take(sk(), ?MODULE()) -> {[kv()], ?MODULE()}.
-
-take(SK, {P, S}) ->
- case gb_trees:lookup(SK, S) of
- none -> {[], {P, S}};
- {value, PKS} -> {KVs, P1} = take2(PKS, SK, P),
- {KVs, {P1, gb_trees:delete(SK, S)}}
- end.
-
-%% Drop an entry with the primary key and clears secondary keys for this key,
-%% returning a list with a key-value pair as a result.
-%% If the primary key does not exist, returns an empty list.
-
--spec take_one(pk(), ?MODULE()) -> {[{pk(), val()}], ?MODULE()}.
-
-take_one(PK, {P, S}) ->
- case gb_trees:lookup(PK, P) of
- {value, {SKS, Value}} ->
- P1 = gb_trees:delete(PK, P),
- S1 = gb_sets:fold(
- fun(SK, Acc) ->
- {value, PKS} = gb_trees:lookup(SK, Acc),
- PKS1 = gb_sets:delete(PK, PKS),
- case gb_sets:is_empty(PKS1) of
- true -> gb_trees:delete(SK, Acc);
- false -> gb_trees:update(SK, PKS1, Acc)
- end
- end, S, SKS),
- {[{PK, Value}], {P1, S1}};
- none -> {[], {P, S}}
- end.
-
-%% Drop all entries which contain the given secondary key, returning
-%% the primary-key/value pairs of these entries. It is ok for the
-%% given secondary key to not exist.
-
--spec take_all(sk(), ?MODULE()) -> {[kv()], ?MODULE()}.
-
-take_all(SK, {P, S}) ->
- case gb_trees:lookup(SK, S) of
- none -> {[], {P, S}};
- {value, PKS} -> {KVs, SKS, P1} = take_all2(PKS, P),
- {KVs, {P1, prune(SKS, PKS, S)}}
- end.
-
-%% Drop all entries for the given primary key (which does not have to exist).
-
--spec drop(pk(), ?MODULE()) -> ?MODULE().
-
-drop(PK, {P, S}) ->
- case gb_trees:lookup(PK, P) of
- none -> {P, S};
- {value, {SKS, _V}} -> {gb_trees:delete(PK, P),
- prune(SKS, gb_sets:singleton(PK), S)}
- end.
-
--spec is_defined(sk(), ?MODULE()) -> boolean().
-
-is_defined(SK, {_P, S}) -> gb_trees:is_defined(SK, S).
-
--spec is_empty(?MODULE()) -> boolean().
-
-is_empty({P, _S}) -> gb_trees:is_empty(P).
-
--spec smallest(?MODULE()) -> kv().
-
-smallest({P, _S}) -> {K, {_SKS, V}} = gb_trees:smallest(P),
- {K, V}.
-
--spec size(?MODULE()) -> non_neg_integer().
-
-size({P, _S}) -> gb_trees:size(P).
-
-%%----------------------------------------------------------------------------
-
-take2(PKS, SK, P) ->
- gb_sets:fold(fun (PK, {KVs, P0}) ->
- {SKS, V} = gb_trees:get(PK, P0),
- SKS1 = gb_sets:delete(SK, SKS),
- case gb_sets:is_empty(SKS1) of
- true -> KVs1 = [{PK, V} | KVs],
- {KVs1, gb_trees:delete(PK, P0)};
- false -> {KVs, gb_trees:update(PK, {SKS1, V}, P0)}
- end
- end, {[], P}, PKS).
-
-take_all2(PKS, P) ->
- gb_sets:fold(fun (PK, {KVs, SKS0, P0}) ->
- {SKS, V} = gb_trees:get(PK, P0),
- {[{PK, V} | KVs], gb_sets:union(SKS, SKS0),
- gb_trees:delete(PK, P0)}
- end, {[], gb_sets:empty(), P}, PKS).
-
-prune(SKS, PKS, S) ->
- gb_sets:fold(fun (SK0, S0) ->
- PKS1 = gb_trees:get(SK0, S0),
- PKS2 = gb_sets_difference(PKS1, PKS),
- case gb_sets:is_empty(PKS2) of
- true -> gb_trees:delete(SK0, S0);
- false -> gb_trees:update(SK0, PKS2, S0)
- end
- end, S, SKS).
-
-gb_sets_difference(S1, S2) ->
- gb_sets:fold(fun gb_sets:delete_any/2, S1, S2).
diff --git a/src/rabbit_amqqueue.erl b/src/rabbit_amqqueue.erl
index 9e94dd8f27..85c647ae8c 100644
--- a/src/rabbit_amqqueue.erl
+++ b/src/rabbit_amqqueue.erl
@@ -761,7 +761,9 @@ check_dlxrk_arg({Type, _}, _Args) ->
{error, {unacceptable_type, Type}}.
check_overflow({longstr, Val}, _Args) ->
- case lists:member(Val, [<<"drop-head">>, <<"reject-publish">>]) of
+ case lists:member(Val, [<<"drop-head">>,
+ <<"reject-publish">>,
+ <<"reject-publish-dlx">>]) of
true -> ok;
false -> {error, invalid_overflow}
end;
diff --git a/src/rabbit_amqqueue_process.erl b/src/rabbit_amqqueue_process.erl
index b3f89b7ef0..2666847e77 100644
--- a/src/rabbit_amqqueue_process.erl
+++ b/src/rabbit_amqqueue_process.erl
@@ -82,7 +82,7 @@
%% max length in bytes, if configured
max_bytes,
%% an action to perform if queue is to be over a limit,
- %% can be either drop-head (default) or reject-publish
+ %% can be either drop-head (default), reject-publish or reject-publish-dlx
overflow,
%% when policies change, this version helps queue
%% determine what previously scheduled/set up state to ignore,
@@ -704,12 +704,25 @@ maybe_deliver_or_enqueue(Delivery = #delivery{message = Message},
Delivered,
State = #q{overflow = Overflow,
backing_queue = BQ,
- backing_queue_state = BQS}) ->
+ backing_queue_state = BQS,
+ dlx = DLX,
+ dlx_routing_key = RK}) ->
send_mandatory(Delivery), %% must do this before confirms
case {will_overflow(Delivery, State), Overflow} of
{true, 'reject-publish'} ->
%% Drop publish and nack to publisher
send_reject_publish(Delivery, Delivered, State);
+ {true, 'reject-publish-dlx'} ->
+ %% Publish to DLX
+ with_dlx(
+ DLX,
+ fun (X) ->
+ QName = qname(State),
+ rabbit_dead_letter:publish(Message, maxlen, X, RK, QName)
+ end,
+ fun () -> ok end),
+ %% Drop publish and nack to publisher
+ send_reject_publish(Delivery, Delivered, State);
_ ->
{IsDuplicate, BQS1} = BQ:is_duplicate(Message, BQS),
State1 = State#q{backing_queue_state = BQS1},
@@ -766,6 +779,8 @@ maybe_drop_head(State = #q{max_length = undefined,
{false, State};
maybe_drop_head(State = #q{overflow = 'reject-publish'}) ->
{false, State};
+maybe_drop_head(State = #q{overflow = 'reject-publish-dlx'}) ->
+ {false, State};
maybe_drop_head(State = #q{overflow = 'drop-head'}) ->
maybe_drop_head(false, State).
diff --git a/src/rabbit_channel.erl b/src/rabbit_channel.erl
index 19d4449e21..7c07aa07f3 100644
--- a/src/rabbit_channel.erl
+++ b/src/rabbit_channel.erl
@@ -155,7 +155,7 @@
confirm_enabled,
%% publisher confirm delivery tag sequence
publish_seqno,
- %% a dtree used to track unconfirmed
+ %% an unconfirmed_messages data structure used to track unconfirmed
%% (to publishers) messages
unconfirmed,
%% a list of tags for published messages that were
@@ -518,7 +518,7 @@ init([Channel, ReaderPid, WriterPid, ConnPid, ConnName, Protocol, User, VHost,
delivering_queues = sets:new(),
confirm_enabled = false,
publish_seqno = 1,
- unconfirmed = dtree:empty(),
+ unconfirmed = unconfirmed_messages:new(),
rejected = [],
confirmed = [],
reply_consumer = none,
@@ -707,10 +707,15 @@ handle_cast({mandatory_received, _MsgSeqNo}, State) ->
handle_cast({reject_publish, MsgSeqNo, _QPid}, State = #ch{unconfirmed = UC}) ->
%% It does not matter which queue rejected the message,
- %% if any queue rejected it - it should not be confirmed.
- {MXs, UC1} = dtree:take_one(MsgSeqNo, UC),
+ %% if any queue did, it should not be confirmed.
+ {MaybeRejected, UC1} = unconfirmed_messages:reject_msg(MsgSeqNo, UC),
%% NB: don't call noreply/1 since we don't want to send confirms.
- noreply_coalesce(record_rejects(MXs, State#ch{unconfirmed = UC1}));
+ case MaybeRejected of
+ not_confirmed ->
+ noreply_coalesce(State#ch{unconfirmed = UC1});
+ {rejected, MX} ->
+ noreply_coalesce(record_rejects([MX], State#ch{unconfirmed = UC1}))
+ end;
handle_cast({confirm, MsgSeqNos, QPid}, State) ->
noreply_coalesce(confirm(MsgSeqNos, QPid, State)).
@@ -766,9 +771,12 @@ handle_info({ra_event, {Name, _} = From, _} = Evt,
eol ->
State1 = handle_consuming_queue_down_or_eol(Name, State0),
State2 = handle_delivering_queue_down(Name, State1),
- %% TODO: this should use dtree:take/3
- {MXs, UC1} = dtree:take(Name, State2#ch.unconfirmed),
- State3 = record_confirms(MXs, State1#ch{unconfirmed = UC1}),
+ {ConfirmMXs, RejectMXs, UC1} =
+ unconfirmed_messages:forget_ref(Name, State2#ch.unconfirmed),
+ %% Deleted queue is a special case.
+ %% Do not nack the "rejected" messages.
+ State3 = record_confirms(ConfirmMXs ++ RejectMXs,
+ State2#ch{unconfirmed = UC1}),
erase_queue_stats(QName),
noreply_coalesce(
State3#ch{queue_states = maps:remove(Name, QueueStates),
@@ -776,7 +784,7 @@ handle_info({ra_event, {Name, _} = From, _} = Evt,
end;
_ ->
%% the assumption here is that the queue state has been cleaned up and
- %% this is a residual ra notification
+ %% this is a residual Ra notification
noreply_coalesce(State0)
end;
@@ -1818,14 +1826,33 @@ track_delivering_queue(NoAck, QPid, QName,
false -> sets:add_element(QRef, DQ)
end}.
-handle_publishing_queue_down(QPid, Reason, State = #ch{unconfirmed = UC})
+handle_publishing_queue_down(QPid, Reason,
+ State = #ch{unconfirmed = UC,
+ queue_names = QNames})
when ?IS_CLASSIC(QPid) ->
- case rabbit_misc:is_abnormal_exit(Reason) of
- true -> {MXs, UC1} = dtree:take_all(QPid, UC),
- record_rejects(MXs, State#ch{unconfirmed = UC1});
- false -> {MXs, UC1} = dtree:take(QPid, UC),
- record_confirms(MXs, State#ch{unconfirmed = UC1})
-
+ case maps:get(QPid, QNames, none) of
+ %% The queue is unknown, the confirm must have been processed already
+ none -> State;
+ _QName ->
+ case {rabbit_misc:is_abnormal_exit(Reason), Reason} of
+ {true, _} ->
+ {RejectMXs, UC1} =
+ unconfirmed_messages:reject_all_for_queue(QPid, UC),
+ record_rejects(RejectMXs, State#ch{unconfirmed = UC1});
+ {false, normal} ->
+ {ConfirmMXs, RejectMXs, UC1} =
+ unconfirmed_messages:forget_ref(QPid, UC),
+ %% Deleted queue is a special case.
+ %% Do not nack the "rejected" messages.
+ record_confirms(ConfirmMXs ++ RejectMXs,
+ State#ch{unconfirmed = UC1});
+ {false, _} ->
+ {ConfirmMXs, RejectMXs, UC1} =
+ unconfirmed_messages:forget_ref(QPid, UC),
+ State1 = record_confirms(ConfirmMXs,
+ State#ch{unconfirmed = UC1}),
+ record_rejects(RejectMXs, State1)
+ end
end;
handle_publishing_queue_down(QPid, _Reason, _State) when ?IS_QUORUM(QPid) ->
error(quorum_queues_should_never_be_monitored).
@@ -2095,13 +2122,13 @@ notify_limiter(Limiter, Acked) ->
%% common case.
case rabbit_limiter:is_active(Limiter) of
false -> ok;
- true -> case lists:foldl(fun ({_, CTag, _}, Acc) when is_integer(CTag) ->
+ true -> case lists:foldl(fun ({_, CTag, _, _}, Acc) when is_integer(CTag) ->
%% Quorum queues use integer CTags
%% classic queues use binaries
%% Quorum queues do not interact
%% with limiters
Acc;
- ({_, _, _}, Acc) -> Acc + 1
+ ({_, _, _, _}, Acc) -> Acc + 1
end, 0, Acked) of
0 -> ok;
Count -> rabbit_limiter:ack(Limiter, Count)
@@ -2139,21 +2166,33 @@ deliver_to_queues({Delivery = #delivery{message = Message = #basic_message{
%% frequently would in fact be more expensive in the common case.
{QNames1, QMons1} =
lists:foldl(fun (Q, {QNames0, QMons0}) when ?is_amqqueue(Q) ->
- QPid = amqqueue:get_pid(Q),
- QRef = qpid_to_ref(QPid),
- QName = amqqueue:get_name(Q),
- {case maps:is_key(QRef, QNames0) of
- true -> QNames0;
- false -> maps:put(QRef, QName, QNames0)
- end, maybe_monitor(QPid, QMons0)}
- end, {QNames, maybe_monitor_all(DeliveredQPids, QMons)}, Qs),
+ QPid = amqqueue:get_pid(Q),
+ QRef = qpid_to_ref(QPid),
+ QName = amqqueue:get_name(Q),
+ case ?IS_CLASSIC(QRef) of
+ true ->
+ SPids = amqqueue:get_slave_pids(Q),
+ NewQNames =
+ maps:from_list([{Ref, QName} || Ref <- [QRef | SPids]]),
+ {maps:merge(NewQNames, QNames0),
+ maybe_monitor_all([QPid | SPids], QMons0)};
+ false ->
+ {maps:put(QRef, QName, QNames0), QMons0}
+ end
+ end,
+ {QNames, QMons}, Qs),
State1 = State#ch{queue_names = QNames1,
queue_monitors = QMons1},
%% NB: the order here is important since basic.returns must be
%% sent before confirms.
ok = process_routing_mandatory(Mandatory, AllDeliveredQRefs,
Message, State1),
- State2 = process_routing_confirm(Confirm, AllDeliveredQRefs , MsgSeqNo,
+ AllDeliveredQNames = [ QName || QRef <- AllDeliveredQRefs,
+ {ok, QName} <- [maps:find(QRef, QNames1)]],
+ State2 = process_routing_confirm(Confirm,
+ AllDeliveredQRefs,
+ AllDeliveredQNames,
+ MsgSeqNo,
XName, State1),
case rabbit_event:stats_level(State, #ch.stats_timer) of
fine ->
@@ -2179,16 +2218,22 @@ process_routing_mandatory(_Mandatory = false,
process_routing_mandatory(_, _, _, _) ->
ok.
-process_routing_confirm(false, _, _, _, State) ->
+process_routing_confirm(false, _, _, _, _, State) ->
State;
-process_routing_confirm(true, [], MsgSeqNo, XName, State) ->
+process_routing_confirm(true, [], _, MsgSeqNo, XName, State) ->
record_confirms([{MsgSeqNo, XName}], State);
-process_routing_confirm(true, QRefs, MsgSeqNo, XName, State) ->
- State#ch{unconfirmed = dtree:insert(MsgSeqNo, QRefs, XName,
- State#ch.unconfirmed)}.
-
-confirm(MsgSeqNos, QRef, State = #ch{unconfirmed = UC}) ->
- {MXs, UC1} = dtree:take(MsgSeqNos, QRef, UC),
+process_routing_confirm(true, QRefs, QNames, MsgSeqNo, XName, State) ->
+ State#ch{unconfirmed =
+ unconfirmed_messages:insert(MsgSeqNo, QNames, QRefs, XName,
+ State#ch.unconfirmed)}.
+
+confirm(MsgSeqNos, QRef, State = #ch{queue_names = QNames, unconfirmed = UC}) ->
+ %% NOTE: if queue name does not exist here it's likely that the ref also
+ %% does not exist in unconfirmed messages.
+ %% Neither does the 'ignore' atom, so it's a reasonable fallback.
+ QName = maps:get(QRef, QNames, ignore),
+ {MXs, UC1} =
+ unconfirmed_messages:confirm_multiple_msg_ref(MsgSeqNos, QName, QRef, UC),
%% NB: don't call noreply/1 since we don't want to send confirms.
record_confirms(MXs, State#ch{unconfirmed = UC1}).
@@ -2250,9 +2295,9 @@ send_confirms(Cs, Rs, State) ->
coalesce_and_send(MsgSeqNos, NegativeMsgSeqNos, MkMsgFun, State = #ch{unconfirmed = UC}) ->
SMsgSeqNos = lists:usort(MsgSeqNos),
- UnconfirmedCutoff = case dtree:is_empty(UC) of
+ UnconfirmedCutoff = case unconfirmed_messages:is_empty(UC) of
true -> lists:last(SMsgSeqNos) + 1;
- false -> {SeqNo, _XName} = dtree:smallest(UC), SeqNo
+ false -> unconfirmed_messages:smallest(UC)
end,
Cutoff = lists:min([UnconfirmedCutoff | NegativeMsgSeqNos]),
{Ms, Ss} = lists:splitwith(fun(X) -> X < Cutoff end, SMsgSeqNos),
@@ -2271,7 +2316,7 @@ ack_len(Acks) -> lists:sum([length(L) || {ack, L} <- Acks]).
maybe_complete_tx(State = #ch{tx = {_, _}}) ->
State;
maybe_complete_tx(State = #ch{unconfirmed = UC}) ->
- case dtree:is_empty(UC) of
+ case unconfirmed_messages:is_empty(UC) of
false -> State;
true -> complete_tx(State#ch{confirmed = []})
end.
@@ -2311,7 +2356,7 @@ i(confirm, #ch{confirm_enabled = CE}) -> CE;
i(source, #ch{cfg = #conf{source = ChSrc}}) -> ChSrc;
i(name, State) -> name(State);
i(consumer_count, #ch{consumer_mapping = CM}) -> maps:size(CM);
-i(messages_unconfirmed, #ch{unconfirmed = UC}) -> dtree:size(UC);
+i(messages_unconfirmed, #ch{unconfirmed = UC}) -> unconfirmed_messages:size(UC);
i(messages_unacknowledged, #ch{unacked_message_q = UAMQ}) -> ?QUEUE:len(UAMQ);
i(messages_uncommitted, #ch{tx = {Msgs, _Acks}}) -> ?QUEUE:len(Msgs);
i(messages_uncommitted, #ch{}) -> 0;
diff --git a/src/rabbit_msg_store.erl b/src/rabbit_msg_store.erl
index 1ec5fe9e1a..e3b23cfbca 100644
--- a/src/rabbit_msg_store.erl
+++ b/src/rabbit_msg_store.erl
@@ -23,7 +23,7 @@
client_ref/1, close_all_indicated/1,
write/3, write_flow/3, read/2, contains/2, remove/2]).
--export([set_maximum_since_use/2, has_readers/2, combine_files/3,
+-export([set_maximum_since_use/2, combine_files/3,
delete_file/2]). %% internal
-export([transform_dir/3, force_recovery/2]). %% upgrade
@@ -1970,33 +1970,48 @@ cleanup_after_file_deletion(File,
%% garbage collection / compaction / aggregation -- external
%%----------------------------------------------------------------------------
--spec has_readers(non_neg_integer(), gc_state()) -> boolean().
-
-has_readers(File, #gc_state { file_summary_ets = FileSummaryEts }) ->
- [#file_summary { locked = true, readers = Count }] =
- ets:lookup(FileSummaryEts, File),
- Count /= 0.
-
-spec combine_files(non_neg_integer(), non_neg_integer(), gc_state()) ->
- deletion_thunk().
+ {ok, deletion_thunk()} | {defer, non_neg_integer()}.
combine_files(Source, Destination,
- State = #gc_state { file_summary_ets = FileSummaryEts,
- file_handles_ets = FileHandlesEts,
- dir = Dir,
- msg_store = Server }) ->
- [#file_summary {
+ State = #gc_state { file_summary_ets = FileSummaryEts }) ->
+ [#file_summary{locked = true} = SourceSummary] =
+ ets:lookup(FileSummaryEts, Source),
+
+ [#file_summary{locked = true} = DestinationSummary] =
+ ets:lookup(FileSummaryEts, Destination),
+
+ case {SourceSummary, DestinationSummary} of
+ {#file_summary{readers = 0}, #file_summary{readers = 0}} ->
+ {ok, do_combine_files(SourceSummary, DestinationSummary,
+ Source, Destination, State)};
+ _ ->
+ rabbit_log:debug("Asked to combine files ~p and ~p but they have active readers. Deferring.",
+ [Source, Destination]),
+ DeferredFiles = [FileSummary#file_summary.file
+ || FileSummary <- [SourceSummary, DestinationSummary],
+ FileSummary#file_summary.readers /= 0],
+ {defer, DeferredFiles}
+ end.
+
+do_combine_files(SourceSummary, DestinationSummary,
+ Source, Destination,
+ State = #gc_state { file_summary_ets = FileSummaryEts,
+ file_handles_ets = FileHandlesEts,
+ dir = Dir,
+ msg_store = Server }) ->
+ #file_summary {
readers = 0,
left = Destination,
valid_total_size = SourceValid,
file_size = SourceFileSize,
- locked = true }] = ets:lookup(FileSummaryEts, Source),
- [#file_summary {
+ locked = true } = SourceSummary,
+ #file_summary {
readers = 0,
right = Source,
valid_total_size = DestinationValid,
file_size = DestinationFileSize,
- locked = true }] = ets:lookup(FileSummaryEts, Destination),
+ locked = true } = DestinationSummary,
SourceName = filenum_to_name(Source),
DestinationName = filenum_to_name(Destination),
@@ -2053,22 +2068,30 @@ combine_files(Source, Destination,
{#file_summary.file_size, TotalValidData}]),
Reclaimed = SourceFileSize + DestinationFileSize - TotalValidData,
+ rabbit_log:debug("Combined segment files number ~p (source) and ~p (destination), reclaimed ~p bytes",
+ [Source, Destination, Reclaimed]),
gen_server2:cast(Server, {combine_files, Source, Destination, Reclaimed}),
safe_file_delete_fun(Source, Dir, FileHandlesEts).
--spec delete_file(non_neg_integer(), gc_state()) -> deletion_thunk().
+-spec delete_file(non_neg_integer(), gc_state()) -> {ok, deletion_thunk()} | {defer, non_neg_integer()}.
delete_file(File, State = #gc_state { file_summary_ets = FileSummaryEts,
file_handles_ets = FileHandlesEts,
dir = Dir,
msg_store = Server }) ->
- [#file_summary { valid_total_size = 0,
- locked = true,
- file_size = FileSize,
- readers = 0 }] = ets:lookup(FileSummaryEts, File),
- {[], 0} = load_and_vacuum_message_file(File, State),
- gen_server2:cast(Server, {delete_file, File, FileSize}),
- safe_file_delete_fun(File, Dir, FileHandlesEts).
+ case ets:lookup(FileSummaryEts, File) of
+ [#file_summary { valid_total_size = 0,
+ locked = true,
+ file_size = FileSize,
+ readers = 0 }] ->
+ {[], 0} = load_and_vacuum_message_file(File, State),
+ gen_server2:cast(Server, {delete_file, File, FileSize}),
+ {ok, safe_file_delete_fun(File, Dir, FileHandlesEts)};
+ [#file_summary{readers = Readers}] when Readers > 0 ->
+ rabbit_log:debug("Asked to delete file ~p but it has active readers. Deferring.",
+ [File]),
+ {defer, [File]}
+ end.
load_and_vacuum_message_file(File, State = #gc_state { dir = Dir }) ->
%% Messages here will be end-of-file at start-of-list
diff --git a/src/rabbit_msg_store_gc.erl b/src/rabbit_msg_store_gc.erl
index dfadd5586d..60702b5b95 100644
--- a/src/rabbit_msg_store_gc.erl
+++ b/src/rabbit_msg_store_gc.erl
@@ -119,15 +119,13 @@ attempt_action(Action, Files,
State = #state { pending_no_readers = Pending,
on_action = Thunks,
msg_store_state = MsgStoreState }) ->
- case [File || File <- Files,
- rabbit_msg_store:has_readers(File, MsgStoreState)] of
- [] -> State #state {
- on_action = lists:filter(
- fun (Thunk) -> not Thunk() end,
- [do_action(Action, Files, MsgStoreState) |
- Thunks]) };
- [File | _] -> Pending1 = maps:put(File, {Action, Files}, Pending),
- State #state { pending_no_readers = Pending1 }
+ case do_action(Action, Files, MsgStoreState) of
+ {ok, OkThunk} ->
+ State#state{on_action = lists:filter(fun (Thunk) -> not Thunk() end,
+ [OkThunk | Thunks])};
+ {defer, [File | _]} ->
+ Pending1 = maps:put(File, {Action, Files}, Pending),
+ State #state { pending_no_readers = Pending1 }
end.
do_action(combine, [Source, Destination], MsgStoreState) ->
diff --git a/src/rabbit_policies.erl b/src/rabbit_policies.erl
index 7878bed02d..c4f4226448 100644
--- a/src/rabbit_policies.erl
+++ b/src/rabbit_policies.erl
@@ -131,6 +131,8 @@ validate_policy0(<<"overflow">>, <<"drop-head">>) ->
ok;
validate_policy0(<<"overflow">>, <<"reject-publish">>) ->
ok;
+validate_policy0(<<"overflow">>, <<"reject-publish-dlx">>) ->
+ ok;
validate_policy0(<<"overflow">>, Value) ->
{error, "~p is not a valid overflow value", [Value]};
diff --git a/src/rabbit_quorum_queue.erl b/src/rabbit_quorum_queue.erl
index 8258584e3c..75bd9515f1 100644
--- a/src/rabbit_quorum_queue.erl
+++ b/src/rabbit_quorum_queue.erl
@@ -644,7 +644,6 @@ cluster_state(Name) ->
end.
-spec status(rabbit_types:vhost(), Name :: rabbit_misc:resource_name()) -> rabbit_types:infos() | {error, term()}.
-
status(Vhost, QueueName) ->
%% Handle not found queues
QName = #resource{virtual_host = Vhost, name = QueueName, kind = queue},
@@ -653,19 +652,52 @@ status(Vhost, QueueName) ->
{ok, Q} when ?amqqueue_is_classic(Q) ->
{error, classic_queue_not_supported};
{ok, Q} when ?amqqueue_is_quorum(Q) ->
- {_, Leader} = amqqueue:get_pid(Q),
Nodes = amqqueue:get_quorum_nodes(Q),
- Info = [{leader, Leader}, {members, Nodes}],
- case ets:lookup(ra_state, RName) of
- [{_, State}] ->
- [{local_state, State} | Info];
- [] ->
- Info
- end;
+ [begin
+ case get_sys_status({RName, N}) of
+ {ok, Sys} ->
+ {_, M} = lists:keyfind(ra_server_state, 1, Sys),
+ {_, RaftState} = lists:keyfind(raft_state, 1, Sys),
+ #{commit_index := Commit,
+ machine_version := MacVer,
+ current_term := Term,
+ log := #{last_index := Last,
+ snapshot_index := SnapIdx}} = M,
+ [{<<"Node Name">>, N},
+ {<<"Raft State">>, RaftState},
+ {<<"Log Index">>, Last},
+ {<<"Commit Index">>, Commit},
+ {<<"Snapshot Index">>, SnapIdx},
+ {<<"Term">>, Term},
+ {<<"Machine Version">>, MacVer}
+ ];
+ {error, Err} ->
+ [{<<"Node Name">>, N},
+ {<<"Raft State">>, Err},
+ {<<"Log Index">>, <<>>},
+ {<<"Commit Index">>, <<>>},
+ {<<"Snapshot Index">>, <<>>},
+ {<<"Term">>, <<>>},
+ {<<"Machine Version">>, <<>>}
+ ]
+ end
+ end || N <- Nodes];
{error, not_found} = E ->
E
end.
+get_sys_status(Proc) ->
+ try lists:nth(5, element(4, sys:get_status(Proc))) of
+ Sys -> {ok, Sys}
+ catch
+ _:Err when is_tuple(Err) ->
+ {error, element(1, Err)};
+ _:_ ->
+ {error, other}
+
+ end.
+
+
add_member(VHost, Name, Node) ->
QName = #resource{virtual_host = VHost, name = Name, kind = queue},
case rabbit_amqqueue:lookup(QName) of
diff --git a/src/unconfirmed_messages.erl b/src/unconfirmed_messages.erl
new file mode 100644
index 0000000000..63a504a239
--- /dev/null
+++ b/src/unconfirmed_messages.erl
@@ -0,0 +1,280 @@
+%% The contents of this file are subject to the Mozilla Public License
+%% Version 1.1 (the "License"); you may not use this file except in
+%% compliance with the License. You may obtain a copy of the License
+%% at https://www.mozilla.org/MPL/
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and
+%% limitations under the License.
+%%
+%% The Original Code is RabbitMQ.
+%%
+%% The Initial Developer of the Original Code is GoPivotal, Inc.
+%% Copyright (c) 2007-2019 Pivotal Software, Inc. All rights reserved.
+%%
+
+%% Unconfirmed messages tracking.
+%%
+%% A message should be confirmed to the publisher only when all queues confirm.
+%%
+%% Messages are published to multiple queues while each queue may be
+%% represented by several processes (queue refs).
+%%
+%% Queue refs return confirmations, rejections, can fail or disconnect.
+%% If a queue ref fails, messgae should be rejected.
+%% If all queue refs for a queue disconnect (not fail) without confirmation,
+%% messge should be rejected.
+%%
+%% For simplicity, disconnects do not return a reject until all message refs
+%% confirm or disconnect.
+
+-module(unconfirmed_messages).
+
+-export([new/0,
+ insert/5,
+ confirm_msg_ref/4,
+ confirm_multiple_msg_ref/4,
+ forget_ref/2,
+
+ reject_msg/2,
+ reject_all_for_queue/2,
+
+ smallest/1,
+ size/1,
+ is_empty/1]).
+
+%%----------------------------------------------------------------------------
+
+-export_type([?MODULE/0]).
+-define(SET_VALUE, []).
+
+-type queue_ref() :: term().
+-type msg_id() :: term().
+-type queue_name() :: rabbit_amqqueue:name().
+-type exchange_name() :: rabbit_exchange:name().
+-type map_set(Type) :: #{Type => ?SET_VALUE}.
+
+-record(msg_status, {
+ %% a set of refs waiting for confirm
+ refs = #{} :: map_set(queue_ref()),
+ %% shows which queues had at least one confirmation
+ queue_status = #{} :: #{queue_name() => confirmed | rejected},
+ exchange :: exchange_name()
+}).
+
+-record(unconfirmed, {
+ %% needed to get unconfirmed cutoff
+ ordered = gb_sets:new() :: gb_sets:set(msg_id()),
+ %% contains message statuses of all message IDs
+ index = #{} :: #{msg_id() => #msg_status{}},
+ %% needed to look up message IDs for a queue ref
+ reverse = #{} :: #{queue_ref() => #{msg_id() => ?SET_VALUE}}
+}).
+
+-opaque ?MODULE() :: #unconfirmed{}.
+
+%%----------------------------------------------------------------------------
+
+-spec new() -> ?MODULE().
+new() -> #unconfirmed{}.
+
+%% Insert an entry for the message ID. Fails if there already is
+%% an entry with the given ID.
+-spec insert(msg_id(), [queue_name()], [queue_ref()], exchange_name(), ?MODULE()) -> ?MODULE().
+insert(MsgId, QueueNames, QueueRefs, XName,
+ #unconfirmed{ordered = Ordered,
+ index = Index,
+ reverse = Reverse} = UC) ->
+ case maps:get(MsgId, Index, none) of
+ none ->
+ UC#unconfirmed{
+ ordered = gb_sets:add(MsgId, Ordered),
+ index =
+ Index#{MsgId =>
+ #msg_status{
+ refs = maps:from_list([{QR, ?SET_VALUE} || QR <- QueueRefs]),
+ queue_status = maps:from_list([{QN, rejected} || QN <- QueueNames]),
+ exchange = XName}},
+ reverse = lists:foldl(
+ fun
+ (Ref, R) ->
+ case R of
+ #{Ref := MsgIdsSet} ->
+ R#{Ref => MsgIdsSet#{MsgId => ?SET_VALUE}};
+ _ ->
+ R#{Ref => #{MsgId => ?SET_VALUE}}
+ end
+ end,
+ Reverse, QueueRefs)
+ };
+ _ ->
+ error({message_already_exists, MsgId, QueueNames, QueueRefs, XName, UC})
+ end.
+
+%% Confirms a message on behalf of the given queue. If it was the last queue (ref)
+%% on the waiting list, returns 'confirmed' and performs the necessary cleanup.
+-spec confirm_msg_ref(msg_id(), queue_name(), queue_ref(), ?MODULE()) ->
+ {{confirmed | rejected, {msg_id(), exchange_name()}} | not_confirmed, ?MODULE()}.
+confirm_msg_ref(MsgId, QueueName, QueueRef,
+ #unconfirmed{reverse = Reverse} = UC) ->
+ remove_msg_ref(confirm, MsgId, QueueName, QueueRef,
+ UC#unconfirmed{reverse = remove_from_reverse(QueueRef, [MsgId], Reverse)}).
+
+-spec confirm_multiple_msg_ref(msg_id(), queue_name(), queue_ref(), ?MODULE()) ->
+ {{confirmed | rejected, {msg_id(), exchange_name()}} | not_confirmed, ?MODULE()}.
+confirm_multiple_msg_ref(MsgIds, QueueName, QueueRef,
+ #unconfirmed{reverse = Reverse} = UC0) ->
+ lists:foldl(
+ fun(MsgId, {C, UC}) ->
+ case remove_msg_ref(confirm, MsgId, QueueName, QueueRef, UC) of
+ {{confirmed, V}, UC1} -> {[V | C], UC1};
+ {not_confirmed, UC1} -> {C, UC1}
+ end
+ end,
+ {[], UC0#unconfirmed{reverse = remove_from_reverse(QueueRef, MsgIds, Reverse)}},
+ MsgIds).
+
+%% Removes all messages for a queue.
+%% Returns lists of confirmed and rejected messages.
+%%
+%% If there are no more refs left for the message, either
+%% 'confirmed' or 'rejected'.
+%% 'confirmed' is returned if all queues have confirmed the message.
+-spec forget_ref(queue_ref(), ?MODULE()) ->
+ {Confirmed :: [{msg_id(), exchange_name()}],
+ Rejected :: [{msg_id(), exchange_name()}],
+ ?MODULE()}.
+forget_ref(QueueRef, #unconfirmed{reverse = Reverse0} = UC0) ->
+ MsgIds = maps:keys(maps:get(QueueRef, Reverse0, #{})),
+ lists:foldl(fun(MsgId, {C, R, UC}) ->
+ case remove_msg_ref(no_confirm, MsgId, ignore, QueueRef, UC) of
+ {not_confirmed, UC1} -> {C, R, UC1};
+ {{confirmed, V}, UC1} -> {[V | C], R, UC1};
+ {{rejected, V}, UC1} -> {C, [V | R], UC1}
+ end
+ end,
+ {[], [], UC0#unconfirmed{reverse = maps:remove(QueueRef, Reverse0)}},
+ MsgIds).
+
+%% Rejects a single message with the given ID.
+%% Returns 'rejected' if there was a message with
+%% such ID.
+-spec reject_msg(msg_id(), ?MODULE()) ->
+ {{rejected, {msg_id(), exchange_name()}} | not_confirmed, ?MODULE()}.
+reject_msg(MsgId, #unconfirmed{ordered = Ordered, index = Index, reverse = Reverse} = UC) ->
+ case maps:get(MsgId, Index, none) of
+ none ->
+ {not_confirmed, UC};
+ #msg_status{exchange = XName,
+ refs = Refs} ->
+ {{rejected, {MsgId, XName}},
+ UC#unconfirmed{ordered = gb_sets:del_element(MsgId, Ordered),
+ index = maps:remove(MsgId, Index),
+ reverse = remove_multiple_from_reverse(maps:keys(Refs), [MsgId], Reverse)}}
+ end.
+
+%% Rejects all pending messages for a queue.
+-spec reject_all_for_queue(queue_ref(), ?MODULE()) ->
+ {Rejected :: [{msg_id(), exchange_name()}], ?MODULE()}.
+reject_all_for_queue(QueueRef, #unconfirmed{reverse = Reverse0} = UC0) ->
+ MsgIds = maps:keys(maps:get(QueueRef, Reverse0, #{})),
+ lists:foldl(fun(MsgId, {R, UC}) ->
+ case reject_msg(MsgId, UC) of
+ {not_confirmed, UC1} -> {R, UC1};
+ {{rejected, V}, UC1} -> {[V | R], UC1}
+ end
+ end,
+ {[], UC0#unconfirmed{reverse = maps:remove(QueueRef, Reverse0)}},
+ MsgIds).
+
+%% Returns a smallest message id.
+-spec smallest(?MODULE()) -> msg_id().
+smallest(#unconfirmed{ordered = Ordered}) ->
+ gb_sets:smallest(Ordered).
+
+-spec size(?MODULE()) -> msg_id().
+size(#unconfirmed{index = Index}) -> maps:size(Index).
+
+-spec is_empty(?MODULE()) -> boolean().
+is_empty(#unconfirmed{index = Index, reverse = Reverse, ordered = Ordered} = UC) ->
+ case maps:size(Index) == 0 of
+ true ->
+ %% Assertion
+ case maps:size(Reverse) == gb_sets:size(Ordered)
+ andalso
+ maps:size(Reverse) == 0 of
+ true -> ok;
+ false -> error({size_mismatch, UC})
+ end,
+ true;
+ _ ->
+ false
+ end.
+
+-spec remove_from_reverse(queue_ref(), [msg_id()],
+ #{queue_ref() => #{msg_id() => ?SET_VALUE}}) ->
+ #{queue_ref() => #{msg_id() => ?SET_VALUE}}.
+remove_from_reverse(QueueRef, MsgIds, Reverse) when is_list(MsgIds) ->
+ case maps:get(QueueRef, Reverse, none) of
+ none ->
+ Reverse;
+ MsgIdsSet ->
+ NewMsgIdsSet = maps:without(MsgIds, MsgIdsSet),
+ case maps:size(NewMsgIdsSet) > 0 of
+ true -> Reverse#{QueueRef => NewMsgIdsSet};
+ false -> maps:remove(QueueRef, Reverse)
+ end
+ end.
+
+-spec remove_multiple_from_reverse([queue_ref()], [msg_id()],
+ #{queue_ref() => #{msg_id() => ?SET_VALUE}}) ->
+ #{queue_ref() => #{msg_id() => ?SET_VALUE}}.
+remove_multiple_from_reverse(Refs, MsgIds, Reverse0) ->
+ lists:foldl(
+ fun(Ref, Reverse) ->
+ remove_from_reverse(Ref, MsgIds, Reverse)
+ end,
+ Reverse0,
+ Refs).
+
+-spec remove_msg_ref(confirm | no_confirm, msg_id(), queue_name(), queue_ref(), ?MODULE()) ->
+ {{confirmed | rejected, {msg_id(), exchange_name()}} | not_confirmed,
+ ?MODULE()}.
+remove_msg_ref(Confirm, MsgId, QueueName, QueueRef,
+ #unconfirmed{ordered = Ordered, index = Index} = UC) ->
+ case maps:get(MsgId, Index, none) of
+ none ->
+ {not_confirmed, UC};
+ #msg_status{refs = #{QueueRef := ?SET_VALUE} = Refs,
+ queue_status = QStatus,
+ exchange = XName} = MsgStatus ->
+ QStatus1 = case {Confirm, QueueName} of
+ {no_confirm, _} -> QStatus;
+ {_, ignore} -> QStatus;
+ {confirm, _} -> QStatus#{QueueName => confirmed}
+ end,
+ case maps:size(Refs) == 1 of
+ true ->
+ {{confirm_status(QStatus1), {MsgId, XName}},
+ UC#unconfirmed{
+ ordered = gb_sets:del_element(MsgId, Ordered),
+ index = maps:remove(MsgId, Index)}};
+ false ->
+ {not_confirmed,
+ UC#unconfirmed{
+ index = Index#{MsgId =>
+ MsgStatus#msg_status{
+ refs = maps:remove(QueueRef, Refs),
+ queue_status = QStatus1}}}}
+ end;
+ _ -> {not_confirmed, UC}
+ end.
+
+-spec confirm_status(#{queue_name() => confirmed | rejected}) -> confirmed | rejected.
+confirm_status(QueueStatus) ->
+ case lists:all(fun(confirmed) -> true; (_) -> false end,
+ maps:values(QueueStatus)) of
+ true -> confirmed;
+ false -> rejected
+ end.
diff --git a/test/confirms_rejects_SUITE.erl b/test/confirms_rejects_SUITE.erl
index 722b5240fc..402bca8737 100644
--- a/test/confirms_rejects_SUITE.erl
+++ b/test/confirms_rejects_SUITE.erl
@@ -11,11 +11,17 @@ all() ->
].
groups() ->
+ OverflowTests = [
+ confirms_rejects_conflict,
+ policy_resets_to_default
+ ],
[
{parallel_tests, [parallel], [
- confirms_rejects_conflict,
- policy_resets_to_default
- ]}
+ {overflow_reject_publish_dlx, [parallel], OverflowTests},
+ {overflow_reject_publish, [parallel], OverflowTests},
+ dead_queue_rejects,
+ mixed_dead_alive_queues_reject
+ ]}
].
init_per_suite(Config) ->
@@ -26,6 +32,14 @@ end_per_suite(Config) ->
rabbit_ct_helpers:run_teardown_steps(Config).
+init_per_group(overflow_reject_publish, Config) ->
+ rabbit_ct_helpers:set_config(Config, [
+ {overflow, <<"reject-publish">>}
+ ]);
+init_per_group(overflow_reject_publish_dlx, Config) ->
+ rabbit_ct_helpers:set_config(Config, [
+ {overflow, <<"reject-publish-dlx">>}
+ ]);
init_per_group(Group, Config) ->
ClusterSize = 2,
Config1 = rabbit_ct_helpers:set_config(Config, [
@@ -36,6 +50,10 @@ init_per_group(Group, Config) ->
rabbit_ct_broker_helpers:setup_steps() ++
rabbit_ct_client_helpers:setup_steps()).
+end_per_group(overflow_reject_publish, _Config) ->
+ ok;
+end_per_group(overflow_reject_publish_dlx, _Config) ->
+ ok;
end_per_group(_Group, Config) ->
rabbit_ct_helpers:run_steps(Config,
rabbit_ct_client_helpers:teardown_steps() ++
@@ -45,7 +63,10 @@ init_per_testcase(policy_resets_to_default = Testcase, Config) ->
Conn = rabbit_ct_client_helpers:open_unmanaged_connection(Config),
rabbit_ct_helpers:testcase_started(
rabbit_ct_helpers:set_config(Config, [{conn, Conn}]), Testcase);
-init_per_testcase(confirms_rejects_conflict = Testcase, Config) ->
+init_per_testcase(Testcase, Config)
+ when Testcase == confirms_rejects_conflict;
+ Testcase == dead_queue_rejects;
+ Testcase == mixed_dead_alive_queues_reject ->
Conn = rabbit_ct_client_helpers:open_unmanaged_connection(Config),
Conn1 = rabbit_ct_client_helpers:open_unmanaged_connection(Config),
@@ -55,7 +76,9 @@ init_per_testcase(confirms_rejects_conflict = Testcase, Config) ->
end_per_testcase(policy_resets_to_default = Testcase, Config) ->
{_, Ch} = rabbit_ct_client_helpers:open_connection_and_channel(Config, 0),
- amqp_channel:call(Ch, #'queue.delete'{queue = <<"policy_resets_to_default">>}),
+ XOverflow = ?config(overflow, Config),
+ QueueName = <<"policy_resets_to_default", "_", XOverflow/binary>>,
+ amqp_channel:call(Ch, #'queue.delete'{queue = QueueName}),
rabbit_ct_client_helpers:close_channels_and_connection(Config, 0),
Conn = ?config(conn, Config),
@@ -65,7 +88,22 @@ end_per_testcase(policy_resets_to_default = Testcase, Config) ->
rabbit_ct_helpers:testcase_finished(Config, Testcase);
end_per_testcase(confirms_rejects_conflict = Testcase, Config) ->
{_, Ch} = rabbit_ct_client_helpers:open_connection_and_channel(Config, 0),
- amqp_channel:call(Ch, #'queue.delete'{queue = <<"confirms_rejects_conflict">>}),
+ XOverflow = ?config(overflow, Config),
+ QueueName = <<"confirms_rejects_conflict", "_", XOverflow/binary>>,
+ amqp_channel:call(Ch, #'queue.delete'{queue = QueueName}),
+ end_per_testcase0(Testcase, Config);
+end_per_testcase(dead_queue_rejects = Testcase, Config) ->
+ {_, Ch} = rabbit_ct_client_helpers:open_connection_and_channel(Config, 0),
+ amqp_channel:call(Ch, #'queue.delete'{queue = <<"dead_queue_rejects">>}),
+ end_per_testcase0(Testcase, Config);
+end_per_testcase(mixed_dead_alive_queues_reject = Testcase, Config) ->
+ {_, Ch} = rabbit_ct_client_helpers:open_connection_and_channel(Config, 0),
+ amqp_channel:call(Ch, #'queue.delete'{queue = <<"mixed_dead_alive_queues_reject_dead">>}),
+ amqp_channel:call(Ch, #'queue.delete'{queue = <<"mixed_dead_alive_queues_reject_alive">>}),
+ amqp_channel:call(Ch, #'exchange.delete'{exchange = <<"mixed_dead_alive_queues_reject">>}),
+ end_per_testcase0(Testcase, Config).
+
+end_per_testcase0(Testcase, Config) ->
rabbit_ct_client_helpers:close_channels_and_connection(Config, 0),
Conn = ?config(conn, Config),
@@ -74,9 +112,90 @@ end_per_testcase(confirms_rejects_conflict = Testcase, Config) ->
rabbit_ct_client_helpers:close_connection(Conn),
rabbit_ct_client_helpers:close_connection(Conn1),
+ clean_acks_mailbox(),
+
rabbit_ct_helpers:testcase_finished(Config, Testcase).
+dead_queue_rejects(Config) ->
+ Conn = ?config(conn, Config),
+ {ok, Ch} = amqp_connection:open_channel(Conn),
+ QueueName = <<"dead_queue_rejects">>,
+ amqp_channel:call(Ch, #'confirm.select'{}),
+ amqp_channel:register_confirm_handler(Ch, self()),
+
+ amqp_channel:call(Ch, #'queue.declare'{queue = QueueName,
+ durable = true}),
+
+ amqp_channel:call(Ch, #'basic.publish'{routing_key = QueueName},
+ #amqp_msg{payload = <<"HI">>}),
+
+ receive
+ {'basic.ack',_,_} -> ok
+ after 10000 ->
+ error(timeout_waiting_for_initial_ack)
+ end,
+ kill_the_queue(QueueName, Config),
+
+ amqp_channel:call(Ch, #'basic.publish'{routing_key = QueueName},
+ #amqp_msg{payload = <<"HI">>}),
+
+ receive
+ {'basic.ack',_,_} -> error(expecting_nack_got_ack);
+ {'basic.nack',_,_,_} -> ok
+ after 10000 ->
+ error(timeout_waiting_for_nack)
+ end.
+
+mixed_dead_alive_queues_reject(Config) ->
+ Conn = ?config(conn, Config),
+ {ok, Ch} = amqp_connection:open_channel(Conn),
+ QueueNameDead = <<"mixed_dead_alive_queues_reject_dead">>,
+ QueueNameAlive = <<"mixed_dead_alive_queues_reject_alive">>,
+ ExchangeName = <<"mixed_dead_alive_queues_reject">>,
+
+ amqp_channel:call(Ch, #'confirm.select'{}),
+ amqp_channel:register_confirm_handler(Ch, self()),
+
+ amqp_channel:call(Ch, #'queue.declare'{queue = QueueNameDead,
+ durable = true}),
+ amqp_channel:call(Ch, #'queue.declare'{queue = QueueNameAlive,
+ durable = true}),
+
+ amqp_channel:call(Ch, #'exchange.declare'{exchange = ExchangeName,
+ durable = true}),
+
+ amqp_channel:call(Ch, #'queue.bind'{exchange = ExchangeName,
+ queue = QueueNameAlive,
+ routing_key = <<"route">>}),
+
+ amqp_channel:call(Ch, #'queue.bind'{exchange = ExchangeName,
+ queue = QueueNameDead,
+ routing_key = <<"route">>}),
+
+ amqp_channel:call(Ch, #'basic.publish'{exchange = ExchangeName,
+ routing_key = <<"route">>},
+ #amqp_msg{payload = <<"HI">>}),
+
+ receive
+ {'basic.ack',_,_} -> ok;
+ {'basic.nack',_,_,_} -> error(expecting_ack_got_nack)
+ after 50000 ->
+ error({timeout_waiting_for_initial_ack, process_info(self(), messages)})
+ end,
+
+ kill_the_queue(QueueNameDead, Config),
+
+ amqp_channel:call(Ch, #'basic.publish'{exchange = ExchangeName,
+ routing_key = <<"route">>},
+ #amqp_msg{payload = <<"HI">>}),
+
+ receive
+ {'basic.nack',_,_,_} -> ok;
+ {'basic.ack',_,_} -> error(expecting_nack_got_ack)
+ after 50000 ->
+ error({timeout_waiting_for_nack, process_info(self(), messages)})
+ end.
confirms_rejects_conflict(Config) ->
Conn = ?config(conn, Config),
@@ -88,15 +207,15 @@ confirms_rejects_conflict(Config) ->
false = Conn =:= Conn1,
false = Ch =:= Ch1,
- QueueName = <<"confirms_rejects_conflict">>,
-
amqp_channel:call(Ch, #'confirm.select'{}),
amqp_channel:register_confirm_handler(Ch, self()),
+ XOverflow = ?config(overflow, Config),
+ QueueName = <<"confirms_rejects_conflict", "_", XOverflow/binary>>,
amqp_channel:call(Ch, #'queue.declare'{queue = QueueName,
durable = true,
- arguments = [{<<"x-max-length">>,long,12},
- {<<"x-overflow">>,longstr,<<"reject-publish">>}]
+ arguments = [{<<"x-max-length">>, long, 12},
+ {<<"x-overflow">>, longstr, XOverflow}]
}),
%% Consume 3 messages at once. Do that often.
Consume = fun Consume() ->
@@ -139,12 +258,14 @@ confirms_rejects_conflict(Config) ->
policy_resets_to_default(Config) ->
Conn = ?config(conn, Config),
+
{ok, Ch} = amqp_connection:open_channel(Conn),
- QueueName = <<"policy_resets_to_default">>,
amqp_channel:call(Ch, #'confirm.select'{}),
amqp_channel:register_confirm_handler(Ch, self()),
+ XOverflow = ?config(overflow, Config),
+ QueueName = <<"policy_resets_to_default", "_", XOverflow/binary>>,
amqp_channel:call(Ch, #'queue.declare'{queue = QueueName,
durable = true
}),
@@ -152,7 +273,7 @@ policy_resets_to_default(Config) ->
rabbit_ct_broker_helpers:set_policy(
Config, 0,
QueueName, QueueName, <<"queues">>,
- [{<<"max-length">>, MaxLength}, {<<"overflow">>, <<"reject-publish">>}]),
+ [{<<"max-length">>, MaxLength}, {<<"overflow">>, XOverflow}]),
timer:sleep(1000),
@@ -256,3 +377,26 @@ clean_acks_mailbox() ->
after
1000 -> done
end.
+
+kill_the_queue(QueueName, Config) ->
+ rabbit_ct_broker_helpers:rpc(Config, 0, ?MODULE, kill_the_queue, [QueueName]).
+
+kill_the_queue(QueueName) ->
+ [begin
+ {ok, Q} = rabbit_amqqueue:lookup({resource, <<"/">>, queue, QueueName}),
+ Pid = amqqueue:get_pid(Q),
+ exit(Pid, kill)
+ end
+ || _ <- lists:seq(1, 11)],
+ {ok, Q} = rabbit_amqqueue:lookup({resource, <<"/">>, queue, QueueName}),
+ Pid = amqqueue:get_pid(Q),
+ case is_process_alive(Pid) of
+ %% Try to kill it again
+ true -> kill_the_queue(QueueName);
+ false -> ok
+ end.
+
+
+
+
+
diff --git a/test/dead_lettering_SUITE.erl b/test/dead_lettering_SUITE.erl
index 6fe2a3a522..fe5e91c8ed 100644
--- a/test/dead_lettering_SUITE.erl
+++ b/test/dead_lettering_SUITE.erl
@@ -60,13 +60,15 @@ groups() ->
{dead_letter_tests, [],
[
{classic_queue, [parallel], DeadLetterTests ++ [dead_letter_ttl,
+ dead_letter_max_length_reject_publish_dlx,
dead_letter_routing_key_cycle_ttl,
dead_letter_headers_reason_expired,
dead_letter_headers_reason_expired_per_message]},
{mirrored_queue, [parallel], DeadLetterTests ++ [dead_letter_ttl,
- dead_letter_routing_key_cycle_ttl,
- dead_letter_headers_reason_expired,
- dead_letter_headers_reason_expired_per_message]},
+ dead_letter_max_length_reject_publish_dlx,
+ dead_letter_routing_key_cycle_ttl,
+ dead_letter_headers_reason_expired,
+ dead_letter_headers_reason_expired_per_message]},
{quorum_queue, [parallel], DeadLetterTests}
]}
].
@@ -381,6 +383,31 @@ dead_letter_max_length_drop_head(Config) ->
_ = consume(Ch, DLXQName, [P1, P2]),
consume_empty(Ch, DLXQName).
+%% Another strategy: reject-publish-dlx
+dead_letter_max_length_reject_publish_dlx(Config) ->
+ {_Conn, Ch} = rabbit_ct_client_helpers:open_connection_and_channel(Config, 0),
+ QName = ?config(queue_name, Config),
+ DLXQName = ?config(queue_name_dlx, Config),
+
+ declare_dead_letter_queues(Ch, Config, QName, DLXQName,
+ [{<<"x-max-length">>, long, 1},
+ {<<"x-overflow">>, longstr, <<"reject-publish-dlx">>}]),
+
+ P1 = <<"msg1">>,
+ P2 = <<"msg2">>,
+ P3 = <<"msg3">>,
+
+ %% Publish 3 messages
+ publish(Ch, QName, [P1, P2, P3]),
+ %% Consume the first one from the queue (max-length = 1)
+ wait_for_messages(Config, [[QName, <<"1">>, <<"1">>, <<"0">>]]),
+ _ = consume(Ch, QName, [P1]),
+ consume_empty(Ch, QName),
+ %% Consume the dropped ones from the dead letter queue
+ wait_for_messages(Config, [[DLXQName, <<"2">>, <<"2">>, <<"0">>]]),
+ _ = consume(Ch, DLXQName, [P2, P3]),
+ consume_empty(Ch, DLXQName).
+
%% Dead letter exchange does not have to be declared when the queue is declared, but it should
%% exist by the time messages need to be dead-lettered; if it is missing then, the messages will
%% be silently dropped.
diff --git a/test/priority_queue_SUITE.erl b/test/priority_queue_SUITE.erl
index 5bac8482fa..f7e20a9fe6 100644
--- a/test/priority_queue_SUITE.erl
+++ b/test/priority_queue_SUITE.erl
@@ -33,7 +33,8 @@ groups() ->
{cluster_size_2, [], [
ackfold,
drop,
- reject,
+ {overflow_reject_publish, [], [reject]},
+ {overflow_reject_publish_dlx, [], [reject]},
dropwhile_fetchwhile,
info_head_message_timestamp,
matching,
@@ -87,8 +88,20 @@ init_per_group(cluster_size_3, Config) ->
]),
rabbit_ct_helpers:run_steps(Config1,
rabbit_ct_broker_helpers:setup_steps() ++
- rabbit_ct_client_helpers:setup_steps()).
-
+ rabbit_ct_client_helpers:setup_steps());
+init_per_group(overflow_reject_publish, Config) ->
+ rabbit_ct_helpers:set_config(Config, [
+ {overflow, <<"reject-publish">>}
+ ]);
+init_per_group(overflow_reject_publish_dlx, Config) ->
+ rabbit_ct_helpers:set_config(Config, [
+ {overflow, <<"reject-publish-dlx">>}
+ ]).
+
+end_per_group(overflow_reject_publish, _Config) ->
+ ok;
+end_per_group(overflow_reject_publish_dlx, _Config) ->
+ ok;
end_per_group(_Group, Config) ->
rabbit_ct_helpers:run_steps(Config,
rabbit_ct_client_helpers:teardown_steps() ++
@@ -334,9 +347,10 @@ drop(Config) ->
reject(Config) ->
{Conn, Ch} = rabbit_ct_client_helpers:open_connection_and_channel(Config, 0),
- Q = <<"reject-queue">>,
+ XOverflow = ?config(overflow, Config),
+ Q = <<"reject-queue-", XOverflow/binary>>,
declare(Ch, Q, [{<<"x-max-length">>, long, 4},
- {<<"x-overflow">>, longstr, <<"reject-publish">>}
+ {<<"x-overflow">>, longstr, XOverflow}
| arguments(3)]),
publish(Ch, Q, [1, 2, 3, 1, 2, 3, 1, 2, 3]),
%% First 4 messages are published, all others are discarded.
diff --git a/test/queue_parallel_SUITE.erl b/test/queue_parallel_SUITE.erl
index 632a314d21..f8a039dc65 100644
--- a/test/queue_parallel_SUITE.erl
+++ b/test/queue_parallel_SUITE.erl
@@ -60,8 +60,10 @@ groups() ->
[
{parallel_tests, [],
[
- {classic_queue, [parallel], AllTests ++ [delete_immediately_by_pid_succeeds]},
- {mirrored_queue, [parallel], AllTests ++ [delete_immediately_by_pid_succeeds]},
+ {classic_queue, [parallel], AllTests ++ [delete_immediately_by_pid_succeeds,
+ trigger_message_store_compaction]},
+ {mirrored_queue, [parallel], AllTests ++ [delete_immediately_by_pid_succeeds,
+ trigger_message_store_compaction]},
{quorum_queue, [parallel], AllTests ++ [delete_immediately_by_pid_fails]},
{quorum_queue_in_memory_limit, [parallel], AllTests ++ [delete_immediately_by_pid_fails]},
{quorum_queue_in_memory_bytes, [parallel], AllTests ++ [delete_immediately_by_pid_fails]}
@@ -327,6 +329,27 @@ subscribe_and_multiple_ack(Config) ->
rabbit_ct_client_helpers:close_channel(Ch),
ok.
+trigger_message_store_compaction(Config) ->
+ {_, Ch} = rabbit_ct_client_helpers:open_connection_and_channel(Config, 0),
+ QName = ?config(queue_name, Config),
+ declare_queue(Ch, Config, QName),
+
+ N = 12000,
+ [publish(Ch, QName, [binary:copy(<<"a">>, 5000)]) || _ <- lists:seq(1, N)],
+ wait_for_messages(Config, [[QName, <<"12000">>, <<"12000">>, <<"0">>]]),
+
+ AllDTags = rabbit_ct_client_helpers:consume_without_acknowledging(Ch, QName, N),
+ ToAck = lists:filter(fun (I) -> I > 500 andalso I < 11200 end, AllDTags),
+
+ [amqp_channel:cast(Ch, #'basic.ack'{delivery_tag = Tag,
+ multiple = false}) || Tag <- ToAck],
+
+ %% give compaction a moment to start in and finish
+ timer:sleep(5000),
+ amqp_channel:cast(Ch, #'queue.purge'{queue = QName}),
+ rabbit_ct_client_helpers:close_channel(Ch),
+ ok.
+
subscribe_and_requeue_multiple_nack(Config) ->
{_, Ch} = rabbit_ct_client_helpers:open_connection_and_channel(Config, 0),
QName = ?config(queue_name, Config),
diff --git a/test/quorum_queue_SUITE.erl b/test/quorum_queue_SUITE.erl
index c23b7ac85e..1d9789fe89 100644
--- a/test/quorum_queue_SUITE.erl
+++ b/test/quorum_queue_SUITE.erl
@@ -323,11 +323,12 @@ declare_invalid_args(Config) ->
LQ, [{<<"x-queue-type">>, longstr, <<"quorum">>},
{<<"x-max-priority">>, long, 2000}])),
- ?assertExit(
- {{shutdown, {server_initiated_close, 406, _}}, _},
- declare(rabbit_ct_client_helpers:open_channel(Config, Server),
- LQ, [{<<"x-queue-type">>, longstr, <<"quorum">>},
- {<<"x-overflow">>, longstr, <<"reject-publish">>}])),
+ [?assertExit(
+ {{shutdown, {server_initiated_close, 406, _}}, _},
+ declare(rabbit_ct_client_helpers:open_channel(Config, Server),
+ LQ, [{<<"x-queue-type">>, longstr, <<"quorum">>},
+ {<<"x-overflow">>, longstr, XOverflow}]))
+ || XOverflow <- [<<"reject-publish">>, <<"reject-publish-dlx">>]],
?assertExit(
{{shutdown, {server_initiated_close, 406, _}}, _},
diff --git a/test/simple_ha_SUITE.erl b/test/simple_ha_SUITE.erl
index 20012b09c8..b2caff86a9 100644
--- a/test/simple_ha_SUITE.erl
+++ b/test/simple_ha_SUITE.erl
@@ -30,6 +30,11 @@ all() ->
].
groups() ->
+ RejectTests = [
+ rejects_survive_stop,
+ rejects_survive_sigkill,
+ rejects_survive_policy
+ ],
[
{cluster_size_2, [], [
rapid_redeclare,
@@ -45,9 +50,8 @@ groups() ->
confirms_survive_stop,
confirms_survive_sigkill,
confirms_survive_policy,
- rejects_survive_stop,
- rejects_survive_sigkill,
- rejects_survive_policy
+ {overflow_reject_publish, [], RejectTests},
+ {overflow_reject_publish_dlx, [], RejectTests}
]}
].
@@ -69,6 +73,14 @@ init_per_group(cluster_size_2, Config) ->
init_per_group(cluster_size_3, Config) ->
rabbit_ct_helpers:set_config(Config, [
{rmq_nodes_count, 3}
+ ]);
+init_per_group(overflow_reject_publish, Config) ->
+ rabbit_ct_helpers:set_config(Config, [
+ {overflow, <<"reject-publish">>}
+ ]);
+init_per_group(overflow_reject_publish_dlx, Config) ->
+ rabbit_ct_helpers:set_config(Config, [
+ {overflow, <<"reject-publish-dlx">>}
]).
end_per_group(_, Config) ->
@@ -227,12 +239,13 @@ rejects_survive(Config, DeathFun) ->
Node2Channel = rabbit_ct_client_helpers:open_channel(Config, B),
%% declare the queue on the master, mirrored to the two slaves
- Queue = <<"test_rejects">>,
+ XOverflow = ?config(overflow, Config),
+ Queue = <<"test_rejects", "_", XOverflow/binary>>,
amqp_channel:call(Node1Channel,#'queue.declare'{queue = Queue,
auto_delete = false,
durable = true,
arguments = [{<<"x-max-length">>, long, 1},
- {<<"x-overflow">>, longstr, <<"reject-publish">>}]}),
+ {<<"x-overflow">>, longstr, XOverflow}]}),
Payload = <<"there can be only one">>,
amqp_channel:call(Node1Channel,
#'basic.publish'{routing_key = Queue},