Set editorconfig, format whole codebase

This commit is contained in:
Timothée Floure 2022-02-23 17:53:00 +01:00
parent e62aafd172
commit ebcfabdbd2
Signed by: tfloure
GPG key ID: 4502C902C00A1E12
11 changed files with 380 additions and 329 deletions

12
.editorconfig Normal file
View file

@ -0,0 +1,12 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*.ex]
indent_style = space
indent_size = 2
[*.eex]
indent_style = space
indent_size = 2

View file

@ -6,8 +6,8 @@ defmodule HAHandler do
# Mix is not available in releases, and these things are static # Mix is not available in releases, and these things are static
# anyway (@variables are evaluated at compile time). # anyway (@variables are evaluated at compile time).
@otp_app Mix.Project.config()[:app] @otp_app Mix.Project.config()[:app]
@version Mix.Project.config[:version] @version Mix.Project.config()[:version]
@env Mix.env @env Mix.env()
def http_port, do: Application.get_env(@otp_app, :http_port) def http_port, do: Application.get_env(@otp_app, :http_port)
def haproxy_socket, do: Application.get_env(@otp_app, :haproxy_socket) def haproxy_socket, do: Application.get_env(@otp_app, :haproxy_socket)
@ -26,6 +26,7 @@ defmodule HAHandler do
from: acme_challenge_path() from: acme_challenge_path()
] ]
end end
def assets_static_config() do def assets_static_config() do
[ [
at: "/static", at: "/static",

View file

@ -10,7 +10,8 @@ defmodule HAHandler.Application do
@impl true @impl true
def start(_type, _args) do def start(_type, _args) do
children = [ children = [
{Plug.Cowboy, scheme: :http, plug: HAHandler.Web.Router, options: [port: HAHandler.http_port()]}, {Plug.Cowboy,
scheme: :http, plug: HAHandler.Web.Router, options: [port: HAHandler.http_port()]},
{HAHandler.PGSQL.Supervisor, HAHandler.pgsql_instances()}, {HAHandler.PGSQL.Supervisor, HAHandler.pgsql_instances()},
{HAHandler.Control, []} {HAHandler.Control, []}
] ]

View file

@ -1,82 +1,104 @@
defmodule HAHandler.Control do defmodule HAHandler.Control do
@moduledoc """ @moduledoc """
This module handles the decision-logic and actions to be This module handles the decision-logic and actions to be
taken regarding the current state of the infrastructure. taken regarding the current state of the infrastructure.
""" """
@haproxy_pgsql_backend "pgsql" @haproxy_pgsql_backend "pgsql"
use GenServer use GenServer
require Logger require Logger
alias HAHandler.{PGSQL, HAProxy} alias HAHandler.{PGSQL, HAProxy}
# How much do we wait (ms) between each check/decision-making round? # How much do we wait (ms) between each check/decision-making round?
@refresh 15_000 @refresh 15_000
def start_link(opts) do def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__) GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end end
@impl true @impl true
def init(_opts) do def init(_opts) do
state = [] state = []
# Let's skip the initial startup round so that other components are all up # Let's skip the initial startup round so that other components are all up
# and running. # and running.
Process.send_after self(), :sync, @refresh Process.send_after(self(), :sync, @refresh)
{:ok, state} {:ok, state}
end end
@impl true @impl true
def handle_info(:sync, state) do def handle_info(:sync, state) do
Logger.debug("Executing control logic.") Logger.debug("Executing control logic.")
# Fetch PGSQL state, make sure HAProxy routes to the master # Fetch PGSQL state, make sure HAProxy routes to the master
# process. # process.
pgsql_state = PGSQL.get_instances() pgsql_state =
|> Enum.map(fn {hostname, pid} = instance-> PGSQL.get_instances()
haproxy_server = HAHandler.pgsql_instances() |> Enum.map(fn {hostname, pid} = instance ->
|> Enum.filter(fn opts -> Keyword.get(opts, :hostname) == hostname end) haproxy_server =
|> Enum.at(0) HAHandler.pgsql_instances()
|> Keyword.get(:haproxy_server) |> Enum.filter(fn opts -> Keyword.get(opts, :hostname) == hostname end)
|> Enum.at(0)
|> Keyword.get(:haproxy_server)
%{ %{
haproxy_server: haproxy_server, haproxy_server: haproxy_server,
pgsql_watcher_pid: pid, pgsql_watcher_pid: pid,
pgsql_operation_mode: PGSQL.get_operation_mode(instance) pgsql_operation_mode: PGSQL.get_operation_mode(instance)
} }
end) end)
haproxy_state = HAProxy.get_stats()
|> Map.get("Server", [])
|> Enum.filter(fn mapping -> mapping["pxname"] == @haproxy_pgsql_backend end)
|> Enum.map(fn mapping -> %{mapping["svname"] => mapping["status"]} end)
|> Enum.reduce(&Map.merge/2)
for pgsql_instance <- pgsql_state do haproxy_state =
haproxy_state = Map.get(haproxy_state, pgsql_instance.haproxy_server) HAProxy.get_stats()
|> Map.get("Server", [])
|> Enum.filter(fn mapping -> mapping["pxname"] == @haproxy_pgsql_backend end)
|> Enum.map(fn mapping -> %{mapping["svname"] => mapping["status"]} end)
|> Enum.reduce(&Map.merge/2)
case {pgsql_instance.pgsql_operation_mode, haproxy_state} do for pgsql_instance <- pgsql_state do
{:primary, "UP"} -> haproxy_state = Map.get(haproxy_state, pgsql_instance.haproxy_server)
:noop
{:primary, "MAINT"} ->
Logger.info("Enabling routing PGSQL to (now) primary #{pgsql_instance.haproxy_server}.")
HAProxy.set_server(@haproxy_pgsql_backend, pgsql_instance.haproxy_server, "state", "ready")
{:secondary, "UP"} ->
Logger.info("Disabling routing PGSQL to (now) secondary #{pgsql_instance.haproxy_server}.")
HAProxy.set_server(@haproxy_pgsql_backend, pgsql_instance.haproxy_server, "state", "maint")
{:secondary, "MAINT"} ->
:noop
unknown ->
Logger.warning("Unhandled PGSQL/HAProxy state: #{inspect(unknown)}")
end
end
# Schedule next round. case {pgsql_instance.pgsql_operation_mode, haproxy_state} do
Process.send_after self(), :sync, @refresh {:primary, "UP"} ->
:noop
{:noreply, state} {:primary, "MAINT"} ->
end Logger.info("Enabling routing PGSQL to (now) primary #{pgsql_instance.haproxy_server}.")
HAProxy.set_server(
@haproxy_pgsql_backend,
pgsql_instance.haproxy_server,
"state",
"ready"
)
{:secondary, "UP"} ->
Logger.info(
"Disabling routing PGSQL to (now) secondary #{pgsql_instance.haproxy_server}."
)
HAProxy.set_server(
@haproxy_pgsql_backend,
pgsql_instance.haproxy_server,
"state",
"maint"
)
{:secondary, "MAINT"} ->
:noop
unknown ->
Logger.warning("Unhandled PGSQL/HAProxy state: #{inspect(unknown)}")
end
end
# Schedule next round.
Process.send_after(self(), :sync, @refresh)
{:noreply, state}
end
end end

View file

@ -33,28 +33,31 @@ defmodule HAHandler.HAProxy do
# run out - which will block all operations. # run out - which will block all operations.
close_socket(fd) close_socket(fd)
data data
{:error, err} -> {:error, err} ->
{:error, err} {:error, err}
end end
end end
defp extract_stats(data) do defp extract_stats(data) do
extracted = for entry <- data do extracted =
for mapping <- entry do for entry <- data do
case mapping do for mapping <- entry do
%{ case mapping do
"id" => id, %{
"proxyId" => proxy_id, "id" => id,
"objType" => type, "proxyId" => proxy_id,
"field" => %{"name" => name}, "objType" => type,
"value" => %{"value" => value}, "field" => %{"name" => name},
} -> "value" => %{"value" => value}
%{:id => id, :proxy_id => proxy_id, :type => type, :field => name, :value => value} } ->
_ -> %{:id => id, :proxy_id => proxy_id, :type => type, :field => name, :value => value}
nil
_ ->
nil
end
end end
end end
end
extracted extracted
|> List.flatten() |> List.flatten()
@ -62,18 +65,14 @@ defmodule HAHandler.HAProxy do
fn mapping -> {mapping.type, mapping.id, mapping.proxy_id} end, fn mapping -> {mapping.type, mapping.id, mapping.proxy_id} end,
fn mapping -> %{mapping.field => mapping.value} end fn mapping -> %{mapping.field => mapping.value} end
) )
|> Enum.map( |> Enum.map(fn {{type, id, proxy_id}, grouped_mappings} ->
fn {{type, id, proxy_id}, grouped_mappings} -> grouped_mappings
grouped_mappings |> Enum.reduce(fn l, r -> Map.merge(l, r) end)
|> Enum.reduce(fn l, r -> Map.merge(l,r) end) |> Map.put("type", type)
|> Map.put("type", type) |> Map.put("id", id)
|> Map.put("id", id) |> Map.put("proxy_id", proxy_id)
|> Map.put("proxy_id", proxy_id) end)
end |> Enum.group_by(fn entry -> Map.get(entry, "type") end)
)
|> Enum.group_by(
fn entry -> Map.get(entry, "type") end
)
end end
@doc """ @doc """
@ -81,7 +80,8 @@ defmodule HAHandler.HAProxy do
a list of Maps. a list of Maps.
""" """
def get_stats(opts \\ []) def get_stats(opts \\ [])
def get_stats([hide_error: true]) do
def get_stats(hide_error: true) do
case get_stats() do case get_stats() do
{:error, _err} -> {:error, _err} ->
%{ %{
@ -89,9 +89,12 @@ defmodule HAHandler.HAProxy do
"Backend" => %{}, "Backend" => %{},
"Server" => %{} "Server" => %{}
} }
stats -> stats
stats ->
stats
end end
end end
def get_stats(_opts) do def get_stats(_opts) do
case execute("show stat json") do case execute("show stat json") do
{:ok, raw} -> {:ok, raw} ->
@ -117,8 +120,10 @@ defmodule HAHandler.HAProxy do
case execute("set server #{backend}/#{server} #{key} #{value}") do case execute("set server #{backend}/#{server} #{key} #{value}") do
{:ok, ""} -> {:ok, ""} ->
:ok :ok
{:ok, err} -> {:ok, err} ->
{:error, err} {:error, err}
{:error, err} -> {:error, err} ->
{:error, err} {:error, err}
end end

