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.
125 lines
3.8 KiB
Elixir
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
|