Skip to content

Commit

Permalink
Fail all pending requests during a reset
Browse files Browse the repository at this point in the history
  • Loading branch information
puddly committed Oct 15, 2023
1 parent fc4a131 commit ea87e1c
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 10 deletions.
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 @@ def resolve(self, response: t.CommandBase) -> bool:

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 @@ def _resolve(self, response: t.CommandBase) -> bool:
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 @@ def _resolve(self, response: t.CommandBase) -> bool:
# Callbacks are always resolved
return True

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

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

header = object() # sentinel

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

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 @@ async def _watchdog_loop(self):

try:
await self._znp.request(c.SYS.Ping.Req())
except ControllerResetting:
LOGGER.debug("Controller is resetting, ignoring watchdog failure")
except Exception as e:
LOGGER.error(
"Watchdog check failed",
Expand Down

0 comments on commit ea87e1c

Please sign in to comment.