Skip to content

Commit

Permalink
fix: when sending unsupported requests to a notecard, don't wastefull…
Browse files Browse the repository at this point in the history
…y retry (#142)

* fix: when sending unsupported requests to a notecard, don't wastefully retry

Notecards of all types, when they get an unrecognized request, return both {io} and {not-supported} in their err field.

This is because the notecard has no idea whether the request type is corrupted because of bad I/O, or because it is not supported.

As such, when a host app innocently does a card.wireless expecting to get a {not-supported} error, it takes ten seconds or more to get the failure because it is retrying it over and over because of the {io}.

Although non-zero, the reality is that there is lower chance of it being an {io} error if in fact {not-supported} is found.

As such, I've opted to not do retries if that specific error comes back.

* Tweak Ray's changes to handle "not supported" errors.

- Fix a compilation error (missing parenthesis).
- Add unit tests for NoteRequestResponseWithRetry. I had these sitting on my
machine locally and never pushed them. I added a test that checks the behavior
around "not supported" errors.
- Add a unit test to NoteTransaction_test.cpp around "not supported" errors.

* astlye

---------

Co-authored-by: Hayden Roche <[email protected]>
  • Loading branch information
rayozzie and haydenroche5 authored Apr 1, 2024
1 parent 89f7c70 commit f011ab2
Show file tree
Hide file tree
Showing 6 changed files with 277 additions and 12 deletions.
1 change: 1 addition & 0 deletions n_const.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ const char *c_cmd = "cmd";
const char *c_bad = "bad";
const char *c_iobad = "bad {io}";
const char *c_ioerr = "{io}";
const char *c_unsupported = "{not-supported}";
const char *c_badbinerr = "{bad-bin}";
3 changes: 3 additions & 0 deletions n_lib.h
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@ extern const char *c_iobad;
extern const char *c_ioerr;
#define c_ioerr_len 4

extern const char *c_unsupported;
#define c_unsupported_len 15

extern const char *c_badbinerr;
#define c_badbinerr_len 9

Expand Down
10 changes: 3 additions & 7 deletions n_request.c
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ J *NoteRequestResponseWithRetry(J *req, uint32_t timeoutSeconds)
rsp = NoteTransaction(req);

// Loop if there is no response, or if there is an io error
if ( (rsp == NULL) || JContainsString(rsp, c_err, c_ioerr)) {
if ((rsp == NULL) || (JContainsString(rsp, c_err, c_ioerr) && !JContainsString(rsp, c_err, c_unsupported))) {

// Free error response
if (rsp != NULL) {
Expand All @@ -278,10 +278,6 @@ J *NoteRequestResponseWithRetry(J *req, uint32_t timeoutSeconds)
// Free the request
JDelete(req);

if (rsp == NULL) {
return NULL;
}

// Return the response
return rsp;
}
Expand Down Expand Up @@ -637,8 +633,8 @@ J *noteTransactionShouldLock(J *req, bool lockNotecard)
bool isBadBin = false;
bool isIoError = false;
if (rsp != NULL) {
isBadBin = NoteErrorContains(JGetString(rsp, c_err), c_badbinerr);
isIoError = NoteErrorContains(JGetString(rsp, c_err), c_ioerr);
isBadBin = JContainsString(rsp, c_err, c_badbinerr);
isIoError = JContainsString(rsp, c_err, c_ioerr) && !JContainsString(rsp, c_err, c_unsupported);
} else {
// Failed to parse response as JSON
if (responseJSON == NULL) {
Expand Down
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ endmacro(add_test)
add_test(NoteTransaction_test)
add_test(NoteRequest_test)
add_test(NoteRequestResponse_test)
add_test(NoteRequestResponseWithRetry_test)
add_test(NoteRequestResponseJSON_test)
add_test(NoteRequestWithRetry_test)
add_test(NoteReset_test)
Expand Down
229 changes: 229 additions & 0 deletions test/src/NoteRequestResponseWithRetry_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
/*!
* @file NoteRequestResponseWithRetry_test.cpp
*
* Written by the Blues Inc. team.
*
* Copyright (c) 2024 Blues Inc. MIT License. Use of this source code is
* governed by licenses granted by the copyright holder including that found in
* the
* <a href="https://github.com/blues/note-c/blob/master/LICENSE">LICENSE</a>
* file.
*
*/

#include <catch2/catch_test_macros.hpp>
#include "fff.h"

#include "n_lib.h"

DEFINE_FFF_GLOBALS
FAKE_VALUE_FUNC(J *, NoteTransaction, J *)
FAKE_VALUE_FUNC(uint32_t, NoteGetMs)

namespace
{

J *NoteTransactionIOError(J *)
{
J *resp = JCreateObject();
assert(resp != NULL);
JAddStringToObject(resp, c_err, c_ioerr);

return resp;
}

J *NoteTransactionNotSupportedError(J *)
{
J *resp = JCreateObject();
assert(resp != NULL);
JAddStringToObject(resp, c_err, c_unsupported);

return resp;
}

J *NoteTransactionIOAndNotSupportedErrors(J *)
{
J *resp = JCreateObject();
assert(resp != NULL);
JAddStringToObject(resp, c_err, "{io} {not-supported}");

return resp;
}

J *NoteTransactionOtherError(J *)
{
J *resp = JCreateObject();
assert(resp != NULL);
JAddStringToObject(resp, c_err, c_bad);

return resp;
}

TEST_CASE("NoteRequestResponseWithRetry")
{
NoteSetFnDefault(malloc, free, NULL, NULL);

J *rsp = NULL;

GIVEN("A NULL request") {
J *req = NULL;

WHEN("NoteRequestResponseWithRetry is called") {
rsp = NoteRequestResponseWithRetry(req, 0);

THEN("The returned response is NULL") {
CHECK(rsp == NULL);
}
}
}

GIVEN("A valid request") {
J *req = NoteNewRequest("note.add");
REQUIRE(req != NULL);

AND_GIVEN("A timeout of 5 seconds") {
const uint32_t timeoutSec = 5;

AND_GIVEN("The timeout will trigger after 1 retry") {
uint32_t getMsReturnVals[3] = {0, 3000, 6000};
SET_RETURN_SEQ(NoteGetMs, getMsReturnVals, 3);

AND_GIVEN("The response to NoteTransaction is NULL") {
NoteTransaction_fake.return_val = NULL;

WHEN("NoteRequestResponseWithRetry is called") {
rsp = NoteRequestResponseWithRetry(req, timeoutSec);

THEN("The returned response is NULL") {
CHECK(rsp == NULL);
}

THEN("NoteTransaction will be called twice (1 retry)") {
CHECK(NoteTransaction_fake.call_count == 2);
}
}
}

AND_GIVEN("The response to NoteTransaction contains an I/O "
"error") {
NoteTransaction_fake.custom_fake = NoteTransactionIOError;

WHEN("NoteRequestResponseWithRetry is called") {
rsp = NoteRequestResponseWithRetry(req, timeoutSec);

THEN("The returned response is NULL") {
CHECK(rsp == NULL);
}

THEN("NoteTransaction will be called twice (1 "
"retry)") {
CHECK(NoteTransaction_fake.call_count == 2);
}
}
}

AND_GIVEN("The response to NoteTransaction contains a not "
"supported error") {
NoteTransaction_fake.custom_fake = NoteTransactionNotSupportedError;

WHEN("NoteRequestResponseWithRetry is called") {
rsp = NoteRequestResponseWithRetry(req, timeoutSec);

THEN("The returned response has a not-supported error") {
CHECK(JContainsString(rsp, c_err, c_unsupported));
}

THEN("NoteTransaction is only called once (no retries)") {
CHECK(NoteTransaction_fake.call_count == 1);
}
}
}

AND_GIVEN("The response to NoteTransaction contains a not "
"supported error AND an I/O error") {
NoteTransaction_fake.custom_fake = NoteTransactionIOAndNotSupportedErrors;

WHEN("NoteRequestResponseWithRetry is called") {
rsp = NoteRequestResponseWithRetry(req, timeoutSec);

THEN("The returned response has a not-supported "
"error") {
CHECK(JContainsString(rsp, c_err, c_unsupported));
}

THEN("The returned response has an I/O error") {
CHECK(JContainsString(rsp, c_err, c_ioerr));
}

THEN("NoteTransaction is only called once (no "
"retries)") {
CHECK(NoteTransaction_fake.call_count == 1);
}
}
}

AND_GIVEN("The response to NoteTransaction contains an error "
"that isn't I/O or \"not supported\"") {
NoteTransaction_fake.custom_fake = NoteTransactionOtherError;

WHEN("NoteRequestResponseWithRetry is called") {
rsp = NoteRequestResponseWithRetry(req, timeoutSec);

THEN("The returned response has the error") {
CHECK(JContainsString(rsp, c_err, c_bad));
}

THEN("NoteTransaction is only called once (no "
"retries)") {
CHECK(NoteTransaction_fake.call_count == 1);
}
}
}

AND_GIVEN("There's a valid response on the first "
" NoteTransaction attempt") {
NoteTransaction_fake.return_val = JCreateObject();

WHEN("NoteRequestResponseWithRetry is called") {
rsp = NoteRequestResponseWithRetry(req, timeoutSec);

THEN("The returned response is non-NULL") {
CHECK(rsp != NULL);
}

THEN("NoteTranaction is only called once (no "
"retries)") {
CHECK(NoteTransaction_fake.call_count == 1);
}
}
}

AND_GIVEN("There's a valid response on the second "
"NoteTransaction attempt") {
J *noteTransactionReturnVals[2] = {NULL, JCreateObject()};
SET_RETURN_SEQ(NoteTransaction, noteTransactionReturnVals,
2);

WHEN("NoteRequestResponseWithRetry is called") {
rsp = NoteRequestResponseWithRetry(req, timeoutSec);

THEN("The returned response is non-NULL") {
CHECK(rsp != NULL);
}

THEN("NoteTranaction is only called twice (1 retry)") {
CHECK(NoteTransaction_fake.call_count == 2);
}
}
}
}
}
}

JDelete(rsp);

RESET_FAKE(NoteTransaction);
RESET_FAKE(NoteGetMs);
}

}
45 changes: 40 additions & 5 deletions test/src/NoteTransaction_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,14 @@
*
* Written by the Blues Inc. team.
*
* Copyright (c) 2023 Blues Inc. MIT License. Use of this source code is
* Copyright (c) 2024 Blues Inc. MIT License. Use of this source code is
* governed by licenses granted by the copyright holder including that found in
* the
* <a href="https://github.com/blues/note-c/blob/master/LICENSE">LICENSE</a>
* file.
*
*/



#include <catch2/catch_test_macros.hpp>
#include "fff.h"

Expand Down Expand Up @@ -68,6 +66,19 @@ const char *noteJSONTransactionIOError(const char *, size_t, char **resp, uint32
return NULL;
}

const char *noteJSONTransactionNotSupportedError(const char *, size_t, char **resp, uint32_t)
{
static char respString[] = "{\"err\": \"{not-supported}\"}";

if (resp) {
char* respBuf = reinterpret_cast<char *>(malloc(sizeof(respString)));
memcpy(respBuf, respString, sizeof(respString));
*resp = respBuf;
}

return NULL;
}

SCENARIO("NoteTransaction")
{
NoteSetFnDefault(malloc, free, NULL, NULL);
Expand Down Expand Up @@ -214,6 +225,32 @@ SCENARIO("NoteTransaction")
JDelete(resp);
}

GIVEN("A valid request") {
J *req = NoteNewRequest("note.add");
REQUIRE(req != NULL);

AND_GIVEN("noteJSONTransaction returns a response with a \"not "
"supported\" error") {
noteJSONTransaction_fake.custom_fake = noteJSONTransactionNotSupportedError;

WHEN("NoteTransaction is called") {
J *rsp = NoteTransaction(req);

THEN("The response contains the \"not supported\" error") {
CHECK(JContainsString(rsp, c_err, c_unsupported));
}

THEN("noteJSONTransaction is only called once (no retries)") {
CHECK(noteJSONTransaction_fake.call_count == 1);
}

JDelete(rsp);
}
}

JDelete(req);
}

SECTION("A reset is required and it fails") {
J *req = NoteNewRequest("note.add");
REQUIRE(req != NULL);
Expand Down Expand Up @@ -407,5 +444,3 @@ SCENARIO("NoteTransaction")
}

}


0 comments on commit f011ab2

Please sign in to comment.