Skip to content

Releases: Llucs/backon

v4.4.5

Choose a tag to compare

@Llucs Llucs released this 04 Aug 03:03
7afb17e

4.4.5 - 2026-08-03

  • Fix ThreadPoolExecutor leak on the exception path when attempt_timeout is set (#36). The executor created per attempt was only shut down on the timeout path, leaking worker threads whenever the target raised an exception. _retry_loop_sync now wraps the executor in try/finally so _executor.shutdown(wait=False) runs on every code path (success, timeout, and exception).
  • Fix handler details dict always containing empty args and kwargs (#73). The real call arguments were never threaded into RetryState/RetryCallState, so handlers reading details["args"]/details["kwargs"] (e.g. details["kwargs"]["query_id"]) got ()/{} and raised KeyError. args/kwargs are now plumbed from every entry point (on_exception/on_predicate sync+async, sync/async generators, Retrying.call/async_call, RetryingCaller/AsyncRetryingCaller, hedge) into the retry loop, and RetryState/RetryCallState are constructed with them so every handler (on_attempt, on_backoff, on_giveup, on_success, before, after, before_sleep) receives the actual call arguments.
  • Add regression tests for both issues: executor thread leak (decorator, functional API, RetryingCaller, success path) and handler details args/kwargs (sync+async, generators, Retrying/RetryingCaller/AsyncRetryingCaller, on_backoff/on_giveup/on_success/on_attempt/before/after/before_sleep), including the KeyError reproduction from the issue.

v4.4.4

Choose a tag to compare

@Llucs Llucs released this 01 Aug 12:23
0184fe6

4.4.4 - 2026-07-25

  • Fix hedge() functional API not accepting arguments for the target function (#43). Added args and kw keyword-only parameters to hedge(), _hedge_sync(), and _hedge_async(), forwarding them to the target via _make_hedge_target.
  • Fix fast path silently discarding float return values from condition/giveup (#38). Added _is_custom_wait detection in _retry_fast_sync and _retry_fast_async, so custom wait times are used instead of calling _next_wait().
  • Add tests for both bugs.

v4.4.3

Choose a tag to compare

@Llucs Llucs released this 24 Jul 23:50
d19cdca

4.4.3 - 2026-07-24

  • Remove dead code: 5 unreachable if state.outcome is not None guards in _decide.py, 2 return True dead-code statements in _decorator.py and _helpers.py, 1 always-true if user_handlers is not None guard in _common.py.
  • Add # pragma: no cover to the ParamSpec stub in _typing.py (unreachable on Python 3.13+).
  • Achieve 100% branch coverage across all 20 source files.
  • Remove duplicated tests from test_coverage_gaps.py.

v4.4.2

Choose a tag to compare

@Llucs Llucs released this 22 Jul 16:59
d19cdca

4.4.2 - 2026-07-22

  • Documentation: remove stale Metrics references from README. The metrics instrumentation (MetricsCollector, PrometheusMetrics, OTelMetrics, StructlogMetrics, get_metrics_collector, set_metrics_collector) was removed as dead code in 4.3.0, but the README still listed a [Metrics](#metrics) ToC entry (broken link), advertised "Prometheus / OpenTelemetry / structlog metrics" in the Features list, and included a "Metrics | Prometheus / OTel" row in the migration table. All three references are now gone.
  • Documentation: fix AGENTS.md Release Process step 1, which instructed to bump __version__ in backon/__init__.py. That value is read dynamically from importlib.metadata; only version in pyproject.toml needs bumping. Now consistent with the Versioning section.

Note: mypy python_version stays at 3.10 even though 4.4.0 restored Python 3.9 runtime support. mypy 2.x dropped python_version = "3.9" (minimum supported target is 3.10); 3.9 compatibility is enforced via Union/Optional in module-level aliases, from __future__ import annotations in _decorator.py, and the Python 3.9 entry in the CI matrix.

Validation: All checks pass — ruff, mypy, and 563 tests.

v4.4.1

Choose a tag to compare

@Llucs Llucs released this 21 Jul 22:11

4.4.1 - 2026-07-21

  • Fix raise_on_giveup=False silently swallowing KeyboardInterrupt, SystemExit, and GeneratorExit (#31). All retry loops (fast and slow, sync and async) now use except Exception instead of except BaseException, letting control-flow exceptions propagate naturally.
  • Fix BreakerRetrying.call() and BreakerRetrying.async_call() ignoring half_open_max_calls when the circuit breaker is in HALF_OPEN state (#26). Added the same HALF_OPEN guard that CircuitBreaker.call() uses, preventing unlimited concurrent calls during recovery.
  • Add tests for KeyboardInterrupt/SystemExit/GeneratorExit propagation through retry loops.
  • Add tests for BreakerRetrying HALF_OPEN state enforcement.

Validation: All checks pass — ruff, mypy, and 563 tests.

Release v4.4.0 — Python 3.9 support restored

Choose a tag to compare

@Llucs Llucs released this 21 Jul 19:09

4.4.0 - 2026-07-21

  • Python 3.9 support restored. Minimum Python is now 3.9 (was 3.10).
  • Replace X | Y in module-level type aliases with Union[X, Y] / Optional[X].
  • Add from __future__ import annotations to _decorator.py so X | Y in function signatures is not evaluated at runtime.
  • Add conditional ParamSpec import with a runtime stub for Python 3.9.
  • Unwrap staticmethod objects in on_predicate and on_exception (Python 3.9 doesn't allow calling staticmethod objects directly, fixed in 3.10+).
  • Relax warn_unused_ignores = false in mypy config to support multiple mypy versions safely.
  • Regenerate pdm.lock with target >=3.9.
  • Update CI matrix to include Python 3.9.
  • Update ruff target-version = "py39".

Validation: All checks pass on Python 3.9, 3.10, 3.11, 3.12, 3.13, 3.14 — ruff, mypy, and 551 tests.

v4.3.0

Choose a tag to compare

@Llucs Llucs released this 18 Jul 03:15
  • Breaking: Remove dead metrics instrumentation (MetricsCollector, PrometheusMetrics, OTelMetrics, StructlogMetrics, get_metrics_collector, set_metrics_collector). Never called by retry loops — entirely dead code.
  • Breaking: Remove dead ReadTheDocs config and docs/ directory. RTD was unused and referencing a non-existent docs/conf.py.
  • Fix trio AttemptTimeoutError never caught — unified except trio.TooSlowError / except AttemptTimeoutError into single handler.
  • Fix mutable wait generator singletons (expo, constant, fibo, decay, etc.) sharing internal state across uses. Wrapped in _WaitFactory.
  • Refactor _decorator.py from 843 to 563 lines (‑33%). Extracted _prepare_handlers(), _build_retry_kw(), _make_retry_with().
  • Enable C4 (comprehensions) and RUF (ruff-specific) lint rules.
  • Rewrite CONTRIBUTING.md to reference AGENTS.md as single source of truth for technical rules.
  • Add ruff format --check to the pre-commit checklist in AGENTS.md.

v4.2.5

Choose a tag to compare

@Llucs Llucs released this 17 Jul 18:49
48e7c86

4.2.5 - 2026-07-17

  • Fix _RetryAttempt.__exit__ swallowing KeyboardInterrupt, SystemExit, and GeneratorExit (#39). __exit__ now only suppresses Exception subclasses, letting BaseException subclasses propagate naturally. (codevoyager-ai-dev)

What's Changed

  • [codevoyager] fix: _RetryAttempt.exit no longer suppresses KeyboardInterrupt, SystemExit, GeneratorExit by @codevoyager-ai-dev in #56

New Contributors

Full Changelog: v4.2.4...v4.2.5

v4.2.4

Choose a tag to compare

@Llucs Llucs released this 15 Jul 11:04

4.2.4 - 2026-07-15

  • Fix _maybe_call swallowing legitimate TypeError exceptions from called functions (#48). Previously all TypeError exceptions were caught and silenced; now only argument-mismatch errors cause the fallback to return the callable, while real bugs propagate. (fazalpsinfo-cmyk)
  • Fix _Wait.__call__ not accepting positional arguments (#49). Calling e.g. constant(0.05) or expo(10) no longer raises TypeError. Pre-configured kwargs (e.g. expo(max_value=10)) are preserved when chained with positional args. (fazalpsinfo-cmyk)

v4.2.3

Choose a tag to compare

@Llucs Llucs released this 10 Jul 20:47

Release v4.2.3 consolidates the working-tree progress since v4.1.1: [#19] fast-path contextvars, [#16] hedge crash, [#17] raise_on_giveup ignored in fast path, [#18] "stop triggered on success", [#15] composed wait generators losing kwargs, [#22] assert helpers ignoring their fn, [#20] RetryingCaller/AsyncRetryingCaller dropping most options, [#21] README private imports, [#23] stop_before_delay never stopping the loop + README claims, [#24] AGENTS.md Architecture section outdated.

All 567 tests pass; ruff / mypy clean; coverage 96.11%.


4.2.3 - 2026-07-10

  • Documentation: rewrite the AGENTS.md Architecture section to match the actual repository layout (#24). The previous text pointed AI agents and contributors to backon/_sync.py, backon/_async.py, and a non-existent backon/_retry.py for retry logic; those are now deprecation stubs / non-existent files. The new section documents the _retry/ subpackage (_api.py, _classes.py, _decide.py, _fast.py, _helpers.py, _inner.py, _loops.py) alongside _circuit_breaker.py, _conditions.py, _context.py, _hedging.py, _instrumentation.py, _rate_limiter.py, _state.py, _testing.py, _trio.py, and the py.typed PEP 561 marker, and explicitly calls out _sync.py / _async.py as DeprecationWarning stubs. Notes that tests/ uses feature-oriented naming rather than a one-test_-per-source mirror.

4.2.2 - 2026-07-10

  • Fix stop_before_delay(max_delay) actually stopping the retry loop (#23). _decide_outcome now writes the upcoming wait into state.outcome.wait before invoking the Stop predicate, so elapsed + wait >= max_delay evaluates against the real upcoming wait rather than 0. Verified with a wait-incrementing loop where stop_before_delay(0.3) stops at the 3rd attempt (sum of sleeps < 0.31s). The README description is updated to: "Stop if the upcoming wait would make elapsed + wait >= max_delay".
  • Documentation (#23): the README "Context manager" section now clarifies that Retrying.__exit__/__aexit__ (and the BreakerRetrying/HedgingRetrying twins) are a scoping device and do not re-raise the final exception — use r.statistics after the call. The Rate Limiter section now clarifies that RateLimitError is raised only by direct RateLimiter usage; inside a retry loop exceeding the limit just throttles by sleeping. The migration section adds a note that backon.__version__ is read from importlib.metadata (a pip install backon is required; running from a source checkout without installing returns "0.0.0").
  • The contextvars claim ("thread-safe and async-safe") is now backed by every retry configuration (the fast-path fix landed in #19 / 4.1.4 already covers it), and the operator-composition example is now backed by code (the kwarg-preservation fix landed in #15 / 4.1.5).
  • Add test_stop_before_delay_actually_stops_retry_loop and test_stop_before_delay_predicate_based_actually_stops (tests/test_edge_cases.py) covering the exception-based and predicate-based retry paths.

4.2.1 - 2026-07-10

  • Documentation: replace private-module imports in README examples with the public API (#21). The Circuit Breaker, Hedging, Metrics, Testing Utilities, and auto-detection snippets now use from backon import ... for symbols already in __all__. The _auto_detect_collector private helper was replaced with backon.get_metrics_collector(). The Trio section and migration table now explicitly document backon._trio as the deliberately private, optional-dependency surface (trio helpers are intentionally not re-exported).

4.2.0 - 2026-07-10

  • RetryingCaller and AsyncRetryingCaller now accept and forward the full set of retry options (#20). Previously they silently dropped giveup, predicate, condition, stop, the on_* handlers, retry_error_callback, raise_on_giveup, logger, before/after, and name. The Dynamic Backoff feature (giveup predicate / float return) and the Handlers API are now reachable through the callable caller surface, consistent with Retrying, retry(), and the decorators.
  • Add TestCallerForwardsFullOptions (12 tests) to tests/test_retrying_caller.py covering giveup, predicate-based retry, on_attempt / on_giveup handlers, condition / stop composition objects, retry_error_callback, raise_on_giveup, copy preservation, and the async twins.

4.1.5 - 2026-07-10

  • Fix assert_retried() / assert_not_retried() ignoring their fn argument (#22). Both helpers now wrap the user-supplied fn in a counting spy, drive it through on_exception, and assert the spy was invoked exactly expected_tries (resp. exactly 1) times. assert_retried failing when fn does not actually raise is now detectable instead of silently passing.
  • Add tests/test_assertions.py (7 tests) covering fn invocation, the non-raising-fn failure path, non-ValueError exceptions, and the raising-fn path of assert_not_retried.
  • Fix wait generators losing pre-configured kwargs when composed with +, wait_combine(), or wait_chain() (#15). _Wait now stores its original constructor args/kwargs and __call__ merges them with any caller-supplied kwargs, so pre-configured instances such as backon.expo(base=3) and backon.constant(interval=0.2) preserve their configuration through composition and re-instantiation inside the retry loop.
  • Add TestPreconfiguredKwargsPreserved (7 tests) to tests/test_wait_combine.py covering wait_combine, the + operator, wait_chain, decorator integration, and call-time kwarg override.

4.1.4 - 2026-07-10

  • Fix is_retrying() / get_attempt_number() returning None inside a retry when the fast path was active (#19). The fast paths _retry_fast_sync and _retry_fast_async now enter _retry_context_manager(state.tries) around the target() call, matching the non-fast path. The README's "contextvars — thread-safe and async-safe" guarantee now holds for every retry configuration, not only those that force the slow path.
  • Add TestFastPathRetryContext (5 tests) to tests/test_fast_path.py covering sync, async, functional API, and the outside-retry default.

4.1.3 - 2026-07-10

  • Fix raise_on_giveup=False being silently ignored in the fast path (#17): _retry_fast_sync and _retry_fast_async now thread the raise_on_giveup flag and return None on giveup when it is False, matching the non-fast path (_retry_loop_*).
  • Fix the fast path raising RuntimeError("stop triggered on success") when the stop predicate fires on the same iteration where the target call succeeds but the condition still asks for a retry (#18): the fast path now returns the successful ret in that case, matching _decide_outcome/_retry_loop_* behaviour.
  • Add tests/test_fast_path.py covering both regressions (sync + async, direct _retry_fast_* calls and via decorators).

4.1.2 - 2026-07-10

  • Fix hedge() raising TypeError: 'NoneType' object is not callable whenever the target function raised (issue #16). The hedge helpers now build a default RetryCondition via _make_default_condition(exception, giveup, predicate) before fanning out, mirroring _retry_sync/_retry_async. Hedge futures also honour the final exception by passing raise_on_giveup=True so that a fully-failed hedge surfaces the real error instead of None.
  • Add tests/test_hedging.py with sync/async/decorator/context-manager coverage for hedge(), on_hedge(), and HedgingRetrying (18 tests, 1 skipped pending #18).