View file

@ -1,45 +1,52 @@
defmodule HAHandler.PGSQL do defmodule HAHandler.PGSQL do
@supervisor HAHandler.PGSQL.Supervisor @supervisor HAHandler.PGSQL.Supervisor
@version_query "SELECT version();" @version_query "SELECT version();"
@is_in_recovery_query "SELECT pg_is_in_recovery();" @is_in_recovery_query "SELECT pg_is_in_recovery();"
def get_instances() do def get_instances() do
watchers = Supervisor.which_children(@supervisor) watchers = Supervisor.which_children(@supervisor)
for {hostname, pid, _type, _modules} <- watchers do
{hostname, pid}
end
end
def get_version({hostname, pid}) do for {hostname, pid, _type, _modules} <- watchers do
case GenServer.call(pid, {:execute, @version_query, []}) do {hostname, pid}
{:ok, %Postgrex.Result{rows: [[raw_version_string]]}} -> end
version = case Regex.run(~r/^PostgreSQL (\d+\.\d+)/, raw_version_string) do end
[_, version_number] -> version_number
_ -> "unknown"
end
%{hostname: hostname, version: version, status: "up"}
{:error, %DBConnection.ConnectionError{message: _msg, reason: err}} ->
%{hostname: hostname, version: "unknown", status: err}
_ ->
%{hostname: hostname, version: "unknown", status: :unknown}
end
end
def get_operation_mode({_hostname, pid}) do def get_version({hostname, pid}) do
case GenServer.call(pid, {:execute, @is_in_recovery_query, []}) do case GenServer.call(pid, {:execute, @version_query, []}) do
{:ok, %Postgrex.Result{rows: [[false]]}} -> {:ok, %Postgrex.Result{rows: [[raw_version_string]]}} ->
:primary version =
{:ok, %Postgrex.Result{rows: [[true]]}} -> case Regex.run(~r/^PostgreSQL (\d+\.\d+)/, raw_version_string) do
:secondary [_, version_number] -> version_number
_ -> _ -> "unknown"
:unknown end
end
end
def get_stats() do %{hostname: hostname, version: version, status: "up"}
get_instances()
|> Enum.map(fn instance -> {:error, %DBConnection.ConnectionError{message: _msg, reason: err}} ->
get_version(instance) |> Map.put(:mode, get_operation_mode(instance)) %{hostname: hostname, version: "unknown", status: err}
end)
end _ ->
%{hostname: hostname, version: "unknown", status: :unknown}
end
end
def get_operation_mode({_hostname, pid}) do
case GenServer.call(pid, {:execute, @is_in_recovery_query, []}) do
{:ok, %Postgrex.Result{rows: [[false]]}} ->
:primary
{:ok, %Postgrex.Result{rows: [[true]]}} ->
:secondary
_ ->
:unknown
end
end
def get_stats() do
get_instances()
|> Enum.map(fn instance ->
get_version(instance) |> Map.put(:mode, get_operation_mode(instance))
end)
end
end end

