summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorkjnilsson <knilsson@pivotal.io>2020-09-29 11:43:24 +0100
committerkjnilsson <knilsson@pivotal.io>2020-09-30 14:52:53 +0100
commitf20fa273e99e6dcd925f8cd5988b6385b7fc0b6a (patch)
tree18bea003bb781196698622bb20767477267d7f7b
parentbdb6f9b508dd1aaad5e618d9a620adb7972e618f (diff)
downloadrabbitmq-server-git-f20fa273e99e6dcd925f8cd5988b6385b7fc0b6a.tar.gz
Stream Queue
This is an aggregated commit of all changes related to the initial implementation of queue types and on top of that the stream queue type. The varios commit messages have simply been included mostly un-edited below. Make rabbit_amqqueue:not_found_or_absent_dirty/1 visible For use in the stream plugin. Use bigger retention policy on max-age test Set coordinator timeout to 30s Handle coordinator unavailable error Handle operator policies as maps when checking if is applicable Add is_policy_applicable/2 to classic queues Ignore restart commands if the stream has been deleted It could happen that after termination some of the monitors are still up and trigger writer/replica restarts Policy support on stream queues Remove subscription events on stream coordinator Ensure old leaders are removed from monitors Introduce delay when retrying a failed phase Note that this ensures monitor is setup, there was a bug where no monitor was really started when re-trying the same phase Restart replicas after leader election instead of relying on old monitors Use timer for stream coordinator retries Fix stream stats for members/online Multiple fixes for replica monitoring and restart Ensure pending commands are appended at the end and re-run Ensure phase is reset with the state Remove duplicates from replica list Restart current phase on state_enter Remove unused import Ensure rabbit is running when checking for stream quorum Restart replicas Add a close/1 function to queue types So that we can get a chance of cleaning up resources if needed. Stream queues close their osiris logs at this point. fix compiler errors stream-queue: take retention into account When calculating ready messages metrics. Add osiris to the list of rabbit deps Retry restart of replicas Do not restart replicas or leaders after receiving a delete cluster command Add more logging to the stream coordinator Monitor subscribed processes on the stream coordinator Memory breakdown for stream queues Update quorum queue event formatter rabbit_msg_record fixes Refactor channel confirms Remove old unconfirmed_messages module that was designed to handle multiple queue fan in logic including all ha mirrors etc. Replaced with simpler rabbit_confirms module that handles the fan out and leaves any queue specific logic (such as confirms from mirrors) to the queue type implemention. Also this module has a dedicated test module. Which is nice. Backward compatibility with 3.8.x events Supports mixed version cluster upgrades Match specification when stream queue already exists Max age retention for stream queues Stop all replicas before starting leader election stream: disallow global qos remove IS_CLASSIC|QUORUM macros Ensure only classic queues are notified on channel down This also removes the delivering_queues map in the channel state as it should not be needed for this and just cause additional unecessary accounting. Polish AMQP 1.0/0.9.1 properties conversion Support byte in application properties, handle 1-bit representation for booleans. Use binary in header for long AMQP 1.0 ID Fix AMQP 1.0 to 0.9.1 conversion Fix test due to incorrect type Convert timestamp application properties to/from seconds AMQP 1.0 uses milliseconds for timestamp and AMQP 0.9.1 uses seconds, so conversion needed. Dialyzer fixes Handle all message-id types AMQP 1.0 is more liberal in it's allowed types of message-id and correlation-id - this adds headers to describe the type of the data in the message_id / correlation_id properties and also handles the case where the data cannot fit by again using headers. Resize stream coordinator cluster when broker configuration changes convert timestamp to and fro seconds user_id should be a binary message annotations keys need to be symbols stream-queue: default exchange and routing key As these won't be present for data written using the rabbitmq-stream plugin. Add exchange, routing key as message annotations To the AMQP 1.0 formatted data to enable roundtrip. Add osiris logging module config And update logging config test suite. Restart election when start of new leader fails The node might have just gone down so we need to try another one Only aux keeps track of phase now, as it might change if the leader election fails Stream coordinator refactor - all state is kept on the ra machine Ensure any ra cluster not a qq is not cleaned up Fixes to recovery and monitoring Add AMQP 1.0 common to dependencies Add rabbit_msg_record module To handle conversions into internal stream storage format. Use rabbitmq-common stream-queue branch Use SSH for osiris dependency Stream coordinator: delete replica Stream coordinator: add replica Stream coordinator: leader failover Stream coordinator: declare and delete Test consuming from a random offset Previous offsets should not be delivered to consumers Consume from stream replicas and multiple test fixes Use max-length-bytes and add new max-segment-size Use SSH for osiris dependency Basic cancel for stream queues Publish stream queues and settle/reject/requeue refactor Consume from stream queues Fix recovery Publish stream messages Add/delete stream replicas Use safe queue names Set retention policy for stream queues Required by the ctl command [#171207092] Stream queue delete queue fix missing callback impl Stream queue declare Queue type abstraction And use the implementing module as the value of the amqqueue record `type` field. This will allow for easy dispatch to the queue type implementation. Queue type abstraction Move queue declare into rabbit_queue_type Move queue delete into queue type implementation Queue type: dequeue/basic_get Move info inside queue type abstraction Move policy change into queue type interface Add purge to queue type Add recovery to the queue type interface Rename amqqueue quorum_nodes field To a more generic an extensible opaque queue type specific map. Fix tests and handle classic API response Fix HA queue confirm bug All mirrors need to be present as queue names. This introduces context linking allowing additional queue refs to be linked to a single "master" queue ref contining the actual queue context. Fix issue with events of deleted queues Also update queue type smoke test to use a cluster by default. correct default value of amqqueue getter Move classic queues further inside queue type interface why [TrackerId] Dialyzer fixes
-rw-r--r--Makefile6
-rw-r--r--apps/rabbitmq_prelaunch/src/rabbit_prelaunch_conf.erl4
-rw-r--r--include/amqqueue.hrl5
-rwxr-xr-xscripts/rabbitmq-streams32
-rw-r--r--scripts/rabbitmq-streams.bat63
-rw-r--r--src/amqqueue.erl6
-rw-r--r--src/rabbit.erl16
-rw-r--r--src/rabbit_amqqueue.erl700
-rw-r--r--src/rabbit_amqqueue_process.erl50
-rw-r--r--src/rabbit_basic.erl2
-rw-r--r--src/rabbit_channel.erl694
-rw-r--r--src/rabbit_classic_queue.erl452
-rw-r--r--src/rabbit_confirms.erl151
-rw-r--r--src/rabbit_core_ff.erl12
-rw-r--r--src/rabbit_dead_letter.erl5
-rw-r--r--src/rabbit_definitions.erl1
-rw-r--r--src/rabbit_fifo.erl3
-rw-r--r--src/rabbit_fifo_client.erl216
-rw-r--r--src/rabbit_guid.erl8
-rw-r--r--src/rabbit_mirror_queue_slave.erl12
-rw-r--r--src/rabbit_msg_record.erl399
-rw-r--r--src/rabbit_osiris_metrics.erl103
-rw-r--r--src/rabbit_policies.erl18
-rw-r--r--src/rabbit_policy.erl7
-rw-r--r--src/rabbit_queue_type.erl560
-rw-r--r--src/rabbit_queue_type_util.erl80
-rw-r--r--src/rabbit_quorum_queue.erl377
-rw-r--r--src/rabbit_ra_registry.erl25
-rw-r--r--src/rabbit_stream_coordinator.erl906
-rw-r--r--src/rabbit_stream_queue.erl665
-rw-r--r--src/rabbit_vhost.erl6
-rw-r--r--src/rabbit_vm.erl37
-rw-r--r--src/unconfirmed_messages.erl266
-rw-r--r--test/backing_queue_SUITE.erl77
-rw-r--r--test/channel_operation_timeout_SUITE.erl3
-rw-r--r--test/confirms_rejects_SUITE.erl17
-rw-r--r--test/dead_lettering_SUITE.erl6
-rw-r--r--test/dynamic_ha_SUITE.erl8
-rw-r--r--test/queue_parallel_SUITE.erl21
-rw-r--r--test/queue_type_SUITE.erl234
-rw-r--r--test/quorum_queue_SUITE.erl83
-rw-r--r--test/quorum_queue_utils.erl9
-rw-r--r--test/rabbit_confirms_SUITE.erl154
-rw-r--r--test/rabbit_fifo_SUITE.erl2
-rw-r--r--test/rabbit_fifo_int_SUITE.erl186
-rw-r--r--test/rabbit_ha_test_consumer.erl7
-rw-r--r--test/rabbit_msg_record_SUITE.erl213
-rw-r--r--test/rabbit_stream_queue_SUITE.erl1304
-rw-r--r--test/simple_ha_SUITE.erl4
-rw-r--r--test/unit_log_config_SUITE.erl24
50 files changed, 6571 insertions, 1668 deletions
diff --git a/Makefile b/Makefile
index 3c38e5f57c..3b31513c69 100644
--- a/Makefile
+++ b/Makefile
@@ -117,7 +117,8 @@ define PROJECT_ENV
%% Socket writer will run GC every 1 GB of outgoing data
{writer_gc_threshold, 1000000000},
%% interval at which connection/channel tracking executes post operations
- {tracking_execution_timeout, 15000}
+ {tracking_execution_timeout, 15000},
+ {stream_messages_soft_limit, 256}
]
endef
@@ -130,11 +131,12 @@ APPS_DIR := $(CURDIR)/apps
LOCAL_DEPS = sasl rabbitmq_prelaunch os_mon inets compiler public_key crypto ssl syntax_tools xmerl
BUILD_DEPS = rabbitmq_cli syslog
-DEPS = cuttlefish ranch lager rabbit_common ra sysmon_handler stdout_formatter recon observer_cli
+DEPS = cuttlefish ranch lager rabbit_common ra sysmon_handler stdout_formatter recon observer_cli osiris amqp10_common
TEST_DEPS = rabbitmq_ct_helpers rabbitmq_ct_client_helpers amqp_client meck proper
dep_cuttlefish = hex 2.4.1
dep_syslog = git https://github.com/schlagert/syslog 3.4.5
+dep_osiris = git https://github.com/rabbitmq/osiris master
define usage_xml_to_erl
$(subst __,_,$(patsubst $(DOCS_DIR)/rabbitmq%.1.xml, src/rabbit_%_usage.erl, $(subst -,_,$(1))))
diff --git a/apps/rabbitmq_prelaunch/src/rabbit_prelaunch_conf.erl b/apps/rabbitmq_prelaunch/src/rabbit_prelaunch_conf.erl
index 732c04dbaa..fbbae7a185 100644
--- a/apps/rabbitmq_prelaunch/src/rabbit_prelaunch_conf.erl
+++ b/apps/rabbitmq_prelaunch/src/rabbit_prelaunch_conf.erl
@@ -360,7 +360,9 @@ override_with_hard_coded_critical_config() ->
{ra,
%% Make Ra use a custom logger that dispatches to lager
%% instead of the default OTP logger
- [{logger_module, rabbit_log_ra_shim}]}
+ [{logger_module, rabbit_log_ra_shim}]},
+ {osiris,
+ [{logger_module, rabbit_log_osiris_shim}]}
],
apply_erlang_term_based_config(Config).
diff --git a/include/amqqueue.hrl b/include/amqqueue.hrl
index 48ffd3da77..097f1dfa0c 100644
--- a/include/amqqueue.hrl
+++ b/include/amqqueue.hrl
@@ -54,6 +54,11 @@
?amqqueue_v2_field_type(Q) =:= rabbit_quorum_queue) orelse
false).
+-define(amqqueue_is_stream(Q),
+ (?is_amqqueue_v2(Q) andalso
+ ?amqqueue_v2_field_type(Q) =:= rabbit_stream_queue) orelse
+ false).
+
-define(amqqueue_has_valid_pid(Q),
((?is_amqqueue_v2(Q) andalso
is_pid(?amqqueue_v2_field_pid(Q))) orelse
diff --git a/scripts/rabbitmq-streams b/scripts/rabbitmq-streams
new file mode 100755
index 0000000000..376cc497df
--- /dev/null
+++ b/scripts/rabbitmq-streams
@@ -0,0 +1,32 @@
+#!/bin/sh
+## 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-2020 Pivotal Software, Inc. All rights reserved.
+##
+
+# Exit immediately if a pipeline, which may consist of a single simple command,
+# a list, or a compound command returns a non-zero status
+set -e
+
+# Each variable or function that is created or modified is given the export
+# attribute and marked for export to the environment of subsequent commands.
+set -a
+
+# shellcheck source=/dev/null
+#
+# TODO: when shellcheck adds support for relative paths, change to
+# shellcheck source=./rabbitmq-env
+. "${0%/*}"/rabbitmq-env
+
+run_escript rabbitmqctl_escript "${ESCRIPT_DIR:?must be defined}"/rabbitmq-streams "$@"
diff --git a/scripts/rabbitmq-streams.bat b/scripts/rabbitmq-streams.bat
new file mode 100644
index 0000000000..83572a8d62
--- /dev/null
+++ b/scripts/rabbitmq-streams.bat
@@ -0,0 +1,63 @@
+@echo off
+REM The contents of this file are subject to the Mozilla Public License
+REM Version 1.1 (the "License"); you may not use this file except in
+REM compliance with the License. You may obtain a copy of the License
+REM at https://www.mozilla.org/MPL/
+REM
+REM Software distributed under the License is distributed on an "AS IS"
+REM basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+REM the License for the specific language governing rights and
+REM limitations under the License.
+REM
+REM The Original Code is RabbitMQ.
+REM
+REM The Initial Developer of the Original Code is GoPivotal, Inc.
+REM Copyright (c) 2007-2020 Pivotal Software, Inc. All rights reserved.
+REM
+
+REM Scopes the variables to the current batch file
+setlocal
+
+rem Preserve values that might contain exclamation marks before
+rem enabling delayed expansion
+set TDP0=%~dp0
+set STAR=%*
+setlocal enabledelayedexpansion
+
+REM Get default settings with user overrides for (RABBITMQ_)<var_name>
+REM Non-empty defaults should be set in rabbitmq-env
+call "%TDP0%\rabbitmq-env.bat" %~n0
+
+if not exist "!ERLANG_HOME!\bin\erl.exe" (
+ echo.
+ echo ******************************
+ echo ERLANG_HOME not set correctly.
+ echo ******************************
+ echo.
+ echo Please either set ERLANG_HOME to point to your Erlang installation or place the
+ echo RabbitMQ server distribution in the Erlang lib folder.
+ echo.
+ exit /B 1
+)
+
+REM Disable erl_crash.dump by default for control scripts.
+if not defined ERL_CRASH_DUMP_SECONDS (
+ set ERL_CRASH_DUMP_SECONDS=0
+)
+
+"!ERLANG_HOME!\bin\erl.exe" +B ^
+-boot !CLEAN_BOOT_FILE! ^
+-noinput -noshell -hidden -smp enable ^
+!RABBITMQ_CTL_ERL_ARGS! ^
+-run escript start ^
+-escript main rabbitmqctl_escript ^
+-extra "%RABBITMQ_HOME%\escript\rabbitmq-streams" !STAR!
+
+if ERRORLEVEL 1 (
+ exit /B %ERRORLEVEL%
+)
+
+EXIT /B 0
+
+endlocal
+endlocal
diff --git a/src/amqqueue.erl b/src/amqqueue.erl
index e2c67cf8e3..8756f3fe10 100644
--- a/src/amqqueue.erl
+++ b/src/amqqueue.erl
@@ -548,8 +548,10 @@ set_recoverable_slaves(Queue, Slaves) ->
% type_state (new in v2)
-spec get_type_state(amqqueue()) -> map().
-get_type_state(#amqqueue{type_state = TState}) -> TState;
-get_type_state(_) -> [].
+get_type_state(#amqqueue{type_state = TState}) ->
+ TState;
+get_type_state(_) ->
+ #{}.
-spec set_type_state(amqqueue(), map()) -> amqqueue().
set_type_state(#amqqueue{} = Queue, TState) ->
diff --git a/src/rabbit.erl b/src/rabbit.erl
index f057fb0da3..7d5c502eed 100644
--- a/src/rabbit.erl
+++ b/src/rabbit.erl
@@ -114,6 +114,20 @@
{requires, pre_boot},
{enables, external_infrastructure}]}).
+-rabbit_boot_step({rabbit_osiris_metrics,
+ [{description, "osiris metrics scraper"},
+ {mfa, {rabbit_sup, start_child,
+ [rabbit_osiris_metrics]}},
+ {requires, pre_boot},
+ {enables, external_infrastructure}]}).
+
+%% -rabbit_boot_step({rabbit_stream_coordinator,
+%% [{description, "stream queues coordinator"},
+%% {mfa, {rabbit_stream_coordinator, start,
+%% []}},
+%% {requires, pre_boot},
+%% {enables, external_infrastructure}]}).
+
-rabbit_boot_step({rabbit_event,
[{description, "statistics event manager"},
{mfa, {rabbit_sup, start_restartable_child,
@@ -255,7 +269,7 @@
-include("rabbit_framing.hrl").
-include("rabbit.hrl").
--define(APPS, [os_mon, mnesia, rabbit_common, rabbitmq_prelaunch, ra, sysmon_handler, rabbit]).
+-define(APPS, [os_mon, mnesia, rabbit_common, rabbitmq_prelaunch, ra, sysmon_handler, rabbit, osiris]).
-define(ASYNC_THREADS_WARNING_THRESHOLD, 8).
diff --git a/src/rabbit_amqqueue.erl b/src/rabbit_amqqueue.erl
index 4b796b9d66..b4bee8068f 100644
--- a/src/rabbit_amqqueue.erl
+++ b/src/rabbit_amqqueue.erl
@@ -10,13 +10,14 @@
-export([warn_file_limit/0]).
-export([recover/1, stop/1, start/1, declare/6, declare/7,
delete_immediately/1, delete_exclusive/2, delete/4, purge/1,
- forget_all_durable/1, delete_crashed/1, delete_crashed/2,
- delete_crashed_internal/2]).
+ forget_all_durable/1]).
-export([pseudo_queue/2, pseudo_queue/3, immutable/1]).
--export([lookup/1, lookup_many/1, not_found_or_absent/1, with/2, with/3, with_or_die/2,
+-export([lookup/1, lookup_many/1, not_found_or_absent/1, not_found_or_absent_dirty/1,
+ with/2, with/3, with_or_die/2,
assert_equivalence/5,
check_exclusive_access/2, with_exclusive_access_or_die/3,
- stat/1, deliver/2, deliver/3, requeue/4, ack/4, reject/5]).
+ stat/1,
+ requeue/3, ack/3, reject/4]).
-export([not_found/1, absent/2]).
-export([list/0, list/1, info_keys/0, info/1, info/2, info_all/1, info_all/2,
emit_info_all/5, list_local/1, info_local/1,
@@ -27,9 +28,9 @@
-export([list_by_type/1, sample_local_queues/0, sample_n_by_name/2, sample_n/2]).
-export([force_event_refresh/1, notify_policy_changed/1]).
-export([consumers/1, consumers_all/1, emit_consumers_all/4, consumer_info_keys/0]).
--export([basic_get/6, basic_consume/12, basic_cancel/6, notify_decorators/1]).
+-export([basic_get/5, basic_consume/12, basic_cancel/5, notify_decorators/1]).
-export([notify_sent/2, notify_sent_queue_down/1, resume/2]).
--export([notify_down_all/2, notify_down_all/3, activate_limit_all/2, credit/6]).
+-export([notify_down_all/2, notify_down_all/3, activate_limit_all/2, credit/5]).
-export([on_node_up/1, on_node_down/1]).
-export([update/2, store_queue/1, update_decorators/1, policy_changed/2]).
-export([update_mirroring/1, sync_mirrors/1, cancel_sync_mirrors/1]).
@@ -53,6 +54,8 @@
-export([is_policy_applicable/2]).
+-export([check_max_age/1]).
+
%% internal
-export([internal_declare/2, internal_delete/2, run_backing_queue/3,
set_ram_duration_target/2, set_maximum_since_use/2,
@@ -78,7 +81,8 @@
-type qpids() :: [pid()].
-type qlen() :: rabbit_types:ok(non_neg_integer()).
-type qfun(A) :: fun ((amqqueue:amqqueue()) -> A | no_return()).
--type qmsg() :: {name(), pid() | {atom(), pid()}, msg_id(), boolean(), rabbit_types:message()}.
+-type qmsg() :: {name(), pid() | {atom(), pid()}, msg_id(),
+ boolean(), rabbit_types:message()}.
-type msg_id() :: non_neg_integer().
-type ok_or_errors() ::
'ok' | {'error', [{'error' | 'exit' | 'throw', any()}]}.
@@ -86,7 +90,6 @@
-type queue_not_found() :: not_found.
-type queue_absent() :: {'absent', amqqueue:amqqueue(), absent_reason()}.
-type not_found_or_absent() :: queue_not_found() | queue_absent().
--type quorum_states() :: #{Name :: atom() => rabbit_fifo_client:state()}.
%%----------------------------------------------------------------------------
@@ -110,34 +113,11 @@ warn_file_limit() ->
end.
-spec recover(rabbit_types:vhost()) ->
- {RecoveredClassic :: [amqqueue:amqqueue()],
- FailedClassic :: [amqqueue:amqqueue()],
- Quorum :: [amqqueue:amqqueue()]}.
-
+ {Recovered :: [amqqueue:amqqueue()],
+ Failed :: [amqqueue:amqqueue()]}.
recover(VHost) ->
- AllClassic = find_local_durable_classic_queues(VHost),
- Quorum = find_local_quorum_queues(VHost),
- {RecoveredClassic, FailedClassic} = recover_classic_queues(VHost, AllClassic),
- {RecoveredClassic, FailedClassic, rabbit_quorum_queue:recover(Quorum)}.
-
-recover_classic_queues(VHost, Queues) ->
- {ok, BQ} = application:get_env(rabbit, backing_queue_module),
- %% We rely on BQ:start/1 returning the recovery terms in the same
- %% order as the supplied queue names, so that we can zip them together
- %% for further processing in recover_durable_queues.
- {ok, OrderedRecoveryTerms} =
- BQ:start(VHost, [amqqueue:get_name(Q) || Q <- Queues]),
- case rabbit_amqqueue_sup_sup:start_for_vhost(VHost) of
- {ok, _} ->
- RecoveredQs = recover_durable_queues(lists:zip(Queues, OrderedRecoveryTerms)),
- RecoveredNames = [amqqueue:get_name(Q) || Q <- RecoveredQs],
- FailedQueues = [Q || Q <- Queues,
- not lists:member(amqqueue:get_name(Q), RecoveredNames)],
- {RecoveredQs, FailedQueues};
- {error, Reason} ->
- rabbit_log:error("Failed to start queue supervisor for vhost '~s': ~s", [VHost, Reason]),
- throw({error, Reason})
- end.
+ AllDurable = find_local_durable_queues(VHost),
+ rabbit_queue_type:recover(VHost, AllDurable).
filter_pid_per_type(QPids) ->
lists:partition(fun(QPid) -> ?IS_CLASSIC(QPid) end, QPids).
@@ -151,7 +131,6 @@ filter_resource_per_type(Resources) ->
lists:partition(fun({_Resource, QPid}) -> ?IS_CLASSIC(QPid) end, Queues).
-spec stop(rabbit_types:vhost()) -> 'ok'.
-
stop(VHost) ->
%% Classic queues
ok = rabbit_amqqueue_sup_sup:stop_for_vhost(VHost),
@@ -178,67 +157,32 @@ mark_local_durable_queues_stopped(VHost) ->
do_mark_local_durable_queues_stopped(VHost)).
do_mark_local_durable_queues_stopped(VHost) ->
- Qs = find_local_durable_classic_queues(VHost),
+ Qs = find_local_durable_queues(VHost),
rabbit_misc:execute_mnesia_transaction(
fun() ->
[ store_queue(amqqueue:set_state(Q, stopped))
- || Q <- Qs,
+ || Q <- Qs, amqqueue:get_type(Q) =:= rabbit_classic_queue,
amqqueue:get_state(Q) =/= stopped ]
end).
-find_local_quorum_queues(VHost) ->
- Node = node(),
+find_local_durable_queues(VHost) ->
mnesia:async_dirty(
fun () ->
- qlc:e(qlc:q([Q || Q <- mnesia:table(rabbit_durable_queue),
- amqqueue:get_vhost(Q) =:= VHost,
- amqqueue:is_quorum(Q) andalso
- (lists:member(Node, get_quorum_nodes(Q)))]))
- end).
-
-find_local_durable_classic_queues(VHost) ->
- Node = node(),
- mnesia:async_dirty(
- fun () ->
- qlc:e(qlc:q([Q || Q <- mnesia:table(rabbit_durable_queue),
- amqqueue:get_vhost(Q) =:= VHost,
- amqqueue:is_classic(Q) andalso
- (is_local_to_node(amqqueue:get_pid(Q), Node) andalso
- %% Terminations on node down will not remove the rabbit_queue
- %% record if it is a mirrored queue (such info is now obtained from
- %% the policy). Thus, we must check if the local pid is alive
- %% - if the record is present - in order to restart.
- (mnesia:read(rabbit_queue, amqqueue:get_name(Q), read) =:= []
- orelse not rabbit_mnesia:is_process_alive(amqqueue:get_pid(Q))))
- ]))
+ qlc:e(
+ qlc:q(
+ [Q || Q <- mnesia:table(rabbit_durable_queue),
+ amqqueue:get_vhost(Q) =:= VHost andalso
+ rabbit_queue_type:is_recoverable(Q)
+ ]))
end).
find_recoverable_queues() ->
- Node = node(),
mnesia:async_dirty(
fun () ->
qlc:e(qlc:q([Q || Q <- mnesia:table(rabbit_durable_queue),
- (amqqueue:is_classic(Q) andalso
- (is_local_to_node(amqqueue:get_pid(Q), Node) andalso
- %% Terminations on node down will not remove the rabbit_queue
- %% record if it is a mirrored queue (such info is now obtained from
- %% the policy). Thus, we must check if the local pid is alive
- %% - if the record is present - in order to restart.
- (mnesia:read(rabbit_queue, amqqueue:get_name(Q), read) =:= []
- orelse not rabbit_mnesia:is_process_alive(amqqueue:get_pid(Q)))))
- orelse (amqqueue:is_quorum(Q) andalso lists:member(Node, get_quorum_nodes(Q)))
- ]))
+ rabbit_queue_type:is_recoverable(Q)]))
end).
-recover_durable_queues(QueuesAndRecoveryTerms) ->
- {Results, Failures} =
- gen_server2:mcall(
- [{rabbit_amqqueue_sup_sup:start_queue_process(node(), Q, recovery),
- {init, {self(), Terms}}} || {Q, Terms} <- QueuesAndRecoveryTerms]),
- [rabbit_log:error("Queue ~p failed to initialise: ~p~n",
- [Pid, Error]) || {Pid, Error} <- Failures],
- [Q || {_, {new, Q}} <- Results].
-
-spec declare(name(),
boolean(),
boolean(),
@@ -249,7 +193,6 @@ recover_durable_queues(QueuesAndRecoveryTerms) ->
{'new', amqqueue:amqqueue(), rabbit_fifo_client:state()} |
{'absent', amqqueue:amqqueue(), absent_reason()} |
rabbit_types:channel_exit().
-
declare(QueueName, Durable, AutoDelete, Args, Owner, ActingUser) ->
declare(QueueName, Durable, AutoDelete, Args, Owner, ActingUser, node()).
@@ -266,18 +209,13 @@ declare(QueueName, Durable, AutoDelete, Args, Owner, ActingUser) ->
rabbit_types:username(),
node()) ->
{'new' | 'existing' | 'owner_died', amqqueue:amqqueue()} |
- {'new', amqqueue:amqqueue(), rabbit_fifo_client:state()} |
{'absent', amqqueue:amqqueue(), absent_reason()} |
rabbit_types:channel_exit().
-
declare(QueueName = #resource{virtual_host = VHost}, Durable, AutoDelete, Args,
Owner, ActingUser, Node) ->
ok = check_declare_arguments(QueueName, Args),
Type = get_queue_type(Args),
- TypeIsAllowed =
- Type =:= rabbit_classic_queue orelse
- rabbit_feature_flags:is_enabled(quorum_queue),
- case TypeIsAllowed of
+ case rabbit_queue_type:is_enabled(Type) of
true ->
Q0 = amqqueue:new(QueueName,
none,
@@ -290,7 +228,7 @@ declare(QueueName = #resource{virtual_host = VHost}, Durable, AutoDelete, Args,
Type),
Q = rabbit_queue_decorator:set(
rabbit_policy:set(Q0)),
- do_declare(Q, Node);
+ rabbit_queue_type:declare(Q, Node);
false ->
rabbit_misc:protocol_error(
internal_error,
@@ -299,49 +237,12 @@ declare(QueueName = #resource{virtual_host = VHost}, Durable, AutoDelete, Args,
[rabbit_misc:rs(QueueName), Type, Node])
end.
-do_declare(Q, Node) when ?amqqueue_is_classic(Q) ->
- declare_classic_queue(Q, Node);
-do_declare(Q, _Node) when ?amqqueue_is_quorum(Q) ->
- rabbit_quorum_queue:declare(Q).
-
-declare_classic_queue(Q, Node) ->
- QName = amqqueue:get_name(Q),
- VHost = amqqueue:get_vhost(Q),
- Node1 = case Node of
- {ignore_location, Node0} ->
- Node0;
- _ ->
- case rabbit_queue_master_location_misc:get_location(Q) of
- {ok, Node0} -> Node0;
- undefined -> Node;
- {error, _} -> Node
- end
- end,
- Node1 = rabbit_mirror_queue_misc:initial_queue_node(Q, Node1),
- case rabbit_vhost_sup_sup:get_vhost_sup(VHost, Node1) of
- {ok, _} ->
- gen_server2:call(
- rabbit_amqqueue_sup_sup:start_queue_process(Node1, Q, declare),
- {init, new}, infinity);
- {error, Error} ->
- rabbit_misc:protocol_error(internal_error,
- "Cannot declare a queue '~s' on node '~s': ~255p",
- [rabbit_misc:rs(QName), Node1, Error])
- end.
-
get_queue_type(Args) ->
case rabbit_misc:table_lookup(Args, <<"x-queue-type">>) of
undefined ->
- rabbit_classic_queue;
+ rabbit_queue_type:default();
{_, V} ->
- %% TODO: this mapping of "friendly" queue type name to the
- %% implementing module should be part of some kind of registry
- case V of
- <<"quorum">> ->
- rabbit_quorum_queue;
- <<"classic">> ->
- rabbit_classic_queue
- end
+ rabbit_queue_type:discover(V)
end.
-spec internal_declare(amqqueue:amqqueue(), boolean()) ->
@@ -451,14 +352,12 @@ policy_changed(Q1, Q2) ->
[ok = M:policy_changed(Q1, Q2) || M <- lists:usort(D1 ++ D2)],
%% Make sure we emit a stats event even if nothing
%% mirroring-related has changed - the policy may have changed anyway.
- notify_policy_changed(Q1).
+ notify_policy_changed(Q2).
is_policy_applicable(QName, Policy) ->
case lookup(QName) of
- {ok, Q} when ?amqqueue_is_quorum(Q) ->
- rabbit_quorum_queue:is_policy_applicable(Q, Policy);
- {ok, Q} when ?amqqueue_is_classic(Q) ->
- rabbit_amqqueue_process:is_policy_applicable(Q, Policy);
+ {ok, Q} ->
+ rabbit_queue_type:is_policy_applicable(Q, Policy);
_ ->
%% Defaults to previous behaviour. Apply always
true
@@ -872,7 +771,9 @@ declare_args() ->
{<<"x-queue-mode">>, fun check_queue_mode/2},
{<<"x-single-active-consumer">>, fun check_single_active_consumer_arg/2},
{<<"x-queue-type">>, fun check_queue_type/2},
- {<<"x-quorum-initial-group-size">>, fun check_default_quorum_initial_group_size_arg/2}].
+ {<<"x-quorum-initial-group-size">>, fun check_default_quorum_initial_group_size_arg/2},
+ {<<"x-max-age">>, fun check_max_age_arg/2},
+ {<<"x-max-segment-size">>, fun check_non_neg_int_arg/2}].
consume_args() -> [{<<"x-priority">>, fun check_int_arg/2},
{<<"x-cancel-on-ha-failover">>, fun check_bool_arg/2}].
@@ -926,6 +827,48 @@ check_default_quorum_initial_group_size_arg({Type, Val}, Args) ->
Error -> Error
end.
+check_max_age_arg({longstr, Val}, _Args) ->
+ case check_max_age(Val) of
+ {error, _} = E ->
+ E;
+ _ ->
+ ok
+ end;
+check_max_age_arg({Type, _}, _Args) ->
+ {error, {unacceptable_type, Type}}.
+
+check_max_age(MaxAge) ->
+ case re:run(MaxAge, "(^[0-9]*)(.*)", [{capture, all_but_first, list}]) of
+ {match, [Value, Unit]} ->
+ case list_to_integer(Value) of
+ I when I > 0 ->
+ case lists:member(Unit, ["Y", "M", "D", "h", "m", "s"]) of
+ true ->
+ Int = list_to_integer(Value),
+ Int * unit_value_in_ms(Unit);
+ false ->
+ {error, invalid_max_age}
+ end;
+ _ ->
+ {error, invalid_max_age}
+ end;
+ _ ->
+ {error, invalid_max_age}
+ end.
+
+unit_value_in_ms("Y") ->
+ 365 * unit_value_in_ms("D");
+unit_value_in_ms("M") ->
+ 30 * unit_value_in_ms("D");
+unit_value_in_ms("D") ->
+ 24 * unit_value_in_ms("h");
+unit_value_in_ms("h") ->
+ 3600 * unit_value_in_ms("s");
+unit_value_in_ms("m") ->
+ 60 * unit_value_in_ms("s");
+unit_value_in_ms("s") ->
+ 1000.
+
%% Note that the validity of x-dead-letter-exchange is already verified
%% by rabbit_channel's queue.declare handler.
check_dlxname_arg({longstr, _}, _) -> ok;
@@ -958,7 +901,7 @@ check_queue_mode({Type, _}, _Args) ->
{error, {unacceptable_type, Type}}.
check_queue_type({longstr, Val}, _Args) ->
- case lists:member(Val, [<<"classic">>, <<"quorum">>]) of
+ case lists:member(Val, [<<"classic">>, <<"quorum">>, <<"stream">>]) of
true -> ok;
false -> {error, invalid_queue_type}
end;
@@ -1051,7 +994,7 @@ list_by_type(Type) ->
list_local_quorum_queue_names() ->
[ amqqueue:get_name(Q) || Q <- list_by_type(quorum),
amqqueue:get_state(Q) =/= crashed,
- lists:member(node(), get_quorum_nodes(Q))].
+ lists:member(node(), get_quorum_nodes(Q))].
-spec list_local_quorum_queues() -> [amqqueue:amqqueue()].
list_local_quorum_queues() ->
@@ -1067,10 +1010,13 @@ list_local_leaders() ->
-spec list_local_followers() -> [amqqueue:amqqueue()].
list_local_followers() ->
- [ Q || Q <- list(),
+ [Q
+ || Q <- list(),
amqqueue:is_quorum(Q),
- amqqueue:get_state(Q) =/= crashed, amqqueue:get_leader(Q) =/= node(),
- lists:member(node(), get_quorum_nodes(Q))].
+ amqqueue:get_state(Q) =/= crashed,
+ amqqueue:get_leader(Q) =/= node(),
+ rabbit_quorum_queue:is_recoverable(Q)
+ ].
-spec list_local_mirrored_classic_queues() -> [amqqueue:amqqueue()].
list_local_mirrored_classic_queues() ->
@@ -1243,28 +1189,14 @@ format(Q) -> rabbit_amqqueue_process:format(Q).
-spec info(amqqueue:amqqueue()) -> rabbit_types:infos().
-info(Q) when ?amqqueue_is_quorum(Q) -> rabbit_quorum_queue:info(Q);
-info(Q) when ?amqqueue_state_is(Q, crashed) -> info_down(Q, crashed);
-info(Q) when ?amqqueue_state_is(Q, stopped) -> info_down(Q, stopped);
-info(Q) ->
- QPid = amqqueue:get_pid(Q),
- delegate:invoke(QPid, {gen_server2, call, [info, infinity]}).
+info(Q) when ?is_amqqueue(Q) -> rabbit_queue_type:info(Q, all_keys).
+
-spec info(amqqueue:amqqueue(), rabbit_types:info_keys()) ->
rabbit_types:infos().
-info(Q, Items) when ?amqqueue_is_quorum(Q) ->
- rabbit_quorum_queue:info(Q, Items);
-info(Q, Items) when ?amqqueue_state_is(Q, crashed) ->
- info_down(Q, Items, crashed);
-info(Q, Items) when ?amqqueue_state_is(Q, stopped) ->
- info_down(Q, Items, stopped);
-info(Q, Items) ->
- QPid = amqqueue:get_pid(Q),
- case delegate:invoke(QPid, {gen_server2, call, [{info, Items}, infinity]}) of
- {ok, Res} -> Res;
- {error, Error} -> throw(Error)
- end.
+info(Q, Items) when ?is_amqqueue(Q) ->
+ rabbit_queue_type:info(Q, Items).
info_down(Q, DownReason) ->
info_down(Q, rabbit_amqqueue_process:info_keys(), DownReason).
@@ -1367,14 +1299,8 @@ force_event_refresh(Ref) ->
ok.
-spec notify_policy_changed(amqqueue:amqqueue()) -> 'ok'.
-
-notify_policy_changed(Q) when ?amqqueue_is_classic(Q) ->
- QPid = amqqueue:get_pid(Q),
- gen_server2:cast(QPid, policy_changed);
-notify_policy_changed(Q) when ?amqqueue_is_quorum(Q) ->
- QPid = amqqueue:get_pid(Q),
- QName = amqqueue:get_name(Q),
- rabbit_quorum_queue:policy_changed(QName, QPid).
+notify_policy_changed(Q) when ?is_amqqueue(Q) ->
+ rabbit_queue_type:policy_changed(Q).
-spec consumers(amqqueue:amqqueue()) ->
[{pid(), rabbit_types:ctag(), boolean(), non_neg_integer(),
@@ -1389,7 +1315,12 @@ consumers(Q) when ?amqqueue_is_quorum(Q) ->
case ra:local_query(QPid, fun rabbit_fifo:query_consumers/1) of
{ok, {_, Result}, _} -> maps:values(Result);
_ -> []
- end.
+ end;
+consumers(Q) when ?amqqueue_is_stream(Q) ->
+ %% TODO how??? they only exist on the channel
+ %% we could list the offset listener on the writer but we don't even have a consumer tag,
+ %% only a (channel) pid and offset
+ [].
-spec consumer_info_keys() -> rabbit_types:info_keys().
@@ -1425,9 +1356,8 @@ get_queue_consumer_info(Q, ConsumerInfoKeys) ->
-spec stat(amqqueue:amqqueue()) ->
{'ok', non_neg_integer(), non_neg_integer()}.
-
-stat(Q) when ?amqqueue_is_quorum(Q) -> rabbit_quorum_queue:stat(Q);
-stat(Q) -> delegate:invoke(amqqueue:get_pid(Q), {gen_server2, call, [stat, infinity]}).
+stat(Q) ->
+ rabbit_queue_type:stat(Q).
-spec pid_of(amqqueue:amqqueue()) ->
pid().
@@ -1476,162 +1406,46 @@ delete_immediately_by_resource(Resources) ->
qlen() |
rabbit_types:error('in_use') |
rabbit_types:error('not_empty').
-
-delete(Q,
- IfUnused, IfEmpty, ActingUser) when ?amqqueue_is_quorum(Q) ->
- rabbit_quorum_queue:delete(Q, IfUnused, IfEmpty, ActingUser);
delete(Q, IfUnused, IfEmpty, ActingUser) ->
- case wait_for_promoted_or_stopped(Q) of
- {promoted, Q1} ->
- QPid = amqqueue:get_pid(Q1),
- delegate:invoke(QPid, {gen_server2, call, [{delete, IfUnused, IfEmpty, ActingUser}, infinity]});
- {stopped, Q1} ->
- #resource{name = Name, virtual_host = Vhost} = amqqueue:get_name(Q1),
- case IfEmpty of
- true ->
- rabbit_log:error("Queue ~s in vhost ~s has its master node down and "
- "no mirrors available or eligible for promotion. "
- "The queue may be non-empty. "
- "Refusing to force-delete.",
- [Name, Vhost]),
- {error, not_empty};
- false ->
- rabbit_log:warning("Queue ~s in vhost ~s has its master node is down and "
- "no mirrors available or eligible for promotion. "
- "Forcing queue deletion.",
- [Name, Vhost]),
- delete_crashed_internal(Q1, ActingUser),
- {ok, 0}
- end;
- {error, not_found} ->
- %% Assume the queue was deleted
- {ok, 0}
- end.
-
--spec wait_for_promoted_or_stopped(amqqueue:amqqueue()) ->
- {promoted, amqqueue:amqqueue()} |
- {stopped, amqqueue:amqqueue()} |
- {error, not_found}.
-wait_for_promoted_or_stopped(Q0) ->
- QName = amqqueue:get_name(Q0),
- case lookup(QName) of
- {ok, Q} ->
- QPid = amqqueue:get_pid(Q),
- SPids = amqqueue:get_slave_pids(Q),
- case rabbit_mnesia:is_process_alive(QPid) of
- true -> {promoted, Q};
- false ->
- case lists:any(fun(Pid) ->
- rabbit_mnesia:is_process_alive(Pid)
- end, SPids) of
- %% There is a live mirror. May be promoted
- true ->
- timer:sleep(100),
- wait_for_promoted_or_stopped(Q);
- %% All mirror pids are stopped.
- %% No process left for the queue
- false -> {stopped, Q}
- end
- end;
- {error, not_found} ->
- {error, not_found}
- end.
-
--spec delete_crashed(amqqueue:amqqueue()) -> 'ok'.
-
-delete_crashed(Q) ->
- delete_crashed(Q, ?INTERNAL_USER).
-
-delete_crashed(Q, ActingUser) ->
- ok = rpc:call(amqqueue:qnode(Q), ?MODULE, delete_crashed_internal, [Q, ActingUser]).
-
--spec delete_crashed_internal(amqqueue:amqqueue(), rabbit_types:username()) -> 'ok'.
-
-delete_crashed_internal(Q, ActingUser) ->
- QName = amqqueue:get_name(Q),
- {ok, BQ} = application:get_env(rabbit, backing_queue_module),
- BQ:delete_crashed(Q),
- ok = internal_delete(QName, ActingUser).
-
--spec purge(amqqueue:amqqueue()) -> {ok, qlen()}.
+ rabbit_queue_type:delete(Q, IfUnused, IfEmpty, ActingUser).
-purge(Q) when ?amqqueue_is_classic(Q) ->
- QPid = amqqueue:get_pid(Q),
- delegate:invoke(QPid, {gen_server2, call, [purge, infinity]});
-purge(Q) when ?amqqueue_is_quorum(Q) ->
- NodeId = amqqueue:get_pid(Q),
- rabbit_quorum_queue:purge(NodeId).
+-spec purge(amqqueue:amqqueue()) -> qlen().
+purge(Q) when ?is_amqqueue(Q) ->
+ rabbit_queue_type:purge(Q).
--spec requeue(pid() | amqqueue:ra_server_id(),
+-spec requeue(name(),
{rabbit_fifo:consumer_tag(), [msg_id()]},
- pid(),
- quorum_states()) ->
- 'ok'.
-requeue(QPid, {_, MsgIds}, ChPid, QuorumStates) when ?IS_CLASSIC(QPid) ->
- ok = delegate:invoke(QPid, {gen_server2, call, [{requeue, MsgIds, ChPid}, infinity]}),
- QuorumStates;
-requeue({Name, _} = QPid, {CTag, MsgIds}, _ChPid, QuorumStates)
- when ?IS_QUORUM(QPid) ->
- case QuorumStates of
- #{Name := QState0} ->
- {ok, QState} = rabbit_quorum_queue:requeue(CTag, MsgIds, QState0),
- maps:put(Name, QState, QuorumStates);
- _ ->
- % queue was not found
- QuorumStates
- end.
+ rabbit_queue_type:state()) ->
+ {ok, rabbit_queue_type:state(), rabbit_queue_type:actions()}.
+requeue(QRef, {CTag, MsgIds}, QStates) ->
+ reject(QRef, true, {CTag, MsgIds}, QStates).
--spec ack(pid(),
+-spec ack(name(),
{rabbit_fifo:consumer_tag(), [msg_id()]},
- pid(),
- quorum_states()) ->
- quorum_states().
-
-ack(QPid, {_, MsgIds}, ChPid, QueueStates) when ?IS_CLASSIC(QPid) ->
- delegate:invoke_no_result(QPid, {gen_server2, cast, [{ack, MsgIds, ChPid}]}),
- QueueStates;
-ack({Name, _} = QPid, {CTag, MsgIds}, _ChPid, QuorumStates)
- when ?IS_QUORUM(QPid) ->
- case QuorumStates of
- #{Name := QState0} ->
- {ok, QState} = rabbit_quorum_queue:ack(CTag, MsgIds, QState0),
- maps:put(Name, QState, QuorumStates);
- _ ->
- %% queue was not found
- QuorumStates
- end.
+ rabbit_queue_type:state()) ->
+ {ok, rabbit_queue_type:state(), rabbit_queue_type:actions()}.
+ack(QPid, {CTag, MsgIds}, QueueStates) ->
+ rabbit_queue_type:settle(QPid, complete, CTag, MsgIds, QueueStates).
+
--spec reject(pid() | amqqueue:ra_server_id(),
+-spec reject(name(),
boolean(),
{rabbit_fifo:consumer_tag(), [msg_id()]},
- pid(),
- quorum_states()) ->
- quorum_states().
-
-reject(QPid, Requeue, {_, MsgIds}, ChPid, QStates) when ?IS_CLASSIC(QPid) ->
- ok = delegate:invoke_no_result(QPid, {gen_server2, cast,
- [{reject, Requeue, MsgIds, ChPid}]}),
- QStates;
-reject({Name, _} = QPid, Requeue, {CTag, MsgIds}, _ChPid, QuorumStates)
- when ?IS_QUORUM(QPid) ->
- case QuorumStates of
- #{Name := QState0} ->
- {ok, QState} = rabbit_quorum_queue:reject(Requeue, CTag,
- MsgIds, QState0),
- maps:put(Name, QState, QuorumStates);
- _ ->
- %% queue was not found
- QuorumStates
- end.
+ rabbit_queue_type:state()) ->
+ {ok, rabbit_queue_type:state(), rabbit_queue_type:actions()}.
+reject(QRef, Requeue, {CTag, MsgIds}, QStates) ->
+ Op = case Requeue of
+ true -> requeue;
+ false -> discard
+ end,
+ rabbit_queue_type:settle(QRef, Op, CTag, MsgIds, QStates).
-spec notify_down_all(qpids(), pid()) -> ok_or_errors().
-
notify_down_all(QPids, ChPid) ->
notify_down_all(QPids, ChPid, ?CHANNEL_OPERATION_TIMEOUT).
-spec notify_down_all(qpids(), pid(), non_neg_integer()) ->
ok_or_errors().
-
notify_down_all(QPids, ChPid, Timeout) ->
case rpc:call(node(), delegate, invoke,
[QPids, {gen_server2, call, [{notify_down, ChPid}, infinity]}], Timeout) of
@@ -1657,130 +1471,55 @@ activate_limit_all(QRefs, ChPid) ->
[{activate_limit, ChPid}]}).
-spec credit(amqqueue:amqqueue(),
- pid(),
rabbit_types:ctag(),
non_neg_integer(),
boolean(),
- quorum_states()) ->
- {'ok', quorum_states()}.
-
-credit(Q, ChPid, CTag, Credit,
- Drain, QStates) when ?amqqueue_is_classic(Q) ->
- QPid = amqqueue:get_pid(Q),
- delegate:invoke_no_result(QPid, {gen_server2, cast,
- [{credit, ChPid, CTag, Credit, Drain}]}),
- {ok, QStates};
-credit(Q,
- _ChPid, CTag, Credit,
- Drain, QStates) when ?amqqueue_is_quorum(Q) ->
- {Name, _} = Id = amqqueue:get_pid(Q),
- QName = amqqueue:get_name(Q),
- QState0 = get_quorum_state(Id, QName, QStates),
- {ok, QState} = rabbit_quorum_queue:credit(CTag, Credit, Drain, QState0),
- {ok, maps:put(Name, QState, QStates)}.
-
--spec basic_get(amqqueue:amqqueue(), pid(), boolean(), pid(), rabbit_types:ctag(),
- #{Name :: atom() => rabbit_fifo_client:state()}) ->
- {'ok', non_neg_integer(), qmsg(), quorum_states()} |
- {'empty', quorum_states()} |
+ rabbit_queue_type:state()) ->
+ rabbit_queue_type:state().
+credit(Q, CTag, Credit, Drain, QStates) ->
+ rabbit_queue_type:credit(Q, CTag, Credit, Drain, QStates).
+
+-spec basic_get(amqqueue:amqqueue(), boolean(), pid(), rabbit_types:ctag(),
+ rabbit_queue_type:state()) ->
+ {'ok', non_neg_integer(), qmsg(), rabbit_queue_type:state()} |
+ {'empty', rabbit_queue_type:state()} |
rabbit_types:channel_exit().
+basic_get(Q, NoAck, LimiterPid, CTag, QStates0) ->
+ rabbit_queue_type:dequeue(Q, NoAck, LimiterPid, CTag, QStates0).
-basic_get(Q, ChPid, NoAck, LimiterPid, _CTag, _)
- when ?amqqueue_is_classic(Q) ->
- QPid = amqqueue:get_pid(Q),
- delegate:invoke(QPid, {gen_server2, call,
- [{basic_get, ChPid, NoAck, LimiterPid}, infinity]});
-basic_get(Q, _ChPid, NoAck, _LimiterPid, CTag, QStates)
- when ?amqqueue_is_quorum(Q) ->
- {Name, _} = Id = amqqueue:get_pid(Q),
- QName = amqqueue:get_name(Q),
- QState0 = get_quorum_state(Id, QName, QStates),
- case rabbit_quorum_queue:basic_get(Q, NoAck, CTag, QState0) of
- {ok, empty, QState} ->
- {empty, maps:put(Name, QState, QStates)};
- {ok, Count, Msg, QState} ->
- {ok, Count, Msg, maps:put(Name, QState, QStates)};
- {error, Reason} ->
- rabbit_misc:protocol_error(internal_error,
- "Cannot get a message from quorum queue '~s': ~p",
- [rabbit_misc:rs(QName), Reason])
- end.
-
--spec basic_consume
- (amqqueue:amqqueue(), boolean(), pid(), pid(), boolean(),
- non_neg_integer(), rabbit_types:ctag(), boolean(),
- rabbit_framing:amqp_table(), any(), rabbit_types:username(),
- #{Name :: atom() => rabbit_fifo_client:state()}) ->
- rabbit_types:ok_or_error('exclusive_consume_unavailable').
+-spec basic_consume(amqqueue:amqqueue(), boolean(), pid(), pid(), boolean(),
+ non_neg_integer(), rabbit_types:ctag(), boolean(),
+ rabbit_framing:amqp_table(), any(), rabbit_types:username(),
+ rabbit_queue_type:state()) ->
+ {ok, rabbit_queue_type:state(), rabbit_queue_type:actions()} |
+ {error, term()}.
basic_consume(Q, NoAck, ChPid, LimiterPid,
LimiterActive, ConsumerPrefetchCount, ConsumerTag,
- ExclusiveConsume, Args, OkMsg, ActingUser, QState)
- when ?amqqueue_is_classic(Q) ->
- QPid = amqqueue:get_pid(Q),
+ ExclusiveConsume, Args, OkMsg, ActingUser, Contexts) ->
QName = amqqueue:get_name(Q),
+ %% first phase argument validation
+ %% each queue type may do further validations
ok = check_consume_arguments(QName, Args),
- case delegate:invoke(QPid,
- {gen_server2, call,
- [{basic_consume, NoAck, ChPid, LimiterPid, LimiterActive,
- ConsumerPrefetchCount, ConsumerTag, ExclusiveConsume,
- Args, OkMsg, ActingUser}, infinity]}) of
- ok ->
- {ok, QState};
- Err ->
- Err
- end;
-basic_consume(Q, _NoAck, _ChPid,
- _LimiterPid, true, _ConsumerPrefetchCount, _ConsumerTag,
- _ExclusiveConsume, _Args, _OkMsg, _ActingUser, _QStates)
- when ?amqqueue_is_quorum(Q) ->
- {error, global_qos_not_supported_for_queue_type};
-basic_consume(Q,
- NoAck, ChPid, _LimiterPid, _LimiterActive, ConsumerPrefetchCount,
- ConsumerTag, ExclusiveConsume, Args, OkMsg,
- ActingUser, QStates)
- when ?amqqueue_is_quorum(Q) ->
- {Name, _} = Id = amqqueue:get_pid(Q),
- QName = amqqueue:get_name(Q),
- ok = check_consume_arguments(QName, Args),
- QState0 = get_quorum_state(Id, QName, QStates),
- case rabbit_quorum_queue:basic_consume(Q, NoAck, ChPid,
- ConsumerPrefetchCount,
- ConsumerTag,
- ExclusiveConsume, Args,
- ActingUser,
- OkMsg, QState0) of
- {ok, QState} ->
- {ok, maps:put(Name, QState, QStates)};
- {error, Reason} ->
- rabbit_misc:protocol_error(internal_error,
- "Cannot consume a message from quorum queue '~s': ~w",
- [rabbit_misc:rs(QName), Reason])
- end.
-
--spec basic_cancel
- (amqqueue:amqqueue(), pid(), rabbit_types:ctag(), any(),
- rabbit_types:username(), #{Name :: atom() => rabbit_fifo_client:state()}) ->
- 'ok' | {'ok', #{Name :: atom() => rabbit_fifo_client:state()}}.
-
-basic_cancel(Q, ChPid, ConsumerTag, OkMsg, ActingUser,
- QState)
- when ?amqqueue_is_classic(Q) ->
- QPid = amqqueue:get_pid(Q),
- case delegate:invoke(QPid, {gen_server2, call,
- [{basic_cancel, ChPid, ConsumerTag, OkMsg, ActingUser},
- infinity]}) of
- ok ->
- {ok, QState};
- Err -> Err
- end;
-basic_cancel(Q, ChPid,
- ConsumerTag, OkMsg, _ActingUser, QStates)
- when ?amqqueue_is_quorum(Q) ->
- {Name, _} = Id = amqqueue:get_pid(Q),
- QState0 = get_quorum_state(Id, QStates),
- {ok, QState} = rabbit_quorum_queue:basic_cancel(ConsumerTag, ChPid, OkMsg, QState0),
- {ok, maps:put(Name, QState, QStates)}.
+ Spec = #{no_ack => NoAck,
+ channel_pid => ChPid,
+ limiter_pid => LimiterPid,
+ limiter_active => LimiterActive,
+ prefetch_count => ConsumerPrefetchCount,
+ consumer_tag => ConsumerTag,
+ exclusive_consume => ExclusiveConsume,
+ args => Args,
+ ok_msg => OkMsg,
+ acting_user => ActingUser},
+ rabbit_queue_type:consume(Q, Spec, Contexts).
+
+-spec basic_cancel(amqqueue:amqqueue(), rabbit_types:ctag(), any(),
+ rabbit_types:username(),
+ rabbit_queue_type:state()) ->
+ {ok, rabbit_queue_type:state()} | {error, term()}.
+basic_cancel(Q, ConsumerTag, OkMsg, ActingUser, QStates) ->
+ rabbit_queue_type:cancel(Q, ConsumerTag,
+ OkMsg, ActingUser, QStates).
-spec notify_decorators(amqqueue:amqqueue()) -> 'ok'.
@@ -1796,7 +1535,8 @@ notify_sent_queue_down(QPid) ->
-spec resume(pid(), pid()) -> 'ok'.
-resume(QPid, ChPid) -> delegate:invoke_no_result(QPid, {gen_server2, cast, [{resume, ChPid}]}).
+resume(QPid, ChPid) -> delegate:invoke_no_result(QPid, {gen_server2, cast,
+ [{resume, ChPid}]}).
internal_delete1(QueueName, OnlyDurable) ->
internal_delete1(QueueName, OnlyDurable, normal).
@@ -1862,10 +1602,9 @@ forget_all_durable(Node) ->
%% Try to promote a mirror while down - it should recover as a
%% master. We try to take the oldest mirror here for best chance of
%% recovery.
-forget_node_for_queue(DeadNode, Q)
+forget_node_for_queue(_DeadNode, Q)
when ?amqqueue_is_quorum(Q) ->
- QN = get_quorum_nodes(Q),
- forget_node_for_queue(DeadNode, QN, Q);
+ ok;
forget_node_for_queue(DeadNode, Q) ->
RS = amqqueue:get_recoverable_slaves(Q),
forget_node_for_queue(DeadNode, RS, Q).
@@ -1887,7 +1626,7 @@ forget_node_for_queue(DeadNode, [H|T], Q) when ?is_amqqueue(Q) ->
{false, _} -> forget_node_for_queue(DeadNode, T, Q);
{true, rabbit_classic_queue} ->
Q1 = amqqueue:set_pid(Q, rabbit_misc:node_to_fake_pid(H)),
- ok = mnesia:write(rabbit_durable_queue, Q1, write);
+ ok = mnesia:write(rabbit_durable_queue, Q1, write);
{true, rabbit_quorum_queue} ->
ok
end.
@@ -2109,110 +1848,7 @@ pseudo_queue(#resource{kind = queue} = QueueName, Pid, Durable)
immutable(Q) -> amqqueue:set_immutable(Q).
--spec deliver([amqqueue:amqqueue()], rabbit_types:delivery()) -> 'ok'.
-
-deliver(Qs, Delivery) ->
- deliver(Qs, Delivery, untracked),
- ok.
-
--spec deliver([amqqueue:amqqueue()],
- rabbit_types:delivery(),
- quorum_states() | 'untracked') ->
- {qpids(),
- [{amqqueue:ra_server_id(), name()}],
- quorum_states()}.
-
-deliver([], _Delivery, QueueState) ->
- %% /dev/null optimisation
- {[], [], QueueState};
-
-deliver(Qs, Delivery = #delivery{flow = Flow,
- confirm = Confirm}, QueueState0) ->
- {Quorum, MPids, SPids} = qpids(Qs),
- QPids = MPids ++ SPids,
- %% We use up two credits to send to a mirror since the message
- %% arrives at the mirror from two directions. We will ack one when
- %% the mirror receives the message direct from the channel, and the
- %% other when it receives it via GM.
-
- case Flow of
- %% Here we are tracking messages sent by the rabbit_channel
- %% process. We are accessing the rabbit_channel process
- %% dictionary.
- flow -> [credit_flow:send(QPid) || QPid <- QPids],
- [credit_flow:send(QPid) || QPid <- SPids];
- noflow -> ok
- end,
-
- %% We let mirrors know that they were being addressed as mirrors at
- %% the time - if they receive such a message from the channel
- %% after they have become master they should mark the message as
- %% 'delivered' since they do not know what the master may have
- %% done with it.
- MMsg = {deliver, Delivery, false},
- SMsg = {deliver, Delivery, true},
- delegate:invoke_no_result(MPids, {gen_server2, cast, [MMsg]}),
- delegate:invoke_no_result(SPids, {gen_server2, cast, [SMsg]}),
- QueueState =
- case QueueState0 of
- untracked ->
- lists:foreach(
- fun({Pid, _QName}) ->
- rabbit_quorum_queue:stateless_deliver(Pid, Delivery)
- end, Quorum),
- untracked;
- _ ->
- lists:foldl(
- fun({{Name, _} = Pid, QName}, QStates) ->
- QState0 = get_quorum_state(Pid, QName, QStates),
- case rabbit_quorum_queue:deliver(Confirm, Delivery,
- QState0) of
- {ok, QState} ->
- maps:put(Name, QState, QStates);
- {slow, QState} ->
- maps:put(Name, QState, QStates)
- end
- end, QueueState0, Quorum)
- end,
- {QuorumPids, _} = lists:unzip(Quorum),
- {QPids, QuorumPids, QueueState}.
-
-qpids([]) -> {[], [], []}; %% optimisation
-qpids([Q]) when ?amqqueue_is_quorum(Q) ->
- QName = amqqueue:get_name(Q),
- {LocalName, LeaderNode} = amqqueue:get_pid(Q),
- {[{{LocalName, LeaderNode}, QName}], [], []}; %% opt
-qpids([Q]) ->
- QPid = amqqueue:get_pid(Q),
- SPids = amqqueue:get_slave_pids(Q),
- {[], [QPid], SPids}; %% opt
-qpids(Qs) ->
- {QuoPids, MPids, SPids} =
- lists:foldl(fun (Q,
- {QuoPidAcc, MPidAcc, SPidAcc})
- when ?amqqueue_is_quorum(Q) ->
- QPid = amqqueue:get_pid(Q),
- QName = amqqueue:get_name(Q),
- {[{QPid, QName} | QuoPidAcc], MPidAcc, SPidAcc};
- (Q,
- {QuoPidAcc, MPidAcc, SPidAcc}) ->
- QPid = amqqueue:get_pid(Q),
- SPids = amqqueue:get_slave_pids(Q),
- {QuoPidAcc, [QPid | MPidAcc], [SPids | SPidAcc]}
- end, {[], [], []}, Qs),
- {QuoPids, MPids, lists:append(SPids)}.
-
-get_quorum_state({Name, _} = Id, QName, Map) ->
- case maps:find(Name, Map) of
- {ok, S} -> S;
- error ->
- rabbit_quorum_queue:init_state(Id, QName)
- end.
-
-get_quorum_state({Name, _}, Map) ->
- maps:get(Name, Map).
-
-get_quorum_nodes(Q) when ?is_amqqueue(Q) ->
+get_quorum_nodes(Q) ->
case amqqueue:get_type_state(Q) of
#{nodes := Nodes} ->
Nodes;
diff --git a/src/rabbit_amqqueue_process.erl b/src/rabbit_amqqueue_process.erl
index 0da33acd4c..888bd04416 100644
--- a/src/rabbit_amqqueue_process.erl
+++ b/src/rabbit_amqqueue_process.erl
@@ -425,19 +425,9 @@ process_args_policy(State = #q{q = Q,
{<<"queue-mode">>, fun res_arg/2, fun init_queue_mode/2}],
drop_expired_msgs(
lists:foldl(fun({Name, Resolve, Fun}, StateN) ->
- Fun(args_policy_lookup(Name, Resolve, Q), StateN)
+ Fun(rabbit_queue_type_util:args_policy_lookup(Name, Resolve, Q), StateN)
end, State#q{args_policy_version = N + 1}, ArgsTable)).
-args_policy_lookup(Name, Resolve, Q) ->
- Args = amqqueue:get_arguments(Q),
- AName = <<"x-", Name/binary>>,
- case {rabbit_policy:get(Name, Q), rabbit_misc:table_lookup(Args, AName)} of
- {undefined, undefined} -> undefined;
- {undefined, {_Type, Val}} -> Val;
- {Val, undefined} -> Val;
- {PolVal, {_Type, ArgVal}} -> Resolve(PolVal, ArgVal)
- end.
-
res_arg(_PolVal, ArgVal) -> ArgVal.
res_min(PolVal, ArgVal) -> erlang:min(PolVal, ArgVal).
@@ -498,12 +488,13 @@ noreply(NewState) ->
{NewState1, Timeout} = next_state(NewState),
{noreply, ensure_stats_timer(ensure_rate_timer(NewState1)), Timeout}.
-next_state(State = #q{backing_queue = BQ,
+next_state(State = #q{q = Q,
+ backing_queue = BQ,
backing_queue_state = BQS,
msg_id_to_channel = MTC}) ->
assert_invariant(State),
{MsgIds, BQS1} = BQ:drain_confirmed(BQS),
- MTC1 = confirm_messages(MsgIds, MTC),
+ MTC1 = confirm_messages(MsgIds, MTC, amqqueue:get_name(Q)),
State1 = State#q{backing_queue_state = BQS1, msg_id_to_channel = MTC1},
case BQ:needs_timeout(BQS1) of
false -> {stop_sync_timer(State1), hibernate };
@@ -586,9 +577,9 @@ maybe_send_drained(WasEmpty, State) ->
end,
State.
-confirm_messages([], MTC) ->
+confirm_messages([], MTC, _QName) ->
MTC;
-confirm_messages(MsgIds, MTC) ->
+confirm_messages(MsgIds, MTC, QName) ->
{CMs, MTC1} =
lists:foldl(
fun(MsgId, {CMs, MTC0}) ->
@@ -608,7 +599,7 @@ confirm_messages(MsgIds, MTC) ->
end, {#{}, MTC}, MsgIds),
maps:fold(
fun(Pid, MsgSeqNos, _) ->
- rabbit_misc:confirm_to_sender(Pid, MsgSeqNos)
+ rabbit_misc:confirm_to_sender(Pid, QName, MsgSeqNos)
end,
ok,
CMs),
@@ -629,8 +620,9 @@ send_or_record_confirm(#delivery{confirm = true,
{eventually, State#q{msg_id_to_channel = MTC1}};
send_or_record_confirm(#delivery{confirm = true,
sender = SenderPid,
- msg_seq_no = MsgSeqNo}, State) ->
- rabbit_misc:confirm_to_sender(SenderPid, [MsgSeqNo]),
+ msg_seq_no = MsgSeqNo},
+ #q{q = Q} = State) ->
+ rabbit_misc:confirm_to_sender(SenderPid, amqqueue:get_name(Q), [MsgSeqNo]),
{immediately, State}.
%% This feature was used by `rabbit_amqqueue_process` and
@@ -648,9 +640,9 @@ send_mandatory(#delivery{mandatory = true,
discard(#delivery{confirm = Confirm,
sender = SenderPid,
flow = Flow,
- message = #basic_message{id = MsgId}}, BQ, BQS, MTC) ->
+ message = #basic_message{id = MsgId}}, BQ, BQS, MTC, QName) ->
MTC1 = case Confirm of
- true -> confirm_messages([MsgId], MTC);
+ true -> confirm_messages([MsgId], MTC, QName);
false -> MTC
end,
BQS1 = BQ:discard(MsgId, SenderPid, Flow, BQS),
@@ -679,7 +671,8 @@ run_message_queue(ActiveConsumersChanged, State) ->
attempt_delivery(Delivery = #delivery{sender = SenderPid,
flow = Flow,
message = Message},
- Props, Delivered, State = #q{backing_queue = BQ,
+ Props, Delivered, State = #q{q = Q,
+ backing_queue = BQ,
backing_queue_state = BQS,
msg_id_to_channel = MTC}) ->
case rabbit_queue_consumers:deliver(
@@ -689,7 +682,7 @@ attempt_delivery(Delivery = #delivery{sender = SenderPid,
Message, Props, SenderPid, Flow, BQS),
{{Message, Delivered, AckTag}, {BQS1, MTC}};
(false) -> {{Message, Delivered, undefined},
- discard(Delivery, BQ, BQS, MTC)}
+ discard(Delivery, BQ, BQS, MTC, amqqueue:get_name(Q))}
end, qname(State), State#q.consumers, State#q.single_active_consumer_on, State#q.active_consumer) of
{delivered, ActiveConsumersChanged, {BQS1, MTC1}, Consumers} ->
{delivered, maybe_notify_decorators(
@@ -745,7 +738,7 @@ deliver_or_enqueue(Delivery = #delivery{message = Message,
sender = SenderPid,
flow = Flow},
Delivered,
- State = #q{backing_queue = BQ}) ->
+ State = #q{q = Q, backing_queue = BQ}) ->
{Confirm, State1} = send_or_record_confirm(Delivery, State),
Props = message_properties(Message, Confirm, State1),
case attempt_delivery(Delivery, Props, Delivered, State1) of
@@ -755,7 +748,7 @@ deliver_or_enqueue(Delivery = #delivery{message = Message,
{undelivered, State2 = #q{ttl = 0, dlx = undefined,
backing_queue_state = BQS,
msg_id_to_channel = MTC}} ->
- {BQS1, MTC1} = discard(Delivery, BQ, BQS, MTC),
+ {BQS1, MTC1} = discard(Delivery, BQ, BQS, MTC, amqqueue:get_name(Q)),
State2#q{backing_queue_state = BQS1, msg_id_to_channel = MTC1};
{undelivered, State2 = #q{backing_queue_state = BQS}} ->
@@ -809,10 +802,11 @@ send_reject_publish(#delivery{confirm = true,
msg_seq_no = MsgSeqNo,
message = #basic_message{id = MsgId}},
_Delivered,
- State = #q{ backing_queue = BQ,
+ State = #q{ q = Q,
+ backing_queue = BQ,
backing_queue_state = BQS,
msg_id_to_channel = MTC}) ->
- gen_server2:cast(SenderPid, {reject_publish, MsgSeqNo, self()}),
+ gen_server2:cast(SenderPid, {queue_event, Q, {reject_publish, MsgSeqNo, self()}}),
MTC1 = maps:remove(MsgId, MTC),
BQS1 = BQ:discard(MsgId, SenderPid, Flow, BQS),
@@ -1273,7 +1267,7 @@ handle_call({init, Recover}, From, State) ->
end;
handle_call(info, _From, State) ->
- reply(infos(info_keys(), State), State);
+ reply({ok, infos(info_keys(), State)}, State);
handle_call({info, Items}, _From, State) ->
try
@@ -1547,7 +1541,7 @@ handle_cast({deliver,
noreply(maybe_deliver_or_enqueue(Delivery, SlaveWhenPublished, State1));
%% [0] The second ack is since the channel thought we were a mirror at
%% the time it published this message, so it used two credits (see
-%% rabbit_amqqueue:deliver/2).
+%% rabbit_queue_type:deliver/2).
handle_cast({ack, AckTags, ChPid}, State) ->
noreply(ack(AckTags, ChPid, State));
diff --git a/src/rabbit_basic.erl b/src/rabbit_basic.erl
index 9c5a5c8775..92b137c97a 100644
--- a/src/rabbit_basic.erl
+++ b/src/rabbit_basic.erl
@@ -69,7 +69,7 @@ publish(Delivery = #delivery{
publish(X, Delivery) ->
Qs = rabbit_amqqueue:lookup(rabbit_exchange:route(X, Delivery)),
- rabbit_amqqueue:deliver(Qs, Delivery).
+ rabbit_queue_type:deliver(Qs, Delivery, stateless).
-spec delivery
(boolean(), boolean(), rabbit_types:message(), undefined | integer()) ->
diff --git a/src/rabbit_channel.erl b/src/rabbit_channel.erl
index 1755b4b8e2..f20e7463c3 100644
--- a/src/rabbit_channel.erl
+++ b/src/rabbit_channel.erl
@@ -120,6 +120,12 @@
writer_gc_threshold
}).
+-record(pending_ack, {delivery_tag,
+ tag,
+ delivered_at,
+ queue, %% queue name
+ msg_id}).
+
-record(ch, {cfg :: #conf{},
%% limiter state, see rabbit_limiter
limiter,
@@ -129,8 +135,6 @@
next_tag,
%% messages pending consumer acknowledgement
unacked_message_q,
- %% a map of queue ref to queue name
- queue_names,
%% queue processes are monitored to update
%% queue names
queue_monitors,
@@ -140,9 +144,6 @@
consumer_mapping,
%% a map of queue pids to consumer tag lists
queue_consumers,
- %% a set of pids of queues that have unacknowledged
- %% deliveries
- delivering_queues,
%% timer used to emit statistics
stats_timer,
%% are publisher confirms enabled for this channel?
@@ -189,6 +190,7 @@
state,
garbage_collection]).
+
-define(CREATION_EVENT_KEYS,
[pid,
name,
@@ -217,9 +219,6 @@
put({Type, Key}, none)
end).
--define(IS_CLASSIC(QPid), is_pid(QPid)).
--define(IS_QUORUM(QPid), is_tuple(QPid)).
-
%%----------------------------------------------------------------------------
-export_type([channel_number/0]).
@@ -295,7 +294,7 @@ send_command(Pid, Msg) ->
(pid(), rabbit_types:ctag(), boolean(), rabbit_amqqueue:qmsg()) -> 'ok'.
deliver(Pid, ConsumerTag, AckRequired, Msg) ->
- gen_server2:cast(Pid, {deliver, ConsumerTag, AckRequired, Msg}).
+ gen_server2:cast(Pid, {deliver, ConsumerTag, AckRequired, [Msg]}).
-spec deliver_reply(binary(), rabbit_types:delivery()) -> 'ok'.
@@ -527,20 +526,18 @@ init([Channel, ReaderPid, WriterPid, ConnPid, ConnName, Protocol, User, VHost,
tx = none,
next_tag = 1,
unacked_message_q = ?QUEUE:new(),
- queue_names = #{},
queue_monitors = pmon:new(),
consumer_mapping = #{},
queue_consumers = #{},
- delivering_queues = sets:new(),
confirm_enabled = false,
publish_seqno = 1,
- unconfirmed = unconfirmed_messages:new(),
+ unconfirmed = rabbit_confirms:init(),
rejected = [],
confirmed = [],
reply_consumer = none,
delivery_flow = Flow,
interceptor_state = undefined,
- queue_states = #{}
+ queue_states = rabbit_queue_type:init()
},
State1 = State#ch{
interceptor_state = rabbit_channel_interceptor:init(State)},
@@ -566,6 +563,8 @@ prioritise_cast(Msg, _Len, _State) ->
case Msg of
{confirm, _MsgSeqNos, _QPid} -> 5;
{reject_publish, _MsgSeqNos, _QPid} -> 5;
+ {queue_event, _, {confirm, _MsgSeqNos, _QPid}} -> 5;
+ {queue_event, _, {reject_publish, _MsgSeqNos, _QPid}} -> 5;
_ -> 0
end.
@@ -611,7 +610,8 @@ handle_call({declare_fast_reply_to, Key}, _From,
handle_call(list_queue_states, _From, State = #ch{queue_states = QueueStates}) ->
%% For testing of cleanup only
- {reply, maps:keys(QueueStates), State};
+ %% HACK
+ {reply, maps:keys(element(2, QueueStates)), State};
handle_call(_Request, _From, State) ->
noreply(State).
@@ -666,6 +666,7 @@ handle_cast({deliver, _CTag, _AckReq, _Msg},
State = #ch{cfg = #conf{state = closing}}) ->
noreply(State);
handle_cast({deliver, ConsumerTag, AckRequired, Msg}, State) ->
+ % TODO: handle as action
noreply(handle_deliver(ConsumerTag, AckRequired, Msg, State));
handle_cast({deliver_reply, _K, _Del},
@@ -724,93 +725,40 @@ handle_cast({mandatory_received, _MsgSeqNo}, State) ->
%% NB: don't call noreply/1 since we don't want to send confirms.
noreply_coalesce(State);
-handle_cast({reject_publish, MsgSeqNo, _QPid}, State = #ch{unconfirmed = UC}) ->
- %% It does not matter which queue rejected the message,
- %% 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.
- 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({reject_publish, _MsgSeqNo, QPid} = Evt, State) ->
+ %% For backwards compatibility
+ QRef = find_queue_name_from_pid(QPid, State#ch.queue_states),
+ handle_cast({queue_event, QRef, Evt}, State);
+
+handle_cast({confirm, _MsgSeqNo, QPid} = Evt, State) ->
+ %% For backwards compatibility
+ QRef = find_queue_name_from_pid(QPid, State#ch.queue_states),
+ handle_cast({queue_event, QRef, Evt}, State);
+
+handle_cast({queue_event, QRef, Evt},
+ #ch{queue_states = QueueStates0} = State0) ->
+ case rabbit_queue_type:handle_event(QRef, Evt, QueueStates0) of
+ {ok, QState1, Actions} ->
+ State1 = State0#ch{queue_states = QState1},
+ State = handle_queue_actions(Actions, State1),
+ noreply_coalesce(State);
+ eol ->
+ State1 = handle_consuming_queue_down_or_eol(QRef, QRef, State0),
+ {ConfirmMXs, UC1} =
+ rabbit_confirms:remove_queue(QRef, State1#ch.unconfirmed),
+ %% Deleted queue is a special case.
+ %% Do not nack the "rejected" messages.
+ State2 = record_confirms(ConfirmMXs,
+ State1#ch{unconfirmed = UC1}),
+ erase_queue_stats(QRef),
+ noreply_coalesce(
+ State2#ch{queue_states = rabbit_queue_type:remove(QRef, QueueStates0)})
+ end.
-handle_cast({confirm, MsgSeqNos, QPid}, State) ->
- noreply_coalesce(confirm(MsgSeqNos, QPid, State)).
-
-handle_info({ra_event, {Name, _} = From, _} = Evt,
- #ch{queue_states = QueueStates,
- queue_names = QNames,
- consumer_mapping = ConsumerMapping} = State0) ->
- case QueueStates of
- #{Name := QState0} ->
- QName = rabbit_quorum_queue:queue_name(QState0),
- case rabbit_quorum_queue:handle_event(Evt, QState0) of
- {{delivery, CTag, Msgs}, QState1} ->
- AckRequired = case maps:find(CTag, ConsumerMapping) of
- error ->
- true;
- {ok, {_, {NoAck, _, _, _}}} ->
- not NoAck
- end,
- QState2 = case AckRequired of
- false ->
- {MsgIds, _} = lists:unzip(Msgs),
- {ok, FS} = rabbit_quorum_queue:ack(CTag, MsgIds, QState1),
- FS;
- true ->
- QState1
- end,
- State = lists:foldl(
- fun({MsgId, {MsgHeader, Msg}}, Acc) ->
- IsDelivered = case MsgHeader of
- #{delivery_count := _} ->
- true;
- _ ->
- false
- end,
- Msg1 = add_delivery_count_header(MsgHeader, Msg),
- handle_deliver(CTag, AckRequired,
- {QName, From, MsgId, IsDelivered, Msg1},
- Acc)
- end, State0#ch{queue_states = maps:put(Name, QState2, QueueStates)}, Msgs),
- noreply(State);
- {internal, MsgSeqNos, Actions, QState1} ->
- State = State0#ch{queue_states = maps:put(Name, QState1, QueueStates)},
- %% execute actions
- WriterPid = State#ch.cfg#conf.writer_pid,
- lists:foreach(fun ({send_credit_reply, Avail}) ->
- ok = rabbit_writer:send_command(
- WriterPid,
- #'basic.credit_ok'{available =
- Avail});
- ({send_drained, {CTag, Credit}}) ->
- ok = rabbit_writer:send_command(
- WriterPid,
- #'basic.credit_drained'{consumer_tag = CTag,
- credit_drained = Credit})
- end, Actions),
- noreply_coalesce(confirm(MsgSeqNos, Name, State));
- eol ->
- State1 = handle_consuming_queue_down_or_eol(Name, State0),
- State2 = handle_delivering_queue_down(Name, State1),
- {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),
- queue_names = maps:remove(Name, QNames)})
- end;
- _ ->
- %% the assumption here is that the queue state has been cleaned up and
- %% this is a residual Ra notification
- noreply_coalesce(State0)
- end;
+handle_info({ra_event, {Name, _} = From, Evt}, State) ->
+ %% For backwards compatibility
+ QRef = find_queue_name_from_quorum_name(Name, State#ch.queue_states),
+ handle_cast({queue_event, QRef, {From, Evt}}, State);
handle_info({bump_credit, Msg}, State) ->
%% A rabbit_amqqueue_process is granting credit to our channel. If
@@ -832,23 +780,26 @@ handle_info(emit_stats, State) ->
%% stats timer.
{noreply, send_confirms_and_nacks(State1), hibernate};
-handle_info({'DOWN', _MRef, process, QPid, Reason}, State) ->
- State1 = handle_publishing_queue_down(QPid, Reason, State),
- State3 = handle_consuming_queue_down_or_eol(QPid, State1),
- State4 = handle_delivering_queue_down(QPid, State3),
- %% A rabbit_amqqueue_process has died. If our channel was being
- %% blocked by this process, and no other process is blocking our
- %% channel, then this channel will be unblocked. This means that
- %% any credit that was deferred will be sent to the rabbit_reader
- %% processs that might be blocked by this particular channel.
- credit_flow:peer_down(QPid),
- #ch{queue_names = QNames, queue_monitors = QMons} = State4,
- case maps:find(QPid, QNames) of
- {ok, QName} -> erase_queue_stats(QName);
- error -> ok
- end,
- noreply(State4#ch{queue_names = maps:remove(QPid, QNames),
- queue_monitors = pmon:erase(QPid, QMons)});
+handle_info({'DOWN', _MRef, process, QPid, Reason},
+ #ch{queue_states = QStates0,
+ queue_monitors = _QMons} = State0) ->
+ case rabbit_queue_type:handle_down(QPid, Reason, QStates0) of
+ {ok, QState1, Actions} ->
+ State1 = State0#ch{queue_states = QState1},
+ State = handle_queue_actions(Actions, State1),
+ noreply_coalesce(State);
+ {eol, QRef} ->
+ State1 = handle_consuming_queue_down_or_eol(QRef, QRef, State0),
+ {ConfirmMXs, UC1} =
+ rabbit_confirms:remove_queue(QRef, State1#ch.unconfirmed),
+ %% Deleted queue is a special case.
+ %% Do not nack the "rejected" messages.
+ State2 = record_confirms(ConfirmMXs,
+ State1#ch{unconfirmed = UC1}),
+ erase_queue_stats(QRef),
+ noreply_coalesce(
+ State2#ch{queue_states = rabbit_queue_type:remove(QRef, QStates0)})
+ end;
handle_info({'EXIT', _Pid, Reason}, State) ->
{stop, Reason, State};
@@ -865,12 +816,7 @@ handle_info(tick, State0 = #ch{queue_states = QueueStates0}) ->
true -> ok = clear_permission_cache();
_ -> ok
end,
- QueueStates1 =
- maps:filter(fun(_, QS) ->
- QName = rabbit_quorum_queue:queue_name(QS),
- [] /= rabbit_amqqueue:lookup([QName])
- end, QueueStates0),
- case evaluate_consumer_timeout(State0#ch{queue_states = QueueStates1}) of
+ case evaluate_consumer_timeout(State0#ch{queue_states = QueueStates0}) of
{noreply, State} ->
noreply(init_tick_timer(reset_tick_timer(State)));
Return ->
@@ -896,7 +842,9 @@ handle_post_hibernate(State0) ->
{noreply, State}.
terminate(_Reason,
- State = #ch{cfg = #conf{user = #user{username = Username}}}) ->
+ State = #ch{cfg = #conf{user = #user{username = Username}},
+ queue_states = QueueCtxs}) ->
+ _ = rabbit_queue_type:close(QueueCtxs),
{_Res, _State1} = notify_queues(State),
pg_local:leave(rabbit_channels, self()),
rabbit_event:if_enabled(State, #ch.stats_timer,
@@ -1357,7 +1305,8 @@ handle_method(#'basic.ack'{delivery_tag = DeliveryTag,
{Acked, Remaining} = collect_acks(UAMQ, DeliveryTag, Multiple),
State1 = State#ch{unacked_message_q = Remaining},
{noreply, case Tx of
- none -> ack(Acked, State1);
+ none -> {State2, Actions} = ack(Acked, State1),
+ handle_queue_actions(Actions, State2);
{Msgs, Acks} -> Acks1 = ack_cons(ack, Acked, Acks),
State1#ch{tx = {Msgs, Acks1}}
end};
@@ -1377,12 +1326,11 @@ handle_method(#'basic.get'{queue = QueueNameBin, no_ack = NoAck},
case rabbit_amqqueue:with_exclusive_access_or_die(
QueueName, ConnPid,
%% Use the delivery tag as consumer tag for quorum queues
- fun (Q) -> rabbit_amqqueue:basic_get(
- Q, self(), NoAck, rabbit_limiter:pid(Limiter),
- DeliveryTag, QueueStates0)
+ fun (Q) ->
+ rabbit_amqqueue:basic_get(
+ Q, NoAck, rabbit_limiter:pid(Limiter),
+ DeliveryTag, QueueStates0)
end) of
- {ok, MessageCount, Msg} ->
- handle_basic_get(WriterPid, DeliveryTag, NoAck, MessageCount, Msg, State);
{ok, MessageCount, Msg, QueueStates} ->
handle_basic_get(WriterPid, DeliveryTag, NoAck, MessageCount, Msg,
State#ch{queue_states = QueueStates});
@@ -1391,7 +1339,18 @@ handle_method(#'basic.get'{queue = QueueNameBin, no_ack = NoAck},
{reply, #'basic.get_empty'{}, State#ch{queue_states = QueueStates}};
empty ->
?INCR_STATS(queue_stats, QueueName, 1, get_empty, State),
- {reply, #'basic.get_empty'{}, State}
+ {reply, #'basic.get_empty'{}, State};
+ {error, {unsupported, single_active_consumer}} ->
+ rabbit_misc:protocol_error(
+ resource_locked,
+ "cannot obtain access to locked ~s. basic.get operations "
+ "are not supported by quorum queues with single active consumer",
+ [rabbit_misc:rs(QueueName)]);
+ {error, Reason} ->
+ %% TODO add queue type to error message
+ rabbit_misc:protocol_error(internal_error,
+ "Cannot get a message from queue '~s': ~p",
+ [rabbit_misc:rs(QueueName), Reason])
end;
handle_method(#'basic.consume'{queue = <<"amq.rabbitmq.reply-to">>,
@@ -1521,7 +1480,7 @@ handle_method(#'basic.cancel'{consumer_tag = ConsumerTag, nowait = NoWait},
fun () -> {error, not_found} end,
fun () ->
rabbit_amqqueue:basic_cancel(
- Q, self(), ConsumerTag, ok_msg(NoWait, OkMsg),
+ Q, ConsumerTag, ok_msg(NoWait, OkMsg),
Username, QueueStates0)
end) of
{ok, QueueStates} ->
@@ -1561,31 +1520,33 @@ handle_method(#'basic.qos'{global = true,
case ((not rabbit_limiter:is_active(Limiter)) andalso
rabbit_limiter:is_active(Limiter1)) of
true -> rabbit_amqqueue:activate_limit_all(
- consumer_queue_refs(State#ch.consumer_mapping), self());
+ classic_consumer_queue_pids(State#ch.consumer_mapping), self());
false -> ok
end,
{reply, #'basic.qos_ok'{}, State#ch{limiter = Limiter1}};
handle_method(#'basic.recover_async'{requeue = true},
- _, State = #ch{unacked_message_q = UAMQ, limiter = Limiter,
+ _, State = #ch{unacked_message_q = UAMQ,
+ limiter = Limiter,
queue_states = QueueStates0}) ->
OkFun = fun () -> ok end,
UAMQL = ?QUEUE:to_list(UAMQ),
- QueueStates =
+ {QueueStates, Actions} =
foreach_per_queue(
- fun ({QPid, CTag}, MsgIds, Acc0) ->
+ fun ({QPid, CTag}, MsgIds, {Acc0, Actions0}) ->
rabbit_misc:with_exit_handler(
OkFun,
fun () ->
- rabbit_amqqueue:requeue(QPid, {CTag, MsgIds},
- self(), Acc0)
+ {ok, Acc, Act} = rabbit_amqqueue:requeue(QPid, {CTag, MsgIds}, Acc0),
+ {Acc, Act ++ Actions0}
end)
- end, lists:reverse(UAMQL), QueueStates0),
+ end, lists:reverse(UAMQL), {QueueStates0, []}),
ok = notify_limiter(Limiter, UAMQL),
+ State1 = handle_queue_actions(Actions, State#ch{unacked_message_q = ?QUEUE:new(),
+ queue_states = QueueStates}),
%% No answer required - basic.recover is the newer, synchronous
%% variant of this method
- {noreply, State#ch{unacked_message_q = ?QUEUE:new(),
- queue_states = QueueStates}};
+ {noreply, State1};
handle_method(#'basic.recover_async'{requeue = false}, _, _State) ->
rabbit_misc:protocol_error(not_implemented, "requeue=false", []);
@@ -1704,12 +1665,16 @@ handle_method(#'tx.commit'{}, _, State = #ch{tx = {Msgs, Acks},
limiter = Limiter}) ->
State1 = queue_fold(fun deliver_to_queues/2, State, Msgs),
Rev = fun (X) -> lists:reverse(lists:sort(X)) end,
- State2 = lists:foldl(fun ({ack, A}, Acc) ->
- ack(Rev(A), Acc);
- ({Requeue, A}, Acc) ->
- internal_reject(Requeue, Rev(A), Limiter, Acc)
- end, State1, lists:reverse(Acks)),
- {noreply, maybe_complete_tx(State2#ch{tx = committing})};
+ {State2, Actions2} =
+ lists:foldl(fun ({ack, A}, {Acc, Actions}) ->
+ {Acc0, Actions0} = ack(Rev(A), Acc),
+ {Acc0, Actions ++ Actions0};
+ ({Requeue, A}, {Acc, Actions}) ->
+ {Acc0, Actions0} = internal_reject(Requeue, Rev(A), Limiter, Acc),
+ {Acc0, Actions ++ Actions0}
+ end, {State1, []}, lists:reverse(Acks)),
+ State3 = handle_queue_actions(Actions2, State2),
+ {noreply, maybe_complete_tx(State3#ch{tx = committing})};
handle_method(#'tx.rollback'{}, _, #ch{tx = none}) ->
precondition_failed("channel is not transactional");
@@ -1741,8 +1706,7 @@ handle_method(#'basic.credit'{consumer_tag = CTag,
queue_states = QStates0}) ->
case maps:find(CTag, Consumers) of
{ok, {Q, _CParams}} ->
- {ok, QStates} = rabbit_amqqueue:credit(
- Q, self(), CTag, Credit, Drain, QStates0),
+ QStates = rabbit_amqqueue:credit(Q, CTag, Credit, Drain, QStates0),
{noreply, State#ch{queue_states = QStates}};
error -> precondition_failed(
"unknown consumer tag '~s'", [CTag])
@@ -1774,38 +1738,21 @@ basic_consume(QueueName, NoAck, ConsumerPrefetch, ActualConsumerTag,
ConsumerPrefetch, ActualConsumerTag,
ExclusiveConsume, Args,
ok_msg(NoWait, #'basic.consume_ok'{
- consumer_tag = ActualConsumerTag}),
+ consumer_tag = ActualConsumerTag}),
Username, QueueStates0),
Q}
end) of
- {{ok, QueueStates}, Q} when ?is_amqqueue(Q) ->
- QPid = amqqueue:get_pid(Q),
- QName = amqqueue:get_name(Q),
- CM1 = maps:put(
- ActualConsumerTag,
- {Q, {NoAck, ConsumerPrefetch, ExclusiveConsume, Args}},
- ConsumerMapping),
- State1 = track_delivering_queue(
- NoAck, QPid, QName,
- State#ch{consumer_mapping = CM1,
- queue_states = QueueStates}),
- {ok, case NoWait of
- true -> consumer_monitor(ActualConsumerTag, State1);
- false -> State1
- end};
- {ok, Q} when ?is_amqqueue(Q) ->
- QPid = amqqueue:get_pid(Q),
- QName = amqqueue:get_name(Q),
+ {{ok, QueueStates, Actions}, Q} when ?is_amqqueue(Q) ->
CM1 = maps:put(
ActualConsumerTag,
{Q, {NoAck, ConsumerPrefetch, ExclusiveConsume, Args}},
ConsumerMapping),
- State1 = track_delivering_queue(
- NoAck, QPid, QName,
- State#ch{consumer_mapping = CM1}),
+ State1 = State#ch{consumer_mapping = CM1,
+ queue_states = QueueStates},
+ State2 = handle_queue_actions(Actions, State1),
{ok, case NoWait of
- true -> consumer_monitor(ActualConsumerTag, State1);
- false -> State1
+ true -> consumer_monitor(ActualConsumerTag, State2);
+ false -> State2
end};
{{error, exclusive_consume_unavailable} = E, _Q} ->
E;
@@ -1818,82 +1765,34 @@ maybe_stat(true, _Q) -> {ok, 0, 0}.
consumer_monitor(ConsumerTag,
State = #ch{consumer_mapping = ConsumerMapping,
- queue_monitors = QMons,
queue_consumers = QCons}) ->
{Q, _} = maps:get(ConsumerTag, ConsumerMapping),
- QPid = amqqueue:get_pid(Q),
- QRef = qpid_to_ref(QPid),
+ QRef = amqqueue:get_name(Q),
CTags1 = case maps:find(QRef, QCons) of
{ok, CTags} -> gb_sets:insert(ConsumerTag, CTags);
error -> gb_sets:singleton(ConsumerTag)
end,
QCons1 = maps:put(QRef, CTags1, QCons),
- State#ch{queue_monitors = maybe_monitor(QRef, QMons),
- queue_consumers = QCons1}.
-
-track_delivering_queue(NoAck, QPid, QName,
- State = #ch{queue_names = QNames,
- queue_monitors = QMons,
- delivering_queues = DQ}) ->
- QRef = qpid_to_ref(QPid),
- State#ch{queue_names = maps:put(QRef, QName, QNames),
- queue_monitors = maybe_monitor(QRef, QMons),
- delivering_queues = case NoAck of
- true -> DQ;
- false -> sets:add_element(QRef, DQ)
- end}.
-
-handle_publishing_queue_down(QPid, Reason,
- State = #ch{unconfirmed = UC,
- queue_names = QNames})
- when ?IS_CLASSIC(QPid) ->
- 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) ->
- %% this should never happen after the queue type refactoring in 3.9
- State.
+ State#ch{queue_consumers = QCons1}.
-handle_consuming_queue_down_or_eol(QRef,
- State = #ch{queue_consumers = QCons,
- queue_names = QNames}) ->
+handle_consuming_queue_down_or_eol(QRef, QName,
+ State = #ch{queue_consumers = QCons}) ->
ConsumerTags = case maps:find(QRef, QCons) of
error -> gb_sets:new();
{ok, CTags} -> CTags
end,
gb_sets:fold(
fun (CTag, StateN = #ch{consumer_mapping = CMap}) ->
- QName = maps:get(QRef, QNames),
case queue_down_consumer_action(CTag, CMap) of
remove ->
cancel_consumer(CTag, QName, StateN);
{recover, {NoAck, ConsumerPrefetch, Exclusive, Args}} ->
- case catch basic_consume( %% [0]
+ case catch basic_consume(
QName, NoAck, ConsumerPrefetch, CTag,
Exclusive, Args, true, StateN) of
{ok, StateN1} -> StateN1;
- _ -> cancel_consumer(CTag, QName, StateN)
+ _Err ->
+ cancel_consumer(CTag, QName, StateN)
end
end
end, State#ch{queue_consumers = maps:remove(QRef, QCons)}, ConsumerTags).
@@ -1924,9 +1823,6 @@ queue_down_consumer_action(CTag, CMap) ->
_ -> {recover, ConsumeSpec}
end.
-handle_delivering_queue_down(QRef, State = #ch{delivering_queues = DQ}) ->
- State#ch{delivering_queues = sets:del_element(QRef, DQ)}.
-
binding_action(Fun, SourceNameBin0, DestinationType, DestinationNameBin0,
RoutingKey, Arguments, VHostPath, ConnPid, AuthzContext,
#user{username = Username} = User) ->
@@ -1993,24 +1889,32 @@ reject(DeliveryTag, Requeue, Multiple,
{Acked, Remaining} = collect_acks(UAMQ, DeliveryTag, Multiple),
State1 = State#ch{unacked_message_q = Remaining},
{noreply, case Tx of
- none -> internal_reject(Requeue, Acked, State1#ch.limiter, State1);
- {Msgs, Acks} -> Acks1 = ack_cons(Requeue, Acked, Acks),
- State1#ch{tx = {Msgs, Acks1}}
+ none ->
+ {State2, Actions} = internal_reject(Requeue, Acked, State1#ch.limiter, State1),
+ handle_queue_actions(Actions, State2);
+ {Msgs, Acks} ->
+ Acks1 = ack_cons(Requeue, Acked, Acks),
+ State1#ch{tx = {Msgs, Acks1}}
end}.
%% NB: Acked is in youngest-first order
internal_reject(Requeue, Acked, Limiter,
State = #ch{queue_states = QueueStates0}) ->
- QueueStates = foreach_per_queue(
- fun({QPid, CTag}, MsgIds, Acc0) ->
- rabbit_amqqueue:reject(QPid, Requeue, {CTag, MsgIds},
- self(), Acc0)
- end, Acked, QueueStates0),
+ {QueueStates, Actions} =
+ foreach_per_queue(
+ fun({QRef, CTag}, MsgIds, {Acc0, Actions0}) ->
+ Op = case Requeue of
+ false -> discard;
+ true -> requeue
+ end,
+ {ok, Acc, Actions} = rabbit_queue_type:settle(QRef, Op, CTag, MsgIds, Acc0),
+ {Acc, Actions0 ++ Actions}
+ end, Acked, {QueueStates0, []}),
ok = notify_limiter(Limiter, Acked),
- State#ch{queue_states = QueueStates}.
+ {State#ch{queue_states = QueueStates}, Actions}.
record_sent(Type, Tag, AckRequired,
- Msg = {QName, QPid, MsgId, Redelivered, _Message},
+ Msg = {QName, _QPid, MsgId, Redelivered, _Message},
State = #ch{cfg = #conf{channel = ChannelNum,
trace_state = TraceState,
user = #user{username = Username},
@@ -2032,9 +1936,14 @@ record_sent(Type, Tag, AckRequired,
DeliveredAt = os:system_time(millisecond),
rabbit_trace:tap_out(Msg, ConnName, ChannelNum, Username, TraceState),
UAMQ1 = case AckRequired of
- true -> ?QUEUE:in({DeliveryTag, Tag, DeliveredAt,
- {QPid, MsgId}}, UAMQ);
- false -> UAMQ
+ true ->
+ ?QUEUE:in(#pending_ack{delivery_tag = DeliveryTag,
+ tag = Tag,
+ delivered_at = DeliveredAt,
+ queue = QName,
+ msg_id = MsgId}, UAMQ);
+ false ->
+ UAMQ
end,
State#ch{unacked_message_q = UAMQ1, next_tag = DeliveryTag + 1}.
@@ -2046,16 +1955,16 @@ collect_acks(Q, DeliveryTag, Multiple) ->
collect_acks(ToAcc, PrefixAcc, Q, DeliveryTag, Multiple) ->
case ?QUEUE:out(Q) of
- {{value, UnackedMsg = {CurrentDeliveryTag, _ConsumerTag, _Time, _Msg}},
+ {{value, UnackedMsg = #pending_ack{delivery_tag = CurrentDeliveryTag}},
QTail} ->
if CurrentDeliveryTag == DeliveryTag ->
- {[UnackedMsg | ToAcc],
- case PrefixAcc of
- [] -> QTail;
- _ -> ?QUEUE:join(
+ {[UnackedMsg | ToAcc],
+ case PrefixAcc of
+ [] -> QTail;
+ _ -> ?QUEUE:join(
?QUEUE:from_list(lists:reverse(PrefixAcc)),
QTail)
- end};
+ end};
Multiple ->
collect_acks([UnackedMsg | ToAcc], PrefixAcc,
QTail, DeliveryTag, Multiple);
@@ -2068,24 +1977,21 @@ collect_acks(ToAcc, PrefixAcc, Q, DeliveryTag, Multiple) ->
end.
%% NB: Acked is in youngest-first order
-ack(Acked, State = #ch{queue_names = QNames,
- queue_states = QueueStates0}) ->
- QueueStates =
+ack(Acked, State = #ch{queue_states = QueueStates0}) ->
+ {QueueStates, Actions} =
foreach_per_queue(
- fun ({QPid, CTag}, MsgIds, Acc0) ->
- Acc = rabbit_amqqueue:ack(QPid, {CTag, MsgIds}, self(), Acc0),
- incr_queue_stats(QPid, QNames, MsgIds, State),
- Acc
- end, Acked, QueueStates0),
+ fun ({QRef, CTag}, MsgIds, {Acc0, ActionsAcc0}) ->
+ {ok, Acc, ActionsAcc} = rabbit_queue_type:settle(QRef, complete, CTag,
+ MsgIds, Acc0),
+ incr_queue_stats(QRef, MsgIds, State),
+ {Acc, ActionsAcc0 ++ ActionsAcc}
+ end, Acked, {QueueStates0, []}),
ok = notify_limiter(State#ch.limiter, Acked),
- State#ch{queue_states = QueueStates}.
+ {State#ch{queue_states = QueueStates}, Actions}.
-incr_queue_stats(QPid, QNames, MsgIds, State) ->
- case maps:find(qpid_to_ref(QPid), QNames) of
- {ok, QName} -> Count = length(MsgIds),
- ?INCR_STATS(queue_stats, QName, Count, ack, State);
- error -> ok
- end.
+incr_queue_stats(QName, MsgIds, State) ->
+ Count = length(MsgIds),
+ ?INCR_STATS(queue_stats, QName, Count, ack, State).
%% {Msgs, Acks}
%%
@@ -2104,31 +2010,32 @@ new_tx() -> {?QUEUE:new(), []}.
notify_queues(State = #ch{cfg = #conf{state = closing}}) ->
{ok, State};
notify_queues(State = #ch{consumer_mapping = Consumers,
- cfg = Cfg,
- delivering_queues = DQ }) ->
- QRefs0 = sets:to_list(
- sets:union(sets:from_list(consumer_queue_refs(Consumers)), DQ)),
- %% filter to only include pids to avoid trying to notify quorum queues
- QPids = [P || P <- QRefs0, ?IS_CLASSIC(P)],
+ cfg = Cfg}) ->
+ QPids = classic_consumer_queue_pids(Consumers),
Timeout = get_operation_timeout(),
{rabbit_amqqueue:notify_down_all(QPids, self(), Timeout),
State#ch{cfg = Cfg#conf{state = closing}}}.
foreach_per_queue(_F, [], Acc) ->
Acc;
-foreach_per_queue(F, [{_DTag, CTag, _Time, {QPid, MsgId}}], Acc) ->
+foreach_per_queue(F, [#pending_ack{tag = CTag,
+ queue = QName,
+ msg_id = MsgId}], Acc) ->
%% quorum queue, needs the consumer tag
- F({QPid, CTag}, [MsgId], Acc);
+ F({QName, CTag}, [MsgId], Acc);
foreach_per_queue(F, UAL, Acc) ->
- T = lists:foldl(fun ({_DTag, CTag, _Time, {QPid, MsgId}}, T) ->
- rabbit_misc:gb_trees_cons({QPid, CTag}, MsgId, T)
+ T = lists:foldl(fun (#pending_ack{tag = CTag,
+ queue = QName,
+ msg_id = MsgId}, T) ->
+ rabbit_misc:gb_trees_cons({QName, CTag}, MsgId, T)
end, gb_trees:empty(), UAL),
rabbit_misc:gb_trees_fold(fun (Key, Val, Acc0) -> F(Key, Val, Acc0) end, Acc, T).
-consumer_queue_refs(Consumers) ->
- lists:usort([qpid_to_ref(amqqueue:get_pid(Q))
- || {_Key, {Q, _CParams}} <- maps:to_list(Consumers),
- amqqueue:is_amqqueue(Q)]).
+%% hack to patch up missing queue type behaviour for classic queue
+classic_consumer_queue_pids(Consumers) ->
+ lists:usort([amqqueue:get_pid(Q)
+ || {Q, _CParams} <- maps:values(Consumers),
+ amqqueue:get_type(Q) == rabbit_classic_queue]).
%% tell the limiter about the number of acks that have been received
%% for messages delivered to subscribed consumers, but not acks for
@@ -2165,63 +2072,35 @@ deliver_to_queues({Delivery = #delivery{message = Message = #basic_message{
mandatory = Mandatory,
confirm = Confirm,
msg_seq_no = MsgSeqNo},
- DelQNames}, State = #ch{queue_names = QNames,
- queue_monitors = QMons,
- queue_states = QueueStates0}) ->
+ DelQNames}, State0 = #ch{queue_states = QueueStates0}) ->
Qs = rabbit_amqqueue:lookup(DelQNames),
- {DeliveredQPids, DeliveredQQPids, QueueStates} =
- rabbit_amqqueue:deliver(Qs, Delivery, QueueStates0),
- AllDeliveredQRefs = DeliveredQPids ++ [N || {N, _} <- DeliveredQQPids],
- %% The maybe_monitor_all/2 monitors all queues to which we
- %% delivered. But we want to monitor even queues we didn't deliver
- %% to, since we need their 'DOWN' messages to clean
- %% queue_names. So we also need to monitor each QPid from
- %% queues. But that only gets the masters (which is fine for
- %% cleaning queue_names), so we need the union of both.
- %%
- %% ...and we need to add even non-delivered queues to queue_names
- %% since alternative algorithms to update queue_names less
- %% 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 ?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},
+ {ok, QueueStates, Actions} =
+ rabbit_queue_type:deliver(Qs, Delivery, QueueStates0),
+ State1 = handle_queue_actions(Actions,
+ State0#ch{queue_states = QueueStates}),
%% NB: the order here is important since basic.returns must be
%% sent before confirms.
+ %% TODO: fix - HACK TO WORK OUT ALL QREFS
+ AllDeliveredQRefs = lists:foldl(fun (Q, Acc) ->
+ QRef = amqqueue:get_name(Q),
+ [QRef | Acc]
+ end, [], Qs),
ok = process_routing_mandatory(Mandatory, AllDeliveredQRefs,
Message, State1),
- 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
+ State = process_routing_confirm(Confirm,
+ AllDeliveredQRefs,
+ MsgSeqNo,
+ XName, State1),
+ case rabbit_event:stats_level(State1, #ch.stats_timer) of
fine ->
?INCR_STATS(exchange_stats, XName, 1, publish),
[?INCR_STATS(queue_exchange_stats,
- {amqqueue:get_name(Q), XName}, 1, publish) ||
- Q <- Qs];
+ {amqqueue:get_name(Q), XName}, 1, publish)
+ || Q <- Qs];
_ ->
ok
end,
- State2#ch{queue_states = QueueStates}.
+ State.
process_routing_mandatory(_Mandatory = true,
_RoutedToQs = [],
@@ -2236,25 +2115,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, QNames, MsgSeqNo, XName, State) ->
+process_routing_confirm(true, QRefs, MsgSeqNo, XName, State) ->
State#ch{unconfirmed =
- unconfirmed_messages:insert(MsgSeqNo, QNames, QRefs, XName,
- State#ch.unconfirmed)}.
+ rabbit_confirms:insert(MsgSeqNo, QRefs, XName, State#ch.unconfirmed)}.
-confirm(MsgSeqNos, QRef, State = #ch{queue_names = QNames, unconfirmed = UC}) ->
+confirm(MsgSeqNos, QRef, State = #ch{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),
- {ConfirmMXs, RejectMXs, UC1} =
- unconfirmed_messages:confirm_multiple_msg_ref(MsgSeqNos, QName, QRef, UC),
+ {ConfirmMXs, UC1} = rabbit_confirms:confirm(MsgSeqNos, QRef, UC),
%% NB: don't call noreply/1 since we don't want to send confirms.
- State1 = record_confirms(ConfirmMXs, State#ch{unconfirmed = UC1}),
- record_rejects(RejectMXs, State1).
+ record_confirms(ConfirmMXs, State#ch{unconfirmed = UC1}).
+ % record_rejects(RejectMXs, State1).
send_confirms_and_nacks(State = #ch{tx = none, confirmed = [], rejected = []}) ->
State;
@@ -2314,9 +2190,9 @@ send_confirms(Cs, Rs, State) ->
coalesce_and_send(MsgSeqNos, NegativeMsgSeqNos, MkMsgFun, State = #ch{unconfirmed = UC}) ->
SMsgSeqNos = lists:usort(MsgSeqNos),
- UnconfirmedCutoff = case unconfirmed_messages:is_empty(UC) of
+ UnconfirmedCutoff = case rabbit_confirms:is_empty(UC) of
true -> lists:last(SMsgSeqNos) + 1;
- false -> unconfirmed_messages:smallest(UC)
+ false -> rabbit_confirms:smallest(UC)
end,
Cutoff = lists:min([UnconfirmedCutoff | NegativeMsgSeqNos]),
{Ms, Ss} = lists:splitwith(fun(X) -> X < Cutoff end, SMsgSeqNos),
@@ -2335,7 +2211,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 unconfirmed_messages:is_empty(UC) of
+ case rabbit_confirms:is_empty(UC) of
false -> State;
true -> complete_tx(State#ch{confirmed = []})
end.
@@ -2374,7 +2250,7 @@ i(transactional, #ch{tx = Tx}) -> Tx =/= none;
i(confirm, #ch{confirm_enabled = CE}) -> CE;
i(name, State) -> name(State);
i(consumer_count, #ch{consumer_mapping = CM}) -> maps:size(CM);
-i(messages_unconfirmed, #ch{unconfirmed = UC}) -> unconfirmed_messages:size(UC);
+i(messages_unconfirmed, #ch{unconfirmed = UC}) -> rabbit_confirms: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;
@@ -2398,9 +2274,15 @@ i(Item, _) ->
throw({bad_argument, Item}).
pending_raft_commands(QStates) ->
- maps:fold(fun (_, V, Acc) ->
- Acc + rabbit_fifo_client:pending_size(V)
- end, 0, QStates).
+ Fun = fun(_, V, Acc) ->
+ case rabbit_queue_type:state_info(V) of
+ #{pending_raft_commands := P} ->
+ Acc + P;
+ _ ->
+ Acc
+ end
+ end,
+ rabbit_queue_type:fold_state(Fun, 0, QStates).
name(#ch{cfg = #conf{conn_name = ConnName, channel = Channel}}) ->
list_to_binary(rabbit_misc:format("~s (~p)", [ConnName, Channel])).
@@ -2598,13 +2480,16 @@ handle_method(#'queue.delete'{queue = QueueNameBin,
rabbit_amqqueue:check_exclusive_access(Q, ConnPid),
rabbit_amqqueue:delete(Q, IfUnused, IfEmpty, Username)
end,
- fun (not_found) -> {ok, 0};
- %% TODO delete crashed should clean up fifo states?
- ({absent, Q, crashed}) -> rabbit_amqqueue:delete_crashed(Q, Username),
- {ok, 0};
- ({absent, Q, stopped}) -> rabbit_amqqueue:delete_crashed(Q, Username),
- {ok, 0};
- ({absent, Q, Reason}) -> rabbit_amqqueue:absent(Q, Reason)
+ fun (not_found) ->
+ {ok, 0};
+ ({absent, Q, crashed}) ->
+ _ = rabbit_classic_queue:delete_crashed(Q, Username),
+ {ok, 0};
+ ({absent, Q, stopped}) ->
+ _ = rabbit_classic_queue:delete_crashed(Q, Username),
+ {ok, 0};
+ ({absent, Q, Reason}) ->
+ rabbit_amqqueue:absent(Q, Reason)
end) of
{error, in_use} ->
precondition_failed("~s in use", [rabbit_misc:rs(QueueName)]);
@@ -2682,24 +2567,30 @@ handle_method(#'exchange.declare'{exchange = ExchangeNameBin,
check_not_default_exchange(ExchangeName),
_ = rabbit_exchange:lookup_or_die(ExchangeName).
-handle_deliver(ConsumerTag, AckRequired,
- Msg = {_QName, QPid, _MsgId, Redelivered,
+handle_deliver(CTag, Ack, Msgs, State) ->
+ lists:foldl(fun(Msg, S) ->
+ handle_deliver0(CTag, Ack, Msg, S)
+ end, State, Msgs).
+
+handle_deliver0(ConsumerTag, AckRequired,
+ Msg = {QName, QPid, _MsgId, Redelivered,
#basic_message{exchange_name = ExchangeName,
routing_keys = [RoutingKey | _CcRoutes],
content = Content}},
State = #ch{cfg = #conf{writer_pid = WriterPid,
writer_gc_threshold = GCThreshold},
- next_tag = DeliveryTag}) ->
+ next_tag = DeliveryTag,
+ queue_states = Qs}) ->
Deliver = #'basic.deliver'{consumer_tag = ConsumerTag,
delivery_tag = DeliveryTag,
redelivered = Redelivered,
exchange = ExchangeName#resource.name,
routing_key = RoutingKey},
- case ?IS_CLASSIC(QPid) of
- true ->
+ case rabbit_queue_type:module(QName, Qs) of
+ {ok, rabbit_classic_queue} ->
ok = rabbit_writer:send_command_and_notify(
WriterPid, QPid, self(), Deliver, Content);
- false ->
+ _ ->
ok = rabbit_writer:send_command(WriterPid, Deliver, Content)
end,
case GCThreshold of
@@ -2709,7 +2600,7 @@ handle_deliver(ConsumerTag, AckRequired,
record_sent(deliver, ConsumerTag, AckRequired, Msg, State).
handle_basic_get(WriterPid, DeliveryTag, NoAck, MessageCount,
- Msg = {QName, QPid, _MsgId, Redelivered,
+ Msg = {_QName, _QPid, _MsgId, Redelivered,
#basic_message{exchange_name = ExchangeName,
routing_keys = [RoutingKey | _CcRoutes],
content = Content}}, State) ->
@@ -2721,8 +2612,7 @@ handle_basic_get(WriterPid, DeliveryTag, NoAck, MessageCount,
routing_key = RoutingKey,
message_count = MessageCount},
Content),
- State1 = track_delivering_queue(NoAck, QPid, QName, State),
- {noreply, record_sent(get, DeliveryTag, not(NoAck), Msg, State1)}.
+ {noreply, record_sent(get, DeliveryTag, not(NoAck), Msg, State)}.
init_tick_timer(State = #ch{tick_timer = undefined}) ->
{ok, Interval} = application:get_env(rabbit, channel_tick_interval),
@@ -2748,23 +2638,9 @@ maybe_cancel_tick_timer(#ch{tick_timer = TRef,
State
end.
-%% only classic queues need monitoring so rather than special casing
-%% everywhere monitors are set up we wrap it here for this module
-maybe_monitor(QPid, QMons) when ?IS_CLASSIC(QPid) ->
- pmon:monitor(QPid, QMons);
-maybe_monitor(_, QMons) ->
- QMons.
-
-maybe_monitor_all([Item], S) -> maybe_monitor(Item, S); %% optimisation
-maybe_monitor_all(Items, S) -> lists:foldl(fun maybe_monitor/2, S, Items).
-
-add_delivery_count_header(#{delivery_count := Count}, Msg) ->
- rabbit_basic:add_header(<<"x-delivery-count">>, long, Count, Msg);
-add_delivery_count_header(_, Msg) ->
- Msg.
-
qpid_to_ref(Pid) when is_pid(Pid) -> Pid;
-qpid_to_ref({Name, _}) -> Name;
+qpid_to_ref({Name, Node}) when is_atom(Name) andalso is_atom(Node) ->
+ Name;
%% assume it already is a ref
qpid_to_ref(Ref) -> Ref.
@@ -2789,7 +2665,8 @@ evaluate_consumer_timeout(State0 = #ch{cfg = #conf{channel = Channel,
unacked_message_q = UAMQ}) ->
Now = os:system_time(millisecond),
case ?QUEUE:peek(UAMQ) of
- {value, {_DTag, ConsumerTag, Time, {_QPid, _Msg}}}
+ {value, #pending_ack{delivery_tag = ConsumerTag,
+ delivered_at = Time}}
when is_integer(Timeout)
andalso Time < Now - Timeout ->
rabbit_log_channel:warning("Consumer ~s on channel ~w has timed out "
@@ -2803,3 +2680,68 @@ evaluate_consumer_timeout(State0 = #ch{cfg = #conf{channel = Channel,
_ ->
{noreply, State0}
end.
+
+handle_queue_actions(Actions, #ch{} = State0) ->
+ WriterPid = State0#ch.cfg#conf.writer_pid,
+ lists:foldl(
+ fun ({send_credit_reply, Avail}, S0) ->
+ ok = rabbit_writer:send_command(WriterPid,
+ #'basic.credit_ok'{available = Avail}),
+ S0;
+ ({send_drained, {CTag, Credit}}, S0) ->
+ ok = rabbit_writer:send_command(
+ WriterPid,
+ #'basic.credit_drained'{consumer_tag = CTag,
+ credit_drained = Credit}),
+ S0;
+ ({settled, QRef, MsgSeqNos}, S0) ->
+ confirm(MsgSeqNos, QRef, S0);
+ ({rejected, _QRef, MsgSeqNos}, S0) ->
+ {U, Rej} =
+ lists:foldr(
+ fun(SeqNo, {U1, Acc}) ->
+ case rabbit_confirms:reject(SeqNo, U1) of
+ {ok, MX, U2} ->
+ {U2, [MX | Acc]};
+ {error, not_found} ->
+ Acc
+ end
+ end, {S0#ch.unconfirmed, []}, MsgSeqNos),
+ S = S0#ch{unconfirmed = U},
+ record_rejects(Rej, S);
+ ({deliver, CTag, AckRequired, Msgs}, S0) ->
+ handle_deliver(CTag, AckRequired, Msgs, S0);
+ ({queue_down, QRef}, S0) ->
+ handle_consuming_queue_down_or_eol(QRef, QRef, S0)
+
+ end, State0, Actions).
+
+find_queue_name_from_pid(Pid, QStates) ->
+ Fun = fun(K, _V, undefined) ->
+ {ok, Q} = rabbit_amqqueue:lookup(K),
+ Pids = get_queue_pids(Q),
+ case lists:member(Pid, Pids) of
+ true ->
+ K;
+ false ->
+ undefined
+ end
+ end,
+ rabbit_queue_type:fold_state(Fun, undefined, QStates).
+
+get_queue_pids(Q) when ?amqqueue_is_quorum(Q) ->
+ [amqqueue:get_leader(Q)];
+get_queue_pids(Q) ->
+ [amqqueue:get_pid(Q) | amqqueue:get_slave_pids(Q)].
+
+find_queue_name_from_quorum_name(Name, QStates) ->
+ Fun = fun(K, _V, undefined) ->
+ {ok, Q} = rabbit_amqqueue:lookup(K),
+ case amqqueue:get_pid(Q) of
+ {Name, _} ->
+ amqqueue:get_name(Q);
+ _ ->
+ undefined
+ end
+ end,
+ rabbit_queue_type:fold_state(Fun, undefined, QStates).
diff --git a/src/rabbit_classic_queue.erl b/src/rabbit_classic_queue.erl
new file mode 100644
index 0000000000..7d94c05332
--- /dev/null
+++ b/src/rabbit_classic_queue.erl
@@ -0,0 +1,452 @@
+-module(rabbit_classic_queue).
+-behaviour(rabbit_queue_type).
+
+-include("amqqueue.hrl").
+-include_lib("rabbit_common/include/rabbit.hrl").
+
+-record(?MODULE, {pid :: undefined | pid(), %% the current master pid
+ qref :: term(), %% TODO
+ unconfirmed = #{} :: #{non_neg_integer() => [pid()]}}).
+-define(STATE, ?MODULE).
+
+-opaque state() :: #?STATE{}.
+
+-export_type([state/0]).
+
+-export([
+ is_enabled/0,
+ declare/2,
+ delete/4,
+ is_recoverable/1,
+ recover/2,
+ purge/1,
+ policy_changed/1,
+ stat/1,
+ init/1,
+ close/1,
+ update/2,
+ consume/3,
+ cancel/5,
+ handle_event/2,
+ deliver/2,
+ settle/4,
+ credit/4,
+ dequeue/4,
+ info/2,
+ state_info/1,
+ is_policy_applicable/2
+ ]).
+
+-export([delete_crashed/1,
+ delete_crashed/2,
+ delete_crashed_internal/2]).
+
+is_enabled() -> true.
+
+declare(Q, Node) when ?amqqueue_is_classic(Q) ->
+ QName = amqqueue:get_name(Q),
+ VHost = amqqueue:get_vhost(Q),
+ Node1 = case Node of
+ {ignore_location, Node0} ->
+ Node0;
+ _ ->
+ case rabbit_queue_master_location_misc:get_location(Q) of
+ {ok, Node0} -> Node0;
+ {error, _} -> Node
+ end
+ end,
+ Node1 = rabbit_mirror_queue_misc:initial_queue_node(Q, Node1),
+ case rabbit_vhost_sup_sup:get_vhost_sup(VHost, Node1) of
+ {ok, _} ->
+ gen_server2:call(
+ rabbit_amqqueue_sup_sup:start_queue_process(Node1, Q, declare),
+ {init, new}, infinity);
+ {error, Error} ->
+ rabbit_misc:protocol_error(internal_error,
+ "Cannot declare a queue '~s' on node '~s': ~255p",
+ [rabbit_misc:rs(QName), Node1, Error])
+ end.
+
+delete(Q, IfUnused, IfEmpty, ActingUser) when ?amqqueue_is_classic(Q) ->
+ case wait_for_promoted_or_stopped(Q) of
+ {promoted, Q1} ->
+ QPid = amqqueue:get_pid(Q1),
+ delegate:invoke(QPid, {gen_server2, call,
+ [{delete, IfUnused, IfEmpty, ActingUser},
+ infinity]});
+ {stopped, Q1} ->
+ #resource{name = Name, virtual_host = Vhost} = amqqueue:get_name(Q1),
+ case IfEmpty of
+ true ->
+ rabbit_log:error("Queue ~s in vhost ~s has its master node down and "
+ "no mirrors available or eligible for promotion. "
+ "The queue may be non-empty. "
+ "Refusing to force-delete.",
+ [Name, Vhost]),
+ {error, not_empty};
+ false ->
+ rabbit_log:warning("Queue ~s in vhost ~s has its master node is down and "
+ "no mirrors available or eligible for promotion. "
+ "Forcing queue deletion.",
+ [Name, Vhost]),
+ delete_crashed_internal(Q1, ActingUser),
+ {ok, 0}
+ end;
+ {error, not_found} ->
+ %% Assume the queue was deleted
+ {ok, 0}
+ end.
+
+is_recoverable(Q) when ?is_amqqueue(Q) ->
+ Node = node(),
+ Node =:= node(amqqueue:get_pid(Q)) andalso
+ %% Terminations on node down will not remove the rabbit_queue
+ %% record if it is a mirrored queue (such info is now obtained from
+ %% the policy). Thus, we must check if the local pid is alive
+ %% - if the record is present - in order to restart.
+ (mnesia:read(rabbit_queue, amqqueue:get_name(Q), read) =:= []
+ orelse not rabbit_mnesia:is_process_alive(amqqueue:get_pid(Q))).
+
+recover(VHost, Queues) ->
+ {ok, BQ} = application:get_env(rabbit, backing_queue_module),
+ %% We rely on BQ:start/1 returning the recovery terms in the same
+ %% order as the supplied queue names, so that we can zip them together
+ %% for further processing in recover_durable_queues.
+ {ok, OrderedRecoveryTerms} =
+ BQ:start(VHost, [amqqueue:get_name(Q) || Q <- Queues]),
+ case rabbit_amqqueue_sup_sup:start_for_vhost(VHost) of
+ {ok, _} ->
+ RecoveredQs = recover_durable_queues(lists:zip(Queues,
+ OrderedRecoveryTerms)),
+ RecoveredNames = [amqqueue:get_name(Q) || Q <- RecoveredQs],
+ FailedQueues = [Q || Q <- Queues,
+ not lists:member(amqqueue:get_name(Q), RecoveredNames)],
+ {RecoveredQs, FailedQueues};
+ {error, Reason} ->
+ rabbit_log:error("Failed to start queue supervisor for vhost '~s': ~s", [VHost, Reason]),
+ throw({error, Reason})
+ end.
+
+-spec policy_changed(amqqueue:amqqueue()) -> ok.
+policy_changed(Q) ->
+ QPid = amqqueue:get_pid(Q),
+ gen_server2:cast(QPid, policy_changed).
+
+stat(Q) ->
+ delegate:invoke(amqqueue:get_pid(Q),
+ {gen_server2, call, [stat, infinity]}).
+
+-spec init(amqqueue:amqqueue()) -> state().
+init(Q) when ?amqqueue_is_classic(Q) ->
+ QName = amqqueue:get_name(Q),
+ #?STATE{pid = amqqueue:get_pid(Q),
+ qref = QName}.
+
+-spec close(state()) -> ok.
+close(_State) ->
+ ok.
+
+-spec update(amqqueue:amqqueue(), state()) -> state().
+update(Q, #?STATE{pid = Pid} = State) when ?amqqueue_is_classic(Q) ->
+ case amqqueue:get_pid(Q) of
+ Pid ->
+ State;
+ NewPid ->
+ %% master pid is different, update
+ State#?STATE{pid = NewPid}
+ end.
+
+consume(Q, Spec, State) when ?amqqueue_is_classic(Q) ->
+ QPid = amqqueue:get_pid(Q),
+ QRef = amqqueue:get_name(Q),
+ #{no_ack := NoAck,
+ channel_pid := ChPid,
+ limiter_pid := LimiterPid,
+ limiter_active := LimiterActive,
+ prefetch_count := ConsumerPrefetchCount,
+ consumer_tag := ConsumerTag,
+ exclusive_consume := ExclusiveConsume,
+ args := Args,
+ ok_msg := OkMsg,
+ acting_user := ActingUser} = Spec,
+ case delegate:invoke(QPid,
+ {gen_server2, call,
+ [{basic_consume, NoAck, ChPid, LimiterPid,
+ LimiterActive, ConsumerPrefetchCount, ConsumerTag,
+ ExclusiveConsume, Args, OkMsg, ActingUser},
+ infinity]}) of
+ ok ->
+ %% ask the host process to monitor this pid
+ %% TODO: track pids as they change
+ {ok, State#?STATE{pid = QPid}, [{monitor, QPid, QRef}]};
+ Err ->
+ Err
+ end.
+
+cancel(Q, ConsumerTag, OkMsg, ActingUser, State) ->
+ QPid = amqqueue:get_pid(Q),
+ case delegate:invoke(QPid, {gen_server2, call,
+ [{basic_cancel, self(), ConsumerTag,
+ OkMsg, ActingUser}, infinity]}) of
+ ok ->
+ {ok, State};
+ Err -> Err
+ end.
+
+-spec settle(rabbit_queue_type:settle_op(), rabbit_types:ctag(),
+ [non_neg_integer()], state()) ->
+ {state(), rabbit_queue_type:actions()}.
+settle(complete, _CTag, MsgIds, State) ->
+ delegate:invoke_no_result(State#?STATE.pid,
+ {gen_server2, cast, [{ack, MsgIds, self()}]}),
+ {State, []};
+settle(Op, _CTag, MsgIds, State) ->
+ ChPid = self(),
+ ok = delegate:invoke_no_result(State#?STATE.pid,
+ {gen_server2, cast,
+ [{reject, Op == requeue, MsgIds, ChPid}]}),
+ {State, []}.
+
+credit(CTag, Credit, Drain, State) ->
+ ChPid = self(),
+ delegate:invoke_no_result(State#?STATE.pid,
+ {gen_server2, cast,
+ [{credit, ChPid, CTag, Credit, Drain}]}),
+ State.
+
+handle_event({confirm, MsgSeqNos, Pid}, #?STATE{qref = QRef,
+ unconfirmed = U0} = State) ->
+ {Unconfirmed, ConfirmedSeqNos} = confirm_seq_nos(MsgSeqNos, Pid, U0),
+ Actions = [{settled, QRef, ConfirmedSeqNos}],
+ %% handle confirm event from queues
+ %% in this case the classic queue should track each individual publish and
+ %% the processes involved and only emit a settle action once they have all
+ %% been received (or DOWN has been received).
+ %% Hence this part of the confirm logic is queue specific.
+ {ok, State#?STATE{unconfirmed = Unconfirmed}, Actions};
+handle_event({reject_publish, SeqNo, _QPid},
+ #?STATE{qref = QRef,
+ unconfirmed = U0} = State) ->
+ %% It does not matter which queue rejected the message,
+ %% if any queue did, it should not be confirmed.
+ {U, Rejected} = reject_seq_no(SeqNo, U0),
+ Actions = [{rejected, QRef, Rejected}],
+ {ok, State#?STATE{unconfirmed = U}, Actions};
+handle_event({down, Pid, Info}, #?STATE{qref = QRef,
+ pid = MasterPid,
+ unconfirmed = U0} = State0) ->
+ case rabbit_misc:is_abnormal_exit(Info) of
+ false when Info =:= normal andalso Pid == MasterPid ->
+ %% queue was deleted and masterpid is down
+ eol;
+ false ->
+ %% this assumes the mirror isn't part of the active set
+ %% so we can confirm this particular pid
+ MsgSeqNos = maps:keys(
+ maps:filter(fun (_, Pids) ->
+ lists:member(Pid, Pids)
+ end, U0)),
+ %% if the exit is normal, treat it as a "confirm"
+ {Unconfirmed, ConfirmedSeqNos} = confirm_seq_nos(MsgSeqNos, Pid, U0),
+ Actions = [{settled, QRef, ConfirmedSeqNos}],
+ {ok, State0#?STATE{unconfirmed = Unconfirmed}, Actions};
+ true ->
+ %% any abnormal exit should be considered a full reject of the
+ %% oustanding message ids - If the message didn't get to all
+ %% mirrors we have to assume it will never get there
+ MsgIds = maps:fold(
+ fun (SeqNo, Pids, Acc) ->
+ case lists:member(Pid, Pids) of
+ true ->
+ [SeqNo | Acc];
+ false ->
+ Acc
+ end
+ end, [], U0),
+ U = maps:without(MsgIds, U0),
+ {ok, State0#?STATE{unconfirmed = U}, [{rejected, QRef, MsgIds}]}
+ end.
+
+-spec deliver([{amqqueue:amqqueue(), state()}],
+ Delivery :: term()) ->
+ {[{amqqueue:amqqueue(), state()}], rabbit_queue_type:actions()}.
+deliver(Qs0, #delivery{flow = Flow,
+ msg_seq_no = MsgNo,
+ message = #basic_message{exchange_name = _Ex},
+ confirm = _Confirm} = Delivery) ->
+ %% TODO: record master and slaves for confirm processing
+ {MPids, SPids, Qs, Actions} = qpids(Qs0, MsgNo),
+ QPids = MPids ++ SPids,
+ case Flow of
+ %% Here we are tracking messages sent by the rabbit_channel
+ %% process. We are accessing the rabbit_channel process
+ %% dictionary.
+ flow -> [credit_flow:send(QPid) || QPid <- QPids],
+ [credit_flow:send(QPid) || QPid <- SPids];
+ noflow -> ok
+ end,
+ MMsg = {deliver, Delivery, false},
+ SMsg = {deliver, Delivery, true},
+ delegate:invoke_no_result(MPids, {gen_server2, cast, [MMsg]}),
+ delegate:invoke_no_result(SPids, {gen_server2, cast, [SMsg]}),
+ {Qs, Actions}.
+
+
+-spec dequeue(NoAck :: boolean(), LimiterPid :: pid(),
+ rabbit_types:ctag(), state()) ->
+ {ok, Count :: non_neg_integer(), rabbit_amqqueue:qmsg(), state()} |
+ {empty, state()}.
+dequeue(NoAck, LimiterPid, _CTag, State) ->
+ QPid = State#?STATE.pid,
+ case delegate:invoke(QPid, {gen_server2, call,
+ [{basic_get, self(), NoAck, LimiterPid}, infinity]}) of
+ empty ->
+ {empty, State};
+ {ok, Count, Msg} ->
+ {ok, Count, Msg, State}
+ end.
+
+-spec state_info(state()) -> #{atom() := term()}.
+state_info(_State) ->
+ #{}.
+
+%% general queue info
+-spec info(amqqueue:amqqueue(), all_keys | rabbit_types:info_keys()) ->
+ rabbit_types:infos().
+info(Q, Items) ->
+ QPid = amqqueue:get_pid(Q),
+ Req = case Items of
+ all_keys -> info;
+ _ -> {info, Items}
+ end,
+ case delegate:invoke(QPid, {gen_server2, call, [Req, infinity]}) of
+ {ok, Result} ->
+ Result;
+ {error, _Err} ->
+ []
+ end.
+
+-spec purge(amqqueue:amqqueue()) ->
+ {ok, non_neg_integer()}.
+purge(Q) when ?is_amqqueue(Q) ->
+ QPid = amqqueue:get_pid(Q),
+ delegate:invoke(QPid, {gen_server2, call, [purge, infinity]}).
+
+qpids(Qs, MsgNo) ->
+ lists:foldl(
+ fun ({Q, S0}, {MPidAcc, SPidAcc, Qs0, Actions0}) ->
+ QPid = amqqueue:get_pid(Q),
+ SPids = amqqueue:get_slave_pids(Q),
+ QRef = amqqueue:get_name(Q),
+ Actions = [{monitor, QPid, QRef}
+ | [{monitor, P, QRef} || P <- SPids]] ++ Actions0,
+ %% confirm record
+ S = case S0 of
+ #?STATE{unconfirmed = U0} ->
+ Rec = [QPid | SPids],
+ U = U0#{MsgNo => Rec},
+ S0#?STATE{unconfirmed = U};
+ stateless ->
+ S0
+ end,
+ {[QPid | MPidAcc],
+ SPidAcc ++ SPids,
+ [{Q, S} | Qs0],
+ Actions}
+ end, {[], [], [], []}, Qs).
+
+%% internal-ish
+-spec wait_for_promoted_or_stopped(amqqueue:amqqueue()) ->
+ {promoted, amqqueue:amqqueue()} |
+ {stopped, amqqueue:amqqueue()} |
+ {error, not_found}.
+wait_for_promoted_or_stopped(Q0) ->
+ QName = amqqueue:get_name(Q0),
+ case rabbit_amqqueue:lookup(QName) of
+ {ok, Q} ->
+ QPid = amqqueue:get_pid(Q),
+ SPids = amqqueue:get_slave_pids(Q),
+ case rabbit_mnesia:is_process_alive(QPid) of
+ true -> {promoted, Q};
+ false ->
+ case lists:any(fun(Pid) ->
+ rabbit_mnesia:is_process_alive(Pid)
+ end, SPids) of
+ %% There is a live slave. May be promoted
+ true ->
+ timer:sleep(100),
+ wait_for_promoted_or_stopped(Q);
+ %% All slave pids are stopped.
+ %% No process left for the queue
+ false -> {stopped, Q}
+ end
+ end;
+ {error, not_found} ->
+ {error, not_found}
+ end.
+
+-spec delete_crashed(amqqueue:amqqueue()) -> ok.
+delete_crashed(Q) ->
+ delete_crashed(Q, ?INTERNAL_USER).
+
+delete_crashed(Q, ActingUser) ->
+ ok = rpc:call(amqqueue:qnode(Q), ?MODULE, delete_crashed_internal,
+ [Q, ActingUser]).
+
+delete_crashed_internal(Q, ActingUser) ->
+ QName = amqqueue:get_name(Q),
+ {ok, BQ} = application:get_env(rabbit, backing_queue_module),
+ BQ:delete_crashed(Q),
+ ok = rabbit_amqqueue:internal_delete(QName, ActingUser).
+
+recover_durable_queues(QueuesAndRecoveryTerms) ->
+ {Results, Failures} =
+ gen_server2:mcall(
+ [{rabbit_amqqueue_sup_sup:start_queue_process(node(), Q, recovery),
+ {init, {self(), Terms}}} || {Q, Terms} <- QueuesAndRecoveryTerms]),
+ [rabbit_log:error("Queue ~p failed to initialise: ~p~n",
+ [Pid, Error]) || {Pid, Error} <- Failures],
+ [Q || {_, {new, Q}} <- Results].
+
+-spec is_policy_applicable(amqqueue:amqqueue(), any()) -> boolean().
+is_policy_applicable(_Q, Policy) ->
+ Applicable = [<<"expires">>, <<"message-ttl">>, <<"dead-letter-exchange">>,
+ <<"dead-letter-routing-key">>, <<"max-length">>,
+ <<"max-length-bytes">>, <<"max-in-memory-length">>, <<"max-in-memory-bytes">>,
+ <<"max-priority">>, <<"overflow">>, <<"queue-mode">>,
+ <<"single-active-consumer">>, <<"delivery-limit">>,
+ <<"ha-mode">>, <<"ha-params">>, <<"ha-sync-mode">>,
+ <<"ha-promote-on-shutdown">>, <<"ha-promote-on-failure">>,
+ <<"queue-master-locator">>],
+ lists:all(fun({P, _}) ->
+ lists:member(P, Applicable)
+ end, Policy).
+
+reject_seq_no(SeqNo, U0) ->
+ reject_seq_no(SeqNo, U0, []).
+
+reject_seq_no(SeqNo, U0, Acc) ->
+ case maps:take(SeqNo, U0) of
+ {_, U} ->
+ {U, [SeqNo | Acc]};
+ error ->
+ {U0, Acc}
+ end.
+
+confirm_seq_nos(MsgSeqNos, Pid, U0) ->
+ lists:foldl(
+ fun (SeqNo, {U, A0}) ->
+ case U of
+ #{SeqNo := Pids0} ->
+ case lists:delete(Pid, Pids0) of
+ [] ->
+ %% the updated unconfirmed state
+ %% and the seqnos to settle
+ {maps:remove(SeqNo, U), [SeqNo | A0]};
+ Pids ->
+ {U#{SeqNo => Pids}, A0}
+ end;
+ _ ->
+ {U, A0}
+ end
+ end, {U0, []}, MsgSeqNos).
diff --git a/src/rabbit_confirms.erl b/src/rabbit_confirms.erl
new file mode 100644
index 0000000000..5aa3abdf12
--- /dev/null
+++ b/src/rabbit_confirms.erl
@@ -0,0 +1,151 @@
+-module(rabbit_confirms).
+
+-compile({no_auto_import, [size/1]}).
+
+-include_lib("rabbit_common/include/rabbit.hrl").
+
+-export([init/0,
+ insert/4,
+ confirm/3,
+ reject/2,
+
+ remove_queue/2,
+
+ smallest/1,
+ size/1,
+ is_empty/1]).
+
+-type seq_no() :: non_neg_integer().
+-type queue_name() :: rabbit_amqqueue:name().
+-type exchange_name() :: rabbit_exchange:name().
+
+-record(?MODULE, {smallest :: undefined | seq_no(),
+ unconfirmed = #{} :: #{seq_no() =>
+ {exchange_name(),
+ #{queue_name() => ok}}}
+ }).
+
+-type mx() :: {seq_no(), exchange_name()}.
+
+-opaque state() :: #?MODULE{}.
+
+-export_type([
+ state/0
+ ]).
+
+-spec init() -> state().
+init() ->
+ #?MODULE{}.
+
+-spec insert(seq_no(), [queue_name()], exchange_name(), state()) ->
+ state().
+insert(SeqNo, QNames, #resource{kind = exchange} = XName,
+ #?MODULE{smallest = S0,
+ unconfirmed = U0} = State)
+ when is_integer(SeqNo)
+ andalso is_list(QNames)
+ andalso is_map_key(SeqNo, U0) == false ->
+ U = U0#{SeqNo => {XName, maps:from_list([{Q, ok} || Q <- QNames])}},
+ S = case S0 of
+ undefined -> SeqNo;
+ _ -> S0
+ end,
+ State#?MODULE{smallest = S,
+ unconfirmed = U}.
+
+-spec confirm([seq_no()], queue_name(), state()) ->
+ {[mx()], state()}.
+confirm(SeqNos, QName, #?MODULE{smallest = Smallest0,
+ unconfirmed = U0} = State)
+ when is_list(SeqNos) ->
+ {Confirmed, U} = lists:foldr(
+ fun (SeqNo, Acc) ->
+ confirm_one(SeqNo, QName, Acc)
+ end, {[], U0}, SeqNos),
+ %% check if smallest is in Confirmed
+ Smallest =
+ case lists:any(fun ({S, _}) -> S == Smallest0 end, Confirmed) of
+ true ->
+ %% work out new smallest
+ next_smallest(Smallest0, U);
+ false ->
+ Smallest0
+ end,
+ {Confirmed, State#?MODULE{smallest = Smallest,
+ unconfirmed = U}}.
+
+-spec reject(seq_no(), state()) ->
+ {ok, mx(), state()} | {error, not_found}.
+reject(SeqNo, #?MODULE{smallest = Smallest0,
+ unconfirmed = U0} = State)
+ when is_integer(SeqNo) ->
+ case maps:take(SeqNo, U0) of
+ {{XName, _QS}, U} ->
+ Smallest = case SeqNo of
+ Smallest0 ->
+ %% need to scan as the smallest was removed
+ next_smallest(Smallest0, U);
+ _ ->
+ Smallest0
+ end,
+ {ok, {SeqNo, XName}, State#?MODULE{unconfirmed = U,
+ smallest = Smallest}};
+ error ->
+ {error, not_found}
+ end.
+
+%% idempotent
+-spec remove_queue(queue_name(), state()) ->
+ {[mx()], state()}.
+remove_queue(QName, #?MODULE{unconfirmed = U} = State) ->
+ SeqNos = maps:fold(
+ fun (SeqNo, {_XName, QS0}, Acc) ->
+ case maps:is_key(QName, QS0) of
+ true ->
+ [SeqNo | Acc];
+ false ->
+ Acc
+ end
+ end, [], U),
+ confirm(lists:sort(SeqNos), QName,State).
+
+-spec smallest(state()) -> seq_no() | undefined.
+smallest(#?MODULE{smallest = Smallest}) ->
+ Smallest.
+
+-spec size(state()) -> non_neg_integer().
+size(#?MODULE{unconfirmed = U}) ->
+ maps:size(U).
+
+-spec is_empty(state()) -> boolean().
+is_empty(State) ->
+ size(State) == 0.
+
+%% INTERNAL
+
+confirm_one(SeqNo, QName, {Acc, U0}) ->
+ case maps:take(SeqNo, U0) of
+ {{XName, QS}, U1}
+ when is_map_key(QName, QS)
+ andalso map_size(QS) == 1 ->
+ %% last queue confirm
+ {[{SeqNo, XName} | Acc], U1};
+ {{XName, QS}, U1} ->
+ {Acc, U1#{SeqNo => {XName, maps:remove(QName, QS)}}};
+ error ->
+ {Acc, U0}
+ end.
+
+next_smallest(_S, U) when map_size(U) == 0 ->
+ undefined;
+next_smallest(S, U) when is_map_key(S, U) ->
+ S;
+next_smallest(S, U) ->
+ %% TODO: this is potentially infinitely recursive if called incorrectly
+ next_smallest(S+1, U).
+
+
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+-endif.
diff --git a/src/rabbit_core_ff.erl b/src/rabbit_core_ff.erl
index a251def11c..027ae4f0f3 100644
--- a/src/rabbit_core_ff.erl
+++ b/src/rabbit_core_ff.erl
@@ -8,6 +8,7 @@
-module(rabbit_core_ff).
-export([quorum_queue_migration/3,
+ stream_queue_migration/3,
implicit_default_bindings_migration/3,
virtual_host_metadata_migration/3,
maintenance_mode_status_migration/3,
@@ -22,6 +23,14 @@
}}).
-rabbit_feature_flag(
+ {stream_queue,
+ #{desc => "Support queues of type `stream`",
+ doc_url => "https://www.rabbitmq.com/stream-queues.html",
+ stability => stable,
+ migration_fun => {?MODULE, stream_queue_migration}
+ }}).
+
+-rabbit_feature_flag(
{implicit_default_bindings,
#{desc => "Default bindings are now implicit, instead of "
"being stored in the database",
@@ -69,6 +78,9 @@ quorum_queue_migration(_FeatureName, _FeatureProps, is_enabled) ->
mnesia:table_info(rabbit_queue, attributes) =:= Fields andalso
mnesia:table_info(rabbit_durable_queue, attributes) =:= Fields.
+stream_queue_migration(_FeatureName, _FeatureProps, _Enable) ->
+ ok.
+
migrate_to_amqqueue_with_type(FeatureName, [Table | Rest], Fields) ->
rabbit_log_feature_flags:info(
"Feature flag `~s`: migrating Mnesia table ~s...",
diff --git a/src/rabbit_dead_letter.erl b/src/rabbit_dead_letter.erl
index 894a852942..755de5cf53 100644
--- a/src/rabbit_dead_letter.erl
+++ b/src/rabbit_dead_letter.erl
@@ -20,14 +20,15 @@
-spec publish(rabbit_types:message(), reason(), rabbit_types:exchange(),
'undefined' | binary(), rabbit_amqqueue:name()) -> 'ok'.
-
publish(Msg, Reason, X, RK, QName) ->
DLMsg = make_msg(Msg, Reason, X#exchange.name, RK, QName),
Delivery = rabbit_basic:delivery(false, false, DLMsg, undefined),
{Queues, Cycles} = detect_cycles(Reason, DLMsg,
rabbit_exchange:route(X, Delivery)),
lists:foreach(fun log_cycle_once/1, Cycles),
- rabbit_amqqueue:deliver(rabbit_amqqueue:lookup(Queues), Delivery).
+ _ = rabbit_queue_type:deliver(rabbit_amqqueue:lookup(Queues),
+ Delivery, stateless),
+ ok.
make_msg(Msg = #basic_message{content = Content,
exchange_name = Exchange,
diff --git a/src/rabbit_definitions.erl b/src/rabbit_definitions.erl
index 98f621996f..8fa8c651c1 100644
--- a/src/rabbit_definitions.erl
+++ b/src/rabbit_definitions.erl
@@ -661,6 +661,7 @@ queue_definition(Q) ->
Type = case amqqueue:get_type(Q) of
rabbit_classic_queue -> classic;
rabbit_quorum_queue -> quorum;
+ rabbit_stream_queue -> stream;
T -> T
end,
#{
diff --git a/src/rabbit_fifo.erl b/src/rabbit_fifo.erl
index 03d1625b9c..51acfffd0d 100644
--- a/src/rabbit_fifo.erl
+++ b/src/rabbit_fifo.erl
@@ -286,7 +286,7 @@ apply(Meta, #credit{credit = NewCredit, delivery_count = RemoteDelCnt,
end;
apply(_, #checkout{spec = {dequeue, _}},
#?MODULE{cfg = #cfg{consumer_strategy = single_active}} = State0) ->
- {State0, {error, unsupported}};
+ {State0, {error, {unsupported, single_active_consumer}}};
apply(#{index := Index,
system_time := Ts,
from := From} = Meta, #checkout{spec = {dequeue, Settlement},
@@ -968,7 +968,6 @@ usage(Name) when is_atom(Name) ->
messages_ready(#?MODULE{messages = M,
prefix_msgs = {RCnt, _R, PCnt, _P},
returns = R}) ->
-
%% prefix messages will rarely have anything in them during normal
%% operations so length/1 is fine here
lqueue:len(M) + lqueue:len(R) + RCnt + PCnt.
diff --git a/src/rabbit_fifo_client.erl b/src/rabbit_fifo_client.erl
index dc98210817..a5695fbfcb 100644
--- a/src/rabbit_fifo_client.erl
+++ b/src/rabbit_fifo_client.erl
@@ -51,6 +51,7 @@
-type cluster_name() :: rabbit_types:r(queue).
-record(consumer, {last_msg_id :: seq() | -1,
+ ack = false :: boolean(),
delivery_count = 0 :: non_neg_integer()}).
-record(cfg, {cluster_name :: cluster_name(),
@@ -219,10 +220,11 @@ enqueue(Msg, State) ->
%% @returns `{ok, IdMsg, State}' or `{error | timeout, term()}'
-spec dequeue(rabbit_fifo:consumer_tag(),
Settlement :: settled | unsettled, state()) ->
- {ok, {rabbit_fifo:delivery_msg(), non_neg_integer()}
- | empty, state()} | {error | timeout, term()}.
+ {ok, non_neg_integer(), term(), non_neg_integer()}
+ | {empty, state()} | {error | timeout, term()}.
dequeue(ConsumerTag, Settlement,
- #state{cfg = #cfg{timeout = Timeout}} = State0) ->
+ #state{cfg = #cfg{timeout = Timeout,
+ cluster_name = QName}} = State0) ->
Node = pick_server(State0),
ConsumerId = consumer_id(ConsumerTag),
case ra:process_command(Node,
@@ -231,9 +233,16 @@ dequeue(ConsumerTag, Settlement,
#{}),
Timeout) of
{ok, {dequeue, empty}, Leader} ->
- {ok, empty, State0#state{leader = Leader}};
- {ok, {dequeue, Msg, NumReady}, Leader} ->
- {ok, {Msg, NumReady},
+ {empty, State0#state{leader = Leader}};
+ {ok, {dequeue, {MsgId, {MsgHeader, Msg0}}, MsgsReady}, Leader} ->
+ Count = case MsgHeader of
+ #{delivery_count := C} -> C;
+ _ -> 0
+ end,
+ IsDelivered = Count > 0,
+ Msg = add_delivery_count_header(Msg0, Count),
+ {ok, MsgsReady,
+ {QName, qref(Leader), MsgId, IsDelivered, Msg},
State0#state{leader = Leader}};
{ok, {error, _} = Err, _Leader} ->
Err;
@@ -241,6 +250,13 @@ dequeue(ConsumerTag, Settlement,
Err
end.
+add_delivery_count_header(#basic_message{} = Msg0, Count)
+ when is_integer(Count) ->
+ rabbit_basic:add_header(<<"x-delivery-count">>, long, Count, Msg0);
+add_delivery_count_header(Msg, _Count) ->
+ Msg.
+
+
%% @doc Settle a message. Permanently removes message from the queue.
%% @param ConsumerTag the tag uniquely identifying the consumer.
%% @param MsgIds the message ids received with the {@link rabbit_fifo:delivery/0.}
@@ -251,16 +267,14 @@ dequeue(ConsumerTag, Settlement,
%% the sending rate.
%%
-spec settle(rabbit_fifo:consumer_tag(), [rabbit_fifo:msg_id()], state()) ->
- {ok, state()}.
+ {state(), list()}.
settle(ConsumerTag, [_|_] = MsgIds, #state{slow = false} = State0) ->
Node = pick_server(State0),
Cmd = rabbit_fifo:make_settle(consumer_id(ConsumerTag), MsgIds),
case send_command(Node, undefined, Cmd, normal, State0) of
- {slow, S} ->
+ {_, S} ->
% turn slow into ok for this function
- {ok, S};
- {ok, _} = Ret ->
- Ret
+ {S, []}
end;
settle(ConsumerTag, [_|_] = MsgIds,
#state{unsent_commands = Unsent0} = State0) ->
@@ -271,7 +285,7 @@ settle(ConsumerTag, [_|_] = MsgIds,
fun ({Settles, Returns, Discards}) ->
{Settles ++ MsgIds, Returns, Discards}
end, {MsgIds, [], []}, Unsent0),
- {ok, State0#state{unsent_commands = Unsent}}.
+ {State0#state{unsent_commands = Unsent}, []}.
%% @doc Return a message to the queue.
%% @param ConsumerTag the tag uniquely identifying the consumer.
@@ -284,17 +298,14 @@ settle(ConsumerTag, [_|_] = MsgIds,
%% the sending rate.
%%
-spec return(rabbit_fifo:consumer_tag(), [rabbit_fifo:msg_id()], state()) ->
- {ok, state()}.
+ {state(), list()}.
return(ConsumerTag, [_|_] = MsgIds, #state{slow = false} = State0) ->
Node = pick_server(State0),
% TODO: make rabbit_fifo return support lists of message ids
Cmd = rabbit_fifo:make_return(consumer_id(ConsumerTag), MsgIds),
case send_command(Node, undefined, Cmd, normal, State0) of
- {slow, S} ->
- % turn slow into ok for this function
- {ok, S};
- {ok, _} = Ret ->
- Ret
+ {_, S} ->
+ {S, []}
end;
return(ConsumerTag, [_|_] = MsgIds,
#state{unsent_commands = Unsent0} = State0) ->
@@ -305,7 +316,7 @@ return(ConsumerTag, [_|_] = MsgIds,
fun ({Settles, Returns, Discards}) ->
{Settles, Returns ++ MsgIds, Discards}
end, {[], MsgIds, []}, Unsent0),
- {ok, State0#state{unsent_commands = Unsent}}.
+ {State0#state{unsent_commands = Unsent}, []}.
%% @doc Discards a checked out message.
%% If the queue has a dead_letter_handler configured this will be called.
@@ -318,16 +329,14 @@ return(ConsumerTag, [_|_] = MsgIds,
%% tag is `slow' it means the limit is approaching and it is time to slow down
%% the sending rate.
-spec discard(rabbit_fifo:consumer_tag(), [rabbit_fifo:msg_id()], state()) ->
- {ok | slow, state()}.
+ {state(), list()}.
discard(ConsumerTag, [_|_] = MsgIds, #state{slow = false} = State0) ->
Node = pick_server(State0),
Cmd = rabbit_fifo:make_discard(consumer_id(ConsumerTag), MsgIds),
case send_command(Node, undefined, Cmd, normal, State0) of
- {slow, S} ->
+ {_, S} ->
% turn slow into ok for this function
- {ok, S};
- {ok, _} = Ret ->
- Ret
+ {S, []}
end;
discard(ConsumerTag, [_|_] = MsgIds,
#state{unsent_commands = Unsent0} = State0) ->
@@ -338,7 +347,7 @@ discard(ConsumerTag, [_|_] = MsgIds,
fun ({Settles, Returns, Discards}) ->
{Settles, Returns, Discards ++ MsgIds}
end, {[], [], MsgIds}, Unsent0),
- {ok, State0#state{unsent_commands = Unsent}}.
+ {State0#state{unsent_commands = Unsent}, []}.
%% @doc Register with the rabbit_fifo queue to "checkout" messages as they
@@ -357,7 +366,8 @@ discard(ConsumerTag, [_|_] = MsgIds,
-spec checkout(rabbit_fifo:consumer_tag(), NumUnsettled :: non_neg_integer(),
rabbit_fifo:consumer_meta(),
state()) -> {ok, state()} | {error | timeout, term()}.
-checkout(ConsumerTag, NumUnsettled, ConsumerInfo, State0) ->
+checkout(ConsumerTag, NumUnsettled, ConsumerInfo, State0)
+ when is_map(ConsumerInfo) ->
checkout(ConsumerTag, NumUnsettled, simple_prefetch, ConsumerInfo, State0).
%% @doc Register with the rabbit_fifo queue to "checkout" messages as they
@@ -381,13 +391,23 @@ checkout(ConsumerTag, NumUnsettled, ConsumerInfo, State0) ->
CreditMode :: rabbit_fifo:credit_mode(),
Meta :: rabbit_fifo:consumer_meta(),
state()) -> {ok, state()} | {error | timeout, term()}.
-checkout(ConsumerTag, NumUnsettled, CreditMode, Meta, State0) ->
+checkout(ConsumerTag, NumUnsettled, CreditMode, Meta,
+ #state{consumer_deliveries = CDels0} = State0) ->
Servers = sorted_servers(State0),
ConsumerId = {ConsumerTag, self()},
Cmd = rabbit_fifo:make_checkout(ConsumerId,
{auto, NumUnsettled, CreditMode},
Meta),
- try_process_command(Servers, Cmd, State0).
+ %% ???
+ Ack = maps:get(ack, Meta, true),
+
+ SDels = maps:update_with(ConsumerTag,
+ fun (V) ->
+ V#consumer{ack = Ack}
+ end,
+ #consumer{last_msg_id = -1,
+ ack = Ack}, CDels0),
+ try_process_command(Servers, Cmd, State0#state{consumer_deliveries = SDels}).
%% @doc Provide credit to the queue
%%
@@ -401,7 +421,7 @@ checkout(ConsumerTag, NumUnsettled, CreditMode, Meta, State0) ->
Credit :: non_neg_integer(),
Drain :: boolean(),
state()) ->
- {ok, state()}.
+ state().
credit(ConsumerTag, Credit, Drain,
#state{consumer_deliveries = CDels} = State0) ->
ConsumerId = consumer_id(ConsumerTag),
@@ -412,11 +432,9 @@ credit(ConsumerTag, Credit, Drain,
Cmd = rabbit_fifo:make_credit(ConsumerId, Credit,
C#consumer.last_msg_id + 1, Drain),
case send_command(Node, undefined, Cmd, normal, State0) of
- {slow, S} ->
+ {_, S} ->
% turn slow into ok for this function
- {ok, S};
- {ok, _} = Ret ->
- Ret
+ S
end.
%% @doc Cancels a checkout with the rabbit_fifo queue for the consumer tag
@@ -532,12 +550,20 @@ update_machine_state(Server, Conf) ->
{internal, Correlators :: [term()], actions(), state()} |
{rabbit_fifo:client_msg(), state()} | eol.
handle_ra_event(From, {applied, Seqs},
- #state{cfg = #cfg{soft_limit = SftLmt,
- unblock_handler = UnblockFun}} = State00) ->
- State0 = State00#state{leader = From},
- {Corrs, Actions, State1} = lists:foldl(fun seq_applied/2,
- {[], [], State0},
+ #state{cfg = #cfg{cluster_name = QRef,
+ soft_limit = SftLmt,
+ unblock_handler = UnblockFun}} = State0) ->
+
+ {Corrs, Actions0, State1} = lists:foldl(fun seq_applied/2,
+ {[], [], State0#state{leader = From}},
Seqs),
+ Actions = case Corrs of
+ [] ->
+ lists:reverse(Actions0);
+ _ ->
+ [{settled, QRef, Corrs}
+ | lists:reverse(Actions0)]
+ end,
case maps:size(State1#state.pending) < SftLmt of
true when State1#state.slow == true ->
% we have exited soft limit state
@@ -567,9 +593,9 @@ handle_ra_event(From, {applied, Seqs},
end
end, State2, Commands),
UnblockFun(),
- {internal, lists:reverse(Corrs), lists:reverse(Actions), State};
+ {ok, State, Actions};
_ ->
- {internal, lists:reverse(Corrs), lists:reverse(Actions), State1}
+ {ok, State1, Actions}
end;
handle_ra_event(From, {machine, {delivery, _ConsumerTag, _} = Del}, State0) ->
handle_delivery(From, Del, State0);
@@ -580,27 +606,27 @@ handle_ra_event(_, {machine, {queue_status, Status}},
handle_ra_event(Leader, {machine, leader_change},
#state{leader = Leader} = State) ->
%% leader already known
- {internal, [], [], State};
+ {ok, State, []};
handle_ra_event(Leader, {machine, leader_change}, State0) ->
%% we need to update leader
%% and resend any pending commands
State = resend_all_pending(State0#state{leader = Leader}),
- {internal, [], [], State};
+ {ok, State, []};
handle_ra_event(_From, {rejected, {not_leader, undefined, _Seq}}, State0) ->
- % set timer to try find leder and resend
- {internal, [], [], set_timer(State0)};
+ % TODO: how should these be handled? re-sent on timer or try random
+ {ok, State0, []};
handle_ra_event(_From, {rejected, {not_leader, Leader, Seq}}, State0) ->
State1 = State0#state{leader = Leader},
State = resend(Seq, State1),
- {internal, [], [], State};
+ {ok, State, []};
handle_ra_event(_, timeout, #state{cfg = #cfg{servers = Servers}} = State0) ->
case find_leader(Servers) of
undefined ->
%% still no leader, set the timer again
- {internal, [], [], set_timer(State0)};
+ {ok, set_timer(State0), []};
Leader ->
State = resend_all_pending(State0#state{leader = Leader}),
- {internal, [], [], State}
+ {ok, State, []}
end;
handle_ra_event(_Leader, {machine, eol}, _State0) ->
eol.
@@ -695,21 +721,35 @@ resend(OldSeq, #state{pending = Pending0, leader = Leader} = State) ->
resend_all_pending(#state{pending = Pend} = State) ->
Seqs = lists:sort(maps:keys(Pend)),
- rabbit_log:info("rabbit_fifo_client: resend all pending ~w", [Seqs]),
lists:foldl(fun resend/2, State, Seqs).
-handle_delivery(From, {delivery, Tag, [{FstId, _} | _] = IdMsgs} = Del0,
- #state{consumer_deliveries = CDels0} = State0) ->
+maybe_auto_ack(true, Deliver, State0) ->
+ %% manual ack is enabled
+ {ok, State0, [Deliver]};
+maybe_auto_ack(false, {deliver, Tag, _Ack, Msgs} = Deliver, State0) ->
+ %% we have to auto ack these deliveries
+ MsgIds = [I || {_, _, I, _, _} <- Msgs],
+ {State, Actions} = settle(Tag, MsgIds, State0),
+ {ok, State, [Deliver] ++ Actions}.
+
+
+handle_delivery(Leader, {delivery, Tag, [{FstId, _} | _] = IdMsgs},
+ #state{cfg = #cfg{cluster_name = QName},
+ consumer_deliveries = CDels0} = State0) ->
+ QRef = qref(Leader),
{LastId, _} = lists:last(IdMsgs),
- %% NB: deliveries may not be from the leader so we will not update the
- %% tracked leader id here
+ Consumer = #consumer{ack = Ack} = maps:get(Tag, CDels0),
+ %% format as a deliver action
+ Del = {deliver, Tag, Ack, transform_msgs(QName, QRef, IdMsgs)},
%% TODO: remove potential default allocation
- case maps:get(Tag, CDels0, #consumer{last_msg_id = -1}) of
+ case Consumer of
#consumer{last_msg_id = Prev} = C
when FstId =:= Prev+1 ->
- {Del0, State0#state{consumer_deliveries =
- update_consumer(Tag, LastId, length(IdMsgs), C,
- CDels0)}};
+ maybe_auto_ack(Ack, Del,
+ State0#state{consumer_deliveries =
+ update_consumer(Tag, LastId,
+ length(IdMsgs), C,
+ CDels0)});
#consumer{last_msg_id = Prev} = C
when FstId > Prev+1 ->
NumMissing = FstId - Prev + 1,
@@ -719,34 +759,49 @@ handle_delivery(From, {delivery, Tag, [{FstId, _} | _] = IdMsgs} = Del0,
%% When the node is disconnected the leader will return all checked
%% out messages to the main queue to ensure they don't get stuck in
%% case the node never comes back.
- Missing = get_missing_deliveries(From, Prev+1, FstId-1, Tag),
- Del = {delivery, Tag, Missing ++ IdMsgs},
- {Del, State0#state{consumer_deliveries =
- update_consumer(Tag, LastId,
- length(IdMsgs) + NumMissing,
- C, CDels0)}};
+ Missing = get_missing_deliveries(Leader, Prev+1, FstId-1, Tag),
+ XDel = {deliver, Tag, Ack, transform_msgs(QName, QRef,
+ Missing ++ IdMsgs)},
+ maybe_auto_ack(Ack, XDel,
+ State0#state{consumer_deliveries =
+ update_consumer(Tag, LastId,
+ length(IdMsgs) + NumMissing,
+ C, CDels0)});
#consumer{last_msg_id = Prev}
when FstId =< Prev ->
case lists:dropwhile(fun({Id, _}) -> Id =< Prev end, IdMsgs) of
[] ->
- {internal, [], [], State0};
+ {ok, State0, []};
IdMsgs2 ->
- handle_delivery(From, {delivery, Tag, IdMsgs2}, State0)
+ handle_delivery(Leader, {delivery, Tag, IdMsgs2}, State0)
end;
- _ when FstId =:= 0 ->
+ C when FstId =:= 0 ->
% the very first delivery
- {Del0, State0#state{consumer_deliveries =
- update_consumer(Tag, LastId,
- length(IdMsgs),
- #consumer{last_msg_id = LastId},
- CDels0)}}
+ maybe_auto_ack(Ack, Del,
+ State0#state{consumer_deliveries =
+ update_consumer(Tag, LastId,
+ length(IdMsgs),
+ C#consumer{last_msg_id = LastId},
+ CDels0)})
end.
+transform_msgs(QName, QRef, Msgs) ->
+ lists:map(
+ fun({MsgId, {MsgHeader, Msg0}}) ->
+ {Msg, Redelivered} = case MsgHeader of
+ #{delivery_count := C} ->
+ {add_delivery_count_header(Msg0, C), true};
+ _ ->
+ {Msg0, false}
+ end,
+ {QName, QRef, MsgId, Redelivered, Msg}
+ end, Msgs).
+
update_consumer(Tag, LastId, DelCntIncr,
- #consumer{delivery_count = D}, Consumers) ->
+ #consumer{delivery_count = D} = C, Consumers) ->
maps:put(Tag,
- #consumer{last_msg_id = LastId,
- delivery_count = D + DelCntIncr},
+ C#consumer{last_msg_id = LastId,
+ delivery_count = D + DelCntIncr},
Consumers).
@@ -821,9 +876,17 @@ add_command(Cid, return, MsgIds, Acc) ->
add_command(Cid, discard, MsgIds, Acc) ->
[rabbit_fifo:make_discard(Cid, MsgIds) | Acc].
-set_timer(#state{cfg = #cfg{servers = [Server | _]}} = State) ->
+set_timer(#state{leader = Leader0,
+ cfg = #cfg{servers = [Server | _],
+ cluster_name = QName}} = State) ->
+ Leader = case Leader0 of
+ undefined -> Server;
+ _ ->
+ Leader0
+ end,
Ref = erlang:send_after(?TIMER_TIME, self(),
- {ra_event, Server, timeout}),
+ {'$gen_cast',
+ {queue_event, QName, {Leader, timeout}}}),
State#state{timer_state = Ref}.
cancel_timer(#state{timer_state = undefined} = State) ->
@@ -840,3 +903,6 @@ find_leader([Server | Servers]) ->
_ ->
find_leader(Servers)
end.
+
+qref({Ref, _}) -> Ref;
+qref(Ref) -> Ref.
diff --git a/src/rabbit_guid.erl b/src/rabbit_guid.erl
index ab1c034e3a..01e6464332 100644
--- a/src/rabbit_guid.erl
+++ b/src/rabbit_guid.erl
@@ -11,7 +11,7 @@
-export([start_link/0]).
-export([filename/0]).
--export([gen/0, gen_secure/0, string/2, binary/2]).
+-export([gen/0, gen_secure/0, string/2, binary/2, to_string/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
code_change/3]).
@@ -151,6 +151,12 @@ string(G, Prefix) when is_binary(Prefix) ->
binary(G, Prefix) ->
list_to_binary(string(G, Prefix)).
+%% copied from https://stackoverflow.com/questions/1657204/erlang-uuid-generator
+to_string(<<TL:32, TM:16, THV:16, CSR:8, CSL:8, N:48>>) ->
+ lists:flatten(
+ io_lib:format("~8.16.0b-~4.16.0b-~4.16.0b-~2.16.0b~2.16.0b-~12.16.0b",
+ [TL, TM, THV, CSR, CSL, N])).
+
%%----------------------------------------------------------------------------
init([Serial]) ->
diff --git a/src/rabbit_mirror_queue_slave.erl b/src/rabbit_mirror_queue_slave.erl
index fa1150497d..bd7a5b457d 100644
--- a/src/rabbit_mirror_queue_slave.erl
+++ b/src/rabbit_mirror_queue_slave.erl
@@ -578,11 +578,12 @@ send_or_record_confirm(published, #delivery { sender = ChPid,
send_or_record_confirm(_Status, #delivery { sender = ChPid,
confirm = true,
msg_seq_no = MsgSeqNo },
- MS, _State) ->
- ok = rabbit_misc:confirm_to_sender(ChPid, [MsgSeqNo]),
+ MS, #state{q = Q} = _State) ->
+ ok = rabbit_misc:confirm_to_sender(ChPid, amqqueue:get_name(Q), [MsgSeqNo]),
MS.
-confirm_messages(MsgIds, State = #state { msg_id_status = MS }) ->
+confirm_messages(MsgIds, State = #state{q = Q, msg_id_status = MS}) ->
+ QName = amqqueue:get_name(Q),
{CMs, MS1} =
lists:foldl(
fun (MsgId, {CMsN, MSN} = Acc) ->
@@ -610,7 +611,10 @@ confirm_messages(MsgIds, State = #state { msg_id_status = MS }) ->
Acc
end
end, {gb_trees:empty(), MS}, MsgIds),
- rabbit_misc:gb_trees_foreach(fun rabbit_misc:confirm_to_sender/2, CMs),
+ Fun = fun (Pid, MsgSeqNos) ->
+ rabbit_misc:confirm_to_sender(Pid, QName, MsgSeqNos)
+ end,
+ rabbit_misc:gb_trees_foreach(Fun, CMs),
State #state { msg_id_status = MS1 }.
handle_process_result({ok, State}) -> noreply(State);
diff --git a/src/rabbit_msg_record.erl b/src/rabbit_msg_record.erl
new file mode 100644
index 0000000000..e76c3f8dc4
--- /dev/null
+++ b/src/rabbit_msg_record.erl
@@ -0,0 +1,399 @@
+-module(rabbit_msg_record).
+
+-export([
+ init/1,
+ to_iodata/1,
+ from_amqp091/2,
+ to_amqp091/1,
+ add_message_annotations/2,
+ message_annotation/2,
+ message_annotation/3
+ ]).
+
+-include("rabbit.hrl").
+-include("rabbit_framing.hrl").
+-include_lib("amqp10_common/include/amqp10_framing.hrl").
+
+-type maybe(T) :: T | undefined.
+-type amqp10_data() :: #'v1_0.data'{} |
+ [#'v1_0.amqp_sequence'{} | #'v1_0.data'{}] |
+ #'v1_0.amqp_value'{}.
+-record(msg,
+ {
+ % header :: maybe(#'v1_0.header'{}),
+ % delivery_annotations :: maybe(#'v1_0.delivery_annotations'{}),
+ message_annotations :: maybe(#'v1_0.message_annotations'{}),
+ properties :: maybe(#'v1_0.properties'{}),
+ application_properties :: maybe(#'v1_0.application_properties'{}),
+ data :: maybe(amqp10_data())
+ % footer :: maybe(#'v1_0.footer'{})
+ }).
+
+%% holds static or rarely changing fields
+-record(cfg, {}).
+-record(?MODULE, {cfg :: #cfg{},
+ msg :: #msg{},
+ %% holds a list of modifications to various sections
+ changes = [] :: list()}).
+
+-opaque state() :: #?MODULE{}.
+
+-export_type([
+ state/0
+ ]).
+
+%% this module acts as a wrapper / converter for the internal binar storage format
+%% (AMQP 1.0) and any format it needs to be converted to / from.
+%% Efficiency is key. No unnecessary allocations or work should be done until it
+%% is absolutely needed
+
+%% init from an AMQP 1.0 encoded binary
+-spec init(binary()) -> state().
+init(Bin) when is_binary(Bin) ->
+ %% TODO: delay parsing until needed
+ {MA, P, AP, D} = decode(amqp10_framing:decode_bin(Bin),
+ {undefined, undefined, undefined, undefined}),
+ #?MODULE{cfg = #cfg{},
+ msg = #msg{properties = P,
+ application_properties = AP,
+ message_annotations = MA,
+ data = D}}.
+
+decode([], Acc) ->
+ Acc;
+decode([#'v1_0.message_annotations'{} = MA | Rem], {_, P, AP, D}) ->
+ decode(Rem, {MA, P, AP, D});
+decode([#'v1_0.properties'{} = P | Rem], {MA, _, AP, D}) ->
+ decode(Rem, {MA, P, AP, D});
+decode([#'v1_0.application_properties'{} = AP | Rem], {MA, P, _, D}) ->
+ decode(Rem, {MA, P, AP, D});
+decode([#'v1_0.data'{} = D | Rem], {MA, P, AP, _}) ->
+ decode(Rem, {MA, P, AP, D}).
+
+amqp10_properties_empty(#'v1_0.properties'{message_id = undefined,
+ user_id = undefined,
+ to = undefined,
+ % subject = wrap(utf8, RKey),
+ reply_to = undefined,
+ correlation_id = undefined,
+ content_type = undefined,
+ content_encoding = undefined,
+ creation_time = undefined}) ->
+ true;
+amqp10_properties_empty(_) ->
+ false.
+
+%% to realise the final binary data representation
+-spec to_iodata(state()) -> iodata().
+to_iodata(#?MODULE{msg = #msg{properties = P,
+ application_properties = AP,
+ message_annotations = MA,
+ data = Data}}) ->
+ [
+ case MA of
+ #'v1_0.message_annotations'{content = []} ->
+ <<>>;
+ _ ->
+ amqp10_framing:encode_bin(MA)
+ end,
+ case amqp10_properties_empty(P) of
+ true -> <<>>;
+ false ->
+ amqp10_framing:encode_bin(P)
+ end,
+ case AP of
+ #'v1_0.application_properties'{content = []} ->
+ <<>>;
+ _ ->
+ amqp10_framing:encode_bin(AP)
+ end,
+ amqp10_framing:encode_bin(Data)
+ ].
+
+%% TODO: refine type spec here
+-spec add_message_annotations(#{binary() => {atom(), term()}}, state()) ->
+ state().
+add_message_annotations(Anns,
+ #?MODULE{msg =
+ #msg{message_annotations = MA0} = Msg} = State) ->
+ Content = maps:fold(
+ fun (K, {T, V}, Acc) ->
+ map_add(symbol, K, T, V, Acc)
+ end,
+ case MA0 of
+ undefined -> [];
+ #'v1_0.message_annotations'{content = C} -> C
+ end,
+ Anns),
+
+ State#?MODULE{msg =
+ Msg#msg{message_annotations =
+ #'v1_0.message_annotations'{content = Content}}}.
+
+%% TODO: refine
+-type amqp10_term() :: {atom(), term()}.
+
+-spec message_annotation(binary(), state()) -> undefined | amqp10_term().
+message_annotation(Key, State) ->
+ message_annotation(Key, State, undefined).
+
+-spec message_annotation(binary(), state(), undefined | amqp10_term()) ->
+ undefined | amqp10_term().
+message_annotation(_Key, #?MODULE{msg = #msg{message_annotations = undefined}},
+ Default) ->
+ Default;
+message_annotation(Key,
+ #?MODULE{msg =
+ #msg{message_annotations =
+ #'v1_0.message_annotations'{content = Content}}},
+ Default)
+ when is_binary(Key) ->
+ case lists:search(fun ({{symbol, K}, _}) -> K == Key end, Content) of
+ {value, {_K, V}} ->
+ V;
+ false ->
+ Default
+ end.
+
+
+%% take a binary AMQP 1.0 input function,
+%% parses it and returns the current parse state
+%% this is the input function from storage and from, e.g. socket input
+-spec from_amqp091(#'P_basic'{}, iodata()) -> state().
+from_amqp091(#'P_basic'{message_id = MsgId,
+ expiration = Expiration,
+ delivery_mode = DelMode,
+ headers = Headers,
+ user_id = UserId,
+ reply_to = ReplyTo,
+ type = Type,
+ priority = Priority,
+ app_id = AppId,
+ correlation_id = CorrId,
+ content_type = ContentType,
+ content_encoding = ContentEncoding,
+ timestamp = Timestamp
+ }, Data) ->
+ %% TODO: support parsing properties bin directly?
+ ConvertedTs = case Timestamp of
+ undefined ->
+ undefined;
+ _ ->
+ Timestamp * 1000
+ end,
+ P = #'v1_0.properties'{message_id = wrap(utf8, MsgId),
+ user_id = wrap(binary, UserId),
+ to = undefined,
+ % subject = wrap(utf8, RKey),
+ reply_to = wrap(utf8, ReplyTo),
+ correlation_id = wrap(utf8, CorrId),
+ content_type = wrap(symbol, ContentType),
+ content_encoding = wrap(symbol, ContentEncoding),
+ creation_time = wrap(timestamp, ConvertedTs)},
+
+ APC0 = [{wrap(utf8, K), from_091(T, V)} || {K, T, V}
+ <- case Headers of
+ undefined -> [];
+ _ -> Headers
+ end],
+ %% properties that do not map directly to AMQP 1.0 properties are stored
+ %% in application properties
+ APC = map_add(utf8, <<"x-basic-type">>, utf8, Type,
+ map_add(utf8, <<"x-basic-app-id">>, utf8, AppId, APC0)),
+
+ MAC = map_add(symbol, <<"x-basic-priority">>, ubyte, Priority,
+ map_add(symbol, <<"x-basic-delivery-mode">>, ubyte, DelMode,
+ map_add(symbol, <<"x-basic-expiration">>, utf8, Expiration, []))),
+
+ AP = #'v1_0.application_properties'{content = APC},
+ MA = #'v1_0.message_annotations'{content = MAC},
+ #?MODULE{cfg = #cfg{},
+ msg = #msg{properties = P,
+ application_properties = AP,
+ message_annotations = MA,
+ data = #'v1_0.data'{content = Data}}}.
+
+map_add(_T, _Key, _Type, undefined, Acc) ->
+ Acc;
+map_add(KeyType, Key, Type, Value, Acc) ->
+ [{wrap(KeyType, Key), wrap(Type, Value)} | Acc].
+
+-spec to_amqp091(state()) -> {#'P_basic'{}, iodata()}.
+to_amqp091(#?MODULE{msg = #msg{properties = P,
+ application_properties = APR,
+ message_annotations = MAR,
+ data = #'v1_0.data'{content = Payload}}}) ->
+ #'v1_0.properties'{message_id = MsgId,
+ user_id = UserId,
+ reply_to = ReplyTo0,
+ correlation_id = CorrId,
+ content_type = ContentType,
+ content_encoding = ContentEncoding,
+ creation_time = Timestamp} = case P of
+ undefined ->
+ #'v1_0.properties'{};
+ _ ->
+ P
+ end,
+
+ AP0 = case APR of
+ #'v1_0.application_properties'{content = AC} -> AC;
+ _ -> []
+ end,
+ MA0 = case MAR of
+ #'v1_0.message_annotations'{content = MC} -> MC;
+ _ -> []
+ end,
+
+ {Type, AP1} = amqp10_map_get(utf8(<<"x-basic-type">>), AP0),
+ {AppId, AP} = amqp10_map_get(utf8(<<"x-basic-app-id">>), AP1),
+
+ {Priority, MA1} = amqp10_map_get(symbol(<<"x-basic-priority">>), MA0),
+ {DelMode, MA2} = amqp10_map_get(symbol(<<"x-basic-delivery-mode">>), MA1),
+ {Expiration, _MA} = amqp10_map_get(symbol(<<"x-basic-expiration">>), MA2),
+
+ Headers0 = [to_091(unwrap(K), V) || {K, V} <- AP],
+ {Headers1, MsgId091} = message_id(MsgId, <<"x-message-id-type">>, Headers0),
+ {Headers, CorrId091} = message_id(CorrId, <<"x-correlation-id-type">>, Headers1),
+
+ BP = #'P_basic'{message_id = MsgId091,
+ delivery_mode = DelMode,
+ expiration = Expiration,
+ user_id = unwrap(UserId),
+ headers = case Headers of
+ [] -> undefined;
+ _ -> Headers
+ end,
+ reply_to = unwrap(ReplyTo0),
+ type = Type,
+ app_id = AppId,
+ priority = Priority,
+ correlation_id = CorrId091,
+ content_type = unwrap(ContentType),
+ content_encoding = unwrap(ContentEncoding),
+ timestamp = case unwrap(Timestamp) of
+ undefined ->
+ undefined;
+ Ts ->
+ Ts div 1000
+ end
+ },
+ {BP, Payload}.
+
+%%% Internal
+
+amqp10_map_get(K, AP0) ->
+ case lists:keytake(K, 1, AP0) of
+ false ->
+ {undefined, AP0};
+ {value, {_, V}, AP} ->
+ {unwrap(V), AP}
+ end.
+
+wrap(_Type, undefined) ->
+ undefined;
+wrap(Type, Val) ->
+ {Type, Val}.
+
+unwrap(undefined) ->
+ undefined;
+unwrap({_Type, V}) ->
+ V.
+
+% symbol_for(#'v1_0.properties'{}) ->
+% {symbol, <<"amqp:properties:list">>};
+
+% number_for(#'v1_0.properties'{}) ->
+% {ulong, 115};
+% encode(Frame = #'v1_0.properties'{}) ->
+% amqp10_framing:encode_described(list, 115, Frame);
+
+% encode_described(list, CodeNumber, Frame) ->
+% {described, {ulong, CodeNumber},
+% {list, lists:map(fun encode/1, tl(tuple_to_list(Frame)))}};
+
+% -spec generate(amqp10_type()) -> iolist().
+% generate({described, Descriptor, Value}) ->
+% DescBin = generate(Descriptor),
+% ValueBin = generate(Value),
+% [ ?DESCRIBED_BIN, DescBin, ValueBin ].
+
+to_091(Key, {utf8, V}) when is_binary(V) -> {Key, longstr, V};
+to_091(Key, {long, V}) -> {Key, long, V};
+to_091(Key, {byte, V}) -> {Key, byte, V};
+to_091(Key, {ubyte, V}) -> {Key, unsignedbyte, V};
+to_091(Key, {short, V}) -> {Key, short, V};
+to_091(Key, {ushort, V}) -> {Key, unsignedshort, V};
+to_091(Key, {uint, V}) -> {Key, unsignedint, V};
+to_091(Key, {int, V}) -> {Key, signedint, V};
+to_091(Key, {double, V}) -> {Key, double, V};
+to_091(Key, {float, V}) -> {Key, float, V};
+%% NB: header values can never be shortstr!
+to_091(Key, {timestamp, V}) -> {Key, timestamp, V div 1000};
+to_091(Key, {binary, V}) -> {Key, binary, V};
+to_091(Key, {boolean, V}) -> {Key, bool, V};
+to_091(Key, true) -> {Key, bool, true};
+to_091(Key, false) -> {Key, bool, false}.
+
+from_091(longstr, V) when is_binary(V) -> {utf8, V};
+from_091(long, V) -> {long, V};
+from_091(unsignedbyte, V) -> {ubyte, V};
+from_091(short, V) -> {short, V};
+from_091(unsignedshort, V) -> {ushort, V};
+from_091(unsignedint, V) -> {uint, V};
+from_091(signedint, V) -> {int, V};
+from_091(double, V) -> {double, V};
+from_091(float, V) -> {float, V};
+from_091(bool, V) -> {boolean, V};
+from_091(binary, V) -> {binary, V};
+from_091(timestamp, V) -> {timestamp, V * 1000}.
+
+% convert_header(signedint, V) -> [$I, <<V:32/signed>>];
+% convert_header(decimal, V) -> {Before, After} = V,
+% [$D, Before, <<After:32>>];
+% convert_header(timestamp, V) -> [$T, <<V:64>>];
+% % convert_header(table, V) -> [$F | table_to_binary(V)];
+% % convert_header(array, V) -> [$A | array_to_binary(V)];
+% convert_header(byte, V) -> [$b, <<V:8/signed>>];
+% convert_header(double, V) -> [$d, <<V:64/float>>];
+% convert_header(float, V) -> [$f, <<V:32/float>>];
+% convert_header(short, V) -> [$s, <<V:16/signed>>];
+% convert_header(binary, V) -> [$x | long_string_to_binary(V)];
+% convert_header(unsignedbyte, V) -> [$B, <<V:8/unsigned>>];
+% convert_header(unsignedshort, V) -> [$u, <<V:16/unsigned>>];
+% convert_header(unsignedint, V) -> [$i, <<V:32/unsigned>>];
+% convert_header(void, _V) -> [$V].
+
+utf8(T) -> {utf8, T}.
+symbol(T) -> {symbol, T}.
+
+message_id({uuid, UUID}, HKey, H0) ->
+ H = [{HKey, longstr, <<"uuid">>} | H0],
+ {H, rabbit_data_coercion:to_binary(rabbit_guid:to_string(UUID))};
+message_id({ulong, N}, HKey, H0) ->
+ H = [{HKey, longstr, <<"ulong">>} | H0],
+ {H, erlang:integer_to_binary(N)};
+message_id({binary, B}, HKey, H0) ->
+ E = base64:encode(B),
+ case byte_size(E) > 256 of
+ true ->
+ K = binary:replace(HKey, <<"-type">>, <<>>),
+ {[{K, longstr, B} | H0], undefined};
+ false ->
+ H = [{HKey, longstr, <<"binary">>} | H0],
+ {H, E}
+ end;
+message_id({utf8, S}, HKey, H0) ->
+ case byte_size(S) > 256 of
+ true ->
+ K = binary:replace(HKey, <<"-type">>, <<>>),
+ {[{K, longstr, S} | H0], undefined};
+ false ->
+ {H0, S}
+ end;
+message_id(MsgId, _, H) ->
+ {H, unwrap(MsgId)}.
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+-endif.
diff --git a/src/rabbit_osiris_metrics.erl b/src/rabbit_osiris_metrics.erl
new file mode 100644
index 0000000000..7b2574c7e1
--- /dev/null
+++ b/src/rabbit_osiris_metrics.erl
@@ -0,0 +1,103 @@
+%% 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.
+%%
+%% Copyright (c) 2012-2020 VMware, Inc. or its affiliates. All rights reserved.
+%%
+
+-module(rabbit_osiris_metrics).
+
+-behaviour(gen_server).
+
+-export([start_link/0]).
+
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
+ code_change/3]).
+
+-define(TICK_TIMEOUT, 5000).
+-define(SERVER, ?MODULE).
+
+-define(STATISTICS_KEYS,
+ [policy,
+ operator_policy,
+ effective_policy_definition,
+ state,
+ leader,
+ online,
+ members
+ ]).
+
+-record(state, {timeout :: non_neg_integer()}).
+
+%%----------------------------------------------------------------------------
+%% Starts the raw metrics storage and owns the ETS tables.
+%%----------------------------------------------------------------------------
+
+-spec start_link() -> rabbit_types:ok_pid_or_error().
+
+start_link() ->
+ gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
+
+init([]) ->
+ Timeout = application:get_env(rabbit, stream_tick_interval,
+ ?TICK_TIMEOUT),
+ erlang:send_after(Timeout, self(), tick),
+ {ok, #state{timeout = Timeout}}.
+
+handle_call(_Request, _From, State) ->
+ {noreply, State}.
+
+handle_cast(_Request, State) ->
+ {noreply, State}.
+
+handle_info(tick, #state{timeout = Timeout} = State) ->
+ Data = osiris_counters:overview(),
+ maps:map(
+ fun ({osiris_writer, QName}, #{offset := Offs,
+ first_offset := FstOffs}) ->
+ COffs = Offs + 1 - FstOffs,
+ rabbit_core_metrics:queue_stats(QName, COffs, 0, COffs, 0),
+ Infos = try
+ %% TODO complete stats!
+ case rabbit_amqqueue:lookup(QName) of
+ {ok, Q} ->
+ rabbit_stream_queue:info(Q, ?STATISTICS_KEYS);
+ _ ->
+ []
+ end
+ catch
+ _:_ ->
+ %% It's possible that the writer has died but
+ %% it's still on the amqqueue record, so the
+ %% `erlang:process_info/2` calls will return
+ %% `undefined` and crash with a badmatch.
+ %% At least for now, skipping the metrics might
+ %% be the best option. Otherwise this brings
+ %% down `rabbit_sup` and the whole `rabbit` app.
+ []
+ end,
+ rabbit_core_metrics:queue_stats(QName, Infos),
+ rabbit_event:notify(queue_stats, Infos ++ [{name, QName},
+ {messages, COffs},
+ {messages_ready, COffs},
+ {messages_unacknowledged, 0}]),
+ ok;
+ (_, _V) ->
+ ok
+ end, Data),
+ erlang:send_after(Timeout, self(), tick),
+ {noreply, State}.
+
+terminate(_Reason, _State) ->
+ ok.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
diff --git a/src/rabbit_policies.erl b/src/rabbit_policies.erl
index d1d0401ba4..0dd56a8ccd 100644
--- a/src/rabbit_policies.erl
+++ b/src/rabbit_policies.erl
@@ -39,6 +39,8 @@ register() ->
{policy_validator, <<"queue-mode">>},
{policy_validator, <<"overflow">>},
{policy_validator, <<"delivery-limit">>},
+ {policy_validator, <<"max-age">>},
+ {policy_validator, <<"max-segment-size">>},
{operator_policy_validator, <<"expires">>},
{operator_policy_validator, <<"message-ttl">>},
{operator_policy_validator, <<"max-length">>},
@@ -135,7 +137,21 @@ validate_policy0(<<"delivery-limit">>, Value)
when is_integer(Value), Value >= 0 ->
ok;
validate_policy0(<<"delivery-limit">>, Value) ->
- {error, "~p is not a valid delivery limit", [Value]}.
+ {error, "~p is not a valid delivery limit", [Value]};
+
+validate_policy0(<<"max-age">>, Value) ->
+ case rabbit_amqqueue:check_max_age(Value) of
+ {error, _} ->
+ {error, "~p is not a valid max age", [Value]};
+ _ ->
+ ok
+ end;
+
+validate_policy0(<<"max-segment-size">>, Value)
+ when is_integer(Value), Value >= 0 ->
+ ok;
+validate_policy0(<<"max-segment-size">>, Value) ->
+ {error, "~p is not a valid segment size", [Value]}.
merge_policy_value(<<"message-ttl">>, Val, OpVal) -> min(Val, OpVal);
merge_policy_value(<<"max-length">>, Val, OpVal) -> min(Val, OpVal);
diff --git a/src/rabbit_policy.erl b/src/rabbit_policy.erl
index 7da6e35cbd..44807de97d 100644
--- a/src/rabbit_policy.erl
+++ b/src/rabbit_policy.erl
@@ -472,10 +472,15 @@ matches_type(_, _) -> false.
sort_pred(A, B) -> pget(priority, A) >= pget(priority, B).
is_applicable(#resource{kind = queue} = Resource, Policy) ->
- rabbit_amqqueue:is_policy_applicable(Resource, Policy);
+ rabbit_amqqueue:is_policy_applicable(Resource, to_list(Policy));
is_applicable(_, _) ->
true.
+to_list(L) when is_list(L) ->
+ L;
+to_list(M) when is_map(M) ->
+ maps:to_list(M).
+
%%----------------------------------------------------------------------------
operator_policy_validation() ->
diff --git a/src/rabbit_queue_type.erl b/src/rabbit_queue_type.erl
new file mode 100644
index 0000000000..f8dae3910e
--- /dev/null
+++ b/src/rabbit_queue_type.erl
@@ -0,0 +1,560 @@
+-module(rabbit_queue_type).
+-include("amqqueue.hrl").
+-include_lib("rabbit_common/include/resource.hrl").
+
+-export([
+ init/0,
+ close/1,
+ discover/1,
+ default/0,
+ is_enabled/1,
+ declare/2,
+ delete/4,
+ is_recoverable/1,
+ recover/2,
+ purge/1,
+ policy_changed/1,
+ stat/1,
+ remove/2,
+ info/2,
+ state_info/1,
+ info_down/3,
+ %% stateful client API
+ new/2,
+ consume/3,
+ cancel/5,
+ handle_down/3,
+ handle_event/3,
+ module/2,
+ deliver/3,
+ settle/5,
+ credit/5,
+ dequeue/5,
+ fold_state/3,
+ is_policy_applicable/2
+ ]).
+
+%% temporary
+-export([with/3]).
+
+%% gah what is a good identity of a classic queue including all replicas
+-type queue_name() :: rabbit_types:r(queue).
+-type queue_ref() :: queue_name() | atom().
+-type queue_state() :: term().
+-type msg_tag() :: term().
+
+-define(STATE, ?MODULE).
+
+-define(QREF(QueueReference),
+ (is_tuple(QueueReference) andalso element(1, QueueReference) == resource)
+ orelse is_atom(QueueReference)).
+%% anything that the host process needs to do on behalf of the queue type
+%% session, like knowing when to notify on monitor down
+-type action() ::
+ {monitor, Pid :: pid(), queue_ref()} |
+ %% indicate to the queue type module that a message has been delivered
+ %% fully to the queue
+ {settled, Success :: boolean(), [msg_tag()]} |
+ {deliver, rabbit_type:ctag(), boolean(), [rabbit_amqqueue:qmsg()]}.
+
+-type actions() :: [action()].
+
+-type event() ::
+ {down, pid(), Info :: term()} |
+ term().
+
+-record(ctx, {module :: module(),
+ name :: queue_name(),
+ %% "publisher confirm queue accounting"
+ %% queue type implementation should emit a:
+ %% {settle, Success :: boolean(), msg_tag()}
+ %% to either settle or reject the delivery of a
+ %% message to the queue instance
+ %% The queue type module will then emit a {confirm | reject, [msg_tag()}
+ %% action to the channel or channel like process when a msg_tag
+ %% has reached its conclusion
+ % unsettled = #{} :: #{msg_tag() => [queue_ref()]},
+ state :: queue_state()}).
+
+
+-record(?STATE, {ctxs = #{} :: #{queue_ref() => #ctx{} | queue_ref()},
+ monitor_registry = #{} :: #{pid() => queue_ref()}
+ }).
+
+-opaque state() :: #?STATE{}.
+
+-type consume_spec() :: #{no_ack := boolean(),
+ channel_pid := pid(),
+ limiter_pid => pid(),
+ limiter_active => boolean(),
+ prefetch_count => non_neg_integer(),
+ consumer_tag := rabbit_types:ctag(),
+ exclusive_consume => boolean(),
+ args => rabbit_framing:amqp_table(),
+ ok_msg := term(),
+ acting_user := rabbit_types:username()}.
+
+
+
+% copied from rabbit_amqqueue
+-type absent_reason() :: 'nodedown' | 'crashed' | stopped | timeout.
+
+-type settle_op() :: 'complete' | 'requeue' | 'discard'.
+
+-export_type([state/0,
+ consume_spec/0,
+ action/0,
+ actions/0,
+ settle_op/0]).
+
+%% is the queue type feature enabled
+-callback is_enabled() -> boolean().
+
+-callback declare(amqqueue:amqqueue(), node()) ->
+ {'new' | 'existing' | 'owner_died', amqqueue:amqqueue()} |
+ {'absent', amqqueue:amqqueue(), absent_reason()} |
+ rabbit_types:channel_exit().
+
+-callback delete(amqqueue:amqqueue(),
+ boolean(),
+ boolean(),
+ rabbit_types:username()) ->
+ rabbit_types:ok(non_neg_integer()) |
+ rabbit_types:error(in_use | not_empty).
+
+-callback recover(rabbit_types:vhost(), [amqqueue:amqqueue()]) ->
+ {Recovered :: [amqqueue:amqqueue()],
+ Failed :: [amqqueue:amqqueue()]}.
+
+%% checks if the queue should be recovered
+-callback is_recoverable(amqqueue:amqqueue()) ->
+ boolean().
+
+-callback purge(amqqueue:amqqueue()) ->
+ {ok, non_neg_integer()} | {error, term()}.
+
+-callback policy_changed(amqqueue:amqqueue()) -> ok.
+
+%% stateful
+%% intitialise and return a queue type specific session context
+-callback init(amqqueue:amqqueue()) -> queue_state().
+
+-callback close(queue_state()) -> ok.
+%% update the queue type state from amqqrecord
+-callback update(amqqueue:amqqueue(), queue_state()) -> queue_state().
+
+-callback consume(amqqueue:amqqueue(),
+ consume_spec(),
+ queue_state()) ->
+ {ok, queue_state(), actions()} | {error, term()}.
+
+-callback cancel(amqqueue:amqqueue(),
+ rabbit_types:ctag(),
+ term(),
+ rabbit_types:username(),
+ queue_state()) ->
+ {ok, queue_state()} | {error, term()}.
+
+%% any async events returned from the queue system should be processed through
+%% this
+-callback handle_event(Event :: event(),
+ queue_state()) ->
+ {ok, queue_state(), actions()} | {error, term()} | eol.
+
+-callback deliver([{amqqueue:amqqueue(), queue_state()}],
+ Delivery :: term()) ->
+ {[{amqqueue:amqqueue(), queue_state()}], actions()}.
+
+-callback settle(settle_op(), rabbit_types:ctag(), [non_neg_integer()], queue_state()) ->
+ {queue_state(), actions()}.
+
+-callback credit(rabbit_types:ctag(),
+ non_neg_integer(), Drain :: boolean(), queue_state()) ->
+ queue_state().
+
+-callback dequeue(NoAck :: boolean(), LimiterPid :: pid(),
+ rabbit_types:ctag(), queue_state()) ->
+ {ok, Count :: non_neg_integer(), rabbit_amqqueue:qmsg(), queue_state()} |
+ {empty, queue_state()} |
+ {error, term()}.
+
+%% return a map of state summary information
+-callback state_info(queue_state()) ->
+ #{atom() := term()}.
+
+%% general queue info
+-callback info(amqqueue:amqqueue(), all_keys | rabbit_types:info_keys()) ->
+ rabbit_types:infos().
+
+-callback stat(amqqueue:amqqueue()) ->
+ {'ok', non_neg_integer(), non_neg_integer()}.
+
+-callback is_policy_applicable(amqqueue:amqqueue(), any()) ->
+ boolean().
+
+%% TODO: this should be controlled by a registry that is populated on boot
+discover(<<"quorum">>) ->
+ rabbit_quorum_queue;
+discover(<<"classic">>) ->
+ rabbit_classic_queue;
+discover(<<"stream">>) ->
+ rabbit_stream_queue.
+
+default() ->
+ rabbit_classic_queue.
+
+-spec is_enabled(module()) -> boolean().
+is_enabled(Type) ->
+ Type:is_enabled().
+
+-spec declare(amqqueue:amqqueue(), node()) ->
+ {'new' | 'existing' | 'owner_died', amqqueue:amqqueue()} |
+ {'absent', amqqueue:amqqueue(), absent_reason()} |
+ rabbit_types:channel_exit().
+declare(Q, Node) ->
+ Mod = amqqueue:get_type(Q),
+ Mod:declare(Q, Node).
+
+-spec delete(amqqueue:amqqueue(), boolean(),
+ boolean(), rabbit_types:username()) ->
+ rabbit_types:ok(non_neg_integer()) |
+ rabbit_types:error(in_use | not_empty).
+delete(Q, IfUnused, IfEmpty, ActingUser) ->
+ Mod = amqqueue:get_type(Q),
+ Mod:delete(Q, IfUnused, IfEmpty, ActingUser).
+
+-spec purge(amqqueue:amqqueue()) ->
+ {'ok', non_neg_integer()}.
+purge(Q) ->
+ Mod = amqqueue:get_type(Q),
+ Mod:purge(Q).
+
+-spec policy_changed(amqqueue:amqqueue()) -> 'ok'.
+policy_changed(Q) ->
+ Mod = amqqueue:get_type(Q),
+ Mod:policy_changed(Q).
+
+-spec stat(amqqueue:amqqueue()) ->
+ {'ok', non_neg_integer(), non_neg_integer()}.
+stat(Q) ->
+ Mod = amqqueue:get_type(Q),
+ Mod:stat(Q).
+
+-spec remove(queue_ref(), state()) -> state().
+remove(QRef, #?STATE{ctxs = Ctxs0} = State) ->
+ case maps:take(QRef, Ctxs0) of
+ error ->
+ State#?STATE{ctxs = Ctxs0};
+ {_, Ctxs} ->
+ %% remove all linked queue refs
+ State#?STATE{ctxs = maps:filter(fun (_, V) ->
+ V == QRef
+ end, Ctxs)}
+ end.
+
+-spec info(amqqueue:amqqueue(), all_keys | rabbit_types:info_keys()) ->
+ rabbit_types:infos().
+info(Q, Items) when ?amqqueue_state_is(Q, crashed) ->
+ info_down(Q, Items, crashed);
+info(Q, Items) when ?amqqueue_state_is(Q, stopped) ->
+ info_down(Q, Items, stopped);
+info(Q, Items) ->
+ Mod = amqqueue:get_type(Q),
+ Mod:info(Q, Items).
+
+fold_state(Fun, Acc, #?STATE{ctxs = Ctxs}) ->
+ maps:fold(Fun, Acc, Ctxs).
+
+state_info(#ctx{state = S,
+ module = Mod}) ->
+ Mod:state_info(S);
+state_info(_) ->
+ #{}.
+
+info_down(Q, all_keys, DownReason) ->
+ info_down(Q, rabbit_amqqueue_process:info_keys(), DownReason);
+info_down(Q, Items, DownReason) ->
+ [{Item, i_down(Item, Q, DownReason)} || Item <- Items].
+
+i_down(name, Q, _) -> amqqueue:get_name(Q);
+i_down(durable, Q, _) -> amqqueue:is_durable(Q);
+i_down(auto_delete, Q, _) -> amqqueue:is_auto_delete(Q);
+i_down(arguments, Q, _) -> amqqueue:get_arguments(Q);
+i_down(pid, Q, _) -> amqqueue:get_pid(Q);
+i_down(recoverable_slaves, Q, _) -> amqqueue:get_recoverable_slaves(Q);
+i_down(type, Q, _) -> amqqueue:get_type(Q);
+i_down(state, _Q, DownReason) -> DownReason;
+i_down(K, _Q, _DownReason) ->
+ case lists:member(K, rabbit_amqqueue_process:info_keys()) of
+ true -> '';
+ false -> throw({bad_argument, K})
+ end.
+
+is_policy_applicable(Q, Policy) ->
+ Mod = amqqueue:get_type(Q),
+ Mod:is_policy_applicable(Q, Policy).
+
+-spec init() -> state().
+init() ->
+ #?STATE{}.
+
+-spec close(state()) -> ok.
+close(#?STATE{ctxs = Contexts}) ->
+ _ = maps:map(
+ fun (_, #ctx{module = Mod,
+ state = S}) ->
+ ok = Mod:close(S)
+ end, Contexts),
+ ok.
+
+-spec new(amqqueue:amqqueue(), state()) -> state().
+new(Q, State) when ?is_amqqueue(Q) ->
+ Ctx = get_ctx(Q, State),
+ set_ctx(Q, Ctx, State).
+
+-spec consume(amqqueue:amqqueue(), consume_spec(), state()) ->
+ {ok, state(), actions()} | {error, term()}.
+consume(Q, Spec, State) ->
+ #ctx{state = State0} = Ctx = get_ctx(Q, State),
+ Mod = amqqueue:get_type(Q),
+ case Mod:consume(Q, Spec, State0) of
+ {ok, CtxState, Actions} ->
+ return_ok(set_ctx(Q, Ctx#ctx{state = CtxState}, State), Actions);
+ Err ->
+ Err
+ end.
+
+%% TODO switch to cancel spec api
+-spec cancel(amqqueue:amqqueue(),
+ rabbit_types:ctag(),
+ term(),
+ rabbit_types:username(),
+ state()) ->
+ {ok, state()} | {error, term()}.
+cancel(Q, Tag, OkMsg, ActiveUser, Ctxs) ->
+ #ctx{state = State0} = Ctx = get_ctx(Q, Ctxs),
+ Mod = amqqueue:get_type(Q),
+ case Mod:cancel(Q, Tag, OkMsg, ActiveUser, State0) of
+ {ok, State} ->
+ {ok, set_ctx(Q, Ctx#ctx{state = State}, Ctxs)};
+ Err ->
+ Err
+ end.
+
+-spec is_recoverable(amqqueue:amqqueue()) ->
+ boolean().
+is_recoverable(Q) ->
+ Mod = amqqueue:get_type(Q),
+ Mod:is_recoverable(Q).
+
+-spec recover(rabbit_types:vhost(), [amqqueue:amqqueue()]) ->
+ {Recovered :: [amqqueue:amqqueue()],
+ Failed :: [amqqueue:amqqueue()]}.
+recover(VHost, Qs) ->
+ ByType = lists:foldl(
+ fun (Q, Acc) ->
+ T = amqqueue:get_type(Q),
+ maps:update_with(T, fun (X) ->
+ [Q | X]
+ end, Acc)
+ %% TODO resolve all registered queue types from registry
+ end, #{rabbit_classic_queue => [],
+ rabbit_quorum_queue => [],
+ rabbit_stream_queue => []}, Qs),
+ maps:fold(fun (Mod, Queues, {R0, F0}) ->
+ {R, F} = Mod:recover(VHost, Queues),
+ {R0 ++ R, F0 ++ F}
+ end, {[], []}, ByType).
+
+-spec handle_down(pid(), term(), state()) ->
+ {ok, state(), actions()} | {eol, queue_ref()} | {error, term()}.
+handle_down(Pid, Info, #?STATE{monitor_registry = Reg} = State0) ->
+ %% lookup queue ref in monitor registry
+ case Reg of
+ #{Pid := QRef} ->
+ %% TODO: remove Pid from monitor_registry
+ case handle_event(QRef, {down, Pid, Info}, State0) of
+ {ok, State, Actions} ->
+ {ok, State, [{queue_down, QRef} | Actions]};
+ eol ->
+ {eol, QRef};
+ Err ->
+ Err
+ end;
+ _ ->
+ {ok, State0, []}
+ end.
+
+%% messages sent from queues
+-spec handle_event(queue_ref(), term(), state()) ->
+ {ok, state(), actions()} | eol | {error, term()}.
+handle_event(QRef, Evt, Ctxs) ->
+ %% events can arrive after a queue state has been cleared up
+ %% so need to be defensive here
+ case get_ctx(QRef, Ctxs, undefined) of
+ #ctx{module = Mod,
+ state = State0} = Ctx ->
+ case Mod:handle_event(Evt, State0) of
+ {ok, State, Actions} ->
+ return_ok(set_ctx(QRef, Ctx#ctx{state = State}, Ctxs), Actions);
+ Err ->
+ Err
+ end;
+ undefined ->
+ {ok, Ctxs, []}
+ end.
+
+-spec module(queue_ref(), state()) ->
+ {ok, module()} | {error, not_found}.
+module(QRef, Ctxs) ->
+ %% events can arrive after a queue state has been cleared up
+ %% so need to be defensive here
+ case get_ctx(QRef, Ctxs, undefined) of
+ #ctx{module = Mod} ->
+ {ok, Mod};
+ undefined ->
+ {error, not_found}
+ end.
+
+-spec deliver([amqqueue:amqqueue()], Delivery :: term(),
+ stateless | state()) ->
+ {ok, state(), actions()}.
+deliver(Qs, Delivery, stateless) ->
+ _ = lists:map(fun(Q) ->
+ Mod = amqqueue:get_type(Q),
+ _ = Mod:deliver([{Q, stateless}], Delivery)
+ end, Qs),
+ {ok, stateless, []};
+deliver(Qs, Delivery, #?STATE{} = State0) ->
+ %% sort by queue type - then dispatch each group
+ ByType = lists:foldl(
+ fun (Q, Acc) ->
+ T = amqqueue:get_type(Q),
+ Ctx = get_ctx(Q, State0),
+ maps:update_with(
+ T, fun (A) ->
+ [{Q, Ctx#ctx.state} | A]
+ end, [{Q, Ctx#ctx.state}], Acc)
+ end, #{}, Qs),
+ %%% dispatch each group to queue type interface?
+ {Xs, Actions} = maps:fold(fun(Mod, QSs, {X0, A0}) ->
+ {X, A} = Mod:deliver(QSs, Delivery),
+ {X0 ++ X, A0 ++ A}
+ end, {[], []}, ByType),
+ State = lists:foldl(
+ fun({Q, S}, Acc) ->
+ Ctx = get_ctx(Q, Acc),
+ set_ctx(qref(Q), Ctx#ctx{state = S}, Acc)
+ end, State0, Xs),
+ return_ok(State, Actions).
+
+
+-spec settle(queue_ref(), settle_op(), rabbit_types:ctag(),
+ [non_neg_integer()], state()) -> {ok, state(), actions()}.
+settle(QRef, Op, CTag, MsgIds, Ctxs)
+ when ?QREF(QRef) ->
+ #ctx{state = State0,
+ module = Mod} = Ctx = get_ctx(QRef, Ctxs),
+ {State, Actions} = Mod:settle(Op, CTag, MsgIds, State0),
+ {ok, set_ctx(QRef, Ctx#ctx{state = State}, Ctxs), Actions}.
+
+-spec credit(amqqueue:amqqueue() | queue_ref(),
+ rabbit_types:ctag(), non_neg_integer(),
+ boolean(), state()) -> state().
+credit(Q, CTag, Credit, Drain, Ctxs) ->
+ #ctx{state = State0,
+ module = Mod} = Ctx = get_ctx(Q, Ctxs),
+ State = Mod:credit(CTag, Credit, Drain, State0),
+ set_ctx(Q, Ctx#ctx{state = State}, Ctxs).
+
+-spec dequeue(amqqueue:amqqueue(), boolean(),
+ pid(), rabbit_types:ctag(), state()) ->
+ {ok, non_neg_integer(), term(), state()} |
+ {empty, state()}.
+dequeue(Q, NoAck, LimiterPid, CTag, Ctxs) ->
+ #ctx{state = State0} = Ctx = get_ctx(Q, Ctxs),
+ Mod = amqqueue:get_type(Q),
+ case Mod:dequeue(NoAck, LimiterPid, CTag, State0) of
+ {ok, Num, Msg, State} ->
+ {ok, Num, Msg, set_ctx(Q, Ctx#ctx{state = State}, Ctxs)};
+ {empty, State} ->
+ {empty, set_ctx(Q, Ctx#ctx{state = State}, Ctxs)};
+ {error, _} = Err ->
+ Err
+ end.
+
+%% temporary
+with(QRef, Fun, Ctxs) ->
+ #ctx{state = State0} = Ctx = get_ctx(QRef, Ctxs),
+ {Res, State} = Fun(State0),
+ {Res, set_ctx(QRef, Ctx#ctx{state = State}, Ctxs)}.
+
+
+get_ctx(Q, #?STATE{ctxs = Contexts}) when ?is_amqqueue(Q) ->
+ Ref = qref(Q),
+ case Contexts of
+ #{Ref := #ctx{module = Mod,
+ state = State} = Ctx} ->
+ Ctx#ctx{state = Mod:update(Q, State)};
+ _ ->
+ %% not found - initialize
+ Mod = amqqueue:get_type(Q),
+ Name = amqqueue:get_name(Q),
+ #ctx{module = Mod,
+ name = Name,
+ state = Mod:init(Q)}
+ end;
+get_ctx(QRef, Contexts) when ?QREF(QRef) ->
+ case get_ctx(QRef, Contexts, undefined) of
+ undefined ->
+ exit({queue_context_not_found, QRef});
+ Ctx ->
+ Ctx
+ end.
+
+get_ctx(QRef, #?STATE{ctxs = Contexts}, Default) ->
+ Ref = qref(QRef),
+ %% if we use a QRef it should always be initialised
+ case maps:get(Ref, Contexts, undefined) of
+ #ctx{} = Ctx ->
+ Ctx;
+ undefined ->
+ Default
+ end.
+
+set_ctx(Q, Ctx, #?STATE{ctxs = Contexts} = State)
+ when ?is_amqqueue(Q) ->
+ Ref = qref(Q),
+ State#?STATE{ctxs = maps:put(Ref, Ctx, Contexts)};
+set_ctx(QRef, Ctx, #?STATE{ctxs = Contexts} = State) ->
+ Ref = qref(QRef),
+ State#?STATE{ctxs = maps:put(Ref, Ctx, Contexts)}.
+
+qref(#resource{kind = queue} = QName) ->
+ QName;
+qref(Q) when ?is_amqqueue(Q) ->
+ amqqueue:get_name(Q).
+
+return_ok(State0, []) ->
+ {ok, State0, []};
+return_ok(State0, Actions0) ->
+ {State, Actions} =
+ lists:foldl(
+ fun({monitor, Pid, QRef},
+ {#?STATE{monitor_registry = M0} = S0, A0}) ->
+ case M0 of
+ #{Pid := QRef} ->
+ %% already monitored by the qref
+ {S0, A0};
+ #{Pid := _} ->
+ %% TODO: allow multiple Qrefs to monitor the same pid
+ exit(return_ok_duplicate_montored_pid);
+ _ ->
+ _ = erlang:monitor(process, Pid),
+ M = M0#{Pid => QRef},
+ {S0#?STATE{monitor_registry = M}, A0}
+ end;
+ (Act, {S, A0}) ->
+ {S, [Act | A0]}
+ end, {State0, []}, Actions0),
+ {ok, State, lists:reverse(Actions)}.
diff --git a/src/rabbit_queue_type_util.erl b/src/rabbit_queue_type_util.erl
new file mode 100644
index 0000000000..0ff7fb312d
--- /dev/null
+++ b/src/rabbit_queue_type_util.erl
@@ -0,0 +1,80 @@
+%% 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) 2018-2020 Pivotal Software, Inc. All rights reserved.
+%%
+
+-module(rabbit_queue_type_util).
+
+-export([check_invalid_arguments/3,
+ args_policy_lookup/3,
+ qname_to_internal_name/1,
+ check_auto_delete/1,
+ check_exclusive/1,
+ check_non_durable/1]).
+
+-include("rabbit.hrl").
+-include("amqqueue.hrl").
+
+check_invalid_arguments(QueueName, Args, Keys) ->
+ [case rabbit_misc:table_lookup(Args, Key) of
+ undefined -> ok;
+ _TypeVal -> rabbit_misc:protocol_error(
+ precondition_failed,
+ "invalid arg '~s' for ~s",
+ [Key, rabbit_misc:rs(QueueName)])
+ end || Key <- Keys],
+ ok.
+
+args_policy_lookup(Name, Resolve, Q) when ?is_amqqueue(Q) ->
+ Args = amqqueue:get_arguments(Q),
+ AName = <<"x-", Name/binary>>,
+ case {rabbit_policy:get(Name, Q), rabbit_misc:table_lookup(Args, AName)} of
+ {undefined, undefined} -> undefined;
+ {undefined, {_Type, Val}} -> Val;
+ {Val, undefined} -> Val;
+ {PolVal, {_Type, ArgVal}} -> Resolve(PolVal, ArgVal)
+ end.
+
+%% TODO escape hack
+qname_to_internal_name(#resource{virtual_host = <<"/">>, name = Name}) ->
+ erlang:binary_to_atom(<<"%2F_", Name/binary>>, utf8);
+qname_to_internal_name(#resource{virtual_host = VHost, name = Name}) ->
+ erlang:binary_to_atom(<<VHost/binary, "_", Name/binary>>, utf8).
+
+check_auto_delete(Q) when ?amqqueue_is_auto_delete(Q) ->
+ Name = amqqueue:get_name(Q),
+ rabbit_misc:protocol_error(
+ precondition_failed,
+ "invalid property 'auto-delete' for ~s",
+ [rabbit_misc:rs(Name)]);
+check_auto_delete(_) ->
+ ok.
+
+check_exclusive(Q) when ?amqqueue_exclusive_owner_is(Q, none) ->
+ ok;
+check_exclusive(Q) when ?is_amqqueue(Q) ->
+ Name = amqqueue:get_name(Q),
+ rabbit_misc:protocol_error(
+ precondition_failed,
+ "invalid property 'exclusive-owner' for ~s",
+ [rabbit_misc:rs(Name)]).
+
+check_non_durable(Q) when ?amqqueue_is_durable(Q) ->
+ ok;
+check_non_durable(Q) when not ?amqqueue_is_durable(Q) ->
+ Name = amqqueue:get_name(Q),
+ rabbit_misc:protocol_error(
+ precondition_failed,
+ "invalid property 'non-durable' for ~s",
+ [rabbit_misc:rs(Name)]).
diff --git a/src/rabbit_quorum_queue.erl b/src/rabbit_quorum_queue.erl
index 4061260f4f..eb02333e43 100644
--- a/src/rabbit_quorum_queue.erl
+++ b/src/rabbit_quorum_queue.erl
@@ -7,13 +7,18 @@
-module(rabbit_quorum_queue).
--export([init_state/2, handle_event/2]).
--export([declare/1, recover/1, stop/1, delete/4, delete_immediately/2]).
--export([info/1, info/2, stat/1, stat/2, infos/1]).
--export([ack/3, reject/4, basic_get/4, basic_consume/10, basic_cancel/4]).
+-behaviour(rabbit_queue_type).
+
+-export([init/1,
+ close/1,
+ update/2,
+ handle_event/2]).
+-export([is_recoverable/1, recover/2, stop/1, delete/4, delete_immediately/2]).
+-export([state_info/1, info/2, stat/1, infos/1]).
+-export([settle/4, dequeue/4, consume/3, cancel/5]).
-export([credit/4]).
-export([purge/1]).
--export([stateless_deliver/2, deliver/3]).
+-export([stateless_deliver/2, deliver/3, deliver/2]).
-export([dead_letter_publish/4]).
-export([queue_name/1]).
-export([cluster_state/1, status/2]).
@@ -27,7 +32,8 @@
-export([add_member/4]).
-export([delete_member/3]).
-export([requeue/3]).
--export([policy_changed/2]).
+-export([policy_changed/1]).
+-export([format_ra_event/3]).
-export([cleanup_data_dir/0]).
-export([shrink_all/1,
grow/4]).
@@ -43,6 +49,12 @@
]).
-export([reclaim_memory/2]).
+-export([is_enabled/0,
+ declare/2]).
+
+-import(rabbit_queue_type_util, [args_policy_lookup/3,
+ qname_to_internal_name/1]).
+
-include_lib("stdlib/include/qlc.hrl").
-include("rabbit.hrl").
-include("amqqueue.hrl").
@@ -73,17 +85,22 @@
-define(DELETE_TIMEOUT, 5000).
-define(ADD_MEMBER_TIMEOUT, 5000).
+%%----------- rabbit_queue_type ---------------------------------------------
+
+-spec is_enabled() -> boolean().
+is_enabled() ->
+ rabbit_feature_flags:is_enabled(quorum_queue).
+
%%----------------------------------------------------------------------------
--spec init_state(amqqueue:ra_server_id(), rabbit_amqqueue:name()) ->
- rabbit_fifo_client:state().
-init_state({Name, _}, QName = #resource{}) ->
+-spec init(amqqueue:amqqueue()) -> rabbit_fifo_client:state().
+init(Q) when ?is_amqqueue(Q) ->
{ok, SoftLimit} = application:get_env(rabbit, quorum_commands_soft_limit),
%% This lookup could potentially return an {error, not_found}, but we do not
%% know what to do if the queue has `disappeared`. Let it crash.
- {ok, Q} = rabbit_amqqueue:lookup(QName),
- Leader = amqqueue:get_pid(Q),
- Nodes = rabbit_amqqueue:get_quorum_nodes(Q),
+ {Name, _LeaderNode} = Leader = amqqueue:get_pid(Q),
+ Nodes = get_nodes(Q),
+ QName = amqqueue:get_name(Q),
%% Ensure the leader is listed first
Servers0 = [{Name, N} || N <- Nodes],
Servers = [Leader | lists:delete(Leader, Servers0)],
@@ -91,19 +108,26 @@ init_state({Name, _}, QName = #resource{}) ->
fun() -> credit_flow:block(Name) end,
fun() -> credit_flow:unblock(Name), ok end).
--spec handle_event({'ra_event', amqqueue:ra_server_id(), any()},
+-spec close(rabbit_fifo_client:state()) -> ok.
+close(_State) ->
+ ok.
+
+-spec update(amqqueue:amqqueue(), rabbit_fifo_client:state()) ->
+ rabbit_fifo_client:state().
+update(Q, State) when ?amqqueue_is_quorum(Q) ->
+ %% QQ state maintains it's own updates
+ State.
+
+-spec handle_event({amqqueue:ra_server_id(), any()},
rabbit_fifo_client:state()) ->
- {internal, Correlators :: [term()], rabbit_fifo_client:actions(),
- rabbit_fifo_client:state()} |
- {rabbit_fifo:client_msg(), rabbit_fifo_client:state()} |
+ {ok, rabbit_fifo_client:state(), rabbit_queue_type:actions()} |
eol.
-handle_event({ra_event, From, Evt}, QState) ->
+handle_event({From, Evt}, QState) ->
rabbit_fifo_client:handle_ra_event(From, Evt, QState).
--spec declare(amqqueue:amqqueue()) ->
+-spec declare(amqqueue:amqqueue(), node()) ->
{new | existing, amqqueue:amqqueue()} | rabbit_types:channel_exit().
-
-declare(Q) when ?amqqueue_is_quorum(Q) ->
+declare(Q, _Node) when ?amqqueue_is_quorum(Q) ->
QName = amqqueue:get_name(Q),
Durable = amqqueue:is_durable(Q),
AutoDelete = amqqueue:is_auto_delete(Q),
@@ -111,11 +135,11 @@ declare(Q) when ?amqqueue_is_quorum(Q) ->
Opts = amqqueue:get_options(Q),
ActingUser = maps:get(user, Opts, ?UNKNOWN_USER),
check_invalid_arguments(QName, Arguments),
- check_auto_delete(Q),
- check_exclusive(Q),
- check_non_durable(Q),
+ rabbit_queue_type_util:check_auto_delete(Q),
+ rabbit_queue_type_util:check_exclusive(Q),
+ rabbit_queue_type_util:check_non_durable(Q),
QuorumSize = get_default_quorum_initial_group_size(Arguments),
- RaName = qname_to_rname(QName),
+ RaName = qname_to_internal_name(QName),
Id = {RaName, node()},
Nodes = select_quorum_nodes(QuorumSize, rabbit_mnesia:cluster_nodes(all)),
NewQ0 = amqqueue:set_pid(Q, Id),
@@ -250,7 +274,8 @@ all_replica_states() ->
-spec list_with_minimum_quorum() -> [amqqueue:amqqueue()].
list_with_minimum_quorum() ->
- filter_quorum_critical(rabbit_amqqueue:list_local_quorum_queues()).
+ filter_quorum_critical(
+ rabbit_amqqueue:list_local_quorum_queues()).
-spec list_with_minimum_quorum_for_cli() -> [#{binary() => term()}].
list_with_minimum_quorum_for_cli() ->
@@ -436,51 +461,64 @@ reductions(Name) ->
0
end.
--spec recover([amqqueue:amqqueue()]) -> [amqqueue:amqqueue()].
+is_recoverable(Q) ->
+ Node = node(),
+ Nodes = get_nodes(Q),
+ lists:member(Node, Nodes).
-recover(Queues) ->
- [begin
+-spec recover(binary(), [amqqueue:amqqueue()]) ->
+ {[amqqueue:amqqueue()], [amqqueue:amqqueue()]}.
+recover(_Vhost, Queues) ->
+ lists:foldl(
+ fun (Q0, {R0, F0}) ->
{Name, _} = amqqueue:get_pid(Q0),
- case ra:restart_server({Name, node()}) of
- ok ->
- % queue was restarted, good
- ok;
- {error, Err1}
- when Err1 == not_started orelse
- Err1 == name_not_registered ->
- % queue was never started on this node
- % so needs to be started from scratch.
- TickTimeout = application:get_env(rabbit, quorum_tick_interval,
- ?TICK_TIMEOUT),
- Conf = make_ra_conf(Q0, {Name, node()}, TickTimeout),
- case ra:start_server(Conf) of
- ok ->
- ok;
- Err2 ->
- rabbit_log:warning("recover: quorum queue ~w could not"
- " be started ~w", [Name, Err2]),
- ok
- end;
- {error, {already_started, _}} ->
- %% this is fine and can happen if a vhost crashes and performs
- %% recovery whilst the ra application and servers are still
- %% running
- ok;
- Err ->
- %% catch all clause to avoid causing the vhost not to start
- rabbit_log:warning("recover: quorum queue ~w could not be "
- "restarted ~w", [Name, Err]),
- ok
- end,
+ QName = amqqueue:get_name(Q0),
+ Nodes = get_nodes(Q0),
+ Formatter = {?MODULE, format_ra_event, [QName]},
+ Res = case ra:restart_server({Name, node()},
+ #{ra_event_formatter => Formatter}) of
+ ok ->
+ % queue was restarted, good
+ ok;
+ {error, Err1}
+ when Err1 == not_started orelse
+ Err1 == name_not_registered ->
+ % queue was never started on this node
+ % so needs to be started from scratch.
+ Machine = ra_machine(Q0),
+ RaNodes = [{Name, Node} || Node <- Nodes],
+ case ra:start_server(Name, {Name, node()}, Machine, RaNodes) of
+ ok -> ok;
+ Err2 ->
+ rabbit_log:warning("recover: quorum queue ~w could not"
+ " be started ~w", [Name, Err2]),
+ fail
+ end;
+ {error, {already_started, _}} ->
+ %% this is fine and can happen if a vhost crashes and performs
+ %% recovery whilst the ra application and servers are still
+ %% running
+ ok;
+ Err ->
+ %% catch all clause to avoid causing the vhost not to start
+ rabbit_log:warning("recover: quorum queue ~w could not be "
+ "restarted ~w", [Name, Err]),
+ fail
+ end,
%% we have to ensure the quorum queue is
- %% present in the rabbit_queue table and not just in rabbit_durable_queue
+ %% present in the rabbit_queue table and not just in
+ %% rabbit_durable_queue
%% So many code paths are dependent on this.
{ok, Q} = rabbit_amqqueue:ensure_rabbit_queue_record_is_initialized(Q0),
- Q
- end || Q0 <- Queues].
-
--spec stop(rabbit_types:vhost()) -> 'ok'.
+ case Res of
+ ok ->
+ {[Q | R0], F0};
+ fail ->
+ {R0, [Q | F0]}
+ end
+ end, {[], []}, Queues).
+-spec stop(rabbit_types:vhost()) -> ok.
stop(VHost) ->
_ = [begin
Pid = amqqueue:get_pid(Q),
@@ -492,7 +530,6 @@ stop(VHost) ->
boolean(), boolean(),
rabbit_types:username()) ->
{ok, QLen :: non_neg_integer()}.
-
delete(Q, true, _IfEmpty, _ActingUser) when ?amqqueue_is_quorum(Q) ->
rabbit_misc:protocol_error(
not_implemented,
@@ -503,8 +540,7 @@ delete(Q, _IfUnused, true, _ActingUser) when ?amqqueue_is_quorum(Q) ->
not_implemented,
"cannot delete ~s. queue.delete operations with if-empty flag set are not supported by quorum queues",
[rabbit_misc:rs(amqqueue:get_name(Q))]);
-delete(Q,
- _IfUnused, _IfEmpty, ActingUser) when ?amqqueue_is_quorum(Q) ->
+delete(Q, _IfUnused, _IfEmpty, ActingUser) when ?amqqueue_is_quorum(Q) ->
{Name, _} = amqqueue:get_pid(Q),
QName = amqqueue:get_name(Q),
QNodes = get_nodes(Q),
@@ -547,7 +583,6 @@ delete(Q,
end
end.
-
force_delete_queue(Servers) ->
[begin
case catch(ra:force_delete_server(S)) of
@@ -573,32 +608,22 @@ delete_immediately(Resource, {_Name, _} = QPid) ->
rabbit_core_metrics:queue_deleted(Resource),
ok.
--spec ack(rabbit_types:ctag(), [msg_id()], rabbit_fifo_client:state()) ->
- {'ok', rabbit_fifo_client:state()}.
-
-ack(CTag, MsgIds, QState) ->
- rabbit_fifo_client:settle(quorum_ctag(CTag), MsgIds, QState).
-
--spec reject(Confirm :: boolean(), rabbit_types:ctag(), [msg_id()], rabbit_fifo_client:state()) ->
- {'ok', rabbit_fifo_client:state()}.
-
-reject(true, CTag, MsgIds, QState) ->
+settle(complete, CTag, MsgIds, QState) ->
+ rabbit_fifo_client:settle(quorum_ctag(CTag), MsgIds, QState);
+settle(requeue, CTag, MsgIds, QState) ->
rabbit_fifo_client:return(quorum_ctag(CTag), MsgIds, QState);
-reject(false, CTag, MsgIds, QState) ->
+settle(discard, CTag, MsgIds, QState) ->
rabbit_fifo_client:discard(quorum_ctag(CTag), MsgIds, QState).
credit(CTag, Credit, Drain, QState) ->
rabbit_fifo_client:credit(quorum_ctag(CTag), Credit, Drain, QState).
--spec basic_get(amqqueue:amqqueue(), NoAck :: boolean(), rabbit_types:ctag(),
- rabbit_fifo_client:state()) ->
- {'ok', 'empty', rabbit_fifo_client:state()} |
- {'ok', QLen :: non_neg_integer(), qmsg(), rabbit_fifo_client:state()} |
- {error, timeout | term()}.
-
-basic_get(Q, NoAck, CTag0, QState0) when ?amqqueue_is_quorum(Q) ->
- QName = amqqueue:get_name(Q),
- Id = amqqueue:get_pid(Q),
+-spec dequeue(NoAck :: boolean(), pid(),
+ rabbit_types:ctag(), rabbit_fifo_client:state()) ->
+ {empty, rabbit_fifo_client:state()} |
+ {ok, QLen :: non_neg_integer(), qmsg(), rabbit_fifo_client:state()} |
+ {error, term()}.
+dequeue(NoAck, _LimiterPid, CTag0, QState0) ->
CTag = quorum_ctag(CTag0),
Settlement = case NoAck of
true ->
@@ -606,39 +631,25 @@ basic_get(Q, NoAck, CTag0, QState0) when ?amqqueue_is_quorum(Q) ->
false ->
unsettled
end,
- case rabbit_fifo_client:dequeue(CTag, Settlement, QState0) of
- {ok, empty, QState} ->
- {ok, empty, QState};
- {ok, {{MsgId, {MsgHeader, Msg0}}, MsgsReady}, QState} ->
- Count = case MsgHeader of
- #{delivery_count := C} -> C;
- _ -> 0
- end,
- IsDelivered = Count > 0,
- Msg = rabbit_basic:add_header(<<"x-delivery-count">>, long, Count, Msg0),
- {ok, MsgsReady, {QName, Id, MsgId, IsDelivered, Msg}, QState};
- {error, unsupported} ->
- rabbit_misc:protocol_error(
- resource_locked,
- "cannot obtain access to locked ~s. basic.get operations are not supported by quorum queues with single active consumer",
- [rabbit_misc:rs(QName)]);
- {error, _} = Err ->
- Err;
- {timeout, _} ->
- {error, timeout}
- end.
+ rabbit_fifo_client:dequeue(CTag, Settlement, QState0).
--spec basic_consume(amqqueue:amqqueue(), NoAck :: boolean(), ChPid :: pid(),
- ConsumerPrefetchCount :: non_neg_integer(),
- rabbit_types:ctag(), ExclusiveConsume :: boolean(),
- Args :: rabbit_framing:amqp_table(), ActingUser :: binary(),
- any(), rabbit_fifo_client:state()) ->
- {'ok', rabbit_fifo_client:state()} |
- {error, timeout | term()}.
-
-basic_consume(Q, NoAck, ChPid,
- ConsumerPrefetchCount, ConsumerTag0, ExclusiveConsume, Args,
- ActingUser, OkMsg, QState0) when ?amqqueue_is_quorum(Q) ->
+-spec consume(amqqueue:amqqueue(),
+ rabbit_queue_type:consume_spec(),
+ rabbit_fifo_client:state()) ->
+ {ok, rabbit_fifo_client:state(), rabbit_queue_type:actions()} |
+ {error, global_qos_not_supported_for_queue_type}.
+consume(Q, #{limiter_active := true}, _State)
+ when ?amqqueue_is_quorum(Q) ->
+ {error, global_qos_not_supported_for_queue_type};
+consume(Q, Spec, QState0) when ?amqqueue_is_quorum(Q) ->
+ #{no_ack := NoAck,
+ channel_pid := ChPid,
+ prefetch_count := ConsumerPrefetchCount,
+ consumer_tag := ConsumerTag0,
+ exclusive_consume := ExclusiveConsume,
+ args := Args,
+ ok_msg := OkMsg,
+ acting_user := ActingUser} = Spec,
%% TODO: validate consumer arguments
%% currently quorum queues do not support any arguments
QName = amqqueue:get_name(Q),
@@ -681,19 +692,19 @@ basic_consume(Q, NoAck, ChPid,
emit_consumer_created(ChPid, ConsumerTag, ExclusiveConsume,
AckRequired, QName, Prefetch,
Args, none, ActingUser),
- {ok, QState};
+ {ok, QState, []};
{error, Error} ->
Error;
{timeout, _} ->
{error, timeout}
end.
--spec basic_cancel(rabbit_types:ctag(), ChPid :: pid(), any(), rabbit_fifo_client:state()) ->
- {'ok', rabbit_fifo_client:state()}.
+% -spec basic_cancel(rabbit_types:ctag(), ChPid :: pid(), any(), rabbit_fifo_client:state()) ->
+% {'ok', rabbit_fifo_client:state()}.
-basic_cancel(ConsumerTag, ChPid, OkMsg, QState0) ->
- maybe_send_reply(ChPid, OkMsg),
- rabbit_fifo_client:cancel_checkout(quorum_ctag(ConsumerTag), QState0).
+cancel(_Q, ConsumerTag, OkMsg, _ActingUser, State) ->
+ maybe_send_reply(self(), OkMsg),
+ rabbit_fifo_client:cancel_checkout(quorum_ctag(ConsumerTag), State).
emit_consumer_created(ChPid, CTag, Exclusive, AckRequired, QName, PrefetchCount, Args, Ref, ActingUser) ->
rabbit_event:notify(consumer_created,
@@ -746,14 +757,25 @@ deliver(true, Delivery, QState0) ->
{ok, State}
end.
--spec info(amqqueue:amqqueue()) -> rabbit_types:infos().
+deliver(QSs, #delivery{confirm = Confirm} = Delivery) ->
+ lists:foldl(
+ fun({Q, stateless}, {Qs, Actions}) ->
+ QRef = amqqueue:get_pid(Q),
+ ok = rabbit_fifo_client:untracked_enqueue(
+ [QRef], Delivery#delivery.message),
+ {Qs, Actions};
+ ({Q, S0}, {Qs, Actions}) ->
+ {_, S} = deliver(Confirm, Delivery, S0),
+ {[{Q, S} | Qs], Actions}
+ end, {[], []}, QSs).
+
+
+state_info(S) ->
+ #{pending_raft_commands => rabbit_fifo_client:pending_size(S)}.
-info(Q) ->
- info(Q, [name, durable, auto_delete, arguments, pid, state, messages,
- messages_ready, messages_unacknowledged]).
--spec infos(rabbit_types:r('queue')) -> rabbit_types:infos().
+-spec infos(rabbit_types:r('queue')) -> rabbit_types:infos().
infos(QName) ->
infos(QName, ?STATISTICS_KEYS).
@@ -765,7 +787,6 @@ infos(QName, Keys) ->
[]
end.
--spec info(amqqueue:amqqueue(), rabbit_types:info_keys()) -> rabbit_types:infos().
info(Q, Items) ->
lists:foldr(fun(totals, Acc) ->
@@ -776,8 +797,8 @@ info(Q, Items) ->
[{Item, i(Item, Q)} | Acc]
end, [], Items).
--spec stat(amqqueue:amqqueue()) -> {'ok', non_neg_integer(), non_neg_integer()}.
-
+-spec stat(amqqueue:amqqueue()) ->
+ {'ok', non_neg_integer(), non_neg_integer()}.
stat(Q) when ?is_amqqueue(Q) ->
%% same short default timeout as in rabbit_fifo_client:stat/1
stat(Q, 250).
@@ -798,7 +819,10 @@ stat(Q, Timeout) when ?is_amqqueue(Q) ->
{ok, 0, 0}
end.
-purge(Node) ->
+-spec purge(amqqueue:amqqueue()) ->
+ {ok, non_neg_integer()}.
+purge(Q) when ?is_amqqueue(Q) ->
+ Node = amqqueue:get_pid(Q),
rabbit_fifo_client:purge(Node).
requeue(ConsumerTag, MsgIds, QState) ->
@@ -811,9 +835,11 @@ cleanup_data_dir() ->
end
|| Q <- rabbit_amqqueue:list_by_type(?MODULE),
lists:member(node(), get_nodes(Q))],
+ NoQQClusters = rabbit_ra_registry:list_not_quorum_clusters(),
Registered = ra_directory:list_registered(),
+ Running = Names ++ NoQQClusters,
_ = [maybe_delete_data_dir(UId) || {Name, UId} <- Registered,
- not lists:member(Name, Names)],
+ not lists:member(Name, Running)],
ok.
maybe_delete_data_dir(UId) ->
@@ -827,9 +853,10 @@ maybe_delete_data_dir(UId) ->
ok
end.
-policy_changed(QName, Server) ->
- {ok, Q} = rabbit_amqqueue:lookup(QName),
- rabbit_fifo_client:update_machine_state(Server, ra_machine_config(Q)).
+policy_changed(Q) ->
+ QPid = amqqueue:get_pid(Q),
+ _ = rabbit_fifo_client:update_machine_state(QPid, ra_machine_config(Q)),
+ ok.
-spec cluster_state(Name :: atom()) -> 'down' | 'recovering' | 'running'.
@@ -848,7 +875,7 @@ cluster_state(Name) ->
status(Vhost, QueueName) ->
%% Handle not found queues
QName = #resource{virtual_host = Vhost, name = QueueName, kind = queue},
- RName = qname_to_rname(QName),
+ RName = qname_to_internal_name(QName),
case rabbit_amqqueue:lookup(QName) of
{ok, Q} when ?amqqueue_is_classic(Q) ->
{error, classic_queue_not_supported};
@@ -1147,16 +1174,6 @@ init_dlx(DLX, Q) when ?is_amqqueue(Q) ->
res_arg(_PolVal, ArgVal) -> ArgVal.
-args_policy_lookup(Name, Resolve, Q) when ?is_amqqueue(Q) ->
- Args = amqqueue:get_arguments(Q),
- AName = <<"x-", Name/binary>>,
- case {rabbit_policy:get(Name, Q), rabbit_misc:table_lookup(Args, AName)} of
- {undefined, undefined} -> undefined;
- {undefined, {_Type, Val}} -> Val;
- {Val, undefined} -> Val;
- {PolVal, {_Type, ArgVal}} -> Resolve(PolVal, ArgVal)
- end.
-
dead_letter_publish(undefined, _, _, _) ->
ok;
dead_letter_publish(X, RK, QName, ReasonMsgs) ->
@@ -1168,12 +1185,6 @@ dead_letter_publish(X, RK, QName, ReasonMsgs) ->
ok
end.
-%% TODO escape hack
-qname_to_rname(#resource{virtual_host = <<"/">>, name = Name}) ->
- erlang:binary_to_atom(<<"%2F_", Name/binary>>, utf8);
-qname_to_rname(#resource{virtual_host = VHost, name = Name}) ->
- erlang:binary_to_atom(<<VHost/binary, "_", Name/binary>>, utf8).
-
find_quorum_queues(VHost) ->
Node = node(),
mnesia:async_dirty(
@@ -1413,52 +1424,7 @@ maybe_send_reply(ChPid, Msg) -> ok = rabbit_channel:send_command(ChPid, Msg).
check_invalid_arguments(QueueName, Args) ->
Keys = [<<"x-message-ttl">>,
<<"x-max-priority">>, <<"x-queue-mode">>],
- [case rabbit_misc:table_lookup(Args, Key) of
- undefined -> ok;
- _TypeVal -> rabbit_misc:protocol_error(
- precondition_failed,
- "invalid arg '~s' for ~s",
- [Key, rabbit_misc:rs(QueueName)])
- end || Key <- Keys],
-
- case rabbit_misc:table_lookup(Args, <<"x-overflow">>) of
- undefined -> ok;
- {_, <<"reject-publish-dlx">>} ->
- rabbit_misc:protocol_error(
- precondition_failed,
- "invalid arg 'x-overflow' with value 'reject-publish-dlx' for ~s",
- [rabbit_misc:rs(QueueName)]);
- _ ->
- ok
- end,
- ok.
-
-check_auto_delete(Q) when ?amqqueue_is_auto_delete(Q) ->
- Name = amqqueue:get_name(Q),
- rabbit_misc:protocol_error(
- precondition_failed,
- "invalid property 'auto-delete' for ~s",
- [rabbit_misc:rs(Name)]);
-check_auto_delete(_) ->
- ok.
-
-check_exclusive(Q) when ?amqqueue_exclusive_owner_is(Q, none) ->
- ok;
-check_exclusive(Q) when ?is_amqqueue(Q) ->
- Name = amqqueue:get_name(Q),
- rabbit_misc:protocol_error(
- precondition_failed,
- "invalid property 'exclusive-owner' for ~s",
- [rabbit_misc:rs(Name)]).
-
-check_non_durable(Q) when ?amqqueue_is_durable(Q) ->
- ok;
-check_non_durable(Q) when not ?amqqueue_is_durable(Q) ->
- Name = amqqueue:get_name(Q),
- rabbit_misc:protocol_error(
- precondition_failed,
- "invalid property 'non-durable' for ~s",
- [rabbit_misc:rs(Name)]).
+ rabbit_queue_type_util:check_invalid_arguments(QueueName, Args, Keys).
queue_name(RaFifoState) ->
rabbit_fifo_client:cluster_name(RaFifoState).
@@ -1492,12 +1458,16 @@ members(Q) when ?amqqueue_is_quorum(Q) ->
Nodes = lists:delete(LeaderNode, get_nodes(Q)),
[{RaName, N} || N <- [LeaderNode | Nodes]].
+format_ra_event(ServerId, Evt, QRef) ->
+ {'$gen_cast', {queue_event, QRef, {ServerId, Evt}}}.
+
make_ra_conf(Q, ServerId, TickTimeout) ->
QName = amqqueue:get_name(Q),
RaMachine = ra_machine(Q),
[{ClusterName, _} | _] = Members = members(Q),
UId = ra:new_uid(ra_lib:to_binary(ClusterName)),
FName = rabbit_misc:rs(QName),
+ Formatter = {?MODULE, format_ra_event, [QName]},
#{cluster_name => ClusterName,
id => ServerId,
uid => UId,
@@ -1506,7 +1476,8 @@ make_ra_conf(Q, ServerId, TickTimeout) ->
initial_members => Members,
log_init_args => #{uid => UId},
tick_timeout => TickTimeout,
- machine => RaMachine}.
+ machine => RaMachine,
+ ra_event_formatter => Formatter}.
get_nodes(Q) when ?is_amqqueue(Q) ->
#{nodes := Nodes} = amqqueue:get_type_state(Q),
diff --git a/src/rabbit_ra_registry.erl b/src/rabbit_ra_registry.erl
new file mode 100644
index 0000000000..b02d89eda5
--- /dev/null
+++ b/src/rabbit_ra_registry.erl
@@ -0,0 +1,25 @@
+%% 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-2020 VMware, Inc. or its affiliates. All rights reserved.
+%%
+
+-module(rabbit_ra_registry).
+
+-export([list_not_quorum_clusters/0]).
+
+%% Not all ra clusters are quorum queues. We need to keep a list of these so we don't
+%% take them into account in operations such as memory calculation and data cleanup.
+%% Hardcoded atm
+list_not_quorum_clusters() ->
+ [rabbit_stream_coordinator].
diff --git a/src/rabbit_stream_coordinator.erl b/src/rabbit_stream_coordinator.erl
new file mode 100644
index 0000000000..472cf5d70f
--- /dev/null
+++ b/src/rabbit_stream_coordinator.erl
@@ -0,0 +1,906 @@
+%% 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.
+%%
+%% Copyright (c) 2012-2020 VMware, Inc. or its affiliates. All rights reserved.
+%%
+-module(rabbit_stream_coordinator).
+
+-behaviour(ra_machine).
+
+-export([start/0]).
+-export([format_ra_event/2]).
+
+-export([init/1,
+ apply/3,
+ state_enter/2,
+ init_aux/1,
+ handle_aux/6,
+ tick/2]).
+
+-export([recover/0,
+ start_cluster/1,
+ delete_cluster/2,
+ add_replica/2,
+ delete_replica/2]).
+
+-export([policy_changed/1]).
+
+-export([phase_repair_mnesia/2,
+ phase_start_cluster/1,
+ phase_delete_cluster/2,
+ phase_check_quorum/1,
+ phase_start_new_leader/1,
+ phase_stop_replicas/1,
+ phase_start_replica/3,
+ phase_delete_replica/2]).
+
+-export([log_overview/1]).
+
+-define(STREAM_COORDINATOR_STARTUP, {stream_coordinator_startup, self()}).
+-define(TICK_TIMEOUT, 60000).
+-define(RESTART_TIMEOUT, 1000).
+-define(PHASE_RETRY_TIMEOUT, 10000).
+-define(CMD_TIMEOUT, 30000).
+
+-record(?MODULE, {streams, monitors}).
+
+start() ->
+ Nodes = rabbit_mnesia:cluster_nodes(all),
+ ServerId = {?MODULE, node()},
+ case ra:restart_server(ServerId) of
+ {error, Reason} when Reason == not_started orelse
+ Reason == name_not_registered ->
+ case ra:start_server(make_ra_conf(node(), Nodes)) of
+ ok ->
+ global:set_lock(?STREAM_COORDINATOR_STARTUP),
+ case find_members(Nodes) of
+ [] ->
+ %% We're the first (and maybe only) one
+ ra:trigger_election(ServerId);
+ Members ->
+ %% What to do if we get a timeout?
+ {ok, _, _} = ra:add_member(Members, ServerId, 30000)
+ end,
+ global:del_lock(?STREAM_COORDINATOR_STARTUP),
+ _ = ra:members(ServerId),
+ ok;
+ Error ->
+ exit(Error)
+ end;
+ ok ->
+ ok;
+ Error ->
+ exit(Error)
+ end.
+
+find_members([]) ->
+ [];
+find_members([Node | Nodes]) ->
+ case ra:members({?MODULE, Node}) of
+ {_, Members, _} ->
+ Members;
+ {error, noproc} ->
+ find_members(Nodes);
+ {timeout, _} ->
+ %% not sure what to do here
+ find_members(Nodes)
+ end.
+
+recover() ->
+ ra:restart_server({?MODULE, node()}).
+
+start_cluster(Q) ->
+ process_command({start_cluster, #{queue => Q}}).
+
+delete_cluster(StreamId, ActingUser) ->
+ process_command({delete_cluster, #{stream_id => StreamId, acting_user => ActingUser}}).
+
+add_replica(StreamId, Node) ->
+ process_command({start_replica, #{stream_id => StreamId, node => Node,
+ retries => 1}}).
+
+policy_changed(StreamId) ->
+ process_command({policy_changed, #{stream_id => StreamId}}).
+
+delete_replica(StreamId, Node) ->
+ process_command({delete_replica, #{stream_id => StreamId, node => Node}}).
+
+process_command(Cmd) ->
+ Servers = ensure_coordinator_started(),
+ process_command(Servers, Cmd).
+
+process_command([], _Cmd) ->
+ {error, coordinator_unavailable};
+process_command([Server | Servers], {CmdName, _} = Cmd) ->
+ case ra:process_command(Server, Cmd, ?CMD_TIMEOUT) of
+ {timeout, _} ->
+ rabbit_log:warning("Coordinator timeout on server ~p when processing command ~p",
+ [Server, CmdName]),
+ process_command(Servers, Cmd);
+ {error, noproc} ->
+ process_command(Servers, Cmd);
+ Reply ->
+ Reply
+ end.
+
+ensure_coordinator_started() ->
+ Local = {?MODULE, node()},
+ AllNodes = all_nodes(),
+ case ra:restart_server(Local) of
+ {error, Reason} when Reason == not_started orelse
+ Reason == name_not_registered ->
+ OtherNodes = all_nodes() -- [Local],
+ %% We can't use find_members/0 here as a process that timeouts means the cluster is up
+ case lists:filter(fun(N) -> global:whereis_name(N) =/= undefined end, OtherNodes) of
+ [] ->
+ start_coordinator_cluster();
+ _ ->
+ OtherNodes
+ end;
+ ok ->
+ AllNodes;
+ {error, {already_started, _}} ->
+ AllNodes;
+ _ ->
+ AllNodes
+ end.
+
+start_coordinator_cluster() ->
+ Nodes = rabbit_mnesia:cluster_nodes(running),
+ case ra:start_cluster([make_ra_conf(Node, Nodes) || Node <- Nodes]) of
+ {ok, Started, _} ->
+ Started;
+ {error, cluster_not_formed} ->
+ rabbit_log:warning("Stream coordinator cluster not formed", []),
+ []
+ end.
+
+all_nodes() ->
+ Nodes = rabbit_mnesia:cluster_nodes(running) -- [node()],
+ [{?MODULE, Node} || Node <- [node() | Nodes]].
+
+init(_Conf) ->
+ #?MODULE{streams = #{},
+ monitors = #{}}.
+
+apply(#{from := From}, {policy_changed, #{stream_id := StreamId}} = Cmd,
+ #?MODULE{streams = Streams0} = State) ->
+ case maps:get(StreamId, Streams0, undefined) of
+ undefined ->
+ {State, ok, []};
+ #{conf := Conf,
+ state := running} ->
+ case rabbit_stream_queue:update_stream_conf(Conf) of
+ Conf ->
+ %% No changes, ensure we only trigger an election if it's a must
+ {State, ok, []};
+ _ ->
+ {State, ok, [{mod_call, osiris_writer, stop, [Conf]}]}
+ end;
+ SState0 ->
+ Streams = maps:put(StreamId, add_pending_cmd(From, Cmd, SState0), Streams0),
+ {State#?MODULE{streams = Streams}, '$ra_no_reply', []}
+
+ end;
+apply(#{from := From}, {start_cluster, #{queue := Q}}, #?MODULE{streams = Streams} = State) ->
+ #{name := StreamId} = Conf = amqqueue:get_type_state(Q),
+ case maps:is_key(StreamId, Streams) of
+ true ->
+ {State, '$ra_no_reply', wrap_reply(From, {error, already_started})};
+ false ->
+ Phase = phase_start_cluster,
+ PhaseArgs = [Q],
+ SState = #{state => start_cluster,
+ phase => Phase,
+ phase_args => PhaseArgs,
+ conf => Conf,
+ reply_to => From,
+ pending_cmds => [],
+ pending_replicas => []},
+ rabbit_log:debug("rabbit_stream_coordinator: ~p entering phase_start_cluster", [StreamId]),
+ {State#?MODULE{streams = maps:put(StreamId, SState, Streams)}, '$ra_no_reply',
+ [{aux, {phase, StreamId, Phase, PhaseArgs}}]}
+ end;
+apply(_Meta, {start_cluster_reply, Q}, #?MODULE{streams = Streams,
+ monitors = Monitors0} = State) ->
+ #{name := StreamId,
+ leader_pid := LeaderPid,
+ replica_pids := ReplicaPids} = Conf = amqqueue:get_type_state(Q),
+ SState0 = maps:get(StreamId, Streams),
+ Phase = phase_repair_mnesia,
+ PhaseArgs = [new, Q],
+ SState = SState0#{conf => Conf,
+ phase => Phase,
+ phase_args => PhaseArgs},
+ Monitors = lists:foldl(fun(Pid, M) ->
+ maps:put(Pid, {StreamId, follower}, M)
+ end, maps:put(LeaderPid, {StreamId, leader}, Monitors0), ReplicaPids),
+ MonitorActions = [{monitor, process, Pid} || Pid <- ReplicaPids ++ [LeaderPid]],
+ rabbit_log:debug("rabbit_stream_coordinator: ~p entering ~p "
+ "after start_cluster_reply", [StreamId, Phase]),
+ {State#?MODULE{streams = maps:put(StreamId, SState, Streams),
+ monitors = Monitors}, ok,
+ MonitorActions ++ [{aux, {phase, StreamId, Phase, PhaseArgs}}]};
+apply(_Meta, {start_replica_failed, StreamId, Node, Retries, Reply},
+ #?MODULE{streams = Streams0} = State) ->
+ rabbit_log:debug("rabbit_stream_coordinator: ~p start replica failed", [StreamId]),
+ case maps:get(StreamId, Streams0, undefined) of
+ undefined ->
+ {State, {error, not_found}, []};
+ #{pending_replicas := Pending,
+ reply_to := From} = SState ->
+ Streams = Streams0#{StreamId => clear_stream_state(SState#{pending_replicas =>
+ add_unique(Node, Pending)})},
+ reply_and_run_pending(
+ From, StreamId, ok, Reply,
+ [{timer, {pipeline,
+ [{start_replica, #{stream_id => StreamId,
+ node => Node,
+ from => undefined,
+ retries => Retries + 1}}]},
+ ?RESTART_TIMEOUT * Retries}],
+ State#?MODULE{streams = Streams})
+ end;
+apply(_Meta, {phase_finished, StreamId, Reply}, #?MODULE{streams = Streams0} = State) ->
+ rabbit_log:debug("rabbit_stream_coordinator: ~p phase finished", [StreamId]),
+ case maps:get(StreamId, Streams0, undefined) of
+ undefined ->
+ {State, {error, not_found}, []};
+ #{reply_to := From} = SState ->
+ Streams = Streams0#{StreamId => clear_stream_state(SState)},
+ reply_and_run_pending(From, StreamId, ok, Reply, [], State#?MODULE{streams = Streams})
+ end;
+apply(#{from := From}, {start_replica, #{stream_id := StreamId, node := Node,
+ retries := Retries}} = Cmd,
+ #?MODULE{streams = Streams0} = State) ->
+ case maps:get(StreamId, Streams0, undefined) of
+ undefined ->
+ case From of
+ undefined ->
+ {State, ok, []};
+ _ ->
+ {State, '$ra_no_reply', wrap_reply(From, {error, not_found})}
+ end;
+ #{conf := Conf,
+ state := running} = SState0 ->
+ Phase = phase_start_replica,
+ PhaseArgs = [Node, Conf, Retries],
+ SState = update_stream_state(From, start_replica, Phase, PhaseArgs, SState0),
+ rabbit_log:debug("rabbit_stream_coordinator: ~p entering ~p on node ~p",
+ [StreamId, Phase, Node]),
+ {State#?MODULE{streams = Streams0#{StreamId => SState}}, '$ra_no_reply',
+ [{aux, {phase, StreamId, Phase, PhaseArgs}}]};
+ SState0 ->
+ Streams = maps:put(StreamId, add_pending_cmd(From, Cmd, SState0), Streams0),
+ {State#?MODULE{streams = Streams}, '$ra_no_reply', []}
+ end;
+apply(_Meta, {start_replica_reply, StreamId, Pid},
+ #?MODULE{streams = Streams, monitors = Monitors0} = State) ->
+ case maps:get(StreamId, Streams, undefined) of
+ undefined ->
+ {State, {error, not_found}, []};
+ #{conf := Conf0} = SState0 ->
+ #{replica_nodes := Replicas0,
+ replica_pids := ReplicaPids0} = Conf0,
+ {ReplicaPids, MaybePid} = delete_replica_pid(node(Pid), ReplicaPids0),
+ Conf = Conf0#{replica_pids => [Pid | ReplicaPids],
+ replica_nodes => add_unique(node(Pid), Replicas0)},
+ Phase = phase_repair_mnesia,
+ PhaseArgs = [update, Conf],
+ rabbit_log:debug("rabbit_stream_coordinator: ~p entering ~p after start replica", [StreamId, Phase]),
+ #{pending_replicas := Pending} = SState0 = maps:get(StreamId, Streams),
+ SState = SState0#{conf => Conf,
+ phase => Phase,
+ phase_args => PhaseArgs,
+ pending_replicas => lists:delete(node(Pid), Pending)},
+ Monitors1 = Monitors0#{Pid => {StreamId, follower}},
+ Monitors = case MaybePid of
+ [P] -> maps:remove(P, Monitors1);
+ _ -> Monitors1
+ end,
+ {State#?MODULE{streams = Streams#{StreamId => SState},
+ monitors = Monitors}, ok,
+ [{monitor, process, Pid}, {aux, {phase, StreamId, Phase, PhaseArgs}}]}
+ end;
+apply(#{from := From}, {delete_replica, #{stream_id := StreamId, node := Node}} = Cmd,
+ #?MODULE{streams = Streams0,
+ monitors = Monitors0} = State) ->
+ case maps:get(StreamId, Streams0, undefined) of
+ undefined ->
+ {State, '$ra_no_reply', wrap_reply(From, {error, not_found})};
+ #{conf := Conf0,
+ state := running,
+ pending_replicas := Pending0} = SState0 ->
+ Replicas0 = maps:get(replica_nodes, Conf0),
+ ReplicaPids0 = maps:get(replica_pids, Conf0),
+ case lists:member(Node, Replicas0) of
+ false ->
+ reply_and_run_pending(From, StreamId, '$ra_no_reply', ok, [], State);
+ true ->
+ [Pid] = lists:filter(fun(P) -> node(P) == Node end, ReplicaPids0),
+ ReplicaPids = lists:delete(Pid, ReplicaPids0),
+ Replicas = lists:delete(Node, Replicas0),
+ Pending = lists:delete(Node, Pending0),
+ Conf = Conf0#{replica_pids => ReplicaPids,
+ replica_nodes => Replicas},
+ Phase = phase_delete_replica,
+ PhaseArgs = [Node, Conf],
+ SState = update_stream_state(From, delete_replica,
+ Phase, PhaseArgs,
+ SState0#{conf => Conf0,
+ pending_replicas => Pending}),
+ Monitors = maps:remove(Pid, Monitors0),
+ rabbit_log:debug("rabbit_stream_coordinator: ~p entering ~p on node ~p", [StreamId, Phase, Node]),
+ {State#?MODULE{monitors = Monitors,
+ streams = Streams0#{StreamId => SState}},
+ '$ra_no_reply',
+ [{demonitor, process, Pid},
+ {aux, {phase, StreamId, Phase, PhaseArgs}}]}
+ end;
+ SState0 ->
+ Streams = maps:put(StreamId, add_pending_cmd(From, Cmd, SState0), Streams0),
+ {State#?MODULE{streams = Streams}, '$ra_no_reply', []}
+ end;
+apply(#{from := From}, {delete_cluster, #{stream_id := StreamId,
+ acting_user := ActingUser}} = Cmd,
+ #?MODULE{streams = Streams0, monitors = Monitors0} = State) ->
+ case maps:get(StreamId, Streams0, undefined) of
+ undefined ->
+ {State, '$ra_no_reply', wrap_reply(From, {ok, 0})};
+ #{conf := Conf,
+ state := running} = SState0 ->
+ ReplicaPids = maps:get(replica_pids, Conf),
+ LeaderPid = maps:get(leader_pid, Conf),
+ Monitors = lists:foldl(fun(Pid, M) ->
+ maps:remove(Pid, M)
+ end, Monitors0, ReplicaPids ++ [LeaderPid]),
+ Phase = phase_delete_cluster,
+ PhaseArgs = [Conf, ActingUser],
+ SState = update_stream_state(From, delete_cluster, Phase, PhaseArgs, SState0),
+ Demonitors = [{demonitor, process, Pid} || Pid <- [LeaderPid | ReplicaPids]],
+ rabbit_log:debug("rabbit_stream_coordinator: ~p entering ~p",
+ [StreamId, Phase]),
+ {State#?MODULE{monitors = Monitors,
+ streams = Streams0#{StreamId => SState}}, '$ra_no_reply',
+ Demonitors ++ [{aux, {phase, StreamId, Phase, PhaseArgs}}]};
+ SState0 ->
+ Streams = maps:put(StreamId, add_pending_cmd(From, Cmd, SState0), Streams0),
+ {State#?MODULE{streams = Streams}, '$ra_no_reply', []}
+ end;
+apply(_Meta, {delete_cluster_reply, StreamId}, #?MODULE{streams = Streams} = State0) ->
+ #{reply_to := From,
+ pending_cmds := Pending} = maps:get(StreamId, Streams),
+ State = State0#?MODULE{streams = maps:remove(StreamId, Streams)},
+ rabbit_log:debug("rabbit_stream_coordinator: ~p finished delete_cluster_reply",
+ [StreamId]),
+ Actions = [{ra, pipeline_command, [{?MODULE, node()}, Cmd]} || Cmd <- Pending],
+ {State, ok, Actions ++ wrap_reply(From, {ok, 0})};
+apply(_Meta, {down, Pid, _Reason} = Cmd, #?MODULE{streams = Streams,
+ monitors = Monitors0} = State) ->
+ case maps:get(Pid, Monitors0, undefined) of
+ {StreamId, Role} ->
+ Monitors = maps:remove(Pid, Monitors0),
+ case maps:get(StreamId, Streams, undefined) of
+ #{state := delete_cluster} ->
+ {State#?MODULE{monitors = Monitors}, ok, []};
+ undefined ->
+ {State#?MODULE{monitors = Monitors}, ok, []};
+ #{state := running,
+ conf := #{replica_pids := Pids} = Conf0,
+ pending_cmds := Pending0} = SState0 ->
+ case Role of
+ leader ->
+ rabbit_log:info("rabbit_stream_coordinator: ~p leader is down, starting election", [StreamId]),
+ Phase = phase_stop_replicas,
+ PhaseArgs = [Conf0],
+ SState = update_stream_state(undefined, leader_election, Phase, PhaseArgs, SState0),
+ Events = [{demonitor, process, P} || P <- Pids],
+ Monitors1 = lists:foldl(fun(P, M) ->
+ maps:remove(P, M)
+ end, Monitors, Pids),
+ rabbit_log:debug("rabbit_stream_coordinator: ~p entering ~p", [StreamId, Phase]),
+ {State#?MODULE{monitors = Monitors1,
+ streams = Streams#{StreamId => SState}},
+ ok, Events ++ [{aux, {phase, StreamId, Phase, PhaseArgs}}]};
+ follower ->
+ case rabbit_misc:is_process_alive(maps:get(leader_pid, Conf0)) of
+ true ->
+ Phase = phase_start_replica,
+ PhaseArgs = [node(Pid), Conf0, 1],
+ SState = update_stream_state(undefined,
+ replica_restart,
+ Phase, PhaseArgs,
+ SState0),
+ rabbit_log:debug("rabbit_stream_coordinator: ~p replica on node ~p is down, entering ~p", [StreamId, node(Pid), Phase]),
+ {State#?MODULE{monitors = Monitors,
+ streams = Streams#{StreamId => SState}},
+ ok, [{aux, {phase, StreamId, Phase, PhaseArgs}}]};
+ false ->
+ SState = SState0#{pending_cmds => Pending0 ++ [Cmd]},
+ reply_and_run_pending(undefined, StreamId, ok, ok, [], State#?MODULE{streams = Streams#{StreamId => SState}})
+ end
+ end;
+ #{pending_cmds := Pending0} = SState0 ->
+ SState = SState0#{pending_cmds => Pending0 ++ [Cmd]},
+ {State#?MODULE{streams = Streams#{StreamId => SState}}, ok, []}
+ end;
+ undefined ->
+ {State, ok, []}
+ end;
+apply(_Meta, {start_leader_election, StreamId, NewEpoch, Offsets},
+ #?MODULE{streams = Streams} = State) ->
+ #{conf := Conf0} = SState0 = maps:get(StreamId, Streams),
+ #{leader_node := Leader,
+ replica_nodes := Replicas,
+ replica_pids := ReplicaPids0} = Conf0,
+ NewLeader = find_max_offset(Offsets),
+ rabbit_log:info("rabbit_stream_coordinator: ~p starting new leader on node ~p",
+ [StreamId, NewLeader]),
+ {ReplicaPids, _} = delete_replica_pid(NewLeader, ReplicaPids0),
+ Conf = rabbit_stream_queue:update_stream_conf(
+ Conf0#{epoch => NewEpoch,
+ leader_node => NewLeader,
+ replica_nodes => lists:delete(NewLeader, Replicas ++ [Leader]),
+ replica_pids => ReplicaPids}),
+ Phase = phase_start_new_leader,
+ PhaseArgs = [Conf],
+ SState = SState0#{conf => Conf,
+ phase => Phase,
+ phase_args => PhaseArgs},
+ rabbit_log:debug("rabbit_stream_coordinator: ~p entering phase_start_new_leader",
+ [StreamId]),
+ {State#?MODULE{streams = Streams#{StreamId => SState}}, ok,
+ [{aux, {phase, StreamId, Phase, PhaseArgs}}]};
+apply(_Meta, {leader_elected, StreamId, NewLeaderPid},
+ #?MODULE{streams = Streams, monitors = Monitors0} = State) ->
+ rabbit_log:info("rabbit_stream_coordinator: ~p leader elected", [StreamId]),
+ #{conf := Conf0,
+ pending_cmds := Pending0} = SState0 = maps:get(StreamId, Streams),
+ #{leader_pid := LeaderPid,
+ replica_nodes := Replicas} = Conf0,
+ Conf = Conf0#{leader_pid => NewLeaderPid},
+ Phase = phase_repair_mnesia,
+ PhaseArgs = [update, Conf],
+ Pending = Pending0 ++ [{start_replica, #{stream_id => StreamId, node => R,
+ retries => 1, from => undefined}}
+ || R <- Replicas],
+ SState = SState0#{conf => Conf,
+ phase => Phase,
+ phase_args => PhaseArgs,
+ pending_replicas => Replicas,
+ pending_cmds => Pending},
+ Monitors = maps:put(NewLeaderPid, {StreamId, leader}, maps:remove(LeaderPid, Monitors0)),
+ rabbit_log:debug("rabbit_stream_coordinator: ~p entering ~p after "
+ "leader election", [StreamId, Phase]),
+ {State#?MODULE{streams = Streams#{StreamId => SState},
+ monitors = Monitors}, ok,
+ [{monitor, process, NewLeaderPid},
+ {aux, {phase, StreamId, Phase, PhaseArgs}}]};
+apply(_Meta, {replicas_stopped, StreamId}, #?MODULE{streams = Streams} = State) ->
+ case maps:get(StreamId, Streams, undefined) of
+ undefined ->
+ {State, {error, not_found}, []};
+ #{conf := Conf0} = SState0 ->
+ Phase = phase_check_quorum,
+ Conf = Conf0#{replica_pids => []},
+ PhaseArgs = [Conf],
+ SState = SState0#{conf => Conf,
+ phase => Phase,
+ phase_args => PhaseArgs},
+ rabbit_log:info("rabbit_stream_coordinator: ~p all replicas have been stopped, "
+ "checking quorum available", [StreamId]),
+ {State#?MODULE{streams = Streams#{StreamId => SState}}, ok,
+ [{aux, {phase, StreamId, Phase, PhaseArgs}}]}
+ end;
+apply(_Meta, {stream_updated, #{name := StreamId} = Conf}, #?MODULE{streams = Streams} = State) ->
+ SState0 = maps:get(StreamId, Streams),
+ Phase = phase_repair_mnesia,
+ PhaseArgs = [update, Conf],
+ SState = SState0#{conf => Conf,
+ phase => Phase,
+ phase_args => PhaseArgs},
+ rabbit_log:debug("rabbit_stream_coordinator: ~p entering ~p after"
+ " stream_updated", [StreamId, Phase]),
+ {State#?MODULE{streams = Streams#{StreamId => SState}}, ok,
+ [{aux, {phase, StreamId, Phase, PhaseArgs}}]};
+apply(_, {timeout, {pipeline, Cmds}}, State) ->
+ Actions = [{mod_call, ra, pipeline_command, [{?MODULE, node()}, Cmd]} || Cmd <- Cmds],
+ {State, ok, Actions};
+apply(_, {timeout, {aux, Cmd}}, State) ->
+ {State, ok, [{aux, Cmd}]};
+apply(Meta, {_, #{from := From}} = Cmd, State) ->
+ ?MODULE:apply(Meta#{from => From}, Cmd, State).
+
+state_enter(leader, #?MODULE{streams = Streams, monitors = Monitors}) ->
+ maps:fold(fun(_, #{conf := #{name := StreamId},
+ pending_replicas := Pending,
+ state := State,
+ phase := Phase,
+ phase_args := PhaseArgs}, Acc) ->
+ restart_aux_phase(State, Phase, PhaseArgs, StreamId) ++
+ pipeline_restart_replica_cmds(StreamId, Pending) ++
+ Acc
+ end, [{monitor, process, P} || P <- maps:keys(Monitors)], Streams);
+state_enter(follower, #?MODULE{monitors = Monitors}) ->
+ [{monitor, process, P} || P <- maps:keys(Monitors)];
+state_enter(recover, _) ->
+ put('$rabbit_vm_category', ?MODULE),
+ [];
+state_enter(_, _) ->
+ [].
+
+restart_aux_phase(running, _, _, _) ->
+ [];
+restart_aux_phase(_State, Phase, PhaseArgs, StreamId) ->
+ [{aux, {phase, StreamId, Phase, PhaseArgs}}].
+
+pipeline_restart_replica_cmds(StreamId, Pending) ->
+ [{timer, {pipeline, [{start_replica, #{stream_id => StreamId,
+ node => Node,
+ from => undefined,
+ retries => 1}}
+ || Node <- Pending]}, ?RESTART_TIMEOUT}].
+
+tick(_Ts, _State) ->
+ [{aux, maybe_resize_coordinator_cluster}].
+
+maybe_resize_coordinator_cluster() ->
+ spawn(fun() ->
+ case ra:members({?MODULE, node()}) of
+ {_, Members, _} ->
+ MemberNodes = [Node || {_, Node} <- Members],
+ Running = rabbit_mnesia:cluster_nodes(running),
+ All = rabbit_mnesia:cluster_nodes(all),
+ case Running -- MemberNodes of
+ [] ->
+ ok;
+ New ->
+ rabbit_log:warning("New rabbit node(s) detected, "
+ "adding stream coordinator in: ~p", [New]),
+ add_members(Members, New)
+ end,
+ case MemberNodes -- All of
+ [] ->
+ ok;
+ Old ->
+ rabbit_log:warning("Rabbit node(s) removed from the cluster, "
+ "deleting stream coordinator in: ~p", [Old]),
+ remove_members(Members, Old)
+ end;
+ _ ->
+ ok
+ end
+ end).
+
+add_members(_, []) ->
+ ok;
+add_members(Members, [Node | Nodes]) ->
+ Conf = make_ra_conf(Node, [N || {_, N} <- Members]),
+ case ra:start_server(Conf) of
+ ok ->
+ case ra:add_member(Members, {?MODULE, Node}) of
+ {ok, NewMembers, _} ->
+ add_members(NewMembers, Nodes);
+ _ ->
+ add_members(Members, Nodes)
+ end;
+ Error ->
+ rabbit_log:warning("Stream coordinator failed to start on node ~p : ~p",
+ [Node, Error]),
+ add_members(Members, Nodes)
+ end.
+
+remove_members(_, []) ->
+ ok;
+remove_members(Members, [Node | Nodes]) ->
+ case ra:remove_member(Members, {?MODULE, Node}) of
+ {ok, NewMembers, _} ->
+ remove_members(NewMembers, Nodes);
+ _ ->
+ remove_members(Members, Nodes)
+ end.
+
+init_aux(_Name) ->
+ {#{}, undefined}.
+
+%% TODO ensure the dead writer is restarted as a replica at some point in time, increasing timeout?
+handle_aux(leader, _, maybe_resize_coordinator_cluster, {Monitors, undefined}, LogState, _) ->
+ Pid = maybe_resize_coordinator_cluster(),
+ {no_reply, {Monitors, Pid}, LogState, [{monitor, process, aux, Pid}]};
+handle_aux(leader, _, maybe_resize_coordinator_cluster, AuxState, LogState, _) ->
+ %% Coordinator resizing is still happening, let's ignore this tick event
+ {no_reply, AuxState, LogState};
+handle_aux(leader, _, {down, Pid, _}, {Monitors, Pid}, LogState, _) ->
+ %% Coordinator resizing has finished
+ {no_reply, {Monitors, undefined}, LogState};
+handle_aux(leader, _, {phase, _, Fun, Args} = Cmd, {Monitors, Coordinator}, LogState, _) ->
+ Pid = erlang:apply(?MODULE, Fun, Args),
+ Actions = [{monitor, process, aux, Pid}],
+ {no_reply, {maps:put(Pid, Cmd, Monitors), Coordinator}, LogState, Actions};
+handle_aux(leader, _, {down, Pid, normal}, {Monitors, Coordinator}, LogState, _) ->
+ {no_reply, {maps:remove(Pid, Monitors), Coordinator}, LogState};
+handle_aux(leader, _, {down, Pid, Reason}, {Monitors0, Coordinator}, LogState, _) ->
+ %% The phase has failed, let's retry it
+ case maps:get(Pid, Monitors0) of
+ {phase, StreamId, phase_start_new_leader, Args} ->
+ rabbit_log:warning("Error while starting new leader for stream queue ~p, "
+ "restarting election: ~p", [StreamId, Reason]),
+ Monitors = maps:remove(Pid, Monitors0),
+ Cmd = {phase, StreamId, phase_check_quorum, Args},
+ {no_reply, {Monitors, Coordinator}, LogState, [{timer, {aux, Cmd}, ?PHASE_RETRY_TIMEOUT}]};
+ {phase, StreamId, Fun, _} = Cmd ->
+ rabbit_log:warning("Error while executing coordinator phase ~p for stream queue ~p ~p",
+ [Fun, StreamId, Reason]),
+ Monitors = maps:remove(Pid, Monitors0),
+ {no_reply, {Monitors, Coordinator}, LogState, [{timer, {aux, Cmd}, ?PHASE_RETRY_TIMEOUT}]}
+ end;
+handle_aux(_, _, _, AuxState, LogState, _) ->
+ {no_reply, AuxState, LogState}.
+
+reply_and_run_pending(From, StreamId, Reply, WrapReply, Actions0, #?MODULE{streams = Streams} = State) ->
+ #{pending_cmds := Pending} = SState0 = maps:get(StreamId, Streams),
+ AuxActions = [{mod_call, ra, pipeline_command, [{?MODULE, node()}, Cmd]}
+ || Cmd <- Pending],
+ SState = maps:put(pending_cmds, [], SState0),
+ Actions = case From of
+ undefined ->
+ AuxActions ++ Actions0;
+ _ ->
+ wrap_reply(From, WrapReply) ++ AuxActions ++ Actions0
+ end,
+ {State#?MODULE{streams = Streams#{StreamId => SState}}, Reply, Actions}.
+
+wrap_reply(From, Reply) ->
+ [{reply, From, {wrap_reply, Reply}}].
+
+add_pending_cmd(From, {CmdName, CmdMap}, #{pending_cmds := Pending0} = StreamState) ->
+ %% Remove from pending the leader election and automatic replica restart when
+ %% the command is delete_cluster
+ Pending = case CmdName of
+ delete_cluster ->
+ lists:filter(fun({down, _, _}) ->
+ false;
+ (_) ->
+ true
+ end, Pending0);
+ _ ->
+ Pending0
+ end,
+ maps:put(pending_cmds, Pending ++ [{CmdName, maps:put(from, From, CmdMap)}],
+ StreamState).
+
+clear_stream_state(StreamState) ->
+ StreamState#{reply_to => undefined,
+ state => running,
+ phase => undefined,
+ phase_args => undefined}.
+
+update_stream_state(From, State, Phase, PhaseArgs, StreamState) ->
+ StreamState#{reply_to => From,
+ state => State,
+ phase => Phase,
+ phase_args => PhaseArgs}.
+
+phase_start_replica(Node, #{name := StreamId} = Conf0,
+ Retries) ->
+ spawn(
+ fun() ->
+ %% If a new leader hasn't yet been elected, this will fail with a badmatch
+ %% as get_reader_context returns a no proc. An unhandled failure will
+ %% crash this monitored process and restart it later.
+ %% TODO However, do we want that crash in the log? We might need to try/catch
+ %% to provide a log message instead as it's 'expected'. We could try to
+ %% verify first that the leader is alive, but there would still be potential
+ %% for a race condition in here.
+ try
+ case osiris_replica:start(Node, Conf0) of
+ {ok, Pid} ->
+ ra:pipeline_command({?MODULE, node()},
+ {start_replica_reply, StreamId, Pid});
+ {error, already_present} ->
+ ra:pipeline_command({?MODULE, node()}, {phase_finished, StreamId, ok});
+ {error, {already_started, _}} ->
+ ra:pipeline_command({?MODULE, node()}, {phase_finished, StreamId, ok});
+ {error, Reason} = Error ->
+ rabbit_log:warning("Error while starting replica for ~p : ~p",
+ [maps:get(name, Conf0), Reason]),
+ ra:pipeline_command({?MODULE, node()},
+ {start_replica_failed, StreamId, Node, Retries, Error})
+ end
+ catch _:E->
+ rabbit_log:warning("Error while starting replica for ~p : ~p",
+ [maps:get(name, Conf0), E]),
+ ra:pipeline_command({?MODULE, node()},
+ {start_replica_failed, StreamId, Node, Retries, {error, E}})
+ end
+ end).
+
+phase_delete_replica(Node, Conf) ->
+ spawn(
+ fun() ->
+ ok = osiris_replica:delete(Node, Conf),
+ ra:pipeline_command({?MODULE, node()}, {stream_updated, Conf})
+ end).
+
+phase_stop_replicas(#{replica_nodes := Replicas,
+ name := StreamId} = Conf) ->
+ spawn(
+ fun() ->
+ [try
+ osiris_replica:stop(Node, Conf)
+ catch _:{{nodedown, _}, _} ->
+ %% It could be the old leader that is still down, it's normal.
+ ok
+ end || Node <- Replicas],
+ ra:pipeline_command({?MODULE, node()}, {replicas_stopped, StreamId})
+ end).
+
+phase_start_new_leader(#{name := StreamId, leader_node := Node, leader_pid := LPid} = Conf) ->
+ spawn(fun() ->
+ osiris_replica:stop(Node, Conf),
+ %% If the start fails, the monitor will capture the crash and restart it
+ case osiris_writer:start(Conf) of
+ {ok, Pid} ->
+ ra:pipeline_command({?MODULE, node()},
+ {leader_elected, StreamId, Pid});
+ {error, already_present} ->
+ ra:pipeline_command({?MODULE, node()},
+ {leader_elected, StreamId, LPid});
+ {error, {already_started, Pid}} ->
+ ra:pipeline_command({?MODULE, node()},
+ {leader_elected, StreamId, Pid})
+ end
+ end).
+
+phase_check_quorum(#{name := StreamId,
+ epoch := Epoch,
+ replica_nodes := Nodes} = Conf) ->
+ spawn(fun() ->
+ Offsets = find_replica_offsets(Conf),
+ case is_quorum(length(Nodes) + 1, length(Offsets)) of
+ true ->
+ ra:pipeline_command({?MODULE, node()},
+ {start_leader_election, StreamId, Epoch + 1, Offsets});
+ false ->
+ %% Let's crash this process so the monitor will restart it
+ exit({not_enough_quorum, StreamId})
+ end
+ end).
+
+find_replica_offsets(#{replica_nodes := Nodes,
+ leader_node := Leader} = Conf) ->
+ lists:foldl(
+ fun(Node, Acc) ->
+ try
+ %% osiris_log:overview/1 needs the directory - last item of the list
+ case rpc:call(Node, rabbit, is_running, []) of
+ false ->
+ Acc;
+ true ->
+ case rpc:call(Node, ?MODULE, log_overview, [Conf]) of
+ {badrpc, nodedown} ->
+ Acc;
+ {_Range, Offsets} ->
+ [{Node, select_highest_offset(Offsets)} | Acc]
+ end
+ end
+ catch
+ _:_ ->
+ Acc
+ end
+ end, [], Nodes ++ [Leader]).
+
+select_highest_offset([]) ->
+ empty;
+select_highest_offset(Offsets) ->
+ lists:last(Offsets).
+
+log_overview(Config) ->
+ Dir = osiris_log:directory(Config),
+ osiris_log:overview(Dir).
+
+find_max_offset(Offsets) ->
+ [{Node, _} | _] = lists:sort(fun({_, {Ao, E}}, {_, {Bo, E}}) ->
+ Ao >= Bo;
+ ({_, {_, Ae}}, {_, {_, Be}}) ->
+ Ae >= Be;
+ ({_, empty}, _) ->
+ false;
+ (_, {_, empty}) ->
+ true
+ end, Offsets),
+ Node.
+
+is_quorum(1, 1) ->
+ true;
+is_quorum(NumReplicas, NumAlive) ->
+ NumAlive >= ((NumReplicas div 2) + 1).
+
+phase_repair_mnesia(new, Q) ->
+ spawn(fun() ->
+ Reply = rabbit_amqqueue:internal_declare(Q, false),
+ #{name := StreamId} = amqqueue:get_type_state(Q),
+ ra:pipeline_command({?MODULE, node()}, {phase_finished, StreamId, Reply})
+ end);
+
+phase_repair_mnesia(update, #{reference := QName,
+ leader_pid := LeaderPid,
+ name := StreamId} = Conf) ->
+ Fun = fun (Q) ->
+ amqqueue:set_type_state(amqqueue:set_pid(Q, LeaderPid), Conf)
+ end,
+ spawn(fun() ->
+ case rabbit_misc:execute_mnesia_transaction(
+ fun() ->
+ rabbit_amqqueue:update(QName, Fun)
+ end) of
+ not_found ->
+ %% This can happen during recovery
+ [Q] = mnesia:dirty_read(rabbit_durable_queue, QName),
+ rabbit_amqqueue:ensure_rabbit_queue_record_is_initialized(Fun(Q));
+ _ ->
+ ok
+ end,
+ ra:pipeline_command({?MODULE, node()}, {phase_finished, StreamId, ok})
+ end).
+
+phase_start_cluster(Q0) ->
+ spawn(
+ fun() ->
+ case osiris:start_cluster(amqqueue:get_type_state(Q0)) of
+ {ok, #{leader_pid := Pid} = Conf} ->
+ Q = amqqueue:set_type_state(amqqueue:set_pid(Q0, Pid), Conf),
+ ra:pipeline_command({?MODULE, node()}, {start_cluster_reply, Q});
+ {error, {already_started, _}} ->
+ ra:pipeline_command({?MODULE, node()}, {start_cluster_finished, {error, already_started}})
+ end
+ end).
+
+phase_delete_cluster(#{name := StreamId,
+ reference := QName} = Conf, ActingUser) ->
+ spawn(
+ fun() ->
+ ok = osiris:delete_cluster(Conf),
+ _ = rabbit_amqqueue:internal_delete(QName, ActingUser),
+ ra:pipeline_command({?MODULE, node()}, {delete_cluster_reply, StreamId})
+ end).
+
+format_ra_event(ServerId, Evt) ->
+ {stream_coordinator_event, ServerId, Evt}.
+
+make_ra_conf(Node, Nodes) ->
+ UId = ra:new_uid(ra_lib:to_binary(?MODULE)),
+ Formatter = {?MODULE, format_ra_event, []},
+ Members = [{?MODULE, N} || N <- Nodes],
+ TickTimeout = application:get_env(rabbit, stream_tick_interval,
+ ?TICK_TIMEOUT),
+ #{cluster_name => ?MODULE,
+ id => {?MODULE, Node},
+ uid => UId,
+ friendly_name => atom_to_list(?MODULE),
+ metrics_key => ?MODULE,
+ initial_members => Members,
+ log_init_args => #{uid => UId},
+ tick_timeout => TickTimeout,
+ machine => {module, ?MODULE, #{}},
+ ra_event_formatter => Formatter}.
+
+add_unique(Node, Nodes) ->
+ case lists:member(Node, Nodes) of
+ true ->
+ Nodes;
+ _ ->
+ [Node | Nodes]
+ end.
+
+delete_replica_pid(Node, ReplicaPids) ->
+ lists:partition(fun(P) -> node(P) =/= Node end, ReplicaPids).
diff --git a/src/rabbit_stream_queue.erl b/src/rabbit_stream_queue.erl
new file mode 100644
index 0000000000..612bf81d00
--- /dev/null
+++ b/src/rabbit_stream_queue.erl
@@ -0,0 +1,665 @@
+%% 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.
+%%
+%% Copyright (c) 2012-2020 VMware, Inc. or its affiliates. All rights reserved.
+%%
+
+-module(rabbit_stream_queue).
+
+-behaviour(rabbit_queue_type).
+
+-export([is_enabled/0,
+ declare/2,
+ delete/4,
+ purge/1,
+ policy_changed/1,
+ recover/2,
+ is_recoverable/1,
+ consume/3,
+ cancel/5,
+ handle_event/2,
+ deliver/2,
+ settle/4,
+ credit/4,
+ dequeue/4,
+ info/2,
+ init/1,
+ close/1,
+ update/2,
+ state_info/1,
+ stat/1,
+ is_policy_applicable/2]).
+
+-export([set_retention_policy/3]).
+-export([add_replica/3,
+ delete_replica/3]).
+-export([format_osiris_event/2]).
+-export([update_stream_conf/1]).
+
+-include("rabbit.hrl").
+-include("amqqueue.hrl").
+
+-type appender_seq() :: non_neg_integer().
+
+-record(stream, {name :: rabbit_types:r('queue'),
+ credit :: integer(),
+ max :: non_neg_integer(),
+ start_offset = 0 :: non_neg_integer(),
+ listening_offset = 0 :: non_neg_integer(),
+ log :: undefined | orisis_log:state()}).
+
+-record(stream_client, {name :: term(),
+ leader :: pid(),
+ next_seq = 1 :: non_neg_integer(),
+ correlation = #{} :: #{appender_seq() => term()},
+ soft_limit :: non_neg_integer(),
+ slow = false :: boolean(),
+ readers = #{} :: #{term() => #stream{}}
+ }).
+
+-import(rabbit_queue_type_util, [args_policy_lookup/3]).
+
+-type client() :: #stream_client{}.
+
+-spec is_enabled() -> boolean().
+is_enabled() ->
+ rabbit_feature_flags:is_enabled(stream_queue).
+
+-spec declare(amqqueue:amqqueue(), node()) ->
+ {'new' | 'existing', amqqueue:amqqueue()} |
+ rabbit_types:channel_exit().
+declare(Q0, Node) when ?amqqueue_is_stream(Q0) ->
+ Arguments = amqqueue:get_arguments(Q0),
+ QName = amqqueue:get_name(Q0),
+ check_invalid_arguments(QName, Arguments),
+ rabbit_queue_type_util:check_auto_delete(Q0),
+ rabbit_queue_type_util:check_exclusive(Q0),
+ rabbit_queue_type_util:check_non_durable(Q0),
+ Opts = amqqueue:get_options(Q0),
+ ActingUser = maps:get(user, Opts, ?UNKNOWN_USER),
+ Conf0 = make_stream_conf(Node, Q0),
+ case rabbit_stream_coordinator:start_cluster(
+ amqqueue:set_type_state(Q0, Conf0)) of
+ {ok, {error, already_started}, _} ->
+ rabbit_misc:protocol_error(precondition_failed,
+ "safe queue name already in use '~s'",
+ [Node]);
+ {ok, {created, Q}, _} ->
+ rabbit_event:notify(queue_created,
+ [{name, QName},
+ {durable, true},
+ {auto_delete, false},
+ {arguments, Arguments},
+ {user_who_performed_action,
+ ActingUser}]),
+ {new, Q};
+ {ok, {error, Error}, _} ->
+ _ = rabbit_amqqueue:internal_delete(QName, ActingUser),
+ rabbit_misc:protocol_error(
+ internal_error,
+ "Cannot declare a queue '~s' on node '~s': ~255p",
+ [rabbit_misc:rs(QName), node(), Error]);
+ {ok, {existing, Q}, _} ->
+ {existing, Q};
+ {error, coordinator_unavailable} ->
+ _ = rabbit_amqqueue:internal_delete(QName, ActingUser),
+ rabbit_misc:protocol_error(
+ internal_error,
+ "Cannot declare a queue '~s' on node '~s': coordinator unavailable",
+ [rabbit_misc:rs(QName), node()])
+ end.
+
+-spec delete(amqqueue:amqqueue(), boolean(),
+ boolean(), rabbit_types:username()) ->
+ rabbit_types:ok(non_neg_integer()) |
+ rabbit_types:error(in_use | not_empty).
+delete(Q, _IfUnused, _IfEmpty, ActingUser) ->
+ Name = maps:get(name, amqqueue:get_type_state(Q)),
+ {ok, Reply, _} = rabbit_stream_coordinator:delete_cluster(Name, ActingUser),
+ Reply.
+
+-spec purge(amqqueue:amqqueue()) ->
+ {'ok', non_neg_integer()}.
+purge(_Q) ->
+ {ok, 0}.
+
+-spec policy_changed(amqqueue:amqqueue()) -> 'ok'.
+policy_changed(Q) ->
+ Name = maps:get(name, amqqueue:get_type_state(Q)),
+ _ = rabbit_stream_coordinator:policy_changed(Name),
+ ok.
+
+stat(_) ->
+ {ok, 0, 0}.
+
+consume(Q, #{prefetch_count := 0}, _)
+ when ?amqqueue_is_stream(Q) ->
+ rabbit_misc:protocol_error(precondition_failed,
+ "consumer prefetch count is not set for '~s'",
+ [rabbit_misc:rs(amqqueue:get_name(Q))]);
+consume(Q, #{no_ack := true}, _)
+ when ?amqqueue_is_stream(Q) ->
+ rabbit_misc:protocol_error(
+ not_implemented,
+ "automatic acknowledgement not supported by stream queues ~s",
+ [rabbit_misc:rs(amqqueue:get_name(Q))]);
+consume(Q, #{limiter_active := true}, _State)
+ when ?amqqueue_is_stream(Q) ->
+ {error, global_qos_not_supported_for_queue_type};
+consume(Q, Spec, QState0) when ?amqqueue_is_stream(Q) ->
+ %% Messages should include the offset as a custom header.
+ check_queue_exists_in_local_node(Q),
+ #{no_ack := NoAck,
+ channel_pid := ChPid,
+ prefetch_count := ConsumerPrefetchCount,
+ consumer_tag := ConsumerTag,
+ exclusive_consume := ExclusiveConsume,
+ args := Args,
+ ok_msg := OkMsg} = Spec,
+ QName = amqqueue:get_name(Q),
+ Offset = case rabbit_misc:table_lookup(Args, <<"x-stream-offset">>) of
+ undefined ->
+ next;
+ {_, <<"first">>} ->
+ first;
+ {_, <<"last">>} ->
+ last;
+ {_, <<"next">>} ->
+ next;
+ {_, V} ->
+ V
+ end,
+ rabbit_core_metrics:consumer_created(ChPid, ConsumerTag, ExclusiveConsume,
+ not NoAck, QName,
+ ConsumerPrefetchCount, false,
+ up, Args),
+ %% FIXME: reply needs to be sent before the stream begins sending
+ %% really it should be sent by the stream queue process like classic queues
+ %% do
+ maybe_send_reply(ChPid, OkMsg),
+ QState = begin_stream(QState0, Q, ConsumerTag, Offset,
+ ConsumerPrefetchCount),
+ {ok, QState, []}.
+
+get_local_pid(#{leader_pid := Pid}) when node(Pid) == node() ->
+ Pid;
+get_local_pid(#{replica_pids := ReplicaPids}) ->
+ [Local | _] = lists:filter(fun(Pid) ->
+ node(Pid) == node()
+ end, ReplicaPids),
+ Local.
+
+begin_stream(#stream_client{readers = Readers0} = State,
+ Q, Tag, Offset, Max) ->
+ LocalPid = get_local_pid(amqqueue:get_type_state(Q)),
+ {ok, Seg0} = osiris:init_reader(LocalPid, Offset),
+ NextOffset = osiris_log:next_offset(Seg0) - 1,
+ osiris:register_offset_listener(LocalPid, NextOffset),
+ %% TODO: avoid double calls to the same process
+ StartOffset = case Offset of
+ last -> NextOffset;
+ next -> NextOffset;
+ _ -> Offset
+ end,
+ Str0 = #stream{name = amqqueue:get_name(Q),
+ credit = Max,
+ start_offset = StartOffset,
+ listening_offset = NextOffset,
+ log = Seg0,
+ max = Max},
+ State#stream_client{readers = Readers0#{Tag => Str0}}.
+
+cancel(_Q, ConsumerTag, OkMsg, ActingUser, #stream_client{readers = Readers0,
+ name = QName} = State) ->
+ Readers = maps:remove(ConsumerTag, Readers0),
+ rabbit_core_metrics:consumer_deleted(self(), ConsumerTag, QName),
+ rabbit_event:notify(consumer_deleted, [{consumer_tag, ConsumerTag},
+ {channel, self()},
+ {queue, QName},
+ {user_who_performed_action, ActingUser}]),
+ maybe_send_reply(self(), OkMsg),
+ {ok, State#stream_client{readers = Readers}}.
+
+credit(_, _, _, _) ->
+ ok.
+
+deliver(QSs, #delivery{confirm = Confirm} = Delivery) ->
+ lists:foldl(
+ fun({_Q, stateless}, {Qs, Actions}) ->
+ %% TODO what do we do with stateless?
+ %% QRef = amqqueue:get_pid(Q),
+ %% ok = rabbit_fifo_client:untracked_enqueue(
+ %% [QRef], Delivery#delivery.message),
+ {Qs, Actions};
+ ({Q, S0}, {Qs, Actions}) ->
+ S = deliver(Confirm, Delivery, S0),
+ {[{Q, S} | Qs], Actions}
+ end, {[], []}, QSs).
+
+deliver(_Confirm, #delivery{message = Msg, msg_seq_no = MsgId},
+ #stream_client{name = Name,
+ leader = LeaderPid,
+ next_seq = Seq,
+ correlation = Correlation0,
+ soft_limit = SftLmt,
+ slow = Slow0} = State) ->
+ ok = osiris:write(LeaderPid, Seq, msg_to_iodata(Msg)),
+ Correlation = case MsgId of
+ undefined ->
+ Correlation0;
+ _ when is_number(MsgId) ->
+ Correlation0#{Seq => MsgId}
+ end,
+ Slow = case maps:size(Correlation) >= SftLmt of
+ true when not Slow0 ->
+ credit_flow:block(Name),
+ true;
+ Bool ->
+ Bool
+ end,
+ State#stream_client{next_seq = Seq + 1,
+ correlation = Correlation,
+ slow = Slow}.
+-spec dequeue(_, _, _, client()) -> no_return().
+dequeue(_, _, _, #stream_client{name = Name}) ->
+ rabbit_misc:protocol_error(
+ not_implemented,
+ "basic.get not supported by stream queues ~s",
+ [rabbit_misc:rs(Name)]).
+
+handle_event({osiris_written, From, Corrs}, State = #stream_client{correlation = Correlation0,
+ soft_limit = SftLmt,
+ slow = Slow0,
+ name = Name}) ->
+ MsgIds = maps:values(maps:with(Corrs, Correlation0)),
+ Correlation = maps:without(Corrs, Correlation0),
+ Slow = case maps:size(Correlation) < SftLmt of
+ true when Slow0 ->
+ credit_flow:unblock(Name),
+ false;
+ _ ->
+ Slow0
+ end,
+ {ok, State#stream_client{correlation = Correlation,
+ slow = Slow}, [{settled, From, MsgIds}]};
+handle_event({osiris_offset, _From, _Offs}, State = #stream_client{leader = Leader,
+ readers = Readers0,
+ name = Name}) ->
+ %% offset isn't actually needed as we use the atomic to read the
+ %% current committed
+ {Readers, TagMsgs} = maps:fold(
+ fun (Tag, Str0, {Acc, TM}) ->
+ {Str, Msgs} = stream_entries(Name, Leader, Str0),
+ %% HACK for now, better to just return but
+ %% tricky with acks credits
+ %% that also evaluate the stream
+ % gen_server:cast(self(), {stream_delivery, Tag, Msgs}),
+ {Acc#{Tag => Str}, [{Tag, Leader, Msgs} | TM]}
+ end, {#{}, []}, Readers0),
+ Ack = true,
+ Deliveries = [{deliver, Tag, Ack, OffsetMsg}
+ || {Tag, _LeaderPid, OffsetMsg} <- TagMsgs],
+ {ok, State#stream_client{readers = Readers}, Deliveries}.
+
+is_recoverable(Q) ->
+ Node = node(),
+ #{replica_nodes := Nodes,
+ leader_node := Leader} = amqqueue:get_type_state(Q),
+ lists:member(Node, Nodes ++ [Leader]).
+
+recover(_VHost, Queues) ->
+ lists:foldl(
+ fun (Q0, {R0, F0}) ->
+ {ok, Q} = recover(Q0),
+ {[Q | R0], F0}
+ end, {[], []}, Queues).
+
+settle(complete, CTag, MsgIds, #stream_client{readers = Readers0,
+ name = Name,
+ leader = Leader} = State) ->
+ Credit = length(MsgIds),
+ {Readers, Msgs} = case Readers0 of
+ #{CTag := #stream{credit = Credit0} = Str0} ->
+ Str1 = Str0#stream{credit = Credit0 + Credit},
+ {Str, Msgs0} = stream_entries(Name, Leader, Str1),
+ {Readers0#{CTag => Str}, Msgs0};
+ _ ->
+ {Readers0, []}
+ end,
+ {State#stream_client{readers = Readers}, [{deliver, CTag, true, Msgs}]};
+settle(_, _, _, #stream_client{name = Name}) ->
+ rabbit_misc:protocol_error(
+ not_implemented,
+ "basic.nack and basic.reject not supported by stream queues ~s",
+ [rabbit_misc:rs(Name)]).
+
+info(Q, Items) ->
+ lists:foldr(fun(Item, Acc) ->
+ [{Item, i(Item, Q)} | Acc]
+ end, [], Items).
+
+i(name, Q) when ?is_amqqueue(Q) -> amqqueue:get_name(Q);
+i(durable, Q) when ?is_amqqueue(Q) -> amqqueue:is_durable(Q);
+i(auto_delete, Q) when ?is_amqqueue(Q) -> amqqueue:is_auto_delete(Q);
+i(arguments, Q) when ?is_amqqueue(Q) -> amqqueue:get_arguments(Q);
+i(leader, Q) when ?is_amqqueue(Q) ->
+ #{leader_node := Leader} = amqqueue:get_type_state(Q),
+ Leader;
+i(members, Q) when ?is_amqqueue(Q) ->
+ #{replica_nodes := Nodes} = amqqueue:get_type_state(Q),
+ Nodes;
+i(online, Q) ->
+ #{replica_pids := ReplicaPids,
+ leader_pid := LeaderPid} = amqqueue:get_type_state(Q),
+ [node(P) || P <- ReplicaPids ++ [LeaderPid], rabbit_misc:is_process_alive(P)];
+i(state, Q) when ?is_amqqueue(Q) ->
+ %% TODO the coordinator should answer this, I guess??
+ running;
+i(messages, Q) when ?is_amqqueue(Q) ->
+ QName = amqqueue:get_name(Q),
+ case ets:lookup(queue_coarse_metrics, QName) of
+ [{_, _, _, M, _}] ->
+ M;
+ [] ->
+ 0
+ end;
+i(messages_ready, Q) when ?is_amqqueue(Q) ->
+ QName = amqqueue:get_name(Q),
+ case ets:lookup(queue_coarse_metrics, QName) of
+ [{_, MR, _, _, _}] ->
+ MR;
+ [] ->
+ 0
+ end;
+i(messages_unacknowledged, Q) when ?is_amqqueue(Q) ->
+ QName = amqqueue:get_name(Q),
+ case ets:lookup(queue_coarse_metrics, QName) of
+ [{_, _, MU, _, _}] ->
+ MU;
+ [] ->
+ 0
+ end;
+i(committed_offset, Q) ->
+ %% TODO should it be on a metrics table?
+ Data = osiris_counters:overview(),
+ maps:get(committed_offset,
+ maps:get({osiris_writer, amqqueue:get_name(Q)}, Data));
+i(policy, Q) ->
+ case rabbit_policy:name(Q) of
+ none -> '';
+ Policy -> Policy
+ end;
+i(operator_policy, Q) ->
+ case rabbit_policy:name_op(Q) of
+ none -> '';
+ Policy -> Policy
+ end;
+i(effective_policy_definition, Q) ->
+ case rabbit_policy:effective_definition(Q) of
+ undefined -> [];
+ Def -> Def
+ end;
+i(type, _) ->
+ stream.
+
+init(Q) when ?is_amqqueue(Q) ->
+ Leader = amqqueue:get_pid(Q),
+ {ok, SoftLimit} = application:get_env(rabbit, stream_messages_soft_limit),
+ #stream_client{name = amqqueue:get_name(Q),
+ leader = Leader,
+ soft_limit = SoftLimit}.
+
+close(#stream_client{readers = Readers}) ->
+ _ = maps:map(fun (_, #stream{log = Log}) ->
+ osiris_log:close(Log)
+ end, Readers),
+ ok.
+
+update(_, State) ->
+ State.
+
+state_info(_) ->
+ #{}.
+
+set_retention_policy(Name, VHost, Policy) ->
+ case rabbit_amqqueue:check_max_age(Policy) of
+ {error, _} = E ->
+ E;
+ MaxAge ->
+ QName = rabbit_misc:r(VHost, queue, Name),
+ Fun = fun(Q) ->
+ Conf = amqqueue:get_type_state(Q),
+ amqqueue:set_type_state(Q, Conf#{max_age => MaxAge})
+ end,
+ case rabbit_misc:execute_mnesia_transaction(
+ fun() -> rabbit_amqqueue:update(QName, Fun) end) of
+ not_found ->
+ {error, not_found};
+ _ ->
+ ok
+ end
+ end.
+
+add_replica(VHost, Name, Node) ->
+ QName = rabbit_misc:r(VHost, queue, Name),
+ case rabbit_amqqueue:lookup(QName) of
+ {ok, Q} when ?amqqueue_is_classic(Q) ->
+ {error, classic_queue_not_supported};
+ {ok, Q} when ?amqqueue_is_quorum(Q) ->
+ {error, quorum_queue_not_supported};
+ {ok, Q} when ?amqqueue_is_stream(Q) ->
+ case lists:member(Node, rabbit_mnesia:cluster_nodes(running)) of
+ false ->
+ {error, node_not_running};
+ true ->
+ #{name := StreamId} = amqqueue:get_type_state(Q),
+ {ok, Reply, _} = rabbit_stream_coordinator:add_replica(StreamId, Node),
+ Reply
+ end;
+ E ->
+ E
+ end.
+
+delete_replica(VHost, Name, Node) ->
+ QName = rabbit_misc:r(VHost, queue, Name),
+ case rabbit_amqqueue:lookup(QName) of
+ {ok, Q} when ?amqqueue_is_classic(Q) ->
+ {error, classic_queue_not_supported};
+ {ok, Q} when ?amqqueue_is_quorum(Q) ->
+ {error, quorum_queue_not_supported};
+ {ok, Q} when ?amqqueue_is_stream(Q) ->
+ case lists:member(Node, rabbit_mnesia:cluster_nodes(running)) of
+ false ->
+ {error, node_not_running};
+ true ->
+ #{name := StreamId} = amqqueue:get_type_state(Q),
+ {ok, Reply, _} = rabbit_stream_coordinator:delete_replica(StreamId, Node),
+ Reply
+ end;
+ E ->
+ E
+ end.
+
+make_stream_conf(Node, Q) ->
+ QName = amqqueue:get_name(Q),
+ Name = queue_name(QName),
+ %% MaxLength = args_policy_lookup(<<"max-length">>, fun min/2, Q),
+ MaxBytes = args_policy_lookup(<<"max-length-bytes">>, fun min/2, Q),
+ MaxAge = max_age(args_policy_lookup(<<"max-age">>, fun max_age/2, Q)),
+ MaxSegmentSize = args_policy_lookup(<<"max-segment-size">>, fun min/2, Q),
+ Replicas = rabbit_mnesia:cluster_nodes(all) -- [Node],
+ Formatter = {?MODULE, format_osiris_event, [QName]},
+ Retention = lists:filter(fun({_, R}) ->
+ R =/= undefined
+ end, [{max_bytes, MaxBytes},
+ {max_age, MaxAge}]),
+ add_if_defined(max_segment_size, MaxSegmentSize, #{reference => QName,
+ name => Name,
+ retention => Retention,
+ leader_node => Node,
+ replica_nodes => Replicas,
+ event_formatter => Formatter,
+ epoch => 1}).
+
+update_stream_conf(#{reference := QName} = Conf) ->
+ case rabbit_amqqueue:lookup(QName) of
+ {ok, Q} ->
+ MaxBytes = args_policy_lookup(<<"max-length-bytes">>, fun min/2, Q),
+ MaxAge = max_age(args_policy_lookup(<<"max-age">>, fun max_age/2, Q)),
+ MaxSegmentSize = args_policy_lookup(<<"max-segment-size">>, fun min/2, Q),
+ Retention = lists:filter(fun({_, R}) ->
+ R =/= undefined
+ end, [{max_bytes, MaxBytes},
+ {max_age, MaxAge}]),
+ add_if_defined(max_segment_size, MaxSegmentSize, Conf#{retention => Retention});
+ _ ->
+ Conf
+ end.
+
+add_if_defined(_, undefined, Map) ->
+ Map;
+add_if_defined(Key, Value, Map) ->
+ maps:put(Key, Value, Map).
+
+format_osiris_event(Evt, QRef) ->
+ {'$gen_cast', {queue_event, QRef, Evt}}.
+
+max_age(undefined) ->
+ undefined;
+max_age(Bin) when is_binary(Bin) ->
+ rabbit_amqqueue:check_max_age(Bin);
+max_age(Age) ->
+ Age.
+
+max_age(Age1, Age2) ->
+ min(rabbit_amqqueue:check_max_age(Age1), rabbit_amqqueue:check_max_age(Age2)).
+
+check_invalid_arguments(QueueName, Args) ->
+ Keys = [<<"x-expires">>, <<"x-message-ttl">>,
+ <<"x-max-priority">>, <<"x-queue-mode">>, <<"x-overflow">>,
+ <<"x-max-in-memory-length">>, <<"x-max-in-memory-bytes">>,
+ <<"x-quorum-initial-group-size">>, <<"x-cancel-on-ha-failover">>],
+ rabbit_queue_type_util:check_invalid_arguments(QueueName, Args, Keys).
+
+queue_name(#resource{virtual_host = VHost, name = Name}) ->
+ Timestamp = erlang:integer_to_binary(erlang:system_time()),
+ osiris_util:to_base64uri(erlang:binary_to_list(<<VHost/binary, "_", Name/binary, "_",
+ Timestamp/binary>>)).
+
+recover(Q) ->
+ rabbit_stream_coordinator:recover(),
+ {ok, Q}.
+
+check_queue_exists_in_local_node(Q) ->
+ Conf = amqqueue:get_type_state(Q),
+ AllNodes = [maps:get(leader_node, Conf) | maps:get(replica_nodes, Conf)],
+ case lists:member(node(), AllNodes) of
+ true ->
+ ok;
+ false ->
+ rabbit_misc:protocol_error(precondition_failed,
+ "queue '~s' does not a have a replica on the local node",
+ [rabbit_misc:rs(amqqueue:get_name(Q))])
+ end.
+
+maybe_send_reply(_ChPid, undefined) -> ok;
+maybe_send_reply(ChPid, Msg) -> ok = rabbit_channel:send_command(ChPid, Msg).
+
+stream_entries(Name, Id, Str) ->
+ stream_entries(Name, Id, Str, []).
+
+stream_entries(Name, LeaderPid,
+ #stream{name = QName,
+ credit = Credit,
+ start_offset = StartOffs,
+ listening_offset = LOffs,
+ log = Seg0} = Str0, MsgIn)
+ when Credit > 0 ->
+ case osiris_log:read_chunk_parsed(Seg0) of
+ {end_of_stream, Seg} ->
+ NextOffset = osiris_log:next_offset(Seg),
+ case NextOffset > LOffs of
+ true ->
+ osiris:register_offset_listener(LeaderPid, NextOffset),
+ {Str0#stream{log = Seg,
+ listening_offset = NextOffset}, MsgIn};
+ false ->
+ {Str0#stream{log = Seg}, MsgIn}
+ end;
+ {Records, Seg} ->
+ Msgs = [begin
+ Msg0 = binary_to_msg(QName, B),
+ Msg = rabbit_basic:add_header(<<"x-stream-offset">>,
+ long, O, Msg0),
+ {Name, LeaderPid, O, false, Msg}
+ end || {O, B} <- Records,
+ O >= StartOffs],
+
+ NumMsgs = length(Msgs),
+
+ Str = Str0#stream{credit = Credit - NumMsgs,
+ log = Seg},
+ case Str#stream.credit < 1 of
+ true ->
+ %% we are done here
+ {Str, MsgIn ++ Msgs};
+ false ->
+ %% if there are fewer Msgs than Entries0 it means there were non-events
+ %% in the log and we should recurse and try again
+ stream_entries(Name, LeaderPid, Str, MsgIn ++ Msgs)
+ end
+ end;
+stream_entries(_Name, _Id, Str, Msgs) ->
+ {Str, Msgs}.
+
+binary_to_msg(#resource{virtual_host = VHost,
+ kind = queue,
+ name = QName}, Data) ->
+ R0 = rabbit_msg_record:init(Data),
+ %% if the message annotation isn't present the data most likely came from
+ %% the rabbitmq-stream plugin so we'll choose defaults that simulate use
+ %% of the direct exchange
+ {utf8, Exchange} = rabbit_msg_record:message_annotation(<<"x-exchange">>,
+ R0, {utf8, <<>>}),
+ {utf8, RoutingKey} = rabbit_msg_record:message_annotation(<<"x-routing-key">>,
+ R0, {utf8, QName}),
+ {Props, Payload} = rabbit_msg_record:to_amqp091(R0),
+ XName = #resource{kind = exchange,
+ virtual_host = VHost,
+ name = Exchange},
+ Content = #content{class_id = 60,
+ properties = Props,
+ properties_bin = none,
+ payload_fragments_rev = [Payload]},
+ {ok, Msg} = rabbit_basic:message(XName, RoutingKey, Content),
+ Msg.
+
+
+msg_to_iodata(#basic_message{exchange_name = #resource{name = Exchange},
+ routing_keys = [RKey | _],
+ content = Content}) ->
+ #content{properties = Props,
+ payload_fragments_rev = Payload} =
+ rabbit_binary_parser:ensure_content_decoded(Content),
+ R0 = rabbit_msg_record:from_amqp091(Props, lists:reverse(Payload)),
+ %% TODO durable?
+ R = rabbit_msg_record:add_message_annotations(
+ #{<<"x-exchange">> => {utf8, Exchange},
+ <<"x-routing-key">> => {utf8, RKey}}, R0),
+ rabbit_msg_record:to_iodata(R).
+
+-spec is_policy_applicable(amqqueue:amqqueue(), any()) -> boolean().
+is_policy_applicable(_Q, Policy) ->
+ Applicable = [<<"max-length-bytes">>, <<"max-age">>, <<"max-segment-size">>],
+ lists:all(fun({P, _}) ->
+ lists:member(P, Applicable)
+ end, Policy).
diff --git a/src/rabbit_vhost.erl b/src/rabbit_vhost.erl
index 81eb067c55..c8c5fc961a 100644
--- a/src/rabbit_vhost.erl
+++ b/src/rabbit_vhost.erl
@@ -48,11 +48,11 @@ recover(VHost) ->
VHostStubFile = filename:join(VHostDir, ".vhost"),
ok = rabbit_file:ensure_dir(VHostStubFile),
ok = file:write_file(VHostStubFile, VHost),
- {RecoveredClassic, FailedClassic, Quorum} = rabbit_amqqueue:recover(VHost),
- AllQs = RecoveredClassic ++ FailedClassic ++ Quorum,
+ {Recovered, Failed} = rabbit_amqqueue:recover(VHost),
+ AllQs = Recovered ++ Failed,
QNames = [amqqueue:get_name(Q) || Q <- AllQs],
ok = rabbit_binding:recover(rabbit_exchange:recover(VHost), QNames),
- ok = rabbit_amqqueue:start(RecoveredClassic),
+ ok = rabbit_amqqueue:start(Recovered),
%% Start queue mirrors.
ok = rabbit_mirror_queue_misc:on_vhost_up(VHost),
ok.
diff --git a/src/rabbit_vm.erl b/src/rabbit_vm.erl
index 6ecb79055c..b014e090c5 100644
--- a/src/rabbit_vm.erl
+++ b/src/rabbit_vm.erl
@@ -20,8 +20,8 @@ memory() ->
{Sums, _Other} = sum_processes(
lists:append(All), distinguishers(), [memory]),
- [Qs, QsSlave, Qqs, ConnsReader, ConnsWriter, ConnsChannel, ConnsOther,
- MsgIndexProc, MgmtDbProc, Plugins] =
+ [Qs, QsSlave, Qqs, Ssqs, Srqs, SCoor, ConnsReader, ConnsWriter, ConnsChannel,
+ ConnsOther, MsgIndexProc, MgmtDbProc, Plugins] =
[aggregate(Names, Sums, memory, fun (X) -> X end)
|| Names <- distinguished_interesting_sups()],
@@ -55,7 +55,8 @@ memory() ->
OtherProc = Processes
- ConnsReader - ConnsWriter - ConnsChannel - ConnsOther
- - Qs - QsSlave - Qqs - MsgIndexProc - Plugins - MgmtDbProc - MetricsProc,
+ - Qs - QsSlave - Qqs - Ssqs - Srqs - SCoor - MsgIndexProc - Plugins
+ - MgmtDbProc - MetricsProc,
[
%% Connections
@@ -68,6 +69,9 @@ memory() ->
{queue_procs, Qs},
{queue_slave_procs, QsSlave},
{quorum_queue_procs, Qqs},
+ {stream_queue_procs, Ssqs},
+ {stream_queue_replica_reader_procs, Srqs},
+ {stream_queue_coordinator_procs, SCoor},
%% Processes
{plugins, Plugins},
@@ -114,8 +118,8 @@ binary() ->
sets:add_element({Ptr, Sz}, Acc0)
end, Acc, Info)
end, distinguishers(), [{binary, sets:new()}]),
- [Other, Qs, QsSlave, Qqs, ConnsReader, ConnsWriter, ConnsChannel, ConnsOther,
- MsgIndexProc, MgmtDbProc, Plugins] =
+ [Other, Qs, QsSlave, Qqs, Ssqs, Srqs, Scoor, ConnsReader, ConnsWriter,
+ ConnsChannel, ConnsOther, MsgIndexProc, MgmtDbProc, Plugins] =
[aggregate(Names, [{other, Rest} | Sums], binary, fun sum_binary/1)
|| Names <- [[other] | distinguished_interesting_sups()]],
[{connection_readers, ConnsReader},
@@ -125,6 +129,9 @@ binary() ->
{queue_procs, Qs},
{queue_slave_procs, QsSlave},
{quorum_queue_procs, Qqs},
+ {stream_queue_procs, Ssqs},
+ {stream_queue_replica_reader_procs, Srqs},
+ {stream_queue_coordinator_procs, Scoor},
{plugins, Plugins},
{mgmt_db, MgmtDbProc},
{msg_index, MsgIndexProc},
@@ -168,7 +175,8 @@ bytes(Words) -> try
end.
interesting_sups() ->
- [queue_sups(), quorum_sups(), conn_sups() | interesting_sups0()].
+ [queue_sups(), quorum_sups(), stream_server_sups(), stream_reader_sups(),
+ conn_sups() | interesting_sups0()].
queue_sups() ->
all_vhosts_children(rabbit_amqqueue_sup_sup).
@@ -184,6 +192,9 @@ quorum_sups() ->
supervisor:which_children(ra_server_sup_sup)]
end.
+stream_server_sups() -> [osiris_server_sup].
+stream_reader_sups() -> [osiris_replica_reader_sup].
+
msg_stores() ->
all_vhosts_children(msg_store_transient)
++
@@ -229,13 +240,17 @@ ranch_server_sups() ->
with(Sups, With) -> [{Sup, With} || Sup <- Sups].
distinguishers() -> with(queue_sups(), fun queue_type/1) ++
- with(conn_sups(), fun conn_type/1).
+ with(conn_sups(), fun conn_type/1) ++
+ with(quorum_sups(), fun ra_type/1).
distinguished_interesting_sups() ->
[
with(queue_sups(), master),
with(queue_sups(), slave),
- quorum_sups(),
+ with(quorum_sups(), quorum),
+ stream_server_sups(),
+ stream_reader_sups(),
+ with(quorum_sups(), stream),
with(conn_sups(), reader),
with(conn_sups(), writer),
with(conn_sups(), channel),
@@ -292,6 +307,12 @@ conn_type(PDict) ->
_ -> other
end.
+ra_type(PDict) ->
+ case keyfind('$rabbit_vm_category', PDict) of
+ {value, rabbit_stream_coordinator} -> stream;
+ _ -> quorum
+ end.
+
%%----------------------------------------------------------------------------
%% NB: this code is non-rabbit specific.
diff --git a/src/unconfirmed_messages.erl b/src/unconfirmed_messages.erl
deleted file mode 100644
index b124b46808..0000000000
--- a/src/unconfirmed_messages.erl
+++ /dev/null
@@ -1,266 +0,0 @@
-%% 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-2020 VMware, Inc. or its affiliates. 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_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 messages on behalf of the given queue. If it was the last queue (ref)
-%% on the waiting list, returns message id and excahnge name
-%% and performs the necessary cleanup.
--spec confirm_multiple_msg_ref(msg_id(), queue_name(), queue_ref(), ?MODULE()) ->
- {[{msg_id(), exchange_name()}], [{msg_id(), exchange_name()}], ?MODULE()}.
-confirm_multiple_msg_ref(MsgIds, QueueName, QueueRef,
- #unconfirmed{reverse = Reverse} = UC0) ->
- lists:foldl(
- fun(MsgId, {C, R, UC}) ->
- case remove_msg_ref(confirm, MsgId, QueueName, QueueRef, UC) of
- {{confirmed, V}, UC1} -> {[V | C], R, UC1};
- {{rejected, V}, UC1} -> {C, [V | R], UC1};
- {not_confirmed, UC1} -> {C, R, 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() | 'ignore', 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/backing_queue_SUITE.erl b/test/backing_queue_SUITE.erl
index 2025576a57..ff37e1fb04 100644
--- a/test/backing_queue_SUITE.erl
+++ b/test/backing_queue_SUITE.erl
@@ -684,17 +684,17 @@ bq_variable_queue_delete_msg_store_files_callback1(Config) ->
QPid = amqqueue:get_pid(Q),
Payload = <<0:8388608>>, %% 1MB
Count = 30,
- publish_and_confirm(Q, Payload, Count),
+ QTState = publish_and_confirm(Q, Payload, Count),
rabbit_amqqueue:set_ram_duration_target(QPid, 0),
{ok, Limiter} = rabbit_limiter:start_link(no_id),
CountMinusOne = Count - 1,
- {ok, CountMinusOne, {QName, QPid, _AckTag, false, _Msg}} =
- rabbit_amqqueue:basic_get(Q, self(), true, Limiter,
+ {ok, CountMinusOne, {QName, QPid, _AckTag, false, _Msg}, _} =
+ rabbit_amqqueue:basic_get(Q, true, Limiter,
<<"bq_variable_queue_delete_msg_store_files_callback1">>,
- #{}),
+ QTState),
{ok, CountMinusOne} = rabbit_amqqueue:purge(Q),
%% give the queue a second to receive the close_fds callback msg
@@ -713,8 +713,7 @@ bq_queue_recover1(Config) ->
{new, Q} = rabbit_amqqueue:declare(QName0, true, false, [], none, <<"acting-user">>),
QName = amqqueue:get_name(Q),
QPid = amqqueue:get_pid(Q),
- publish_and_confirm(Q, <<>>, Count),
-
+ QT = publish_and_confirm(Q, <<>>, Count),
SupPid = get_queue_sup_pid(Q),
true = is_pid(SupPid),
exit(SupPid, kill),
@@ -724,7 +723,7 @@ bq_queue_recover1(Config) ->
after 10000 -> exit(timeout_waiting_for_queue_death)
end,
rabbit_amqqueue:stop(?VHOST),
- {Recovered, [], []} = rabbit_amqqueue:recover(?VHOST),
+ {Recovered, []} = rabbit_amqqueue:recover(?VHOST),
rabbit_amqqueue:start(Recovered),
{ok, Limiter} = rabbit_limiter:start_link(no_id),
rabbit_amqqueue:with_or_die(
@@ -732,9 +731,9 @@ bq_queue_recover1(Config) ->
fun (Q1) when ?is_amqqueue(Q1) ->
QPid1 = amqqueue:get_pid(Q1),
CountMinusOne = Count - 1,
- {ok, CountMinusOne, {QName, QPid1, _AckTag, true, _Msg}} =
- rabbit_amqqueue:basic_get(Q1, self(), false, Limiter,
- <<"bq_queue_recover1">>, #{}),
+ {ok, CountMinusOne, {QName, QPid1, _AckTag, true, _Msg}, _} =
+ rabbit_amqqueue:basic_get(Q1, false, Limiter,
+ <<"bq_queue_recover1">>, QT),
exit(QPid1, shutdown),
VQ1 = variable_queue_init(Q, true),
{{_Msg1, true, _AckTag1}, VQ2} =
@@ -1366,25 +1365,34 @@ variable_queue_init(Q, Recover) ->
publish_and_confirm(Q, Payload, Count) ->
Seqs = lists:seq(1, Count),
- [begin
- Msg = rabbit_basic:message(rabbit_misc:r(<<>>, exchange, <<>>),
- <<>>, #'P_basic'{delivery_mode = 2},
- Payload),
- Delivery = #delivery{mandatory = false, sender = self(),
- confirm = true, message = Msg, msg_seq_no = Seq,
- flow = noflow},
- _QPids = rabbit_amqqueue:deliver([Q], Delivery)
- end || Seq <- Seqs],
- wait_for_confirms(gb_sets:from_list(Seqs)).
+ QTState0 = rabbit_queue_type:new(Q, rabbit_queue_type:init()),
+ QTState =
+ lists:foldl(
+ fun (Seq, Acc0) ->
+ Msg = rabbit_basic:message(rabbit_misc:r(<<>>, exchange, <<>>),
+ <<>>, #'P_basic'{delivery_mode = 2},
+ Payload),
+ Delivery = #delivery{mandatory = false, sender = self(),
+ confirm = true, message = Msg, msg_seq_no = Seq,
+ flow = noflow},
+ {ok, Acc, _Actions} = rabbit_queue_type:deliver([Q], Delivery, Acc0),
+ Acc
+ end, QTState0, Seqs),
+ wait_for_confirms(gb_sets:from_list(Seqs)),
+ QTState.
wait_for_confirms(Unconfirmed) ->
case gb_sets:is_empty(Unconfirmed) of
true -> ok;
- false -> receive {'$gen_cast', {confirm, Confirmed, _}} ->
+ false -> receive {'$gen_cast',
+ {queue_event, _QName,
+ {confirm, Confirmed, _}}} ->
wait_for_confirms(
rabbit_misc:gb_sets_difference(
Unconfirmed, gb_sets:from_list(Confirmed)))
- after ?TIMEOUT -> exit(timeout_waiting_for_confirm)
+ after ?TIMEOUT ->
+ flush(),
+ exit(timeout_waiting_for_confirm)
end
end.
@@ -1436,6 +1444,7 @@ variable_queue_publish(IsPersistent, Start, Count, PropFun, PayloadFun, VQ) ->
variable_queue_wait_for_shuffling_end(
lists:foldl(
fun (N, VQN) ->
+
rabbit_variable_queue:publish(
rabbit_basic:message(
rabbit_misc:r(<<>>, exchange, <<>>),
@@ -1526,12 +1535,13 @@ variable_queue_status(VQ) ->
variable_queue_wait_for_shuffling_end(VQ) ->
case credit_flow:blocked() of
false -> VQ;
- true -> receive
- {bump_credit, Msg} ->
- credit_flow:handle_bump_msg(Msg),
- variable_queue_wait_for_shuffling_end(
- rabbit_variable_queue:resume(VQ))
- end
+ true ->
+ receive
+ {bump_credit, Msg} ->
+ credit_flow:handle_bump_msg(Msg),
+ variable_queue_wait_for_shuffling_end(
+ rabbit_variable_queue:resume(VQ))
+ end
end.
msg2int(#basic_message{content = #content{ payload_fragments_rev = P}}) ->
@@ -1576,11 +1586,13 @@ variable_queue_with_holes(VQ0) ->
fun (_, P) -> P end, fun erlang:term_to_binary/1, VQ7),
%% assertions
Status = variable_queue_status(VQ8),
+
vq_with_holes_assertions(VQ8, proplists:get_value(mode, Status)),
Depth = Count + Interval,
Depth = rabbit_variable_queue:depth(VQ8),
Len = Depth - length(Subset3),
Len = rabbit_variable_queue:len(VQ8),
+
{Seq3, Seq -- Seq3, lists:seq(Count + 1, Count + Interval), VQ8}.
vq_with_holes_assertions(VQ, default) ->
@@ -1604,3 +1616,12 @@ check_variable_queue_status(VQ0, Props) ->
S = variable_queue_status(VQ1),
assert_props(S, Props),
VQ1.
+
+flush() ->
+ receive
+ Any ->
+ ct:pal("flush ~p", [Any]),
+ flush()
+ after 0 ->
+ ok
+ end.
diff --git a/test/channel_operation_timeout_SUITE.erl b/test/channel_operation_timeout_SUITE.erl
index f8da35d6ff..15e0188604 100644
--- a/test/channel_operation_timeout_SUITE.erl
+++ b/test/channel_operation_timeout_SUITE.erl
@@ -71,11 +71,13 @@ notify_down_all(Config) ->
RabbitCh = rabbit_ct_client_helpers:open_channel(Config, 0),
HareCh = rabbit_ct_client_helpers:open_channel(Config, 1),
+ ct:pal("one"),
%% success
set_channel_operation_timeout_config(Config, 1000),
configure_bq(Config),
QCfg0 = qconfig(RabbitCh, <<"q0">>, <<"ex0">>, true, false),
declare(QCfg0),
+ ct:pal("two"),
%% Testing rabbit_amqqueue:notify_down_all via rabbit_channel.
%% Consumer count = 0 after correct channel termination and
%% notification of queues via delegate:call/3
@@ -83,6 +85,7 @@ notify_down_all(Config) ->
rabbit_ct_client_helpers:close_channel(RabbitCh),
0 = length(get_consumers(Config, Rabbit, ?DEFAULT_VHOST)),
false = is_process_alive(RabbitCh),
+ ct:pal("three"),
%% fail
set_channel_operation_timeout_config(Config, 10),
diff --git a/test/confirms_rejects_SUITE.erl b/test/confirms_rejects_SUITE.erl
index aaaeb4a939..a51253885c 100644
--- a/test/confirms_rejects_SUITE.erl
+++ b/test/confirms_rejects_SUITE.erl
@@ -388,9 +388,12 @@ kill_the_queue(QueueName) ->
[begin
{ok, Q} = rabbit_amqqueue:lookup({resource, <<"/">>, queue, QueueName}),
Pid = amqqueue:get_pid(Q),
+ ct:pal("~w killed", [Pid]),
+ timer:sleep(1),
exit(Pid, kill)
end
- || _ <- lists:seq(1, 11)],
+ || _ <- lists:seq(1, 50)],
+ timer:sleep(1),
{ok, Q} = rabbit_amqqueue:lookup({resource, <<"/">>, queue, QueueName}),
Pid = amqqueue:get_pid(Q),
case is_process_alive(Pid) of
@@ -399,7 +402,11 @@ kill_the_queue(QueueName) ->
false -> ok
end.
-
-
-
-
+flush() ->
+ receive
+ Any ->
+ ct:pal("flush ~p", [Any]),
+ flush()
+ after 0 ->
+ ok
+ end.
diff --git a/test/dead_lettering_SUITE.erl b/test/dead_lettering_SUITE.erl
index 87b5566c57..4ee917aa21 100644
--- a/test/dead_lettering_SUITE.erl
+++ b/test/dead_lettering_SUITE.erl
@@ -1059,9 +1059,11 @@ dead_letter_headers_BCC(Config) ->
?assertMatch({array, _}, rabbit_misc:table_lookup(Headers3, <<"x-death">>)).
-%% Three top-level headers are added for the very first dead-lettering event. They are
+%% Three top-level headers are added for the very first dead-lettering event.
+%% They are
%% x-first-death-reason, x-first-death-queue, x-first-death-exchange
-%% They have the same values as the reason, queue, and exchange fields of the original
+%% They have the same values as the reason, queue, and exchange fields of the
+%% original
%% dead lettering event. Once added, these headers are never modified.
dead_letter_headers_first_death(Config) ->
{_Conn, Ch} = rabbit_ct_client_helpers:open_connection_and_channel(Config, 0),
diff --git a/test/dynamic_ha_SUITE.erl b/test/dynamic_ha_SUITE.erl
index 25027c7ef9..c881aef8a1 100644
--- a/test/dynamic_ha_SUITE.erl
+++ b/test/dynamic_ha_SUITE.erl
@@ -424,8 +424,7 @@ nodes_policy_should_pick_master_from_its_params(Config) ->
nodename),
Ch = rabbit_ct_client_helpers:open_channel(Config, A),
- ?assertEqual(true, apply_policy_to_declared_queue(Config, Ch, [A],
- [all])),
+ ?assertEqual(true, apply_policy_to_declared_queue(Config, Ch, [A], [all])),
%% --> Master: A
%% Slaves: [B, C] or [C, B]
SSPids = ?awaitMatch(SSPids when is_list(SSPids),
@@ -450,7 +449,7 @@ nodes_policy_should_pick_master_from_its_params(Config) ->
%% should instead use an existing synchronised mirror as the new master,
%% even though that isn't in the policy.
?assertEqual(true, apply_policy_to_declared_queue(Config, Ch, [A],
- [{nodes, [LastSlave, A]}])),
+ [{nodes, [LastSlave, A]}])),
%% --> Master: B or C (same as previous policy)
%% Slaves: [A]
@@ -931,6 +930,7 @@ apply_in_parallel(Config, Nodes, Policies) ->
Self = self(),
[spawn_link(fun() ->
[begin
+
apply_policy(Config, N, Policy)
end || Policy <- Policies],
Self ! parallel_task_done
@@ -969,7 +969,7 @@ wait_for_last_policy(QueueName, NodeA, TestedPolicies, Tries) ->
%% Let's wait a bit longer.
timer:sleep(1000),
wait_for_last_policy(QueueName, NodeA, TestedPolicies, Tries - 1);
- FinalInfo ->
+ {ok, FinalInfo} ->
%% The last policy is the final state
LastPolicy = lists:last(TestedPolicies),
case verify_policy(LastPolicy, FinalInfo) of
diff --git a/test/queue_parallel_SUITE.erl b/test/queue_parallel_SUITE.erl
index c4d16a5900..0fbf7ec975 100644
--- a/test/queue_parallel_SUITE.erl
+++ b/test/queue_parallel_SUITE.erl
@@ -57,7 +57,8 @@ groups() ->
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]}
+ {quorum_queue_in_memory_bytes, [parallel], AllTests ++ [delete_immediately_by_pid_fails]},
+ {stream_queue, [parallel], AllTests}
]}
].
@@ -122,13 +123,24 @@ init_per_group(mirrored_queue, Config) ->
{queue_args, [{<<"x-queue-type">>, longstr, <<"classic">>}]},
{queue_durable, true}]),
rabbit_ct_helpers:run_steps(Config1, []);
+init_per_group(stream_queue, Config) ->
+ case rabbit_ct_broker_helpers:enable_feature_flag(Config, stream_queue) of
+ ok ->
+ rabbit_ct_helpers:set_config(
+ Config,
+ [{queue_args, [{<<"x-queue-type">>, longstr, <<"stream">>}]},
+ {queue_durable, true}]);
+ Skip ->
+ Skip
+ end;
init_per_group(Group, Config0) ->
case lists:member({group, Group}, all()) of
true ->
ClusterSize = 3,
Config = rabbit_ct_helpers:merge_app_env(
Config0, {rabbit, [{channel_tick_interval, 1000},
- {quorum_tick_interval, 1000}]}),
+ {quorum_tick_interval, 1000},
+ {stream_tick_interval, 1000}]}),
Config1 = rabbit_ct_helpers:set_config(
Config, [ {rmq_nodename_suffix, Group},
{rmq_nodes_count, ClusterSize}
@@ -514,6 +526,11 @@ basic_cancel(Config) ->
publish(Ch, QName, [<<"msg1">>]),
wait_for_messages(Config, [[QName, <<"1">>, <<"1">>, <<"0">>]]),
CTag = atom_to_binary(?FUNCTION_NAME, utf8),
+
+ %% Let's set consumer prefetch so it works with stream queues
+ ?assertMatch(#'basic.qos_ok'{},
+ amqp_channel:call(Ch, #'basic.qos'{global = false,
+ prefetch_count = 1})),
subscribe(Ch, QName, false, CTag),
receive
{#'basic.deliver'{delivery_tag = DeliveryTag}, _} ->
diff --git a/test/queue_type_SUITE.erl b/test/queue_type_SUITE.erl
new file mode 100644
index 0000000000..eeeabc3d1e
--- /dev/null
+++ b/test/queue_type_SUITE.erl
@@ -0,0 +1,234 @@
+-module(queue_type_SUITE).
+
+-compile(export_all).
+
+-export([
+ ]).
+
+-include_lib("common_test/include/ct.hrl").
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("amqp_client/include/amqp_client.hrl").
+
+%%%===================================================================
+%%% Common Test callbacks
+%%%===================================================================
+
+all() ->
+ [
+ {group, classic},
+ {group, quorum}
+ ].
+
+
+all_tests() ->
+ [
+ smoke
+ ].
+
+groups() ->
+ [
+ {classic, [], all_tests()},
+ {quorum, [], all_tests()}
+ ].
+
+init_per_suite(Config0) ->
+ rabbit_ct_helpers:log_environment(),
+ Config = rabbit_ct_helpers:merge_app_env(
+ Config0, {rabbit, [{quorum_tick_interval, 1000}]}),
+ rabbit_ct_helpers:run_setup_steps(Config).
+
+end_per_suite(Config) ->
+ rabbit_ct_helpers:run_teardown_steps(Config),
+ ok.
+
+init_per_group(Group, Config) ->
+ ClusterSize = 3,
+ Config1 = rabbit_ct_helpers:set_config(Config,
+ [{rmq_nodes_count, ClusterSize},
+ {rmq_nodename_suffix, Group},
+ {tcp_ports_base}]),
+ Config1b = rabbit_ct_helpers:set_config(Config1,
+ [{queue_type, atom_to_binary(Group, utf8)},
+ {net_ticktime, 10}]),
+ Config2 = rabbit_ct_helpers:run_steps(Config1b,
+ [fun merge_app_env/1 ] ++
+ rabbit_ct_broker_helpers:setup_steps()),
+ Config3 =
+ case rabbit_ct_broker_helpers:enable_feature_flag(Config2, quorum_queue) of
+ ok ->
+ ok = rabbit_ct_broker_helpers:rpc(
+ Config2, 0, application, set_env,
+ [rabbit, channel_tick_interval, 100]),
+ %% HACK: the larger cluster sizes benefit for a bit more time
+ %% after clustering before running the tests.
+ case Group of
+ cluster_size_5 ->
+ timer:sleep(5000),
+ Config2;
+ _ ->
+ Config2
+ end;
+ Skip ->
+ end_per_group(Group, Config2),
+ Skip
+ end,
+ rabbit_ct_broker_helpers:set_policy(
+ Config3, 0,
+ <<"ha-policy">>, <<".*">>, <<"queues">>,
+ [{<<"ha-mode">>, <<"all">>}]),
+ Config3.
+
+merge_app_env(Config) ->
+ rabbit_ct_helpers:merge_app_env(
+ rabbit_ct_helpers:merge_app_env(Config,
+ {rabbit,
+ [{core_metrics_gc_interval, 100},
+ {log, [{file, [{level, debug}]}]}]}),
+ {ra, [{min_wal_roll_over_interval, 30000}]}).
+
+end_per_group(_Group, Config) ->
+ rabbit_ct_helpers:run_steps(Config,
+ rabbit_ct_broker_helpers:teardown_steps()).
+
+init_per_testcase(Testcase, Config) ->
+ Config1 = rabbit_ct_helpers:testcase_started(Config, Testcase),
+ rabbit_ct_broker_helpers:rpc(Config, 0, ?MODULE, delete_queues, []),
+ Q = rabbit_data_coercion:to_binary(Testcase),
+ Config2 = rabbit_ct_helpers:set_config(Config1,
+ [{queue_name, Q},
+ {alt_queue_name, <<Q/binary, "_alt">>}
+ ]),
+ rabbit_ct_helpers:run_steps(Config2,
+ rabbit_ct_client_helpers:setup_steps()).
+
+end_per_testcase(Testcase, Config) ->
+ catch delete_queues(),
+ Config1 = rabbit_ct_helpers:run_steps(
+ Config,
+ rabbit_ct_client_helpers:teardown_steps()),
+ rabbit_ct_helpers:testcase_finished(Config1, Testcase).
+
+%%%===================================================================
+%%% Test cases
+%%%===================================================================
+
+smoke(Config) ->
+ Server = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename),
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
+ QName = ?config(queue_name, Config),
+ ?assertEqual({'queue.declare_ok', QName, 0, 0},
+ declare(Ch, QName, [{<<"x-queue-type">>, longstr,
+ ?config(queue_type, Config)}])),
+ #'confirm.select_ok'{} = amqp_channel:call(Ch, #'confirm.select'{}),
+ amqp_channel:register_confirm_handler(Ch, self()),
+ publish(Ch, QName, <<"msg1">>),
+ ct:pal("waiting for confirms from ~s", [QName]),
+ ok = receive
+ #'basic.ack'{} -> ok;
+ #'basic.nack'{} -> fail
+ after 2500 ->
+ flush(),
+ exit(confirm_timeout)
+ end,
+ DTag = basic_get(Ch, QName),
+
+ basic_ack(Ch, DTag),
+ basic_get_empty(Ch, QName),
+
+ %% consume
+ publish(Ch, QName, <<"msg2">>),
+ ConsumerTag1 = <<"ctag1">>,
+ ok = subscribe(Ch, QName, ConsumerTag1),
+ %% receive and ack
+ receive
+ {#'basic.deliver'{delivery_tag = DeliveryTag,
+ redelivered = false},
+ #amqp_msg{}} ->
+ basic_ack(Ch, DeliveryTag)
+ after 5000 ->
+ flush(),
+ exit(basic_deliver_timeout)
+ end,
+ basic_cancel(Ch, ConsumerTag1),
+
+ %% assert empty
+ basic_get_empty(Ch, QName),
+
+ %% consume and nack
+ ConsumerTag2 = <<"ctag2">>,
+ ok = subscribe(Ch, QName, ConsumerTag2),
+ publish(Ch, QName, <<"msg3">>),
+ receive
+ {#'basic.deliver'{delivery_tag = T,
+ redelivered = false},
+ #amqp_msg{}} ->
+ basic_cancel(Ch, ConsumerTag2),
+ basic_nack(Ch, T)
+ after 5000 ->
+ exit(basic_deliver_timeout)
+ end,
+ %% get and ack
+ basic_ack(Ch, basic_get(Ch, QName)),
+ ok.
+
+%% Utility
+delete_queues() ->
+ [rabbit_amqqueue:delete(Q, false, false, <<"dummy">>)
+ || Q <- rabbit_amqqueue:list()].
+
+declare(Ch, Q, Args) ->
+ amqp_channel:call(Ch, #'queue.declare'{queue = Q,
+ durable = true,
+ auto_delete = false,
+ arguments = Args}).
+
+publish(Ch, Queue, Msg) ->
+ ok = amqp_channel:cast(Ch,
+ #'basic.publish'{routing_key = Queue},
+ #amqp_msg{props = #'P_basic'{delivery_mode = 2},
+ payload = Msg}).
+
+basic_get(Ch, Queue) ->
+ {GetOk, _} = Reply = amqp_channel:call(Ch, #'basic.get'{queue = Queue,
+ no_ack = false}),
+ ?assertMatch({#'basic.get_ok'{}, #amqp_msg{}}, Reply),
+ GetOk#'basic.get_ok'.delivery_tag.
+
+basic_get_empty(Ch, Queue) ->
+ ?assertMatch(#'basic.get_empty'{},
+ amqp_channel:call(Ch, #'basic.get'{queue = Queue,
+ no_ack = false})).
+
+subscribe(Ch, Queue, CTag) ->
+ amqp_channel:subscribe(Ch, #'basic.consume'{queue = Queue,
+ no_ack = false,
+ consumer_tag = CTag},
+ self()),
+ receive
+ #'basic.consume_ok'{consumer_tag = CTag} ->
+ ok
+ after 5000 ->
+ exit(basic_consume_timeout)
+ end.
+
+basic_ack(Ch, DTag) ->
+ amqp_channel:cast(Ch, #'basic.ack'{delivery_tag = DTag,
+ multiple = false}).
+
+basic_cancel(Ch, CTag) ->
+ #'basic.cancel_ok'{} =
+ amqp_channel:call(Ch, #'basic.cancel'{consumer_tag = CTag}).
+
+basic_nack(Ch, DTag) ->
+ amqp_channel:cast(Ch, #'basic.nack'{delivery_tag = DTag,
+ requeue = true,
+ multiple = false}).
+
+flush() ->
+ receive
+ Any ->
+ ct:pal("flush ~p", [Any]),
+ flush()
+ after 0 ->
+ ok
+ end.
diff --git a/test/quorum_queue_SUITE.erl b/test/quorum_queue_SUITE.erl
index ecb4fdac63..16042b71e8 100644
--- a/test/quorum_queue_SUITE.erl
+++ b/test/quorum_queue_SUITE.erl
@@ -383,13 +383,19 @@ start_queue(Config) ->
Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
LQ = ?config(queue_name, Config),
+ %% The stream coordinator is also a ra process, we need to ensure the quorum tests
+ %% are not affected by any other ra cluster that could be added in the future
+ Children = length(rpc:call(Server, supervisor, which_children, [ra_server_sup_sup])),
+
?assertEqual({'queue.declare_ok', LQ, 0, 0},
declare(Ch, LQ, [{<<"x-queue-type">>, longstr, <<"quorum">>}])),
%% Check that the application and one ra node are up
?assertMatch({ra, _, _}, lists:keyfind(ra, 1,
rpc:call(Server, application, which_applications, []))),
- ?assertMatch([_], rpc:call(Server, supervisor, which_children, [ra_server_sup_sup])),
+ Expected = Children + 1,
+ ?assertMatch(Expected,
+ length(rpc:call(Server, supervisor, which_children, [ra_server_sup_sup]))),
%% Test declare an existing queue
?assertEqual({'queue.declare_ok', LQ, 0, 0},
@@ -405,7 +411,8 @@ start_queue(Config) ->
%% Check that the application and process are still up
?assertMatch({ra, _, _}, lists:keyfind(ra, 1,
rpc:call(Server, application, which_applications, []))),
- ?assertMatch([_], rpc:call(Server, supervisor, which_children, [ra_server_sup_sup])).
+ ?assertMatch(Expected,
+ length(rpc:call(Server, supervisor, which_children, [ra_server_sup_sup]))).
start_queue_concurrent(Config) ->
Servers = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
@@ -463,6 +470,10 @@ quorum_cluster_size_x(Config, Max, Expected) ->
stop_queue(Config) ->
Server = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename),
+ %% The stream coordinator is also a ra process, we need to ensure the quorum tests
+ %% are not affected by any other ra cluster that could be added in the future
+ Children = length(rpc:call(Server, supervisor, which_children, [ra_server_sup_sup])),
+
Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
LQ = ?config(queue_name, Config),
?assertEqual({'queue.declare_ok', LQ, 0, 0},
@@ -471,13 +482,15 @@ stop_queue(Config) ->
%% Check that the application and one ra node are up
?assertMatch({ra, _, _}, lists:keyfind(ra, 1,
rpc:call(Server, application, which_applications, []))),
- ?assertMatch([_], rpc:call(Server, supervisor, which_children, [ra_server_sup_sup])),
+ Expected = Children + 1,
+ ?assertMatch(Expected,
+ length(rpc:call(Server, supervisor, which_children, [ra_server_sup_sup]))),
%% Delete the quorum queue
?assertMatch(#'queue.delete_ok'{}, amqp_channel:call(Ch, #'queue.delete'{queue = LQ})),
%% Check that the application and process are down
wait_until(fun() ->
- [] == rpc:call(Server, supervisor, which_children, [ra_server_sup_sup])
+ Children == length(rpc:call(Server, supervisor, which_children, [ra_server_sup_sup]))
end),
?assertMatch({ra, _, _}, lists:keyfind(ra, 1,
rpc:call(Server, application, which_applications, []))).
@@ -485,6 +498,10 @@ stop_queue(Config) ->
restart_queue(Config) ->
Server = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename),
+ %% The stream coordinator is also a ra process, we need to ensure the quorum tests
+ %% are not affected by any other ra cluster that could be added in the future
+ Children = length(rpc:call(Server, supervisor, which_children, [ra_server_sup_sup])),
+
Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
LQ = ?config(queue_name, Config),
?assertEqual({'queue.declare_ok', LQ, 0, 0},
@@ -496,7 +513,9 @@ restart_queue(Config) ->
%% Check that the application and one ra node are up
?assertMatch({ra, _, _}, lists:keyfind(ra, 1,
rpc:call(Server, application, which_applications, []))),
- ?assertMatch([_], rpc:call(Server, supervisor, which_children, [ra_server_sup_sup])).
+ Expected = Children + 1,
+ ?assertMatch(Expected,
+ length(rpc:call(Server, supervisor, which_children, [ra_server_sup_sup]))).
idempotent_recover(Config) ->
Server = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename),
@@ -554,6 +573,10 @@ restart_all_types(Config) ->
%% ensure there are no regressions
Server = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename),
+ %% The stream coordinator is also a ra process, we need to ensure the quorum tests
+ %% are not affected by any other ra cluster that could be added in the future
+ Children = rpc:call(Server, supervisor, which_children, [ra_server_sup_sup]),
+
Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
QQ1 = <<"restart_all_types-qq1">>,
?assertEqual({'queue.declare_ok', QQ1, 0, 0},
@@ -575,7 +598,9 @@ restart_all_types(Config) ->
%% Check that the application and two ra nodes are up
?assertMatch({ra, _, _}, lists:keyfind(ra, 1,
rpc:call(Server, application, which_applications, []))),
- ?assertMatch([_,_], rpc:call(Server, supervisor, which_children, [ra_server_sup_sup])),
+ Expected = length(Children) + 2,
+ Got = length(rpc:call(Server, supervisor, which_children, [ra_server_sup_sup])),
+ ?assertMatch(Expected, Got),
%% Check the classic queues restarted correctly
Ch2 = rabbit_ct_client_helpers:open_channel(Config, Server),
{#'basic.get_ok'{}, #amqp_msg{}} =
@@ -592,6 +617,10 @@ stop_start_rabbit_app(Config) ->
%% classic) to ensure there are no regressions
Server = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename),
+ %% The stream coordinator is also a ra process, we need to ensure the quorum tests
+ %% are not affected by any other ra cluster that could be added in the future
+ Children = length(rpc:call(Server, supervisor, which_children, [ra_server_sup_sup])),
+
Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
QQ1 = <<"stop_start_rabbit_app-qq">>,
?assertEqual({'queue.declare_ok', QQ1, 0, 0},
@@ -617,7 +646,9 @@ stop_start_rabbit_app(Config) ->
%% Check that the application and two ra nodes are up
?assertMatch({ra, _, _}, lists:keyfind(ra, 1,
rpc:call(Server, application, which_applications, []))),
- ?assertMatch([_,_], rpc:call(Server, supervisor, which_children, [ra_server_sup_sup])),
+ Expected = Children + 2,
+ ?assertMatch(Expected,
+ length(rpc:call(Server, supervisor, which_children, [ra_server_sup_sup]))),
%% Check the classic queues restarted correctly
Ch2 = rabbit_ct_client_helpers:open_channel(Config, Server),
{#'basic.get_ok'{}, #amqp_msg{}} =
@@ -935,6 +966,10 @@ cleanup_queue_state_on_channel_after_publish(Config) ->
%% to verify that the cleanup is propagated through channels
[Server | _] = Servers = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+ %% The stream coordinator is also a ra process, we need to ensure the quorum tests
+ %% are not affected by any other ra cluster that could be added in the future
+ Children = length(rpc:call(Server, supervisor, which_children, [ra_server_sup_sup])),
+
Ch1 = rabbit_ct_client_helpers:open_channel(Config, Server),
Ch2 = rabbit_ct_client_helpers:open_channel(Config, Server),
QQ = ?config(queue_name, Config),
@@ -955,18 +990,22 @@ cleanup_queue_state_on_channel_after_publish(Config) ->
?assertMatch(#'queue.delete_ok'{},
amqp_channel:call(Ch1, #'queue.delete'{queue = QQ})),
wait_until(fun() ->
- [] == rpc:call(Server, supervisor, which_children,
- [ra_server_sup_sup])
+ Children == length(rpc:call(Server, supervisor, which_children,
+ [ra_server_sup_sup]))
end),
%% Check that all queue states have been cleaned
- wait_for_cleanup(Server, NCh1, 0),
- wait_for_cleanup(Server, NCh2, 0).
+ wait_for_cleanup(Server, NCh2, 0),
+ wait_for_cleanup(Server, NCh1, 0).
cleanup_queue_state_on_channel_after_subscribe(Config) ->
%% Declare/delete the queue and publish in one channel, while consuming on a
%% different one to verify that the cleanup is propagated through channels
[Server | _] = Servers = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+ %% The stream coordinator is also a ra process, we need to ensure the quorum tests
+ %% are not affected by any other ra cluster that could be added in the future
+ Children = length(rpc:call(Server, supervisor, which_children, [ra_server_sup_sup])),
+
Ch1 = rabbit_ct_client_helpers:open_channel(Config, Server),
Ch2 = rabbit_ct_client_helpers:open_channel(Config, Server),
QQ = ?config(queue_name, Config),
@@ -993,7 +1032,7 @@ cleanup_queue_state_on_channel_after_subscribe(Config) ->
wait_for_cleanup(Server, NCh2, 1),
?assertMatch(#'queue.delete_ok'{}, amqp_channel:call(Ch1, #'queue.delete'{queue = QQ})),
wait_until(fun() ->
- [] == rpc:call(Server, supervisor, which_children, [ra_server_sup_sup])
+ Children == length(rpc:call(Server, supervisor, which_children, [ra_server_sup_sup]))
end),
%% Check that all queue states have been cleaned
wait_for_cleanup(Server, NCh1, 0),
@@ -1596,8 +1635,8 @@ cleanup_data_dir(Config) ->
declare(Ch, QQ, [{<<"x-queue-type">>, longstr, <<"quorum">>}])),
timer:sleep(100),
- [{_, UId1}] = rpc:call(Server1, ra_directory, list_registered, []),
- [{_, UId2}] = rpc:call(Server2, ra_directory, list_registered, []),
+ UId1 = proplists:get_value(ra_name(QQ), rpc:call(Server1, ra_directory, list_registered, [])),
+ UId2 = proplists:get_value(ra_name(QQ), rpc:call(Server2, ra_directory, list_registered, [])),
DataDir1 = rpc:call(Server1, ra_env, server_data_dir, [UId1]),
DataDir2 = rpc:call(Server2, ra_env, server_data_dir, [UId2]),
?assert(filelib:is_dir(DataDir1)),
@@ -1748,6 +1787,11 @@ reconnect_consumer_and_wait_channel_down(Config) ->
delete_immediately_by_resource(Config) ->
Server = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename),
Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
+
+ %% The stream coordinator is also a ra process, we need to ensure the quorum tests
+ %% are not affected by any other ra cluster that could be added in the future
+ Children = length(rpc:call(Server, supervisor, which_children, [ra_server_sup_sup])),
+
QQ = ?config(queue_name, Config),
?assertEqual({'queue.declare_ok', QQ, 0, 0},
declare(Ch, QQ, [{<<"x-queue-type">>, longstr, <<"quorum">>}])),
@@ -1756,7 +1800,7 @@ delete_immediately_by_resource(Config) ->
%% Check that the application and process are down
wait_until(fun() ->
- [] == rpc:call(Server, supervisor, which_children, [ra_server_sup_sup])
+ Children == length(rpc:call(Server, supervisor, which_children, [ra_server_sup_sup]))
end),
?assertMatch({ra, _, _}, lists:keyfind(ra, 1,
rpc:call(Server, application, which_applications, []))).
@@ -1784,6 +1828,8 @@ subscribe_redelivery_count(Config) ->
amqp_channel:cast(Ch, #'basic.nack'{delivery_tag = DeliveryTag,
multiple = false,
requeue = true})
+ after 5000 ->
+ exit(basic_deliver_timeout)
end,
receive
@@ -1794,6 +1840,8 @@ subscribe_redelivery_count(Config) ->
amqp_channel:cast(Ch, #'basic.nack'{delivery_tag = DeliveryTag1,
multiple = false,
requeue = true})
+ after 5000 ->
+ exit(basic_deliver_timeout_2)
end,
receive
@@ -1803,8 +1851,13 @@ subscribe_redelivery_count(Config) ->
?assertMatch({DCHeader, _, 2}, rabbit_basic:header(DCHeader, H2)),
amqp_channel:cast(Ch, #'basic.ack'{delivery_tag = DeliveryTag2,
multiple = false}),
+ ct:pal("wait_for_messages_ready", []),
wait_for_messages_ready(Servers, RaName, 0),
+ ct:pal("wait_for_messages_pending_ack", []),
wait_for_messages_pending_ack(Servers, RaName, 0)
+ after 5000 ->
+ flush(500),
+ exit(basic_deliver_timeout_3)
end.
subscribe_redelivery_limit(Config) ->
diff --git a/test/quorum_queue_utils.erl b/test/quorum_queue_utils.erl
index 95ddc892f1..caabd617ae 100644
--- a/test/quorum_queue_utils.erl
+++ b/test/quorum_queue_utils.erl
@@ -28,15 +28,10 @@ wait_for_messages_total(Servers, QName, Total) ->
wait_for_messages(Servers, QName, Number, Fun, 0) ->
Msgs = dirty_query(Servers, QName, Fun),
- Totals = lists:map(fun(M) when is_map(M) ->
- maps:size(M);
- (_) ->
- -1
- end, Msgs),
- ?assertEqual(Totals, [Number || _ <- lists:seq(1, length(Servers))]);
+ ?assertEqual(Msgs, [Number || _ <- lists:seq(1, length(Servers))]);
wait_for_messages(Servers, QName, Number, Fun, N) ->
Msgs = dirty_query(Servers, QName, Fun),
- ct:pal("Got messages ~p", [Msgs]),
+ ct:pal("Got messages ~p ~p", [QName, Msgs]),
%% hack to allow the check to succeed in mixed versions clusters if at
%% least one node matches the criteria rather than all nodes for
F = case is_mixed_versions() of
diff --git a/test/rabbit_confirms_SUITE.erl b/test/rabbit_confirms_SUITE.erl
new file mode 100644
index 0000000000..331c3ca7c3
--- /dev/null
+++ b/test/rabbit_confirms_SUITE.erl
@@ -0,0 +1,154 @@
+-module(rabbit_confirms_SUITE).
+
+-compile(export_all).
+
+-export([
+ ]).
+
+-include_lib("common_test/include/ct.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+%%%===================================================================
+%%% Common Test callbacks
+%%%===================================================================
+
+all() ->
+ [
+ {group, tests}
+ ].
+
+
+all_tests() ->
+ [
+ confirm,
+ reject,
+ remove_queue
+ ].
+
+groups() ->
+ [
+ {tests, [], all_tests()}
+ ].
+
+init_per_suite(Config) ->
+ Config.
+
+end_per_suite(_Config) ->
+ ok.
+
+init_per_group(_Group, Config) ->
+ Config.
+
+end_per_group(_Group, _Config) ->
+ ok.
+
+init_per_testcase(_TestCase, Config) ->
+ Config.
+
+end_per_testcase(_TestCase, _Config) ->
+ ok.
+
+%%%===================================================================
+%%% Test cases
+%%%===================================================================
+
+confirm(_Config) ->
+ XName = rabbit_misc:r(<<"/">>, exchange, <<"X">>),
+ QName = rabbit_misc:r(<<"/">>, queue, <<"Q">>),
+ QName2 = rabbit_misc:r(<<"/">>, queue, <<"Q2">>),
+ U0 = rabbit_confirms:init(),
+ ?assertEqual(0, rabbit_confirms:size(U0)),
+ ?assertEqual(undefined, rabbit_confirms:smallest(U0)),
+ ?assertEqual(true, rabbit_confirms:is_empty(U0)),
+
+ U1 = rabbit_confirms:insert(1, [QName], XName, U0),
+ ?assertEqual(1, rabbit_confirms:size(U1)),
+ ?assertEqual(1, rabbit_confirms:smallest(U1)),
+ ?assertEqual(false, rabbit_confirms:is_empty(U1)),
+
+ {[{1, XName}], U2} = rabbit_confirms:confirm([1], QName, U1),
+ ?assertEqual(0, rabbit_confirms:size(U2)),
+ ?assertEqual(undefined, rabbit_confirms:smallest(U2)),
+ ?assertEqual(true, rabbit_confirms:is_empty(U2)),
+
+ U3 = rabbit_confirms:insert(2, [QName], XName, U1),
+ ?assertEqual(2, rabbit_confirms:size(U3)),
+ ?assertEqual(1, rabbit_confirms:smallest(U3)),
+ ?assertEqual(false, rabbit_confirms:is_empty(U3)),
+
+ {[{1, XName}], U4} = rabbit_confirms:confirm([1], QName, U3),
+ ?assertEqual(1, rabbit_confirms:size(U4)),
+ ?assertEqual(2, rabbit_confirms:smallest(U4)),
+ ?assertEqual(false, rabbit_confirms:is_empty(U4)),
+
+ U5 = rabbit_confirms:insert(2, [QName, QName2], XName, U1),
+ ?assertEqual(2, rabbit_confirms:size(U5)),
+ ?assertEqual(1, rabbit_confirms:smallest(U5)),
+ ?assertEqual(false, rabbit_confirms:is_empty(U5)),
+
+ {[{1, XName}], U6} = rabbit_confirms:confirm([1, 2], QName, U5),
+ ?assertEqual(2, rabbit_confirms:smallest(U6)),
+
+ {[{2, XName}], U7} = rabbit_confirms:confirm([2], QName2, U6),
+ ?assertEqual(0, rabbit_confirms:size(U7)),
+ ?assertEqual(undefined, rabbit_confirms:smallest(U7)),
+
+
+ U8 = rabbit_confirms:insert(2, [QName], XName, U1),
+ {[{1, XName}, {2, XName}], _U9} = rabbit_confirms:confirm([1, 2], QName, U8),
+ ok.
+
+
+reject(_Config) ->
+ XName = rabbit_misc:r(<<"/">>, exchange, <<"X">>),
+ QName = rabbit_misc:r(<<"/">>, queue, <<"Q">>),
+ QName2 = rabbit_misc:r(<<"/">>, queue, <<"Q2">>),
+ U0 = rabbit_confirms:init(),
+ ?assertEqual(0, rabbit_confirms:size(U0)),
+ ?assertEqual(undefined, rabbit_confirms:smallest(U0)),
+ ?assertEqual(true, rabbit_confirms:is_empty(U0)),
+
+ U1 = rabbit_confirms:insert(1, [QName], XName, U0),
+
+ {ok, {1, XName}, U2} = rabbit_confirms:reject(1, U1),
+ {error, not_found} = rabbit_confirms:reject(1, U2),
+ ?assertEqual(0, rabbit_confirms:size(U2)),
+ ?assertEqual(undefined, rabbit_confirms:smallest(U2)),
+
+ U3 = rabbit_confirms:insert(2, [QName, QName2], XName, U1),
+
+ {ok, {1, XName}, U4} = rabbit_confirms:reject(1, U3),
+ {error, not_found} = rabbit_confirms:reject(1, U4),
+ ?assertEqual(1, rabbit_confirms:size(U4)),
+ ?assertEqual(2, rabbit_confirms:smallest(U4)),
+
+ {ok, {2, XName}, U5} = rabbit_confirms:reject(2, U3),
+ {error, not_found} = rabbit_confirms:reject(2, U5),
+ ?assertEqual(1, rabbit_confirms:size(U5)),
+ ?assertEqual(1, rabbit_confirms:smallest(U5)),
+
+ ok.
+
+remove_queue(_Config) ->
+ XName = rabbit_misc:r(<<"/">>, exchange, <<"X">>),
+ QName = rabbit_misc:r(<<"/">>, queue, <<"Q">>),
+ QName2 = rabbit_misc:r(<<"/">>, queue, <<"Q2">>),
+ U0 = rabbit_confirms:init(),
+
+ U1 = rabbit_confirms:insert(1, [QName, QName2], XName, U0),
+ U2 = rabbit_confirms:insert(2, [QName2], XName, U1),
+ {[{2, XName}], U3} = rabbit_confirms:remove_queue(QName2, U2),
+ ?assertEqual(1, rabbit_confirms:size(U3)),
+ ?assertEqual(1, rabbit_confirms:smallest(U3)),
+ {[{1, XName}], U4} = rabbit_confirms:remove_queue(QName, U3),
+ ?assertEqual(0, rabbit_confirms:size(U4)),
+ ?assertEqual(undefined, rabbit_confirms:smallest(U4)),
+
+ U5 = rabbit_confirms:insert(1, [QName], XName, U0),
+ U6 = rabbit_confirms:insert(2, [QName], XName, U5),
+ {[{1, XName}, {2, XName}], _U} = rabbit_confirms:remove_queue(QName, U6),
+
+ ok.
+
+
+%% Utility
diff --git a/test/rabbit_fifo_SUITE.erl b/test/rabbit_fifo_SUITE.erl
index d19dcb3682..7b90d91bfa 100644
--- a/test/rabbit_fifo_SUITE.erl
+++ b/test/rabbit_fifo_SUITE.erl
@@ -674,7 +674,7 @@ single_active_consumer_basic_get_test(_) ->
?assertEqual(single_active, State0#rabbit_fifo.cfg#cfg.consumer_strategy),
?assertEqual(0, map_size(State0#rabbit_fifo.consumers)),
{State1, _} = enq(1, 1, first, State0),
- {_State, {error, unsupported}} =
+ {_State, {error, {unsupported, single_active_consumer}}} =
apply(meta(2), rabbit_fifo:make_checkout(Cid, {dequeue, unsettled}, #{}),
State1),
ok.
diff --git a/test/rabbit_fifo_int_SUITE.erl b/test/rabbit_fifo_int_SUITE.erl
index b51975b062..b2ed7160a2 100644
--- a/test/rabbit_fifo_int_SUITE.erl
+++ b/test/rabbit_fifo_int_SUITE.erl
@@ -86,7 +86,7 @@ basics(Config) ->
CustomerTag = UId,
ok = start_cluster(ClusterName, [ServerId]),
FState0 = rabbit_fifo_client:init(ClusterName, [ServerId]),
- {ok, FState1} = rabbit_fifo_client:checkout(CustomerTag, 1, undefined, FState0),
+ {ok, FState1} = rabbit_fifo_client:checkout(CustomerTag, 1, #{}, FState0),
ra_log_wal:force_roll_over(ra_log_wal),
% create segment the segment will trigger a snapshot
@@ -99,11 +99,10 @@ basics(Config) ->
FState5 = receive
{ra_event, From, Evt} ->
case rabbit_fifo_client:handle_ra_event(From, Evt, FState3) of
- {internal, _AcceptedSeqs, _Actions, _FState4} ->
- exit(unexpected_internal_event);
- {{delivery, C, [{MsgId, _Msg}]}, FState4} ->
- {ok, S} = rabbit_fifo_client:settle(C, [MsgId],
- FState4),
+ {ok, FState4,
+ [{deliver, C, true,
+ [{_Qname, _QRef, MsgId, _SomBool, _Msg}]}]} ->
+ {S, _A} = rabbit_fifo_client:settle(C, [MsgId], FState4),
S
end
after 5000 ->
@@ -129,10 +128,9 @@ basics(Config) ->
receive
{ra_event, Frm, E} ->
case rabbit_fifo_client:handle_ra_event(Frm, E, FState6b) of
- {internal, _, _, _FState7} ->
- exit({unexpected_internal_event, E});
- {{delivery, Ctag, [{Mid, {_, two}}]}, FState7} ->
- {ok, _S} = rabbit_fifo_client:return(Ctag, [Mid], FState7),
+ {ok, FState7, [{deliver, Ctag, true,
+ [{_, _, Mid, _, two}]}]} ->
+ {_, _} = rabbit_fifo_client:return(Ctag, [Mid], FState7),
ok
end
after 2000 ->
@@ -150,8 +148,8 @@ return(Config) ->
{ok, F0} = rabbit_fifo_client:enqueue(1, msg1, F00),
{ok, F1} = rabbit_fifo_client:enqueue(2, msg2, F0),
{_, _, F2} = process_ra_events(receive_ra_events(2, 0), F1),
- {ok, {{MsgId, _}, _}, F} = rabbit_fifo_client:dequeue(<<"tag">>, unsettled, F2),
- {ok, _F2} = rabbit_fifo_client:return(<<"tag">>, [MsgId], F),
+ {ok, _, {_, _, MsgId, _, _}, F} = rabbit_fifo_client:dequeue(<<"tag">>, unsettled, F2),
+ _F2 = rabbit_fifo_client:return(<<"tag">>, [MsgId], F),
ra:stop_server(ServerId),
ok.
@@ -165,9 +163,9 @@ rabbit_fifo_returns_correlation(Config) ->
receive
{ra_event, Frm, E} ->
case rabbit_fifo_client:handle_ra_event(Frm, E, F1) of
- {internal, [corr1], [], _F2} ->
+ {ok, _F2, [{settled, _, _}]} ->
ok;
- {Del, _} ->
+ Del ->
exit({unexpected, Del})
end
after 2000 ->
@@ -181,23 +179,24 @@ duplicate_delivery(Config) ->
ServerId = ?config(node_id, Config),
ok = start_cluster(ClusterName, [ServerId]),
F0 = rabbit_fifo_client:init(ClusterName, [ServerId]),
- {ok, F1} = rabbit_fifo_client:checkout(<<"tag">>, 10, undefined, F0),
+ {ok, F1} = rabbit_fifo_client:checkout(<<"tag">>, 10, #{}, F0),
{ok, F2} = rabbit_fifo_client:enqueue(corr1, msg1, F1),
Fun = fun Loop(S0) ->
receive
{ra_event, Frm, E} = Evt ->
case rabbit_fifo_client:handle_ra_event(Frm, E, S0) of
- {internal, [corr1], [], S1} ->
+ {ok, S1, [{settled, _, _}]} ->
Loop(S1);
- {_Del, S1} ->
+ {ok, S1, _} ->
%% repeat event delivery
self() ! Evt,
%% check that then next received delivery doesn't
%% repeat or crash
receive
{ra_event, F, E1} ->
- case rabbit_fifo_client:handle_ra_event(F, E1, S1) of
- {internal, [], [], S2} ->
+ case rabbit_fifo_client:handle_ra_event(
+ F, E1, S1) of
+ {ok, S2, _} ->
S2
end
end
@@ -215,7 +214,7 @@ usage(Config) ->
ServerId = ?config(node_id, Config),
ok = start_cluster(ClusterName, [ServerId]),
F0 = rabbit_fifo_client:init(ClusterName, [ServerId]),
- {ok, F1} = rabbit_fifo_client:checkout(<<"tag">>, 10, undefined, F0),
+ {ok, F1} = rabbit_fifo_client:checkout(<<"tag">>, 10, #{}, F0),
{ok, F2} = rabbit_fifo_client:enqueue(corr1, msg1, F1),
{ok, F3} = rabbit_fifo_client:enqueue(corr2, msg2, F2),
{_, _, _} = process_ra_events(receive_ra_events(2, 2), F3),
@@ -242,9 +241,9 @@ resends_lost_command(Config) ->
meck:unload(ra),
{ok, F3} = rabbit_fifo_client:enqueue(msg3, F2),
{_, _, F4} = process_ra_events(receive_ra_events(2, 0), F3),
- {ok, {{_, {_, msg1}}, _}, F5} = rabbit_fifo_client:dequeue(<<"tag">>, settled, F4),
- {ok, {{_, {_, msg2}}, _}, F6} = rabbit_fifo_client:dequeue(<<"tag">>, settled, F5),
- {ok, {{_, {_, msg3}}, _}, _F7} = rabbit_fifo_client:dequeue(<<"tag">>, settled, F6),
+ {ok, _, {_, _, _, _, msg1}, F5} = rabbit_fifo_client:dequeue(<<"tag">>, settled, F4),
+ {ok, _, {_, _, _, _, msg2}, F6} = rabbit_fifo_client:dequeue(<<"tag">>, settled, F5),
+ {ok, _, {_, _, _, _, msg3}, _F7} = rabbit_fifo_client:dequeue(<<"tag">>, settled, F6),
ra:stop_server(ServerId),
ok.
@@ -268,7 +267,7 @@ detects_lost_delivery(Config) ->
F000 = rabbit_fifo_client:init(ClusterName, [ServerId]),
{ok, F00} = rabbit_fifo_client:enqueue(msg1, F000),
{_, _, F0} = process_ra_events(receive_ra_events(1, 0), F00),
- {ok, F1} = rabbit_fifo_client:checkout(<<"tag">>, 10, undefined, F0),
+ {ok, F1} = rabbit_fifo_client:checkout(<<"tag">>, 10, #{}, F0),
{ok, F2} = rabbit_fifo_client:enqueue(msg2, F1),
{ok, F3} = rabbit_fifo_client:enqueue(msg3, F2),
% lose first delivery
@@ -298,13 +297,13 @@ returns_after_down(Config) ->
_Pid = spawn(fun () ->
F = rabbit_fifo_client:init(ClusterName, [ServerId]),
{ok, _} = rabbit_fifo_client:checkout(<<"tag">>, 10,
- undefined, F),
+ #{}, F),
Self ! checkout_done
end),
receive checkout_done -> ok after 1000 -> exit(checkout_done_timeout) end,
timer:sleep(1000),
% message should be available for dequeue
- {ok, {{_, {_, msg1}}, _}, _} = rabbit_fifo_client:dequeue(<<"tag">>, settled, F2),
+ {ok, _, {_, _, _, _, msg1}, _} = rabbit_fifo_client:dequeue(<<"tag">>, settled, F2),
ra:stop_server(ServerId),
ok.
@@ -327,9 +326,9 @@ resends_after_lost_applied(Config) ->
% send another message
{ok, F4} = rabbit_fifo_client:enqueue(msg3, F3),
{_, _, F5} = process_ra_events(receive_ra_events(1, 0), F4),
- {ok, {{_, {_, msg1}}, _}, F6} = rabbit_fifo_client:dequeue(<<"tag">>, settled, F5),
- {ok, {{_, {_, msg2}}, _}, F7} = rabbit_fifo_client:dequeue(<<"tag">>, settled, F6),
- {ok, {{_, {_, msg3}}, _}, _F8} = rabbit_fifo_client:dequeue(<<"tag">>, settled, F7),
+ {ok, _, {_, _, _, _, msg1}, F6} = rabbit_fifo_client:dequeue(<<"tag">>, settled, F5),
+ {ok, _, {_, _, _, _, msg2}, F7} = rabbit_fifo_client:dequeue(<<"tag">>, settled, F6),
+ {ok, _, {_, _, _, _, msg3}, _F8} = rabbit_fifo_client:dequeue(<<"tag">>, settled, F7),
ra:stop_server(ServerId),
ok.
@@ -377,15 +376,16 @@ discard(Config) ->
_ = ra:members(ServerId),
F0 = rabbit_fifo_client:init(ClusterName, [ServerId]),
- {ok, F1} = rabbit_fifo_client:checkout(<<"tag">>, 10, undefined, F0),
+ {ok, F1} = rabbit_fifo_client:checkout(<<"tag">>, 10, #{}, F0),
{ok, F2} = rabbit_fifo_client:enqueue(msg1, F1),
- F3 = discard_next_delivery(F2, 500),
- {ok, empty, _F4} = rabbit_fifo_client:dequeue(<<"tag1">>, settled, F3),
+ F3 = discard_next_delivery(F2, 5000),
+ {empty, _F4} = rabbit_fifo_client:dequeue(<<"tag1">>, settled, F3),
receive
{dead_letter, Letters} ->
[{_, msg1}] = Letters,
ok
after 500 ->
+ flush(),
exit(dead_letter_timeout)
end,
ra:stop_server(ServerId),
@@ -397,11 +397,11 @@ cancel_checkout(Config) ->
ok = start_cluster(ClusterName, [ServerId]),
F0 = rabbit_fifo_client:init(ClusterName, [ServerId], 4),
{ok, F1} = rabbit_fifo_client:enqueue(m1, F0),
- {ok, F2} = rabbit_fifo_client:checkout(<<"tag">>, 10, undefined, F1),
+ {ok, F2} = rabbit_fifo_client:checkout(<<"tag">>, 10, #{}, F1),
{_, _, F3} = process_ra_events(receive_ra_events(1, 1), F2, [], [], fun (_, S) -> S end),
{ok, F4} = rabbit_fifo_client:cancel_checkout(<<"tag">>, F3),
- {ok, F5} = rabbit_fifo_client:return(<<"tag">>, [0], F4),
- {ok, {{_, {_, m1}}, _}, _} = rabbit_fifo_client:dequeue(<<"d1">>, settled, F5),
+ {F5, _} = rabbit_fifo_client:return(<<"tag">>, [0], F4),
+ {ok, _, {_, _, _, _, m1}, F5} = rabbit_fifo_client:dequeue(<<"d1">>, settled, F5),
ok.
credit(Config) ->
@@ -413,20 +413,20 @@ credit(Config) ->
{ok, F2} = rabbit_fifo_client:enqueue(m2, F1),
{_, _, F3} = process_ra_events(receive_ra_events(2, 0), F2),
%% checkout with 0 prefetch
- {ok, F4} = rabbit_fifo_client:checkout(<<"tag">>, 0, credited, undefined, F3),
+ {ok, F4} = rabbit_fifo_client:checkout(<<"tag">>, 0, credited, #{}, F3),
%% assert no deliveries
{_, _, F5} = process_ra_events(receive_ra_events(), F4, [], [],
fun
(D, _) -> error({unexpected_delivery, D})
end),
%% provide some credit
- {ok, F6} = rabbit_fifo_client:credit(<<"tag">>, 1, false, F5),
- {[{_, {_, m1}}], [{send_credit_reply, _}], F7} =
+ F6 = rabbit_fifo_client:credit(<<"tag">>, 1, false, F5),
+ {[{_, _, _, _, m1}], [{send_credit_reply, _}], F7} =
process_ra_events(receive_ra_events(1, 1), F6),
%% credit and drain
- {ok, F8} = rabbit_fifo_client:credit(<<"tag">>, 4, true, F7),
- {[{_, {_, m2}}], [{send_credit_reply, _}, {send_drained, _}], F9} =
+ F8 = rabbit_fifo_client:credit(<<"tag">>, 4, true, F7),
+ {[{_, _, _, _, m2}], [{send_credit_reply, _}, {send_drained, _}], F9} =
process_ra_events(receive_ra_events(1, 1), F8),
flush(),
@@ -439,9 +439,8 @@ credit(Config) ->
(D, _) -> error({unexpected_delivery, D})
end),
%% credit again and receive the last message
- {ok, F12} = rabbit_fifo_client:credit(<<"tag">>, 10, false, F11),
- {[{_, {_, m3}}], [{send_credit_reply, _}], _} =
- process_ra_events(receive_ra_events(1, 1), F12),
+ F12 = rabbit_fifo_client:credit(<<"tag">>, 10, false, F11),
+ {[{_, _, _, _, m3}], _, _} = process_ra_events(receive_ra_events(1, 1), F12),
ok.
untracked_enqueue(Config) ->
@@ -452,7 +451,7 @@ untracked_enqueue(Config) ->
ok = rabbit_fifo_client:untracked_enqueue([ServerId], msg1),
timer:sleep(100),
F0 = rabbit_fifo_client:init(ClusterName, [ServerId]),
- {ok, {{_, {_, msg1}}, _}, _} = rabbit_fifo_client:dequeue(<<"tag">>, settled, F0),
+ {ok, _, {_, _, _, _, msg1}, _F5} = rabbit_fifo_client:dequeue(<<"tag">>, settled, F0),
ra:stop_server(ServerId),
ok.
@@ -472,6 +471,7 @@ flow(Config) ->
ok.
test_queries(Config) ->
+ % ok = logger:set_primary_config(level, all),
ClusterName = ?config(cluster_name, Config),
ServerId = ?config(node_id, Config),
ok = start_cluster(ClusterName, [ServerId]),
@@ -484,20 +484,23 @@ test_queries(Config) ->
Self ! ready,
receive stop -> ok end
end),
+ receive
+ ready -> ok
+ after 5000 ->
+ exit(ready_timeout)
+ end,
F0 = rabbit_fifo_client:init(ClusterName, [ServerId], 4),
- ok = receive ready -> ok after 5000 -> timeout end,
- {ok, _} = rabbit_fifo_client:checkout(<<"tag">>, 1, undefined, F0),
- ?assertMatch({ok, {_RaIdxTerm, 1}, _Leader},
- ra:local_query(ServerId,
- fun rabbit_fifo:query_messages_ready/1)),
- ?assertMatch({ok, {_RaIdxTerm, 1}, _Leader},
- ra:local_query(ServerId,
- fun rabbit_fifo:query_messages_checked_out/1)),
- ?assertMatch({ok, {_RaIdxTerm, Processes}, _Leader}
- when length(Processes) == 2,
- ra:local_query(ServerId,
- fun rabbit_fifo:query_processes/1)),
- P ! stop,
+ {ok, _} = rabbit_fifo_client:checkout(<<"tag">>, 1, #{}, F0),
+ {ok, {_, Ready}, _} = ra:local_query(ServerId,
+ fun rabbit_fifo:query_messages_ready/1),
+ ?assertEqual(1, Ready),
+ {ok, {_, Checked}, _} = ra:local_query(ServerId,
+ fun rabbit_fifo:query_messages_checked_out/1),
+ ?assertEqual(1, Checked),
+ {ok, {_, Processes}, _} = ra:local_query(ServerId,
+ fun rabbit_fifo:query_processes/1),
+ ?assertEqual(2, length(Processes)),
+ P ! stop,
ra:stop_server(ServerId),
ok.
@@ -511,15 +514,16 @@ dequeue(Config) ->
Tag = UId,
ok = start_cluster(ClusterName, [ServerId]),
F1 = rabbit_fifo_client:init(ClusterName, [ServerId]),
- {ok, empty, F1b} = rabbit_fifo_client:dequeue(Tag, settled, F1),
+ {empty, F1b} = rabbit_fifo_client:dequeue(Tag, settled, F1),
{ok, F2_} = rabbit_fifo_client:enqueue(msg1, F1b),
{_, _, F2} = process_ra_events(receive_ra_events(1, 0), F2_),
- {ok, {{0, {_, msg1}}, _}, F3} = rabbit_fifo_client:dequeue(Tag, settled, F2),
+ % {ok, {{0, {_, msg1}}, _}, F3} = rabbit_fifo_client:dequeue(Tag, settled, F2),
+ {ok, _, {_, _, 0, _, msg1}, F3} = rabbit_fifo_client:dequeue(Tag, settled, F2),
{ok, F4_} = rabbit_fifo_client:enqueue(msg2, F3),
{_, _, F4} = process_ra_events(receive_ra_events(1, 0), F4_),
- {ok, {{MsgId, {_, msg2}}, _}, F5} = rabbit_fifo_client:dequeue(Tag, unsettled, F4),
- {ok, _F6} = rabbit_fifo_client:settle(Tag, [MsgId], F5),
+ {ok, _, {_, _, MsgId, _, msg2}, F5} = rabbit_fifo_client:dequeue(Tag, unsettled, F4),
+ {_F6, _A} = rabbit_fifo_client:settle(Tag, [MsgId], F5),
ra:stop_server(ServerId),
ok.
@@ -534,8 +538,8 @@ conf(ClusterName, UId, ServerId, _, Peers) ->
process_ra_event(State, Wait) ->
receive
{ra_event, From, Evt} ->
- {internal, _, _, S} =
- rabbit_fifo_client:handle_ra_event(From, Evt, State),
+ {ok, S, _Actions} =
+ rabbit_fifo_client:handle_ra_event(From, Evt, State),
S
after Wait ->
exit(ra_event_timeout)
@@ -572,10 +576,10 @@ receive_ra_events(Acc) ->
end.
process_ra_events(Events, State) ->
- DeliveryFun = fun ({delivery, Tag, Msgs}, S) ->
+ DeliveryFun = fun ({deliver, _, Tag, Msgs}, S) ->
MsgIds = [element(1, M) || M <- Msgs],
- {ok, S2} = rabbit_fifo_client:settle(Tag, MsgIds, S),
- S2
+ {S0, _} = rabbit_fifo_client:settle(Tag, MsgIds, S),
+ S0
end,
process_ra_events(Events, State, [], [], DeliveryFun).
@@ -583,43 +587,41 @@ process_ra_events([], State0, Acc, Actions0, _DeliveryFun) ->
{Acc, Actions0, State0};
process_ra_events([{ra_event, From, Evt} | Events], State0, Acc, Actions0, DeliveryFun) ->
case rabbit_fifo_client:handle_ra_event(From, Evt, State0) of
- {internal, _, Actions, State} ->
- process_ra_events(Events, State, Acc, Actions0 ++ Actions, DeliveryFun);
- {{delivery, _Tag, Msgs} = Del, State1} ->
- State = DeliveryFun(Del, State1),
- process_ra_events(Events, State, Acc ++ Msgs, Actions0, DeliveryFun);
+ {ok, State1, Actions1} ->
+ {Msgs, Actions, State} =
+ lists:foldl(
+ fun ({deliver, _, _, Msgs} = Del, {M, A, S}) ->
+ {M ++ Msgs, A, DeliveryFun(Del, S)};
+ (Ac, {M, A, S}) ->
+ {M, A ++ [Ac], S}
+ end, {Acc, [], State1}, Actions1),
+ process_ra_events(Events, State, Msgs, Actions0 ++ Actions, DeliveryFun);
eol ->
eol
end.
discard_next_delivery(State0, Wait) ->
receive
- {ra_event, From, Evt} ->
- case rabbit_fifo_client:handle_ra_event(From, Evt, State0) of
- {internal, _, _Actions, State} ->
- discard_next_delivery(State, Wait);
- {{delivery, Tag, Msgs}, State1} ->
- MsgIds = [element(1, M) || M <- Msgs],
- {ok, State} = rabbit_fifo_client:discard(Tag, MsgIds,
- State1),
- State
- end
+ {ra_event, _, {machine, {delivery, _, _}}} = Evt ->
+ element(3, process_ra_events([Evt], State0, [], [],
+ fun ({deliver, Tag, _, Msgs}, S) ->
+ MsgIds = [element(3, M) || M <- Msgs],
+ {S0, _} = rabbit_fifo_client:discard(Tag, MsgIds, S),
+ S0
+ end))
after Wait ->
- State0
+ State0
end.
return_next_delivery(State0, Wait) ->
receive
- {ra_event, From, Evt} ->
- case rabbit_fifo_client:handle_ra_event(From, Evt, State0) of
- {internal, _, _, State} ->
- return_next_delivery(State, Wait);
- {{delivery, Tag, Msgs}, State1} ->
- MsgIds = [element(1, M) || M <- Msgs],
- {ok, State} = rabbit_fifo_client:return(Tag, MsgIds,
- State1),
- State
- end
+ {ra_event, _, {machine, {delivery, _, _}}} = Evt ->
+ element(3, process_ra_events([Evt], State0, [], [],
+ fun ({deliver, Tag, _, Msgs}, S) ->
+ MsgIds = [element(3, M) || M <- Msgs],
+ {S0, _} = rabbit_fifo_client:return(Tag, MsgIds, S),
+ S0
+ end))
after Wait ->
State0
end.
diff --git a/test/rabbit_ha_test_consumer.erl b/test/rabbit_ha_test_consumer.erl
index 3324a1253c..2467e40028 100644
--- a/test/rabbit_ha_test_consumer.erl
+++ b/test/rabbit_ha_test_consumer.erl
@@ -51,12 +51,15 @@ run(TestPid, Channel, Queue, CancelOnFailover, LowestSeen, MsgsToConsume) ->
%% counter.
if
MsgNum + 1 == LowestSeen ->
+ error_logger:info_msg("recording ~w left ~w",
+ [MsgNum, MsgsToConsume]),
run(TestPid, Channel, Queue,
CancelOnFailover, MsgNum, MsgsToConsume - 1);
MsgNum >= LowestSeen ->
error_logger:info_msg(
- "consumer ~p on ~p ignoring redelivered msg ~p~n",
- [self(), Channel, MsgNum]),
+ "consumer ~p on ~p ignoring redelivered msg ~p"
+ "lowest seen ~w~n",
+ [self(), Channel, MsgNum, LowestSeen]),
true = Redelivered, %% ASSERTION
run(TestPid, Channel, Queue,
CancelOnFailover, LowestSeen, MsgsToConsume);
diff --git a/test/rabbit_msg_record_SUITE.erl b/test/rabbit_msg_record_SUITE.erl
new file mode 100644
index 0000000000..a82ba7481d
--- /dev/null
+++ b/test/rabbit_msg_record_SUITE.erl
@@ -0,0 +1,213 @@
+-module(rabbit_msg_record_SUITE).
+
+-compile(export_all).
+
+-export([
+ ]).
+
+-include("rabbit.hrl").
+-include("rabbit_framing.hrl").
+-include_lib("common_test/include/ct.hrl").
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("amqp10_common/include/amqp10_framing.hrl").
+
+%%%===================================================================
+%%% Common Test callbacks
+%%%===================================================================
+
+all() ->
+ [
+ {group, tests}
+ ].
+
+
+all_tests() ->
+ [
+ ampq091_roundtrip,
+ message_id_ulong,
+ message_id_uuid,
+ message_id_binary,
+ message_id_large_binary,
+ message_id_large_string
+ ].
+
+groups() ->
+ [
+ {tests, [], all_tests()}
+ ].
+
+init_per_suite(Config) ->
+ Config.
+
+end_per_suite(_Config) ->
+ ok.
+
+init_per_group(_Group, Config) ->
+ Config.
+
+end_per_group(_Group, _Config) ->
+ ok.
+
+init_per_testcase(_TestCase, Config) ->
+ Config.
+
+end_per_testcase(_TestCase, _Config) ->
+ ok.
+
+%%%===================================================================
+%%% Test cases
+%%%===================================================================
+
+ampq091_roundtrip(_Config) ->
+ Props = #'P_basic'{content_type = <<"text/plain">>,
+ content_encoding = <<"gzip">>,
+ headers = [{<<"x-stream-offset">>, long, 99},
+ {<<"x-string">>, longstr, <<"a string">>},
+ {<<"x-bool">>, bool, false},
+ {<<"x-unsignedbyte">>, unsignedbyte, 1},
+ {<<"x-unsignedshort">>, unsignedshort, 1},
+ {<<"x-unsignedint">>, unsignedint, 1},
+ {<<"x-signedint">>, signedint, 1},
+ {<<"x-timestamp">>, timestamp, 1},
+ {<<"x-double">>, double, 1.0},
+ {<<"x-float">>, float, 1.0},
+ {<<"x-binary">>, binary, <<"data">>}
+ ],
+ delivery_mode = 2,
+ priority = 99,
+ correlation_id = <<"corr">> ,
+ reply_to = <<"reply-to">>,
+ expiration = <<"1">>,
+ message_id = <<"msg-id">>,
+ timestamp = 99,
+ type = <<"45">>,
+ user_id = <<"banana">>,
+ app_id = <<"rmq">>
+ % cluster_id = <<"adf">>
+ },
+ Payload = [<<"data">>],
+ test_amqp091_roundtrip(Props, Payload),
+ test_amqp091_roundtrip(#'P_basic'{}, Payload),
+ ok.
+
+message_id_ulong(_Config) ->
+ Num = 9876789,
+ ULong = erlang:integer_to_binary(Num),
+ P = #'v1_0.properties'{message_id = {ulong, Num},
+ correlation_id = {ulong, Num}},
+ D = #'v1_0.data'{content = <<"data">>},
+ Bin = [amqp10_framing:encode_bin(P),
+ amqp10_framing:encode_bin(D)],
+ R = rabbit_msg_record:init(iolist_to_binary(Bin)),
+ {Props, _} = rabbit_msg_record:to_amqp091(R),
+ ?assertMatch(#'P_basic'{message_id = ULong,
+ correlation_id = ULong,
+ headers =
+ [
+ %% ordering shouldn't matter
+ {<<"x-correlation-id-type">>, longstr, <<"ulong">>},
+ {<<"x-message-id-type">>, longstr, <<"ulong">>}
+ ]},
+ Props),
+ ok.
+
+message_id_uuid(_Config) ->
+ %% fake a uuid
+ UUId = erlang:md5(term_to_binary(make_ref())),
+ TextUUId = rabbit_data_coercion:to_binary(rabbit_guid:to_string(UUId)),
+ P = #'v1_0.properties'{message_id = {uuid, UUId},
+ correlation_id = {uuid, UUId}},
+ D = #'v1_0.data'{content = <<"data">>},
+ Bin = [amqp10_framing:encode_bin(P),
+ amqp10_framing:encode_bin(D)],
+ R = rabbit_msg_record:init(iolist_to_binary(Bin)),
+ {Props, _} = rabbit_msg_record:to_amqp091(R),
+ ?assertMatch(#'P_basic'{message_id = TextUUId,
+ correlation_id = TextUUId,
+ headers =
+ [
+ %% ordering shouldn't matter
+ {<<"x-correlation-id-type">>, longstr, <<"uuid">>},
+ {<<"x-message-id-type">>, longstr, <<"uuid">>}
+ ]},
+ Props),
+ ok.
+
+message_id_binary(_Config) ->
+ %% fake a uuid
+ Orig = <<"asdfasdf">>,
+ Text = base64:encode(Orig),
+ P = #'v1_0.properties'{message_id = {binary, Orig},
+ correlation_id = {binary, Orig}},
+ D = #'v1_0.data'{content = <<"data">>},
+ Bin = [amqp10_framing:encode_bin(P),
+ amqp10_framing:encode_bin(D)],
+ R = rabbit_msg_record:init(iolist_to_binary(Bin)),
+ {Props, _} = rabbit_msg_record:to_amqp091(R),
+ ?assertMatch(#'P_basic'{message_id = Text,
+ correlation_id = Text,
+ headers =
+ [
+ %% ordering shouldn't matter
+ {<<"x-correlation-id-type">>, longstr, <<"binary">>},
+ {<<"x-message-id-type">>, longstr, <<"binary">>}
+ ]},
+ Props),
+ ok.
+
+message_id_large_binary(_Config) ->
+ %% cannot fit in a shortstr
+ Orig = crypto:strong_rand_bytes(500),
+ P = #'v1_0.properties'{message_id = {binary, Orig},
+ correlation_id = {binary, Orig}},
+ D = #'v1_0.data'{content = <<"data">>},
+ Bin = [amqp10_framing:encode_bin(P),
+ amqp10_framing:encode_bin(D)],
+ R = rabbit_msg_record:init(iolist_to_binary(Bin)),
+ {Props, _} = rabbit_msg_record:to_amqp091(R),
+ ?assertMatch(#'P_basic'{message_id = undefined,
+ correlation_id = undefined,
+ headers =
+ [
+ %% ordering shouldn't matter
+ {<<"x-correlation-id">>, longstr, Orig},
+ {<<"x-message-id">>, longstr, Orig}
+ ]},
+ Props),
+ ok.
+
+message_id_large_string(_Config) ->
+ %% cannot fit in a shortstr
+ Orig = base64:encode(crypto:strong_rand_bytes(500)),
+ P = #'v1_0.properties'{message_id = {utf8, Orig},
+ correlation_id = {utf8, Orig}},
+ D = #'v1_0.data'{content = <<"data">>},
+ Bin = [amqp10_framing:encode_bin(P),
+ amqp10_framing:encode_bin(D)],
+ R = rabbit_msg_record:init(iolist_to_binary(Bin)),
+ {Props, _} = rabbit_msg_record:to_amqp091(R),
+ ?assertMatch(#'P_basic'{message_id = undefined,
+ correlation_id = undefined,
+ headers =
+ [
+ %% ordering shouldn't matter
+ {<<"x-correlation-id">>, longstr, Orig},
+ {<<"x-message-id">>, longstr, Orig}
+ ]},
+ Props),
+ ok.
+
+%% Utility
+
+test_amqp091_roundtrip(Props, Payload) ->
+ MsgRecord0 = rabbit_msg_record:from_amqp091(Props, Payload),
+ MsgRecord = rabbit_msg_record:init(
+ iolist_to_binary(rabbit_msg_record:to_iodata(MsgRecord0))),
+ % meck:unload(),
+ {PropsOut, PayloadOut} = rabbit_msg_record:to_amqp091(MsgRecord),
+ ?assertEqual(Props, PropsOut),
+ ?assertEqual(iolist_to_binary(Payload),
+ iolist_to_binary(PayloadOut)),
+ ok.
+
+
diff --git a/test/rabbit_stream_queue_SUITE.erl b/test/rabbit_stream_queue_SUITE.erl
new file mode 100644
index 0000000000..67ca8eba8b
--- /dev/null
+++ b/test/rabbit_stream_queue_SUITE.erl
@@ -0,0 +1,1304 @@
+%% 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.
+%%
+%% Copyright (c) 2012-2020 VMware, Inc. or its affiliates. All rights reserved.
+%%
+
+-module(rabbit_stream_queue_SUITE).
+
+-include_lib("proper/include/proper.hrl").
+-include_lib("common_test/include/ct.hrl").
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("amqp_client/include/amqp_client.hrl").
+
+-compile(export_all).
+
+suite() ->
+ [{timetrap, 5 * 60000}].
+
+all() ->
+ [
+ {group, single_node},
+ {group, cluster_size_2},
+ {group, cluster_size_3},
+ {group, unclustered_size_3_1},
+ {group, unclustered_size_3_2},
+ {group, unclustered_size_3_3},
+ {group, cluster_size_3_1}
+ ].
+
+groups() ->
+ [
+ {single_node, [], [restart_single_node] ++ all_tests()},
+ {cluster_size_2, [], all_tests()},
+ {cluster_size_3, [], all_tests() ++
+ [delete_replica,
+ delete_down_replica,
+ delete_classic_replica,
+ delete_quorum_replica,
+ consume_from_replica,
+ leader_failover]},
+ {unclustered_size_3_1, [], [add_replica]},
+ {unclustered_size_3_2, [], [consume_without_local_replica]},
+ {unclustered_size_3_3, [], [grow_coordinator_cluster]},
+ {cluster_size_3_1, [], [shrink_coordinator_cluster]}
+ ].
+
+all_tests() ->
+ [
+ declare_args,
+ declare_max_age,
+ declare_invalid_args,
+ declare_invalid_properties,
+ declare_queue,
+ delete_queue,
+ publish,
+ publish_confirm,
+ recover,
+ consume_without_qos,
+ consume,
+ consume_offset,
+ basic_get,
+ consume_with_autoack,
+ consume_and_nack,
+ consume_and_ack,
+ consume_and_reject,
+ consume_from_last,
+ consume_from_next,
+ consume_from_default,
+ consume_credit,
+ consume_credit_out_of_order_ack,
+ consume_credit_multiple_ack,
+ basic_cancel,
+ max_length_bytes,
+ max_age,
+ invalid_policy,
+ max_age_policy,
+ max_segment_size_policy
+ ].
+
+%% -------------------------------------------------------------------
+%% Testsuite setup/teardown.
+%% -------------------------------------------------------------------
+
+init_per_suite(Config0) ->
+ rabbit_ct_helpers:log_environment(),
+ Config = rabbit_ct_helpers:merge_app_env(
+ Config0, {rabbit, [{stream_tick_interval, 1000},
+ {log, [{file, [{level, debug}]}]}]}),
+ rabbit_ct_helpers:run_setup_steps(Config).
+
+end_per_suite(Config) ->
+ rabbit_ct_helpers:run_teardown_steps(Config).
+
+init_per_group(Group, Config) ->
+ ClusterSize = case Group of
+ single_node -> 1;
+ cluster_size_2 -> 2;
+ cluster_size_3 -> 3;
+ cluster_size_3_1 -> 3;
+ unclustered_size_3_1 -> 3;
+ unclustered_size_3_2 -> 3;
+ unclustered_size_3_3 -> 3
+ end,
+ Clustered = case Group of
+ unclustered_size_3_1 -> false;
+ unclustered_size_3_2 -> false;
+ unclustered_size_3_3 -> false;
+ _ -> true
+ end,
+ Config1 = rabbit_ct_helpers:set_config(Config,
+ [{rmq_nodes_count, ClusterSize},
+ {rmq_nodename_suffix, Group},
+ {tcp_ports_base},
+ {rmq_nodes_clustered, Clustered}]),
+ Config1b = rabbit_ct_helpers:set_config(Config1, [{net_ticktime, 10}]),
+ Ret = rabbit_ct_helpers:run_steps(Config1b,
+ [fun merge_app_env/1 ] ++
+ rabbit_ct_broker_helpers:setup_steps()),
+ case Ret of
+ {skip, _} ->
+ Ret;
+ Config2 ->
+ EnableFF = rabbit_ct_broker_helpers:enable_feature_flag(
+ Config2, stream_queue),
+ case EnableFF of
+ ok ->
+ ok = rabbit_ct_broker_helpers:rpc(
+ Config2, 0, application, set_env,
+ [rabbit, channel_tick_interval, 100]),
+ Config2;
+ Skip ->
+ end_per_group(Group, Config2),
+ Skip
+ end
+ end.
+
+end_per_group(_, Config) ->
+ rabbit_ct_helpers:run_steps(Config,
+ rabbit_ct_broker_helpers:teardown_steps()).
+
+init_per_testcase(Testcase, Config) ->
+ Config1 = rabbit_ct_helpers:testcase_started(Config, Testcase),
+ Q = rabbit_data_coercion:to_binary(Testcase),
+ Config2 = rabbit_ct_helpers:set_config(Config1, [{queue_name, Q}]),
+ rabbit_ct_helpers:run_steps(Config2, rabbit_ct_client_helpers:setup_steps()).
+
+merge_app_env(Config) ->
+ rabbit_ct_helpers:merge_app_env(Config,
+ {rabbit, [{core_metrics_gc_interval, 100}]}).
+
+end_per_testcase(Testcase, Config) ->
+ rabbit_ct_broker_helpers:rpc(Config, 0, ?MODULE, delete_queues, []),
+ Config1 = rabbit_ct_helpers:run_steps(
+ Config,
+ rabbit_ct_client_helpers:teardown_steps()),
+ rabbit_ct_helpers:testcase_finished(Config1, Testcase).
+
+%% -------------------------------------------------------------------
+%% Testcases.
+%% -------------------------------------------------------------------
+
+declare_args(Config) ->
+ Server = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename),
+
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q = ?config(queue_name, Config),
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>},
+ {<<"x-max-length">>, long, 2000}])),
+ assert_queue_type(Server, Q, rabbit_stream_queue).
+
+declare_max_age(Config) ->
+ Server = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename),
+
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q = ?config(queue_name, Config),
+
+ ?assertExit(
+ {{shutdown, {server_initiated_close, 406, _}}, _},
+ declare(rabbit_ct_client_helpers:open_channel(Config, Server), Q,
+ [{<<"x-queue-type">>, longstr, <<"stream">>},
+ {<<"x-max-age">>, longstr, <<"1A">>}])),
+
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>},
+ {<<"x-max-age">>, longstr, <<"1Y">>}])),
+ assert_queue_type(Server, Q, rabbit_stream_queue).
+
+declare_invalid_properties(Config) ->
+ Server = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename),
+ Q = ?config(queue_name, Config),
+
+ ?assertExit(
+ {{shutdown, {server_initiated_close, 406, _}}, _},
+ amqp_channel:call(
+ rabbit_ct_client_helpers:open_channel(Config, Server),
+ #'queue.declare'{queue = Q,
+ auto_delete = true,
+ durable = true,
+ arguments = [{<<"x-queue-type">>, longstr, <<"stream">>}]})),
+ ?assertExit(
+ {{shutdown, {server_initiated_close, 406, _}}, _},
+ amqp_channel:call(
+ rabbit_ct_client_helpers:open_channel(Config, Server),
+ #'queue.declare'{queue = Q,
+ exclusive = true,
+ durable = true,
+ arguments = [{<<"x-queue-type">>, longstr, <<"stream">>}]})),
+ ?assertExit(
+ {{shutdown, {server_initiated_close, 406, _}}, _},
+ amqp_channel:call(
+ rabbit_ct_client_helpers:open_channel(Config, Server),
+ #'queue.declare'{queue = Q,
+ durable = false,
+ arguments = [{<<"x-queue-type">>, longstr, <<"stream">>}]})).
+
+declare_invalid_args(Config) ->
+ Server = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename),
+ Q = ?config(queue_name, Config),
+
+ ?assertExit(
+ {{shutdown, {server_initiated_close, 406, _}}, _},
+ declare(rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q, [{<<"x-queue-type">>, longstr, <<"stream">>},
+ {<<"x-expires">>, long, 2000}])),
+ ?assertExit(
+ {{shutdown, {server_initiated_close, 406, _}}, _},
+ declare(rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q, [{<<"x-queue-type">>, longstr, <<"stream">>},
+ {<<"x-message-ttl">>, long, 2000}])),
+
+ ?assertExit(
+ {{shutdown, {server_initiated_close, 406, _}}, _},
+ declare(rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q, [{<<"x-queue-type">>, longstr, <<"stream">>},
+ {<<"x-max-priority">>, long, 2000}])),
+
+ [?assertExit(
+ {{shutdown, {server_initiated_close, 406, _}}, _},
+ declare(rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q, [{<<"x-queue-type">>, longstr, <<"stream">>},
+ {<<"x-overflow">>, longstr, XOverflow}]))
+ || XOverflow <- [<<"reject-publish">>, <<"reject-publish-dlx">>]],
+
+ ?assertExit(
+ {{shutdown, {server_initiated_close, 406, _}}, _},
+ declare(rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q, [{<<"x-queue-type">>, longstr, <<"stream">>},
+ {<<"x-queue-mode">>, longstr, <<"lazy">>}])),
+
+ ?assertExit(
+ {{shutdown, {server_initiated_close, 406, _}}, _},
+ declare(rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q, [{<<"x-queue-type">>, longstr, <<"stream">>},
+ {<<"x-quorum-initial-group-size">>, longstr, <<"hop">>}])).
+
+declare_queue(Config) ->
+ [Server | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q = ?config(queue_name, Config),
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+
+ %% Test declare an existing queue
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+
+ ?assertMatch([_], rpc:call(Server, supervisor, which_children,
+ [osiris_server_sup])),
+
+ %% Test declare an existing queue with different arguments
+ ?assertExit(_, declare(Ch, Q, [])).
+
+delete_queue(Config) ->
+ [Server | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q = ?config(queue_name, Config),
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+ ?assertMatch(#'queue.delete_ok'{},
+ amqp_channel:call(Ch, #'queue.delete'{queue = Q})).
+
+add_replica(Config) ->
+ [Server0, Server1, Server2] =
+ rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server0),
+ Q = ?config(queue_name, Config),
+
+ %% Let's also try the add replica command on other queue types, it should fail
+ %% We're doing it in the same test for efficiency, otherwise we have to
+ %% start new rabbitmq clusters every time for a minor testcase
+ QClassic = <<Q/binary, "_classic">>,
+ QQuorum = <<Q/binary, "_quorum">>,
+
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+ ?assertEqual({'queue.declare_ok', QClassic, 0, 0},
+ declare(Ch, QClassic, [{<<"x-queue-type">>, longstr, <<"classic">>}])),
+ ?assertEqual({'queue.declare_ok', QQuorum, 0, 0},
+ declare(Ch, QQuorum, [{<<"x-queue-type">>, longstr, <<"quorum">>}])),
+
+ %% Not a member of the cluster, what would happen?
+ ?assertEqual({error, node_not_running},
+ rpc:call(Server0, rabbit_stream_queue, add_replica,
+ [<<"/">>, Q, Server1])),
+ ?assertEqual({error, classic_queue_not_supported},
+ rpc:call(Server0, rabbit_stream_queue, add_replica,
+ [<<"/">>, QClassic, Server1])),
+ ?assertEqual({error, quorum_queue_not_supported},
+ rpc:call(Server0, rabbit_stream_queue, add_replica,
+ [<<"/">>, QQuorum, Server1])),
+
+ ok = rabbit_control_helper:command(stop_app, Server1),
+ ok = rabbit_control_helper:command(join_cluster, Server1, [atom_to_list(Server0)], []),
+ rabbit_control_helper:command(start_app, Server1),
+ timer:sleep(1000),
+ ?assertEqual({error, classic_queue_not_supported},
+ rpc:call(Server0, rabbit_stream_queue, add_replica,
+ [<<"/">>, QClassic, Server1])),
+ ?assertEqual({error, quorum_queue_not_supported},
+ rpc:call(Server0, rabbit_stream_queue, add_replica,
+ [<<"/">>, QQuorum, Server1])),
+ ?assertEqual(ok,
+ rpc:call(Server0, rabbit_stream_queue, add_replica,
+ [<<"/">>, Q, Server1])),
+ %% replicas must be recorded on the state, and if we publish messages then they must
+ %% be stored on disk
+ check_leader_and_replicas(Config, Q, Server0, [Server1]),
+ %% And if we try again? Idempotent
+ ?assertEqual(ok, rpc:call(Server0, rabbit_stream_queue, add_replica,
+ [<<"/">>, Q, Server1])),
+ %% Add another node
+ ok = rabbit_control_helper:command(stop_app, Server2),
+ ok = rabbit_control_helper:command(join_cluster, Server2, [atom_to_list(Server0)], []),
+ rabbit_control_helper:command(start_app, Server2),
+ ?assertEqual(ok, rpc:call(Server0, rabbit_stream_queue, add_replica,
+ [<<"/">>, Q, Server2])),
+ check_leader_and_replicas(Config, Q, Server0, [Server1, Server2]).
+
+delete_replica(Config) ->
+ [Server0, Server1, Server2] =
+ rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server0),
+ Q = ?config(queue_name, Config),
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+ check_leader_and_replicas(Config, Q, Server0, [Server1, Server2]),
+ %% Not a member of the cluster, what would happen?
+ ?assertEqual({error, node_not_running},
+ rpc:call(Server0, rabbit_stream_queue, delete_replica,
+ [<<"/">>, Q, 'zen@rabbit'])),
+ ?assertEqual(ok,
+ rpc:call(Server0, rabbit_stream_queue, delete_replica,
+ [<<"/">>, Q, Server1])),
+ %% check it's gone
+ check_leader_and_replicas(Config, Q, Server0, [Server2]),
+ %% And if we try again? Idempotent
+ ?assertEqual(ok, rpc:call(Server0, rabbit_stream_queue, delete_replica,
+ [<<"/">>, Q, Server1])),
+ %% Delete the last replica
+ ?assertEqual(ok, rpc:call(Server0, rabbit_stream_queue, delete_replica,
+ [<<"/">>, Q, Server2])),
+ check_leader_and_replicas(Config, Q, Server0, []).
+
+grow_coordinator_cluster(Config) ->
+ [Server0, Server1, _Server2] =
+ rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server0),
+ Q = ?config(queue_name, Config),
+
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+
+ ok = rabbit_control_helper:command(stop_app, Server1),
+ ok = rabbit_control_helper:command(join_cluster, Server1, [atom_to_list(Server0)], []),
+ rabbit_control_helper:command(start_app, Server1),
+
+ rabbit_ct_helpers:await_condition(
+ fun() ->
+ case rpc:call(Server0, ra, members, [{rabbit_stream_coordinator, Server0}]) of
+ {_, Members, _} ->
+ Nodes = lists:sort([N || {_, N} <- Members]),
+ lists:sort([Server0, Server1]) == Nodes;
+ _ ->
+ false
+ end
+ end, 60000).
+
+shrink_coordinator_cluster(Config) ->
+ [Server0, Server1, Server2] =
+ rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server0),
+ Q = ?config(queue_name, Config),
+
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+
+ ok = rabbit_control_helper:command(stop_app, Server2),
+ ok = rabbit_control_helper:command(forget_cluster_node, Server0, [atom_to_list(Server2)], []),
+
+ rabbit_ct_helpers:await_condition(
+ fun() ->
+ case rpc:call(Server0, ra, members, [{rabbit_stream_coordinator, Server0}]) of
+ {_, Members, _} ->
+ Nodes = lists:sort([N || {_, N} <- Members]),
+ lists:sort([Server0, Server1]) == Nodes;
+ _ ->
+ false
+ end
+ end, 60000).
+
+delete_classic_replica(Config) ->
+ [Server0, Server1, _Server2] =
+ rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server0),
+ Q = ?config(queue_name, Config),
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"classic">>}])),
+ %% Not a member of the cluster, what would happen?
+ ?assertEqual({error, classic_queue_not_supported},
+ rpc:call(Server0, rabbit_stream_queue, delete_replica,
+ [<<"/">>, Q, 'zen@rabbit'])),
+ ?assertEqual({error, classic_queue_not_supported},
+ rpc:call(Server0, rabbit_stream_queue, delete_replica,
+ [<<"/">>, Q, Server1])).
+
+delete_quorum_replica(Config) ->
+ [Server0, Server1, _Server2] =
+ rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server0),
+ Q = ?config(queue_name, Config),
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"quorum">>}])),
+ %% Not a member of the cluster, what would happen?
+ ?assertEqual({error, quorum_queue_not_supported},
+ rpc:call(Server0, rabbit_stream_queue, delete_replica,
+ [<<"/">>, Q, 'zen@rabbit'])),
+ ?assertEqual({error, quorum_queue_not_supported},
+ rpc:call(Server0, rabbit_stream_queue, delete_replica,
+ [<<"/">>, Q, Server1])).
+
+delete_down_replica(Config) ->
+ [Server0, Server1, Server2] =
+ rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server0),
+ Q = ?config(queue_name, Config),
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+ check_leader_and_replicas(Config, Q, Server0, [Server1, Server2]),
+ ok = rabbit_ct_broker_helpers:stop_node(Config, Server1),
+ ?assertEqual({error, node_not_running},
+ rpc:call(Server0, rabbit_stream_queue, delete_replica,
+ [<<"/">>, Q, Server1])),
+ %% check it isn't gone
+ check_leader_and_replicas(Config, Q, Server0, [Server1, Server2]),
+ ok = rabbit_ct_broker_helpers:start_node(Config, Server1),
+ ?assertEqual(ok,
+ rpc:call(Server0, rabbit_stream_queue, delete_replica,
+ [<<"/">>, Q, Server1])).
+
+publish(Config) ->
+ [Server | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q = ?config(queue_name, Config),
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+
+ publish(Ch, Q),
+ quorum_queue_utils:wait_for_messages(Config, [[Q, <<"1">>, <<"1">>, <<"0">>]]).
+
+publish_confirm(Config) ->
+ [Server | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q = ?config(queue_name, Config),
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+
+ #'confirm.select_ok'{} = amqp_channel:call(Ch, #'confirm.select'{}),
+ amqp_channel:register_confirm_handler(Ch, self()),
+ publish(Ch, Q),
+ amqp_channel:wait_for_confirms(Ch, 5000),
+ quorum_queue_utils:wait_for_messages(Config, [[Q, <<"1">>, <<"1">>, <<"0">>]]).
+
+restart_single_node(Config) ->
+ [Server] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q = ?config(queue_name, Config),
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+ publish(Ch, Q),
+ quorum_queue_utils:wait_for_messages(Config, [[Q, <<"1">>, <<"1">>, <<"0">>]]),
+
+ rabbit_control_helper:command(stop_app, Server),
+ rabbit_control_helper:command(start_app, Server),
+
+ quorum_queue_utils:wait_for_messages(Config, [[Q, <<"1">>, <<"1">>, <<"0">>]]),
+ Ch1 = rabbit_ct_client_helpers:open_channel(Config, Server),
+ publish(Ch1, Q),
+ quorum_queue_utils:wait_for_messages(Config, [[Q, <<"2">>, <<"2">>, <<"0">>]]).
+
+recover(Config) ->
+ [Server | _] = Servers = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q = ?config(queue_name, Config),
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+ publish(Ch, Q),
+ quorum_queue_utils:wait_for_messages(Config, [[Q, <<"1">>, <<"1">>, <<"0">>]]),
+
+ [rabbit_ct_broker_helpers:stop_node(Config, S) || S <- Servers],
+ [rabbit_ct_broker_helpers:start_node(Config, S) || S <- lists:reverse(Servers)],
+
+ quorum_queue_utils:wait_for_messages(Config, [[Q, <<"1">>, <<"1">>, <<"0">>]]),
+ Ch1 = rabbit_ct_client_helpers:open_channel(Config, Server),
+ publish(Ch1, Q),
+ quorum_queue_utils:wait_for_messages(Config, [[Q, <<"2">>, <<"2">>, <<"0">>]]).
+
+consume_without_qos(Config) ->
+ [Server | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q = ?config(queue_name, Config),
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+
+ ?assertExit({{shutdown, {server_initiated_close, 406, _}}, _},
+ amqp_channel:subscribe(Ch, #'basic.consume'{queue = Q, consumer_tag = <<"ctag">>},
+ self())).
+
+consume_without_local_replica(Config) ->
+ [Server0, Server1 | _] =
+ rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server0),
+ Q = ?config(queue_name, Config),
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+ %% Add another node to the cluster, but it won't have a replica
+ ok = rabbit_control_helper:command(stop_app, Server1),
+ ok = rabbit_control_helper:command(join_cluster, Server1, [atom_to_list(Server0)], []),
+ rabbit_control_helper:command(start_app, Server1),
+ timer:sleep(1000),
+
+ Ch1 = rabbit_ct_client_helpers:open_channel(Config, Server1),
+ qos(Ch1, 10, false),
+ ?assertExit({{shutdown, {server_initiated_close, 406, _}}, _},
+ amqp_channel:subscribe(Ch1, #'basic.consume'{queue = Q, consumer_tag = <<"ctag">>},
+ self())).
+
+consume(Config) ->
+ [Server | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q = ?config(queue_name, Config),
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+
+ #'confirm.select_ok'{} = amqp_channel:call(Ch, #'confirm.select'{}),
+ amqp_channel:register_confirm_handler(Ch, self()),
+ publish(Ch, Q),
+ amqp_channel:wait_for_confirms(Ch, 5000),
+
+ Ch1 = rabbit_ct_client_helpers:open_channel(Config, Server),
+ qos(Ch1, 10, false),
+ subscribe(Ch1, Q, false, 0),
+ receive
+ {#'basic.deliver'{delivery_tag = DeliveryTag}, _} ->
+ ok = amqp_channel:cast(Ch1, #'basic.ack'{delivery_tag = DeliveryTag,
+ multiple = false}),
+ _ = amqp_channel:call(Ch1, #'basic.cancel'{consumer_tag = <<"ctag">>}),
+ ok = amqp_channel:close(Ch1),
+ ok
+ after 5000 ->
+ exit(timeout)
+ end.
+
+consume_offset(Config) ->
+ [Server | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q = ?config(queue_name, Config),
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+
+ #'confirm.select_ok'{} = amqp_channel:call(Ch, #'confirm.select'{}),
+ amqp_channel:register_confirm_handler(Ch, self()),
+ Payload = << <<"1">> || _ <- lists:seq(1, 500) >>,
+ [publish(Ch, Q, Payload) || _ <- lists:seq(1, 1000)],
+ amqp_channel:wait_for_confirms(Ch, 5000),
+
+ run_proper(
+ fun () ->
+ ?FORALL(Offset, range(0, 999),
+ begin
+ Ch1 = rabbit_ct_client_helpers:open_channel(Config, Server),
+ qos(Ch1, 10, false),
+ subscribe(Ch1, Q, false, Offset),
+ receive_batch(Ch1, Offset, 999),
+ receive
+ {_,
+ #amqp_msg{props = #'P_basic'{headers = [{<<"x-stream-offset">>, long, S}]}}}
+ when S < Offset ->
+ exit({unexpected_offset, S})
+ after 1000 ->
+ ok
+ end,
+ amqp_channel:call(Ch1, #'basic.cancel'{consumer_tag = <<"ctag">>}),
+ true
+ end)
+ end, [], 25).
+
+basic_get(Config) ->
+ [Server | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q = ?config(queue_name, Config),
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+
+ ?assertExit({{shutdown, {connection_closing, {server_initiated_close, 540, _}}}, _},
+ amqp_channel:call(Ch, #'basic.get'{queue = Q})).
+
+consume_with_autoack(Config) ->
+ [Server | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q = ?config(queue_name, Config),
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+
+ Ch1 = rabbit_ct_client_helpers:open_channel(Config, Server),
+ qos(Ch1, 10, false),
+
+ ?assertExit(
+ {{shutdown, {connection_closing, {server_initiated_close, 540, _}}}, _},
+ subscribe(Ch1, Q, true, 0)).
+
+consume_and_nack(Config) ->
+ [Server | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q = ?config(queue_name, Config),
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+
+ #'confirm.select_ok'{} = amqp_channel:call(Ch, #'confirm.select'{}),
+ amqp_channel:register_confirm_handler(Ch, self()),
+ publish(Ch, Q),
+ amqp_channel:wait_for_confirms(Ch, 5000),
+
+ Ch1 = rabbit_ct_client_helpers:open_channel(Config, Server),
+ qos(Ch1, 10, false),
+ subscribe(Ch1, Q, false, 0),
+ receive
+ {#'basic.deliver'{delivery_tag = DeliveryTag}, _} ->
+ ok = amqp_channel:cast(Ch1, #'basic.nack'{delivery_tag = DeliveryTag,
+ multiple = false,
+ requeue = true}),
+ %% Nack will throw a not implemented exception. As it is a cast operation,
+ %% we'll detect the conneciton/channel closure on the next call.
+ %% Let's try to redeclare and see what happens
+ ?assertExit({{shutdown, {connection_closing, {server_initiated_close, 540, _}}}, _},
+ declare(Ch1, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}]))
+ after 10000 ->
+ exit(timeout)
+ end.
+
+basic_cancel(Config) ->
+ [Server | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q = ?config(queue_name, Config),
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+
+ #'confirm.select_ok'{} = amqp_channel:call(Ch, #'confirm.select'{}),
+ amqp_channel:register_confirm_handler(Ch, self()),
+ publish(Ch, Q),
+ amqp_channel:wait_for_confirms(Ch, 5000),
+
+ Ch1 = rabbit_ct_client_helpers:open_channel(Config, Server),
+ qos(Ch1, 10, false),
+ subscribe(Ch1, Q, false, 0),
+ rabbit_ct_helpers:await_condition(
+ fun() ->
+ 1 == length(rabbit_ct_broker_helpers:rpc(Config, Server, ets, tab2list,
+ [consumer_created]))
+ end, 30000),
+ receive
+ {#'basic.deliver'{}, _} ->
+ amqp_channel:call(Ch1, #'basic.cancel'{consumer_tag = <<"ctag">>}),
+ ?assertMatch([], rabbit_ct_broker_helpers:rpc(Config, Server, ets, tab2list, [consumer_created]))
+ after 10000 ->
+ exit(timeout)
+ end.
+
+consume_and_reject(Config) ->
+ [Server | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q = ?config(queue_name, Config),
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+
+ #'confirm.select_ok'{} = amqp_channel:call(Ch, #'confirm.select'{}),
+ amqp_channel:register_confirm_handler(Ch, self()),
+ publish(Ch, Q),
+ amqp_channel:wait_for_confirms(Ch, 5000),
+
+ Ch1 = rabbit_ct_client_helpers:open_channel(Config, Server),
+ qos(Ch1, 10, false),
+ subscribe(Ch1, Q, false, 0),
+ receive
+ {#'basic.deliver'{delivery_tag = DeliveryTag}, _} ->
+ ok = amqp_channel:cast(Ch1, #'basic.reject'{delivery_tag = DeliveryTag,
+ requeue = true}),
+ %% Reject will throw a not implemented exception. As it is a cast operation,
+ %% we'll detect the conneciton/channel closure on the next call.
+ %% Let's try to redeclare and see what happens
+ ?assertExit({{shutdown, {connection_closing, {server_initiated_close, 540, _}}}, _},
+ declare(Ch1, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}]))
+ after 10000 ->
+ exit(timeout)
+ end.
+
+consume_and_ack(Config) ->
+ [Server | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q = ?config(queue_name, Config),
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+
+ #'confirm.select_ok'{} = amqp_channel:call(Ch, #'confirm.select'{}),
+ amqp_channel:register_confirm_handler(Ch, self()),
+ publish(Ch, Q),
+ amqp_channel:wait_for_confirms(Ch, 5000),
+
+ Ch1 = rabbit_ct_client_helpers:open_channel(Config, Server),
+ qos(Ch1, 10, false),
+ subscribe(Ch1, Q, false, 0),
+ receive
+ {#'basic.deliver'{delivery_tag = DeliveryTag}, _} ->
+ ok = amqp_channel:cast(Ch1, #'basic.ack'{delivery_tag = DeliveryTag,
+ multiple = false}),
+ %% It will succeed as ack is now a credit operation. We should be
+ %% able to redeclare a queue (gen_server call op) as the channel
+ %% should still be open and declare is an idempotent operation
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch1, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+ quorum_queue_utils:wait_for_messages(Config, [[Q, <<"1">>, <<"1">>, <<"0">>]])
+ after 5000 ->
+ exit(timeout)
+ end.
+
+consume_from_last(Config) ->
+ [Server | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q = ?config(queue_name, Config),
+
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+
+ #'confirm.select_ok'{} = amqp_channel:call(Ch, #'confirm.select'{}),
+ amqp_channel:register_confirm_handler(Ch, self()),
+ [publish(Ch, Q, <<"msg1">>) || _ <- lists:seq(1, 100)],
+ amqp_channel:wait_for_confirms(Ch, 5000),
+
+ Ch1 = rabbit_ct_client_helpers:open_channel(Config, Server),
+ qos(Ch1, 10, false),
+
+ [Info] = rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_amqqueue,
+ info_all, [<<"/">>, [committed_offset]]),
+
+ %% We'll receive data from the last committed offset, let's check that is not the
+ %% first offset
+ CommittedOffset = proplists:get_value(committed_offset, Info),
+ ?assert(CommittedOffset > 0),
+
+ %% If the offset is not provided, we're subscribing to the tail of the stream
+ amqp_channel:subscribe(
+ Ch1, #'basic.consume'{queue = Q,
+ no_ack = false,
+ consumer_tag = <<"ctag">>,
+ arguments = [{<<"x-stream-offset">>, longstr, <<"last">>}]},
+ self()),
+ receive
+ #'basic.consume_ok'{consumer_tag = <<"ctag">>} ->
+ ok
+ end,
+
+ %% And receive the messages from the last committed offset to the end of the stream
+ receive_batch(Ch1, CommittedOffset, 99),
+
+ %% Publish a few more
+ [publish(Ch, Q, <<"msg2">>) || _ <- lists:seq(1, 100)],
+ amqp_channel:wait_for_confirms(Ch, 5000),
+
+ %% Yeah! we got them
+ receive_batch(Ch1, 100, 199).
+
+consume_from_next(Config) ->
+ consume_from_next(Config, [{<<"x-stream-offset">>, longstr, <<"next">>}]).
+
+consume_from_default(Config) ->
+ consume_from_next(Config, []).
+
+consume_from_next(Config, Args) ->
+ [Server | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q = ?config(queue_name, Config),
+
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+
+ #'confirm.select_ok'{} = amqp_channel:call(Ch, #'confirm.select'{}),
+ amqp_channel:register_confirm_handler(Ch, self()),
+ [publish(Ch, Q, <<"msg1">>) || _ <- lists:seq(1, 100)],
+ amqp_channel:wait_for_confirms(Ch, 5000),
+
+ Ch1 = rabbit_ct_client_helpers:open_channel(Config, Server),
+ qos(Ch1, 10, false),
+
+ [Info] = rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_amqqueue,
+ info_all, [<<"/">>, [committed_offset]]),
+
+ %% We'll receive data from the last committed offset, let's check that is not the
+ %% first offset
+ CommittedOffset = proplists:get_value(committed_offset, Info),
+ ?assert(CommittedOffset > 0),
+
+ %% If the offset is not provided, we're subscribing to the tail of the stream
+ amqp_channel:subscribe(
+ Ch1, #'basic.consume'{queue = Q,
+ no_ack = false,
+ consumer_tag = <<"ctag">>,
+ arguments = Args},
+ self()),
+ receive
+ #'basic.consume_ok'{consumer_tag = <<"ctag">>} ->
+ ok
+ end,
+
+ %% Publish a few more
+ [publish(Ch, Q, <<"msg2">>) || _ <- lists:seq(1, 100)],
+ amqp_channel:wait_for_confirms(Ch, 5000),
+
+ %% Yeah! we got them
+ receive_batch(Ch1, 100, 199).
+
+consume_from_replica(Config) ->
+ [Server1, Server2 | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+
+ Ch1 = rabbit_ct_client_helpers:open_channel(Config, Server1),
+ Q = ?config(queue_name, Config),
+
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch1, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+
+ #'confirm.select_ok'{} = amqp_channel:call(Ch1, #'confirm.select'{}),
+ amqp_channel:register_confirm_handler(Ch1, self()),
+ [publish(Ch1, Q, <<"msg1">>) || _ <- lists:seq(1, 100)],
+ amqp_channel:wait_for_confirms(Ch1, 5000),
+
+ Ch2 = rabbit_ct_client_helpers:open_channel(Config, Server2),
+ qos(Ch2, 10, false),
+
+ subscribe(Ch2, Q, false, 0),
+ receive_batch(Ch2, 0, 99).
+
+consume_credit(Config) ->
+ %% Because osiris provides one chunk on every read and we don't want to buffer
+ %% messages in the broker to avoid memory penalties, the credit value won't
+ %% be strict - we allow it into the negative values.
+ %% We can test that after receiving a chunk, no more messages are delivered until
+ %% the credit goes back to a positive value.
+ [Server | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q = ?config(queue_name, Config),
+
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+
+ #'confirm.select_ok'{} = amqp_channel:call(Ch, #'confirm.select'{}),
+ amqp_channel:register_confirm_handler(Ch, self()),
+ %% Let's publish a big batch, to ensure we have more than a chunk available
+ NumMsgs = 100,
+ [publish(Ch, Q, <<"msg1">>) || _ <- lists:seq(1, NumMsgs)],
+ amqp_channel:wait_for_confirms(Ch, 5000),
+
+ Ch1 = rabbit_ct_client_helpers:open_channel(Config, Server),
+
+ %% Let's subscribe with a small credit, easier to test
+ Credit = 2,
+ qos(Ch1, Credit, false),
+ subscribe(Ch1, Q, false, 0),
+
+ %% Receive everything
+ DeliveryTags = receive_batch(),
+
+ %% We receive at least the given credit as we know there are 100 messages in the queue
+ ?assert(length(DeliveryTags) >= Credit),
+
+ %% Let's ack as many messages as we can while avoiding a positive credit for new deliveries
+ {ToAck, Pending} = lists:split(length(DeliveryTags) - Credit, DeliveryTags),
+
+ [ok = amqp_channel:cast(Ch1, #'basic.ack'{delivery_tag = DeliveryTag,
+ multiple = false})
+ || DeliveryTag <- ToAck],
+
+ %% Nothing here, this is good
+ receive
+ {#'basic.deliver'{}, _} ->
+ exit(unexpected_delivery)
+ after 1000 ->
+ ok
+ end,
+
+ %% Let's ack one more, we should receive a new chunk
+ ok = amqp_channel:cast(Ch1, #'basic.ack'{delivery_tag = hd(Pending),
+ multiple = false}),
+
+ %% Yeah, here is the new chunk!
+ receive
+ {#'basic.deliver'{}, _} ->
+ ok
+ after 5000 ->
+ exit(timeout)
+ end.
+
+consume_credit_out_of_order_ack(Config) ->
+ %% Like consume_credit but acknowledging the messages out of order.
+ %% We want to ensure it doesn't behave like multiple, that is if we have
+ %% credit 2 and received 10 messages, sending the ack for the message id
+ %% number 10 should only increase credit by 1.
+ [Server | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q = ?config(queue_name, Config),
+
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+
+ #'confirm.select_ok'{} = amqp_channel:call(Ch, #'confirm.select'{}),
+ amqp_channel:register_confirm_handler(Ch, self()),
+ %% Let's publish a big batch, to ensure we have more than a chunk available
+ NumMsgs = 100,
+ [publish(Ch, Q, <<"msg1">>) || _ <- lists:seq(1, NumMsgs)],
+ amqp_channel:wait_for_confirms(Ch, 5000),
+
+ Ch1 = rabbit_ct_client_helpers:open_channel(Config, Server),
+
+ %% Let's subscribe with a small credit, easier to test
+ Credit = 2,
+ qos(Ch1, Credit, false),
+ subscribe(Ch1, Q, false, 0),
+
+ %% ******* This is the difference with consume_credit
+ %% Receive everything, let's reverse the delivery tags here so we ack out of order
+ DeliveryTags = lists:reverse(receive_batch()),
+
+ %% We receive at least the given credit as we know there are 100 messages in the queue
+ ?assert(length(DeliveryTags) >= Credit),
+
+ %% Let's ack as many messages as we can while avoiding a positive credit for new deliveries
+ {ToAck, Pending} = lists:split(length(DeliveryTags) - Credit, DeliveryTags),
+
+ [ok = amqp_channel:cast(Ch1, #'basic.ack'{delivery_tag = DeliveryTag,
+ multiple = false})
+ || DeliveryTag <- ToAck],
+
+ %% Nothing here, this is good
+ receive
+ {#'basic.deliver'{}, _} ->
+ exit(unexpected_delivery)
+ after 1000 ->
+ ok
+ end,
+
+ %% Let's ack one more, we should receive a new chunk
+ ok = amqp_channel:cast(Ch1, #'basic.ack'{delivery_tag = hd(Pending),
+ multiple = false}),
+
+ %% Yeah, here is the new chunk!
+ receive
+ {#'basic.deliver'{}, _} ->
+ ok
+ after 5000 ->
+ exit(timeout)
+ end.
+
+consume_credit_multiple_ack(Config) ->
+ %% Like consume_credit but acknowledging the messages out of order.
+ %% We want to ensure it doesn't behave like multiple, that is if we have
+ %% credit 2 and received 10 messages, sending the ack for the message id
+ %% number 10 should only increase credit by 1.
+ [Server | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q = ?config(queue_name, Config),
+
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+
+ #'confirm.select_ok'{} = amqp_channel:call(Ch, #'confirm.select'{}),
+ amqp_channel:register_confirm_handler(Ch, self()),
+ %% Let's publish a big batch, to ensure we have more than a chunk available
+ NumMsgs = 100,
+ [publish(Ch, Q, <<"msg1">>) || _ <- lists:seq(1, NumMsgs)],
+ amqp_channel:wait_for_confirms(Ch, 5000),
+
+ Ch1 = rabbit_ct_client_helpers:open_channel(Config, Server),
+
+ %% Let's subscribe with a small credit, easier to test
+ Credit = 2,
+ qos(Ch1, Credit, false),
+ subscribe(Ch1, Q, false, 0),
+
+ %% ******* This is the difference with consume_credit
+ %% Receive everything, let's reverse the delivery tags here so we ack out of order
+ DeliveryTag = lists:last(receive_batch()),
+
+ ok = amqp_channel:cast(Ch1, #'basic.ack'{delivery_tag = DeliveryTag,
+ multiple = true}),
+
+ %% Yeah, here is the new chunk!
+ receive
+ {#'basic.deliver'{}, _} ->
+ ok
+ after 5000 ->
+ exit(timeout)
+ end.
+
+max_length_bytes(Config) ->
+ [Server | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q = ?config(queue_name, Config),
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>},
+ {<<"x-max-length-bytes">>, long, 500},
+ {<<"x-max-segment-size">>, long, 250}])),
+
+ Payload = << <<"1">> || _ <- lists:seq(1, 500) >>,
+
+ #'confirm.select_ok'{} = amqp_channel:call(Ch, #'confirm.select'{}),
+ amqp_channel:register_confirm_handler(Ch, self()),
+ [publish(Ch, Q, Payload) || _ <- lists:seq(1, 100)],
+ amqp_channel:wait_for_confirms(Ch, 5000),
+
+ %% We don't yet have reliable metrics, as the committed offset doesn't work
+ %% as a counter once we start applying retention policies.
+ %% Let's wait for messages and hope these are less than the number of published ones
+ Ch1 = rabbit_ct_client_helpers:open_channel(Config, Server),
+ qos(Ch1, 100, false),
+ subscribe(Ch1, Q, false, 0),
+
+ ?assert(length(receive_batch()) < 100).
+
+max_age(Config) ->
+ [Server | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q = ?config(queue_name, Config),
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>},
+ {<<"x-max-age">>, longstr, <<"10s">>},
+ {<<"x-max-segment-size">>, long, 250}])),
+
+ Payload = << <<"1">> || _ <- lists:seq(1, 500) >>,
+
+ #'confirm.select_ok'{} = amqp_channel:call(Ch, #'confirm.select'{}),
+ amqp_channel:register_confirm_handler(Ch, self()),
+ [publish(Ch, Q, Payload) || _ <- lists:seq(1, 100)],
+ amqp_channel:wait_for_confirms(Ch, 5000),
+
+ timer:sleep(10000),
+
+ %% Let's publish again so the new segments will trigger the retention policy
+ [publish(Ch, Q, Payload) || _ <- lists:seq(1, 100)],
+ amqp_channel:wait_for_confirms(Ch, 5000),
+
+ Ch1 = rabbit_ct_client_helpers:open_channel(Config, Server),
+ qos(Ch1, 200, false),
+ subscribe(Ch1, Q, false, 0),
+ ?assertEqual(100, length(receive_batch())).
+
+leader_failover(Config) ->
+ [Server1, Server2, Server3] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+
+ Ch1 = rabbit_ct_client_helpers:open_channel(Config, Server1),
+ Q = ?config(queue_name, Config),
+
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch1, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+
+ #'confirm.select_ok'{} = amqp_channel:call(Ch1, #'confirm.select'{}),
+ amqp_channel:register_confirm_handler(Ch1, self()),
+ [publish(Ch1, Q, <<"msg">>) || _ <- lists:seq(1, 100)],
+ amqp_channel:wait_for_confirms(Ch1, 5000),
+
+ check_leader_and_replicas(Config, Q, Server1, [Server2, Server3]),
+
+ ok = rabbit_ct_broker_helpers:stop_node(Config, Server1),
+ timer:sleep(30000),
+
+ [Info] = lists:filter(
+ fun(Props) ->
+ QName = rabbit_misc:r(<<"/">>, queue, Q),
+ lists:member({name, QName}, Props)
+ end,
+ rabbit_ct_broker_helpers:rpc(Config, 1, rabbit_amqqueue,
+ info_all, [<<"/">>, [name, leader, members]])),
+ NewLeader = proplists:get_value(leader, Info),
+ ?assert(NewLeader =/= Server1),
+ ok = rabbit_ct_broker_helpers:start_node(Config, Server1).
+
+invalid_policy(Config) ->
+ [Server | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q = ?config(queue_name, Config),
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+ ok = rabbit_ct_broker_helpers:set_policy(
+ Config, 0, <<"ha">>, <<"invalid_policy.*">>, <<"queues">>,
+ [{<<"ha-mode">>, <<"all">>}]),
+ ok = rabbit_ct_broker_helpers:set_policy(
+ Config, 0, <<"ttl">>, <<"invalid_policy.*">>, <<"queues">>,
+ [{<<"message-ttl">>, 5}]),
+
+ [Info] = rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_amqqueue,
+ info_all, [<<"/">>, [policy, operator_policy,
+ effective_policy_definition]]),
+
+ ?assertEqual('', proplists:get_value(policy, Info)),
+ ?assertEqual('', proplists:get_value(operator_policy, Info)),
+ ?assertEqual([], proplists:get_value(effective_policy_definition, Info)),
+ ok = rabbit_ct_broker_helpers:clear_policy(Config, 0, <<"ha">>),
+ ok = rabbit_ct_broker_helpers:clear_policy(Config, 0, <<"ttl">>).
+
+max_age_policy(Config) ->
+ [Server | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q = ?config(queue_name, Config),
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+ ok = rabbit_ct_broker_helpers:set_policy(
+ Config, 0, <<"age">>, <<"max_age_policy.*">>, <<"queues">>,
+ [{<<"max-age">>, <<"1Y">>}]),
+
+ [Info] = rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_amqqueue,
+ info_all, [<<"/">>, [policy, operator_policy,
+ effective_policy_definition]]),
+
+ ?assertEqual(<<"age">>, proplists:get_value(policy, Info)),
+ ?assertEqual('', proplists:get_value(operator_policy, Info)),
+ ?assertEqual([{<<"max-age">>, <<"1Y">>}],
+ proplists:get_value(effective_policy_definition, Info)),
+ ok = rabbit_ct_broker_helpers:clear_policy(Config, 0, <<"age">>).
+
+max_segment_size_policy(Config) ->
+ [Server | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
+
+ Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
+ Q = ?config(queue_name, Config),
+ ?assertEqual({'queue.declare_ok', Q, 0, 0},
+ declare(Ch, Q, [{<<"x-queue-type">>, longstr, <<"stream">>}])),
+ ok = rabbit_ct_broker_helpers:set_policy(
+ Config, 0, <<"segment">>, <<"max_segment_size.*">>, <<"queues">>,
+ [{<<"max-segment-size">>, 5000}]),
+
+ [Info] = rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_amqqueue,
+ info_all, [<<"/">>, [policy, operator_policy,
+ effective_policy_definition]]),
+
+ ?assertEqual(<<"segment">>, proplists:get_value(policy, Info)),
+ ?assertEqual('', proplists:get_value(operator_policy, Info)),
+ ?assertEqual([{<<"max-segment-size">>, 5000}],
+ proplists:get_value(effective_policy_definition, Info)),
+ ok = rabbit_ct_broker_helpers:clear_policy(Config, 0, <<"segment">>).
+
+%%----------------------------------------------------------------------------
+
+delete_queues() ->
+ [{ok, _} = rabbit_amqqueue:delete(Q, false, false, <<"dummy">>)
+ || Q <- rabbit_amqqueue:list()].
+
+declare(Ch, Q) ->
+ declare(Ch, Q, []).
+
+declare(Ch, Q, Args) ->
+ amqp_channel:call(Ch, #'queue.declare'{queue = Q,
+ durable = true,
+ auto_delete = false,
+ arguments = Args}).
+assert_queue_type(Server, Q, Expected) ->
+ Actual = get_queue_type(Server, Q),
+ Expected = Actual.
+
+get_queue_type(Server, Q0) ->
+ QNameRes = rabbit_misc:r(<<"/">>, queue, Q0),
+ {ok, Q1} = rpc:call(Server, rabbit_amqqueue, lookup, [QNameRes]),
+ amqqueue:get_type(Q1).
+
+check_leader_and_replicas(Config, Name, Leader, Replicas0) ->
+ QNameRes = rabbit_misc:r(<<"/">>, queue, Name),
+ [Info] = lists:filter(
+ fun(Props) ->
+ lists:member({name, QNameRes}, Props)
+ end,
+ rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_amqqueue,
+ info_all, [<<"/">>, [name, leader, members]])),
+ ?assertEqual(Leader, proplists:get_value(leader, Info)),
+ Replicas = lists:sort(Replicas0),
+ ?assertEqual(Replicas, lists:sort(proplists:get_value(members, Info))).
+
+publish(Ch, Queue) ->
+ publish(Ch, Queue, <<"msg">>).
+
+publish(Ch, Queue, Msg) ->
+ ok = amqp_channel:cast(Ch,
+ #'basic.publish'{routing_key = Queue},
+ #amqp_msg{props = #'P_basic'{delivery_mode = 2},
+ payload = Msg}).
+
+subscribe(Ch, Queue, NoAck, Offset) ->
+ amqp_channel:subscribe(Ch, #'basic.consume'{queue = Queue,
+ no_ack = NoAck,
+ consumer_tag = <<"ctag">>,
+ arguments = [{<<"x-stream-offset">>, long, Offset}]},
+ self()),
+ receive
+ #'basic.consume_ok'{consumer_tag = <<"ctag">>} ->
+ ok
+ end.
+
+qos(Ch, Prefetch, Global) ->
+ ?assertMatch(#'basic.qos_ok'{},
+ amqp_channel:call(Ch, #'basic.qos'{global = Global,
+ prefetch_count = Prefetch})).
+
+receive_batch(Ch, N, N) ->
+ receive
+ {#'basic.deliver'{delivery_tag = DeliveryTag},
+ #amqp_msg{props = #'P_basic'{headers = [{<<"x-stream-offset">>, long, N}]}}} ->
+ ok = amqp_channel:cast(Ch, #'basic.ack'{delivery_tag = DeliveryTag,
+ multiple = false})
+ after 5000 ->
+ exit({missing_offset, N})
+ end;
+receive_batch(Ch, N, M) ->
+ receive
+ {_,
+ #amqp_msg{props = #'P_basic'{headers = [{<<"x-stream-offset">>, long, S}]}}}
+ when S < N ->
+ exit({unexpected_offset, S});
+ {#'basic.deliver'{delivery_tag = DeliveryTag},
+ #amqp_msg{props = #'P_basic'{headers = [{<<"x-stream-offset">>, long, N}]}}} ->
+ ok = amqp_channel:cast(Ch, #'basic.ack'{delivery_tag = DeliveryTag,
+ multiple = false}),
+ receive_batch(Ch, N + 1, M)
+ after 5000 ->
+ exit({missing_offset, N})
+ end.
+
+receive_batch() ->
+ receive_batch([]).
+
+receive_batch(Acc) ->
+ receive
+ {#'basic.deliver'{delivery_tag = DeliveryTag}, _} ->
+ receive_batch([DeliveryTag | Acc])
+ after 5000 ->
+ lists:reverse(Acc)
+ end.
+
+run_proper(Fun, Args, NumTests) ->
+ ?assertEqual(
+ true,
+ proper:counterexample(
+ erlang:apply(Fun, Args),
+ [{numtests, NumTests},
+ {on_output, fun(".", _) -> ok; % don't print the '.'s on new lines
+ (F, A) -> ct:pal(?LOW_IMPORTANCE, F, A)
+ end}])).
diff --git a/test/simple_ha_SUITE.erl b/test/simple_ha_SUITE.erl
index 013e625159..8b2c1d6ebb 100644
--- a/test/simple_ha_SUITE.erl
+++ b/test/simple_ha_SUITE.erl
@@ -234,8 +234,10 @@ consume_survives(Config,
DeathFun(Config, A),
%% verify that the consumer got all msgs, or die - the await_response
%% calls throw an exception if anything goes wrong....
- rabbit_ha_test_consumer:await_response(ConsumerPid),
+ ct:pal("awaiting produce ~w", [ProducerPid]),
rabbit_ha_test_producer:await_response(ProducerPid),
+ ct:pal("awaiting consumer ~w", [ConsumerPid]),
+ rabbit_ha_test_consumer:await_response(ConsumerPid),
ok.
confirms_survive(Config, DeathFun) ->
diff --git a/test/unit_log_config_SUITE.erl b/test/unit_log_config_SUITE.erl
index 3610fd1a80..6be403fd3e 100644
--- a/test/unit_log_config_SUITE.erl
+++ b/test/unit_log_config_SUITE.erl
@@ -126,6 +126,10 @@ sink_rewrite_sinks() ->
{rabbit_log_mirroring_lager_event,
[{handlers,[{lager_forwarder_backend,[lager_event,info]}]},
{rabbit_handlers,[{lager_forwarder_backend,[lager_event,info]}]}]},
+ {rabbit_log_osiris_lager_event,
+ [{handlers,[{lager_forwarder_backend,[lager_event,info]}]},
+ {rabbit_handlers,
+ [{lager_forwarder_backend,[lager_event,info]}]}]},
{rabbit_log_prelaunch_lager_event,
[{handlers,[{lager_forwarder_backend,[lager_event,info]}]},
{rabbit_handlers,[{lager_forwarder_backend,[lager_event,info]}]}]},
@@ -226,6 +230,10 @@ sink_handlers_merged_with_lager_extra_sinks_handlers(_) ->
{rabbit_log_mirroring_lager_event,
[{handlers,[{lager_forwarder_backend,[lager_event,DefaultLevel]}]},
{rabbit_handlers,[{lager_forwarder_backend,[lager_event,DefaultLevel]}]}]},
+ {rabbit_log_osiris_lager_event,
+ [{handlers,[{lager_forwarder_backend,[lager_event,DefaultLevel]}]},
+ {rabbit_handlers,
+ [{lager_forwarder_backend,[lager_event,DefaultLevel]}]}]},
{rabbit_log_prelaunch_lager_event,
[{handlers,[{lager_forwarder_backend,[lager_event,DefaultLevel]}]},
{rabbit_handlers,[{lager_forwarder_backend,[lager_event,DefaultLevel]}]}]},
@@ -317,6 +325,10 @@ level_sinks() ->
{rabbit_log_mirroring_lager_event,
[{handlers,[{lager_forwarder_backend,[lager_event,error]}]},
{rabbit_handlers,[{lager_forwarder_backend,[lager_event,error]}]}]},
+ {rabbit_log_osiris_lager_event,
+ [{handlers,[{lager_forwarder_backend,[lager_event,info]}]},
+ {rabbit_handlers,
+ [{lager_forwarder_backend,[lager_event,info]}]}]},
{rabbit_log_prelaunch_lager_event,
[{handlers,[{lager_forwarder_backend,[lager_event,info]}]},
{rabbit_handlers,[{lager_forwarder_backend,[lager_event,info]}]}]},
@@ -427,6 +439,10 @@ file_sinks(DefaultLevel) ->
{rabbit_log_mirroring_lager_event,
[{handlers,[{lager_forwarder_backend,[lager_event,DefaultLevel]}]},
{rabbit_handlers,[{lager_forwarder_backend,[lager_event,DefaultLevel]}]}]},
+ {rabbit_log_osiris_lager_event,
+ [{handlers,[{lager_forwarder_backend,[lager_event,DefaultLevel]}]},
+ {rabbit_handlers,
+ [{lager_forwarder_backend,[lager_event,DefaultLevel]}]}]},
{rabbit_log_prelaunch_lager_event,
[{handlers,[{lager_forwarder_backend,[lager_event,DefaultLevel]}]},
{rabbit_handlers,[{lager_forwarder_backend,[lager_event,DefaultLevel]}]}]},
@@ -674,6 +690,10 @@ default_expected_sinks(UpgradeFile) ->
{rabbit_log_mirroring_lager_event,
[{handlers,[{lager_forwarder_backend,[lager_event,info]}]},
{rabbit_handlers,[{lager_forwarder_backend,[lager_event,info]}]}]},
+ {rabbit_log_osiris_lager_event,
+ [{handlers,[{lager_forwarder_backend,[lager_event,info]}]},
+ {rabbit_handlers,
+ [{lager_forwarder_backend,[lager_event,info]}]}]},
{rabbit_log_prelaunch_lager_event,
[{handlers,[{lager_forwarder_backend,[lager_event,info]}]},
{rabbit_handlers,[{lager_forwarder_backend,[lager_event,info]}]}]},
@@ -761,6 +781,10 @@ tty_expected_sinks() ->
{rabbit_log_mirroring_lager_event,
[{handlers,[{lager_forwarder_backend,[lager_event,info]}]},
{rabbit_handlers,[{lager_forwarder_backend,[lager_event,info]}]}]},
+ {rabbit_log_osiris_lager_event,
+ [{handlers,[{lager_forwarder_backend,[lager_event,info]}]},
+ {rabbit_handlers,
+ [{lager_forwarder_backend,[lager_event,info]}]}]},
{rabbit_log_prelaunch_lager_event,
[{handlers,[{lager_forwarder_backend,[lager_event,info]}]},
{rabbit_handlers,[{lager_forwarder_backend,[lager_event,info]}]}]},