- New fields: hrrr_status, weather_status, terrain_status, iemre_status with values: pending, queued, processing, complete, failed, unavailable - EnrichmentStatus module defines valid state transitions - Migration backfills: queued=true → complete, false → pending - Workers set status on transitions (queued on enqueue, complete on finish) - Show page reads status from contact struct, not computed dynamically - Backfill dashboard queries status fields for accurate progress counts - Remove cron-scheduled ContactWeatherEnqueueWorker (enrichment only on submission, page view, or manual backfill) - Partial indexes on status != 'complete' for fast unprocessed lookups
57 lines
1.9 KiB
Elixir
57 lines
1.9 KiB
Elixir
defmodule Microwaveprop.Radio.EnrichmentStatus do
|
|
@moduledoc """
|
|
State machine for contact enrichment lifecycle.
|
|
|
|
States:
|
|
:pending → initial, no work started
|
|
:queued → Oban job inserted
|
|
:processing → worker actively running
|
|
:complete → data exists in DB
|
|
:failed → worker exhausted retries
|
|
:unavailable → data cannot be fetched (missing pos, archive gone, etc.)
|
|
|
|
Valid transitions:
|
|
pending → queued, unavailable
|
|
queued → processing, failed
|
|
processing → complete, failed
|
|
failed → queued (retry)
|
|
unavailable → queued (retry with new data source)
|
|
complete → pending (reset for re-enrichment)
|
|
"""
|
|
|
|
@states [:pending, :queued, :processing, :complete, :failed, :unavailable]
|
|
@fields [:hrrr_status, :weather_status, :terrain_status, :iemre_status]
|
|
|
|
@transitions %{
|
|
pending: [:queued, :unavailable],
|
|
queued: [:processing, :failed],
|
|
processing: [:complete, :failed],
|
|
failed: [:queued],
|
|
unavailable: [:queued],
|
|
complete: [:pending]
|
|
}
|
|
|
|
def states, do: @states
|
|
def fields, do: @fields
|
|
|
|
@doc "Returns true if transitioning from `from` to `to` is valid."
|
|
def valid_transition?(from, to) do
|
|
to in Map.get(@transitions, from, [])
|
|
end
|
|
|
|
@doc "Validates and applies a status transition on a changeset."
|
|
def transition(changeset, field, new_status) when field in @fields and new_status in @states do
|
|
current = Ecto.Changeset.get_field(changeset, field)
|
|
|
|
if valid_transition?(current, new_status) do
|
|
Ecto.Changeset.put_change(changeset, field, new_status)
|
|
else
|
|
Ecto.Changeset.add_error(changeset, field, "invalid transition from #{current} to #{new_status}")
|
|
end
|
|
end
|
|
|
|
@doc "Force-sets status without transition validation (for migrations, resets)."
|
|
def force_status(changeset, field, status) when field in @fields and status in @states do
|
|
Ecto.Changeset.put_change(changeset, field, status)
|
|
end
|
|
end
|