diff --git a/config/runtime.exs b/config/runtime.exs index 3d7b83c6..d2731fd0 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -154,7 +154,9 @@ if config_env() != :test do prom_poll_rate: System.get_env("PROM_POLL_RATE", "15000") |> String.to_integer(), global_upstream_ca: upstream_ca, global_downstream_cert: downstream_cert, - global_downstream_key: downstream_key + global_downstream_key: downstream_key, + cache_publ_name: System.get_env("CACHE_PUBL_NAME", "supavisor_cache"), + cache_repl_slot: System.get_env("CACHE_REPL_SLOT", "supavisor_cache") config :supavisor, Supavisor.Repo, url: System.get_env("DATABASE_URL", "ecto://postgres:postgres@localhost:6432/postgres"), diff --git a/lib/supavisor.ex b/lib/supavisor.ex index bdabc020..189a9fac 100644 --- a/lib/supavisor.ex +++ b/lib/supavisor.ex @@ -134,7 +134,7 @@ defmodule Supavisor do {method, secrets} = auth_secrets Logger.debug("Starting pool for #{inspect({tenant, user_alias, method})}") - case Tenants.get_pool_config(tenant, user_alias) do + case Tenants.get_pool_config(:cached, tenant, user_alias) do %Tenant{} = tenant_record -> %{ db_host: db_host, diff --git a/lib/supavisor/application.ex b/lib/supavisor/application.ex index 53627ea9..9c3a19a1 100644 --- a/lib/supavisor/application.ex +++ b/lib/supavisor/application.ex @@ -5,6 +5,8 @@ defmodule Supavisor.Application do use Application require Logger + import Cachex.Spec + alias Supavisor.Monitoring.PromEx @impl true @@ -72,8 +74,13 @@ defmodule Supavisor.Application do child_spec: DynamicSupervisor, strategy: :one_for_one, name: Supavisor.DynamicSupervisor }, Supavisor.Vault, - {Cachex, name: Supavisor.Cache}, + {Cachex, + [ + name: Supavisor.Cache, + limit: limit(size: 50_000, policy: Cachex.Policy.LRW, reclaim: 0.5) + ]}, Supavisor.TenantsMetrics, + Supavisor.CacheBuster, # Start the Endpoint (http/https) SupavisorWeb.Endpoint ] diff --git a/lib/supavisor/cache_buster.ex b/lib/supavisor/cache_buster.ex new file mode 100644 index 00000000..74cf7594 --- /dev/null +++ b/lib/supavisor/cache_buster.ex @@ -0,0 +1,166 @@ +defmodule Supavisor.CacheBuster do + @moduledoc false + + use Postgrex.ReplicationConnection + require Logger + alias Supavisor.Protocol.WalDecoder.Decoder + + alias Decoder.Messages.{ + Relation, + Update, + Delete + } + + def start_link(args) do + opts = + Application.get_env(:supavisor, Supavisor.Repo)[:url] + |> Ecto.Repo.Supervisor.parse_url() + + Postgrex.ReplicationConnection.start_link(__MODULE__, args, opts) + end + + @spec stop(pid) :: :ok + def stop(pid) do + GenServer.stop(pid) + end + + @impl true + def init(_args) do + slot_name = Application.get_env(:supavisor, :cache_repl_slot) + + state = %{ + publication: Application.get_env(:supavisor, :cache_publ_name), + slot: slot_name <> "#{:erlang.phash2(node())}", + rels: %{}, + step: nil + } + + {:ok, state} + end + + @impl true + def handle_connect(state) do + query = "CREATE_REPLICATION_SLOT #{state.slot} TEMPORARY LOGICAL pgoutput NOEXPORT_SNAPSHOT" + {:query, query, %{state | step: :create_slot}} + end + + @impl true + def handle_result(results, %{step: :create_slot} = state) + when is_list(results) do + Logger.info("Replication slot created: #{inspect(results)}") + + query = + "START_REPLICATION SLOT #{state.slot} LOGICAL 0/0 (proto_version '1', publication_names '#{state.publication}')" + + {:stream, query, [], %{state | step: :streaming}} + end + + def handle_result(results, state) do + Logger.error("Replication is failed: #{inspect(results)}") + {:noreply, state} + end + + @impl true + def handle_data(<>, state) do + new_state = + Decoder.decode_message(msg) + |> process_message(state) + + {:noreply, new_state} + end + + # keepalive + def handle_data(<>, state) do + {:noreply, [], state} + end + + def handle_data(<>, state) do + messages = [ + <> + ] + + {:noreply, messages, state} + end + + def handle_data(data, state) do + Logger.error("Unknown data: #{inspect(data)}") + {:noreply, state} + end + + defp process_message(%Relation{id: id, columns: columns, namespace: schema, name: table}, state) do + columns = + Enum.map(columns, fn %{name: name, type: type} -> + %{name: name, type: type} + end) + + put_in(state, [:rels, id], {columns, schema, table}) + end + + defp process_message(%Update{} = msg, state) do + drop_cache(msg, state.rels[msg.relation_id]) + state + end + + defp process_message(%Delete{} = msg, state) do + drop_cache(msg, state.rels[msg.relation_id]) + state + end + + defp process_message(_msg, state) do + state + end + + def drop_cache(msg, relation) do + Logger.debug("Got message: #{inspect(msg)}") + {columns, _schema, table} = relation + + record = data_tuple_to_map(columns, msg.old_tuple_data) + + tenant = + if table == "tenants" do + record["external_id"] + else + record["tenant_external_id"] + end + + Logger.warning("Got update for tenant: #{inspect(tenant)}") + + Supavisor.dirty_terminate(tenant) + end + + ## Internal functions + + defp data_tuple_to_map(column, tuple_data) do + column + |> Enum.with_index() + |> Enum.reduce_while(%{}, fn {column_map, index}, acc -> + case column_map do + %{name: column_name, type: column_type} + when is_binary(column_name) and is_binary(column_type) -> + try do + {:ok, elem(tuple_data, index)} + rescue + ArgumentError -> :error + end + |> case do + {:ok, record} -> + {:cont, + Map.put( + acc, + column_name, + record + )} + + :error -> + {:halt, acc} + end + + _ -> + {:cont, acc} + end + end) + end + + @epoch DateTime.to_unix(~U[2000-01-01 00:00:00Z], :microsecond) + defp current_time(), do: System.os_time(:microsecond) - @epoch +end diff --git a/lib/supavisor/client_handler.ex b/lib/supavisor/client_handler.ex index 97180cda..b7526c4c 100644 --- a/lib/supavisor/client_handler.ex +++ b/lib/supavisor/client_handler.ex @@ -112,7 +112,7 @@ defmodule Supavisor.ClientHandler do sni_hostname = try_get_sni(sock) - case Tenants.get_user(user, external_id, sni_hostname) do + case Tenants.get_user(:cached, user, external_id, sni_hostname) do {:ok, info} -> if info.tenant.enforce_ssl and !data.ssl do Logger.error("Tenant is not allowed to connect without SSL, user #{user}") @@ -522,16 +522,8 @@ defmodule Supavisor.ClientHandler do def auth_secrets(%{user: user, tenant: tenant} = info, db_user) do cache_key = {:secrets, tenant.external_id, user} - - case Cachex.fetch(Supavisor.Cache, cache_key, fn _key -> - {:commit, {:cached, get_secrets(info, db_user)}, ttl: 15_000} - end) do - {_, {:cached, value}} -> - value - - {_, {:cached, value}, _} -> - value - end + fun = fn -> get_secrets(info, db_user) end + H.cached(cache_key, fun, 60_000) end @spec get_secrets(map, String.t()) :: {:ok, {:auth_query, fun()}} | {:error, term()} diff --git a/lib/supavisor/helpers.ex b/lib/supavisor/helpers.ex index 05c1c21b..3282d645 100644 --- a/lib/supavisor/helpers.ex +++ b/lib/supavisor/helpers.ex @@ -1,5 +1,6 @@ defmodule Supavisor.Helpers do @moduledoc false + require Logger @spec check_creds_get_ver(map) :: {:ok, String.t()} | {:error, String.t()} @@ -303,4 +304,21 @@ defmodule Supavisor.Helpers do def parse_server_first(message, nonce) do :pgo_scram.parse_server_first(message, nonce) |> Map.new() end + + def cached(key, fun, ttl \\ 0) do + Logger.debug("Fetching #{inspect(key)} from cache") + + opts = if ttl > 0, do: [ttl: ttl], else: [] + + Cachex.fetch(Supavisor.Cache, key, fn _key -> + {:commit, {:cached, fun.()}, opts} + end) + |> case do + {_, {:cached, value}} -> + value + + {_, {:cached, value}, _} -> + value + end + end end diff --git a/lib/supavisor/protocol/wal_decoder/decoder.ex b/lib/supavisor/protocol/wal_decoder/decoder.ex new file mode 100644 index 00000000..f919a686 --- /dev/null +++ b/lib/supavisor/protocol/wal_decoder/decoder.ex @@ -0,0 +1,385 @@ +# This file draws heavily from https://github.com/cainophile/pgoutput_decoder +# License: https://github.com/cainophile/pgoutput_decoder/blob/master/LICENSE + +defmodule Supavisor.Protocol.WalDecoder.Decoder do + @moduledoc """ + Functions for decoding different types of logical replication messages. + """ + defmodule Messages do + @moduledoc """ + Different types of logical replication messages from Postgres + """ + defmodule Begin do + @moduledoc """ + Struct representing the BEGIN message in PostgreSQL's logical decoding output. + + * `final_lsn` - The LSN of the commit that this transaction ended at. + * `commit_timestamp` - The timestamp of the commit that this transaction ended at. + * `xid` - The transaction ID of this transaction. + """ + defstruct [:final_lsn, :commit_timestamp, :xid] + end + + defmodule Commit do + @moduledoc """ + Struct representing the COMMIT message in PostgreSQL's logical decoding output. + + * `flags` - Bitmask of flags associated with this commit. + * `lsn` - The LSN of the commit. + * `end_lsn` - The LSN of the next record in the WAL stream. + * `commit_timestamp` - The timestamp of the commit. + """ + defstruct [:flags, :lsn, :end_lsn, :commit_timestamp] + end + + defmodule Origin do + @moduledoc """ + Struct representing the ORIGIN message in PostgreSQL's logical decoding output. + + * `origin_commit_lsn` - The LSN of the commit in the database that the change originated from. + * `name` - The name of the origin. + """ + defstruct [:origin_commit_lsn, :name] + end + + defmodule Relation do + @moduledoc """ + Struct representing the RELATION message in PostgreSQL's logical decoding output. + + * `id` - The OID of the relation. + * `namespace` - The OID of the namespace that the relation belongs to. + * `name` - The name of the relation. + * `replica_identity` - The replica identity setting of the relation. + * `columns` - A list of columns in the relation. + """ + defstruct [:id, :namespace, :name, :replica_identity, :columns] + + defmodule Column do + @moduledoc """ + Struct representing a column in a relation. + + * `flags` - Bitmask of flags associated with this column. + * `name` - The name of the column. + * `type` - The OID of the data type of the column. + * `type_modifier` - The type modifier of the column. + """ + defstruct [:flags, :name, :type, :type_modifier] + end + end + + defmodule Insert do + @moduledoc """ + Struct representing the INSERT message in PostgreSQL's logical decoding output. + + * `relation_id` - The OID of the relation that the tuple was inserted into. + * `tuple_data` - The data of the inserted tuple. + """ + defstruct [:relation_id, :tuple_data] + end + + defmodule Update do + @moduledoc """ + Struct representing the UPDATE message in PostgreSQL's logical decoding output. + + * `relation_id` - The OID of the relation that the tuple was updated in. + * `changed_key_tuple_data` - The data of the tuple with the old key values. + * `old_tuple_data` - The data of the tuple before the update. + * `tuple_data` - The data of the tuple after the update. + """ + defstruct [:relation_id, :changed_key_tuple_data, :old_tuple_data, :tuple_data] + end + + defmodule Delete do + @moduledoc """ + Struct representing the DELETE message in PostgreSQL's logical decoding output. + + * `relation_id` - The OID of the relation that the tuple was deleted from. + * `changed_key_tuple_data` - The data of the tuple with the old key values. + * `old_tuple_data` - The data of the tuple before the delete. + """ + defstruct [:relation_id, :changed_key_tuple_data, :old_tuple_data] + end + + defmodule Truncate do + @moduledoc """ + Struct representing the TRUNCATE message in PostgreSQL's logical decoding output. + + * `number_of_relations` - The number of truncated relations. + * `options` - Additional options provided when truncating the relations. + * `truncated_relations` - List of relations that have been truncated. + """ + defstruct [:number_of_relations, :options, :truncated_relations] + end + + defmodule Type do + @moduledoc """ + Struct representing the TYPE message in PostgreSQL's logical decoding output. + + * `id` - The OID of the type. + * `namespace` - The namespace of the type. + * `name` - The name of the type. + """ + defstruct [:id, :namespace, :name] + end + + defmodule Unsupported do + @moduledoc """ + Struct representing an unsupported message in PostgreSQL's logical decoding output. + + * `data` - The raw data of the unsupported message. + """ + defstruct [:data] + end + + defmodule Relation.Column do + @moduledoc """ + Struct representing a column in a relation in PostgreSQL's logical decoding output. + + * `flags` - Column flags. + * `name` - The name of the column. + * `type` - The OID of the column type. + * `type_modifier` - The type modifier of the column. + """ + defstruct [:flags, :name, :type, :type_modifier] + end + end + + require Logger + + @pg_epoch DateTime.from_iso8601("2000-01-01T00:00:00Z") + + alias Messages.{ + Begin, + Commit, + Origin, + Relation, + Relation.Column, + Insert, + Update, + Delete, + Truncate, + Type, + Unsupported + } + + alias Supavisor.Protocol.WalDecoder.OidDatabase + + @doc """ + Parses logical replication messages from Postgres + + ## Examples + + iex> decode_message(<<73, 0, 0, 96, 0, 78, 0, 2, 116, 0, 0, 0, 3, 98, 97, 122, 116, 0, 0, 0, 3, 53, 54, 48>>) + %Supavisor.Protocol.WalDecoder.Decoder.Messages.Insert{relation_id: 24576, tuple_data: {"baz", "560"}} + + """ + def decode_message(message) when is_binary(message) do + # Logger.debug("Message before conversion " <> message) + decode_message_impl(message) + end + + defp decode_message_impl(<<"B", lsn::binary-8, timestamp::integer-64, xid::integer-32>>) do + %Begin{ + final_lsn: decode_lsn(lsn), + commit_timestamp: pgtimestamp_to_timestamp(timestamp), + xid: xid + } + end + + defp decode_message_impl( + <<"C", _flags::binary-1, lsn::binary-8, end_lsn::binary-8, timestamp::integer-64>> + ) do + %Commit{ + flags: [], + lsn: decode_lsn(lsn), + end_lsn: decode_lsn(end_lsn), + commit_timestamp: pgtimestamp_to_timestamp(timestamp) + } + end + + # TODO: Verify this is correct with real data from Postgres + defp decode_message_impl(<<"O", lsn::binary-8, name::binary>>) do + %Origin{ + origin_commit_lsn: decode_lsn(lsn), + name: name + } + end + + defp decode_message_impl(<<"R", id::integer-32, rest::binary>>) do + [ + namespace + | [name | [<>]] + ] = String.split(rest, <<0>>, parts: 3) + + # TODO: Handle case where pg_catalog is blank, we should still return the schema as pg_catalog + friendly_replica_identity = + case replica_identity do + "d" -> :default + "n" -> :nothing + "f" -> :all_columns + "i" -> :index + end + + %Relation{ + id: id, + namespace: namespace, + name: name, + replica_identity: friendly_replica_identity, + columns: decode_columns(columns) + } + end + + defp decode_message_impl( + <<"I", relation_id::integer-32, "N", number_of_columns::integer-16, tuple_data::binary>> + ) do + {<<>>, decoded_tuple_data} = decode_tuple_data(tuple_data, number_of_columns) + + %Insert{ + relation_id: relation_id, + tuple_data: decoded_tuple_data + } + end + + defp decode_message_impl( + <<"U", relation_id::integer-32, "N", number_of_columns::integer-16, tuple_data::binary>> + ) do + {<<>>, decoded_tuple_data} = decode_tuple_data(tuple_data, number_of_columns) + + %Update{ + relation_id: relation_id, + tuple_data: decoded_tuple_data + } + end + + defp decode_message_impl( + <<"U", relation_id::integer-32, key_or_old::binary-1, number_of_columns::integer-16, + tuple_data::binary>> + ) + when key_or_old == "O" or key_or_old == "K" do + {<<"N", new_number_of_columns::integer-16, new_tuple_binary::binary>>, old_decoded_tuple_data} = + decode_tuple_data(tuple_data, number_of_columns) + + {<<>>, decoded_tuple_data} = decode_tuple_data(new_tuple_binary, new_number_of_columns) + + base_update_msg = %Update{ + relation_id: relation_id, + tuple_data: decoded_tuple_data + } + + case key_or_old do + "K" -> Map.put(base_update_msg, :changed_key_tuple_data, old_decoded_tuple_data) + "O" -> Map.put(base_update_msg, :old_tuple_data, old_decoded_tuple_data) + end + end + + defp decode_message_impl( + <<"D", relation_id::integer-32, key_or_old::binary-1, number_of_columns::integer-16, + tuple_data::binary>> + ) + when key_or_old == "K" or key_or_old == "O" do + {<<>>, decoded_tuple_data} = decode_tuple_data(tuple_data, number_of_columns) + + base_delete_msg = %Delete{ + relation_id: relation_id + } + + case key_or_old do + "K" -> Map.put(base_delete_msg, :changed_key_tuple_data, decoded_tuple_data) + "O" -> Map.put(base_delete_msg, :old_tuple_data, decoded_tuple_data) + end + end + + defp decode_message_impl( + <<"T", number_of_relations::integer-32, options::integer-8, column_ids::binary>> + ) do + truncated_relations = + for relation_id_bin <- column_ids |> :binary.bin_to_list() |> Enum.chunk_every(4), + do: relation_id_bin |> :binary.list_to_bin() |> :binary.decode_unsigned() + + decoded_options = + case options do + 0 -> [] + 1 -> [:cascade] + 2 -> [:restart_identity] + 3 -> [:cascade, :restart_identity] + end + + %Truncate{ + number_of_relations: number_of_relations, + options: decoded_options, + truncated_relations: truncated_relations + } + end + + defp decode_message_impl(<<"Y", data_type_id::integer-32, namespace_and_name::binary>>) do + [namespace, name_with_null] = :binary.split(namespace_and_name, <<0>>) + name = String.slice(name_with_null, 0..-2) + + %Type{ + id: data_type_id, + namespace: namespace, + name: name + } + end + + defp decode_message_impl(binary), do: %Unsupported{data: binary} + + defp decode_tuple_data(binary, columns_remaining, accumulator \\ []) + + defp decode_tuple_data(remaining_binary, 0, accumulator) when is_binary(remaining_binary), + do: {remaining_binary, accumulator |> Enum.reverse() |> List.to_tuple()} + + defp decode_tuple_data(<<"n", rest::binary>>, columns_remaining, accumulator), + do: decode_tuple_data(rest, columns_remaining - 1, [nil | accumulator]) + + defp decode_tuple_data(<<"u", rest::binary>>, columns_remaining, accumulator), + do: decode_tuple_data(rest, columns_remaining - 1, [:unchanged_toast | accumulator]) + + defp decode_tuple_data( + <<"t", column_length::integer-32, rest::binary>>, + columns_remaining, + accumulator + ), + do: + decode_tuple_data( + :erlang.binary_part(rest, {byte_size(rest), -(byte_size(rest) - column_length)}), + columns_remaining - 1, + [ + :erlang.binary_part(rest, {0, column_length}) | accumulator + ] + ) + + defp decode_columns(binary, accumulator \\ []) + defp decode_columns(<<>>, accumulator), do: Enum.reverse(accumulator) + + defp decode_columns(<>, accumulator) do + [name | [<>]] = + String.split(rest, <<0>>, parts: 2) + + decoded_flags = + case flags do + 1 -> [:key] + _ -> [] + end + + decode_columns(columns, [ + %Column{ + name: name, + flags: decoded_flags, + type: OidDatabase.name_for_type_id(data_type_id), + # type: data_type_id, + type_modifier: type_modifier + } + | accumulator + ]) + end + + defp pgtimestamp_to_timestamp(microsecond_offset) when is_integer(microsecond_offset) do + {:ok, epoch, 0} = @pg_epoch + + DateTime.add(epoch, microsecond_offset, :microsecond) + end + + defp decode_lsn(<>), + do: {xlog_file, xlog_offset} +end diff --git a/lib/supavisor/protocol/wal_decoder/oid_database.ex b/lib/supavisor/protocol/wal_decoder/oid_database.ex new file mode 100644 index 00000000..52826cab --- /dev/null +++ b/lib/supavisor/protocol/wal_decoder/oid_database.ex @@ -0,0 +1,178 @@ +# CREDITS +# This file draws heavily from https://github.com/cainophile/pgoutput_decoder +# License: https://github.com/cainophile/pgoutput_decoder/blob/master/LICENSE + +# Lifted from epgsql (src/epgsql_binary.erl), this module licensed under +# 3-clause BSD found here: https://raw.githubusercontent.com/epgsql/epgsql/devel/LICENSE + +# https://github.com/brianc/node-pg-types/blob/master/lib/builtins.js +# MIT License (MIT) + +# Following query was used to generate this file: +# SELECT json_object_agg(UPPER(PT.typname), PT.oid::int4 ORDER BY pt.oid) +# FROM pg_type PT +# WHERE typnamespace = (SELECT pgn.oid FROM pg_namespace pgn WHERE nspname = 'pg_catalog') -- Take only builting Postgres types with stable OID (extension types are not guaranted to be stable) +# AND typtype = 'b' -- Only basic types +# AND typisdefined -- Ignore undefined types + +defmodule Supavisor.Protocol.WalDecoder.OidDatabase do + @moduledoc "This module maps a numeric PostgreSQL type ID to a descriptive string." + + @doc """ + Maps a numeric PostgreSQL type ID to a descriptive string. + + ## Examples + + iex> name_for_type_id(1700) + "numeric" + + iex> name_for_type_id(25) + "text" + + iex> name_for_type_id(3802) + "jsonb" + + """ + def name_for_type_id(type_id) do + case type_id do + 16 -> "bool" + 17 -> "bytea" + 18 -> "char" + 19 -> "name" + 20 -> "int8" + 21 -> "int2" + 22 -> "int2vector" + 23 -> "int4" + 24 -> "regproc" + 25 -> "text" + 26 -> "oid" + 27 -> "tid" + 28 -> "xid" + 29 -> "cid" + 30 -> "oidvector" + 114 -> "json" + 142 -> "xml" + 143 -> "_xml" + 194 -> "pg_node_tree" + 199 -> "_json" + 210 -> "smgr" + 600 -> "point" + 601 -> "lseg" + 602 -> "path" + 603 -> "box" + 604 -> "polygon" + 628 -> "line" + 629 -> "_line" + 650 -> "cidr" + 651 -> "_cidr" + 700 -> "float4" + 701 -> "float8" + 702 -> "abstime" + 703 -> "reltime" + 704 -> "tinterval" + 718 -> "circle" + 719 -> "_circle" + 774 -> "macaddr8" + 775 -> "_macaddr8" + 790 -> "money" + 791 -> "_money" + 829 -> "macaddr" + 869 -> "inet" + 1000 -> "_bool" + 1001 -> "_bytea" + 1002 -> "_char" + 1003 -> "_name" + 1005 -> "_int2" + 1006 -> "_int2vector" + 1007 -> "_int4" + 1008 -> "_regproc" + 1009 -> "_text" + 1010 -> "_tid" + 1011 -> "_xid" + 1012 -> "_cid" + 1013 -> "_oidvector" + 1014 -> "_bpchar" + 1015 -> "_varchar" + 1016 -> "_int8" + 1017 -> "_point" + 1018 -> "_lseg" + 1019 -> "_path" + 1020 -> "_box" + 1021 -> "_float4" + 1022 -> "_float8" + 1023 -> "_abstime" + 1024 -> "_reltime" + 1025 -> "_tinterval" + 1027 -> "_polygon" + 1028 -> "_oid" + 1033 -> "aclitem" + 1034 -> "_aclitem" + 1040 -> "_macaddr" + 1041 -> "_inet" + 1042 -> "bpchar" + 1043 -> "varchar" + 1082 -> "date" + 1083 -> "time" + 1114 -> "timestamp" + 1115 -> "_timestamp" + 1182 -> "_date" + 1183 -> "_time" + 1184 -> "timestamptz" + 1185 -> "_timestamptz" + 1186 -> "interval" + 1187 -> "_interval" + 1231 -> "_numeric" + 1263 -> "_cstring" + 1266 -> "timetz" + 1270 -> "_timetz" + 1560 -> "bit" + 1561 -> "_bit" + 1562 -> "varbit" + 1563 -> "_varbit" + 1700 -> "numeric" + 1790 -> "refcursor" + 2201 -> "_refcursor" + 2202 -> "regprocedure" + 2203 -> "regoper" + 2204 -> "regoperator" + 2205 -> "regclass" + 2206 -> "regtype" + 2207 -> "_regprocedure" + 2208 -> "_regoper" + 2209 -> "_regoperator" + 2210 -> "_regclass" + 2211 -> "_regtype" + 2949 -> "_txid_snapshot" + 2950 -> "uuid" + 2951 -> "_uuid" + 2970 -> "txid_snapshot" + 3220 -> "pg_lsn" + 3221 -> "_pg_lsn" + 3361 -> "pg_ndistinct" + 3402 -> "pg_dependencies" + 3614 -> "tsvector" + 3615 -> "tsquery" + 3642 -> "gtsvector" + 3643 -> "_tsvector" + 3644 -> "_gtsvector" + 3645 -> "_tsquery" + 3734 -> "regconfig" + 3735 -> "_regconfig" + 3769 -> "regdictionary" + 3770 -> "_regdictionary" + 3802 -> "jsonb" + 3807 -> "_jsonb" + 3905 -> "_int4range" + 3907 -> "_numrange" + 3909 -> "_tsrange" + 3911 -> "_tstzrange" + 3913 -> "_daterange" + 3927 -> "_int8range" + 4089 -> "regnamespace" + 4090 -> "_regnamespace" + 4096 -> "regrole" + 4097 -> "_regrole" + _ -> type_id + end + end +end diff --git a/lib/supavisor/tenants.ex b/lib/supavisor/tenants.ex index b1f037e4..15852932 100644 --- a/lib/supavisor/tenants.ex +++ b/lib/supavisor/tenants.ex @@ -5,6 +5,7 @@ defmodule Supavisor.Tenants do import Ecto.Query, warn: false alias Supavisor.Repo + alias Supavisor.Helpers, as: H alias Supavisor.Tenants.Tenant alias Supavisor.Tenants.User @@ -38,11 +39,25 @@ defmodule Supavisor.Tenants do """ def get_tenant!(id), do: Repo.get!(Tenant, id) + @spec get_tenant_by_external_id(:cached, any()) :: Tenant.t() | nil + def get_tenant_by_external_id(:cached, external_id) do + key = {:tenant_by_external_id, external_id} + fun = fn -> get_tenant_by_external_id(external_id) end + H.cached(key, fun) + end + @spec get_tenant_by_external_id(String.t()) :: Tenant.t() | nil def get_tenant_by_external_id(external_id) do Tenant |> Repo.get_by(external_id: external_id) |> Repo.preload(:users) end + @spec get_user(:cached, any(), any(), any()) :: {:ok, map()} | {:error, any()} + def get_user(:cached, user, external_id, sni_hostname) do + key = {:user, user, external_id, sni_hostname} + fun = fn -> get_user(user, external_id, sni_hostname) end + H.cached(key, fun) + end + @spec get_user(String.t(), String.t() | nil, String.t() | nil) :: {:ok, map()} | {:error, any()} def get_user(_, nil, nil) do @@ -64,6 +79,12 @@ defmodule Supavisor.Tenants do end end + def get_pool_config(:cached, external_id, user) do + key = {:pool_config, external_id, user} + fun = fn -> get_pool_config(external_id, user) end + H.cached(key, fun) + end + def get_pool_config(external_id, user) do query = from(a in User, diff --git a/mix.lock b/mix.lock index 3f68bc15..270d2592 100644 --- a/mix.lock +++ b/mix.lock @@ -4,9 +4,9 @@ "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", "dfb3e39d478944c6a17fc37c24ed3ad4a8cc2af0", []}, + "burrito": {:git, "https://github.com/burrito-elixir/burrito.git", "68ec772f22f623d75bd1f667b1cb4c95f2935b3b", []}, "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.2", "0c6292ecf3e3f20b7c88408f00096337c4bfd99bd46cc2fe63413ddbe45b3573", [:mix], [], "hexpm", "40b2dd2836199203df8500e4a270f10fc006cc95adc8a319e148dc3077391d96"}, + "castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"}, "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"}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, @@ -27,32 +27,32 @@ "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "finch": {:hex, :finch, "0.16.0", "40733f02c89f94a112518071c0a91fe86069560f5dbdb39f9150042f44dcfb1a", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f660174c4d519e5fec629016054d60edd822cdfe2b7270836739ac2f97735ec5"}, "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, - "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, + "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "joken": {:hex, :joken, "2.5.0", "09be497d804b8115eb6f07615cef2e60c2a1008fb89dc0aef0d4c4b4609b99aa", [:mix], [{:jose, "~> 1.11.2", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "22b25c89617c5ed8ca7b31026340a25ea0f9ca7160f9706b79be9ed81fdf74e7"}, - "jose": {:hex, :jose, "1.11.5", "3bc2d75ffa5e2c941ca93e5696b54978323191988eb8d225c2e663ddfefd515e", [:mix, :rebar3], [], "hexpm", "dcd3b215bafe02ea7c5b23dafd3eb8062a5cd8f2d904fd9caa323d37034ab384"}, + "jose": {:hex, :jose, "1.11.6", "613fda82552128aa6fb804682e3a616f4bc15565a048dabd05b1ebd5827ed965", [:mix, :rebar3], [], "hexpm", "6275cb75504f9c1e60eeacb771adfeee4905a9e182103aa59b53fed651ff9738"}, "jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm", "318c59078ac220e966d27af3646026db9b5a5e6703cb2aa3e26bcfaba65b7433"}, - "libcluster": {:hex, :libcluster, "3.3.2", "84c6ebfdc72a03805955abfb5ff573f71921a3e299279cc3445445d5af619ad1", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8b691ce8185670fc8f3fc0b7ed59eff66c6889df890d13411f8f1a0e6871d8a5"}, + "libcluster": {:hex, :libcluster, "3.3.3", "a4f17721a19004cfc4467268e17cff8b1f951befe428975dd4f6f7b84d927fe0", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "7c0a2275a0bb83c07acd17dab3c3bfb4897b145106750eeccc62d302e3bdfee5"}, "logflare_api_client": {:hex, :logflare_api_client, "0.3.5", "c427ebf65a8402d68b056d4a5ef3e1eb3b90c0ad1d0de97d1fe23807e0c1b113", [:mix], [{:bertex, "~> 1.3", [hex: :bertex, repo: "hexpm", optional: false]}, {:finch, "~> 0.10", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.0", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "16d29abcb80c4f72745cdf943379da02a201504813c3aa12b4d4acb0302b7723"}, "logflare_logger_backend": {:git, "https://github.com/Logflare/logflare_logger_backend.git", "5c117de97376c560c82946a057b5778924e4e205", [tag: "v0.11.1-rc.1"]}, "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, - "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"}, + "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mint": {:hex, :mint, "1.5.1", "8db5239e56738552d85af398798c80648db0e90f343c8469f6c6d8898944fb6f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4a63e1e76a7c3956abd2c72f370a0d0aecddc3976dea5c27eccbecfa5e7d5b1e"}, "nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"}, "nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"}, "octo_fetch": {:hex, :octo_fetch, "0.3.0", "89ff501d2ac0448556ff1931634a538fe6d6cd358ba827ce1747e6a42a46efbf", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "c07e44f2214ab153743b7b3182f380798d0b294b1f283811c1e30cff64096d3d"}, - "open_api_spex": {:hex, :open_api_spex, "3.17.1", "045764edc980833ce43ca663aaa585018a8f397566dbaf1f4edfd03cbafee5f7", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "96e6e8667975d3b8773c9725e1edb67a8cc075dd9711a52d8bda36b177fcaf88"}, - "opentelemetry_api": {:hex, :opentelemetry_api, "1.2.1", "7b69ed4f40025c005de0b74fce8c0549625d59cb4df12d15c32fe6dc5076ff42", [:mix, :rebar3], [{:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}], "hexpm", "6d7a27b7cad2ad69a09cabf6670514cafcec717c8441beb5c96322bac3d05350"}, + "open_api_spex": {:hex, :open_api_spex, "3.17.3", "ada8e352eb786050dd639db2439d3316e92f3798eb2abd051f55bb9af825b37e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "165db21a85ca83cffc8e7c8890f35b354eddda8255de7404a2848ed652b9f0fe"}, + "opentelemetry_api": {:hex, :opentelemetry_api, "1.2.2", "693f47b0d8c76da2095fe858204cfd6350c27fe85d00e4b763deecc9588cf27a", [:mix, :rebar3], [{:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}], "hexpm", "dc77b9a00f137a858e60a852f14007bb66eda1ffbeb6c05d5fe6c9e678b05e9d"}, "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.2", "c375ffb482beb4e3d20894f84dd7920442884f5f5b70b9f4528cbe0cedefec63", [: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.4", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "1ebca94b32b4d0e097ab2444a9742ed8ff3361acad17365e4e6b2e79b4792159"}, - "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.1", "fe7a02387a7d26002a46b97e9879591efee7ebffe5f5e114fd196632e6e4a08d", [: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", "ddccf8b4966180afe7630b105edb3402b1ca485e7468109540d262e842048ba4"}, - "phoenix_html": {:hex, :phoenix_html, "3.3.1", "4788757e804a30baac6b3fc9695bf5562465dd3f1da8eb8460ad5b404d9a2178", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bed1906edd4906a15fd7b412b85b05e521e1f67c9a85418c55999277e553d0d3"}, + "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_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.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"}, - "phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"}, + "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"}, "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"}, @@ -61,7 +61,7 @@ "postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {: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", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"}, "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.8", "e254074435c970b1d7699777f1a8466acbacab5e6ba4a264d35053bf52c03467", [: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", "a17244d18a7fbf3e9892c38c10628224f6f7974fd364392ca0d85f91e3cc8251"}, + "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"}, "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"}, @@ -72,6 +72,6 @@ "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"}, "typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"}, "unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm", "6c7729a2d214806450d29766abc2afaa7a2cbecf415be64f36a6691afebb50e5"}, - "websock": {:hex, :websock, "0.5.1", "c496036ce95bc26d08ba086b2a827b212c67e7cabaa1c06473cd26b40ed8cf10", [:mix], [], "hexpm", "b9f785108b81cd457b06e5f5dabe5f65453d86a99118b2c0a515e1e296dc2d2c"}, - "websock_adapter": {:hex, :websock_adapter, "0.5.1", "292e6c56724e3457e808e525af0e9bcfa088cc7b9c798218e78658c7f9b85066", [: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", "8e2e1544bfde5f9d0442f9cec2f5235398b224f75c9e06b60557debf64248ec1"}, + "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"}, }