Skip to content

fix: reap stale netius HTTP clients from dead threads#85

Closed
joamag wants to merge 1 commit into
masterfrom
fix/netius-client-thread-leak
Closed

fix: reap stale netius HTTP clients from dead threads#85
joamag wants to merge 1 commit into
masterfrom
fix/netius-client-thread-leak

Conversation

@joamag

@joamag joamag commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds _reap_netius_clients() to clean up HTTPClient objects from dead worker threads in the global _netius_clients dict, preventing unbounded memory growth in long-running multi-threaded WSGI applications
  • Reaping runs lazily (only when a new client is about to be created), inside the global lock for thread safety
  • Fixes a TOCTOU race condition where client dict insertion was outside the lock

Test plan

  • test_reap_netius_clients - verifies dead thread entries are removed and cleanup() called, while alive thread entries are preserved
  • test_client_netius_cleanup - verifies all dead entries are fully reaped from an empty-after-reap scenario

🤖 Generated with Claude Code

When WSGI worker threads die and get recycled, old thread IDs with their
HTTPClient objects are never cleaned up from the global _netius_clients
dict, causing unbounded memory growth. This adds a periodic reaper that
runs when creating a new client, cleaning up entries for dead threads.
Also fixes a TOCTOU race on client insertion by moving it inside the lock.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 8, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@joamag has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 13 minutes and 25 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 13 minutes and 25 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b38270a9-6ddf-478c-806a-1ca7907ef3e6

📥 Commits

Reviewing files that changed from the base of the PR and between ee03aab and d92f7ff.

📒 Files selected for processing (2)
  • src/appier/http.py
  • src/appier/test/http.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/netius-client-thread-leak

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 and usage tips.

@joamag joamag marked this pull request as ready for review April 8, 2026 07:15
Copilot AI review requested due to automatic review settings April 8, 2026 07:15

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d92f7ffa8a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/appier/test/http.py
""" The license for the module """

import unittest
import unittest.mock

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restore Python 2-compatible mock import

Importing unittest.mock unconditionally breaks this test module on Python 2.7, where unittest.mock is not available; src/appier/test/http.py will fail at import time before any tests execute in a Python-2 environment. Since the project targets Python 2.7+ compatibility, this should follow the guarded/fallback pattern already used in other tests (for example, src/appier/test/config.py).

Useful? React with 👍 / 👎.

Copilot AI 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.

Pull request overview

This PR addresses unbounded growth of per-thread Netius HTTPClient instances in multi-threaded environments by introducing a stale-client reaper and tightening thread-safety around client registration.

Changes:

  • Add _reap_netius_clients() to remove and cleanup() clients associated with dead threads.
  • Invoke reaping lazily from _client_netius() before creating a new client.
  • Move _netius_clients[tid] insertion under ACCESS_LOCK to avoid a TOCTOU race.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
src/appier/http.py Adds stale-client reaping and locks client registration to prevent leaks/races.
src/appier/test/http.py Adds unit tests covering stale-client reaping and cleanup behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/appier/http.py
Comment on lines +998 to +1002
if not _netius_clients:
return
live_tids = set(t.ident for t in threading.enumerate())
ACCESS_LOCK.acquire()
try:

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

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

live_tids is captured before acquiring ACCESS_LOCK, so another thread can create/insert its client into _netius_clients after the snapshot but before the sweep. That thread’s TID won’t be in live_tids, causing its (live) client to be incorrectly reaped/cleaned up. Compute live_tids while holding ACCESS_LOCK (and ideally also move the empty-check under the lock) so the snapshot + sweep are atomic w.r.t. insertions.

Suggested change
if not _netius_clients:
return
live_tids = set(t.ident for t in threading.enumerate())
ACCESS_LOCK.acquire()
try:
ACCESS_LOCK.acquire()
try:
if not _netius_clients:
return
live_tids = set(t.ident for t in threading.enumerate())

Copilot uses AI. Check for mistakes.
Comment thread src/appier/http.py
dead_tids = [tid for tid in _netius_clients if tid not in live_tids]
for tid in dead_tids:
client = _netius_clients.pop(tid, None)
if not client:

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

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

if not client: can skip cleanup for valid client objects that define falsey truthiness. Since pop(..., None) is used, this should check explicitly for None (e.g., if client is None:) to avoid leaving an uncleaned client removed from the map.

Suggested change
if not client:
if client is None:

Copilot uses AI. Check for mistakes.
Comment thread src/appier/http.py
try:
client.cleanup()
except Exception:
pass

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

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

The except Exception: pass around client.cleanup() suppresses all cleanup failures silently, making resource leaks/debugging much harder in production. Consider at least logging the exception (possibly at debug level) before continuing, or narrowing the caught exception types if there are known benign failures.

Suggested change
pass
logging.getLogger(__name__).debug(
"Failed to cleanup netius client for dead thread %s",
tid,
exc_info=True,
)

Copilot uses AI. Check for mistakes.
Comment thread src/appier/test/http.py
Comment on lines +271 to +273
alive_tid = threading.current_thread().ident
dead_tid = 99999999

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

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

Using a hard-coded dead_tid = 99999999 can collide with a real thread ident on some platforms/runtimes, which would make this test flaky (it would treat the entry as “alive”). Prefer deriving a guaranteed-dead TID at runtime (e.g., compute a value not in {t.ident for t in threading.enumerate()}) so the test is deterministic.

Copilot uses AI. Check for mistakes.
Comment thread src/appier/test/http.py
Comment on lines +295 to +297
dead_client = unittest.mock.MagicMock()
dead_tid = 99999999

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

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

Same issue here: dead_tid = 99999999 is not guaranteed to be absent from threading.enumerate() idents, so the test can become flaky on some systems. Compute a TID that is definitely not in the current live set instead of relying on a magic constant.

Copilot uses AI. Check for mistakes.
@joamag

joamag commented Apr 8, 2026

Copy link
Copy Markdown
Contributor Author

Closing this as won't fix - this does not make sense for the time being

@joamag joamag closed this Apr 8, 2026
@joamag joamag deleted the fix/netius-client-thread-leak branch April 8, 2026 15:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants