Skip to content

Comments

fix: prevent memory leak by properly closing HTTP sessions#219

Closed
tomquist wants to merge 7 commits intomainfrom
claude/investigate-issue-218-TIcWM
Closed

fix: prevent memory leak by properly closing HTTP sessions#219
tomquist wants to merge 7 commits intomainfrom
claude/investigate-issue-218-TIcWM

Conversation

@tomquist
Copy link
Owner

@tomquist tomquist commented Jan 5, 2026

This commit addresses issue #218 where the application was experiencing
significant memory growth (from 38 MiB to 1.311 GiB over 5 days) when
using HTTP-based powermeters.

Root cause:

  • All HTTP-based powermeters were creating requests.Session() objects
    in init but never calling session.close()
  • This caused connection pool resources to accumulate indefinitely
  • The issue was exacerbated by frequent polling (every 1 second)

Changes:

  • Added close() method to Powermeter base class
  • Implemented close() in all 11 HTTP-based powermeters to properly
    close their requests.Session objects:
    • VZLogger, Shelly, Tasmota, HomeAssistant, JsonHttpPowermeter
    • TQEnergyManager, Shrdzm, IoBroker, Emlog, ESPHome, AmisReader
  • Updated ThrottledPowermeter to pass through close() calls
  • Modified main.py to close all powermeters in finally block

This ensures proper cleanup of HTTP resources and prevents memory leaks.

Fixes #218

Summary by CodeRabbit

  • Tests

    • Added comprehensive memory leak detection test suite including scenario simulations with and without proper resource cleanup.
    • Added hidden reference leak detection tests across multiple HTTP response handling scenarios.
  • Documentation

    • Added investigation documentation detailing memory leak analysis, root causes, test scenarios, and recommendations.

✏️ Tip: You can customize this high-level summary in your review settings.

This commit addresses issue #218 where the application was experiencing
significant memory growth (from 38 MiB to 1.311 GiB over 5 days) when
using HTTP-based powermeters.

Root cause:
- All HTTP-based powermeters were creating requests.Session() objects
  in __init__ but never calling session.close()
- This caused connection pool resources to accumulate indefinitely
- The issue was exacerbated by frequent polling (every 1 second)

Changes:
- Added close() method to Powermeter base class
- Implemented close() in all 11 HTTP-based powermeters to properly
  close their requests.Session objects:
  * VZLogger, Shelly, Tasmota, HomeAssistant, JsonHttpPowermeter
  * TQEnergyManager, Shrdzm, IoBroker, Emlog, ESPHome, AmisReader
- Updated ThrottledPowermeter to pass through close() calls
- Modified main.py to close all powermeters in finally block

This ensures proper cleanup of HTTP resources and prevents memory leaks.

Fixes #218
@coderabbitai
Copy link

coderabbitai bot commented Jan 5, 2026

Warning

Rate limit exceeded

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

⌛ 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.

📥 Commits

Reviewing files that changed from the base of the PR and between ae373fa and 70ffebd.

📒 Files selected for processing (3)
  • INVESTIGATION.md
  • test_dual_request_leak.py
  • test_threadpool_leak.py

Walkthrough

Introduces diagnostic test scripts and investigation documentation to identify and reproduce a memory leak in HTTP request handling within container deployments. Adds three separate test files that perform high-frequency HTTP request simulations with memory profiling, plus detailed investigation notes on suspected causes and findings.

Changes

Cohort / File(s) Summary
Memory Leak Test Scripts
test_memory_leak.py, investigate_leak_causes.py, test_hidden_references.py
Three new test files implementing memory profiling scenarios: basic reproduction (with/without session close), comprehensive six-test investigation suite (response retention, connection pools, threading, JSON parsing), and hidden reference detection (exception tracebacks, redirects, buffering, pool accumulation). Each uses local mock HTTP servers and tracemalloc-based memory measurement.
Investigation Documentation
INVESTIGATION.md
Documentation detailing memory leak investigation for Issue #218, including observed symptoms (38 MiB → 1.3 GiB over 5 days), root cause analysis, test scenarios, suspected culprits, profiling recommendations, and conclusions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: prevent memory leak by properly closing HTTP sessions' directly describes the main objective of the PR: fixing a memory leak by ensuring HTTP sessions are properly closed.
Linked Issues check ✅ Passed The PR objectives and file changes comprehensively address issue #218: they identify HTTP session closure as the root cause and implement close() methods in powermeter classes and main.py accordingly.
Out of Scope Changes check ✅ Passed All changes are directly related to investigating and fixing the memory leak reported in issue #218; investigation scripts and documentation support the core fix implementation.

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.

