Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fail all pending requests during a reset #229

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 25 additions & 9 deletions zigpy_znp/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@
CallbackResponseListener,
)
from zigpy_znp.frames import GeneralFrame
from zigpy_znp.exceptions import CommandNotRecognized, InvalidCommandResponse
from zigpy_znp.exceptions import (
ControllerResetting,
CommandNotRecognized,
InvalidCommandResponse,
)
from zigpy_znp.types.nvids import ExNvIds, OsalNvIds

if typing.TYPE_CHECKING:
Expand Down Expand Up @@ -991,15 +995,27 @@ async def request(
if self._uart is None:
raise RuntimeError("Coordinator is disconnected, cannot send request")

# Immediately send reset requests
ctx = (
contextlib.AsyncExitStack()
if isinstance(request, c.SYS.ResetReq.Req)
else self._sync_request_lock
)
if not isinstance(request, c.SYS.ResetReq.Req):
# We should only be sending one SREQ at a time, according to the spec
send_ctx = self._sync_request_lock
else:
# Immediately send reset requests
send_ctx = contextlib.AsyncExitStack()

# Fail all one-shot listeners on a reset
for header, listeners in self._listeners.items():
# Allow any listeners for a reset indication
if header == c.SYS.ResetInd.Callback.header:
continue

for listener in listeners:
if not isinstance(listener, OneShotResponseListener):
continue

LOGGER.log(log.TRACE, "Failing listener %s on reset", listener)
listener.failure(ControllerResetting())

# We should only be sending one SREQ at a time, according to the spec
async with ctx:
async with send_ctx:
LOGGER.debug("Sending request: %s", request)

# If our request has no response, we cannot wait for one
Expand Down
4 changes: 4 additions & 0 deletions zigpy_znp/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ class CommandNotRecognized(Exception):
pass


class ControllerResetting(Exception):
pass


class InvalidCommandResponse(DeliveryError):
def __init__(self, message, response):
super().__init__(message)
Expand Down
22 changes: 22 additions & 0 deletions zigpy_znp/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@

return self._resolve(response)

def failure(self, exception: BaseException) -> bool:
"""
Implement by subclasses to have the listener enter into an error state.

Return value indicates whether or not the listener is failable.
"""
raise NotImplementedError() # pragma: no cover

def _resolve(self, response: t.CommandBase) -> bool:
"""
Implemented by subclasses to handle matched commands.
Expand Down Expand Up @@ -118,6 +126,13 @@
self.future.set_result(response)
return True

def failure(self, exception: BaseException) -> bool:
if self.future.done():
return False

self.future.set_exception(exception)
return True

def cancel(self):
if not self.future.done():
self.future.cancel()
Expand Down Expand Up @@ -149,6 +164,10 @@
# Callbacks are always resolved
return True

def failure(self, exception: BaseException) -> bool:
# You can't fail a callback
return False

Check warning on line 169 in zigpy_znp/utils.py

View check run for this annotation

Codecov / codecov/patch

zigpy_znp/utils.py#L169

Added line #L169 was not covered by tests

def cancel(self):
# You can't cancel a callback
return False
Expand All @@ -161,6 +180,9 @@

header = object() # sentinel

def failure(self, exception: BaseException) -> bool:
return False

Check warning on line 184 in zigpy_znp/utils.py

View check run for this annotation

Codecov / codecov/patch

zigpy_znp/utils.py#L184

Added line #L184 was not covered by tests

def matches(self, other) -> bool:
return True

Expand Down
8 changes: 7 additions & 1 deletion zigpy_znp/zigbee/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@
import zigpy_znp.commands as c
from zigpy_znp.api import ZNP
from zigpy_znp.utils import combine_concurrent_calls
from zigpy_znp.exceptions import CommandNotRecognized, InvalidCommandResponse
from zigpy_znp.exceptions import (
ControllerResetting,
CommandNotRecognized,
InvalidCommandResponse,
)
from zigpy_znp.types.nvids import OsalNvIds
from zigpy_znp.zigbee.device import ZNPCoordinator

Expand Down Expand Up @@ -687,6 +691,8 @@

try:
await self._znp.request(c.SYS.Ping.Req())
except ControllerResetting:
LOGGER.debug("Controller is resetting, ignoring watchdog failure")

Check warning on line 695 in zigpy_znp/zigbee/application.py

View check run for this annotation

Codecov / codecov/patch

zigpy_znp/zigbee/application.py#L695

Added line #L695 was not covered by tests
except Exception as e:
LOGGER.error(
"Watchdog check failed",
Expand Down