prop/test/microwaveprop/weather/grid_cache_test.exs
Graham McIntire b7261e772c
feat(observability): emit telemetry for NotifyListener warm + GridCache hit/miss
Band-warm failures in the Rust propagation_ready pipeline were only
surfaced via Logger.warning, so Prometheus had no way to see them.
The /weather GridCache hit/miss was likewise invisible while its
/map counterpart (ScoreCache) already reported cache ratio.

- NotifyListener emits [:microwaveprop, :propagation, :notify_listener, :warm]
  with %{ok, err} measurements and %{valid_time} metadata after every
  propagation_ready notification. Public handle_propagation_ready/1 so
  tests can exercise the warm+telemetry path without Postgrex.
- GridCache.fetch/1, fetch_bounds/2, and fetch_point/3 emit
  [:microwaveprop, :weather, :grid_cache, :lookup] with a :hit | :miss
  metadata key on every ETS lookup.
2026-04-21 13:30:26 -05:00

198 lines
6.3 KiB
Elixir

defmodule Microwaveprop.Weather.GridCacheTest do
use ExUnit.Case, async: false
alias Microwaveprop.Weather.GridCache
setup do
GridCache.clear()
:ok
end
describe "fetch/1" do
test "returns :miss when nothing is cached" do
assert GridCache.fetch(~U[2026-04-12 12:00:00Z]) == :miss
end
test "returns cached rows after put" do
rows = [%{lat: 32.0, lon: -97.0, temperature: 25.0}]
GridCache.put(~U[2026-04-12 12:00:00Z], rows)
assert {:ok, [%{lat: 32.0, lon: -97.0}]} = GridCache.fetch(~U[2026-04-12 12:00:00Z])
end
end
describe "fetch_bounds/2" do
setup do
rows = [
%{lat: 32.0, lon: -97.0, temperature: 25.0},
%{lat: 40.0, lon: -74.0, temperature: 20.0},
%{lat: 34.0, lon: -98.0, temperature: 28.0}
]
GridCache.put(~U[2026-04-12 12:00:00Z], rows)
:ok
end
test "returns all rows when bounds are nil" do
assert {:ok, list} = GridCache.fetch_bounds(~U[2026-04-12 12:00:00Z], nil)
assert length(list) == 3
end
test "returns only rows inside the bounds" do
bounds = %{"south" => 31.0, "north" => 35.0, "west" => -100.0, "east" => -95.0}
assert {:ok, list} = GridCache.fetch_bounds(~U[2026-04-12 12:00:00Z], bounds)
assert length(list) == 2
end
test "returns :miss when the valid_time is not cached" do
bounds = %{"south" => 0.0, "north" => 90.0, "west" => -180.0, "east" => 0.0}
assert GridCache.fetch_bounds(~U[2099-01-01 00:00:00Z], bounds) == :miss
end
end
describe "fetch_point/3" do
setup do
rows = [
%{lat: 32.0, lon: -97.0, temperature: 25.0},
%{lat: 33.0, lon: -97.0, temperature: 27.0}
]
GridCache.put(~U[2026-04-12 12:00:00Z], rows)
:ok
end
test "returns the cached row for a known point" do
assert {:ok, %{temperature: 25.0}} =
GridCache.fetch_point(~U[2026-04-12 12:00:00Z], 32.0, -97.0)
end
test "returns :miss for an unknown point" do
assert GridCache.fetch_point(~U[2026-04-12 12:00:00Z], 40.0, -74.0) == :miss
end
end
describe "claim_fill/1 and release_fill/1" do
@valid_time ~U[2026-04-21 12:00:00Z]
test "first claimer wins, subsequent claimers see :in_progress" do
assert GridCache.claim_fill(@valid_time) == true
assert GridCache.claim_fill(@valid_time) == false
end
test "release_fill allows a re-claim" do
assert GridCache.claim_fill(@valid_time) == true
:ok = GridCache.release_fill(@valid_time)
assert GridCache.claim_fill(@valid_time) == true
end
test "lock is auto-released when the claimer process crashes" do
parent = self()
{:ok, claimer} =
Task.start(fn ->
true = GridCache.claim_fill(@valid_time)
send(parent, :claimed)
# Block until we're killed
Process.sleep(:infinity)
end)
receive do
:claimed -> :ok
after
1000 -> flunk("claimer never signaled")
end
# Lock is held by the crashed process
assert GridCache.claim_fill(@valid_time) == false
# Kill the claimer
ref = Process.monitor(claimer)
Process.exit(claimer, :kill)
receive do
{:DOWN, ^ref, :process, ^claimer, _} -> :ok
after
1000 -> flunk("claimer didn't die")
end
# Flush the GenServer so the :DOWN handler runs before we re-probe ETS.
:ok = GridCache.sync()
assert GridCache.claim_fill(@valid_time) == true
end
end
describe "lookup telemetry" do
@event [:microwaveprop, :weather, :grid_cache, :lookup]
setup do
test_pid = self()
handler_id = {__MODULE__, :grid_cache_lookup, System.unique_integer([:positive])}
:telemetry.attach(
handler_id,
@event,
fn event, measurements, metadata, _config ->
send(test_pid, {:telemetry, event, measurements, metadata})
end,
nil
)
on_exit(fn -> :telemetry.detach(handler_id) end)
:ok
end
test "fetch/1 emits hit: true when the valid_time is cached" do
GridCache.put(~U[2026-04-12 12:00:00Z], [%{lat: 32.0, lon: -97.0}])
{:ok, _} = GridCache.fetch(~U[2026-04-12 12:00:00Z])
assert_receive {:telemetry, @event, %{}, %{hit: true}}
end
test "fetch/1 emits hit: false when the valid_time is absent" do
:miss = GridCache.fetch(~U[2099-01-01 00:00:00Z])
assert_receive {:telemetry, @event, %{}, %{hit: false}}
end
test "fetch_bounds/2 emits hit: true on a cached valid_time" do
GridCache.put(~U[2026-04-12 12:00:00Z], [%{lat: 32.0, lon: -97.0}])
bounds = %{"south" => 0.0, "north" => 90.0, "west" => -180.0, "east" => 0.0}
{:ok, _} = GridCache.fetch_bounds(~U[2026-04-12 12:00:00Z], bounds)
assert_receive {:telemetry, @event, %{}, %{hit: true}}
end
test "fetch_bounds/2 emits hit: false when the valid_time is absent" do
bounds = %{"south" => 0.0, "north" => 90.0, "west" => -180.0, "east" => 0.0}
:miss = GridCache.fetch_bounds(~U[2099-01-01 00:00:00Z], bounds)
assert_receive {:telemetry, @event, %{}, %{hit: false}}
end
test "fetch_point/3 emits hit: true when the point exists" do
GridCache.put(~U[2026-04-12 12:00:00Z], [%{lat: 32.0, lon: -97.0}])
{:ok, _} = GridCache.fetch_point(~U[2026-04-12 12:00:00Z], 32.0, -97.0)
assert_receive {:telemetry, @event, %{}, %{hit: true}}
end
test "fetch_point/3 emits hit: false when the valid_time is missing" do
:miss = GridCache.fetch_point(~U[2099-01-01 00:00:00Z], 1.0, 2.0)
assert_receive {:telemetry, @event, %{}, %{hit: false}}
end
test "fetch_point/3 emits hit: false when the point is missing from a cached grid" do
GridCache.put(~U[2026-04-12 12:00:00Z], [%{lat: 32.0, lon: -97.0}])
:miss = GridCache.fetch_point(~U[2026-04-12 12:00:00Z], 40.0, -74.0)
assert_receive {:telemetry, @event, %{}, %{hit: false}}
end
end
describe "latest_valid_time/0" do
test "returns the most recent cached valid_time" do
GridCache.put(~U[2026-04-12 10:00:00Z], [])
GridCache.put(~U[2026-04-12 14:00:00Z], [])
GridCache.put(~U[2026-04-12 12:00:00Z], [])
assert GridCache.latest_valid_time() == ~U[2026-04-12 14:00:00Z]
end
test "returns nil when nothing is cached" do
assert GridCache.latest_valid_time() == nil
end
end
end