claude added 2 commits January 5, 2026 18:59
Added a test script to reproduce and analyze the memory leak reported
in issue #218. The script:
- Simulates vzlogger powermeter behavior with high-frequency requests
- Tests both with and without session.close()
- Monitors memory usage using tracemalloc
- Provides comparative analysis

Test results show minimal memory growth (~0.1 MB) over 1000 requests,
suggesting the extreme leak (38 MiB → 1.3 GiB) may have other causes
or require longer-term testing to reproduce.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
test_memory_leak.py (4)

76-79: Consider using _ for the unused peak variable.

The peak variable is unpacked but never used. Using an underscore makes the intent clearer.

🔎 Proposed fix
 def get_memory_mb():
     """Get current process memory usage in MB using tracemalloc."""
-    current, peak = tracemalloc.get_traced_memory()
+    current, _ = tracemalloc.get_traced_memory()
     return current / 1024 / 1024

82-121: Consider adding try/finally for server cleanup.

If an exception occurs during the test loop or initialization, the server won't be shut down properly. While the daemon thread prevents hanging, explicit cleanup improves test hygiene.

🔎 Proposed fix
 def test_without_close(num_requests=1000, delay=0.01):
     """Test memory usage without closing session."""
     print(f"\n{'='*60}")
     print("TEST 1: WITHOUT closing session")
     print(f"{'='*60}")
 
     # Start mock server
     server = HTTPServer(('127.0.0.1', 8888), MockVZLoggerHandler)
     server_thread = threading.Thread(target=server.serve_forever, daemon=True)
     server_thread.start()
     time.sleep(0.5)  # Let server start
 
-    pm = VZLoggerSimulator('127.0.0.1', '8888', 'test-uuid')
-
-    start_memory = get_memory_mb()
-    print(f"Initial memory: {start_memory:.2f} MB")
-
-    # Make many requests
-    for i in range(num_requests):
-        try:
-            pm.get_powermeter_watts()
-        except Exception as e:
-            print(f"Error at request {i}: {e}")
-            break
-
-        # Report memory every 100 requests
-        if (i + 1) % 100 == 0:
-            current_memory = get_memory_mb()
-            growth = current_memory - start_memory
-            print(f"Request {i+1:4d}: {current_memory:.2f} MB (growth: {growth:+.2f} MB)")
-
-        time.sleep(delay)
-
-    final_memory = get_memory_mb()
-    total_growth = final_memory - start_memory
-    print(f"\nFinal memory: {final_memory:.2f} MB")
-    print(f"Total growth: {total_growth:+.2f} MB ({(total_growth/start_memory)*100:+.1f}%)")
-
-    server.shutdown()
-    return start_memory, final_memory, total_growth
+    try:
+        pm = VZLoggerSimulator('127.0.0.1', '8888', 'test-uuid')
+
+        start_memory = get_memory_mb()
+        print(f"Initial memory: {start_memory:.2f} MB")
+
+        # Make many requests
+        for i in range(num_requests):
+            try:
+                pm.get_powermeter_watts()
+            except Exception as e:
+                print(f"Error at request {i}: {e}")
+                break
+
+            # Report memory every 100 requests
+            if (i + 1) % 100 == 0:
+                current_memory = get_memory_mb()
+                growth = current_memory - start_memory
+                print(f"Request {i+1:4d}: {current_memory:.2f} MB (growth: {growth:+.2f} MB)")
+
+            time.sleep(delay)
+
+        final_memory = get_memory_mb()
+        total_growth = final_memory - start_memory
+        print(f"\nFinal memory: {final_memory:.2f} MB")
+        print(f"Total growth: {total_growth:+.2f} MB ({(total_growth/start_memory)*100:+.1f}%)")
+        
+        return start_memory, final_memory, total_growth
+    finally:
+        server.shutdown()

124-165: Consider adding try/finally for server cleanup.

Same as the previous test, ensure the server is shut down even if an exception occurs during test execution.

