Skip to content

v2.0.0

Choose a tag to compare

@jdwyah jdwyah released this 21 Oct 22:09
· 14 commits to main since this release
fc9a6d3

Breaking changes

  • BREAKING / UPGRADE: init() now blocks, and fails by default when no
    config loads (major, qfg-9p4i.8).
    Spec:
    project/plans/2026-10-05-init-failure-design.md (sections A.3, B, C).
    • init() blocks until the first config loads, retrying (hedged primary +
      secondary, jittered backoff 0.5s, 1s, 2s, ... capped at 30s) for up to
      init_timeout_ms (default 10s). A healthy boot now waits for the first
      config (typically 100-300 ms) before init() returns.
    • on_init_failure takes "fail" (the new default), "wait" or
      "continue":
      • "fail": init() raises QuonfigInitError at the timeout
        (reason="unavailable"), or its subclass QuonfigInitTimeoutError when
        no leg answered at all (reason="timeout"). If every leg rejects the SDK
        key (401/403/404/410 from every configured leg), it raises at once with
        reason="unauthorized".
      • "wait": init() keeps blocking and retrying until a config loads.
      • "continue": init() returns at the timeout; getters serve code
        defaults until a config loads.
    • In 1.x, init() returned at once and the default "raise" raised lazily
      from the first getter. If you relied on init() never raising, either
      catch QuonfigInitError around init() or pass
      on_init_failure="continue".
      In every mode a client you keep after a
      caught QuonfigInitError keeps retrying in the background (SSE / the
      fallback poller) and heals when delivery answers.
    • Getters never block on init. init() is the only gate. A getter that
      runs before the first config (on another thread during init(), or after
      a caught failure) returns the caller's default at once, or applies
      on_no_default when there is none. *_details getters report
      reason="ERROR", error_code="PROVIDER_NOT_READY". Getters no longer
      raise QuonfigInitTimeoutError.
    • The 1.x spellings are deprecated aliases that log one WARN per process:
      "raise" means "fail"; "return" and "return_zero_value" mean
      "continue". An unknown on_init_failure value now raises ValueError
      instead of silently selecting a mode.
    • New readiness API: is_ready() (a config is installed) and
      wait_for_ready(timeout_ms=None) -> bool. ready() stays as an alias of
      is_ready(). Under "wait" or "continue", wire your readiness probe to
      is_ready().
    • New QuonfigInitError base class (with .reason and .leg_errors);
      QuonfigInitTimeoutError now subclasses it, so existing except QuonfigInitTimeoutError blocks still catch the timeout case.
    • Logging follows the cross-SDK contract: a healthy boot logs only DEBUG;
      the first failed attempt logs one WARN, then a WARN every 5s until the
      timeout; the failure logs one ERROR naming each leg's host and error
      (never the SDK key); a client still unready after init logs a WARN every
      30s for 5 minutes, then every 5 minutes; the recovery logs a WARN. Records
      carry event="quonfig.init.<name>" in extra.
    • A forked child's first fetch applies the same on_init_failure. An
      explicit init() in the child raises like a fresh boot; the lazy rebuild
      triggered by a first getter logs the failure and serves defaults instead,
      so a logging filter never raises.
    • Superseded: the 1.x getter-side raise from qfg-9p4i.1 below.
  • Cross-SDK init conformance (qfg-9p4i.17). The init auth set is
    401/403/404/410 from every configured leg, and the unauthorized line reports
    the primary's status. init_timeout_ms <= 0 runs exactly one attempt, then
    applies on_init_failure. A second init() on a started client joins the
    running retry instead of starting a second SSE stream and reporter that
    close() did not stop. The heal line maps an unknown leg to primary, and it
    is logged before wait_for_ready() wakes. URL userinfo is stripped from init
    log lines and the init error. A leg whose body stalls mid-read counts as a
    hang, so an attempt that hung that way raises reason="timeout".
  • Tests: integration-test-data pin bumped to v2026.10.20 (init-failure
    cases); generated tests regenerated.

Fixed

  • The init deadline reason now follows the attempt in flight (qfg-9p4i.17.10,
    Decisions 7 and 10).
    When init_timeout_ms cuts an attempt, that attempt
    decides the reason: timeout only if none of its legs answered. A hung retry
    after a fast 503 now raises QuonfigInitTimeoutError, and a first attempt
    cut while the primary had already failed fast raises reason="unavailable".
    In the backoff gap the last completed attempt still decides, and no new
    attempt starts once the deadline has passed.

  • A fast boot failure now honors on_init_failure="raise" (patch,
    qfg-9p4i.1).
    When every delivery leg failed fast at boot (connection
    refused, DNS failure, 401, 5xx), init latched with an empty store and the
    default on_init_failure="raise" silently served code defaults. Getters now
    raise QuonfigInitTimeoutError (the existing init error) at once, without
    waiting out init_timeout_ms, and keep raising until a config loads. The
    client keeps retrying in the background over SSE / the fallback poller; once
    a config installs, getters serve it. One ERROR is logged naming each leg's
    host and error (never the SDK key). on_init_failure="return" /
    "return_zero_value" is unchanged: it still starts on code defaults.