Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
a15c087
Extend QQ grow command to support target quorum cluster size
Ayanda-D May 8, 2025
bcb61e3
Add tests for QQ grow to target quorum cluster size command
Ayanda-D May 8, 2025
be893fe
Update QQ grow command tests to reflect correct use of queue-pattern …
Ayanda-D May 8, 2025
d487ebf
Dont allow QQ grow for target quorum cluster size less than 0 in rabb…
Ayanda-D May 8, 2025
2b3a6ab
Randomly select next target node on grow to quorum cluster size
Ayanda-D May 9, 2025
9228b6d
Ensure to only grow QQs when all existing members are in 'voter' status
Ayanda-D May 14, 2025
e231b23
Update QQ grow validate_execution_environment step to only
Ayanda-D May 27, 2025
8f06d03
Use Integer.parse/1 to mediate and parse target-cluster-size argument…
Ayanda-D May 29, 2025
67184c7
Revert "Extend QQ grow command to support target quorum cluster size"
Ayanda-D Jul 18, 2025
b31b521
Revert QQ grow-to-N in original grow command
Ayanda-D Jul 18, 2025
75bc533
use a separate rabbitmq-queues grow_to_count <count> command
Ayanda-D Jul 18, 2025
b635f8d
single validator in use and ensure test uses node count arg
Ayanda-D Jul 18, 2025
fbdf717
ensure grow_to_count node-count argument is an integer
Ayanda-D Oct 10, 2025
507a32d
handle exceptions in rabbit_ct_helpers:await_condition_with_retries/2…
Ayanda-D Jul 22, 2025
6d6773e
Revert "Handle exceptions in rabbit_ct_helpers:await_condition_with_r…
michaelklishin Jul 22, 2025
670dd00
Support concurrent links with stream filtering
ansd Jul 18, 2025
d218166
Expose ra counters (#13895)
mkuratczyk Jul 24, 2025
22203c1
auth_SUITE: Wait for connection tracking to be up-to-date
dumbbell Jul 15, 2025
879566a
Introduce a few new rabbit_plugins and rabbit_nodes functions
michaelklishin Aug 1, 2025
11f9581
Add spring authorization server for testing purposes
MarcialRosales Aug 6, 2025
337b730
Link to new Stream Filtering docs
ansd Aug 7, 2025
aa29177
Permit amqp_filter_set_bug by default (#14361)
ansd Aug 11, 2025
fe366b1
Add config option for enabling local_random_exchange
Aug 8, 2025
c8d39db
Add config option for enabling local_random_exchange
Aug 8, 2025
aa54082
Add config option for enabling local_random_exchange
udeeksha30-netizen Aug 11, 2025
09453e3
Addressed requested changes
udeeksha30-netizen Aug 11, 2025
65833b1
rabbitmq_shovel.schema: remove an outdated link
michaelklishin Aug 15, 2025
3ba1dbd
Local shovels
dcorbacho Jun 23, 2025
0c338a8
WIP
dcorbacho Jul 16, 2025
f90fa73
Local shovel: fix initial delivery count and state handling
dcorbacho Jul 18, 2025
46d7d35
Shovel tests: ignore nodename
dcorbacho Jul 22, 2025
9a3a8e5
Shovel: ignore expected log exceptions in local_SUITE
michaelklishin Aug 20, 2025
d0ba3b6
Re-arrange shovel test suites
michaelklishin Aug 29, 2025
61aa15f
Local shovels: exclude tests in mixed-versions with 3.13.x (#14482)
dcorbacho Sep 2, 2025
f4b54ca
Implement LDAP credentials validation via HTTP API
lukebakken Aug 21, 2025
462183e
Shovel: fix deletion of terminated shovels
dcorbacho Sep 29, 2025
010c8d9
Shovels: return hosting node in terminated shovel status
dcorbacho Sep 29, 2025
da66874
Shovels: make changes to shovel status backward compatible
dcorbacho Sep 30, 2025
453cb52
Revert "Implement LDAP credentials validation via HTTP API"
michaelklishin Sep 30, 2025
d7e084a
ensures the pending returns an integer and not a list for shovels
markusgust Sep 25, 2025
3db784c
fixup: addressing feedback and cleaning up tests
markusgust Sep 29, 2025
1166f2b
Deny amqp_filter_set_bug by default
ansd Oct 14, 2025
0cedd32
Require feature flag `rabbitmq_4.0.0`
ansd Oct 14, 2025
6288edb
Add `stream.read_ahead_limit` config option
the-mikedavis Nov 3, 2025
f508645
Fix quorum queue `drop-head` dead letter order
ansd Nov 10, 2025
053f04d
Add release notes
ansd Nov 11, 2025
ce36fe9
Add QQ at most once dead letter ordering tests
ansd Nov 13, 2025
e99a447
Fix UTF-8 encoding for trace file downloads
bas0N Nov 18, 2025
99f280b
Use charsets_provided callback for charset negotiation
bas0N Nov 18, 2025
735d5f9
IBM MQ testing image action: try an older actions/checkout
michaelklishin Nov 20, 2025
ad58bd2
Bump pivotalrabbitmq/ibm-mqadvanced-server-dev image tag to 9.4.0.16
michaelklishin Nov 20, 2025
81af72d
Provide queue name and reason to publisher for rejected messages
ansd Dec 5, 2025
b18b3ee
assert that quorum queues are always durable on initialization
Ayanda-D Jul 29, 2025
cfc2238
Annotate oauth_resource_server with index attribute
MarcialRosales Dec 3, 2025
e287b18
/login endpoint accepts login preferences
MarcialRosales Dec 9, 2025
d56de3a
Test login preferences
MarcialRosales Dec 10, 2025
45f1124
Remove log statement
MarcialRosales Dec 10, 2025
d254fe5
Return friendly error when AMQP 1.0 receiver has no source
lukebakken Dec 16, 2025
4f427f5
Follow-up of #15147
ansd Dec 17, 2025
9f101aa
Add a missing CLI command clause #13873
michaelklishin Dec 19, 2025
b05b28c
Fix a #13873 #15166 resolution artifact
michaelklishin Dec 19, 2025
403995e
Fix another merge conflict resolution issue #13873 #14926
michaelklishin Dec 19, 2025
03ec951
Fix a #13873 #15166 d7e084a2f4 conflict resolution artifact
michaelklishin Dec 19, 2025
726a8dd
shovel_status_command_SUITE: ignore one more expected log exception
michaelklishin Dec 19, 2025
066ae35
More defensive merge_defaults/2 #13873 #15166
michaelklishin Dec 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/authorization-server-make.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ on:
paths:
- .github/workflows/authorization-server-make.yaml
- selenium/authorization-server

env:
REGISTRY_IMAGE: pivotalrabbitmq/spring-authorization-server
IMAGE_TAG: 0.0.11
jobs:
docker:
runs-on: ubuntu-latest
steps:

- name: CHECKOUT REPOSITORY
uses: actions/checkout@v6

Expand All @@ -28,10 +28,10 @@ jobs:
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}

- name: Build and push
uses: docker/build-push-action@v6
with:
context: selenium/authorization-server
context: selenium/authorization-server
push: true
tags: ${{ env.REGISTRY_IMAGE }}:${{ env.IMAGE_TAG }}
2 changes: 1 addition & 1 deletion .github/workflows/test-management-ui-for-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,4 @@ jobs:
with:
name: test-artifacts-${{ matrix.browser }}-${{ matrix.erlang_version }}
path: ${{ env.SELENIUM_ARTIFACTS }}/*

5 changes: 5 additions & 0 deletions deps/rabbit/priv/schema/rabbit.schema
Original file line number Diff line number Diff line change
Expand Up @@ -2859,6 +2859,11 @@ end}.
{mapping, "stream.read_ahead", "rabbit.stream_read_ahead",
[{datatype, [{enum, [true, false]}, integer, string]}]}.

{mapping, "stream.read_ahead_limit", "rabbit.stream_read_ahead_limit", [
{datatype, [integer, string]},
{validators, ["is_supported_information_unit"]}
]}.

{mapping, "cluster_tags.$tag", "rabbit.cluster_tags", [
{datatype, [binary]}
]}.
Expand Down
1 change: 1 addition & 0 deletions deps/rabbit/src/rabbit_amqp_session.erl
Original file line number Diff line number Diff line change
Expand Up @@ -2613,6 +2613,7 @@ rejected(QNameBin, down) ->
[{{symbol, <<"queue">>}, {utf8, QNameBin}},
{{symbol, <<"reason">>}, {symbol, <<"unavailable">>}}]}}}.


maybe_grant_link_credit(Credit, MaxLinkCredit, DeliveryCount, NumUnconfirmed, Handle) ->
case grant_link_credit(Credit, MaxLinkCredit, NumUnconfirmed) of
true ->
Expand Down
26 changes: 13 additions & 13 deletions deps/rabbit/src/rabbit_fifo.erl
Original file line number Diff line number Diff line change
Expand Up @@ -1638,27 +1638,27 @@ drop_head(#?STATE{ra_indexes = Indexes0} = State0, Effects) ->
#?STATE{cfg = #cfg{dead_letter_handler = DLH},
dlx = DlxState} = State = State3,
{_, DlxEffects} = rabbit_fifo_dlx:discard([Msg], maxlen, DLH, DlxState),
{State, combine_effects(DlxEffects, Effects)};
{State, add_drop_head_effects(DlxEffects, Effects)};
empty ->
{State0, Effects}
end.

%% combine global counter update effects to avoid bulding a huge list of
%% effects if many messages are dropped at the same time as could happen
%% when the `max_length' is changed via a configuration update.
combine_effects([{mod_call,
rabbit_global_counters,
messages_dead_lettered,
[Reason, rabbit_quorum_queue, Type, NewLen]}],
[{mod_call,
rabbit_global_counters,
messages_dead_lettered,
[Reason, rabbit_quorum_queue, Type, PrevLen]} | Rem]) ->
add_drop_head_effects([{mod_call,
rabbit_global_counters,
messages_dead_lettered,
[Reason, rabbit_quorum_queue, Type, NewLen]}],
[{mod_call,
rabbit_global_counters,
messages_dead_lettered,
[Reason, rabbit_quorum_queue, Type, PrevLen]} | Rem]) ->
%% combine global counter update effects to avoid bulding a huge list of
%% effects if many messages are dropped at the same time as could happen
%% when the `max_length' is changed via a configuration update.
[{mod_call,
rabbit_global_counters,
messages_dead_lettered,
[Reason, rabbit_quorum_queue, Type, PrevLen + NewLen]} | Rem];
combine_effects(New, Old) ->
add_drop_head_effects(New, Old) ->
New ++ Old.

maybe_set_msg_ttl(Msg, RaCmdTs, Header,
Expand Down
1 change: 1 addition & 0 deletions deps/rabbit/src/rabbit_plugins.erl
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ list() ->
PluginsPath = plugins_dir(),
list(PluginsPath).


%% @doc Get the list of plugins which are ready to be enabled.

-spec list(string()) -> [#plugin{}].
Expand Down
107 changes: 91 additions & 16 deletions deps/rabbit/src/rabbit_quorum_queue.erl
Original file line number Diff line number Diff line change
Expand Up @@ -1515,29 +1515,20 @@ shrink_all(Node) ->
amqqueue:get_type(Q) == ?MODULE,
lists:member(Node, get_nodes(Q))].


-spec grow(node() | integer(), binary(), binary(), all | even) ->
[{rabbit_amqqueue:name(),
{ok, pos_integer()} | {error, pos_integer(), term()}}].
grow(Node, VhostSpec, QueueSpec, Strategy) ->
grow(Node, VhostSpec, QueueSpec, Strategy, promotable).

-spec grow(node(), binary(), binary(), all | even, membership()) ->
-spec grow(node() | integer(), binary(), binary(), all | even, membership()) ->
[{rabbit_amqqueue:name(),
{ok, pos_integer()} | {error, pos_integer(), term()}}].
grow(Node, VhostSpec, QueueSpec, Strategy, Membership) ->
grow(Node, VhostSpec, QueueSpec, Strategy, Membership) when is_atom(Node) ->
Running = rabbit_nodes:list_running(),
[begin
Size = length(get_nodes(Q)),
QName = amqqueue:get_name(Q),
?LOG_INFO("~ts: adding a new member (replica) on node ~w",
[rabbit_misc:rs(QName), Node]),
case add_member(Q, Node, Membership) of
ok ->
{QName, {ok, Size + 1}};
{error, Err} ->
?LOG_WARNING(
"~ts: failed to add member (replica) on node ~w, error: ~w",
[rabbit_misc:rs(QName), Node, Err]),
{QName, {error, Size, Err}}
end
maybe_grow(Q, Node, Membership, Size)
end
|| Q <- rabbit_amqqueue:list(),
amqqueue:get_type(Q) == ?MODULE,
Expand All @@ -1547,7 +1538,91 @@ grow(Node, VhostSpec, QueueSpec, Strategy, Membership) ->
lists:member(Node, Running),
matches_strategy(Strategy, get_nodes(Q)),
is_match(amqqueue:get_vhost(Q), VhostSpec) andalso
is_match(get_resource_name(amqqueue:get_name(Q)), QueueSpec) ].
is_match(get_resource_name(amqqueue:get_name(Q)), QueueSpec) ];

grow(QuorumClusterSize, VhostSpec, QueueSpec, Strategy, Membership)
when is_integer(QuorumClusterSize), QuorumClusterSize > 0 ->
Running = rabbit_nodes:list_running(),
TotalRunning = length(Running),

TargetQuorumClusterSize =
if QuorumClusterSize > TotalRunning ->
%% we can't grow beyond total running nodes
TotalRunning;
true ->
QuorumClusterSize
end,

lists:flatten(
[begin
QNodes = get_nodes(Q),
case length(QNodes) of
Size when Size < TargetQuorumClusterSize ->
TargetAvailableNodes = Running -- QNodes,
N = length(TargetAvailableNodes),
Node = lists:nth(rand:uniform(N), TargetAvailableNodes),
maybe_grow(Q, Node, Membership, Size);
_ ->
[]
end
end
|| _ <- lists:seq(1, TargetQuorumClusterSize),
Q <- rabbit_amqqueue:list(),
amqqueue:get_type(Q) == ?MODULE,
matches_strategy(Strategy, get_nodes(Q)),
is_match(amqqueue:get_vhost(Q), VhostSpec) andalso
is_match(get_resource_name(amqqueue:get_name(Q)), QueueSpec)]);

grow(QuorumClusterSize, _VhostSpec, _QueueSpec, _Strategy, _Membership)
when is_integer(QuorumClusterSize) ->
rabbit_log:warning(
"cannot grow queues to a quorum cluster size less than zero (~tp)",
[QuorumClusterSize]),
{error, bad_quorum_cluster_size}.

maybe_grow(Q, Node, Membership, Size) ->
QNodes = get_nodes(Q),
maybe_grow(Q, Node, Membership, Size, QNodes).

maybe_grow(Q, Node, Membership, Size, QNodes) ->
QName = amqqueue:get_name(Q),
{ok, RaName} = qname_to_internal_name(QName),
case check_all_memberships(RaName, QNodes, voter) of
true ->
?LOG_INFO("~ts: adding a new member (replica) on node ~w",
[rabbit_misc:rs(QName), Node]),
case add_member(Q, Node, Membership) of
ok ->
{QName, {ok, Size + 1}};
{error, Err} ->
?LOG_WARNING(
"~ts: failed to add member (replica) on node ~w, error: ~w",
[rabbit_misc:rs(QName), Node, Err]),
{QName, {error, Size, Err}}
end;
false ->
Err = {error, non_voters_found},
?LOG_WARNING(
"~ts: failed to add member (replica) on node ~w, error: ~w",
[rabbit_misc:rs(QName), Node, Err]),
{QName, {error, Size, Err}}
end.

%% Compare local membership states of all nodes in parallel.
%%
%% Note a few things:
%% 1. This function intentionally queries local member state and not the leader
%% 2. ra:key_metrics/1 is sequential and not parallel
%% 3. ra:key_metrics/1 is not multicall-friendly because it relies on erlang:node/0
check_all_memberships(RaName, QNodes, CompareMembership) ->
case rpc:multicall(QNodes, ets, lookup, [ra_state, RaName]) of
{Result, []} ->
lists:all(
fun(M) -> M == CompareMembership end,
[Membership || [{_RaName, _RaState, Membership}] <- Result]);
_ ->
false
end.

-spec transfer_leadership(amqqueue:amqqueue(), node()) ->
{migrated, node()} | {not_migrated, atom()}.
Expand Down
1 change: 1 addition & 0 deletions deps/rabbit/src/rabbit_stream_queue.erl
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@ credit(QName, CTag, DeliveryCountRcv, LinkCreditRcv, Drain,

supports_stateful_delivery() -> true.


deliver(QSs, Msg, Options) ->
lists:foldl(
fun({Q, stateless}, {Qs, Actions}) ->
Expand Down
113 changes: 112 additions & 1 deletion deps/rabbit/test/quorum_queue_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ groups() ->
node_removal_is_not_quorum_critical,
select_nodes_with_least_replicas,
select_nodes_with_least_replicas_node_down,
subscribe_from_each
subscribe_from_each,
grow_queue


]},
Expand Down Expand Up @@ -1768,6 +1769,116 @@ dont_leak_file_handles(Config) ->
rabbit_ct_client_helpers:close_channel(C),
ok.

grow_queue(Config) ->
[Server0, Server1, Server2, _Server3, _Server4] =
rabbit_ct_broker_helpers:get_node_configs(Config, nodename),

Ch = rabbit_ct_client_helpers:open_channel(Config, Server0),
QQ = ?config(queue_name, Config),
AQ = ?config(alt_queue_name, Config),
?assertEqual({'queue.declare_ok', QQ, 0, 0},
declare(Ch, QQ, [{<<"x-queue-type">>, longstr, <<"quorum">>},
{<<"x-quorum-initial-group-size">>, long, 5}])),
?assertEqual({'queue.declare_ok', AQ, 0, 0},
declare(Ch, AQ, [{<<"x-queue-type">>, longstr, <<"quorum">>},
{<<"x-quorum-initial-group-size">>, long, 5}])),

QQs = [QQ, AQ],
MsgCount = 3,

[begin
RaName = ra_name(Q),
rabbit_ct_client_helpers:publish(Ch, Q, MsgCount),
wait_for_messages_ready([Server0], RaName, MsgCount),
{ok, Q0} = rpc:call(Server0, rabbit_amqqueue, lookup, [Q, <<"/">>]),
#{nodes := Nodes0} = amqqueue:get_type_state(Q0),
?assertEqual(5, length(Nodes0))
end || Q <- QQs],

rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_quorum_queue,
force_all_queues_shrink_member_to_current_member, []),

TargetClusterSize_1 = 1,
assert_grown_queues(QQs, Server0, TargetClusterSize_1, MsgCount),

%% grow queues to node 'Server1'
TargetClusterSize_2 = 2,
Result1 = rpc:call(Server0, rabbit_quorum_queue, grow, [Server1, <<"/">>, <<".*">>, all]),
%% [{{resource,<<"/">>,queue,<<"grow_queue">>},{ok,2}},
%% {{resource,<<"/">>,queue,<<"grow_queue_alt">>},{ok,2}},...]
?assert(lists:all(fun({_, {R, _}}) -> R =:= ok end, Result1)),
assert_grown_queues(QQs, Server0, TargetClusterSize_2, MsgCount),

%% grow queues to quorum cluster size '2' has no effect
Result2 = rpc:call(Server0, rabbit_quorum_queue, grow, [TargetClusterSize_2, <<"/">>, <<".*">>, all]),
?assertEqual([], Result2),
assert_grown_queues(QQs, Server0, TargetClusterSize_2, MsgCount),

%% grow queues to quorum cluster size '3'
TargetClusterSize_3 = 3,
Result3 = rpc:call(Server0, rabbit_quorum_queue, grow, [TargetClusterSize_3, <<"/">>, <<".*">>, all, voter]),
?assert(lists:all(fun({_, {R, _}}) -> R =:= ok end, Result3)),
assert_grown_queues(QQs, Server0, TargetClusterSize_3, MsgCount),

%% grow queues to quorum cluster size '5'
TargetClusterSize_5 = 5,
Result4 = rpc:call(Server0, rabbit_quorum_queue, grow, [TargetClusterSize_5, <<"/">>, <<".*">>, all, voter]),
?assert(lists:all(fun({_, {R, _}}) -> R =:= ok end, Result4)),
assert_grown_queues(QQs, Server0, TargetClusterSize_5, MsgCount),

%% shrink all queues again down to 1 member
rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_quorum_queue,
force_all_queues_shrink_member_to_current_member, []),
assert_grown_queues(QQs, Server0, TargetClusterSize_1, MsgCount),

%% grow queues to quorum cluster size > '5' (limit = 5).
TargetClusterSize_10 = 10,
Result5 = rpc:call(Server0, rabbit_quorum_queue, grow, [TargetClusterSize_10, <<"/">>, <<".*">>, all]),
?assert(lists:all(fun({_, {R, _}}) -> R =:= ok end, Result5)),
assert_grown_queues(QQs, Server0, TargetClusterSize_5, MsgCount),

%% shrink all queues again down to 1 member
rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_quorum_queue,
force_all_queues_shrink_member_to_current_member, []),
assert_grown_queues(QQs, Server0, TargetClusterSize_1, MsgCount),

%% attempt to grow queues to quorum cluster size < '0'.
BadTargetClusterSize = -5,
?assertEqual({error, bad_quorum_cluster_size},
rpc:call(Server0, rabbit_quorum_queue, grow, [BadTargetClusterSize, <<"/">>, <<".*">>, all])),

%% shrink all queues again down to 1 member
rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_quorum_queue,
force_all_queues_shrink_member_to_current_member, []),
assert_grown_queues(QQs, Server0, TargetClusterSize_1, MsgCount),

%% grow queues to node 'Server1': non_voter
rpc:call(Server0, rabbit_quorum_queue, grow, [Server1, <<"/">>, <<".*">>, all, non_voter]),
assert_grown_queues(QQs, Server0, TargetClusterSize_2, MsgCount),

%% grow queues to node 'Server2': fail, non_voters found
Result6 = rpc:call(Server0, rabbit_quorum_queue, grow, [Server2, <<"/">>, <<".*">>, all, voter]),
%% [{{resource,<<"/">>,queue,<<"grow_queue">>},{error, 2, {error, non_voters_found}},
%% {{resource,<<"/">>,queue,<<"grow_queue_alt">>},{error, 2, {error, non_voters_found}},...]
?assert(lists:all(
fun({_, Err}) -> Err =:= {error, TargetClusterSize_2, {error, non_voters_found}} end, Result6)),
assert_grown_queues(QQs, Server0, TargetClusterSize_2, MsgCount),

%% grow queues to target quorum cluster size '5': fail, non_voters found
Result7 = rpc:call(Server0, rabbit_quorum_queue, grow, [TargetClusterSize_5, <<"/">>, <<".*">>, all]),
?assert(lists:all(
fun({_, Err}) -> Err =:= {error, TargetClusterSize_2, {error, non_voters_found}} end, Result7)),
assert_grown_queues(QQs, Server0, TargetClusterSize_2, MsgCount).

assert_grown_queues(Qs, Node, TargetClusterSize, MsgCount) ->
[begin
RaName = ra_name(Q),
wait_for_messages_ready([Node], RaName, MsgCount),
{ok, Q0} = rpc:call(Node, rabbit_amqqueue, lookup, [Q, <<"/">>]),
#{nodes := Nodes0} = amqqueue:get_type_state(Q0),
?assertEqual(TargetClusterSize, length(Nodes0))
end || Q <- Qs].

gh_12635(Config) ->
% https://github.com/rabbitmq/rabbitmq-server/issues/12635
[Server0, _Server1, Server2] =
Expand Down
Loading
Loading