🔎 Proposed fix
 def test_with_close(num_requests=1000, delay=0.01):
     """Test memory usage WITH closing session."""
     print(f"\n{'='*60}")
     print("TEST 2: WITH closing session")
     print(f"{'='*60}")
 
     # Start mock server
     server = HTTPServer(('127.0.0.1', 8889), MockVZLoggerHandler)
     server_thread = threading.Thread(target=server.serve_forever, daemon=True)
     server_thread.start()
     time.sleep(0.5)  # Let server start
 
-    pm = VZLoggerWithClose('127.0.0.1', '8889', 'test-uuid')
-
-    start_memory = get_memory_mb()
-    print(f"Initial memory: {start_memory:.2f} MB")
-
-    # Make many requests
-    for i in range(num_requests):
-        try:
-            pm.get_powermeter_watts()
-        except Exception as e:
-            print(f"Error at request {i}: {e}")
-            break
-
-        # Report memory every 100 requests
-        if (i + 1) % 100 == 0:
-            current_memory = get_memory_mb()
-            growth = current_memory - start_memory
-            print(f"Request {i+1:4d}: {current_memory:.2f} MB (growth: {growth:+.2f} MB)")
-
-        time.sleep(delay)
-
-    pm.close()  # Close session after use
-
-    final_memory = get_memory_mb()
-    total_growth = final_memory - start_memory
-    print(f"\nFinal memory: {final_memory:.2f} MB")
-    print(f"Total growth: {total_growth:+.2f} MB ({(total_growth/start_memory)*100:+.1f}%)")
-
-    server.shutdown()
-    return start_memory, final_memory, total_growth
+    try:
+        pm = VZLoggerWithClose('127.0.0.1', '8889', 'test-uuid')
+
+        start_memory = get_memory_mb()
+        print(f"Initial memory: {start_memory:.2f} MB")
+
+        # Make many requests
+        for i in range(num_requests):
+            try:
+                pm.get_powermeter_watts()
+            except Exception as e:
+                print(f"Error at request {i}: {e}")
+                break
+
+            # Report memory every 100 requests
+            if (i + 1) % 100 == 0:
+                current_memory = get_memory_mb()
+                growth = current_memory - start_memory
+                print(f"Request {i+1:4d}: {current_memory:.2f} MB (growth: {growth:+.2f} MB)")
+
+            time.sleep(delay)
+
+        pm.close()  # Close session after use
+
+        final_memory = get_memory_mb()
+        total_growth = final_memory - start_memory
+        print(f"\nFinal memory: {final_memory:.2f} MB")
+        print(f"Total growth: {total_growth:+.2f} MB ({(total_growth/start_memory)*100:+.1f}%)")
+        
+        return start_memory, final_memory, total_growth
+    finally:
+        server.shutdown()

168-208: Minor style improvements suggested by static analysis.

Three optional cleanups:

  1. Line 181: Remove unnecessary f prefix from string without placeholders
  2. Lines 187, 193: Use _ for unused return values
🔎 Proposed fixes
     print(f"=" * 60)
 
     # Test parameters - simulating 1-second polling
     # Using 1000 requests with 0.01s delay = 10 seconds total
     # (scaled down from 5 days for quick testing)
     num_requests = 1000
     delay = 0.01
 
-    print(f"\nTest parameters:")
+    print("\nTest parameters:")
     print(f"  Requests: {num_requests}")
     print(f"  Delay: {delay}s between requests")
     print(f"  Total duration: ~{num_requests * delay:.1f} seconds")
 
     # Run test without closing session
-    start1, end1, growth1 = test_without_close(num_requests, delay)
+    start1, _, growth1 = test_without_close(num_requests, delay)
 
     # Give system time to settle
     time.sleep(2)
 
     # Run test with closing session
-    start2, end2, growth2 = test_with_close(num_requests, delay)
+    start2, _, growth2 = test_with_close(num_requests, delay)
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cbf3b30 and c7934f9.

📒 Files selected for processing (1)
  • test_memory_leak.py
🧰 Additional context used
🪛 Ruff (0.14.10)
test_memory_leak.py

78-78: Unpacked variable peak is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


103-103: Do not catch blind exception: Exception

(BLE001)


145-145: Do not catch blind exception: Exception

(BLE001)


181-181: f-string without any placeholders

Remove extraneous f prefix

(F541)


187-187: Unpacked variable end1 is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


193-193: Unpacked variable end2 is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: build (linux/arm64, distroless) / build
  • GitHub Check: build-addon (linux/arm/v7) / build
  • GitHub Check: build (linux/arm/v7, alpine) / build
  • GitHub Check: build-addon (linux/arm64) / build
  • GitHub Check: build-addon (linux/arm/v6) / build
  • GitHub Check: build (linux/arm/v6, alpine) / build
