towerops/lib/towerops/workers/polling_offset.ex
2026-01-26 17:07:42 -06:00

58 lines
1.7 KiB
Elixir

defmodule Towerops.Workers.PollingOffset do
@moduledoc """
Calculates deterministic polling offsets to distribute load across time.
Uses erlang's phash2 to generate stable offsets from device IDs,
ensuring each device polls at a different time within the interval.
This prevents the "thundering herd" problem where all devices poll
at synchronized clock intervals (e.g., 1:00, 1:05, 1:10), creating
load spikes on the server.
## Examples
# Device with ID "abc-123" and 300-second interval
iex> calculate_offset("abc-123", 300)
94 # Device polls at :01:34, :06:34, :11:34, etc.
# Same device always gets same offset
iex> calculate_offset("abc-123", 300)
94
# Different device gets different offset
iex> calculate_offset("def-456", 300)
171 # Device polls at :02:51, :07:51, :12:51, etc.
"""
@doc """
Calculate polling offset for a device.
Returns an offset in seconds between 0 and interval_seconds-1.
Same device_id always returns same offset for a given interval.
## Parameters
* `device_id` - The device UUID (binary or string format)
* `interval_seconds` - The polling interval in seconds
## Returns
An integer offset in the range [0, interval_seconds)
## Examples
iex> calculate_offset("abc-123", 300)
94
iex> calculate_offset("abc-123", 300)
94 # Always returns same value for same device
iex> offset = calculate_offset("some-uuid", 60)
iex> offset >= 0 and offset < 60
true
"""
@spec calculate_offset(binary(), pos_integer()) :: non_neg_integer()
def calculate_offset(device_id, interval_seconds) do
device_id |> :erlang.phash2() |> rem(interval_seconds)
end
end