Always cancel charge point tasks when stopping - #2013
Conversation
ChargePoint.stop() closed the websocket before cancelling self.tasks, so a close that raised (or was cancelled) skipped cancellation entirely and left monitor_connection running against a connection the charge point no longer owns - duplicate pings and metric writes from an instance that is supposed to be stopped. Move the cancellation into a finally block so it runs on every path, and tolerate self.tasks still being None: stop() is reachable before run() has populated it, where it previously raised TypeError. The close exception still propagates, so callers learn the close failed. Split out of lbbrhzn#2012 at the maintainer's request.
|
Warning Review limit reached
Next review available in: 6 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthrough
ChangesShutdown reliability
Estimated code review effort: 2 (Simple) | ~10 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/test_charge_point_core.py (1)
344-368: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winCover cancelled websocket closure explicitly.
This verifies
OSError, but notasyncio.CancelledError, which is another path protected by the newfinallyblock. Add a test that propagates cancellation while still asserting every task is cancelled.Suggested test
+@pytest.mark.asyncio +async def test_stop_cancels_tasks_when_close_is_cancelled(hass): + cp = _mk_cp(hass) + cp.tasks = [_mk_task(), _mk_task()] + + async def cancelled_close(): + raise asyncio.CancelledError + + cp._connection.state = State.OPEN + cp._connection.close = cancelled_close + + with pytest.raises(asyncio.CancelledError): + await cp.stop() + + assert all(task.cancelled for task in cp.tasks)🤖 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_charge_point_core.py` around lines 344 - 368, Add a sibling async test to test_stop_cancels_tasks_when_close_fails that makes the websocket close callback raise asyncio.CancelledError, asserts cp.stop() propagates that cancellation, and verifies every task in cp.tasks is cancelled afterward. Preserve the existing state setup and unavailable-status assertion as appropriate.
🤖 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.
Nitpick comments:
In `@tests/test_charge_point_core.py`:
- Around line 344-368: Add a sibling async test to
test_stop_cancels_tasks_when_close_fails that makes the websocket close callback
raise asyncio.CancelledError, asserts cp.stop() propagates that cancellation,
and verifies every task in cp.tasks is cancelled afterward. Preserve the
existing state setup and unavailable-status assertion as appropriate.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: b41aca6a-f6d9-4d76-98ca-021256462381
📒 Files selected for processing (2)
custom_components/ocpp/chargepoint.pytests/test_charge_point_core.py
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2013 +/- ##
=======================================
Coverage 95.03% 95.04%
=======================================
Files 12 12
Lines 3004 3005 +1
=======================================
+ Hits 2855 2856 +1
Misses 149 149 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
CancelledError derives from BaseException, so it is the one close failure an "except Exception" guard at a call site cannot catch - only the finally block covers it. Pin that path explicitly. Addresses CodeRabbit review on lbbrhzn#2013.
|
Good catch — CancelledError derives from BaseException, so it's the one close failure an |
…ching number.py: - Add CHARGE_RATE_STEP constant (1.0 A) with measured rationale - Add _quantise_rate() helper: floors to charger's control granularity - Add _last_sent_rate tracking to suppress no-op OCPP round-trips - Boundary values (min/max) always sent regardless of quantisation ocppv16.py: - Add _rate_unit_cache: avoids GetConfiguration on every evcc cycle - Add _applied_limits cache + _invalidate_charging_profile_cache() - Refactor set_charge_rate to use _apply_profile() helper - Pin ChargePointMaxProfile to hardware max, let Tx-purpose profiles modulate - Invalidate cache on BootNotification, StartTransaction, StopTransaction chargepoint.py: kept branch version (already has upstream PR lbbrhzn#2013 finally block) manifest.json: bumped to 1.0.2 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Split out of #2012 — @drc38 asked for the
stop()fix as a separate PR.The problem
ChargePoint.stop()closes the websocket before cancellingself.tasks:If the close raises or is cancelled, the tasks are never cancelled.
self.tasksholdsmonitor_connection(), which loops forever pinging the connection and writing metrics — so a charge point that is supposed to have stopped keeps running against a connection it no longer owns.There are three call sites, and all of them are paths where something has already gone wrong, which is exactly when a close is most likely to fail:
run()'sfinally(chargepoint.py) — reached after the connection droppedreconnect()— the old connection is being torn downCentralSystem.on_connect()— the rebuild path added in Fix OCPP subprotocol negotiation and version handling across reconnects #2012Separately,
self.tasksis initialised toNoneand only populated byrun().stop()is reachable before that, and raisesTypeError: 'NoneType' object is not iterablewhen it is.The fix
Move cancellation into a
finallyand toleratetasksbeingNone:The close exception still propagates, so callers are not deprived of the information that the close failed — only the cancellation is made unconditional.
Tests
Three tests in
tests/test_charge_point_core.py:test_stop_cancels_tasks_when_close_fails— close raisesOSError; asserts the tasks are still cancelled and that theOSErrorstill reaches the callertest_stop_without_tasks_does_not_raise—stop()on a charge point that never rantest_stop_closes_connection_and_cancels_tasks— the normal path still closes and cancelsThe first two were mutation-verified: with the old
stop()restored they fail (TypeError/ uncancelled tasks), and pass with the fix.Note on #2012
CentralSystem.on_connect()currently wrapscharge_point.stop()in atry/exceptthat cancels the stale instance's tasks by hand, precisely because of this bug. Withstop()fixed that inner loop is belt-and-braces — thetry/exceptitself is still needed so a failed close doesn't abort the rebuild. I left it alone to keep this PR to the one change you asked for, but happy to simplify it in a follow-up if you'd prefer.pre-commit run --all-filesand the full test suite pass.Summary by CodeRabbit