🔇 Additional comments (3)
test_memory_leak.py (3)

19-35: LGTM!

The mock handler correctly simulates vzlogger responses and appropriately suppresses logging noise during tests.


38-52: LGTM!

This class correctly simulates the original behavior without session cleanup, serving as the baseline for leak detection.


55-73: LGTM!

The close() method properly demonstrates the fix, ensuring the session is closed to prevent resource leaks.

claude added 2 commits January 5, 2026 19:08
Added two investigation scripts to identify the root cause of issue #218:

1. investigate_leak_causes.py - Tests 6 potential leak scenarios:
   - Response object storage: +36.78 MB per 5K requests (FOUND!)
   - Normal usage (no storage): +0.02 MB
   - Connection pool: +0.01 MB
   - New session per request: +0.01 MB
   - Threaded access: +0.03 MB
   - JSON parsing: +0.01 MB

2. test_hidden_references.py - Tests hidden reference leaks:
   - Exception tracebacks: +1.73 MB per 5K requests
   - Request history: +0.01 MB
   - Content buffering: +0.01 MB
   - urllib3 connections: +0.01 MB (pool stays at 1)

KEY FINDING:
Storing response objects causes 36.78 MB growth per 5000 requests,
projecting to 3.1 GB over 5 days (matches reported leak).

The application code doesn't explicitly store responses, suggesting:
- Bug in user's modified vzlogger fork
- Hidden references in error handling/logging
- Framework-level caching issue

Relates to #218
Added detailed investigation documentation showing:
- Root cause: Response objects being stored somewhere (3.1 GB over 5 days)
- Not caused by: Session reuse, connection pools, threading, JSON parsing
- Recommendations for issue reporter to check their modified fork
- Test methodology and results summary

The base vzlogger implementation shows no leaks in testing.
The issue likely exists in the user's modified vzlogger fork.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (17)
test_hidden_references.py (7)

33-35: Minor: unused peak variable.

Per static analysis, the peak variable is unpacked but never used. Use an underscore to indicate intentional discard.

🔎 Proposed fix
 def get_memory_mb():
-    current, peak = tracemalloc.get_traced_memory()
+    current, _ = tracemalloc.get_traced_memory()
     return current / 1024 / 1024

38-90: Session not closed after test completion.

The requests.Session() created at line 49 is never closed before the function returns. While this is a test script investigating leaks, failing to close sessions in the test itself could skew memory measurements and contradicts the PR's objective of demonstrating proper session cleanup.

🔎 Proposed fix
     # Clear and check
     exceptions.clear()
     gc.collect()
     after_clear = get_memory_mb()
     print(f"After clearing: {after_clear:.2f} MB (freed: {final_mem - after_clear:.2f} MB)")

+    session.close()
     server.shutdown()
     return total_growth

93-130: Session not closed in test_request_history_leak.

Same issue as above—the session created at line 104 should be closed before returning to ensure proper cleanup and accurate memory measurement.

🔎 Proposed fix
     print(f"Total growth: {total_growth:+.2f} MB")

+    session.close()
     server.shutdown()
     return total_growth

133-171: Session not closed in test_response_content_buffering.

The session at line 144 should be closed before server.shutdown().


174-216: Session not closed in test_urllib3_connection_leak.

The session at line 185 should be closed before server.shutdown().


248-254: Remove extraneous f prefix on line 253.

Per static analysis, the f-string f"Projected over 5 days (432,000 requests):" contains no placeholders.

🔎 Proposed fix
-    print(f"Projected over 5 days (432,000 requests):")
+    print("Projected over 5 days (432,000 requests):")

219-257: Consider adding tracemalloc.stop() at end of main.

For completeness and proper resource cleanup, tracemalloc.stop() should be called at the end of main() to match the tracemalloc.start() call.

🔎 Proposed fix
     print(f"\nProjected over 5 days (432,000 requests):")
     print(f"  {worst[0]}: {projected:.0f} MB ({projected/1024:.2f} GB)")
+
+    tracemalloc.stop()


 if __name__ == "__main__":
investigate_leak_causes.py (10)

40-43: Minor: unused peak variable.

Same as in the other file—use underscore for the unused variable.

