logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma git clone https://hacktivis.me/git/pleroma.git

cluster.ex (7568B)


  1. # Pleroma: A lightweight social networking server
  2. # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
  3. # SPDX-License-Identifier: AGPL-3.0-only
  4. defmodule Pleroma.Cluster do
  5. @moduledoc """
  6. Facilities for managing a cluster of slave VM's for federated testing.
  7. ## Spawning the federated cluster
  8. `spawn_cluster/1` spawns a map of slave nodes that are started
  9. within the running VM. During startup, the slave node is sent all configuration
  10. from the parent node, as well as all code. After receiving configuration and
  11. code, the slave then starts all applications currently running on the parent.
  12. The configuration passed to `spawn_cluster/1` overrides any parent application
  13. configuration for the provided OTP app and key. This is useful for customizing
  14. 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:
  18. endpoint_conf = Application.fetch_env!(:pleroma, Pleroma.Web.Endpoint)
  19. repo_conf = Application.fetch_env!(:pleroma, Pleroma.Repo)
  20. Pleroma.Cluster.spawn_cluster(%{
  21. :"federated1@127.0.0.1" => [
  22. {:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test1")},
  23. {:pleroma, Pleroma.Web.Endpoint,
  24. Keyword.merge(endpoint_conf, http: [port: 4011], url: [port: 4011], server: true)}
  25. ]
  26. })
  27. *Note*: application configuration for a given key is not merged,
  28. so any customization requires first fetching the existing values
  29. and merging yourself by providing the merged configuration,
  30. such as above with the endpoint config and repo config.
  31. ## Executing code within a remote node
  32. Use the `within/2` macro to execute code within the context of a remote
  33. federated node. The code block captures all local variable bindings from
  34. the parent's context and returns the result of the expression after executing
  35. it on the remote node. For example:
  36. import Pleroma.Cluster
  37. parent_value = 123
  38. result =
  39. within :"federated1@127.0.0.1" do
  40. {node(), parent_value}
  41. end
  42. assert result == {:"federated1@127.0.0.1, 123}
  43. *Note*: while local bindings are captured and available within the block,
  44. other parent contexts like required, aliased, or imported modules are not
  45. in scope. Those will need to be reimported/aliases/required within the block
  46. as `within/2` is a remote procedure call.
  47. """
  48. @extra_apps Pleroma.Mixfile.application()[:extra_applications]
  49. @doc """
  50. Spawns the default Pleroma federated cluster.
  51. Values before may be customized as needed for the test suite.
  52. """
  53. def spawn_default_cluster do
  54. endpoint_conf = Application.fetch_env!(:pleroma, Pleroma.Web.Endpoint)
  55. repo_conf = Application.fetch_env!(:pleroma, Pleroma.Repo)
  56. spawn_cluster(%{
  57. :"federated1@127.0.0.1" => [
  58. {:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test_federated1")},
  59. {:pleroma, Pleroma.Web.Endpoint,
  60. Keyword.merge(endpoint_conf, http: [port: 4011], url: [port: 4011], server: true)}
  61. ],
  62. :"federated2@127.0.0.1" => [
  63. {:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test_federated2")},
  64. {:pleroma, Pleroma.Web.Endpoint,
  65. Keyword.merge(endpoint_conf, http: [port: 4012], url: [port: 4012], server: true)}
  66. ]
  67. })
  68. end
  69. @doc """
  70. Spawns a configured map of federated nodes.
  71. See `Pleroma.Cluster` module documentation for details.
  72. """
  73. def spawn_cluster(node_configs) do
  74. # Turn node into a distributed node with the given long name
  75. :net_kernel.start([:"primary@127.0.0.1"])
  76. # Allow spawned nodes to fetch all code from this node
  77. {:ok, _} = :erl_boot_server.start([])
  78. allow_boot("127.0.0.1")
  79. silence_logger_warnings(fn ->
  80. node_configs
  81. |> Enum.map(&Task.async(fn -> start_slave(&1) end))
  82. |> Enum.map(&Task.await(&1, 90_000))
  83. end)
  84. end
  85. @doc """
  86. Executes block of code again remote node.
  87. See `Pleroma.Cluster` module documentation for details.
  88. """
  89. defmacro within(node, do: block) do
  90. quote do
  91. rpc(unquote(node), unquote(__MODULE__), :eval_quoted, [
  92. unquote(Macro.escape(block)),
  93. binding()
  94. ])
  95. end
  96. end
  97. @doc false
  98. def eval_quoted(block, binding) do
  99. {result, _binding} = Code.eval_quoted(block, binding, __ENV__)
  100. result
  101. end
  102. defp start_slave({node_host, override_configs}) do
  103. log(node_host, "booting federated VM")
  104. {:ok, node} =
  105. do_start_slave(%{host: "127.0.0.1", name: node_name(node_host), args: vm_args()})
  106. add_code_paths(node)
  107. load_apps_and_transfer_configuration(node, override_configs)
  108. ensure_apps_started(node)
  109. {:ok, node}
  110. end
  111. def rpc(node, module, function, args) do
  112. :rpc.block_call(node, module, function, args)
  113. end
  114. defp vm_args do
  115. ~c"-loader inet -hosts 127.0.0.1 -setcookie #{:erlang.get_cookie()}"
  116. end
  117. defp allow_boot(host) do
  118. {:ok, ipv4} = :inet.parse_ipv4_address(~c"#{host}")
  119. :ok = :erl_boot_server.add_slave(ipv4)
  120. end
  121. defp add_code_paths(node) do
  122. rpc(node, :code, :add_paths, [:code.get_path()])
  123. end
  124. defp load_apps_and_transfer_configuration(node, override_configs) do
  125. Enum.each(Application.loaded_applications(), fn {app_name, _, _} ->
  126. app_name
  127. |> Application.get_all_env()
  128. |> Enum.each(fn {key, primary_config} ->
  129. rpc(node, Application, :put_env, [app_name, key, primary_config, [persistent: true]])
  130. end)
  131. end)
  132. Enum.each(override_configs, fn {app_name, key, val} ->
  133. rpc(node, Application, :put_env, [app_name, key, val, [persistent: true]])
  134. end)
  135. end
  136. defp log(node, msg), do: IO.puts("[#{node}] #{msg}")
  137. defp ensure_apps_started(node) do
  138. loaded_names = Enum.map(Application.loaded_applications(), fn {name, _, _} -> name end)
  139. app_names = @extra_apps ++ (loaded_names -- @extra_apps)
  140. rpc(node, Application, :ensure_all_started, [:mix])
  141. rpc(node, Mix, :env, [Mix.env()])
  142. rpc(node, __MODULE__, :prepare_database, [])
  143. log(node, "starting application")
  144. Enum.reduce(app_names, MapSet.new(), fn app, loaded ->
  145. if Enum.member?(loaded, app) do
  146. loaded
  147. else
  148. {:ok, started} = rpc(node, Application, :ensure_all_started, [app])
  149. MapSet.union(loaded, MapSet.new(started))
  150. end
  151. end)
  152. end
  153. @doc false
  154. def prepare_database do
  155. log(node(), "preparing database")
  156. repo_config = Application.get_env(:pleroma, Pleroma.Repo)
  157. repo_config[:adapter].storage_down(repo_config)
  158. repo_config[:adapter].storage_up(repo_config)
  159. {:ok, _, _} =
  160. Ecto.Migrator.with_repo(Pleroma.Repo, fn repo ->
  161. Ecto.Migrator.run(repo, :up, log: false, all: true)
  162. end)
  163. Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual)
  164. {:ok, _} = Application.ensure_all_started(:ex_machina)
  165. end
  166. defp silence_logger_warnings(func) do
  167. prev_level = Logger.level()
  168. Logger.configure(level: :error)
  169. res = func.()
  170. Logger.configure(level: prev_level)
  171. res
  172. end
  173. defp node_name(node_host) do
  174. node_host
  175. |> to_string()
  176. |> String.split("@")
  177. |> Enum.at(0)
  178. |> String.to_atom()
  179. end
  180. defp do_start_slave(%{host: host, name: name, args: args} = opts) do
  181. peer_module = Application.get_env(__MODULE__, :peer_module)
  182. if peer_module == :peer do
  183. peer_module.start(opts)
  184. else
  185. peer_module.start(host, name, args)
  186. end
  187. end
  188. end