prop/vendor/oban_pro/lib/oban/pro/limiters/rate.ex
Graham McIntire 0db5c2ae3e Vendor oban_pro 1.6.13
Adds the commercial Oban Pro package (vendored from the towerops-web2
vendor tree) so this project can use its workers, plugins, and
Smart engine features.
2026-04-09 14:14:49 -05:00

125 lines
3.8 KiB
Elixir

defmodule Oban.Pro.Limiters.Rate do
@moduledoc false
@behaviour Oban.Pro.Limiter
alias Oban.Pro.Partition
@impl Oban.Pro.Limiter
def check(_repo, %{producer: producer} = changes) when is_map(producer.meta.rate_limit) do
%{allowed: allowed, period: period, window_time: prev_time} = producer.meta.rate_limit
curr_time = unix_now()
weight = div(max(period - curr_time - prev_time, 0), period)
windows =
[producer | changes.all_producers]
|> Enum.uniq_by(& &1.uuid)
|> Enum.map(& &1.meta.rate_limit)
|> Enum.filter(&(is_map(&1) and &1.window_time >= curr_time - &1.period))
|> Enum.reduce(%{}, &merge/2)
windows = update_legacy_windows(windows)
if is_nil(producer.meta.rate_limit.partition) do
{:ok, combined_demands(weight, allowed, windows)}
else
{:ok, separate_demands(changes.conf, weight, allowed, producer, windows)}
end
end
def check(_repo, _changes), do: {:ok, nil}
defp merge(rate_limit, acc) do
Map.merge(rate_limit.windows, acc, fn _key, v1, v2 ->
%{"curr_count" => c1, "prev_count" => p1} = v1
%{"curr_count" => c2, "prev_count" => p2} = v2
%{"curr_count" => c1 + c2, "prev_count" => p1 + p2}
end)
end
defp update_legacy_windows(%{"args" => _} = window), do: %{"none" => window}
defp update_legacy_windows(windows), do: windows
defp combined_demands(weight, allowed, windows) do
total =
Enum.reduce(windows, 0, fn {_, val}, acc ->
acc + (val["prev_count"] * weight + val["curr_count"])
end)
max(allowed - total, 0)
end
defp separate_demands(conf, weight, allowed, producer, windows) do
untracked =
for key <- Partition.available_keys(conf, producer.queue, producer.meta.local_limit),
not is_map_key(windows, key),
into: %{},
do: {key, allowed}
Enum.reduce(windows, untracked, fn {key, val}, acc ->
%{"curr_count" => curr, "prev_count" => prev} = val
case max(allowed - (prev * weight + curr), 0) do
demand when demand > 0 -> Map.put(acc, key, demand)
_ -> acc
end
end)
end
@impl Oban.Pro.Limiter
def track(%{rate_limit: rate_limit} = meta, jobs) when is_map(rate_limit) do
new_mapping =
Enum.reduce(jobs, %{}, fn job, acc ->
key = Partition.get_key(job, "*")
map = %{"curr_count" => 1, "prev_count" => 0}
Map.update(acc, key, map, &%{&1 | "curr_count" => &1["curr_count"] + 1})
end)
new_windows =
for {key, val} <- new_mapping,
not is_map_key(rate_limit.windows, key),
into: %{},
do: {key, val}
prev_unix = rate_limit.window_time
unix_diff = unix_now() - prev_unix
{mode, next_time} =
cond do
unix_diff > rate_limit.period * 2 -> {:dump_prev, unix_now()}
unix_diff > rate_limit.period -> {:swap_prev, unix_now()}
true -> {:bump_curr, prev_unix}
end
all_windows =
for {key, window} <- rate_limit.windows,
not match?(%{"curr_count" => 0, "prev_count" => 0}, window),
into: new_windows,
do: {key, put_next_count(key, window, mode, new_mapping)}
%{meta | rate_limit: %{rate_limit | windows: all_windows, window_time: next_time}}
end
def track(meta, _jobs), do: meta
defp put_next_count(key, window, mode, mapping) do
%{"curr_count" => curr_count, "prev_count" => prev_count} = window
next_count = get_in(mapping, [key, "curr_count"]) || 0
{curr_count, prev_count} =
case mode do
:dump_prev -> {next_count, 0}
:swap_prev -> {next_count, curr_count}
:bump_curr -> {curr_count + next_count, prev_count}
end
%{window | "curr_count" => curr_count, "prev_count" => prev_count}
end
defp unix_now do
DateTime.to_unix(DateTime.utc_now(), :second)
end
end