View file

@ -1,22 +1,23 @@
defmodule HAHandler.PGSQL.Supervisor do defmodule HAHandler.PGSQL.Supervisor do
use Supervisor use Supervisor
alias HAHandler.PGSQL.Watcher, as: PGSQLWatcher alias HAHandler.PGSQL.Watcher, as: PGSQLWatcher
def start_link(opts) do def start_link(opts) do
Supervisor.start_link(__MODULE__, opts, name: __MODULE__) Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
end end
@impl true @impl true
def init(instances) do def init(instances) do
children = Enum.map(instances, fn conf -> children =
%{ Enum.map(instances, fn conf ->
id: Keyword.get(conf, :hostname), %{
start: {PGSQLWatcher, :start_link, [conf]} id: Keyword.get(conf, :hostname),
} start: {PGSQLWatcher, :start_link, [conf]}
end) }
end)
opts = [ strategy: :one_for_one ] opts = [strategy: :one_for_one]
Supervisor.init(children, opts) Supervisor.init(children, opts)
end end
end end

View file

@ -1,29 +1,29 @@
defmodule HAHandler.PGSQL.Watcher do defmodule HAHandler.PGSQL.Watcher do
use GenServer use GenServer
require Logger require Logger
def start_link(opts) do def start_link(opts) do
GenServer.start_link(__MODULE__, opts) GenServer.start_link(__MODULE__, opts)
end end
@impl true @impl true
def init(opts) do def init(opts) do
# Starts a Postgrex child but does not means the connection was # Starts a Postgrex child but does not means the connection was
# successful. # successful.
# TODO: set dbconnections backoff and connect hooks # TODO: set dbconnections backoff and connect hooks
# See https://github.com/elixir-ecto/db_connection/blob/master/lib/db_connection.ex#L343 # See https://github.com/elixir-ecto/db_connection/blob/master/lib/db_connection.ex#L343
{:ok, pid} = Postgrex.start_link(opts) {:ok, pid} = Postgrex.start_link(opts)
state = %{ state = %{
backend: pid, backend: pid,
hostname: Keyword.get(opts, :hostname) hostname: Keyword.get(opts, :hostname)
} }
{:ok, state} {:ok, state}
end end
@impl true @impl true
def handle_call({:execute, query, params}, _from, %{backend: backend} = state) do def handle_call({:execute, query, params}, _from, %{backend: backend} = state) do
{:reply, Postgrex.query(backend, query, params), state} {:reply, Postgrex.query(backend, query, params), state}
end end
end end

