Skip to content

Commit 175a956

Browse files
committed
AWS peer discovery: add multi-hostname path support
Adds support for multiple hostname paths in the AWS peer discovery plugin to enable zero-downtime rolling upgrades during hostname migration scenarios. The implementation allows RabbitMQ nodes to discover peers using multiple hostname paths, ensuring cluster formation succeeds even when nodes are configured with different hostname paths during rolling upgrades. Example usage: cluster_formation.aws.hostname_path.1 = privateDnsName cluster_formation.aws.hostname_path.2 = privateIpAddress
1 parent 742c179 commit 175a956

File tree

4 files changed

+329
-17
lines changed

4 files changed

+329
-17
lines changed

deps/rabbitmq_peer_discovery_aws/priv/schema/rabbitmq_peer_discovery_aws.schema

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,35 @@ fun(Conf) ->
108108
Value -> rabbit_peer_discovery_util:as_list(Value)
109109
end
110110
end}.
111+
112+
%% hostname_paths (multiple paths support)
113+
114+
{mapping, "cluster_formation.aws.hostname_path.$number", "rabbit.cluster_formation.peer_discovery_aws.aws_hostname_paths", [
115+
{datatype, string}
116+
]}.
117+
118+
{translation, "rabbit.cluster_formation.peer_discovery_aws.aws_hostname_paths",
119+
fun(Conf) ->
120+
Settings = cuttlefish_variable:filter_by_prefix("cluster_formation.aws.hostname_path", Conf),
121+
%% Helper function for number validation
122+
IsNumberString = fun(Str) ->
123+
try
124+
_ = list_to_integer(Str),
125+
true
126+
catch
127+
_:_ -> false
128+
end
129+
end,
130+
%% Extract and sort numbered paths (hostname_path.1, hostname_path.2, etc.)
131+
NumberedPaths = [{list_to_integer(N), V} ||
132+
{["cluster_formation", "aws", "hostname_path", N], V} <- Settings,
133+
IsNumberString(N)],
134+
case NumberedPaths of
135+
[] -> cuttlefish:unset();
136+
_ ->
137+
SortedPaths = lists:sort(NumberedPaths),
138+
[rabbit_peer_discovery_util:as_list(V) || {_, V} <- SortedPaths]
139+
end
140+
end}.
141+
142+

deps/rabbitmq_peer_discovery_aws/src/rabbit_peer_discovery_aws.erl

Lines changed: 80 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@
6262
env_variable = "AWS_HOSTNAME_PATH",
6363
default_value = ["privateDnsName"]
6464
},
65+
aws_hostname_paths => #peer_discovery_config_entry_meta{
66+
type = list,
67+
env_variable = "AWS_HOSTNAME_PATHS",
68+
default_value = []
69+
},
6570
aws_use_private_ip => #peer_discovery_config_entry_meta{
6671
type = atom,
6772
env_variable = "AWS_USE_PRIVATE_IP",
@@ -318,10 +323,11 @@ get_hostname_name_from_reservation_set([], Accum) -> Accum;
318323
get_hostname_name_from_reservation_set([{"item", RI}|T], Accum) ->
319324
InstancesSet = proplists:get_value("instancesSet", RI),
320325
Items = [Item || {"item", Item} <- InstancesSet],
321-
HostnamePath = get_hostname_path(),
322-
Hostnames = [get_hostname(HostnamePath, Item) || Item <- Items],
323-
Hostnames2 = [Name || Name <- Hostnames, Name =/= ""],
324-
get_hostname_name_from_reservation_set(T, Accum ++ Hostnames2).
326+
HostnamePaths = get_hostname_paths(),
327+
?LOG_DEBUG("AWS peer discovery: processing reservation with ~p instances using hostname paths: ~tp",
328+
[length(Items), HostnamePaths]),
329+
UniqueHostnames = extract_unique_hostnames(HostnamePaths, Items),
330+
get_hostname_name_from_reservation_set(T, Accum ++ UniqueHostnames).
325331

326332
get_hostname_names(Path) ->
327333
case rabbitmq_aws:api_get_request("ec2", Path) of
@@ -347,23 +353,66 @@ get_hostname_by_tags(Tags) ->
347353
Names
348354
end.
349355

