-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support exchange federation with MQTT 5.0 subscribers
## What? This commit fixes #13040. Prior to this commit, exchange federation crashed if the MQTT topic exchange (`amq.topic` by default) got federated and MQTT 5.0 clients subscribed on the downstream. That's because the federation plugin sends bindings from downstream to upstream via AMQP 0.9.1. However, binding arguments containing Erlang record `mqtt_subscription_opts` (henceforth binding args v1) cannot be encoded in AMQP 0.9.1. ## Why? Federating the MQTT topic exchange could be useful for warm standby use cases. ## How? This commit makes binding arguments a valid AMQP 0.9.1 table (henceforth binding args v2). Binding args v2 can only be used if all nodes support it. Hence binding args v2 comes with feature flag `rabbitmq_4.1.0`. Note that the AMQP over WebSocket [PR](#13071) already introduces this same feature flag. Although the feature flag subsystem supports plugins to define their own feature flags, and the MQTT plugin defined its own feature flags in the past, reusing feature flag `rabbitmq_4.1.0` is simpler. This commit also avoids database migrations for both Mnesia and Khepri if feature flag `rabbitmq_4.1.0` gets enabled. Instead, it's simpler to migrate binding args v1 to binding args v2 at MQTT connection establishment time if the feature flag is enabled. (If the feature flag is disabled at connection etablishment time, but gets enabled during the connection lifetime, the connection keeps using bindings args v1.) This commit adds two new suites: 1. `federation_SUITE` which tests that federating the MQTT topic exchange works, and 2. `feature_flag_SUITE` which tests the binding args migration from v1 to v2.
- Loading branch information
Showing
5 changed files
with
325 additions
and
31 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
%% This Source Code Form is subject to the terms of the Mozilla Public | ||
%% License, v. 2.0. If a copy of the MPL was not distributed with this | ||
%% file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
%% | ||
%% Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. | ||
|
||
%% This suite should be deleted when feature flag 'rabbitmq_4.1.0' becomes required. | ||
-module(feature_flag_SUITE). | ||
-compile([export_all, | ||
nowarn_export_all]). | ||
|
||
-include_lib("eunit/include/eunit.hrl"). | ||
|
||
-import(util, | ||
[connect/2, | ||
connect/3, | ||
non_clean_sess_opts/0 | ||
]). | ||
|
||
-define(RC_SESSION_TAKEN_OVER, 16#8E). | ||
|
||
all() -> | ||
[migrate_binding_args]. | ||
|
||
init_per_suite(Config) -> | ||
rabbit_ct_helpers:log_environment(), | ||
Config1 = rabbit_ct_helpers:set_config( | ||
Config, | ||
[{mqtt_version, v5}, | ||
{rmq_nodename_suffix, ?MODULE}]), | ||
Config2 = rabbit_ct_helpers:merge_app_env( | ||
Config1, | ||
{rabbit, [{forced_feature_flags_on_init, []}]}), | ||
rabbit_ct_helpers:run_setup_steps( | ||
Config2, | ||
rabbit_ct_broker_helpers:setup_steps() ++ | ||
rabbit_ct_client_helpers:setup_steps()). | ||
|
||
end_per_suite(Config) -> | ||
rabbit_ct_helpers:run_teardown_steps( | ||
Config, | ||
rabbit_ct_client_helpers:teardown_steps() ++ | ||
rabbit_ct_broker_helpers:teardown_steps()). | ||
|
||
init_per_testcase(Testcase, Config) -> | ||
rabbit_ct_helpers:testcase_started(Config, Testcase). | ||
|
||
end_per_testcase(Testcase, Config) -> | ||
rabbit_ct_helpers:testcase_finished(Config, Testcase). | ||
|
||
migrate_binding_args(Config) -> | ||
%% Feature flag rabbitmq_4.1.0 enables binding arguments v2. | ||
FeatureFlag = 'rabbitmq_4.1.0', | ||
?assertNot(rabbit_ct_broker_helpers:is_feature_flag_enabled(Config, FeatureFlag)), | ||
|
||
Sub1a = connect(<<"sub 1">>, Config, non_clean_sess_opts()), | ||
{ok, _, [0]} = emqtt:subscribe(Sub1a, <<"x/+">>, qos0), | ||
ok = emqtt:disconnect(Sub1a), | ||
|
||
Sub2a = connect(<<"sub 2">>, Config,non_clean_sess_opts()), | ||
{ok, _, [0, 1]} = emqtt:subscribe( | ||
Sub2a, | ||
#{'Subscription-Identifier' => 9}, | ||
[{<<"x/y">>, [{nl, false}, {rap, false}, {qos, qos0}]}, | ||
{<<"z">>, [{nl, true}, {rap, true}, {qos, qos1}]}]), | ||
|
||
Pub = connect(<<"pub">>, Config), | ||
{ok, _} = emqtt:publish(Pub, <<"x/y">>, <<"m1">>, [{retain, true}, {qos, 1}]), | ||
receive {publish, #{client_pid := Sub2a, | ||
qos := 0, | ||
topic := <<"x/y">>, | ||
payload := <<"m1">>, | ||
retain := false}} -> ok | ||
after 10_000 -> ct:fail({missing_publish, ?LINE}) | ||
end, | ||
|
||
?assertEqual(ok, rabbit_ct_broker_helpers:enable_feature_flag(Config, FeatureFlag)), | ||
|
||
%% Connecting causes binding args to be migrated from v1 to v2. | ||
Sub1b = connect(<<"sub 1">>, Config, [{clean_start, false}]), | ||
receive {publish, #{client_pid := Sub1b, | ||
qos := 0, | ||
topic := <<"x/y">>, | ||
payload := <<"m1">>}} -> ok | ||
after 10_000 -> ct:fail({missing_publish, ?LINE}) | ||
end, | ||
|
||
unlink(Sub2a), | ||
%% Connecting causes binding args to be migrated from v1 to v2. | ||
Sub2b = connect(<<"sub 2">>, Config, [{clean_start, false}]), | ||
receive {disconnected, ?RC_SESSION_TAKEN_OVER, #{}} -> ok | ||
after 10_000 -> ct:fail({missing_disconnected, ?LINE}) | ||
end, | ||
|
||
{ok, _} = emqtt:publish(Sub2b, <<"z">>, <<"m2">>, qos1), | ||
%% We should not receive m2 since it's a local publish. | ||
{ok, _} = emqtt:publish(Pub, <<"z">>, <<"m3">>, [{retain, true}, {qos, qos1}]), | ||
receive {publish, Publish} -> | ||
?assertMatch(#{client_pid := Sub2b, | ||
qos := 1, | ||
topic := <<"z">>, | ||
payload := <<"m3">>, | ||
properties := #{'Subscription-Identifier' := 9}, | ||
retain := true}, | ||
Publish) | ||
after 10_000 -> ct:fail({missing_publish, ?LINE}) | ||
end, | ||
|
||
ok = emqtt:disconnect(Sub1b), | ||
ok = emqtt:disconnect(Sub2b), | ||
ok = emqtt:disconnect(Pub). |
Oops, something went wrong.