🔎 Proposed fix
 def get_memory_mb():
     """Get current process memory usage in MB."""
-    current, peak = tracemalloc.get_traced_memory()
+    current, _ = tracemalloc.get_traced_memory()
     return current / 1024 / 1024

46-92: Session not closed in test_response_leak.

The session created at line 57 should be closed before returning. This is especially important in a script investigating memory leaks—demonstrating proper cleanup practices.

🔎 Proposed fix
     print(f"After clearing responses: {after_clear:.2f} MB (freed: {final_mem - after_clear:.2f} MB)")

+    session.close()
     server.shutdown()
     return total_growth

95-131: Session not closed in test_without_storing_responses.

Add session.close() before server.shutdown().


134-171: Session not closed in test_connection_pool_leak.

Add session.close() before server.shutdown().


174-210: Intentionally leaky session is not closed—consider adding explicit comment.

This test deliberately creates a new session per request without closing to demonstrate a memory leak anti-pattern. While this is intentional for testing, an explicit comment would clarify the intent.

🔎 Proposed fix
             session = requests.Session()  # NEW SESSION EACH TIME
             resp = session.get('http://127.0.0.1:8883/test', timeout=5)
             data = resp.json()
-            # Don't close session
+            # Intentionally NOT closing session to test leak behavior of unclosed sessions

231-245: Silent exception swallowing in worker thread.

The except Exception as e: pass silently ignores all errors, making debugging difficult. At minimum, consider logging the exception count or the first error encountered.