View file

@ -1,35 +1,34 @@
defmodule HAHandler.Web.Controller do defmodule HAHandler.Web.Controller do
import Plug.Conn import Plug.Conn
alias HAHandler.{HAProxy, PGSQL} alias HAHandler.{HAProxy, PGSQL}
@template_dir "lib/ha_handler/web/templates" @template_dir "lib/ha_handler/web/templates"
@index_template EEx.compile_file( @index_template EEx.compile_file(Path.join(@template_dir, "index.html.eex"))
Path.join(@template_dir, "index.html.eex")
)
defp render(conn, template, assigns) do defp render(conn, template, assigns) do
{body, _binding} = Code.eval_quoted(template, assigns) {body, _binding} = Code.eval_quoted(template, assigns)
conn conn
|> put_resp_content_type("text/html") |> put_resp_content_type("text/html")
|> send_resp(200, body) |> send_resp(200, body)
end end
def index(conn) do def index(conn) do
{:ok, hostname} = :net_adm.dns_hostname(:net_adm.localhost) {:ok, hostname} = :net_adm.dns_hostname(:net_adm.localhost())
haproxy_stats = HAProxy.get_stats([hide_error: true]) haproxy_stats = HAProxy.get_stats(hide_error: true)
pgsql_stats = PGSQL.get_stats() pgsql_stats = PGSQL.get_stats()
assigns = [ assigns = [
haproxy_stats: haproxy_stats, haproxy_stats: haproxy_stats,
pgsql_status: pgsql_stats, pgsql_status: pgsql_stats,
hostname: hostname, hostname: hostname,
otp_app: HAHandler.otp_app(), otp_app: HAHandler.otp_app(),
version: HAHandler.version(), version: HAHandler.version(),
env: HAHandler.env() env: HAHandler.env()
] ]
render(conn, @index_template, assigns)
end render(conn, @index_template, assigns)
end
end end

