Upgrading a host from 0.11 to 0.17

Copy Markdown View Source

This page says what a host changes to move statifier_oban from 0.11.0 to any 0.17.x patch, one minor at a time: 0.11 to 0.12, 0.12 to 0.13, 0.13 to 0.14, 0.14 to 0.15, 0.15 to 0.16 and 0.16 to 0.17. A host here is the code that embeds the package: the StatifierOban.Config it builds, the Oban instance and queues it runs the jobs on, its invoke handlers, any StatifierOban.Timer.Delivery or StatifierOban.Invoke.Delivery module of its own, and the telemetry handlers it attaches. What each release added is in CHANGELOG.md; this page lists only what a host has to do about it, and says NONE where the answer is nothing.

Move the pin with each minor, as the README recommends: {:statifier_oban, "~> 0.17.0"}. The statifier requirement (~> 2.5) and the oban requirement (~> 2.19) are the same on every step of this page. No step here adds a migration: the jobs are Oban's rows, in Oban's table.

0.11 to 0.12

Must change: NONE for a host that does not depend on statifier_persistence. 0.12.0 adds it as an optional dependency, so such a host gains neither the dependency nor the module that needs it.

  • If you depend on statifier_persistence, it must be 0.13.0 or later. The optional requirement is ~> 0.13, and Mix applies an optional dependency's requirement whenever the host brings the package in, so an older pin no longer resolves. 0.13.0 is the first release carrying the StatifierPersistence.PinSource behaviour.

May start doing:

  • Let a chart's pending timers hold it against retirement. A host running both packages can declare a pin source in one line,

    defmodule MyApp.TimerPins do
      use StatifierOban.Timer.PinSource, config: {MyApp, :statifier_oban_config}
    end

    where MyApp.statifier_oban_config/0 returns the host's %StatifierOban.Config{}, and pass MyApp.TimerPins wherever statifier_persistence takes pin sources. It answers %{timers: n}, the timers still pending under the execution ids it is handed. It is for a host that schedules its timers under its durable execution ids; a host that schedules under a live session's id writes its own source instead, as the StatifierOban.Timer.PinSource documentation explains. The README's "Timers as a chart pin source" section has the full picture.

0.12 to 0.13

0.13.0 makes two changes to StatifierOban.Config. Must change: NONE for either. Every new option is optional, and its default does exactly what 0.12.0 does. Both are fixed on the job row when the job is enqueued, so a job stored before the upgrade keeps the old behaviour, and a host that sets them after the upgrade changes only the jobs enqueued from then on.

Run-time bounds

StatifierOban.Config.new/1 takes :invoke_timeout, :child_start_timeout and :timer_timeout, one per job kind: an invoke job, a fan-out child start job and a fired-timer job. Each is a positive integer of milliseconds or :infinity, and each defaults to :infinity, no bound, which is what every job had before. The largest accepted value is 4_294_967_295 for :child_start_timeout and :timer_timeout, the BEAM's largest timeout, and 4_294_962_295 for :invoke_timeout, which leaves room for the invoke worker's 5-second backstop; StatifierOban.Config.new/1 rejects a larger integer, zero, or anything else with {:error, {:invalid_option, key, value}}.

Must change: NONE.

May start doing:

  • Set a bound on a job kind that can hang. An attempt that runs past its bound fails with Oban.TimeoutError and retries while attempts remain, like any other failed attempt. An invoke whose last attempt times out delivers error.communication.invoke.<invoke_id> with reason: "run_crashed", the class a raising handler's last attempt already delivers, so a chart that handles error.communication needs no new transition.
  • Check what your invoke handlers read of their own process before you set :invoke_timeout. Under a finite bound the handler's run/1 or run/2 is called in a task linked to the job process, so self() and the process dictionary are the task's, not the job's. Under the :infinity default no task is started.
  • If you run Oban.Plugins.Lifeline, set its :rescue_after above the largest bound you configure; for invoke jobs that is the bound plus the 5-second margin.

The README's "Bounding how long an attempt takes" section and the StatifierOban.Config documentation carry the detail.

Parking an invoke job whose handler is missing

StatifierOban.Config.new/1 takes :unresolved_handler, :retry or :cancel, and defaults to :retry. Under :retry an invoke job whose handler module does not resolve retries to exhaustion and delivers nothing, exactly as in 0.12.0. Any other value is rejected by StatifierOban.Config.new/1.

Must change: NONE.

May start doing:

  • Set unresolved_handler: :cancel if you deploy handler code in a separate release from the one that enqueues invocations. The job is then cancelled on the attempt that finds the handler missing, and the chart hears about it at once: error.communication.invoke.<invoke_id> with reason: "invalid_handler", detail the handler name as stored, and attempts that attempt's own number. A transition on error.communication can re-enqueue the invocation once the handler is deployed. An unresolvable delivery module still retries under either value.
  • If you turn :cancel on, accept the new failure class wherever you match one exhaustively. Your own StatifierOban.Invoke.Delivery module's deliver_failure/3 (or deliver_failure/4) receives reason: "invalid_handler", and a handler on [:statifier_oban, :invoke, :failed] sees the same reason with handler nil. Under the default :retry neither ever sees it.
  • Upgrade every node that runs the invoke queue before you turn :cancel on. The policy travels in the job's meta, and a node still on 0.12.0 does not read it: a job it picks up retries as before.

The README's "Parking a job whose handler is missing" section and the StatifierOban.Config documentation carry the detail.

0.13 to 0.14

0.14.0 adds one return to an invoke handler's run/1 and run/2. Must change: NONE. The returns a handler already gives mean what they meant in 0.13.0, and nothing in StatifierOban.Config, the job rows, the delivery behaviours or the telemetry events changes.

