Skip to content

Commit a384336

Browse files
BenAtAmazonlukebakken
authored andcommitted
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 bc8a030 commit a384336

File tree

4 files changed

+383
-21
lines changed

4 files changed

+383
-21
lines changed

deps/rabbitmq_peer_discovery_aws/priv/schema/rabbitmq_peer_discovery_aws.schema

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,33 @@ 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+
case string:to_integer(Str) of
124+
{_, ""} -> true;
125+
_ -> false
126+
end
127+
end,
128+
%% Extract and sort numbered paths (hostname_path.1, hostname_path.2, etc.)
129+
NumberedPaths = [{list_to_integer(N), V} ||
130+
{["cluster_formation", "aws", "hostname_path", N], V} <- Settings,
131+
IsNumberString(N)],
132+
case NumberedPaths of
133+
[] -> cuttlefish:unset();
134+
_ ->
135+
SortedPaths = lists:sort(NumberedPaths),
136+
[rabbit_peer_discovery_util:as_list(V) || {_, V} <- SortedPaths]
137+
end
138+
end}.
139+
140+

deps/rabbitmq_peer_discovery_aws/src/rabbit_peer_discovery_aws.erl

Lines changed: 79 additions & 20 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,31 +353,69 @@ 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) ->
363401
List = lists:foldl(fun get_value/2, Props, Path),
364402
case io_lib:latin1_char_list(List) of
365-
true -> List;
366-
_ -> ""
403+
true ->
404+
?LOG_DEBUG("AWS peer discovery: extracted hostname '~ts' from path ~tp", [List, Path]),
405+
List;
406+
_ ->
407+
?LOG_DEBUG("AWS peer discovery: invalid hostname format from path ~tp, result: ~tp", [Path, List]),
408+
""
367409
end.
368410

369411
-spec get_value(string()|integer(), props()) -> props().
370-
get_value(_, []) ->
371-
[];
372-
get_value(Key, Props) when is_integer(Key) ->
373-
{"item", Props2} = lists:nth(Key, Props),
374-
Props2;
412+
get_value(Key, Props) when is_integer(Key), is_list(Props), length(Props) >= Key, Key > 0 ->
413+
case lists:nth(Key, Props) of
414+
{"item", Props2} -> Props2;
415+
_ -> [] % Malformed data
416+
end;
417+
get_value(Key, _Props) when is_integer(Key) ->
418+
[]; % Out of bounds or empty list
375419
get_value(Key, Props) ->
376420
Value = proplists:get_value(Key, Props),
377421
sort_ec2_hostname_path_set_members(Key, Value).
@@ -413,3 +457,18 @@ get_tags() ->
413457
maps:from_list(Value);
414458
_ -> Tags
415459
end.
460+
461+
%% Helper functions for multiple hostname paths support
462+
463+
-spec extract_unique_hostnames([path()], [props()]) -> [string()].
464+
extract_unique_hostnames(Paths, Items) ->
465+
?LOG_DEBUG("AWS peer discovery: extracting hostnames using ~p paths for ~p items",
466+
[length(Paths), length(Items)]),
467+
%% Extract all hostnames from all paths for all items
468+
AllHostnames = [get_hostname(Path, Item) || Path <- Paths, Item <- Items],
469+
%% Filter out empty hostnames and remove duplicates
470+
ValidHostnames = [Name || Name <- AllHostnames, Name =/= ""],
471+
UniqueHostnames = lists:uniq(ValidHostnames),
472+
?LOG_DEBUG("AWS peer discovery: extracted ~p total hostnames, ~p valid, ~p unique: ~tp",
473+
[length(AllHostnames), length(ValidHostnames), length(UniqueHostnames), UniqueHostnames]),
474+
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)