View file

@ -1,35 +1,38 @@
defmodule HAHandler.Web.Router do defmodule HAHandler.Web.Router do
@moduledoc """ @moduledoc """
This module dispatch incoming HTTP requests to their This module dispatch incoming HTTP requests to their
related logic. Please refer to [1] for details. related logic. Please refer to [1] for details.
[1] https://hexdocs.pm/plug/Plug.Router.html#content [1] https://hexdocs.pm/plug/Plug.Router.html#content
""" """
use Plug.Router use Plug.Router
alias HAHandler.Web.Controller alias HAHandler.Web.Controller
# Note for plugs: oder is important, as a plug may stop # Note for plugs: oder is important, as a plug may stop
# want to stop the pipeline! # want to stop the pipeline!
plug Plug.Logger, log: :debug plug(Plug.Logger, log: :debug)
# We use replug to allow for runtime configuration in release (as macros such # We use replug to allow for runtime configuration in release (as macros such
# as the `plug` call ae evaluated are compile-time). # as the `plug` call ae evaluated are compile-time).
plug Replug, plug(Replug,
plug: Plug.Static, plug: Plug.Static,
opts: {HAHandler, :acme_challenges_static_config} opts: {HAHandler, :acme_challenges_static_config}
plug Replug, )
plug: Plug.Static,
opts: {HAHandler, :assets_static_config}
plug :match plug(Replug,
plug :dispatch plug: Plug.Static,
opts: {HAHandler, :assets_static_config}
)
get "/", do: Controller.index(conn) plug(:match)
plug(:dispatch)
match _ do get("/", do: Controller.index(conn))
send_resp(conn, 404, "Not found")
end match _ do
send_resp(conn, 404, "Not found")
end
end end

View file

@ -1,124 +1,124 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>[HA] <%= hostname %></title> <title>[HA] <%= hostname %></title>
<link rel="stylesheet" href="/static/app.css"> <link rel="stylesheet" href="/static/app.css">
</head> </head>
<body> <body>
<main> <main>
<div> <div>
<img id="logo" src="/static/logo.svg" /> <img id="logo" src="/static/logo.svg" />
</div> </div>
<h1>Recycled Cloud HA handler</h1> <h1>Recycled Cloud HA handler</h1>
<p> <p>
This service supervises the various components of This service supervises the various components of
the Recycled Cloud's High Availability the Recycled Cloud's High Availability
infrastruture. Documentation and source code can be infrastruture. Documentation and source code can be
found on <a found on <a
href="https://code.recycled.cloud/RecycledCloud/ha-handler">our href="https://code.recycled.cloud/RecycledCloud/ha-handler">our
software forge</a>. software forge</a>.
</p> </p>
<hr /> <hr />
<h2>Handler</h2> <h2>Handler</h2>
<%= otp_app %> <b>v<%= version %></b> (<%= env %>) running on <b><%= hostname %></b> <%= otp_app %> <b>v<%= version %></b> (<%= env %>) running on <b><%= hostname %></b>
<hr /> <hr />
<h2>HAProxy</h2> <h2>HAProxy</h2>
<h3>Frontends</h3> <h3>Frontends</h3>
<table> <table>
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>Status</th> <th>Status</th>
<th>Bytes in</th> <th>Bytes in</th>
<th>Bytes out</th> <th>Bytes out</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<%= for entry <- Map.get(haproxy_stats, "Frontend") do %> <%= for entry <- Map.get(haproxy_stats, "Frontend") do %>
<tr> <tr>
<td><%= entry["pxname"] %></td> <td><%= entry["pxname"] %></td>
<td><%= entry["status"] %></td> <td><%= entry["status"] %></td>
<td><%= entry["bin"] %></td> <td><%= entry["bin"] %></td>
<td><%= entry["bout"] %></td> <td><%= entry["bout"] %></td>
</tr> </tr>
<% end %> <% end %>
</tbody> </tbody>
</table> </table>
<h3>Backends</h3> <h3>Backends</h3>
<table> <table>
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>Status</th> <th>Status</th>
<th>algo</th> <th>algo</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<%= for entry <- Map.get(haproxy_stats, "Backend") do %> <%= for entry <- Map.get(haproxy_stats, "Backend") do %>
<tr> <tr>
<td><%= entry["pxname"] %></td> <td><%= entry["pxname"] %></td>
<td><%= entry["status"] %></td> <td><%= entry["status"] %></td>
<td><%= entry["algo"] %></td> <td><%= entry["algo"] %></td>
</tr> </tr>
<% end %> <% end %>
</tbody> </tbody>
</table> </table>
<h3>Servers</h3> <h3>Servers</h3>
<table> <table>
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>Status</th> <th>Status</th>
<th>Mode</th> <th>Mode</th>
<th>Address</th> <th>Address</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<%= for entry <- Map.get(haproxy_stats, "Server") do %> <%= for entry <- Map.get(haproxy_stats, "Server") do %>
<tr> <tr>
<td><%= entry["pxname"] %>/<%= entry["svname"] %></td> <td><%= entry["pxname"] %>/<%= entry["svname"] %></td>
<td><%= entry["status"] %></td> <td><%= entry["status"] %></td>
<td><%= entry["mode"] %></td> <td><%= entry["mode"] %></td>
<td><%= entry["addr"] %></td> <td><%= entry["addr"] %></td>
</tr> </tr>
<% end %> <% end %>
</tbody> </tbody>
</table> </table>
<hr /> <hr />
<h2>PostgreSQL</h2> <h2>PostgreSQL</h2>
<table> <table>
<thead> <thead>
<tr> <tr>
<th>Hostname</th> <th>Hostname</th>
<th>Version</th> <th>Version</th>
<th>Status</th> <th>Status</th>
<th>Operation</th> <th>Operation</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<%= for entry <- pgsql_status do %> <%= for entry <- pgsql_status do %>
<tr> <tr>
<td><%= entry[:hostname] %></td> <td><%= entry[:hostname] %></td>
<td><%= entry[:version] %></td> <td><%= entry[:version] %></td>
<td><%= entry[:status] %></td> <td><%= entry[:status] %></td>
<td><%= entry[:mode] %></td> <td><%= entry[:mode] %></td>
</tr> </tr>
<% end %> <% end %>
</tbody> </tbody>
</table> </table>
</main> </main>
</body> </body>
</html> </html>