May start doing:

  • Return :deferred from a handler whose work runs somewhere else. A handler that hands its work on - enqueued on another release's own Oban instance and queue, say - returns :deferred once the hand-off is made. The job completes without delivering, the invocation stays open, and the chart stays in its invoking state. Whoever finishes the work answers it later, by scope and invoke id, through the host's own StatifierOban.Invoke.Delivery implementation, the module the config names as :invoke_delivery: deliver/3 for done.invoke.<invoke_id>, deliver_failure/3 for error.communication.invoke.<invoke_id>. Implement run/2 rather than run/1 for this: its context carries the scope the answer needs.
  • Key the hand-off on the invoke id. A handler's run is at least once, so a re-run of the job must not hand the work on twice; the README's example inserts the other release's job unique on the scope and the invoke id. Under a finite :invoke_timeout the bound covers the hand-off only, not the work handed on.
  • Give the chart its own deadline if the answer may never come. While the answer is outstanding this package does nothing: no job waits, polls or times out on it. A deadline is the chart's own delayed send. Leaving the invoking state cancels the invocation through the ordinary cancel path, and a late answer for it is then dropped; telling the other release to stop is the host's to arrange.

The README's "Enqueue elsewhere, answer later" section and ADR-0009 carry the detail.

0.14 to 0.15

0.15.0 adds one telemetry event, [:statifier_oban, :invoke, :deferred], emitted inside the invoke job when a handler's run/1 or run/2 answers :deferred, and only then. StatifierOban.Telemetry.events/0 lists it, so the list grows from fourteen names to fifteen: the five [:statifier_oban, :timer, kind] names and ten [:statifier_oban, :invoke, kind] names.

Must change: NONE for a host none of whose invoke handlers answers :deferred: the event is never emitted for it. Every other event keeps its name, measurements and metadata.

  • If you attach one telemetry handler to every name in StatifierOban.Telemetry.events/0 and match on the event name with no catch-all clause, add a clause for [:statifier_oban, :invoke, :deferred] before a handler of yours answers :deferred. Its measurements are system_time and attempt, its metadata scope, invoke_id, macrostep, handler, delivery and job_id. Without the clause your telemetry handler raises on the event, and :telemetry detaches a handler that raises, so it stops hearing every other event as well.
  • If you run opentelemetry_statifier's Oban bridge, 0.8.0 is the first bridge that spans the new event. An older bridge attaches to its own list of events, which does not name it, so a deferred invocation gets no span there and nothing fails.

The StatifierOban.Telemetry documentation and telemetry.md carry the detail.

0.15 to 0.16

0.16.0 adds one option to use StatifierOban.Invoke.Handler: max_attempts:. Must change: NONE. A handler that declares no cap enqueues its jobs exactly as in 0.15.0, with the invoke worker's own attempt count, Oban's default of 20.

May start doing:

  • Cap a handler whose work should not be repeated that often with use StatifierOban.Invoke.Handler, max_attempts: n, n a positive integer. The permanent failure, error.communication.invoke.<invoke_id>, is then delivered on the capped attempt, so a chart that handles error.communication needs no new transition. The cap is written onto each job when it is enqueued: jobs stored before the change keep the count they were enqueued with.
  • Write the option as a literal keyword list in the use itself. A positive integer sets the cap and max_attempts: nil declares none; any other value fails the handler's compile. An argument that only holds a keyword list (a module attribute, a variable, a function call) is ignored whole and declares no cap, and so is a misspelt key; neither is an error, so check the handler's jobs carry the cap you meant.
  • If you cap a handler, give the chart its own deadline. A node lost in the middle of the capped attempt leaves the job to Oban.Plugins.Lifeline, which discards a job with no attempts left without running it again and without delivering, so the chart hears nothing from this package, whether or not the work is keyed on invoke_id.

The README's "Capping handler attempts" section and the StatifierOban.Invoke.Handler documentation carry the detail.

0.16 to 0.17

0.17.0 adds one answer to deliver/2 on a StatifierOban.Timer.Delivery implementation: {:snooze, seconds}. 0.17.1 is documentation only; its lib/ is 0.17.0's, and a host changes nothing for it.

Must change: NONE. A delivery that answers :delivered or {:discarded, reason}, or raises, means what it meant in 0.16.0, and the default StatifierOban.Timer.Delivery.Session never answers a snooze. seconds must be a positive integer: zero, a negative count and Oban's period tuples match no clause, so each raises and retries, as every snooze-shaped answer did in 0.16.0.

May start doing:

  • If your Timer.Delivery raises or discards on an execution parked by a chart migration, answer {:snooze, seconds} there instead. Such an execution (statifier_persistence's step/5 answers {:error, {:needs_migration, execution}}) is neither live nor finished. A raise is retried, but each retry spends an attempt: the timer worker sets no max_attempts, so a park that outlasts Oban's default of 20 attempts, twelve to thirteen and a half days under its default backoff, ends the job discarded by Oban, and the timer does not fire after the unpark. A {:discarded, _} answer cancels the timer for good. A snooze reschedules the job at least seconds later, spends no retry, and leaves it a pending timer that StatifierOban.Timer.cancel/3 still reaches and StatifierOban.Timer.pending_for/2 still counts.
  • Choose the period, and keep your own ceiling if you want one. This package counts no snoozes and caps none, so a delivery that snoozes for as long as the park lasts keeps the timer pending that long. A short period re-reads your store often while the park lasts; a long one delays the event after the unpark by up to the period.

The README's "Delivering timers to a durable execution" section and the StatifierOban.Timer.Delivery documentation carry the detail.

Timers days out

Nothing on this page changes what a long timer needs from a host. A timer that waits days is one Oban job row for all of that time; long-timers.md says what such a timer survives, what it does not, and the Oban settings a host must keep for that to hold.