Skip to content

Commit f6c0f58

Browse files
authored
support srv cluster queries (#16)
1 parent 420c0bc commit f6c0f58

File tree

2 files changed

+97
-18
lines changed

2 files changed

+97
-18
lines changed

lib/dns_cluster.ex

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,26 @@ defmodule DNSCluster do
5858
{:error, _} -> []
5959
end
6060
end
61+
62+
def lookup(query, type) when is_binary(query) and type in [:srv] do
63+
case :inet_res.getbyname(~c"#{query}", type) do
64+
{:ok, hostent(h_addr_list: srv_list)} ->
65+
lookup_hosts(srv_list)
66+
67+
{:error, _} ->
68+
[]
69+
end
70+
end
71+
72+
defp lookup_hosts(srv_list) do
73+
srv_list
74+
|> Enum.flat_map(fn {_prio, _weight, _port, host_name} ->
75+
case :inet.gethostbyname(host_name) do
76+
{:ok, hostent(h_addr_list: addr_list)} -> addr_list
77+
{:error, _} -> []
78+
end
79+
end)
80+
end
6181
end
6282

6383
@doc ~S"""
@@ -70,6 +90,8 @@ defmodule DNSCluster do
7090
`"myapp.internal"` or `["foo.internal", "bar.internal"]`. If the basename
7191
differs between nodes, a tuple of `{basename, query}` can be provided as well.
7292
The value `:ignore` can be used to ignore starting the DNSCluster.
93+
* `:resource_types` - the resource record types that are used for node discovery.
94+
Defaults to `[:a, :aaaa]` and also supports the `:srv` type.
7395
* `:interval` - the millisec interval between DNS queries. Defaults to `5000`.
7496
* `:connect_timeout` - the millisec timeout to allow discovered nodes to connect.
7597
Defaults to `10_000`.
@@ -86,33 +108,43 @@ defmodule DNSCluster do
86108
GenServer.start_link(__MODULE__, opts, name: Keyword.get(opts, :name, __MODULE__))
87109
end
88110

111+
@valid_resource_types [:a, :aaaa, :srv]
112+
89113
@impl true
90114
def init(opts) do
115+
resource_types = Keyword.get(opts, :resource_types, [:a, :aaaa])
116+
91117
case Keyword.fetch(opts, :query) do
92118
{:ok, :ignore} ->
93119
:ignore
94120

95121
{:ok, query} ->
96-
if valid_query?(query) do
97-
warn_on_invalid_dist()
98-
resolver = Keyword.get(opts, :resolver, Resolver)
99-
100-
state = %{
101-
interval: Keyword.get(opts, :interval, 5_000),
102-
basename: resolver.basename(node()),
103-
query: List.wrap(query),
104-
log: Keyword.get(opts, :log, false),
105-
poll_timer: nil,
106-
connect_timeout: Keyword.get(opts, :connect_timeout, 10_000),
107-
resolver: resolver
108-
}
109-
110-
{:ok, state, {:continue, :discover_ips}}
111-
else
122+
if not valid_query?(query) do
112123
raise ArgumentError,
113124
"expected :query to be a string, {basename, query}, or list, got: #{inspect(query)}"
114125
end
115126

127+
if not valid_resource_types?(resource_types) do
128+
raise ArgumentError,
129+
"expected :resource_types to be a subset of [:a, :aaaa, :srv], got: #{inspect(resource_types)}"
130+
end
131+
132+
warn_on_invalid_dist()
133+
resolver = Keyword.get(opts, :resolver, Resolver)
134+
135+
state = %{
136+
interval: Keyword.get(opts, :interval, 5_000),
137+
basename: resolver.basename(node()),
138+
query: List.wrap(query),
139+
resource_types: resource_types,
140+
log: Keyword.get(opts, :log, false),
141+
poll_timer: nil,
142+
connect_timeout: Keyword.get(opts, :connect_timeout, 10_000),
143+
resolver: resolver
144+
}
145+
146+
{:ok, state, {:continue, :discover_ips}}
147+
116148
:error ->
117149
raise ArgumentError, "missing required :query option in #{inspect(opts)}"
118150
end
@@ -165,8 +197,8 @@ defmodule DNSCluster do
165197
%{state | poll_timer: Process.send_after(self(), :discover_ips, state.interval)}
166198
end
167199

168-
defp discover_ips(%{resolver: resolver, query: queries} = state) do
169-
[:a, :aaaa]
200+
defp discover_ips(%{resolver: resolver, query: queries, resource_types: resource_types} = state) do
201+
resource_types
170202
|> Enum.flat_map(fn type ->
171203
Enum.flat_map(queries, fn query ->
172204
{basename, query} =
@@ -199,6 +231,12 @@ defmodule DNSCluster do
199231
end)
200232
end
201233

234+
defp valid_resource_types?([]), do: false
235+
236+
defp valid_resource_types?(resource_types) do
237+
resource_types -- @valid_resource_types == []
238+
end
239+
202240
defp warn_on_invalid_dist do
203241
release? = is_binary(System.get_env("RELEASE_NAME"))
204242
net_state = if function_exported?(:net_kernel, :get_state, 0), do: :net_kernel.get_state()

test/dns_cluster_test.exs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,45 @@ defmodule DNSClusterTest do
139139
end
140140
end
141141
end
142+
143+
describe "resource_types" do
144+
test "resource_types can be a subset of [:a, :aaaa, :srv]", config do
145+
assert {:ok, _cluster} =
146+
start_supervised(
147+
{DNSCluster,
148+
name: config.test,
149+
query: "app.internal",
150+
resource_types: [:a, :srv],
151+
resolver: __MODULE__}
152+
)
153+
end
154+
155+
test "resource_types can't be outside of [:a, :aaaa, :srv]", config do
156+
assert_raise RuntimeError,
157+
~r/expected :resource_types to be a subset of \[:a, :aaaa, :srv\]/,
158+
fn ->
159+
start_supervised!(
160+
{DNSCluster,
161+
name: config.test,
162+
query: "app.internal",
163+
resource_types: [],
164+
resolver: __MODULE__}
165+
)
166+
end
167+
end
168+
169+
test "resource_types can't be empty", config do
170+
assert_raise RuntimeError,
171+
~r/expected :resource_types to be a subset of \[:a, :aaaa, :srv\]/,
172+
fn ->
173+
start_supervised!(
174+
{DNSCluster,
175+
name: config.test,
176+
query: "app.internal",
177+
resource_types: [],
178+
resolver: __MODULE__}
179+
)
180+
end
181+
end
182+
end
142183
end

0 commit comments

Comments
 (0)