defmodule Oban.Pro.Limiters.Rate do @moduledoc false @behaviour Oban.Pro.Limiter alias Oban.Pro.Limiters.Rate.Algorithm alias Oban.Pro.Partition @impl Oban.Pro.Limiter def check(%{producer: producer} = changes) when is_map(producer.meta.rate_limit) do %{window_time: prev_time} = producer.meta.rate_limit curr_time = unix_now() 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_windows/2) windows = update_legacy_windows(windows) demand = calculate_demand(producer.meta.rate_limit, windows, curr_time, prev_time, changes) {:ok, demand} end def check(_changes), do: {:ok, nil} defp calculate_demand(rate_limit, windows, curr_time, prev_time, changes) do %{allowed: allowed, period: period} = rate_limit algorithm = Algorithm.callback(rate_limit) if is_nil(rate_limit.partition) do algorithm.demand(windows, allowed, period, curr_time, prev_time) else producer = changes.producer available_keys = Partition.available_keys(changes.conf, producer.queue, producer.meta.local_limit) algorithm.partition_demands( windows, allowed, period, curr_time, prev_time, available_keys ) end end defp merge_windows(rate_limit, acc) do Algorithm.callback(rate_limit).merge(rate_limit.windows, acc) end defp update_legacy_windows(%{"args" => _} = window), do: %{"*" => window} defp update_legacy_windows(windows), do: windows def flatten(%{rate_limit: rate_limit}) do Algorithm.callback(rate_limit).flatten(rate_limit.windows) end @impl Oban.Pro.Limiter def track(%{rate_limit: rate_limit} = meta, jobs) when is_map(rate_limit) do new_counts = build_counts(jobs) curr_time = unix_now() {all_windows, next_time} = Algorithm.callback(rate_limit).track( rate_limit.windows, new_counts, rate_limit.period, rate_limit.window_time, curr_time, rate_limit.allowed ) %{meta | rate_limit: %{rate_limit | windows: all_windows, window_time: next_time}} end def track(meta, _jobs), do: meta defp build_counts(jobs) do Enum.reduce(jobs, %{}, fn job, acc -> key = Partition.get_key(job, "*") weight = Oban.Pro.Worker.get_weight(job) Map.update(acc, key, weight, &(&1 + weight)) end) end defp unix_now do DateTime.to_unix(DateTime.utc_now(), :second) end end