towerops/lib/towerops/maintenance.ex
Graham McIntire de986bddf6
Prevent Oban polling/monitoring job stacking per device
When the Oban queue backs up, the 60-second uniqueness window expires
and duplicate jobs stack up per device. Switch to period: :infinity so
only one poll/monitor job exists per device at any time. Add replace
option to supersede stale scheduled jobs with updated scheduled_at.
Remove :executing from unique states so self-scheduling works while
the current job runs. Set max_attempts: 1 since retrying stale polls
is pointless.

Also fix all credo --strict issues across the codebase.
2026-02-16 16:37:48 -06:00

161 lines
4.2 KiB
Elixir

defmodule Towerops.Maintenance do
@moduledoc """
Context for managing maintenance windows.
Maintenance windows suppress alerts for devices, sites, or entire organizations
during scheduled maintenance periods.
"""
import Ecto.Query
alias Towerops.Devices.Device
alias Towerops.Maintenance.MaintenanceWindow
alias Towerops.Repo
@preloads [:organization, :site, :device, :created_by]
def create_window(attrs) do
%MaintenanceWindow{}
|> MaintenanceWindow.changeset(attrs)
|> Repo.insert()
end
def update_window(%MaintenanceWindow{} = window, attrs) do
window
|> MaintenanceWindow.changeset(attrs)
|> Repo.update()
end
def delete_window(%MaintenanceWindow{} = window) do
Repo.delete(window)
end
def get_window!(id) do
MaintenanceWindow
|> Repo.get!(id)
|> Repo.preload(@preloads)
end
def list_windows(organization_id, opts \\ []) do
now = DateTime.utc_now()
MaintenanceWindow
|> where([w], w.organization_id == ^organization_id)
|> apply_filter(opts[:filter], now)
|> order_by([w], desc: w.starts_at)
|> Repo.all()
|> Repo.preload(@preloads)
end
defp apply_filter(query, :active, now) do
where(query, [w], w.starts_at <= ^now and w.ends_at >= ^now)
end
defp apply_filter(query, :upcoming, now) do
where(query, [w], w.starts_at > ^now)
end
defp apply_filter(query, :past, now) do
where(query, [w], w.ends_at < ^now)
end
defp apply_filter(query, _, _now), do: query
@doc """
Checks if a device is currently in a maintenance window.
Checks direct device windows, site windows, and org-wide windows.
"""
def device_in_maintenance?(device_id) do
case active_windows_for_device(device_id) do
[] -> false
_ -> true
end
end
@doc """
Checks if a site is currently in a maintenance window.
"""
def site_in_maintenance?(site_id) do
now = DateTime.utc_now()
MaintenanceWindow
|> where([w], w.site_id == ^site_id and is_nil(w.device_id))
|> where([w], w.starts_at <= ^now and w.ends_at >= ^now)
|> where([w], w.suppress_alerts == true)
|> Repo.exists?()
end
@doc """
Returns all active maintenance windows affecting a device.
Checks: direct device match, site match, and org-wide windows.
"""
def active_windows_for_device(device_id) do
now = DateTime.utc_now()
device = Repo.get!(Device, device_id)
active_base =
MaintenanceWindow
|> where([w], w.organization_id == ^device.organization_id)
|> where([w], w.starts_at <= ^now and w.ends_at >= ^now)
|> where([w], w.suppress_alerts == true)
# Direct device windows
direct = where(active_base, [w], w.device_id == ^device_id)
# Site windows (if device has a site)
site_query =
if device.site_id do
where(active_base, [w], w.site_id == ^device.site_id and is_nil(w.device_id))
else
where(active_base, [w], false)
end
# Org-wide windows
org_wide = where(active_base, [w], is_nil(w.site_id) and is_nil(w.device_id))
direct_results = Repo.all(direct)
site_results = Repo.all(site_query)
org_results = Repo.all(org_wide)
Enum.uniq_by(direct_results ++ site_results ++ org_results, & &1.id)
end
@doc """
Returns all active maintenance windows for an organization.
"""
def active_windows_for_org(organization_id) do
now = DateTime.utc_now()
MaintenanceWindow
|> where([w], w.organization_id == ^organization_id)
|> where([w], w.starts_at <= ^now and w.ends_at >= ^now)
|> Repo.all()
|> Repo.preload(@preloads)
end
@doc """
Returns the name of the active maintenance window for a device or site, or nil.
Accepts a %Device{} or %Site{} struct.
"""
def maintenance_badge(%Device{} = device) do
case active_windows_for_device(device.id) do
[window | _] -> window.name
[] -> nil
end
end
def maintenance_badge(%Towerops.Sites.Site{} = site) do
now = DateTime.utc_now()
MaintenanceWindow
|> where([w], w.site_id == ^site.id and is_nil(w.device_id))
|> where([w], w.starts_at <= ^now and w.ends_at >= ^now)
|> where([w], w.suppress_alerts == true)
|> limit(1)
|> select([w], w.name)
|> Repo.one()
end
def maintenance_badge(_), do: nil
end