Repository navigation
Tear down a removed device after its leave request completes - #1892
zigpy-review-bot wants to merge 1 commit into
Conversation
`_remove_device()` popped the device from the device table once the ZDO leave request finished, but never tore the object down. For a device that is not yet initialized, its own Mgmt_Leave_rsp goes through the uninitialized-device path of `packet_received()`, which re-schedules initialization right before the pop. The orphaned initialize task then keeps sending Node_Desc_req to a device that has already left, and emits `device_init_failure` for a device that is no longer registered. Call `Device.on_remove()` after the leave exchange, so the removed device's tasks and callbacks are cancelled like they are on shutdown and on re-interview swaps. If a distinct object was registered under the same IEEE meanwhile (a re-interview shadow, or an application re-adding the device), it is popped as before and now torn down too. Register `_tasks.discard` instead of `_tasks.remove` as the task done-callback: `on_remove()` clears the set while the cancelled tasks are still completing, and their callbacks raised `KeyError` in the event loop. A re-interview that is in flight while the device is removed no longer registers the device again: `reinterview()` discards the shadow when it is no longer the registered device after discovery (successful or not), tears down the old device whenever it ends up unregistered, and `_device_reinterviewed()` stops before finalizing the shadow when it is no longer registered after the old device was deleted from the database. Callers get `device_reinterview_failure` in that case.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## dev #1892 +/- ##
=======================================
Coverage 99.50% 99.50%
=======================================
Files 59 59
Lines 12415 12433 +18
=======================================
+ Hits 12353 12372 +19
+ Misses 62 61 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
This grew beyond the original two lines in If you would rather review them separately, I can split this into two PRs: this one reduced to the |
Proposed change
_remove_device()popped the device from the device table once the ZDO leave request finished, but never tore theDeviceobject down. For a device that is not yet initialized, its own Mgmt_Leave_rsp goes through the uninitialized-device branch ofpacket_received(), which re-schedules initialization right before the pop. The orphaned initialize task then keeps sending Node_Desc_req to a device that has already left (three attempts of 28 s each ondev, five on older releases) and emitsdevice_init_failurefor a device that is no longer registered. This is the zigpy side of #1802.This PR calls
Device.on_remove()after the leave exchange, so the removed device's tasks and callbacks are cancelled the same way they are on shutdown and on re-interview swaps.on_remove()was never called for a device removed throughremove(), so removed devices also leaked the PollControl check-in listener registered inDevice.__init__. If a distinct object was registered under the same IEEE while the leave request was in flight (a re-interview shadow, or an application re-adding the device from its own database), that object is popped as before and now torn down as well. The task done-callback registered byDevice.create_task()is changed fromset.removetoset.discard, becauseon_remove()clears_taskswhile the cancelled tasks are still completing, and their callbacks then raisedKeyErrorin the event loop (on_remove()is also called on shutdown, so that was reachable before this change too).Related: #1816 adds the same teardown for the original device when a quirk replaces it, and makes the identical
removetodiscardchange inDevice.create_task()(with a different comment), so whichever of the two merges second needs a trivial rebase on that hunk. The_remove_device()change itself does not overlap with it.Removing a device while it is being re-interviewed used to re-register it:
reinterview()restored the old object after a failed discovery, or handed the shadow to_device_reinterviewed()for finalization, regardless of the removal. With the teardown that would have registered a torn-down object, so both paths now check that the shadow is still the registered device (reinterview()after discovery, successful or not, and_device_reinterviewed()after the awaited DB delete) and otherwise discard it, tear down the old device as well (in that orderremove()picks up the shadow, so nothing else would have torn the old object down) and emitdevice_reinterview_failureso callers get a completion signal.Behaviour note for
remove(..., rejoin=True): if the Mgmt_Leave_rsp is lost and the device rejoins and announces within the leave timeout,handle_join()reuses the still-registered object and schedules its initialization, which the teardown now cancels; previously that orphaned initialization could complete after the pop and silently re-register the device through_finalize_device(). The device now comes back through unknown-device discovery on its next packet. zha never passesrejoin=True.Additional information
test_remove_tears_down_device_after_leave_responsereproduces the issue log (uninitialized device,remove(), Mgmt_Leave_rsp fed throughpacket_received()) and asserts no event-loop callback errors;test_remove_tears_down_replacement_devicecovers a distinct object registered during removal;test_reinterview_discards_shadow_when_device_removed,test_reinterview_failure_after_device_removed,test_device_reinterviewed_skips_device_removed_during_swapandtest_device_reinterviewed_keeps_rejoined_device_during_swapcover the re-interview races. All fail ondevwithout the fix. The existing direct callers of_device_reinterviewed()in the tests now register the shadow first, asreinterview()does.Checklist