350-
-spec get_hostname_path() -> path().
351-
get_hostname_path() ->
352-
UsePrivateIP = get_config_key(aws_use_private_ip, ?CONFIG_MODULE:config_map(?BACKEND_CONFIG_KEY)),
353-
HostnamePath = get_config_key(aws_hostname_path, ?CONFIG_MODULE:config_map(?BACKEND_CONFIG_KEY)),
354-
FinalPath = case HostnamePath of
355-
["privateDnsName"] when UsePrivateIP -> ["privateIpAddress"];
356-
P -> P
356+
-spec get_hostname_paths() -> [path()].
357+
get_hostname_paths() ->
358+
M = ?CONFIG_MODULE:config_map(?BACKEND_CONFIG_KEY),
359+
UsePrivateIP = get_config_key(aws_use_private_ip, M),
360+
RawPaths = case get_config_key(aws_hostname_paths, M) of
361+
Paths when is_list(Paths), Paths =/= [] ->
362+
?LOG_DEBUG("AWS peer discovery using multiple hostname paths"),
363+
Paths;
364+
_ ->
365+
%% Use single path configuration (including when Paths is [])
366+
SinglePath = get_single_hostname_path_raw(M),
367+
?LOG_DEBUG("AWS peer discovery using single hostname path"),
368+
[SinglePath]
357369
end,
358-
?LOG_DEBUG("AWS peer discovery using hostname path: ~tp", [FinalPath]),
359-
FinalPath.
370+
%% Apply use_private_ip override to all paths consistently
371+
FinalPaths = apply_private_ip_override(RawPaths, UsePrivateIP),
372+
?LOG_DEBUG("AWS peer discovery final hostname paths: ~tp", [FinalPaths]),
373+
FinalPaths.
374+
375+
-spec get_single_hostname_path_raw(map()) -> path().
376+
get_single_hostname_path_raw(ConfigMap) ->
377+
case get_config_key(aws_hostname_path, ConfigMap) of
378+
undefined ->
379+
["privateDnsName"];
380+
P ->
381+
P
382+
end.
383+
384+
-spec apply_private_ip_override([path()], boolean()) -> [path()].
385+
apply_private_ip_override(Paths, UsePrivateIP) ->
386+
apply_private_ip_override(Paths, UsePrivateIP, []).
387+
apply_private_ip_override([], _, Acc) ->
388+
lists:reverse(Acc);
389+
apply_private_ip_override([["privateDnsName"] | Paths], true, Acc0) ->
390+
Acc1 = [["privateIpAddress"] | Acc0],
391+
apply_private_ip_override(Paths, true, Acc1);
392+
apply_private_ip_override([Path | Paths], UsePrivateIP, Acc0) ->
393+
Acc1 = [Path | Acc0],
394+
apply_private_ip_override(Paths, UsePrivateIP, Acc1).
360395

361396
-spec get_hostname(path(), props()) -> string().
397+
get_hostname([], _Props) ->
398+
?LOG_DEBUG("AWS peer discovery: empty hostname path provided"),
399+
""; %% Handle empty paths gracefully
362400
get_hostname(Path, Props) ->
363-
List = lists:foldl(fun get_value/2, Props, Path),
401+
List = try
402+
lists:foldl(fun get_value/2, Props, Path)
403+
catch
404+
Error:Reason ->
405+
?LOG_DEBUG("AWS peer discovery: hostname extraction failed for path ~tp, error: ~tp:~tp",
406+
[Path, Error, Reason]),
407+
""
408+
end,
364409
case io_lib:latin1_char_list(List) of
365-
true -> List;
366-
_ -> ""
410+
true ->
411+
?LOG_DEBUG("AWS peer discovery: extracted hostname '~ts' from path ~tp", [List, Path]),
412+
List;
413+
_ ->
414+
?LOG_DEBUG("AWS peer discovery: invalid hostname format from path ~tp, result: ~tp", [Path, List]),
415+
""
367416
end.
368417

369418
-spec get_value(string()|integer(), props()) -> props().
@@ -413,3 +462,18 @@ get_tags() ->
413462
maps:from_list(Value);
414463
_ -> Tags
415464
end.
465+
466+
%% Helper functions for multiple hostname paths support
467+
468+
-spec extract_unique_hostnames([path()], [props()]) -> [string()].
469+
extract_unique_hostnames(Paths, Items) ->
470+
?LOG_DEBUG("AWS peer discovery: extracting hostnames using ~p paths for ~p items",
471+
[length(Paths), length(Items)]),
472+
%% Extract all hostnames from all paths for all items
473+
AllHostnames = [get_hostname(Path, Item) || Path <- Paths, Item <- Items],
474+
%% Filter out empty hostnames and remove duplicates
475+
ValidHostnames = [Name || Name <- AllHostnames, Name =/= ""],
476+
UniqueHostnames = lists:uniq(ValidHostnames),
477+
?LOG_DEBUG("AWS peer discovery: extracted ~p total hostnames, ~p valid, ~p unique: ~tp",
478+
[length(AllHostnames), length(ValidHostnames), length(UniqueHostnames), UniqueHostnames]),
479+
UniqueHostnames.

deps/rabbitmq_peer_discovery_aws/test/config_schema_SUITE_data/rabbitmq_peer_discovery_aws.snippets

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,5 +103,35 @@
103103
]}
104104
]}
105105
], [rabbitmq_peer_discovery_aws]
106+
},
107+
{aws_hostname_paths_multiple,
108+
"cluster_formation.aws.hostname_path.1 = networkInterfaceSet,2,privateIpAddressesSet,1,privateDnsName
109+
cluster_formation.aws.hostname_path.2 = privateDnsName
110+
cluster_formation.aws.hostname_path.3 = privateIpAddress",
111+
[
112+
{rabbit, [
113+
{cluster_formation, [
114+
{peer_discovery_aws, [
115+
{aws_hostname_paths, [
116+
["networkInterfaceSet", 2, "privateIpAddressesSet", 1, "privateDnsName"],
117+
["privateDnsName"],
118+
["privateIpAddress"]
119+
]}
120+
]}
121+
]}
122+
]}
123+
], [rabbitmq_peer_discovery_aws]
124+
},
125+
{aws_hostname_paths_single_numbered,
126+
"cluster_formation.aws.hostname_path.1 = privateDnsName",
127+
[
128+
{rabbit, [
129+
{cluster_formation, [
130+
{peer_discovery_aws, [
131+
{aws_hostname_paths, [["privateDnsName"]]}
132+
]}
133+
]}
134+
]}
135+
], [rabbitmq_peer_discovery_aws]
106136
}
107137
].

0 commit comments

Comments
 (0)