Skip to content

Instantly share code, notes, and snippets.

@James-E-A
Last active January 27, 2025 00:38
Show Gist options
  • Save James-E-A/c296b416339cf5ce1edb7c94eb84c84e to your computer and use it in GitHub Desktop.
Save James-E-A/c296b416339cf5ce1edb7c94eb84c84e to your computer and use it in GitHub Desktop.
Elixir lazy upsert storing SECRET_KEY_BASE in Ecto
defmodule Foo.Application do
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
FooWeb.Telemetry,
Foo.Repo,
{Ecto.Migrator,
repos: Application.fetch_env!(:fooApp, :ecto_repos),
skip: skip_migrations?()},
#{DNSCluster, query: Application.get_env(:fooApp, :dns_cluster_query) || :ignore},
#{Phoenix.PubSub, name: Foo.PubSub},
#{DynamicSupervisor, name: Foo.RoomSupervisor},
FooWeb.Endpoint |> Foo.Util.defer_spec(
secret_key_base: {:ecto_simple, Foo.Repo, Foo.Repo.Schemas.Secret, &FooWeb.Util.phx_gen_secret/0})
]
opts = [strategy: :one_for_one, name: Foo.Supervisor]
Supervisor.start_link(children, opts)
end
@impl true
def config_change(changed, _new, removed) do
FooWeb.Endpoint.config_change(changed, removed)
:ok
end
defp skip_migrations?() do
# By default, sqlite migrations are run when using a release
System.get_env("RELEASE_NAME") != nil
end
end
defmodule Foo.Util do
require Ecto.Query
# https://github.com/elixir-ecto/ecto/blob/v3.12.5/lib/ecto/repo/queryable.ex#L161
@spec one_or_insert_lazy!(Ecto.Repo.t(), %Ecto.Query{from: s, select: t}, (-> s)) :: t when s: Ecto.Schema.t(), t: term()
def one_or_insert_lazy!(repo, query, fun) do
repo.transaction(fn r ->
case r.all(query) do
[value] ->
value
[] ->
new_record = fun.()
r.insert!(new_record)
case r.all(query) do
[value] ->
value
[] ->
raise ArgumentError, "changeset doesn't fulfill query (will be rolled back): #{inspect [struct_or_changeset: new_record, query: query]}"
other ->
raise ArgumentError, "changeset produced multiple results for query (will be rolled back): #{inspect [struct_or_changeset: new_record, query: query, result: other]}"
end
other ->
# https://github.com/elixir-ecto/ecto/blob/v3.12.5/lib/ecto/repo/queryable.ex#L165
raise Ecto.MultipleResultsError, queryable: query, count: length(other)
end
end)
end
# https://elixirforum.com/t/managing-secret-key-base-without-kubernetes-docker-etc/67926/20?u=james_e
@spec defer_spec(Supervisor.module_spec() | Supervisor.child_spec(), keyword()) :: Supervisor.child_spec()
def defer_spec(child_spec, deferred_opts)
def defer_spec(module, deferred_opts) when is_atom(module) do
module.child_spec([])
|> defer_spec(deferred_opts)
end
def defer_spec({module, arg}, deferred_opts) when is_atom(module) do
module.child_spec(arg)
|> defer_spec(deferred_opts)
end
def defer_spec(%{id: _, start: {_, _, _} = start} = child_spec, deferred_opts) do
%{child_spec | start: {
__MODULE__,
:_start_with_deferred_opts,
[start, deferred_opts]
}}
end
def defer_spec(_, _) do
raise ArgumentError, "child_spec"
end
defp eval_deferred_opts(opts \\ [], deferred_opts) do
access = if is_list(opts), do: Keyword, else: Access
Enum.reduce(deferred_opts, opts, fn
{key, fun}, acc when is_function(fun, 0) ->
value = fun.()
access.put(acc, key, value)
{key, fun}, acc when is_function(fun, 1) ->
access.get_and_update(acc, key, &{&1, fun.(&1)})
|> elem(1)
{key, {:get_and_update, fun}}, acc when is_function(fun, 1) ->
access.get_and_update(acc, key, fun)
|> elem(1)
{key, {:ecto, repo, query, fun}}, acc when is_function(fun, 0) ->
access.get_and_update(acc, key, fn
value when not is_nil(value) ->
{value, value}
nil ->
value = one_or_insert_lazy!(repo, query, fun)
{nil, value}
end)
|> elem(1)
{key, {:ecto_simple, repo, schema, fun}}, acc when is_function(fun, 0) ->
access.get_and_update(acc, key, fn
value when not is_nil(value) ->
{value, value}
nil ->
key = Atom.to_string(key)
value = one_or_insert_lazy!(
repo,
Ecto.Query.from(
s in schema,
where: s.name == ^key,
select: s.value
),
fn -> struct(
schema,
name: key,
value: fun.()
) end
)
{nil, value}
end)
|> elem(1)
end)
end
def _start_with_deferred_opts({m, f, a}, deferred_opts) do
a = case a do
[_ | _] = a -> List.update_at(a, -1, &eval_deferred_opts(&1, deferred_opts))
[] -> [eval_deferred_opts(deferred_opts)]
end
# https://github.com/elixir-lang/elixir/blob/v1.17.3/lib/elixir/lib/task/supervisor.ex#L539
apply(m, f, a)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment