towerops/vendor/oban_pro/lib/oban/pro/utils.ex

230 lines
6.2 KiB
Elixir

defmodule Oban.Pro.Utils do
@moduledoc false
import Ecto.Query
alias Ecto.Changeset
@legacy_ttl :timer.minutes(5)
@spec encode64(term(), [:compressed | :deterministic]) :: String.t()
def encode64(term, opts \\ [:compressed]) do
term
|> :erlang.term_to_binary(opts)
|> Base.encode64(padding: false)
end
@spec decode64(binary(), [:safe | :used]) :: term()
def decode64(bin, opts \\ [:safe]) do
bin
|> Base.decode64!(padding: false)
|> :erlang.binary_to_term(opts)
end
@spec hash64(iodata()) :: String.t()
def hash64(iodata) when is_binary(iodata) or is_list(iodata) do
:blake2s
|> :crypto.hash(iodata)
|> Base.encode64(padding: false)
end
# Caching
def ets_cache(tab, key, ttl, fun) when is_function(fun, 0) do
now = System.monotonic_time(:millisecond)
case :ets.lookup(tab, key) do
[{^key, value, timestamp}] when now - timestamp < ttl ->
value
_ ->
tap(fun.(), &:ets.insert(tab, {key, &1, now}))
end
end
@spec persistent_cache(tuple(), (-> any())) :: any()
def persistent_cache(key, fun) when is_function(fun, 0) do
persistent_cache(key, :infinity, fun)
end
@spec persistent_cache(tuple(), timeout(), (-> any())) :: any()
def persistent_cache(key, ttl, fun) when is_function(fun, 0) do
now = System.monotonic_time(:millisecond)
exp = if ttl == :infinity, do: :infinity, else: now + ttl
case :persistent_term.get(key, nil) do
{val, :infinity} -> val
{val, expires_at} when expires_at > now -> val
_ -> tap(fun.(), &:persistent_term.put(key, {&1, exp}))
end
end
# Async Execution
def async_maybe_await(fun) when is_function(fun, 0) do
# Await during testing to prevent leaking side-effects after a test concludes.
if Process.get(:oban_draining) do
Oban.Pro.TaskSupervisor
|> Task.Supervisor.async(fun)
|> Task.await()
else
Task.Supervisor.start_child(Oban.Pro.TaskSupervisor, fun)
:ok
end
end
# Feature Detection
@spec has_legacy_workflows?(Oban.Config.t()) :: boolean()
def has_legacy_workflows?(conf) do
persistent_cache({__MODULE__, :legacy_workflows, conf.name}, @legacy_ttl, fn ->
has_legacy?(conf, "workflow_id")
end)
end
@spec has_legacy_chains?(Oban.Config.t()) :: boolean()
def has_legacy_chains?(conf) do
persistent_cache({__MODULE__, :legacy_chains, conf.name}, @legacy_ttl, fn ->
has_legacy?(conf, "chain_id")
end)
end
defp has_legacy?(conf, key) do
query =
from(job in Oban.Job,
where: fragment("? \\? ?", job.meta, ^key),
where: job.state == "scheduled",
where: fragment("? ->> 'on_hold' = 'true'", job.meta),
select: true,
limit: 1
)
Oban.Repo.exists?(conf, query)
end
@spec validate_opts(list(), fun()) :: :ok | {:error, term()}
def validate_opts(opts, validator) do
Enum.reduce_while(opts, :ok, fn opt, acc ->
case validator.(opt) do
{:error, _reason} = error -> {:halt, error}
_ -> {:cont, acc}
end
end)
end
@spec cast_period(pos_integer() | {atom(), pos_integer()}) :: pos_integer()
def cast_period({value, unit}) do
unit = to_string(unit)
cond do
unit in ~w(second seconds) -> value
unit in ~w(minute minutes) -> value * 60
unit in ~w(hour hours) -> value * 60 * 60
unit in ~w(day days) -> value * 24 * 60 * 60
true -> unit
end
end
def cast_period(period), do: period
@spec enforce_keys(Changeset.t(), map(), module()) :: Changeset.t()
def enforce_keys(changeset, params, schema) do
fields =
[:fields, :virtual_fields]
|> Enum.flat_map(&schema.__schema__/1)
|> Enum.map(&to_string/1)
Enum.reduce(params, changeset, fn {key, _val}, acc ->
if to_string(key) in fields do
acc
else
Changeset.add_error(acc, :base, "unknown field #{key} provided")
end
end)
end
@spec take_keys(Changeset.t(), atom(), list()) :: [any()]
def take_keys(changeset, field, keys) do
changeset
|> Changeset.get_field(field)
|> take_keys(keys)
end
@spec take_keys(map(), [atom()]) :: [any()]
def take_keys(map, _keys) when map_size(map) == 0, do: []
def take_keys(map, []) when is_map(map), do: take_keys(map, Map.keys(map))
def take_keys(map, keys) when is_map(map) and is_list(keys) do
map = Map.new(map, fn {key, val} -> {to_string(key), val} end)
keys
|> Enum.map(&to_string/1)
|> Enum.sort()
|> Enum.map(fn key ->
val = map |> Map.get(key) |> to_hash_val()
[key, val]
end)
end
if Application.compile_env(:oban_pro, [__MODULE__, :safe_hash], false) do
defp to_hash_val(val) when is_binary(val), do: val
defp to_hash_val(val) when is_number(val), do: to_string(val)
defp to_hash_val(val) when is_atom(val), do: to_string(val)
defp to_hash_val(val), do: inspect(val)
else
defp to_hash_val(val), do: val |> :erlang.phash2() |> Integer.to_string()
end
def normalize_by(by) do
by
|> List.wrap()
|> Enum.map(fn
{key, val} -> [key, val |> List.wrap() |> Enum.sort()]
field -> field
end)
end
@spec to_exception(Changeset.t()) :: Exception.t()
def to_exception(changeset) do
changeset
|> to_translated_errors()
|> Enum.reverse()
|> Enum.map(fn {field, message} -> ArgumentError.exception("#{field} #{message}") end)
|> List.first()
end
@spec to_translated_errors(Changeset.t()) :: Keyword.t()
def to_translated_errors(changeset) do
changeset
|> Changeset.traverse_errors(&translate_errors/1)
|> Enum.map(&extract_errors/1)
|> List.flatten()
end
## Conversions
@spec maybe_to_atom(atom() | String.t()) :: atom()
def maybe_to_atom(key) when is_binary(key), do: String.to_existing_atom(key)
def maybe_to_atom(key), do: key
# Helpers
defp translate_errors({msg, opt}) do
Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
opt
|> Keyword.get(String.to_existing_atom(key), key)
|> to_string()
end)
end
defp extract_errors({_key, val}) when is_map(val) do
val
|> Map.to_list()
|> Enum.map(&extract_errors/1)
end
defp extract_errors({key, [message | _]}), do: {key, message}
end