Switch to DynamicLifeline for ~30s orphan rescue on deploys

Replace the timer-based Oban.Plugins.Lifeline (45 min rescue_after)
with Oban.Pro.Plugins.DynamicLifeline, which watches producer
records and rescues orphaned jobs within one rescue_interval (30s)
after the owning pod disappears. This cuts PropagationGridWorker
chain recovery from up to 55 min (wait for next hourly cron) down
to ~30s after a rolling deploy kills a mid-flight step. Bump the
worker's max_attempts 3 → 5 so a couple of rescues during a deploy
don't exhaust the chain's retry budget.
This commit is contained in:
Graham McIntire 2026-04-14 16:35:31 -05:00
parent 0a8b5db6fe
commit 1d99efb27c
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
4 changed files with 19 additions and 13 deletions

View file

@ -71,8 +71,9 @@ config :microwaveprop, Oban,
], ],
plugins: [ plugins: [
{Oban.Plugins.Pruner, max_age: 3600 * 24}, {Oban.Plugins.Pruner, max_age: 3600 * 24},
# See runtime.exs — must exceed the worker's timeout/1 callback. # See runtime.exs — producer-record-based orphan rescue, not
{Oban.Plugins.Lifeline, rescue_after: to_timeout(minute: 45)}, # timer-based. Fast recovery from rolling deploys.
{Oban.Pro.Plugins.DynamicLifeline, rescue_interval: to_timeout(second: 30)},
{Oban.Plugins.Cron, {Oban.Plugins.Cron,
crontab: [ crontab: [
{"0 8 * * *", Microwaveprop.Workers.SolarIndexWorker}, {"0 8 * * *", Microwaveprop.Workers.SolarIndexWorker},

View file

@ -86,8 +86,8 @@ config :microwaveprop, Oban,
], ],
plugins: [ plugins: [
{Oban.Plugins.Pruner, max_age: 3600 * 24}, {Oban.Plugins.Pruner, max_age: 3600 * 24},
# See runtime.exs — must exceed the worker's timeout/1 callback. # See runtime.exs — producer-record-based orphan rescue.
{Oban.Plugins.Lifeline, rescue_after: to_timeout(minute: 45)}, {Oban.Pro.Plugins.DynamicLifeline, rescue_interval: to_timeout(second: 30)},
{Oban.Plugins.Cron, {Oban.Plugins.Cron,
crontab: [ crontab: [
{"5 * * * *", Microwaveprop.Workers.PropagationGridWorker}, {"5 * * * *", Microwaveprop.Workers.PropagationGridWorker},

View file

@ -199,14 +199,15 @@ if config_env() == :prod do
], ],
plugins: [ plugins: [
{Oban.Plugins.Pruner, max_age: 3600 * 24}, {Oban.Plugins.Pruner, max_age: 3600 * 24},
# Lifeline rescues jobs stuck in `executing` after this window. # DynamicLifeline uses producer records (heartbeats from each
# MUST be larger than the longest `timeout/1` callback on any worker # live node) to rescue orphans, not a timer. When a pod dies
# or Lifeline races the job's own deadline. The chain-style # mid-deploy its Producer row disappears and any in-flight job
# PropagationGridWorker caps a single forecast-hour step at 20 # with that producer in `attempted_by` gets moved back to
# min and real steps run ~8-10 min, so 45 min gives comfortable # `available` on the next rescue cycle — within ~30s instead of
# headroom for a slow step without letting a truly stuck job # waiting for a timer-based `rescue_after` to expire. Critical
# linger for hours before the safety net trips. # for PropagationGridWorker chain steps to recover from rolling
{Oban.Plugins.Lifeline, rescue_after: to_timeout(minute: 45)}, # deploys before the next hourly cron fire.
{Oban.Pro.Plugins.DynamicLifeline, rescue_interval: to_timeout(second: 30)},
{Oban.Plugins.Cron, {Oban.Plugins.Cron,
crontab: [ crontab: [
{"0 8 * * *", Microwaveprop.Workers.SolarIndexWorker}, {"0 8 * * *", Microwaveprop.Workers.SolarIndexWorker},

View file

@ -20,7 +20,11 @@ defmodule Microwaveprop.Workers.PropagationGridWorker do
use Oban.Worker, use Oban.Worker,
queue: :propagation, queue: :propagation,
max_attempts: 3 # Higher than the default 3 so a few DynamicLifeline rescues
# (e.g., a rolling deploy that kills a mid-flight chain step)
# don't exhaust the chain's retry budget and discard the whole
# run. Legitimate scoring errors still give up after 5 attempts.
max_attempts: 5
alias Microwaveprop.Commercial alias Microwaveprop.Commercial
alias Microwaveprop.Propagation alias Microwaveprop.Propagation