├── test ├── test_helper.exs └── dns_cluster_test.exs ├── .formatter.exs ├── CHANGELOG.md ├── .gitignore ├── LICENSE.md ├── mix.exs ├── lib ├── dns_cluster │ └── resolver.ex └── dns_cluster.ex ├── README.md └── mix.lock /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.2.0 (2025-03-04) 4 | * Support multiple DNS queries 5 | 6 | ## 0.1.3 (2024-02-02) 7 | * Support OTP 24 8 | 9 | ## 0.1.2 (2024-01-08) 10 | * Use `:inet_res.getbyname/2` to resolve the given hostname to support search list for host-name lookup, such as in k8s and similar setups 11 | 12 | ## 0.1.1 (2023-09-27) 13 | * Fix bug where an empty clauses would raise an argument error 14 | 15 | ## 0.1.0 (2023-07-11) 16 | * Initial release -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | dns_cluster-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2023 Chris McCord 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule DNSCluster.MixProject do 2 | use Mix.Project 3 | 4 | @version "0.2.0" 5 | @scm_url "https://github.com/phoenixframework/dns_cluster" 6 | 7 | def project do 8 | [ 9 | app: :dns_cluster, 10 | package: package(), 11 | version: @version, 12 | elixir: "~> 1.11", 13 | start_permanent: Mix.env() == :prod, 14 | deps: deps(), 15 | source_url: @scm_url, 16 | homepage_url: @scm_url, 17 | description: "Simple DNS clustering for distributed Elixir nodes", 18 | docs: docs() 19 | ] 20 | end 21 | 22 | defp package do 23 | [ 24 | maintainers: ["Chris McCord"], 25 | licenses: ["MIT"], 26 | links: %{"GitHub" => @scm_url}, 27 | files: ~w(lib CHANGELOG.md LICENSE.md mix.exs README.md .formatter.exs) 28 | ] 29 | end 30 | 31 | def application do 32 | [ 33 | extra_applications: [:logger] 34 | ] 35 | end 36 | 37 | defp deps do 38 | [{:ex_doc, ">= 0.0.0", only: :docs}] 39 | end 40 | 41 | defp docs do 42 | [ 43 | source_ref: "v#{@version}", 44 | skip_undefined_reference_warnings_on: ["CHANGELOG.md"], 45 | extras: ["CHANGELOG.md"] 46 | ] 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/dns_cluster/resolver.ex: -------------------------------------------------------------------------------- 1 | defmodule DNSCluster.Resolver do 2 | @moduledoc false 3 | 4 | require Record 5 | Record.defrecord(:hostent, Record.extract(:hostent, from_lib: "kernel/include/inet.hrl")) 6 | 7 | def basename(node_name) when is_atom(node_name) do 8 | [basename, _] = String.split(to_string(node_name), "@") 9 | basename 10 | end 11 | 12 | def connect_node(node_name) when is_atom(node_name), do: Node.connect(node_name) 13 | 14 | def list_nodes, do: Node.list(:visible) 15 | 16 | def lookup(query, type) when is_binary(query) and type in [:a, :aaaa] do 17 | case :inet_res.getbyname(~c"#{query}", type) do 18 | {:ok, hostent(h_addr_list: addr_list)} -> addr_list 19 | {:error, _} -> [] 20 | end 21 | end 22 | 23 | def lookup(query, type) when is_binary(query) and type in [:srv] do 24 | case :inet_res.getbyname(~c"#{query}", type) do 25 | {:ok, hostent(h_addr_list: srv_list)} -> 26 | lookup_hosts(srv_list) 27 | 28 | {:error, _} -> 29 | [] 30 | end 31 | end 32 | 33 | defp lookup_hosts(srv_list) do 34 | srv_list 35 | |> Enum.flat_map(fn {_prio, _weight, _port, host_name} -> 36 | case :inet.gethostbyname(host_name) do 37 | {:ok, hostent(h_addr_list: addr_list)} -> addr_list 38 | {:error, _} -> [] 39 | end 40 | end) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DNSCluster 2 | 3 | Simple DNS clustering for distributed Elixir nodes. 4 | 5 | ## Installation 6 | 7 | The package can be installed by adding `dns_cluster` to your list of dependencies in `mix.exs`: 8 | 9 | ```elixir 10 | def deps do 11 | [ 12 | {:dns_cluster, "~> 0.2"} 13 | ] 14 | end 15 | ``` 16 | 17 | Next, you can configure and start the cluster by adding it to your supervision 18 | tree in your `application.ex`: 19 | 20 | ```elixir 21 | children = [ 22 | {Phoenix.PubSub, ...}, 23 | {DNSCluster, query: Application.get_env(:my_app, :dns_cluster_query) || :ignore}, 24 | MyAppWeb.Endpoint 25 | ] 26 | ``` 27 | 28 | Then in your config file, add: 29 | 30 | ```elixir 31 | config :my_app, :dns_cluster_query, ["app.internal"] 32 | ``` 33 | 34 | If you are deploying with Elixir releases, the release must be set to support longnames and 35 | the node must be named, using their IP address by default. These can be set in your 36 | `rel/env.sh.eex` file: 37 | 38 | ```sh 39 | #!/bin/sh 40 | export RELEASE_DISTRIBUTION=name 41 | export RELEASE_NODE="myapp@fully-qualified-ip" 42 | ``` 43 | 44 | By default, nodes from the same release will have the same cookie. If you want different 45 | applications or releases to connect to each other, then you must set the `RELEASE_COOKIE`, 46 | either in your deployment platform or inside `rel/env.sh.eex`: 47 | 48 | ```sh 49 | #!/bin/sh 50 | ... 51 | export RELEASE_COOKIE="my-app-cookie" 52 | ``` 53 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark_parser": {:hex, :earmark_parser, "1.4.43", "34b2f401fe473080e39ff2b90feb8ddfeef7639f8ee0bbf71bb41911831d77c5", [:mix], [], "hexpm", "970a3cd19503f5e8e527a190662be2cee5d98eed1ff72ed9b3d1a3d466692de8"}, 3 | "ex_doc": {:hex, :ex_doc, "0.37.2", "2a3aa7014094f0e4e286a82aa5194a34dd17057160988b8509b15aa6c292720c", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "4dfa56075ce4887e4e8b1dcc121cd5fcb0f02b00391fd367ff5336d98fa49049"}, 4 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 5 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 6 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 7 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 8 | } 9 | -------------------------------------------------------------------------------- /test/dns_cluster_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DNSClusterTest do 2 | use ExUnit.Case 3 | 4 | @ips %{ 5 | already_known: ~c"fdaa:0:36c9:a7b:db:400e:1352:1", 6 | new: ~c"fdaa:0:36c9:a7b:db:400e:1352:2", 7 | no_connect_diff_base: ~c"fdaa:0:36c9:a7b:db:400e:1352:3", 8 | connect_diff_base: ~c"fdaa:0:36c9:a7b:db:400e:1352:4" 9 | } 10 | 11 | @new_node :"app@#{@ips.new}" 12 | def connect_node(@new_node) do 13 | send(__MODULE__, {:try_connect, @new_node}) 14 | true 15 | end 16 | 17 | @no_connect_node :"app@#{@ips.no_connect_diff_base}" 18 | def connect_node(@no_connect_node) do 19 | false 20 | end 21 | 22 | @specified_base_node :"specified@#{@ips.connect_diff_base}" 23 | def connect_node(@specified_base_node) do 24 | send(__MODULE__, {:try_connect, @specified_base_node}) 25 | true 26 | end 27 | 28 | def connect_node(_), do: false 29 | 30 | def basename(_node_name), do: "app" 31 | 32 | def lookup(_query, _type) do 33 | {:ok, dns_ip1} = :inet.parse_address(@ips.already_known) 34 | {:ok, dns_ip2} = :inet.parse_address(@ips.new) 35 | {:ok, dns_ip3} = :inet.parse_address(@ips.no_connect_diff_base) 36 | {:ok, dns_ip4} = :inet.parse_address(@ips.connect_diff_base) 37 | 38 | [dns_ip1, dns_ip2, dns_ip3, dns_ip4] 39 | end 40 | 41 | def list_nodes do 42 | [:"app@#{@ips.already_known}"] 43 | end 44 | 45 | defp wait_for_node_discovery(cluster) do 46 | :sys.get_state(cluster) 47 | :ok 48 | end 49 | 50 | test "discovers nodes", config do 51 | Process.register(self(), __MODULE__) 52 | 53 | {:ok, cluster} = 54 | start_supervised( 55 | {DNSCluster, name: config.test, query: "app.internal", resolver: __MODULE__} 56 | ) 57 | 58 | wait_for_node_discovery(cluster) 59 | 60 | new_node = :"app@#{@ips.new}" 61 | no_connect_node = :"app@#{@ips.no_connect_diff_base}" 62 | assert_receive {:try_connect, ^new_node} 63 | refute_receive {:try_connect, ^no_connect_node} 64 | refute_receive _ 65 | end 66 | 67 | test "discovers nodes with differing basenames if specified", config do 68 | Process.register(self(), __MODULE__) 69 | 70 | {:ok, cluster} = 71 | start_supervised( 72 | {DNSCluster, 73 | name: config.test, 74 | query: ["app.internal", {"specified", "app.internal"}], 75 | resolver: __MODULE__} 76 | ) 77 | 78 | wait_for_node_discovery(cluster) 79 | 80 | new_node = :"app@#{@ips.new}" 81 | specified_base_node = :"specified@#{@ips.connect_diff_base}" 82 | assert_receive {:try_connect, ^new_node} 83 | assert_receive {:try_connect, ^specified_base_node} 84 | refute_receive _ 85 | end 86 | 87 | test "discovers nodes with a list of queries", config do 88 | Process.register(self(), __MODULE__) 89 | 90 | {:ok, cluster} = 91 | start_supervised( 92 | {DNSCluster, name: config.test, query: ["app.internal"], resolver: __MODULE__} 93 | ) 94 | 95 | wait_for_node_discovery(cluster) 96 | 97 | new_node = :"app@#{@ips.new}" 98 | no_connect_node = :"app@#{@ips.no_connect_diff_base}" 99 | assert_receive {:try_connect, ^new_node} 100 | refute_receive {:try_connect, ^no_connect_node} 101 | refute_receive _ 102 | end 103 | 104 | test "query with :ignore does not start child" do 105 | assert DNSCluster.start_link(query: :ignore) == :ignore 106 | end 107 | 108 | describe "query forms" do 109 | test "query can be a string", config do 110 | assert {:ok, _cluster} = 111 | start_supervised( 112 | {DNSCluster, name: config.test, query: "app.internal", resolver: __MODULE__} 113 | ) 114 | end 115 | 116 | test "query can be a {basename, query}", config do 117 | assert {:ok, _cluster} = 118 | start_supervised( 119 | {DNSCluster, 120 | name: config.test, query: {"basename", "app.internal"}, resolver: __MODULE__} 121 | ) 122 | end 123 | 124 | test "query can be a list", config do 125 | assert {:ok, _cluster} = 126 | start_supervised( 127 | {DNSCluster, 128 | name: config.test, 129 | query: ["query", {"basename", "app.internal"}], 130 | resolver: __MODULE__} 131 | ) 132 | end 133 | 134 | test "query can't be other terms", config do 135 | for bad <- [1234, :atom, %{a: 1}, [["query"]]] do 136 | assert_raise RuntimeError, ~r/expected :query to be a string/, fn -> 137 | start_supervised!({DNSCluster, name: config.test, query: bad, resolver: __MODULE__}) 138 | end 139 | end 140 | end 141 | 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 183 | end 184 | -------------------------------------------------------------------------------- /lib/dns_cluster.ex: -------------------------------------------------------------------------------- 1 | defmodule DNSCluster do 2 | @moduledoc """ 3 | Simple DNS based cluster discovery. 4 | 5 | ## Default node discovery 6 | 7 | By default, nodes will only be joined if their node basename matches the basename of the current node. 8 | For example, if `node()` is `myapp-123@fdaa:1:36c9:a7b:198:c4b1:73c6:1`, it will try to connect 9 | to every IP from the DNS query with `Node.connect/1`. But this will only work if the remote node 10 | has the same basename, like `myapp-123@fdaa:1:36c9:a7b:198:c4b1:73c6:2`. If the remote node's 11 | basename is different, the nodes will not connect. 12 | 13 | If you want to connect to nodes with different basenames, use a tuple with the basename and query. 14 | For example, to connect to a node named `remote`, use `{"remote", "remote-app.internal"}`. 15 | 16 | ## Multiple queries 17 | 18 | Sometimes you might want to cluster apps with different domain names. Just pass a list of queries 19 | for this. For instance: `["app-one.internal", "app-two.internal", {"other-basename", "other.internal"}]`. 20 | Remember, all nodes need to share the same secret cookie to connect successfully, and their FQDN 21 | must be their IP address (either IPv4 or IPv6). 22 | 23 | ## Examples 24 | 25 | To start in your supervision tree, add the child: 26 | 27 | children = [ 28 | ..., 29 | {DNSCluster, query: "myapp.internal"} 30 | ] 31 | 32 | See the `start_link/1` docs for all available options. 33 | 34 | If you require more advanced clustering options and strategies, see the 35 | [libcluster](https://hexdocs.pm/libcluster) library. 36 | """ 37 | use GenServer 38 | 39 | alias DNSCluster.Resolver 40 | 41 | require Logger 42 | 43 | @doc ~S""" 44 | Starts DNS based cluster discovery. 45 | 46 | ## Options 47 | 48 | * `:name` - the name of the cluster. Defaults to `DNSCluster`. 49 | * `:query` - the required DNS query for node discovery, for example: 50 | `"myapp.internal"` or `["foo.internal", "bar.internal"]`. If the basename 51 | differs between nodes, a tuple of `{basename, query}` can be provided as well. 52 | The value `:ignore` can be used to ignore starting the DNSCluster. 53 | * `:resource_types` - the resource record types that are used for node discovery. 54 | Defaults to `[:a, :aaaa]` and also supports the `:srv` type. 55 | * `:interval` - the millisec interval between DNS queries. Defaults to `5000`. 56 | * `:connect_timeout` - the millisec timeout to allow discovered nodes to connect. 57 | Defaults to `10_000`. 58 | 59 | ## Examples 60 | 61 | iex> DNSCluster.start_link(query: "myapp.internal") 62 | {:ok, pid} 63 | 64 | iex> DNSCluster.start_link(query: :ignore) 65 | :ignore 66 | """ 67 | def start_link(opts) do 68 | GenServer.start_link(__MODULE__, opts, name: Keyword.get(opts, :name, __MODULE__)) 69 | end 70 | 71 | @valid_resource_types [:a, :aaaa, :srv] 72 | 73 | @impl true 74 | def init(opts) do 75 | case Keyword.fetch(opts, :query) do 76 | {:ok, :ignore} -> 77 | :ignore 78 | 79 | {:ok, query} -> 80 | validate_query!(query) 81 | 82 | resource_types = Keyword.get(opts, :resource_types, [:a, :aaaa]) 83 | validate_resource_types!(resource_types) 84 | 85 | warn_on_invalid_dist() 86 | 87 | resolver = Keyword.get(opts, :resolver, Resolver) 88 | 89 | state = %{ 90 | interval: Keyword.get(opts, :interval, 5_000), 91 | basename: resolver.basename(node()), 92 | query: List.wrap(query), 93 | resource_types: resource_types, 94 | log: Keyword.get(opts, :log, false), 95 | poll_timer: nil, 96 | connect_timeout: Keyword.get(opts, :connect_timeout, 10_000), 97 | resolver: resolver 98 | } 99 | 100 | {:ok, state, {:continue, :discover_ips}} 101 | 102 | :error -> 103 | raise ArgumentError, "missing required :query option in #{inspect(opts)}" 104 | end 105 | end 106 | 107 | @impl true 108 | def handle_continue(:discover_ips, state) do 109 | {:noreply, do_discovery(state)} 110 | end 111 | 112 | @impl true 113 | def handle_info(:discover_ips, state) do 114 | {:noreply, do_discovery(state)} 115 | end 116 | 117 | defp do_discovery(state) do 118 | state 119 | |> connect_new_nodes() 120 | |> schedule_next_poll() 121 | end 122 | 123 | defp connect_new_nodes(%{resolver: resolver, connect_timeout: timeout} = state) do 124 | node_names = for name <- resolver.list_nodes(), into: MapSet.new(), do: to_string(name) 125 | 126 | ips = discover_ips(state) 127 | 128 | _results = 129 | ips 130 | |> Enum.map(fn {basename, ip} -> "#{basename}@#{ip}" end) 131 | |> Enum.filter(fn node_name -> !Enum.member?(node_names, node_name) end) 132 | |> Task.async_stream( 133 | fn new_name -> 134 | if resolver.connect_node(:"#{new_name}") do 135 | log(state, "#{node()} connected to #{new_name}") 136 | end 137 | end, 138 | max_concurrency: max(1, length(ips)), 139 | timeout: timeout 140 | ) 141 | |> Enum.to_list() 142 | 143 | state 144 | end 145 | 146 | defp log(state, msg) do 147 | if level = state.log, do: Logger.log(level, msg) 148 | end 149 | 150 | defp schedule_next_poll(state) do 151 | %{state | poll_timer: Process.send_after(self(), :discover_ips, state.interval)} 152 | end 153 | 154 | defp discover_ips(%{resolver: resolver, query: queries, resource_types: resource_types} = state) do 155 | for resource_type <- resource_types, 156 | query <- queries, 157 | basename = basename_from_query_or_state(query, state), 158 | addr <- resolver.lookup(query, resource_type) do 159 | {basename, addr} 160 | end 161 | |> Enum.uniq() 162 | |> Enum.map(fn {basename, addr} -> {basename, to_string(:inet.ntoa(addr))} end) 163 | end 164 | 165 | defp basename_from_query_or_state({basename, _query}, _state), do: basename 166 | defp basename_from_query_or_state(_query, %{basename: basename}), do: basename 167 | 168 | defp validate_query!(query) do 169 | query 170 | |> List.wrap() 171 | |> Enum.each(fn 172 | string when is_binary(string) -> 173 | true 174 | 175 | {basename, query} when is_binary(basename) and is_binary(query) -> 176 | true 177 | 178 | _ -> 179 | raise ArgumentError, 180 | "expected :query to be a string, {basename, query}, or list, got: #{inspect(query)}" 181 | end) 182 | end 183 | 184 | defp validate_resource_types!(resource_types) do 185 | if resource_types == [] or resource_types -- @valid_resource_types != [] do 186 | raise ArgumentError, 187 | "expected :resource_types to be a subset of [:a, :aaaa, :srv], got: #{inspect(resource_types)}" 188 | end 189 | end 190 | 191 | defp warn_on_invalid_dist do 192 | release? = is_binary(System.get_env("RELEASE_NAME")) 193 | net_state = if function_exported?(:net_kernel, :get_state, 0), do: :net_kernel.get_state() 194 | 195 | cond do 196 | !net_state -> 197 | :ok 198 | 199 | net_state.started == :no and release? -> 200 | Logger.warning(""" 201 | node not running in distributed mode. Ensure the following exports are set in your rel/env.sh.eex file: 202 | 203 | #!/bin/sh 204 | 205 | export RELEASE_DISTRIBUTION=name 206 | export RELEASE_NODE="myapp@fully-qualified-host-or-ip" 207 | """) 208 | 209 | net_state.started == :no or 210 | (!release? and net_state.started != :no and net_state[:name_domain] != :longnames) -> 211 | Logger.warning(""" 212 | node not running in distributed mode. When running outside of a release, you must start net_kernel manually with 213 | longnames. 214 | https://www.erlang.org/doc/man/net_kernel.html#start-2 215 | """) 216 | 217 | net_state[:name_domain] != :longnames and release? -> 218 | Logger.warning(""" 219 | node not running with longnames which are required for DNS discovery. 220 | Ensure the following exports are set in your rel/env.sh.eex file: 221 | 222 | #!/bin/sh 223 | 224 | export RELEASE_DISTRIBUTION=name 225 | export RELEASE_NODE="myapp@fully-qualified-host-or-ip" 226 | """) 227 | 228 | true -> 229 | :ok 230 | end 231 | end 232 | end 233 | --------------------------------------------------------------------------------