defmodule Microwaveprop.Workers.Era5FetchWorker do @moduledoc """ Thin router that ensures an ERA5 profile is available for a single `{lat, lon, valid_time}`. On a cache miss it enqueues the much more efficient `Era5MonthBatchWorker` for the month-tile that contains the requested point, rather than hitting the CDS API point-by-point. Jobs are unique on `{lat, lon, valid_time}` so duplicate enrichment requests collapse. The enqueued month-batch worker is itself unique on `(year, month, tile_lat, tile_lon)` so the first enqueue wins and later ones are no-ops. """ use Oban.Worker, queue: :era5, max_attempts: 5, unique: [ period: :infinity, states: [:available, :scheduled, :executing, :retryable] ] alias Microwaveprop.Repo alias Microwaveprop.Weather.Era5BatchClient alias Microwaveprop.Weather.Era5Profile alias Microwaveprop.Workers.Era5MonthBatchWorker require Logger @impl Oban.Worker def backoff(%Oban.Job{attempt: attempt}) do # Enqueueing the batch is cheap; retry fast so we stop blocking downstream. min(30 * Integer.pow(2, attempt - 1), _one_hour = 3_600) end @impl Oban.Worker def perform(%Oban.Job{args: %{"lat" => lat, "lon" => lon, "valid_time" => valid_time_str}}) do {:ok, valid_time, _} = DateTime.from_iso8601(valid_time_str) rlat = Float.round(lat * 4) / 4 rlon = Float.round(lon * 4) / 4 if has_era5_profile?(rlat, rlon, valid_time) do :ok else {tile_lat, tile_lon} = Era5BatchClient.tile_for_point(rlat, rlon) Logger.info( "ERA5: enqueueing month batch for #{valid_time.year}-#{valid_time.month} tile #{tile_lat},#{tile_lon} (triggered by #{rlat},#{rlon} @ #{valid_time_str})" ) %{ year: valid_time.year, month: valid_time.month, tile_lat: tile_lat, tile_lon: tile_lon } |> Era5MonthBatchWorker.new() |> Oban.insert() :ok end end defp has_era5_profile?(lat, lon, valid_time) do import Ecto.Query dlat = 0.13 dlon = 0.13 time_start = DateTime.add(valid_time, -1800, :second) time_end = DateTime.add(valid_time, 1800, :second) Era5Profile |> where( [p], p.lat >= ^(lat - dlat) and p.lat <= ^(lat + dlat) and p.lon >= ^(lon - dlon) and p.lon <= ^(lon + dlon) and p.valid_time >= ^time_start and p.valid_time <= ^time_end ) |> Repo.exists?() end end