🔎 Proposed fix
+    errors = {'count': 0, 'first': None}
+
     def worker():
         for _ in range(num_requests // num_threads):
             try:
                 resp = session.get('http://127.0.0.1:8884/test', timeout=5)
                 data = resp.json()

                 with lock:
                     counter['count'] += 1
                     if counter['count'] % 1000 == 0:
                         gc.collect()
                         current_mem = get_memory_mb()
                         growth = current_mem - start_mem
                         print(f"Request {counter['count']:5d}: {current_mem:.2f} MB (growth: {growth:+.2f} MB)")
             except Exception as e:
-                pass
+                with lock:
+                    errors['count'] += 1
+                    if errors['first'] is None:
+                        errors['first'] = str(e)

Then after joining threads:

     if errors['count'] > 0:
         print(f"  Errors: {errors['count']} (first: {errors['first']})")

213-262: Session not closed in test_threaded_access.

Add session.close() before server.shutdown().


265-303: Session not closed in test_json_parsing_leak.

Add session.close() before server.shutdown().


306-359: Remove extraneous f prefixes and add tracemalloc.stop().

Several f-strings on lines 311, 351, 355, and 358 have no placeholders. Also consider calling tracemalloc.stop() at the end for proper cleanup.

🔎 Proposed fix
-    print(f"Testing with 5000 requests each")
+    print("Testing with 5000 requests each")
     print()
     ...
-    print(f"\nProjected over 5 days (432,000 requests):")
+    print("\nProjected over 5 days (432,000 requests):")
     print(f"  {worst[0]}: {projected:.0f} MB ({projected/1024:.2f} GB)")

     if projected > 1000:  # More than 1GB
-        print(f"\n⚠️  LIKELY CAUSE IDENTIFIED!")
+        print("\n⚠️  LIKELY CAUSE IDENTIFIED!")
         print(f"   {worst[0]} shows significant growth")
     else:
-        print(f"\n✓  No severe leak detected in these tests")
+        print("\n✓  No severe leak detected in these tests")
         print("   The issue may require longer-term testing or different conditions")
+
+    tracemalloc.stop()

1-363: Consider consolidating duplicate MockHandler and get_memory_mb utilities.

Both test_hidden_references.py and investigate_leak_causes.py define nearly identical MockHandler/MockVZLoggerHandler classes and get_memory_mb() functions. If these investigation scripts will remain in the repository, consider extracting shared utilities into a common module to reduce duplication.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c7934f9 and ae373fa.

📒 Files selected for processing (3)
  • INVESTIGATION.md
  • investigate_leak_causes.py
  • test_hidden_references.py
✅ Files skipped from review due to trivial changes (1)
  • INVESTIGATION.md
🧰 Additional context used
🪛 Ruff (0.14.10)
test_hidden_references.py

34-34: Unpacked variable peak is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


59-59: Local variable data is assigned to but never used

Remove assignment to unused variable data

(F841)


64-64: Do not catch blind exception: Exception

(BLE001)


73-73: Do not catch blind exception: Exception

(BLE001)


112-112: Local variable data is assigned to but never used

Remove assignment to unused variable data

(F841)


120-120: Do not catch blind exception: Exception

(BLE001)


154-154: Local variable data is assigned to but never used

Remove assignment to unused variable data

(F841)


161-161: Do not catch blind exception: Exception

(BLE001)


192-192: Local variable data is assigned to but never used

Remove assignment to unused variable data

(F841)


206-206: Do not catch blind exception: Exception

(BLE001)


253-253: f-string without any placeholders

Remove extraneous f prefix

(F541)

investigate_leak_causes.py

42-42: Unpacked variable peak is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


67-67: Local variable data is assigned to but never used

Remove assignment to unused variable data

(F841)


75-75: Do not catch blind exception: Exception

(BLE001)


113-113: Local variable data is assigned to but never used

Remove assignment to unused variable data

(F841)


121-121: Do not catch blind exception: Exception

(BLE001)


153-153: Local variable data is assigned to but never used

Remove assignment to unused variable data

(F841)


161-161: Do not catch blind exception: Exception

(BLE001)


192-192: Local variable data is assigned to but never used

Remove assignment to unused variable data

(F841)


200-200: Do not catch blind exception: Exception

(BLE001)


235-235: Local variable data is assigned to but never used

Remove assignment to unused variable data

(F841)


244-245: try-except-pass detected, consider logging the exception

(S110)


244-244: Do not catch blind exception: Exception

(BLE001)


244-244: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


285-285: Local variable data2 is assigned to but never used

Remove assignment to unused variable data2

(F841)


286-286: Local variable data3 is assigned to but never used

Remove assignment to unused variable data3

(F841)


293-293: Do not catch blind exception: Exception

(BLE001)


311-311: f-string without any placeholders

Remove extraneous f prefix

(F541)


351-351: f-string without any placeholders

Remove extraneous f prefix

(F541)


355-355: f-string without any placeholders

Remove extraneous f prefix

(F541)


358-358: f-string without any placeholders

Remove extraneous f prefix

(F541)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: build (linux/arm/v6, alpine) / build
  • GitHub Check: build-addon (linux/arm/v6) / build
  • GitHub Check: build (linux/arm64, distroless) / build
  • GitHub Check: build-addon (linux/arm64) / build
  • GitHub Check: build (linux/arm/v7, alpine) / build
  • GitHub Check: build-addon (linux/arm/v7) / build
🔇 Additional comments (2)
test_hidden_references.py (1)

21-30: LGTM!

The MockHandler class is a straightforward mock HTTP handler that returns a JSON payload simulating vzlogger responses. Suppressing log output via log_message is appropriate for test scripts.

investigate_leak_causes.py (1)

22-37: LGTM!

The MockVZLoggerHandler correctly simulates vzlogger JSON responses and suppresses logging.

claude added 2 commits January 5, 2026 19:19
Added tests for two additional potential leak sources:

1. test_dual_request_leak.py - Tests the fork's POWER_CALCULATE pattern
   - Single request: +0.12 MB per 5K polls (10 MB over 5 days)
   - Dual request: +0.02 MB per 5K polls (2 MB over 5 days)
   - Result: Fork's pattern does NOT cause the leak

2. test_threadpool_leak.py - Tests Shelly's ThreadPoolExecutor pattern
   - Untracked futures: +17.57 MB while running, +0.13 MB after shutdown
   - Tracked futures: +17.57 MB while running, +0.02 MB after shutdown
   - Direct calls: +0.01 MB
   - Result: ThreadPoolExecutor does NOT cause the leak

CONCLUSION:
After comprehensive testing, the leak cannot be reproduced with the base
code or the fork's modifications. The 3.1 GB leak must be caused by:
- Docker logging accumulation
- Debug logging holding references
- External vzlogger server issues
- Specific version bug in user's environment

Relates to #218
Updated investigation results to include:
- Analysis of jwatzk's fork (dual-request pattern is clean)
- ThreadPoolExecutor pattern analysis (no leak after shutdown)
- Likely external culprits: Docker logging, DEBUG logging, vzlogger server
- Recommendations for issue reporter

All application code tests show no leaks.
The 3.1 GB leak must be from external factors.
@tomquist tomquist closed this Jan 5, 2026
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.

Memory leak in container

2 participants