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

fix: recovery from missing SendData callback if command may be retried #6343

Merged
merged 1 commit into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions packages/core/src/error/ZWaveError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,11 @@ export function isTransmissionError(e: unknown): e is ZWaveError & {
*
* This explicitly does not include transmission errors.
*/
export function isRecoverableZWaveError(e: unknown): e is ZWaveError {
export function isRecoverableZWaveError(e: unknown): e is ZWaveError & {
code:
| ZWaveErrorCodes.Controller_InterviewRestarted
| ZWaveErrorCodes.Controller_NodeRemoved;
} {
if (!isZWaveError(e)) return false;
switch (e.code) {
case ZWaveErrorCodes.Controller_InterviewRestarted:
Expand All @@ -280,15 +284,21 @@ export function isRecoverableZWaveError(e: unknown): e is ZWaveError {

export function isMissingControllerACK(
e: unknown,
): e is ZWaveError {
): e is ZWaveError & {
code: ZWaveErrorCodes.Controller_Timeout;
context: "ACK";
} {
return isZWaveError(e)
&& e.code === ZWaveErrorCodes.Controller_Timeout
&& e.context === "ACK";
}

export function isMissingControllerCallback(
e: unknown,
): e is ZWaveError {
): e is ZWaveError & {
code: ZWaveErrorCodes.Controller_Timeout;
context: "callback";
} {
return isZWaveError(e)
&& e.code === ZWaveErrorCodes.Controller_Timeout
&& e.context === "callback";
Expand Down
10 changes: 3 additions & 7 deletions packages/zwave-js/src/lib/driver/Driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4673,7 +4673,6 @@ ${handlers.length} left`,
// We got a result - it will be passed to the next iteration
break attemptMessage;
} catch (e: any) {
let delay = 0;
let zwError: ZWaveError;

if (!isZWaveError(e)) {
Expand All @@ -4691,14 +4690,12 @@ ${handlers.length} left`,
});
return;
} else if (
isSendData(msg)
&& e.code === ZWaveErrorCodes.Controller_Timeout
&& e.context === "callback"
isSendData(msg) && isMissingControllerCallback(e)
) {
// If the callback to SendData times out, we need to issue a SendDataAbort
await this.abortSendData();
// Wait a short amount of time so everything can settle
delay = 50;
// Reject the transaction - this will trigger the recovery mechanism and retry the command afterwards
throw e;
} else if (isMissingControllerACK(e)) {
// The controller is unresponsive. Reject the transaction, so we can attempt to recover
throw e;
Expand All @@ -4712,7 +4709,6 @@ ${handlers.length} left`,
)
) {
// Retry the command
if (delay) await wait(delay, true);
continue attemptMessage;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,108 @@ integrationTest(
},
},
);

integrationTest(
"Missing callback recovery works if the command can be retried",
{
// debug: true,

// provisioningDirectory: path.join(
// __dirname,
// "__fixtures/supervision_binary_switch",
// ),

additionalDriverOptions: {
testingHooks: {
skipNodeInterview: true,
},
},

customSetup: async (driver, mockController, mockNode) => {
// This is almost a 1:1 copy of the default behavior, except that the callback never gets sent
const handleBrokenSendData: MockControllerBehavior = {
async onHostMessage(host, controller, msg) {
// If the controller is operating normally, defer to the default behavior
if (!shouldTimeOut) return false;

if (msg instanceof SendDataRequest) {
// Check if this command is legal right now
const state = controller.state.get(
MockControllerStateKeys.CommunicationState,
) as MockControllerCommunicationState | undefined;
if (
state != undefined
&& state !== MockControllerCommunicationState.Idle
) {
throw new Error(
"Received SendDataRequest while not idle",
);
}

// Put the controller into sending state
controller.state.set(
MockControllerStateKeys.CommunicationState,
MockControllerCommunicationState.Sending,
);

// Notify the host that the message was sent
const res = new SendDataResponse(host, {
wasSent: true,
});
await controller.sendToHost(res.serialize());

return true;
} else if (msg instanceof SendDataAbort) {
// Put the controller into idle state
controller.state.set(
MockControllerStateKeys.CommunicationState,
MockControllerCommunicationState.Idle,
);

return true;
}
},
};
mockController.defineBehavior(handleBrokenSendData);

const handleSoftReset: MockControllerBehavior = {
onHostMessage(host, controller, msg) {
// Soft reset should restore normal operation
if (msg instanceof SoftResetRequest) {
shouldTimeOut = false;
return true;
}
},
};
mockController.defineBehavior(handleSoftReset);
},
testBody: async (t, driver, node, mockController, mockNode) => {
// Circumvent the options validation so the test doesn't take forever
driver.options.timeouts.sendDataCallback = 1500;

shouldTimeOut = true;

const firstCommand = node.commandClasses.Basic.set(99);
const followupCommand = node.commandClasses.Basic.set(0);

await wait(2000);

mockController.assertReceivedHostMessage(
(msg) => msg.functionType === FunctionType.SendDataAbort,
);
mockController.clearReceivedHostMessages();

// The stick should have been soft-reset
await wait(1000);
mockController.assertReceivedHostMessage(
(msg) => msg.functionType === FunctionType.SoftReset,
);

// The ping and the followup command should eventually succeed
await firstCommand;
await followupCommand;

t.pass();
},
},
);