Skip to content

Fix #36 and #73 (v4.4.5) - #74

Merged
Llucs merged 1 commit into
mainfrom
fix/issue-36-73-v4.4.5
Aug 4, 2026
Merged

Fix #36 and #73 (v4.4.5)#74
Llucs merged 1 commit into
mainfrom
fix/issue-36-73-v4.4.5

Conversation

@Llucs

@Llucs Llucs commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Fixes #36 — ThreadPoolExecutor leak on the exception path when attempt_timeout is set (try/finally around the executor).

Fixes #73 — handler details dict always had empty args/kwargs; real call arguments are now plumbed from every entry point into RetryState/RetryCallState so all handlers receive them.

Version bumped to 4.4.5; CHANGELOG updated. ruff + mypy pass locally.

Summary by CodeRabbit

  • Bug Fixes
    • Retry handlers now preserve original positional and keyword arguments in state details.
    • Prevented thread-pool leaks during retry failures and timeouts.
    • Improved retry behavior across synchronous, asynchronous, generator, caller, decorator, and hedging APIs.
    • Improved compatibility detection for supported typing features.
  • Tests
    • Added regression coverage for argument handling, executor cleanup, and empty asynchronous generators.
  • Chores
    • Updated the package to version 4.4.5.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The retry APIs now propagate original positional and keyword arguments into retry states and handler details. Synchronous timeout executors now shut down on success, timeout, and exception paths. Regression tests cover retry variants, generators, handlers, and executor cleanup.

Retry corrections

Layer / File(s) Summary
Invocation argument propagation
backon/_decorator.py, backon/_hedging.py, backon/_retry/...
Retry decorators, hedging paths, classes, and internal helpers forward the original args and kwargs.
Retry state and executor cleanup
backon/_retry/_loops.py
Retry states store invocation arguments. Timeout executors shut down from a finally block.
Regression and release updates
tests/test_coverage_v4.py, tests/test_issues_36_73.py, backon/_typing.py, pyproject.toml, CHANGELOG.md
Tests cover empty async generators, handler argument details, generator handlers, and executor cleanup. ParamSpec detection, package version, MyPy target, and changelog entries are updated.

Estimated code review effort: 3 (Moderate) | ~25 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The _typing.py compatibility change and MyPy target update are not required by issues #36 or #73. Remove the unrelated typing and MyPy configuration changes, or link them to a separate objective or issue.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the two fixes and the release version addressed by the changes.
Linked Issues check ✅ Passed The changes fix executor cleanup for issue #36 and propagate call arguments for issue #73 across the documented retry APIs.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-36-73-v4.4.5

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Llucs
Llucs force-pushed the fix/issue-36-73-v4.4.5 branch from 9ed3c33 to ce78cee Compare August 3, 2026 22:30
@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@Llucs
Llucs force-pushed the fix/issue-36-73-v4.4.5 branch from ce78cee to 72231e2 Compare August 3, 2026 22:36
@Llucs
Llucs marked this pull request as ready for review August 3, 2026 22:44
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
pyproject.toml (1)

84-84: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep type checking aligned with the minimum supported Python version.

The package declares requires-python = ">=3.9", but Line 84 makes mypy model Python 3.13. This can allow standard-library APIs that fail on Python 3.9.

Set the target to Python 3.9, or add a separate Python 3.9 mypy check in CI. Verify locally with mypy --python-version 3.9 backon/.

