1 defmodule Pleroma.Cluster do
3 Facilities for managing a cluster of slave VM's for federated testing.
5 ## Spawning the federated cluster
7 `spawn_cluster/1` spawns a map of slave nodes that are started
8 within the running VM. During startup, the slave node is sent all configuration
9 from the parent node, as well as all code. After receiving configuration and
10 code, the slave then starts all applications currently running on the parent.
11 The configuration passed to `spawn_cluster/1` overrides any parent application
12 configuration for the provided OTP app and key. This is useful for customizing
13 the Ecto database, Phoenix webserver ports, etc.
15 For example, to start a single federated VM named ":federated1", with the
16 Pleroma Endpoint running on port 4123, and with a database named
17 "pleroma_test1", you would run:
19 endpoint_conf = Application.fetch_env!(:pleroma, Pleroma.Web.Endpoint)
20 repo_conf = Application.fetch_env!(:pleroma, Pleroma.Repo)
22 Pleroma.Cluster.spawn_cluster(%{
23 :"federated1@127.0.0.1" => [
24 {:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test1")},
25 {:pleroma, Pleroma.Web.Endpoint,
26 Keyword.merge(endpoint_conf, http: [port: 4011], url: [port: 4011], server: true)}
30 *Note*: application configuration for a given key is not merged,
31 so any customization requires first fetching the existing values
32 and merging yourself by providing the merged configuration,
33 such as above with the endpoint config and repo config.
35 ## Executing code within a remote node
37 Use the `within/2` macro to execute code within the context of a remote
38 federated node. The code block captures all local variable bindings from
39 the parent's context and returns the result of the expression after executing
40 it on the remote node. For example:
42 import Pleroma.Cluster
47 within :"federated1@127.0.0.1" do
48 {node(), parent_value}
51 assert result == {:"federated1@127.0.0.1, 123}
53 *Note*: while local bindings are captured and available within the block,
54 other parent contexts like required, aliased, or imported modules are not
55 in scope. Those will need to be reimported/aliases/required within the block
56 as `within/2` is a remote procedure call.
59 @extra_apps Pleroma.Mixfile.application()[:extra_applications]
62 Spawns the default Pleroma federated cluster.
64 Values before may be customized as needed for the test suite.
66 def spawn_default_cluster do
67 endpoint_conf = Application.fetch_env!(:pleroma, Pleroma.Web.Endpoint)
68 repo_conf = Application.fetch_env!(:pleroma, Pleroma.Repo)
71 :"federated1@127.0.0.1" => [
72 {:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test_federated1")},
73 {:pleroma, Pleroma.Web.Endpoint,
74 Keyword.merge(endpoint_conf, http: [port: 4011], url: [port: 4011], server: true)}
76 :"federated2@127.0.0.1" => [
77 {:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test_federated2")},
78 {:pleroma, Pleroma.Web.Endpoint,
79 Keyword.merge(endpoint_conf, http: [port: 4012], url: [port: 4012], server: true)}
85 Spawns a configured map of federated nodes.
87 See `Pleroma.Cluster` module documentation for details.
89 def spawn_cluster(node_configs) do
90 # Turn node into a distributed node with the given long name
91 :net_kernel.start([:"primary@127.0.0.1"])
93 # Allow spawned nodes to fetch all code from this node
94 {:ok, _} = :erl_boot_server.start([])
95 allow_boot("127.0.0.1")
97 silence_logger_warnings(fn ->
99 |> Enum.map(&Task.async(fn -> start_slave(&1) end))
100 |> Enum.map(&Task.await(&1, 90_000))
105 Executes block of code again remote node.
107 See `Pleroma.Cluster` module documentation for details.
109 defmacro within(node, do: block) do
111 rpc(unquote(node), unquote(__MODULE__), :eval_quoted, [
112 unquote(Macro.escape(block)),
119 def eval_quoted(block, binding) do
120 {result, _binding} = Code.eval_quoted(block, binding, __ENV__)
124 defp start_slave({node_host, override_configs}) do
125 log(node_host, "booting federated VM")
126 {:ok, node} = :slave.start(~c"127.0.0.1", node_name(node_host), vm_args())
128 load_apps_and_transfer_configuration(node, override_configs)
129 ensure_apps_started(node)
133 def rpc(node, module, function, args) do
134 :rpc.block_call(node, module, function, args)
138 ~c"-loader inet -hosts 127.0.0.1 -setcookie #{:erlang.get_cookie()}"
141 defp allow_boot(host) do
142 {:ok, ipv4} = :inet.parse_ipv4_address(~c"#{host}")
143 :ok = :erl_boot_server.add_slave(ipv4)
146 defp add_code_paths(node) do
147 rpc(node, :code, :add_paths, [:code.get_path()])
150 defp load_apps_and_transfer_configuration(node, override_configs) do
151 Enum.each(Application.loaded_applications(), fn {app_name, _, _} ->
153 |> Application.get_all_env()
154 |> Enum.each(fn {key, primary_config} ->
155 rpc(node, Application, :put_env, [app_name, key, primary_config, [persistent: true]])
159 Enum.each(override_configs, fn {app_name, key, val} ->
160 rpc(node, Application, :put_env, [app_name, key, val, [persistent: true]])
164 defp log(node, msg), do: IO.puts("[#{node}] #{msg}")
166 defp ensure_apps_started(node) do
167 loaded_names = Enum.map(Application.loaded_applications(), fn {name, _, _} -> name end)
168 app_names = @extra_apps ++ (loaded_names -- @extra_apps)
170 rpc(node, Application, :ensure_all_started, [:mix])
171 rpc(node, Mix, :env, [Mix.env()])
172 rpc(node, __MODULE__, :prepare_database, [])
174 log(node, "starting application")
176 Enum.reduce(app_names, MapSet.new(), fn app, loaded ->
177 if Enum.member?(loaded, app) do
180 {:ok, started} = rpc(node, Application, :ensure_all_started, [app])
181 MapSet.union(loaded, MapSet.new(started))
187 def prepare_database do
188 log(node(), "preparing database")
189 repo_config = Application.get_env(:pleroma, Pleroma.Repo)
190 repo_config[:adapter].storage_down(repo_config)
191 repo_config[:adapter].storage_up(repo_config)
194 Ecto.Migrator.with_repo(Pleroma.Repo, fn repo ->
195 Ecto.Migrator.run(repo, :up, log: false, all: true)
198 Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual)
199 {:ok, _} = Application.ensure_all_started(:ex_machina)
202 defp silence_logger_warnings(func) do
203 prev_level = Logger.level()
204 Logger.configure(level: :error)
206 Logger.configure(level: prev_level)
211 defp node_name(node_host) do