diff --git a/.github/workflows/publish_docker.yml b/.github/workflows/publish_docker.yml index 780051fa..fefea478 100644 --- a/.github/workflows/publish_docker.yml +++ b/.github/workflows/publish_docker.yml @@ -3,7 +3,7 @@ name: Release on Dockerhub on: push: branches: - - main + - main_v0.9 paths: - ".github/workflows/publish_docker.yml" - "VERSION" diff --git a/.github/workflows/stage.yml b/.github/workflows/stage.yml index f66b7e9c..0d81a108 100644 --- a/.github/workflows/stage.yml +++ b/.github/workflows/stage.yml @@ -3,7 +3,7 @@ name: Publish upgrade artifacts to staging on: push: branches: - - main + - main_v0.9 env: INCLUDE_ERTS: false MIX_ENV: prod diff --git a/.gitignore b/.gitignore index f15ae4a7..c396724a 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ npm-debug.log burrito_out/* supavisor-*.tar.gz + +priv/native/* diff --git a/VERSION b/VERSION index 1424e647..3eefcb9d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.9.32 +1.0.0 diff --git a/bench/pg_parser.exs b/bench/pg_parser.exs new file mode 100644 index 00000000..2029094f --- /dev/null +++ b/bench/pg_parser.exs @@ -0,0 +1,7 @@ +alias Supavisor.PgParser, as: Parser + +Benchee.run(%{ + "statement_types/1" => fn -> + Parser.statement_types("insert into table1 values ('a', 'b')") + end +}) diff --git a/bench/protocol.exs b/bench/protocol.exs new file mode 100644 index 00000000..2fc15266 --- /dev/null +++ b/bench/protocol.exs @@ -0,0 +1,9 @@ +alias Supavisor.Protocol.Client + +bin_select_1 = <<81, 0, 0, 0, 14, 115, 101, 108, 101, 99, 116, 32, 49, 59, 0>> + +Benchee.run(%{ + "Client.decode_pkt/1" => fn -> + Client.decode_pkt(bin_select_1) + end +}) diff --git a/config/config.exs b/config/config.exs index 126c3806..5eb444bb 100644 --- a/config/config.exs +++ b/config/config.exs @@ -22,7 +22,7 @@ config :supavisor, SupavisorWeb.Endpoint, # Configures Elixir's Logger config :logger, :console, format: "$time $metadata[$level] $message\n", - metadata: [:request_id, :project, :user, :region, :instance_id, :mode] + metadata: [:request_id, :project, :user, :region, :instance_id, :mode, :type] # Use Jason for JSON parsing in Phoenix config :phoenix, :json_library, Jason diff --git a/config/dev.exs b/config/dev.exs index 8ab27e2a..843b9f47 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -63,7 +63,7 @@ config :logger, :console, format: "$time [$level] $message $metadata\n", level: :debug, # level: :error, - metadata: [:error_code, :file, :line, :pid, :project, :user, :mode] + metadata: [:error_code, :file, :line, :pid, :project, :user, :mode, :type] # Set a higher stacktrace during development. Avoid configuring such # in production as building large stacktraces may be expensive. diff --git a/lib/supavisor.ex b/lib/supavisor.ex index 0d8d5cd0..8b453765 100644 --- a/lib/supavisor.ex +++ b/lib/supavisor.ex @@ -2,7 +2,8 @@ defmodule Supavisor do @moduledoc false require Logger alias Supavisor.Helpers, as: H - alias Supavisor.{Tenants, Tenants.Tenant, Manager} + alias Supavisor.Tenants, as: T + alias Supavisor.Manager @type sock :: tcp_sock() | ssl_sock() @type ssl_sock :: {:ssl, :ssl.sslsocket()} @@ -10,7 +11,7 @@ defmodule Supavisor do @type workers :: %{manager: pid, pool: pid} @type secrets :: {:password | :auth_query, fun()} @type mode :: :transaction | :session | :native - @type id :: {String.t(), String.t(), mode} + @type id :: {{:single | :cluster, String.t()}, String.t(), mode} @type subscribe_opts :: %{workers: workers, ps: list, idle_timeout: integer} @registry Supavisor.Registry.Tenants @@ -125,11 +126,22 @@ defmodule Supavisor do } end - @spec get_local_pool(id) :: pid | nil + @spec get_local_pool(id) :: map | pid | nil def get_local_pool(id) do - case Registry.lookup(@registry, {:pool, id}) do - [{pid, _}] -> pid - _ -> nil + match = {{:pool, :_, :_, id}, :"$2", :"$3"} + body = [{{:"$2", :"$3"}}] + + case Registry.select(@registry, [{match, [], body}]) do + [{pool, _}] -> + pool + + [_ | _] = pools -> + # transform [{pid1, :read}, {pid2, :read}, {pid3, :write}] + # to %{read: [pid1, pid2], write: [pid3]} + Enum.group_by(pools, &elem(&1, 1), &elem(&1, 0)) + + _ -> + nil end end @@ -141,7 +153,7 @@ defmodule Supavisor do end end - @spec id(String.t(), String.t(), mode, mode) :: id + @spec id({:single | :cluster, String.t()}, String.t(), mode, mode) :: id def id(tenant, user, port_mode, user_mode) do # temporary hack mode = @@ -157,84 +169,106 @@ defmodule Supavisor do ## Internal functions @spec start_local_pool(id, secrets, String.t() | nil) :: {:ok, pid} | {:error, any} - defp start_local_pool({tenant, user, mode} = id, {method, secrets}, db_name) do - Logger.debug("Starting pool for #{inspect(id)}") + defp start_local_pool({{type, tenant}, _user, _mode} = id, secrets, db_name) do + Logger.debug("Starting pool(s) for #{inspect(id)}") - case Tenants.get_pool_config(tenant, secrets.().alias) do - %Tenant{} = tenant_record -> - %{ - db_host: db_host, - db_port: db_port, - db_database: db_database, - default_parameter_status: ps, - ip_version: ip_ver, - default_pool_size: def_pool_size, - default_max_clients: def_max_clients, - client_idle_timeout: client_idle_timeout, - default_pool_strategy: default_pool_strategy, - users: [ - %{ - db_user: db_user, - db_password: db_pass, - pool_size: pool_size, - # mode_type: mode_type, - max_clients: max_clients - } - ] - } = tenant_record - - {pool_size, max_clients} = - if method == :auth_query do - {def_pool_size, def_max_clients} - else - {pool_size, max_clients} - end - - auth = %{ - host: String.to_charlist(db_host), - port: db_port, - user: db_user, - database: if(db_name != nil, do: db_name, else: db_database), - password: fn -> db_pass end, - application_name: "Supavisor", - ip_version: H.ip_version(ip_ver, db_host), - upstream_ssl: tenant_record.upstream_ssl, - upstream_verify: tenant_record.upstream_verify, - upstream_tls_ca: H.upstream_cert(tenant_record.upstream_tls_ca), - require_user: tenant_record.require_user, - method: method, - secrets: secrets - } + user = elem(secrets, 1).().alias - args = %{ - id: id, - tenant: tenant, - user: user, - auth: auth, - pool_size: pool_size, - mode: mode, - default_parameter_status: ps, - max_clients: max_clients, - client_idle_timeout: client_idle_timeout, - default_pool_strategy: default_pool_strategy - } + case type do + :single -> T.get_pool_config(tenant, user) + :cluster -> T.get_cluster_config(tenant, user) + end + |> case do + [_ | _] = replicas -> + opts = + Enum.map(replicas, fn replica -> + case replica do + %T.ClusterTenants{tenant: tenant, type: type} -> + Map.put(tenant, :replica_type, type) + + %T.Tenant{} = tenant -> + Map.put(tenant, :replica_type, :write) + end + |> supervisor_args(id, secrets, db_name) + end) DynamicSupervisor.start_child( {:via, PartitionSupervisor, {Supavisor.DynamicSupervisor, id}}, - {Supavisor.TenantSupervisor, args} + {Supavisor.TenantSupervisor, %{id: id, replicas: opts}} ) |> case do {:error, {:already_started, pid}} -> {:ok, pid} resp -> resp end - _ -> - Logger.error("Can't find tenant with external_id #{inspect(id)}") + error -> + Logger.error("Can't find tenant with external_id #{inspect(id)} #{inspect(error)}") {:error, :tenant_not_found} end end + defp supervisor_args(tenant_record, {tenant, user, mode} = id, {method, secrets}, db_name) do + %{ + db_host: db_host, + db_port: db_port, + db_database: db_database, + default_parameter_status: ps, + ip_version: ip_ver, + default_pool_size: def_pool_size, + default_max_clients: def_max_clients, + client_idle_timeout: client_idle_timeout, + replica_type: replica_type, + default_pool_strategy: default_pool_strategy, + users: [ + %{ + db_user: db_user, + db_password: db_pass, + pool_size: pool_size, + # mode_type: mode_type, + max_clients: max_clients + } + ] + } = tenant_record + + {pool_size, max_clients} = + if method == :auth_query do + {def_pool_size, def_max_clients} + else + {pool_size, max_clients} + end + + auth = %{ + host: String.to_charlist(db_host), + port: db_port, + user: db_user, + database: if(db_name != nil, do: db_name, else: db_database), + password: fn -> db_pass end, + application_name: "Supavisor", + ip_version: H.ip_version(ip_ver, db_host), + upstream_ssl: tenant_record.upstream_ssl, + upstream_verify: tenant_record.upstream_verify, + upstream_tls_ca: H.upstream_cert(tenant_record.upstream_tls_ca), + require_user: tenant_record.require_user, + method: method, + secrets: secrets + } + + %{ + id: id, + tenant: tenant, + replica_type: replica_type, + user: user, + auth: auth, + pool_size: pool_size, + mode: mode, + default_parameter_status: ps, + max_clients: max_clients, + client_idle_timeout: client_idle_timeout, + default_pool_strategy: default_pool_strategy + } + end + @spec set_parameter_status(id, [{binary, binary}]) :: :ok | {:error, :not_found} def set_parameter_status(id, ps) do diff --git a/lib/supavisor/client_handler.ex b/lib/supavisor/client_handler.ex index e8213bde..3c847cb6 100644 --- a/lib/supavisor/client_handler.ex +++ b/lib/supavisor/client_handler.ex @@ -15,7 +15,7 @@ defmodule Supavisor.ClientHandler do alias Supavisor.DbHandler, as: Db alias Supavisor.Helpers, as: H alias Supavisor.HandlerHelpers, as: HH - alias Supavisor.{Tenants, Monitoring.Telem, Protocol.Server} + alias Supavisor.{Tenants, Monitoring.Telem, Protocol.Client, Protocol.Server} @impl true def start_link(ref, _sock, transport, opts) do @@ -26,8 +26,8 @@ defmodule Supavisor.ClientHandler do @impl true def callback_mode, do: [:handle_event_function] - def client_cast(pid, bin, ready?) do - :gen_statem.cast(pid, {:client_cast, bin, ready?}) + def client_cast(pid, bin, status) do + :gen_statem.cast(pid, {:client_cast, bin, status}) end @impl true @@ -58,7 +58,8 @@ defmodule Supavisor.ClientHandler do mode: opts.mode, stats: %{}, idle_timeout: 0, - db_name: nil + db_name: nil, + last_query: nil } :gen_statem.enter_loop(__MODULE__, [hibernate_after: 5_000], :exchange, data) @@ -82,8 +83,7 @@ defmodule Supavisor.ClientHandler do def handle_event(:info, :cancel_query, :busy, data) do key = {data.tenant, data.db_pid} Logger.debug("Cancel query for #{inspect(key)}") - - db_pid = data.db_pid + {_pool, db_pid} = data.db_pid case db_pid_meta(key) do [{^db_pid, meta}] -> @@ -134,10 +134,16 @@ defmodule Supavisor.ClientHandler do case Server.decode_startup_packet(bin) do {:ok, hello} -> Logger.debug("Client startup message: #{inspect(hello)}") - {user, external_id} = HH.parse_user_info(hello.payload) - Logger.metadata(project: external_id, user: user, mode: data.mode) - data = %{data | db_name: hello.payload["database"]} - {:keep_state, data, {:next_event, :internal, {:hello, {user, external_id}}}} + {type, {user, tenant_or_alias}} = HH.parse_user_info(hello.payload) + + Logger.metadata( + project: tenant_or_alias, + user: user, + mode: data.mode, + type: type + ) + + {:keep_state, data, {:next_event, :internal, {:hello, {type, {user, tenant_or_alias}}}}} {:error, error} -> Logger.error("Client startup message error: #{inspect(error)}") @@ -146,16 +152,29 @@ defmodule Supavisor.ClientHandler do end end - def handle_event(:internal, {:hello, {user, external_id}}, :exchange, %{sock: sock} = data) do + def handle_event( + :internal, + {:hello, {type, {user, tenant_or_alias}}}, + :exchange, + %{sock: sock} = data + ) do sni_hostname = HH.try_get_sni(sock) - case Tenants.get_user_cache(user, external_id, sni_hostname) do + case Tenants.get_user_cache(type, user, tenant_or_alias, sni_hostname) do {:ok, info} -> - id = Supavisor.id(info.tenant.external_id, user, data.mode, info.user.mode_type) + id = + Supavisor.id( + {type, tenant_or_alias}, + user, + data.mode, + info.user.mode_type + ) + Registry.register(Supavisor.Registry.TenantClients, id, []) if info.tenant.enforce_ssl and !data.ssl do Logger.error("Tenant is not allowed to connect without SSL, user #{user}") + :ok = HH.send_error(sock, "XX000", "SSL connection is required") Telem.client_join(:fail, data.id) {:stop, :normal} @@ -165,6 +184,7 @@ defmodule Supavisor.ClientHandler do case auth_secrets(info, user) do {:ok, auth_secrets} -> Logger.debug("Authentication method: #{inspect(auth_secrets)}") + {:keep_state, new_data, {:next_event, :internal, {:handle, auth_secrets}}} {:error, reason} -> @@ -177,7 +197,9 @@ defmodule Supavisor.ClientHandler do end {:error, reason} -> - Logger.error("User not found: #{inspect(reason)} #{inspect({user, external_id})}") + Logger.error( + "User not found: #{inspect(reason)} #{inspect({type, user, tenant_or_alias})}" + ) :ok = HH.send_error(sock, "XX000", "Tenant or user not found") Telem.client_join(:fail, data.id) @@ -185,7 +207,12 @@ defmodule Supavisor.ClientHandler do end end - def handle_event(:internal, {:handle, {method, secrets}}, _, %{sock: sock} = data) do + def handle_event( + :internal, + {:handle, {method, secrets}}, + _, + %{sock: sock} = data + ) do Logger.debug("Handle exchange, auth method: #{inspect(method)}") case handle_exchange(sock, {method, secrets}) do @@ -229,7 +256,7 @@ defmodule Supavisor.ClientHandler do {:ok, opts} <- Supavisor.subscribe(sup, data.id) do Process.monitor(opts.workers.manager) data = Map.merge(data, opts.workers) - db_pid = db_checkout(:on_connect, data) + db_pid = db_checkout(:both, :on_connect, data) data = %{data | db_pid: db_pid, idle_timeout: opts.idle_timeout} next = @@ -284,13 +311,15 @@ defmodule Supavisor.ClientHandler do end # handle Terminate message - def handle_event(:info, {proto, _, <>}, :idle, _) when proto in [:tcp, :ssl] do + def handle_event(:info, {proto, _, <>}, :idle, _) + when proto in [:tcp, :ssl] do Logger.debug("Receive termination") {:stop, :normal} end # handle Sync message - def handle_event(:info, {proto, _, <>}, :idle, data) when proto in [:tcp, :ssl] do + def handle_event(:info, {proto, _, <>}, :idle, data) + when proto in [:tcp, :ssl] do Logger.debug("Receive sync") :ok = HH.sock_send(data.sock, Server.ready_for_query()) @@ -301,16 +330,49 @@ defmodule Supavisor.ClientHandler do end end - def handle_event(:info, {proto, _, bin}, :idle, data) do + # incoming query with a single pool + def handle_event(:info, {proto, _, bin}, :idle, %{pool: pid} = data) when is_pid(pid) do ts = System.monotonic_time() - db_pid = db_checkout(:on_query, data) + db_pid = db_checkout(:both, :on_query, data) + handle_prepared_statements(db_pid, bin, data) {:next_state, :busy, %{data | db_pid: db_pid, query_start: ts}, {:next_event, :internal, {proto, nil, bin}}} end - def handle_event(_, {proto, _, bin}, :busy, data) when proto in [:tcp, :ssl] do - case Db.call(data.db_pid, self(), bin) do + # incoming query with read/write pools + def handle_event(:info, {proto, _, bin}, :idle, data) do + query_type = + with {:ok, payload} <- Client.get_payload(bin), + {:ok, statements} <- Supavisor.PgParser.statements(payload) do + Logger.debug( + "Receive payload #{inspect(payload, pretty: true)} statements #{inspect(statements)}" + ) + + case statements do + # naive check for read only queries + ["SelectStmt"] -> :read + _ -> :write + end + else + other -> + Logger.error("Receive query error: #{inspect(other)}") + :write + end + + ts = System.monotonic_time() + db_pid = db_checkout(query_type, :on_query, data) + + {:next_state, :busy, %{data | db_pid: db_pid, query_start: ts, last_query: bin}, + {:next_event, :internal, {proto, nil, bin}}} + end + + # forward query to db + def handle_event(_, {proto, _, bin}, :busy, data) + when proto in [:tcp, :ssl] do + {_, db_pid} = data.db_pid + + case Db.call(db_pid, self(), bin) do :ok -> Logger.debug("DB call success") :keep_state_and_data @@ -346,6 +408,11 @@ defmodule Supavisor.ClientHandler do end # client closed connection + def handle_event(_, {:tcp_closed, _}, _, data) do + Logger.debug("tcp soket closed for #{inspect(data.tenant)}") + {:stop, :normal} + end + def handle_event(_, {closed, _}, _, data) when closed in [:tcp_closed, :ssl_closed] do Logger.debug("#{closed} soket closed for #{inspect(data.tenant)}") @@ -374,30 +441,45 @@ defmodule Supavisor.ClientHandler do end # emulate handle_cast - def handle_event(:cast, {:client_cast, bin, ready?}, _, data) do + def handle_event(:cast, {:client_cast, bin, status}, _, data) do Logger.debug("--> --> bin #{inspect(byte_size(bin))} bytes") - :ok = HH.sock_send(data.sock, bin) + case status do + :ready_for_query -> + Logger.debug("Client is ready") - if ready? do - Logger.debug("Client is ready") + db_pid = handle_db_pid(data.mode, data.pool, data.db_pid) - db_pid = handle_db_pid(data.mode, data.pool, data.db_pid) + {_, stats} = Telem.network_usage(:client, data.sock, data.id, data.stats) - {_, stats} = Telem.network_usage(:client, data.sock, data.id, data.stats) - Telem.client_query_time(data.query_start, data.id) + Telem.client_query_time(data.query_start, data.id) + :ok = HH.sock_send(data.sock, bin) - actions = - if data.idle_timeout > 0 do - idle_check(data.idle_timeout) - else - [] - end + actions = + if data.idle_timeout > 0 do + idle_check(data.idle_timeout) + else + [] + end - {:next_state, :idle, %{data | db_pid: db_pid, stats: stats}, actions} - else - Logger.debug("Client is not ready") - :keep_state_and_data + {:next_state, :idle, %{data | db_pid: db_pid, stats: stats}, actions} + + :continue -> + Logger.debug("Client is not ready") + :ok = HH.sock_send(data.sock, bin) + :keep_state_and_data + + :read_sql_error -> + Logger.error("read only sql transaction, reruning the query to write pool") + + # release the read pool + _ = handle_db_pid(data.mode, data.pool, data.db_pid) + + ts = System.monotonic_time() + db_pid = db_checkout(:write, :on_query, data) + + {:keep_state, %{data | db_pid: db_pid, query_start: ts}, + {:next_event, :internal, {:tcp, nil, data.last_query}}} end end @@ -463,13 +545,23 @@ defmodule Supavisor.ClientHandler do %{ tag: :password_message, payload: {:scram_sha_256, %{"n" => user, "r" => nonce, "c" => channel}} - }, _} <- receive_next(socket, "Timeout while waiting for the first password message"), + }, + _} <- + receive_next( + socket, + "Timeout while waiting for the first password message" + ), {:ok, signatures} = reply_first_exchange(sock, method, secrets, channel, nonce, user), {:ok, %{ tag: :password_message, payload: {:first_msg_response, %{"p" => p}} - }, _} <- receive_next(socket, "Timeout while waiting for the second password message"), + }, + _} <- + receive_next( + socket, + "Timeout while waiting for the second password message" + ), {:ok, key} <- authenticate_exchange(method, secrets, signatures, p) do message = "v=#{Base.encode64(signatures.server)}" :ok = HH.sock_send(sock, Server.exchange_message(:final, message)) @@ -520,23 +612,37 @@ defmodule Supavisor.ClientHandler do end end - @spec db_checkout(:on_connect | :on_query, map()) :: pid() | nil - defp db_checkout(_, %{mode: :session, db_pid: db_pid}) when is_pid(db_pid) do + @spec db_checkout(:write | :read | :both, :on_connect | :on_query, map) :: {pid, pid} | nil + defp db_checkout(_, _, %{mode: :session, db_pid: db_pid}) when is_pid(db_pid) do db_pid end - defp db_checkout(:on_connect, %{mode: :transaction}), do: nil + defp db_checkout(_, :on_connect, %{mode: :transaction}), do: nil + + defp db_checkout(query_type, _, data) when query_type in [:write, :read] do + pool = + data.pool[query_type] + |> Enum.random() - defp db_checkout(_, data) do + {time, db_pid} = :timer.tc(:poolboy, :checkout, [pool, true, data.timeout]) + Process.link(db_pid) + Telem.pool_checkout_time(time, data.id) + {pool, db_pid} + end + + defp db_checkout(_, _, data) do {time, db_pid} = :timer.tc(:poolboy, :checkout, [data.pool, true, data.timeout]) + Process.link(db_pid) Telem.pool_checkout_time(time, data.id) - db_pid + {data.pool, db_pid} end - @spec handle_db_pid(:transaction, pid(), pid()) :: nil + @spec handle_db_pid(:transaction, pid(), pid() | nil) :: nil @spec handle_db_pid(:session, pid(), pid()) :: pid() - defp handle_db_pid(:transaction, pool, db_pid) do + defp handle_db_pid(:transaction, _pool, nil), do: nil + + defp handle_db_pid(:transaction, _pool, {pool, db_pid}) do Process.unlink(db_pid) :poolboy.checkin(pool, db_pid) nil @@ -672,19 +778,54 @@ defmodule Supavisor.ClientHandler do {message, sings} end + @spec try_get_sni(S.sock()) :: String.t() | nil + def try_get_sni({:ssl, sock}) do + case :ssl.connection_information(sock, [:sni_hostname]) do + {:ok, [sni_hostname: sni]} -> List.to_string(sni) + _ -> nil + end + end + + def try_get_sni(_), do: nil + @spec idle_check(non_neg_integer) :: {:timeout, non_neg_integer, :idle_terminate} defp idle_check(timeout) do {:timeout, timeout, :idle_terminate} end - defp db_pid_meta({_, pid} = key) do + defp db_pid_meta({_, {_, pid}} = _key) do rkey = Supavisor.Registry.PoolPids fnode = node(pid) if fnode == node() do - Registry.lookup(rkey, key) + Registry.lookup(rkey, pid) else - :erpc.call(fnode, Registry, :lookup, [rkey, key], 15_000) + :erpc.call(fnode, Registry, :lookup, [rkey, pid], 15_000) end end + + @spec handle_prepared_statements({pid, pid}, binary, map) :: :ok | nil + defp handle_prepared_statements({_, pid}, bin, %{mode: :transaction} = data) do + with {:ok, payload} <- Client.get_payload(bin), + {:ok, statamets} <- Supavisor.PgParser.statements(payload), + true <- Enum.member?([["PrepareStmt"], ["DeallocateStmt"]], statamets) do + Logger.info("Handle prepared statement #{inspect(payload)}") + + GenServer.call(data.pool, :get_all_workers) + |> Enum.each(fn + {_, ^pid, _, [Supavisor.DbHandler]} -> + Logger.debug("Linked db_handler #{inspect(pid)}") + nil + + {_, pool_proc, _, [Supavisor.DbHandler]} -> + Logger.debug( + "Sending prepared statement change #{inspect(payload)} to #{inspect(pool_proc)}" + ) + + send(pool_proc, {:handle_ps, payload, bin}) + end) + end + end + + defp handle_prepared_statements(_, _, _), do: nil end diff --git a/lib/supavisor/db_handler.ex b/lib/supavisor/db_handler.ex index 0d28e8c5..c9a448a6 100644 --- a/lib/supavisor/db_handler.ex +++ b/lib/supavisor/db_handler.ex @@ -38,13 +38,15 @@ defmodule Supavisor.DbHandler do user: args.user, tenant: args.tenant, buffer: [], + anon_buffer: [], db_state: nil, parameter_status: %{}, nonce: nil, messages: "", server_proof: nil, stats: %{}, - mode: args.mode + mode: args.mode, + replica_type: args.replica_type } {:ok, :connect, data, {:next_event, :internal, :connect}} @@ -88,7 +90,10 @@ defmodule Supavisor.DbHandler do end other -> - Logger.error("Connection failed #{inspect(other)}") + Logger.error( + "Connection failed #{inspect(other)} to #{inspect(auth.host)}:#{inspect(auth.port)}" + ) + reconnect_callback end end @@ -111,7 +116,7 @@ defmodule Supavisor.DbHandler do {ps, db_state} %{tag: :backend_key_data, payload: payload}, acc -> - key = {data.tenant, self()} + key = self() conn = %{host: data.auth.host, port: data.auth.port, ip_ver: data.auth.ip_version} Registry.register(Supavisor.Registry.PoolPids, key, Map.merge(payload, conn)) Logger.debug("Backend #{inspect(key)} data: #{inspect(payload)}") @@ -240,13 +245,50 @@ defmodule Supavisor.DbHandler do {:keep_state, %{data | buffer: []}} end - def handle_event(:info, {_proto, _, bin}, _, %{caller: caller} = data) when is_pid(caller) do - # check if the response ends with "ready for query" - ready = String.ends_with?(bin, Server.ready_for_query()) - Logger.debug("Db ready #{inspect(ready)}") - :ok = Client.client_cast(caller, bin, ready) + # check if it needs to apply queries from the anon buffer + def handle_event(:internal, :check_anon_buffer, _, %{anon_buffer: buff, caller: nil} = data) do + if buff != [] do + Logger.debug("Anon buffer is not empty, try to send #{IO.iodata_length(buff)} bytes") + buff = Enum.reverse(buff) + :ok = sock_send(data.sock, buff) + end + + {:keep_state, %{data | anon_buffer: []}} + end + + # the process received message from db without linked caller + def handle_event(:info, {proto, _, bin}, _, %{caller: nil}) when proto in [:tcp, :ssl] do + Logger.warning("Got db response #{inspect(bin)} when caller was nil") + :keep_state_and_data + end + + def handle_event(:info, {proto, _, bin}, _, %{replica_type: :read} = data) + when proto in [:tcp, :ssl] do + Logger.debug("Got read replica message #{inspect(bin)}") + pkts = Server.decode(bin) + + resp = + cond do + Server.has_read_only_error?(pkts) -> + Logger.error("read only error") + + with [_] <- pkts do + # need to flush ready_for_query if it's not in same packet + :ok = receive_ready_for_query() + end + + :read_sql_error + + List.last(pkts).tag == :ready_for_query -> + :ready_for_query + + true -> + :continue + end + + :ok = Client.client_cast(data.caller, bin, resp) - if ready do + if resp != :continue do {_, stats} = Telem.network_usage(:db, data.sock, data.id, data.stats) {:keep_state, %{data | stats: stats, caller: nil}} else @@ -254,6 +296,38 @@ defmodule Supavisor.DbHandler do end end + def handle_event(:info, {proto, _, bin}, _, %{caller: caller} = data) + when is_pid(caller) and proto in [:tcp, :ssl] do + Logger.debug("Got write replica message #{inspect(bin)}") + # check if the response ends with "ready for query" + ready = + if String.ends_with?(bin, Server.ready_for_query()) do + :ready_for_query + else + :continue + end + + :ok = Client.client_cast(data.caller, bin, ready) + + case ready do + :ready_for_query -> + {_, stats} = Telem.network_usage(:db, data.sock, data.id, data.stats) + + {:keep_state, %{data | stats: stats, caller: nil}, + {:next_event, :internal, :check_anon_buffer}} + + :continue -> + :keep_state_and_data + end + end + + def handle_event(:info, {:handle_ps, payload, bin}, _state, data) do + Logger.notice("Apply prepare statement change #{inspect(payload)}") + + {:keep_state, %{data | anon_buffer: [bin | data.anon_buffer]}, + {:next_event, :internal, :check_anon_buffer}} + end + def handle_event({:call, from}, {:db_call, caller, bin}, :idle, %{sock: sock} = data) do reply = {:reply, from, sock_send(sock, bin)} {:keep_state, %{data | caller: caller}, reply} @@ -300,6 +374,13 @@ defmodule Supavisor.DbHandler do :keep_state_and_data end + @impl true + def terminate(:shutdown, _state, _data), do: :ok + + def terminate(reason, state, _data) do + Logger.error("Terminating with reason #{inspect(reason)} when state was #{inspect(state)}") + end + @spec try_ssl_handshake(S.tcp_sock(), map) :: {:ok, S.sock()} | {:error, term()} defp try_ssl_handshake(sock, %{upstream_ssl: true} = auth) do case sock_send(sock, Server.ssl_request()) do @@ -384,4 +465,14 @@ defmodule Supavisor.DbHandler do auth.secrets.().user end end + + @spec receive_ready_for_query() :: :ok | :timeout_error + defp receive_ready_for_query() do + receive do + {_proto, _socket, <>} -> + :ok + after + 15_000 -> :timeout_error + end + end end diff --git a/lib/supavisor/handler_helpers.ex b/lib/supavisor/handler_helpers.ex index cef52004..0257ccee 100644 --- a/lib/supavisor/handler_helpers.ex +++ b/lib/supavisor/handler_helpers.ex @@ -79,20 +79,27 @@ defmodule Supavisor.HandlerHelpers do def try_get_sni(_), do: nil - @spec parse_user_info(map) :: {String.t() | nil, String.t()} + @spec parse_user_info(map) :: {:cluster | :single, {String.t() | nil, String.t()}} def parse_user_info(%{"user" => user, "options" => %{"reference" => ref}}) do - {user, ref} + # TODO: parse ref for cluster + {:single, {user, ref}} end def parse_user_info(%{"user" => user}) do - case :binary.matches(user, ".") do - [] -> - {user, nil} - - matches -> - {pos, 1} = List.last(matches) - <> = user - {name, external_id} + case :binary.split(user, ".cluster.") do + [user] -> + case :binary.matches(user, ".") do + [] -> + {:single, {user, nil}} + + matches -> + {pos, 1} = List.last(matches) + <> = user + {:single, {name, external_id}} + end + + [user, tenant] -> + {:cluster, {user, tenant}} end end diff --git a/lib/supavisor/manager.ex b/lib/supavisor/manager.ex index 19fc15d9..35a1eec3 100644 --- a/lib/supavisor/manager.ex +++ b/lib/supavisor/manager.ex @@ -35,6 +35,8 @@ defmodule Supavisor.Manager do def init(args) do tid = :ets.new(__MODULE__, [:public]) + [args | _] = Enum.filter(args.replicas, fn e -> e.replica_type == :write end) + state = %{ id: args.id, check_ref: check_subscribers(), @@ -47,8 +49,8 @@ defmodule Supavisor.Manager do idle_timeout: args.client_idle_timeout } - {tenant, user, _mode} = args.id - Logger.metadata(project: tenant, user: user) + {{type, tenant}, user, _mode} = args.id + Logger.metadata(project: tenant, user: user, type: type) Registry.register(Supavisor.Registry.ManagerTables, args.id, tid) {:ok, state} @@ -122,6 +124,11 @@ defmodule Supavisor.Manager do end end + def handle_info(msg, state) do + Logger.warning("Undefined msg: #{inspect(msg, pretty: true)}") + {:noreply, state} + end + ## Internal functions defp check_subscribers() do diff --git a/lib/supavisor/monitoring/prom_ex.ex b/lib/supavisor/monitoring/prom_ex.ex index d52d7af4..d0ea0174 100644 --- a/lib/supavisor/monitoring/prom_ex.ex +++ b/lib/supavisor/monitoring/prom_ex.ex @@ -30,8 +30,8 @@ defmodule Supavisor.Monitoring.PromEx do end @spec remove_metrics(S.id()) :: non_neg_integer - def remove_metrics({tenant, user, mode}) do - meta = %{tenant: tenant, user: user, mode: mode} + def remove_metrics({{type, tenant}, user, mode}) do + meta = %{tenant: tenant, user: user, mode: mode, type: type} Supavisor.Monitoring.PromEx.Metrics |> :ets.select_delete([{{{:_, meta}, :_}, [], [true]}]) @@ -92,7 +92,7 @@ defmodule Supavisor.Monitoring.PromEx do |> Enum.uniq() _ = - Enum.reduce(pools, metrics, fn {tenant, _, _}, acc -> + Enum.reduce(pools, metrics, fn {{_type, tenant}, _, _}, acc -> {matched, rest} = Enum.split_with(acc, &String.contains?(&1, "tenant=\"#{tenant}\"")) if matched != [] do diff --git a/lib/supavisor/monitoring/telem.ex b/lib/supavisor/monitoring/telem.ex index 441c390b..ede5413e 100644 --- a/lib/supavisor/monitoring/telem.ex +++ b/lib/supavisor/monitoring/telem.ex @@ -14,12 +14,12 @@ defmodule Supavisor.Monitoring.Telem do values = Map.new(values) diff = Map.merge(values, stats, fn _, v1, v2 -> v1 - v2 end) - {tenant, user, mode} = id + {{ptype, tenant}, user, mode} = id :telemetry.execute( [:supavisor, type, :network, :stat], diff, - %{tenant: tenant, user: user, mode: mode} + %{tenant: tenant, user: user, mode: mode, type: ptype} ) {:ok, values} @@ -31,20 +31,20 @@ defmodule Supavisor.Monitoring.Telem do end @spec pool_checkout_time(integer(), S.id()) :: :ok - def pool_checkout_time(time, {tenant, user, mode}) do + def pool_checkout_time(time, {{type, tenant}, user, mode}) do :telemetry.execute( [:supavisor, :pool, :checkout, :stop], %{duration: time}, - %{tenant: tenant, user: user, mode: mode} + %{tenant: tenant, user: user, mode: mode, type: type} ) end @spec client_query_time(integer(), S.id()) :: :ok - def client_query_time(start, {tenant, user, mode}) do + def client_query_time(start, {{type, tenant}, user, mode}) do :telemetry.execute( [:supavisor, :client, :query, :stop], %{duration: System.monotonic_time() - start}, - %{tenant: tenant, user: user, mode: mode} + %{tenant: tenant, user: user, mode: mode, type: type} ) end diff --git a/lib/supavisor/monitoring/tenant.ex b/lib/supavisor/monitoring/tenant.ex index ecc075af..c3e4cdd2 100644 --- a/lib/supavisor/monitoring/tenant.ex +++ b/lib/supavisor/monitoring/tenant.ex @@ -6,7 +6,7 @@ defmodule Supavisor.PromEx.Plugins.Tenant do alias Supavisor, as: S - @tags [:tenant, :user, :mode] + @tags [:tenant, :user, :mode, :type] @impl true def polling_metrics(opts) do @@ -134,11 +134,11 @@ defmodule Supavisor.PromEx.Plugins.Tenant do end @spec emit_telemetry_for_tenant({S.id(), non_neg_integer()}) :: :ok - def emit_telemetry_for_tenant({{tenant, user, mode}, count}) do + def emit_telemetry_for_tenant({{{type, tenant}, user, mode}, count}) do :telemetry.execute( [:supavisor, :connections], %{active: count}, - %{tenant: tenant, user: user, mode: mode} + %{tenant: tenant, user: user, mode: mode, type: type} ) end diff --git a/lib/supavisor/native_handler.ex b/lib/supavisor/native_handler.ex index b42f1c73..fed7d9a8 100644 --- a/lib/supavisor/native_handler.ex +++ b/lib/supavisor/native_handler.ex @@ -131,7 +131,7 @@ defmodule Supavisor.NativeHandler do ) do {:ok, hello} = Server.decode_startup_packet(bin) Logger.debug("Startup packet: #{inspect(hello, pretty: true)}") - {user, external_id} = HH.parse_user_info(hello.payload) + {_, {user, external_id}} = HH.parse_user_info(hello.payload) sni_hostname = HH.try_get_sni(sock) Logger.metadata(project: external_id, user: user, mode: "native") diff --git a/lib/supavisor/pg_parser.ex b/lib/supavisor/pg_parser.ex new file mode 100644 index 00000000..961482a9 --- /dev/null +++ b/lib/supavisor/pg_parser.ex @@ -0,0 +1,22 @@ +defmodule Supavisor.PgParser do + use Rustler, otp_app: :supavisor, crate: "pgparser" + + # When your NIF is loaded, it will override this function. + @doc """ + Returns a list of all statements in the given sql string. + + ## Examples + + iex> Supavisor.PgParser.statement_types("select 1; insert into table1 values ('a', 'b')") + {:ok, ["SelectStmt", "InsertStmt"]} + + iex> Supavisor.PgParser.statement_types("not a valid sql") + {:error, "Error parsing query"} + """ + @spec statement_types(String.t()) :: {:ok, [String.t()]} | {:error, String.t()} + def statement_types(_query), do: :erlang.nif_error(:nif_not_loaded) + + @spec statements(String.t()) :: {:ok, [String.t()]} | {:error, String.t()} + def statements(query) when is_binary(query), do: statement_types(query) + def statements(_), do: {:error, "Query must be a string"} +end diff --git a/lib/supavisor/protocol/client.ex b/lib/supavisor/protocol/client.ex new file mode 100644 index 00000000..ecf23f69 --- /dev/null +++ b/lib/supavisor/protocol/client.ex @@ -0,0 +1,164 @@ +defmodule Supavisor.Protocol.Client do + require Logger + + @pkt_header_size 5 + + defmodule Pkt do + defstruct([:tag, :len, :payload, :bin]) + + @type t :: %Pkt{ + tag: atom, + len: integer, + payload: any, + bin: binary + } + end + + def pkt_header_size, do: @pkt_header_size + + def header(<>) do + {tag(char), pkt_len} + end + + @spec decode(binary) :: {:ok, [Pkt.t()], binary} | {:error, any} + def decode(data) do + decode(data, []) + end + + @spec decode(binary, [Pkt.t()]) :: {:ok, [Pkt.t()], binary} | {:error, any} + def decode("", acc), do: {:ok, Enum.reverse(acc), ""} + + def decode(data, acc) do + case decode_pkt(data) do + {:ok, pkt, rest} -> decode(rest, [pkt | acc]) + {:error, :payload_too_small} -> {:ok, Enum.reverse(acc), data} + end + end + + @spec decode_pkt(binary) :: + {:ok, Pkt.t(), binary} + | {:acc, nil, binary} + | {:error, :payload_too_small} + def decode_pkt(<<_::8, pkt_len::32, payload::binary>>) + when byte_size(payload) < pkt_len - 4 do + {:error, :payload_too_small} + end + + def decode_pkt(<>) do + case tag(char) do + nil -> + {:error, {:undefined_tag, <>}} + + tag -> + payload_len = pkt_len - 4 + <> = rest + + {:ok, + %Pkt{ + tag: tag, + len: pkt_len + 1, + payload: decode_payload(tag, bin_payload), + bin: <> + }, rest2} + end + end + + def decode_pkt(_), do: {:error, :header_mismatch} + + @spec get_payload(binary) :: {:ok, String.t()} | {:error, any} + def get_payload(<>) do + case tag(char) do + nil -> + {:error, {:undefined_tag, <>}} + + tag -> + payload_len = pkt_len - 4 + <> = rest + + {:ok, decode_payload(tag, bin_payload)} + end + end + + @spec tag(byte) :: atom | nil + def tag(char) do + case char do + ?Q -> :simple_query + ?H -> :flush_message + ?P -> :parse_message + ?B -> :bind_message + ?D -> :describe_message + ?E -> :execute_message + ?S -> :sync_message + ?X -> :termination_message + ?C -> :close_message + _ -> nil + end + end + + def decode_payload(:simple_query, payload) do + case String.split(payload, <<0>>) do + [query, ""] -> query + _ -> :undefined + end + end + + def decode_payload(:parse_message, payload) do + case String.split(payload, <<0>>) do + [""] -> + :undefined + + other -> + case Enum.filter(other, &(&1 != "")) do + [sql] -> sql + message -> message + end + end + end + + def decode_payload(:describe_message, <>) do + str_name = String.trim_trailing(str_name, <<0>>) + %{char: char, str_name: str_name} + end + + def decode_payload(:close_message, <>) do + str_name = String.trim_trailing(str_name, <<0>>) + %{char: char, str_name: str_name} + end + + def decode_payload(:flush_message, <<4::32>>), do: nil + + def decode_payload(:termination_message, _payload), do: nil + + def decode_payload(:bind_message, _payload), do: nil + + def decode_payload(:execute_message, _payload), do: nil + + def decode_payload(_tag, ""), do: nil + + def decode_payload(_tag, payload) do + Logger.error("undefined payload: #{inspect(payload)}") + :undefined + end + + def decode_startup_packet(<>) do + # <> = protocol + + %Pkt{ + len: len, + payload: + String.split(rest, <<0>>, trim: true) + |> Enum.chunk_every(2) + |> Enum.into(%{}, fn [k, v] -> {k, v} end), + tag: :startup + } + end + + def decode_startup_packet(_) do + :undef + end + + def parse_msg_sel_1() do + <<80, 0, 0, 0, 16, 0, 115, 101, 108, 101, 99, 116, 32, 49, 0, 0, 0, 66, 0, 0, 0, 12, 0, 0, 0, + 0, 0, 0, 0, 0, 68, 0, 0, 0, 6, 80, 0, 69, 0, 0, 0, 9, 0, 0, 0, 0, 200, 83, 0, 0, 0, 4>> + end +end diff --git a/lib/supavisor/protocol/server.ex b/lib/supavisor/protocol/server.ex index 96c04e38..dbbffa66 100644 --- a/lib/supavisor/protocol/server.ex +++ b/lib/supavisor/protocol/server.ex @@ -12,14 +12,22 @@ defmodule Supavisor.Protocol.Server do @authentication_ok <> @ready_for_query <> @ssl_request <<8::32, 1234::16, 5679::16>> + @auth_request <> @scram_request <> @msg_cancel_header <<16::32, 1234::16, 5678::16>> defmodule Pkt do @moduledoc "Representing a packet structure with tag, length, and payload fields." defstruct([:tag, :len, :payload]) + + @type t :: %Pkt{ + tag: atom, + len: integer, + payload: any + } end + @spec decode(iodata()) :: [Pkt.t()] | [] def decode(data) do decode(data, []) end @@ -445,4 +453,12 @@ defmodule Supavisor.Protocol.Server do def cancel_message(pid, key) do [@msg_cancel_header, <>] end + + @spec has_read_only_error?(list) :: boolean + def has_read_only_error?(pkts) do + Enum.any?(pkts, fn + %{payload: ["SERROR", "VERROR", "C25006" | _]} -> true + _ -> false + end) + end end diff --git a/lib/supavisor/syn_handler.ex b/lib/supavisor/syn_handler.ex index e2885a3c..db6affa0 100644 --- a/lib/supavisor/syn_handler.ex +++ b/lib/supavisor/syn_handler.ex @@ -7,7 +7,7 @@ defmodule Supavisor.SynHandler do def on_process_unregistered( :tenants, - {tenant, user, _mode} = id, + {{_type, tenant}, user, _mode} = id, _pid, _meta, reason diff --git a/lib/supavisor/tenant_supervisor.ex b/lib/supavisor/tenant_supervisor.ex index 6c426b94..32fd2424 100644 --- a/lib/supavisor/tenant_supervisor.ex +++ b/lib/supavisor/tenant_supervisor.ex @@ -10,34 +10,31 @@ defmodule Supavisor.TenantSupervisor do end @impl true - def init(%{pool_size: pool_size} = args) do - {size, overflow} = - case args.mode do - :session -> {1, pool_size} - :transaction -> {pool_size, 0} - end + def init(%{replicas: replicas} = args) do + pools = + replicas + |> Enum.with_index() + |> Enum.map(fn {e, i} -> + id = {:pool, e.replica_type, i, args.id} - pool_spec = [ - name: {:via, Registry, {Supavisor.Registry.Tenants, {:pool, args.id}}}, - worker_module: Supavisor.DbHandler, - size: size, - max_overflow: overflow, - strategy: args.default_pool_strategy - ] + %{ + id: {:pool, id}, + start: {:poolboy, :start_link, [pool_spec(id, e), e]}, + restart: :temporary + } + end) - children = [ - %{ - id: {:pool, args.id}, - start: {:poolboy, :start_link, [pool_spec, args]}, - restart: :transient - }, - {Manager, args} - ] + children = [{Manager, args} | pools] - {tenant, user, mode} = args.id - map_id = %{user: user, mode: mode} + {{type, tenant}, user, mode} = args.id + map_id = %{user: user, mode: mode, type: type} Registry.register(Supavisor.Registry.TenantSups, tenant, map_id) - Supervisor.init(children, strategy: :one_for_all, max_restarts: 10, max_seconds: 60) + + Supervisor.init(children, + strategy: :one_for_all, + max_restarts: 10, + max_seconds: 60 + ) end def child_spec(args) do @@ -47,4 +44,21 @@ defmodule Supavisor.TenantSupervisor do restart: :transient } end + + @spec pool_spec(tuple, map) :: Keyword.t() + defp pool_spec(id, args) do + {size, overflow} = + case args.mode do + :session -> {1, args.pool_size} + :transaction -> {args.pool_size, 0} + end + + [ + name: {:via, Registry, {Supavisor.Registry.Tenants, id, args.replica_type}}, + worker_module: Supavisor.DbHandler, + size: size, + max_overflow: overflow, + strategy: :fifo + ] + end end diff --git a/lib/supavisor/tenants.ex b/lib/supavisor/tenants.ex index 0ddba30a..54272d89 100644 --- a/lib/supavisor/tenants.ex +++ b/lib/supavisor/tenants.ex @@ -8,6 +8,8 @@ defmodule Supavisor.Tenants do alias Supavisor.Tenants.Tenant alias Supavisor.Tenants.User + alias Supavisor.Tenants.Cluster + alias Supavisor.Tenants.ClusterTenants @doc """ Returns the list of tenants. @@ -43,6 +45,11 @@ defmodule Supavisor.Tenants do Tenant |> Repo.get_by(external_id: external_id) |> Repo.preload(:users) end + @spec get_cluster_by_alias(String.t()) :: Cluster.t() | nil + def get_cluster_by_alias(alias) do + Cluster |> Repo.get_by(alias: alias) |> Repo.preload(:cluster_tenants) + end + @spec get_tenant_cache(String.t() | nil, String.t() | nil) :: Tenant.t() | nil def get_tenant_cache(external_id, sni_hostname) do cache_key = {:tenant_cache, external_id, sni_hostname} @@ -66,26 +73,45 @@ defmodule Supavisor.Tenants do def get_tenant(_, _), do: nil - @spec get_user_cache(String.t(), String.t() | nil, String.t() | nil) :: + @spec get_user_cache(:single | :cluster, String.t(), String.t() | nil, String.t() | nil) :: {:ok, map()} | {:error, any()} - def get_user_cache(user, external_id, sni_hostname) do - cache_key = {:user_cache, user, external_id, sni_hostname} + def get_user_cache(type, user, external_id, sni_hostname) do + cache_key = {:user_cache, type, user, external_id, sni_hostname} case Cachex.fetch(Supavisor.Cache, cache_key, fn _key -> - {:commit, {:cached, get_user(user, external_id, sni_hostname)}, ttl: 5_000} + {:commit, {:cached, get_user(type, user, external_id, sni_hostname)}, ttl: 5_000} end) do {_, {:cached, value}} -> value {_, {:cached, value}, _} -> value end end - @spec get_user(String.t(), String.t() | nil, String.t() | nil) :: + @spec get_user(atom(), String.t(), String.t() | nil, String.t() | nil) :: {:ok, map()} | {:error, any()} - def get_user(_, nil, nil) do + def get_user(_, _, nil, nil) do {:error, "Either external_id or sni_hostname must be provided"} end - def get_user(user, external_id, sni_hostname) do + def get_user(:cluster, user, external_id, sni_hostname) do + query = + from(ct in ClusterTenants, + where: ct.cluster_alias == ^external_id and ct.active == true, + limit: 1 + ) + + case Repo.all(query) do + [%ClusterTenants{} = ct] -> + get_user(:single, user, ct.tenant_external_id, sni_hostname) + + [_ | _] -> + {:error, :multiple_results} + + _ -> + {:error, :not_found} + end + end + + def get_user(:single, user, external_id, sni_hostname) do query = build_user_query(user, external_id, sni_hostname) case Repo.all(query) do @@ -106,7 +132,7 @@ defmodule Supavisor.Tenants do where: a.db_user_alias == ^user ) - Repo.one( + Repo.all( from(p in Tenant, where: p.external_id == ^external_id, preload: [users: ^query] @@ -114,6 +140,35 @@ defmodule Supavisor.Tenants do ) end + @spec get_cluster_config(String.t(), String.t()) :: [ClusterTenants.t()] | {:error, any()} + def get_cluster_config(external_id, user) do + case Repo.all(ClusterTenants, cluster_alias: external_id) do + [%{cluster_alias: cluster_alias, active: true} | _] -> + user = from(u in User, where: u.db_user_alias == ^user) + tenant = from(t in Tenant, preload: [users: ^user]) + + from(ct in ClusterTenants, + where: ct.cluster_alias == ^cluster_alias and ct.active == true, + preload: [tenant: ^tenant] + ) + |> Repo.all() + |> Enum.reduce_while({nil, []}, &process_cluster/2) + + _ -> + {:error, :not_found} + end + end + + defp process_cluster(cluster, {type, acc}) do + type = if is_nil(type), do: cluster.tenant.require_user, else: type + + case cluster.tenant.users do + [_user] when type == cluster.tenant.require_user -> {:cont, {type, [cluster | acc]}} + [_user] -> {:halt, {:error, {:config, :different_users, cluster.tenant.external_id}}} + _ -> {:halt, {:error, {:config, :multiple_users, cluster.tenant.external_id}}} + end + end + @doc """ Creates a tenant. @@ -186,6 +241,19 @@ defmodule Supavisor.Tenants do end end + @spec delete_cluster_by_alias(String.t()) :: boolean() + def delete_cluster_by_alias(id) do + from(t in Cluster, where: t.alias == ^id) + |> Repo.delete_all() + |> case do + {num, _} when num > 0 -> + true + + _ -> + false + end + end + @doc """ Returns an `%Ecto.Changeset{}` for tracking tenant changes. @@ -287,7 +355,7 @@ defmodule Supavisor.Tenants do on: u.tenant_external_id == t.external_id, where: (u.db_user_alias == ^user and t.require_user == true) or - t.require_user == false, + (t.require_user == false and u.is_manager == true), select: {u, t} ) |> where(^with_tenant(external_id, sni_hostname)) @@ -300,4 +368,207 @@ defmodule Supavisor.Tenants do defp with_tenant(external_id, _) do dynamic([_, t], t.external_id == ^external_id) end + + alias Supavisor.Tenants.Cluster + + @doc """ + Returns the list of clusters. + + ## Examples + + iex> list_clusters() + [%Cluster{}, ...] + + """ + def list_clusters do + Repo.all(Cluster) + end + + @doc """ + Gets a single cluster. + + Raises `Ecto.NoResultsError` if the Cluster does not exist. + + ## Examples + + iex> get_cluster!(123) + %Cluster{} + + iex> get_cluster!(456) + ** (Ecto.NoResultsError) + + """ + def get_cluster!(id), do: Repo.get!(Cluster, id) + + @spec get_cluster_with_rel(String.t()) :: {:ok, Cluster.t()} | {:error, any()} + def get_cluster_with_rel(id) do + case Repo.get(Cluster, id) do + nil -> + {:error, :not_found} + + cluster -> + {:ok, Repo.preload(cluster, :cluster_tenants)} + end + end + + @doc """ + Creates a cluster. + + ## Examples + + iex> create_cluster(%{field: value}) + {:ok, %Cluster{}} + + iex> create_cluster(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_cluster(attrs \\ %{}) do + %Cluster{} + |> Cluster.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a cluster. + + ## Examples + + iex> update_cluster(cluster, %{field: new_value}) + {:ok, %Cluster{}} + + iex> update_cluster(cluster, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_cluster(%Cluster{} = cluster, attrs) do + cluster + |> Cluster.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a cluster. + + ## Examples + + iex> delete_cluster(cluster) + {:ok, %Cluster{}} + + iex> delete_cluster(cluster) + {:error, %Ecto.Changeset{}} + + """ + def delete_cluster(%Cluster{} = cluster) do + Repo.delete(cluster) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking cluster changes. + + ## Examples + + iex> change_cluster(cluster) + %Ecto.Changeset{data: %Cluster{}} + + """ + def change_cluster(%Cluster{} = cluster, attrs \\ %{}) do + Cluster.changeset(cluster, attrs) + end + + alias Supavisor.Tenants.ClusterTenants + + @doc """ + Returns the list of cluster_tenants. + + ## Examples + + iex> list_cluster_tenants() + [%ClusterTenants{}, ...] + + """ + def list_cluster_tenants do + Repo.all(ClusterTenants) + end + + @doc """ + Gets a single cluster_tenants. + + Raises `Ecto.NoResultsError` if the Cluster tenants does not exist. + + ## Examples + + iex> get_cluster_tenants!(123) + %ClusterTenants{} + + iex> get_cluster_tenants!(456) + ** (Ecto.NoResultsError) + + """ + def get_cluster_tenants!(id), do: Repo.get!(ClusterTenants, id) + + @doc """ + Creates a cluster_tenants. + + ## Examples + + iex> create_cluster_tenants(%{field: value}) + {:ok, %ClusterTenants{}} + + iex> create_cluster_tenants(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_cluster_tenants(attrs \\ %{}) do + %ClusterTenants{} + |> ClusterTenants.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a cluster_tenants. + + ## Examples + + iex> update_cluster_tenants(cluster_tenants, %{field: new_value}) + {:ok, %ClusterTenants{}} + + iex> update_cluster_tenants(cluster_tenants, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_cluster_tenants(%ClusterTenants{} = cluster_tenants, attrs) do + cluster_tenants + |> ClusterTenants.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a cluster_tenants. + + ## Examples + + iex> delete_cluster_tenants(cluster_tenants) + {:ok, %ClusterTenants{}} + + iex> delete_cluster_tenants(cluster_tenants) + {:error, %Ecto.Changeset{}} + + """ + def delete_cluster_tenants(%ClusterTenants{} = cluster_tenants) do + Repo.delete(cluster_tenants) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking cluster_tenants changes. + + ## Examples + + iex> change_cluster_tenants(cluster_tenants) + %Ecto.Changeset{data: %ClusterTenants{}} + + """ + def change_cluster_tenants(%ClusterTenants{} = cluster_tenants, attrs \\ %{}) do + ClusterTenants.changeset(cluster_tenants, attrs) + end end diff --git a/lib/supavisor/tenants/cluster.ex b/lib/supavisor/tenants/cluster.ex new file mode 100644 index 00000000..eb7e5de6 --- /dev/null +++ b/lib/supavisor/tenants/cluster.ex @@ -0,0 +1,33 @@ +defmodule Supavisor.Tenants.Cluster do + use Ecto.Schema + import Ecto.Changeset + alias Supavisor.Tenants.ClusterTenants + + @type t :: %__MODULE__{} + + @primary_key {:id, :binary_id, autogenerate: true} + @schema_prefix "_supavisor" + + schema "clusters" do + field(:active, :boolean, default: false) + field(:alias, :string) + + has_many(:cluster_tenants, ClusterTenants, + foreign_key: :cluster_alias, + references: :alias, + on_delete: :delete_all, + on_replace: :delete + ) + + timestamps() + end + + @doc false + def changeset(cluster, attrs) do + cluster + |> cast(attrs, [:active, :alias]) + |> validate_required([:active, :alias]) + |> unique_constraint([:alias]) + |> cast_assoc(:cluster_tenants, with: &ClusterTenants.changeset/2) + end +end diff --git a/lib/supavisor/tenants/cluster_tenants.ex b/lib/supavisor/tenants/cluster_tenants.ex new file mode 100644 index 00000000..10c57759 --- /dev/null +++ b/lib/supavisor/tenants/cluster_tenants.ex @@ -0,0 +1,33 @@ +defmodule Supavisor.Tenants.ClusterTenants do + use Ecto.Schema + import Ecto.Changeset + alias Supavisor.Tenants.Tenant + alias Supavisor.Tenants.Cluster + + @type t :: %__MODULE__{} + + @primary_key {:id, :binary_id, autogenerate: true} + @schema_prefix "_supavisor" + + schema "cluster_tenants" do + field(:type, Ecto.Enum, values: [:write, :read]) + field(:active, :boolean, default: false) + belongs_to(:cluster, Cluster, foreign_key: :cluster_alias, type: :string) + + belongs_to(:tenant, Tenant, + type: :string, + foreign_key: :tenant_external_id, + references: :external_id + ) + + timestamps() + end + + @doc false + def changeset(cluster, attrs) do + cluster + |> cast(attrs, [:type, :active, :cluster_alias, :tenant_external_id]) + |> validate_required([:type, :active, :cluster_alias, :tenant_external_id]) + |> unique_constraint([:tenant_external_id]) + end +end diff --git a/lib/supavisor/tenants_metrics.ex b/lib/supavisor/tenants_metrics.ex index 5a90964b..5171c5d4 100644 --- a/lib/supavisor/tenants_metrics.ex +++ b/lib/supavisor/tenants_metrics.ex @@ -26,7 +26,7 @@ defmodule Supavisor.TenantsMetrics do active_pools = PromEx.do_cache_tenants_metrics() |> MapSet.new() MapSet.difference(state.pools, active_pools) - |> Enum.each(fn {tenant, _, _} = pool -> + |> Enum.each(fn {{_type, tenant}, _, _} = pool -> Logger.debug("Removing cached metrics for #{inspect(pool)}") Cachex.del(Supavisor.Cache, {:metrics, tenant}) end) diff --git a/lib/supavisor_web/controllers/changeset_json.ex b/lib/supavisor_web/controllers/changeset_json.ex new file mode 100644 index 00000000..7fcf9764 --- /dev/null +++ b/lib/supavisor_web/controllers/changeset_json.ex @@ -0,0 +1,25 @@ +defmodule SupavisorWeb.ChangesetJSON do + @doc """ + Renders changeset errors. + """ + def error(%{changeset: changeset}) do + # When encoded, the changeset returns its errors + # as a JSON object. So we just pass it forward. + %{errors: Ecto.Changeset.traverse_errors(changeset, &translate_error/1)} + end + + defp translate_error({msg, opts}) do + # You can make use of gettext to translate error messages by + # uncommenting and adjusting the following code: + + # if count = opts[:count] do + # Gettext.dngettext(SupavisorWeb.Gettext, "errors", msg, msg, count, opts) + # else + # Gettext.dgettext(SupavisorWeb.Gettext, "errors", msg, opts) + # end + + Enum.reduce(opts, msg, fn {key, value}, acc -> + String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end) + end) + end +end diff --git a/lib/supavisor_web/controllers/cluster_controller.ex b/lib/supavisor_web/controllers/cluster_controller.ex new file mode 100644 index 00000000..de93e1e3 --- /dev/null +++ b/lib/supavisor_web/controllers/cluster_controller.ex @@ -0,0 +1,69 @@ +defmodule SupavisorWeb.ClusterController do + use SupavisorWeb, :controller + + require Logger + + alias Supavisor.Repo + alias Supavisor.Tenants + alias Supavisor.Tenants.Cluster, as: ClusterModel + + action_fallback(SupavisorWeb.FallbackController) + + # def index(conn, _params) do + # clusters = Tenants.list_clusters() + # render(conn, :index, clusters: clusters) + # end + + def create(conn, %{"cluster" => params}) do + with {:ok, %ClusterModel{} = cluster} <- Tenants.create_cluster(params) do + conn + |> put_status(:created) + |> put_resp_header("location", Routes.tenant_path(conn, :show, cluster)) + |> render(:show, cluster: cluster) + end + end + + def show(conn, %{"alias" => id}) do + id + |> Tenants.get_cluster_by_alias() + |> case do + %ClusterModel{} = cluster -> + render(conn, "show.json", cluster: cluster) + + nil -> + conn + |> put_status(404) + |> render("not_found.json", cluster: nil) + end + end + + def update(conn, %{"alias" => id, "cluster" => params}) do + cluster_tenants = + Enum.map(params["cluster_tenants"], fn e -> + Map.put(e, "cluster_alias", id) + end) + + params = %{params | "cluster_tenants" => cluster_tenants} + + case Tenants.get_cluster_by_alias(id) do + nil -> + create(conn, %{"cluster" => Map.put(params, "alias", id)}) + + cluster -> + cluster = Repo.preload(cluster, :cluster_tenants) + + with {:ok, %ClusterModel{} = cluster} <- + Tenants.update_cluster(cluster, params) do + result = Supavisor.terminate_global("cluster.#{cluster.alias}") + Logger.warning("Stop #{cluster.alias}: #{inspect(result)}") + render(conn, "show.json", cluster: cluster) + end + end + end + + def delete(conn, %{"alias" => id}) do + code = if Tenants.delete_cluster_by_alias(id), do: 204, else: 404 + + send_resp(conn, code, "") + end +end diff --git a/lib/supavisor_web/controllers/cluster_json.ex b/lib/supavisor_web/controllers/cluster_json.ex new file mode 100644 index 00000000..9a64396e --- /dev/null +++ b/lib/supavisor_web/controllers/cluster_json.ex @@ -0,0 +1,24 @@ +defmodule SupavisorWeb.ClusterJSON do + alias Supavisor.Tenants.Cluster + + @doc """ + Renders a list of clusters. + """ + def index(%{clusters: clusters}) do + %{data: for(cluster <- clusters, do: data(cluster))} + end + + @doc """ + Renders a single cluster. + """ + def show(%{cluster: cluster}) do + %{data: data(cluster)} + end + + defp data(%Cluster{} = cluster) do + %{ + id: cluster.id, + active: cluster.active + } + end +end diff --git a/lib/supavisor_web/controllers/cluster_tenants_controller.ex b/lib/supavisor_web/controllers/cluster_tenants_controller.ex new file mode 100644 index 00000000..1e21b491 --- /dev/null +++ b/lib/supavisor_web/controllers/cluster_tenants_controller.ex @@ -0,0 +1,45 @@ +defmodule SupavisorWeb.ClusterTenantsController do + use SupavisorWeb, :controller + + alias Supavisor.Tenants + alias Supavisor.Tenants.ClusterTenants + + action_fallback(SupavisorWeb.FallbackController) + + def index(conn, _params) do + cluster_tenants = Tenants.list_cluster_tenants() + render(conn, :index, cluster_tenants: cluster_tenants) + end + + def create(conn, %{"cluster_tenants" => cluster_tenants_params}) do + with {:ok, %ClusterTenants{} = cluster_tenants} <- + Tenants.create_cluster_tenants(cluster_tenants_params) do + conn + |> put_status(:created) + # |> put_resp_header("location", ~p"/api/cluster_tenants/#{cluster_tenants}") + |> render(:show, cluster_tenants: cluster_tenants) + end + end + + def show(conn, %{"id" => id}) do + cluster_tenants = Tenants.get_cluster_tenants!(id) + render(conn, :show, cluster_tenants: cluster_tenants) + end + + def update(conn, %{"id" => id, "cluster_tenants" => cluster_tenants_params}) do + cluster_tenants = Tenants.get_cluster_tenants!(id) + + with {:ok, %ClusterTenants{} = cluster_tenants} <- + Tenants.update_cluster_tenants(cluster_tenants, cluster_tenants_params) do + render(conn, :show, cluster_tenants: cluster_tenants) + end + end + + def delete(conn, %{"id" => id}) do + cluster_tenants = Tenants.get_cluster_tenants!(id) + + with {:ok, %ClusterTenants{}} <- Tenants.delete_cluster_tenants(cluster_tenants) do + send_resp(conn, :no_content, "") + end + end +end diff --git a/lib/supavisor_web/controllers/cluster_tenants_json.ex b/lib/supavisor_web/controllers/cluster_tenants_json.ex new file mode 100644 index 00000000..85d67b20 --- /dev/null +++ b/lib/supavisor_web/controllers/cluster_tenants_json.ex @@ -0,0 +1,24 @@ +defmodule SupavisorWeb.ClusterTenantsJSON do + alias Supavisor.Tenants.ClusterTenants + + @doc """ + Renders a list of cluster_tenants. + """ + def index(%{cluster_tenants: cluster_tenants}) do + %{data: for(cluster_tenants <- cluster_tenants, do: data(cluster_tenants))} + end + + @doc """ + Renders a single cluster_tenants. + """ + def show(%{cluster_tenants: cluster_tenants}) do + %{data: data(cluster_tenants)} + end + + defp data(%ClusterTenants{} = cluster_tenants) do + %{ + id: cluster_tenants.id, + active: cluster_tenants.active + } + end +end diff --git a/lib/supavisor_web/router.ex b/lib/supavisor_web/router.ex index df8eded4..50608dfe 100644 --- a/lib/supavisor_web/router.ex +++ b/lib/supavisor_web/router.ex @@ -46,6 +46,11 @@ defmodule SupavisorWeb.Router do delete("/tenants/:external_id", TenantController, :delete) get("/tenants/:external_id/terminate", TenantController, :terminate) get("/health", TenantController, :health) + + get("/clusters/:alias", ClusterController, :show) + put("/clusters/:alias", ClusterController, :update) + delete("/clusters/:alias", ClusterController, :delete) + # get("/clusters/:alias/terminate", ClusterController, :terminate) end scope "/metrics", SupavisorWeb do diff --git a/lib/supavisor_web/views/cluster_tenants_view.ex b/lib/supavisor_web/views/cluster_tenants_view.ex new file mode 100644 index 00000000..5f1a0771 --- /dev/null +++ b/lib/supavisor_web/views/cluster_tenants_view.ex @@ -0,0 +1,31 @@ +defmodule SupavisorWeb.ClusterTenantsView do + use SupavisorWeb, :view + alias SupavisorWeb.ClusterTenantsView + + def render("index.json", %{cluster_tenants: cluster_tenants}) do + %{data: render_many(cluster_tenants, ClusterTenantsView, "cluster_tenant.json")} + end + + def render("show.json", %{cluster_tenant: cluster_tenant}) do + %{data: render_one(cluster_tenant, ClusterTenantsView, "cluster_tenant.json")} + end + + def render("cluster_tenant.json", %{cluster_tenants: ct}) do + %{ + id: ct.id, + active: ct.active, + cluster_alias: ct.cluster_alias, + tenant_external_id: ct.tenant_external_id, + inserted_at: ct.inserted_at, + updated_at: ct.updated_at + } + end + + def render("error.json", %{error: reason}) do + %{error: reason} + end + + def render("show_terminate.json", %{result: result}) do + %{result: result} + end +end diff --git a/lib/supavisor_web/views/cluster_view.ex b/lib/supavisor_web/views/cluster_view.ex new file mode 100644 index 00000000..8478edd9 --- /dev/null +++ b/lib/supavisor_web/views/cluster_view.ex @@ -0,0 +1,33 @@ +defmodule SupavisorWeb.ClusterView do + use SupavisorWeb, :view + alias SupavisorWeb.ClusterView + alias SupavisorWeb.ClusterTenantsView + + def render("index.json", %{clusters: clusters}) do + %{data: render_many(clusters, ClusterView, "cluster.json")} + end + + def render("show.json", %{cluster: cluster}) do + %{data: render_one(cluster, ClusterView, "cluster.json")} + end + + def render("cluster.json", %{cluster: cluster}) do + %{ + id: cluster.id, + alias: cluster.alias, + active: cluster.active, + inserted_at: cluster.inserted_at, + updated_at: cluster.updated_at, + cluster_tenants: + render_many(cluster.cluster_tenants, ClusterTenantsView, "cluster_tenant.json") + } + end + + def render("error.json", %{error: reason}) do + %{error: reason} + end + + def render("show_terminate.json", %{result: result}) do + %{result: result} + end +end diff --git a/mix.exs b/mix.exs index 830b400f..f4c90f09 100644 --- a/mix.exs +++ b/mix.exs @@ -64,7 +64,8 @@ defmodule Supavisor.MixProject do # pooller {:poolboy, "~> 1.5.2"}, {:syn, "~> 3.3"}, - {:pgo, "~> 0.13"} + {:pgo, "~> 0.13"}, + {:rustler, "~> 0.29.1"} # TODO: add ranch deps ] end diff --git a/mix.lock b/mix.lock index ea923ee5..51bb54a4 100644 --- a/mix.lock +++ b/mix.lock @@ -4,16 +4,16 @@ "benchee": {:hex, :benchee, "1.1.0", "f3a43817209a92a1fade36ef36b86e1052627fd8934a8b937ac9ab3a76c43062", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}], "hexpm", "7da57d545003165a012b587077f6ba90b89210fd88074ce3c60ce239eb5e6d93"}, "bertex": {:hex, :bertex, "1.3.0", "0ad0df9159b5110d9d2b6654f72fbf42a54884ef43b6b651e6224c0af30ba3cb", [:mix], [], "hexpm", "0a5d5e478bb5764b7b7bae37cae1ca491200e58b089df121a2fe1c223d8ee57a"}, "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, - "burrito": {:git, "https://github.com/burrito-elixir/burrito.git", "fee74fe37fe8481f559300780ab61f7d3b6e9e03", []}, + "burrito": {:git, "https://github.com/burrito-elixir/burrito.git", "a60c6ab21156fc4c788907d33bfd0c546a022272", []}, "cachex": {:hex, :cachex, "3.6.0", "14a1bfbeee060dd9bec25a5b6f4e4691e3670ebda28c8ba2884b12fe30b36bf8", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "ebf24e373883bc8e0c8d894a63bbe102ae13d918f790121f5cfe6e485cc8e2e2"}, - "castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"}, + "castore": {:hex, :castore, "1.0.4", "ff4d0fb2e6411c0479b1d965a814ea6d00e51eb2f58697446e9c41a97d940b28", [:mix], [], "hexpm", "9418c1b8144e11656f0be99943db4caf04612e3eaecefb5dae9a2a87565584f8"}, "cloak": {:hex, :cloak, "1.1.2", "7e0006c2b0b98d976d4f559080fabefd81f0e0a50a3c4b621f85ceeb563e80bb", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "940d5ac4fcd51b252930fd112e319ea5ae6ab540b722f3ca60a85666759b9585"}, "cloak_ecto": {:hex, :cloak_ecto, "1.2.0", "e86a3df3bf0dc8980f70406bcb0af2858bac247d55494d40bc58a152590bd402", [:mix], [{:cloak, "~> 1.1.1", [hex: :cloak, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "8bcc677185c813fe64b786618bd6689b1707b35cd95acaae0834557b15a0c62f"}, "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, "credo": {:hex, :credo, "1.6.7", "323f5734350fd23a456f2688b9430e7d517afb313fbd38671b8a4449798a7854", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "41e110bfb007f7eda7f897c10bf019ceab9a0b269ce79f015d54b0dcf4fc7dd3"}, - "db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"}, + "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, @@ -44,23 +44,24 @@ "opentelemetry_semantic_conventions": {:hex, :opentelemetry_semantic_conventions, "0.2.0", "b67fe459c2938fcab341cb0951c44860c62347c005ace1b50f8402576f241435", [:mix, :rebar3], [], "hexpm", "d61fa1f5639ee8668d74b527e6806e0503efc55a42db7b5f39939d84c07d6895"}, "pg_types": {:hex, :pg_types, "0.4.0", "3ce365c92903c5bb59c0d56382d842c8c610c1b6f165e20c4b652c96fa7e9c14", [:rebar3], [], "hexpm", "b02efa785caececf9702c681c80a9ca12a39f9161a846ce17b01fb20aeeed7eb"}, "pgo": {:hex, :pgo, "0.14.0", "f53711d103d7565db6fc6061fcf4ff1007ab39892439be1bb02d9f686d7e6663", [:rebar3], [{:backoff, "~> 1.1.6", [hex: :backoff, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:pg_types, "~> 0.4.0", [hex: :pg_types, repo: "hexpm", optional: false]}], "hexpm", "71016c22599936e042dc0012ee4589d24c71427d266292f775ebf201d97df9c9"}, - "phoenix": {:hex, :phoenix, "1.7.7", "4cc501d4d823015007ba3cdd9c41ecaaf2ffb619d6fb283199fa8ddba89191e0", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "8966e15c395e5e37591b6ed0bd2ae7f48e961f0f60ac4c733f9566b519453085"}, - "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.2", "b21bd01fdeffcfe2fab49e4942aa938b6d3e89e93a480d4aee58085560a0bc0d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "70242edd4601d50b69273b057ecf7b684644c19ee750989fd555625ae4ce8f5d"}, - "phoenix_html": {:hex, :phoenix_html, "3.3.2", "d6ce982c6d8247d2fc0defe625255c721fb8d5f1942c5ac051f6177bffa5973f", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "44adaf8e667c1c20fb9d284b6b0fa8dc7946ce29e81ce621860aa7e96de9a11d"}, + "phoenix": {:hex, :phoenix, "1.7.10", "02189140a61b2ce85bb633a9b6fd02dff705a5f1596869547aeb2b2b95edd729", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "cf784932e010fd736d656d7fead6a584a4498efefe5b8227e9f383bf15bb79d0"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.3", "86e9878f833829c3f66da03d75254c155d91d72a201eb56ae83482328dc7ca93", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"}, + "phoenix_html": {:hex, :phoenix_html, "3.3.3", "380b8fb45912b5638d2f1d925a3771b4516b9a78587249cabe394e0a5d579dc9", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "923ebe6fec6e2e3b3e569dfbdc6560de932cd54b000ada0208b5f45024bdd76c"}, "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"}, "phoenix_live_view": {:hex, :phoenix_live_view, "0.18.18", "1f38fbd7c363723f19aad1a04b5490ff3a178e37daaf6999594d5f34796c47fc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a5810d0472f3189ede6d2a95bda7f31c6113156b91784a3426cb0ab6a6d85214"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "phoenix_template": {:hex, :phoenix_template, "1.0.3", "32de561eefcefa951aead30a1f94f1b5f0379bc9e340bb5c667f65f1edfa4326", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "16f4b6588a4152f3cc057b9d0c0ba7e82ee23afa65543da535313ad8d25d8e2c"}, - "phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"}, - "plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"}, + "phoenix_view": {:hex, :phoenix_view, "2.0.3", "4d32c4817fce933693741deeb99ef1392619f942633dde834a5163124813aad3", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "cd34049af41be2c627df99cd4eaa71fc52a328c0c3d8e7d4aa28f880c30e7f64"}, + "plug": {:hex, :plug, "1.15.1", "b7efd81c1a1286f13efb3f769de343236bd8b7d23b4a9f40d3002fc39ad8f74c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "459497bd94d041d98d948054ec6c0b76feacd28eec38b219ca04c0de13c79d30"}, "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"}, - "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, + "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, "postgrex": {:hex, :postgrex, "0.17.3", "c92cda8de2033a7585dae8c61b1d420a1a1322421df84da9a82a6764580c503d", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "946cf46935a4fdca7a81448be76ba3503cff082df42c6ec1ff16a4bdfbfb098d"}, "prom_ex": {:hex, :prom_ex, "1.8.0", "662615e1d2f2ab3e0dc13a51c92ad0ccfcab24336a90cb9b114ee1bce9ef88aa", [:mix], [{:absinthe, ">= 1.6.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:broadway, ">= 1.0.2", [hex: :broadway, repo: "hexpm", optional: true]}, {:ecto, ">= 3.5.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, ">= 2.4.0", [hex: :oban, repo: "hexpm", optional: true]}, {:octo_fetch, "~> 0.3", [hex: :octo_fetch, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.5.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, ">= 0.14.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.12.1", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.5", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry, ">= 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.0", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "3eea763dfa941e25de50decbf17a6a94dbd2270e7b32f88279aa6e9bbb8e23e7"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, - "req": {:hex, :req, "0.3.11", "462315e50db6c6e1f61c45e8c0b267b0d22b6bd1f28444c136908dfdca8d515a", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.9", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0e4b331627fedcf90b29aa8064cd5a95619ef6134d5ab13919b6e1c4d7cccd4b"}, + "req": {:hex, :req, "0.3.12", "f84c2f9e7cc71c81d7cbeacf7c61e763e53ab5f3065703792a4ab264b4f22672", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.9", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "c91103d4d1c8edeba90c84e0ba223a59865b673eaab217bfd17da3aa54ab136c"}, + "rustler": {:hex, :rustler, "0.29.1", "880f20ae3027bd7945def6cea767f5257bc926f33ff50c0d5d5a5315883c084d", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "109497d701861bfcd26eb8f5801fe327a8eef304f56a5b63ef61151ff44ac9b6"}, "sleeplocks": {:hex, :sleeplocks, "1.1.2", "d45aa1c5513da48c888715e3381211c859af34bee9b8290490e10c90bb6ff0ca", [:rebar3], [], "hexpm", "9fe5d048c5b781d6305c1a3a0f40bb3dfc06f49bf40571f3d2d0c57eaa7f59a5"}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, "syn": {:hex, :syn, "3.3.0", "4684a909efdfea35ce75a9662fc523e4a8a4e8169a3df275e4de4fa63f99c486", [:rebar3], [], "hexpm", "e58ee447bc1094bdd21bf0acc102b1fbf99541a508cd48060bf783c245eaf7d6"}, @@ -68,9 +69,10 @@ "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, "telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.1.0", "4e15f6d7dbedb3a4e3aed2262b7e1407f166fcb9c30ca3f96635dfbbef99965c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0dd10e7fe8070095df063798f82709b0a1224c31b8baf6278b423898d591a069"}, "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, - "tesla": {:hex, :tesla, "1.7.0", "a62dda2f80d4f8a925eb7b8c5b78c461e0eb996672719fe1a63b26321a5f8b4e", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "2e64f01ebfdb026209b47bc651a0e65203fcff4ae79c11efb73c4852b00dc313"}, + "tesla": {:hex, :tesla, "1.8.0", "d511a4f5c5e42538d97eef7c40ec4f3e44effdc5068206f42ed859e09e51d1fd", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "10501f360cd926a309501287470372af1a6e1cbed0f43949203a4c13300bc79f"}, + "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, "typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"}, "unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, - "websock_adapter": {:hex, :websock_adapter, "0.5.4", "7af8408e7ed9d56578539594d1ee7d8461e2dd5c3f57b0f2a5352d610ddde757", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d2c238c79c52cbe223fcdae22ca0bb5007a735b9e933870e241fce66afb4f4ab"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.5", "9dfeee8269b27e958a65b3e235b7e447769f66b5b5925385f5a569269164a210", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "4b977ba4a01918acbf77045ff88de7f6972c2a009213c515a445c48f224ffce9"}, } diff --git a/native/pgparser/.cargo/config.toml b/native/pgparser/.cargo/config.toml new file mode 100644 index 00000000..20f03f3d --- /dev/null +++ b/native/pgparser/.cargo/config.toml @@ -0,0 +1,5 @@ +[target.'cfg(target_os = "macos")'] +rustflags = [ + "-C", "link-arg=-undefined", + "-C", "link-arg=dynamic_lookup", +] diff --git a/native/pgparser/.gitignore b/native/pgparser/.gitignore new file mode 100644 index 00000000..ea8c4bf7 --- /dev/null +++ b/native/pgparser/.gitignore @@ -0,0 +1 @@ +/target diff --git a/native/pgparser/Cargo.lock b/native/pgparser/Cargo.lock new file mode 100644 index 00000000..bf7e070c --- /dev/null +++ b/native/pgparser/Cargo.lock @@ -0,0 +1,813 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bindgen" +version = "0.60.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "062dddbc1ba4aca46de6338e2bf87771414c335f7b2f2036e8f3e9befebf88e6" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "clap", + "env_logger", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "which", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clang-sys" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "atty", + "bitflags 1.3.2", + "clap_lex", + "indexmap 1.9.3", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "env_logger" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fastrand" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "os_str_bytes" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "petgraph" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +dependencies = [ + "fixedbitset", + "indexmap 2.0.0", +] + +[[package]] +name = "pg_query" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85d1f7024e222610ae77191d1fb53131c739af47e8905b1f2feedb1b6cd6699d" +dependencies = [ + "bindgen", + "fs_extra", + "itertools", + "prost", + "prost-build", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "pgparser" +version = "0.1.0" +dependencies = [ + "pg_query", + "rustler", +] + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71adf41db68aa0daaefc69bb30bcd68ded9b9abaad5d1fbb6304c4fb390e083e" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae5a4388762d5815a9fc0dea33c56b021cdc8dde0c55e0c9ca57197254b0cab" +dependencies = [ + "bytes", + "cfg-if", + "cmake", + "heck", + "itertools", + "lazy_static", + "log", + "multimap", + "petgraph", + "prost", + "prost-types", + "regex", + "tempfile", + "which", +] + +[[package]] +name = "prost-derive" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b670f45da57fb8542ebdbb6105a925fe571b67f9e7ed9f47a06a84e72b4e7cc" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "prost-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d0a014229361011dc8e69c8a1ec6c2e8d0f2af7c91e3ea3f5b2170298461e68" +dependencies = [ + "bytes", + "prost", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed6248e1caa625eb708e266e06159f135e8c26f2bb7ceb72dc4b2766d0340964" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustler" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0884cb623b9f43d3e2c51f9071c5e96a5acf3e6e6007866812884ff0cb983f1e" +dependencies = [ + "lazy_static", + "rustler_codegen", + "rustler_sys", +] + +[[package]] +name = "rustler_codegen" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e277af754f2560cf4c4ebedb68c1a735292fb354505c6133e47ec406e699cf" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.29", +] + +[[package]] +name = "rustler_sys" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7c0740e5322b64e2b952d8f0edce5f90fcf6f6fe74cca3f6e78eb3de5ea858" +dependencies = [ + "regex", + "unreachable", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "serde" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + +[[package]] +name = "serde_json" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + +[[package]] +name = "thiserror" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "unreachable" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" +dependencies = [ + "void", +] + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/native/pgparser/Cargo.toml b/native/pgparser/Cargo.toml new file mode 100644 index 00000000..a4f11746 --- /dev/null +++ b/native/pgparser/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "pgparser" +version = "0.1.0" +edition = "2021" + +[lib] +name = "pgparser" +path = "src/lib.rs" +crate-type = ["cdylib"] + +[dependencies] +rustler = "0.29.1" +pg_query = "0.8.1" diff --git a/native/pgparser/README.md b/native/pgparser/README.md new file mode 100644 index 00000000..81b34811 --- /dev/null +++ b/native/pgparser/README.md @@ -0,0 +1,24 @@ +## Benchmarks + +``` +Operating System: macOS +CPU Information: Apple M1 Pro +Number of Available Cores: 10 +Available memory: 16 GB +Elixir 1.14.3 +Erlang 24.3.4 + +Benchmark suite executing with the following configuration: +warmup: 2 s +time: 5 s +memory time: 0 ns +reduction time: 0 ns +parallel: 1 +inputs: none specified +Estimated total run time: 7 s + +Benchmarking statement_types/1 ... + +Name ips average deviation median 99th % +statement_types/1 171.60 K 5.83 μs ±91.30% 5.71 μs 6.29 μs +``` \ No newline at end of file diff --git a/native/pgparser/src/lib.rs b/native/pgparser/src/lib.rs new file mode 100644 index 00000000..83f2d795 --- /dev/null +++ b/native/pgparser/src/lib.rs @@ -0,0 +1,28 @@ +use rustler::{Atom, Error as RustlerError, NifTuple}; + +mod atoms { + rustler::atoms! { + ok, + error, + } +} + +#[derive(NifTuple)] +struct Response { + status: Atom, + message: Vec +} + +#[rustler::nif] +fn statement_types(query: &str) -> Result { + let result = pg_query::parse(&query); + + if let Ok(result) = result { + let message = result.statement_types().into_iter().map(|s| s.to_string()).collect(); + return Ok(Response{status: atoms::ok(), message}); + } else { + return Err(RustlerError::Term(Box::new("Error parsing query"))); + } +} + +rustler::init!("Elixir.Supavisor.PgParser", [statement_types]); diff --git a/priv/repo/migrations/20230919091334_create_clusters.exs b/priv/repo/migrations/20230919091334_create_clusters.exs new file mode 100644 index 00000000..cbd6f824 --- /dev/null +++ b/priv/repo/migrations/20230919091334_create_clusters.exs @@ -0,0 +1,15 @@ +defmodule Supavisor.Repo.Migrations.CreateClusters do + use Ecto.Migration + + def change do + create table("clusters", primary_key: false, prefix: "_supavisor") do + add(:id, :binary_id, primary_key: true) + add(:active, :boolean, default: false, null: false) + add(:alias, :string, null: false) + + timestamps() + end + + create(index(:clusters, [:alias], unique: true, prefix: "_supavisor")) + end +end diff --git a/priv/repo/migrations/20230919100141_create_cluster_tenants.exs b/priv/repo/migrations/20230919100141_create_cluster_tenants.exs new file mode 100644 index 00000000..8f0572d0 --- /dev/null +++ b/priv/repo/migrations/20230919100141_create_cluster_tenants.exs @@ -0,0 +1,38 @@ +defmodule Supavisor.Repo.Migrations.CreateClusterTenants do + use Ecto.Migration + + def change do + create table("cluster_tenants", primary_key: false, prefix: "_supavisor") do + add(:id, :binary_id, primary_key: true) + add(:type, :string, null: false) + add(:active, :boolean, default: false, null: false) + + add( + :cluster_alias, + references(:clusters, on_delete: :delete_all, type: :string, column: :alias) + ) + + add( + :tenant_external_id, + references(:tenants, type: :string, column: :external_id) + ) + + timestamps() + end + + create( + constraint( + :cluster_tenants, + :type, + check: "type IN ('read', 'write')" + ) + ) + + create( + index(:cluster_tenants, [:tenant_external_id], + unique: true, + prefix: "_supavisor" + ) + ) + end +end diff --git a/priv/repo/seeds_after_migration.exs b/priv/repo/seeds_after_migration.exs index 4069aad8..58b1ce42 100644 --- a/priv/repo/seeds_after_migration.exs +++ b/priv/repo/seeds_after_migration.exs @@ -30,7 +30,7 @@ end %{ "db_user" => db_conf[:username], "db_password" => db_conf[:password], - "pool_size" => 3, + "pool_size" => 9, "mode_type" => "transaction" }, %{ diff --git a/test/integration/proxy_test.exs b/test/integration/proxy_test.exs index 98dd98e6..43d716b8 100644 --- a/test/integration/proxy_test.exs +++ b/test/integration/proxy_test.exs @@ -28,6 +28,26 @@ defmodule Supavisor.Integration.ProxyTest do %{proxy: proxy, origin: origin, user: db_conf[:username]} end + test "prepared statement", %{proxy: proxy} do + db_conf = Application.get_env(:supavisor, Repo) + + url = """ + postgresql://#{db_conf[:username] <> "." <> @tenant}:#{db_conf[:password]}\ + @#{db_conf[:hostname]}:#{Application.get_env(:supavisor, :proxy_port_transaction)}/postgres + """ + + prepare_sql = + "PREPARE tenant (text) AS SELECT id, external_id FROM _supavisor.tenants WHERE external_id = $1;" + + {result, _} = System.cmd("psql", [url, "-c", prepare_sql], stderr_to_stdout: true) + assert result =~ "PREPARE" + + {result, _} = + System.cmd("psql", [url, "-c", "EXECUTE tenant('#{@tenant}');"], stderr_to_stdout: true) + + assert result =~ "#{@tenant}\n(1 row)" + end + test "the wrong password" do db_conf = Application.get_env(:supavisor, Repo) @@ -157,7 +177,7 @@ defmodule Supavisor.Integration.ProxyTest do :timer.sleep(500) [{_, client_pid, _}] = - Supavisor.get_local_manager({"proxy_tenant", "transaction", :transaction}) + Supavisor.get_local_manager({{:single, "proxy_tenant"}, "transaction", :transaction}) |> :sys.get_state() |> then(& &1[:tid]) |> :ets.tab2list() diff --git a/test/supavisor/client_handler_test.exs b/test/supavisor/client_handler_test.exs index 206f3ed9..37c2ff4c 100644 --- a/test/supavisor/client_handler_test.exs +++ b/test/supavisor/client_handler_test.exs @@ -6,7 +6,7 @@ defmodule Supavisor.ClientHandlerTest do describe "parse_user_info/1" do test "extracts the external_id from the username" do payload = %{"user" => "test.user.external_id"} - {name, external_id} = HH.parse_user_info(payload) + {:single, {name, external_id}} = HH.parse_user_info(payload) assert name == "test.user" assert external_id == "external_id" end @@ -14,22 +14,28 @@ defmodule Supavisor.ClientHandlerTest do test "username consists only of username" do username = "username" payload = %{"user" => username} - {user, nil} = HH.parse_user_info(payload) + {:single, {user, nil}} = HH.parse_user_info(payload) assert username == user end + test "consist cluster" do + username = "some.user.cluster.alias" + {t, {u, a}} = HH.parse_user_info(%{"user" => username}) + assert {t, {u, a}} == {:cluster, {"some.user", "alias"}} + end + test "external_id in options" do user = "test.user" external_id = "external_id" payload = %{"options" => %{"reference" => external_id}, "user" => user} - {user1, external_id1} = HH.parse_user_info(payload) + {:single, {user1, external_id1}} = HH.parse_user_info(payload) assert user1 == user assert external_id1 == external_id end test "unicode in username" do payload = %{"user" => "тестовe.імʼя.external_id"} - {name, external_id} = HH.parse_user_info(payload) + {:single, {name, external_id}} = HH.parse_user_info(payload) assert name == "тестовe.імʼя" assert external_id == "external_id" end diff --git a/test/supavisor/db_handler_test.exs b/test/supavisor/db_handler_test.exs index 4b2ed693..12819cca 100644 --- a/test/supavisor/db_handler_test.exs +++ b/test/supavisor/db_handler_test.exs @@ -11,7 +11,8 @@ defmodule Supavisor.DbHandlerTest do tenant: "test_tenant", user_alias: "test_user_alias", user: "user", - mode: :transaction + mode: :transaction, + replica_type: :single } {:ok, :connect, data, {_, next_event, _}} = Db.init(args) diff --git a/test/supavisor/pg_parser_test.exs b/test/supavisor/pg_parser_test.exs new file mode 100644 index 00000000..9209cb8d --- /dev/null +++ b/test/supavisor/pg_parser_test.exs @@ -0,0 +1,4 @@ +defmodule Supavisor.PgParserTest do + use ExUnit.Case, async: true + doctest Supavisor.PgParser +end diff --git a/test/supavisor/prom_ex_test.exs b/test/supavisor/prom_ex_test.exs index 83e14d85..e06382b6 100644 --- a/test/supavisor/prom_ex_test.exs +++ b/test/supavisor/prom_ex_test.exs @@ -28,9 +28,9 @@ defmodule Supavisor.PromExTest do Process.sleep(500) metrics = PromEx.get_metrics() assert metrics =~ "tenant=\"#{@tenant}\"" - DynamicSupervisor.stop(proxy, user) + GenServer.stop(proxy) Process.sleep(500) - Supavisor.stop({@tenant, user, :transaction}) + Supavisor.stop({{:single, @tenant}, "postgres", :transaction}) Process.sleep(500) refute PromEx.get_metrics() =~ "tenant=\"#{@tenant}\"" end diff --git a/test/supavisor/syn_handler_test.exs b/test/supavisor/syn_handler_test.exs index b6b26d47..bfbad4ae 100644 --- a/test/supavisor/syn_handler_test.exs +++ b/test/supavisor/syn_handler_test.exs @@ -4,7 +4,7 @@ defmodule Supavisor.SynHandlerTest do require Logger alias Ecto.Adapters.SQL.Sandbox - @id {"syn_tenant", "postgres", :session} + @id {{:single, "syn_tenant"}, "postgres", :session} test "resolving conflict" do node2 = :"secondary@127.0.0.1" diff --git a/test/supavisor/tenants_test.exs b/test/supavisor/tenants_test.exs index b5e0d395..542e18b3 100644 --- a/test/supavisor/tenants_test.exs +++ b/test/supavisor/tenants_test.exs @@ -70,10 +70,64 @@ defmodule Supavisor.TenantsTest do assert %Ecto.Changeset{} = Tenants.change_tenant(tenant) end - test "get_user/3" do + test "get_user/4" do _tenant = tenant_fixture() - assert {:error, :not_found} = Tenants.get_user("no_user", "no_tenant", "") - assert {:ok, %{tenant: _, user: _}} = Tenants.get_user("postgres", "dev_tenant", "") + assert {:error, :not_found} = Tenants.get_user(:single, "no_user", "no_tenant", "") + + assert {:ok, %{tenant: _, user: _}} = + Tenants.get_user(:single, "postgres", "dev_tenant", "") + end + end + + describe "clusters" do + alias Supavisor.Tenants.Cluster + + import Supavisor.TenantsFixtures + + @invalid_attrs %{active: nil, alias: nil} + @valid_attrs %{active: true, alias: "some_alias"} + + test "list_clusters/0 returns all clusters" do + cluster = cluster_fixture() + assert Tenants.list_clusters() |> Repo.preload(:cluster_tenants) == [cluster] + end + + test "get_cluster!/1 returns the cluster with given id" do + cluster = cluster_fixture() + assert Tenants.get_cluster!(cluster.id) |> Repo.preload(:cluster_tenants) == cluster + end + + test "create_cluster/1 with valid data creates a cluster" do + assert {:ok, %Cluster{} = cluster} = Tenants.create_cluster(@valid_attrs) + assert cluster.active == true + end + + test "create_cluster/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Tenants.create_cluster(@invalid_attrs) + end + + test "update_cluster/2 with valid data updates the cluster" do + cluster = cluster_fixture() + + assert {:ok, %Cluster{} = cluster} = Tenants.update_cluster(cluster, @valid_attrs) + assert cluster.active == true + end + + test "update_cluster/2 with invalid data returns error changeset" do + cluster = cluster_fixture() + assert {:error, %Ecto.Changeset{}} = Tenants.update_cluster(cluster, @invalid_attrs) + assert cluster == Tenants.get_cluster!(cluster.id) |> Repo.preload(:cluster_tenants) + end + + test "delete_cluster/1 deletes the cluster" do + cluster = cluster_fixture() + assert {:ok, %Cluster{}} = Tenants.delete_cluster(cluster) + assert_raise Ecto.NoResultsError, fn -> Tenants.get_cluster!(cluster.id) end + end + + test "change_cluster/1 returns a cluster changeset" do + cluster = cluster_fixture() + assert %Ecto.Changeset{} = Tenants.change_cluster(cluster) end end end diff --git a/test/support/fixtures/tenants_fixtures.ex b/test/support/fixtures/tenants_fixtures.ex index af4a5d73..66295fb9 100644 --- a/test/support/fixtures/tenants_fixtures.ex +++ b/test/support/fixtures/tenants_fixtures.ex @@ -30,4 +30,38 @@ defmodule Supavisor.TenantsFixtures do tenant end + + # @doc """ + # Generate a unique cluster tenant_external_id. + # """ + # def unique_cluster_tenant_external_id, + # do: "some tenant_external_id#{System.unique_integer([:positive])}" + + @doc """ + Generate a unique cluster type. + """ + def unique_cluster_type, do: "some type#{System.unique_integer([:positive])}" + + @doc """ + Generate a cluster. + """ + def cluster_fixture(attrs \\ %{}) do + {:ok, cluster} = + attrs + |> Enum.into(%{ + active: true, + alias: "some_alias", + cluster_tenants: [ + %{ + type: "write", + cluster_alias: "some_alias", + tenant_external_id: "proxy_tenant", + active: true + } + ] + }) + |> Supavisor.Tenants.create_cluster() + + cluster + end end