Proposed change
-python_version = "3.13"
+python_version = "3.9"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyproject.toml` at line 84, Update the mypy configuration’s python_version
setting from 3.13 to the package’s minimum supported version, 3.9, so type
checking rejects APIs unavailable on Python 3.9. Verify the result with mypy
--python-version 3.9 against the backon package.
tests/test_issues_36_73.py (1)

28-33: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the thread-count polling loop into a helper.

The same six-line polling block appears four times in this class, at lines 28-33, 52-57, 75-80, and 101-106. Extract it into one module-level helper and call it from each test.

Note also that threading.active_count() counts threads for the whole process. If other tests start threads concurrently, the <= baseline assertion can fail for unrelated reasons. The polling window reduces this risk but does not remove it.

♻️ Proposed helper
def _wait_for_thread_baseline(baseline: int, timeout: float = 1.0) -> None:
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        if threading.active_count() <= baseline:
            return
        time.sleep(0.01)

Then each test ends with:

-        deadline = time.monotonic() + 1.0
-        while time.monotonic() < deadline:
-            if threading.active_count() <= baseline:
-                break
-            time.sleep(0.01)
-        assert threading.active_count() <= baseline
+        _wait_for_thread_baseline(baseline)
+        assert threading.active_count() <= baseline
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_issues_36_73.py` around lines 28 - 33, Extract the repeated
thread-count polling logic from the tests in this class into a module-level
_wait_for_thread_baseline helper accepting baseline and optional timeout
parameters. Have it return once threading.active_count() reaches the baseline or
the timeout expires, then replace all four duplicated loops with helper calls
and retain each test’s final baseline assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backon/_typing.py`:
- Around line 9-11: Remove the inline “# pragma: no cover” comment from the
ImportError branch of the ParamSpec import fallback in _typing.py, leaving the
existing import and fallback behavior unchanged.

In `@tests/test_coverage_v4.py`:
- Around line 536-609: Add a per-test execution counter to each empty
async-generator fixture and assert the expected invocation count for all enabled
and disabled on_predicate and on_exception paths, alongside the existing
empty-result assertions. Ensure the enabled on_predicate case reflects whether
empty output should retry; if it must execute only once, adjust the
implementation or predicate configuration and keep the regression assertion
aligned with that behavior.

In `@tests/test_issues_36_73.py`:
- Around line 65-73: Update the test’s RetryingCaller setup to assign the result
of caller.on(ValueError) back to caller, preserving the configured exception
retry condition, and pass interval=0 to backon.constant so all four attempts run
without sleeping. Keep the existing cleanup-path assertions and test structure
unchanged.

---

Nitpick comments:
In `@pyproject.toml`:
- Line 84: Update the mypy configuration’s python_version setting from 3.13 to
the package’s minimum supported version, 3.9, so type checking rejects APIs
unavailable on Python 3.9. Verify the result with mypy --python-version 3.9
against the backon package.

In `@tests/test_issues_36_73.py`:
- Around line 28-33: Extract the repeated thread-count polling logic from the
tests in this class into a module-level _wait_for_thread_baseline helper
accepting baseline and optional timeout parameters. Have it return once
threading.active_count() reaches the baseline or the timeout expires, then
replace all four duplicated loops with helper calls and retain each test’s final
baseline assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5488524f-8cb1-4b42-b85f-b3b2ea74ec97

📥 Commits

Reviewing files that changed from the base of the PR and between ff96a98 and 72231e2.

📒 Files selected for processing (12)
  • CHANGELOG.md
  • backon/_decorator.py
  • backon/_hedging.py
  • backon/_retry/_api.py
  • backon/_retry/_classes.py
  • backon/_retry/_fast.py
  • backon/_retry/_inner.py
  • backon/_retry/_loops.py
  • backon/_typing.py
  • pyproject.toml
  • tests/test_coverage_v4.py
  • tests/test_issues_36_73.py

Comment thread backon/_typing.py
Comment on lines +9 to +11
try:
from typing import ParamSpec
else:
except ImportError: # pragma: no cover

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the new inline comment.

Line 11 adds # pragma: no cover. The backon/**/*.py rules require zero comments.

As per coding guidelines, “Use zero comments.”

Proposed change
-except ImportError:  # pragma: no cover
+except ImportError:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
from typing import ParamSpec
else:
except ImportError: # pragma: no cover
try:
from typing import ParamSpec
except ImportError:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backon/_typing.py` around lines 9 - 11, Remove the inline “# pragma: no
cover” comment from the ImportError branch of the ParamSpec import fallback in
_typing.py, leaving the existing import and fallback behavior unchanged.

Source: Coding guidelines

Comment thread tests/test_coverage_v4.py
Comment thread tests/test_issues_36_73.py
…ndler details args/kwargs

#36: ThreadPoolExecutor leaked worker threads whenever the target raised
an exception (not a timeout) under attempt_timeout, because shutdown()
was only called on the timeout path. _retry_loop_sync now wraps the
executor in try/finally so shutdown(wait=False) runs on every path.

#73: Handler details dicts always had empty args/kwargs because the real
call arguments were never threaded into RetryState/RetryCallState. Args
and 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, so handlers receive the actual call arguments.

Version 4.4.5.
@Llucs
Llucs force-pushed the fix/issue-36-73-v4.4.5 branch from 72231e2 to 41e02e9 Compare August 4, 2026 02:57

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_issues_36_73.py`:
- Around line 10-16: Update _wait_for_thread_baseline to enforce the 100ms
test-time budget by reducing its default timeout below 100ms and using a
correspondingly smaller polling interval; preserve the existing baseline check
and boolean return behavior.
- Around line 46-54: Extend the tests around the existing retry case to cover
both the backon.retry entry point and the hedging entry point with target
positional and keyword arguments. Assert in the resulting callback details that
details["args"] and details["kwargs"] preserve exactly the arguments supplied to
the target.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a1c50527-38ec-4ff0-b790-8f00b1ac61b2

📥 Commits

Reviewing files that changed from the base of the PR and between 72231e2 and 41e02e9.

📒 Files selected for processing (12)
  • CHANGELOG.md
  • backon/_decorator.py
  • backon/_hedging.py
  • backon/_retry/_api.py
  • backon/_retry/_classes.py
  • backon/_retry/_fast.py
  • backon/_retry/_inner.py
  • backon/_retry/_loops.py
  • backon/_typing.py
  • pyproject.toml
  • tests/test_coverage_v4.py
  • tests/test_issues_36_73.py
🚧 Files skipped from review as they are similar to previous changes (11)
  • backon/_typing.py
  • backon/_decorator.py
  • backon/_hedging.py
  • pyproject.toml
  • backon/_retry/_inner.py
  • backon/_retry/_fast.py
  • backon/_retry/_loops.py
  • backon/_retry/_api.py
  • CHANGELOG.md
  • tests/test_coverage_v4.py
  • backon/_retry/_classes.py

Comment on lines +10 to +16
def _wait_for_thread_baseline(baseline, timeout=1.0):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if threading.active_count() <= baseline:
break
time.sleep(0.01)
return threading.active_count() <= baseline

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the cleanup regression within the test-time budget.

At Line 10, timeout=1.0 permits a failing test to wait for one second. This exceeds the required 100ms test budget. Reduce the deadline and polling interval, or observe executor shutdown directly without waiting for thread termination.

As per coding guidelines: “Keep tests under 100ms, use jitter=None and small intervals.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_issues_36_73.py` around lines 10 - 16, Update
_wait_for_thread_baseline to enforce the 100ms test-time budget by reducing its
default timeout below 100ms and using a correspondingly smaller polling
interval; preserve the existing baseline check and boolean return behavior.

Source: Coding guidelines

Comment on lines +46 to +54
backon.retry(
fn,
backon.constant,
exception=ValueError,
max_tries=4,
interval=0,
jitter=None,
attempt_timeout=5.0,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline backon/_retry/_api.py --items all
ast-grep outline backon/_hedging.py --items all

rg -n -C 5 --type py '\b(backon\.)?retry\s*\(|\bhedg\w*\s*\(' tests
rg -n -C 5 --type py 'details\["(args|kwargs)"\]|call_state\.(args|kwargs)' tests

Repository: Llucs/backon

Length of output: 49242


Add functional/hedging regression for target argument propagation.

The decorator coverage here does not exercise backon.retry or the hedging entry point with target positional/keyword arguments, but the argument-propagation change affects backon/_retry/_api.py and backon/_hedging.py. Add coverage that calls those entry points with positional and keyword arguments and asserts details["args"] and details["kwargs"].

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_issues_36_73.py` around lines 46 - 54, Extend the tests around the
existing retry case to cover both the backon.retry entry point and the hedging
entry point with target positional and keyword arguments. Assert in the
resulting callback details that details["args"] and details["kwargs"] preserve
exactly the arguments supplied to the target.

Source: Coding guidelines

@Llucs
Llucs merged commit 7afb17e into main Aug 4, 2026
14 checks passed
@Llucs
Llucs deleted the fix/issue-36-73-v4.4.5 branch August 4, 2026 03:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant