Skip to content

Commit 32a42f7

Browse files
authored
feat: heartbeat control (#205)
* chore: gate heartbeat callback * fix: unit-tests
1 parent e6143df commit 32a42f7

11 files changed

+84
-19
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@ settings.json
1313
CMakeFiles/
1414
CMakeCache.txt
1515
cppcheck_output.txt
16+
17+
.claude/

CMakeLists.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ option(NOTE_C_MEM_CHECK "Run tests with Valgrind." OFF)
2323
option(NOTE_C_NO_LIBC "Build the library without linking against libc, generating errors for any undefined symbols." OFF)
2424
option(NOTE_C_SHOW_MALLOC "Build the library with flags required to log memory usage." OFF)
2525
option(NOTE_C_SINGLE_PRECISION "Use single precision for JSON floating point numbers." OFF)
26+
option(NOTE_C_HEARTBEAT_CALLBACK "Enable heartbeat callback support." OFF)
2627

2728
set(NOTE_C_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR})
2829
add_library(note_c SHARED)
@@ -120,6 +121,14 @@ if(NOTE_C_SINGLE_PRECISION)
120121
)
121122
endif()
122123

124+
if(NOTE_C_HEARTBEAT_CALLBACK)
125+
target_compile_definitions(
126+
note_c
127+
PUBLIC
128+
NOTE_C_HEARTBEAT_CALLBACK
129+
)
130+
endif()
131+
123132
if(NOTE_C_BUILD_TESTS)
124133
# Including CTest here rather than in test/CMakeLists.txt allows us to run
125134
# ctest from the root build directory (e.g. build/ instead of build/test/).

n_hooks.c

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,19 +168,21 @@ NOTE_C_STATIC i2cTransmitFn hookI2CTransmit = NULL;
168168
*/
169169
/**************************************************************************/
170170
NOTE_C_STATIC i2cReceiveFn hookI2CReceive = NULL;
171+
#ifdef NOTE_C_HEARTBEAT_CALLBACK
171172
//**************************************************************************/
172173
/*!
173174
@brief Hook for a heartbeat notification
174175
*/
175176
/**************************************************************************/
176177
NOTE_C_STATIC heartbeatFn hookHeartbeat = NULL;
177178
NOTE_C_STATIC void *hookHeartbeatContext = NULL;
179+
#endif
180+
#ifndef NOTE_NODEBUG
178181
//**************************************************************************/
179182
/*!
180183
@brief Variable used to determine the runtime logging level
181184
*/
182185
/**************************************************************************/
183-
#ifndef NOTE_NODEBUG
184186
NOTE_C_STATIC int noteLogLevel = NOTE_C_LOG_LEVEL;
185187
#endif
186188

@@ -303,6 +305,7 @@ void NoteSetFn(mallocFn mallocHook, freeFn freeHook, delayMsFn delayMsHook,
303305
_UnlockNote();
304306
}
305307

308+
#ifdef NOTE_C_HEARTBEAT_CALLBACK
306309
//**************************************************************************/
307310
/*!
308311
@brief Set the heartbeat function
@@ -317,6 +320,7 @@ void NoteSetFnHeartbeat(heartbeatFn fn, void *context)
317320
hookHeartbeatContext = context;
318321
_UnlockNote();
319322
}
323+
#endif
320324

321325
//**************************************************************************/
322326
/*!
@@ -785,18 +789,22 @@ void NoteUnlockI2C(void)
785789
}
786790
}
787791

792+
#ifdef NOTE_C_HEARTBEAT_CALLBACK
788793
//**************************************************************************/
789794
/*!
790795
@brief Call a heartbeat function if registered
791-
@param heartbeatJson Pointer to null-terminated heartbeat Json string.
796+
@param heartbeatJson Pointer to null-terminated heartbeat JSON string.
797+
@returns `true` if the heartbeat callback wishes to abandon the transaction.
792798
*/
793799
/**************************************************************************/
794-
void _noteHeartbeat(const char *heartbeatJson)
800+
bool _noteHeartbeat(const char *heartbeatJson)
795801
{
796802
if (hookHeartbeat != NULL) {
797-
hookHeartbeat(heartbeatJson, hookHeartbeatContext);
803+
return hookHeartbeat(heartbeatJson, hookHeartbeatContext);
798804
}
805+
return false;
799806
}
807+
#endif
800808

801809
//**************************************************************************/
802810
/*!
@@ -858,6 +866,7 @@ void NoteGetFnDebugOutput(debugOutputFn *fn)
858866
}
859867
}
860868

869+
#ifdef NOTE_C_HEARTBEAT_CALLBACK
861870
/*!
862871
@brief Get the user-defined heartbeat function.
863872
@param fn Pointer to store the heartbeat function pointer.
@@ -872,6 +881,7 @@ void NoteGetFnHeartbeat(heartbeatFn *fn, void **context)
872881
*context = hookHeartbeatContext;
873882
}
874883
}
884+
#endif
875885

876886
/*!
877887
@brief Get the platform-specific transaction functions.

n_lib.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,9 @@ const char *_noteJSONTransaction(const char *request, size_t reqLen, char **resp
150150
const char *_noteChunkedReceive(uint8_t *buffer, uint32_t *size, bool delay, uint32_t timeoutMs, uint32_t *available);
151151
const char *_noteChunkedTransmit(uint8_t *buffer, uint32_t size, bool delay);
152152
bool _noteIsDebugOutputActive(void);
153-
void _noteHeartbeat(const char *heartbeatJson);
153+
#ifdef NOTE_C_HEARTBEAT_CALLBACK
154+
bool _noteHeartbeat(const char *heartbeatJson);
155+
#endif
154156

155157
// Utilities
156158
void _n_htoa32(uint32_t n, char *p);

n_request.c

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -801,7 +801,15 @@ J *_noteTransactionShouldLock(J *req, bool lockNotecard)
801801
_Free(rspJsonStr);
802802
const char * const status = JGetString(rsp, c_status);
803803
NOTE_C_LOG_DEBUG(ERRSTR(status, c_heartbeat));
804-
_noteHeartbeat(status);
804+
#ifdef NOTE_C_HEARTBEAT_CALLBACK
805+
if (_noteHeartbeat(status)) {
806+
errStr = ERRSTR("host abandoned transaction {heartbeat}", c_heartbeat);
807+
NoteResetRequired();
808+
break;
809+
}
810+
#else
811+
(void)status; // avoid unused variable warning when NOTE_C_LOW_MEM defined
812+
#endif
805813
--lastRequestRetries; // Heartbeats do not count against retry limit
806814
continue;
807815
} else if (isIoError || isBadBin) {

note.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,15 +142,19 @@ typedef uint32_t (*getMsFn) (void);
142142

143143
typedef size_t (*debugOutputFn) (const char *text);
144144

145+
#ifdef NOTE_C_HEARTBEAT_CALLBACK
145146
/*!
146147
@typedef heartbeatFn
147148
148149
@brief The type for a heartbeat notification
149150
150151
@param heartbeatJson The heartbeat status received
151152
@param context User context passed to the heartbeat function
153+
154+
@returns `true` if the heartbeat processing should abandon the transaction.
152155
*/
153-
typedef void (*heartbeatFn) (const char *heartbeatJson, void *context);
156+
typedef bool (*heartbeatFn) (const char *heartbeatJson, void *context);
157+
#endif
154158

155159
/*!
156160
@typedef i2cReceiveFn
@@ -302,8 +306,10 @@ bool NoteErrorContains(const char *errstr, const char *errtype);
302306
void NoteErrorClean(char *errbuf);
303307
void NoteSetFnDebugOutput(debugOutputFn fn);
304308
void NoteGetFnDebugOutput(debugOutputFn *fn);
309+
#ifdef NOTE_C_HEARTBEAT_CALLBACK
305310
void NoteSetFnHeartbeat(heartbeatFn fn, void *context);
306311
void NoteGetFnHeartbeat(heartbeatFn *fn, void **context);
312+
#endif
307313
void NoteSetFnTransaction(txnStartFn startFn, txnStopFn stopFn);
308314
void NoteGetFnTransaction(txnStartFn *startFn, txnStopFn *stopFn);
309315
void NoteSetFnMutex(mutexFn lockI2Cfn, mutexFn unlockI2Cfn, mutexFn lockNotefn,

scripts/run_unit_tests.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/bin/bash
22

33
COVERAGE=0
4+
HEARTBEAT_CALLBACK=0
45
MEM_CHECK=0
56
LOW_MEM=0
67
SHOW_MALLOC=0
@@ -11,6 +12,7 @@ VERBOSE=0
1112
while [[ "$#" -gt 0 ]]; do
1213
case $1 in
1314
--coverage) COVERAGE=1 ;;
15+
--heartbeat-callback) HEARTBEAT_CALLBACK=1 ;;
1416
--low-mem) LOW_MEM=1 ;;
1517
--mem-check) MEM_CHECK=1 ;;
1618
--show-malloc) SHOW_MALLOC=1 ;;
@@ -59,6 +61,9 @@ fi
5961
if [[ $SINGLE_PRECISION -eq 1 ]]; then
6062
CMAKE_OPTIONS="${CMAKE_OPTIONS} -DNOTE_C_SINGLE_PRECISION:BOOL=ON"
6163
fi
64+
if [[ $HEARTBEAT_CALLBACK -eq 1 ]]; then
65+
CMAKE_OPTIONS="${CMAKE_OPTIONS} -DNOTE_C_HEARTBEAT_CALLBACK:BOOL=ON"
66+
fi
6267
if [[ $VERBOSE -eq 1 ]]; then
6368
CMAKE_OPTIONS="${CMAKE_OPTIONS} -DCMAKE_VERBOSE_MAKEFILE:BOOL=ON --log-level=VERBOSE"
6469
fi

test/src/NoteGetFnHeartbeat_test.cpp

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,18 @@
1515

1616
#include "n_lib.h"
1717

18+
#ifdef NOTE_C_HEARTBEAT_CALLBACK
19+
1820
namespace
1921
{
2022

2123
static int testContext = 42;
2224

23-
void mockHeartbeatFn(const char *heartbeatJson, void *context)
25+
bool mockHeartbeatFn(const char *heartbeatJson, void *context)
2426
{
2527
(void)heartbeatJson;
2628
(void)context;
29+
return false;
2730
}
2831

2932
SCENARIO("NoteGetFnHeartbeat")
@@ -39,7 +42,7 @@ SCENARIO("NoteGetFnHeartbeat")
3942

4043
WHEN("NoteGetFnHeartbeat is called with NULL function pointer") {
4144
void *retrievedContext = nullptr;
42-
45+
4346
THEN("It should not crash and retrieve context") {
4447
NoteGetFnHeartbeat(NULL, &retrievedContext);
4548
REQUIRE(retrievedContext == &testContext);
@@ -48,7 +51,7 @@ SCENARIO("NoteGetFnHeartbeat")
4851

4952
WHEN("NoteGetFnHeartbeat is called with NULL context pointer") {
5053
heartbeatFn retrievedFn = nullptr;
51-
54+
5255
THEN("It should not crash and retrieve function") {
5356
NoteGetFnHeartbeat(&retrievedFn, NULL);
5457
REQUIRE(retrievedFn == mockHeartbeatFn);
@@ -83,4 +86,6 @@ SCENARIO("NoteGetFnHeartbeat")
8386
}
8487
}
8588

86-
}
89+
}
90+
91+
#endif // NOTE_C_HEARTBEAT_CALLBACK

test/src/NoteSetFnHeartbeat_test.cpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
#include "n_lib.h"
1818

19+
#ifdef NOTE_C_HEARTBEAT_CALLBACK
20+
1921
DEFINE_FFF_GLOBALS
2022
FAKE_VOID_FUNC(_noteLockNote)
2123
FAKE_VOID_FUNC(_noteUnlockNote)
@@ -30,10 +32,11 @@ static int testContext = 42;
3032
static const char *lastHeartbeat = nullptr;
3133
static void *lastContext = nullptr;
3234

33-
void mockHeartbeatFn(const char *heartbeatJson, void *context)
35+
bool mockHeartbeatFn(const char *heartbeatJson, void *context)
3436
{
3537
lastHeartbeat = heartbeatJson;
3638
lastContext = context;
39+
return false;
3740
}
3841

3942
SCENARIO("NoteSetFnHeartbeat")
@@ -99,4 +102,6 @@ SCENARIO("NoteSetFnHeartbeat")
99102
RESET_FAKE(_noteUnlockNote);
100103
}
101104

102-
}
105+
}
106+
107+
#endif // NOTE_C_HEARTBEAT_CALLBACK

test/src/NoteTransaction_test.cpp

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -833,16 +833,18 @@ SCENARIO("NoteTransaction")
833833
JDelete(resp);
834834
}
835835

836+
#ifdef NOTE_C_HEARTBEAT_CALLBACK
836837
SECTION("Heartbeat callback integration - callback invoked on heartbeat response") {
837838
static char receivedHeartbeat[sizeof("testing stsafe")];
838839
static void *receivedContext = nullptr;
839840
static int callbackCount = 0;
840841
static int testContext = 42;
841842

842-
auto heartbeatCallback = [](const char *heartbeatJson, void *context) {
843+
auto heartbeatCallback = [](const char *heartbeatJson, void *context) -> bool {
843844
strlcpy(receivedHeartbeat, heartbeatJson, sizeof(receivedHeartbeat));
844845
receivedContext = context;
845846
callbackCount++;
847+
return false; // Continue processing
846848
};
847849

848850
// Reset callback state
@@ -882,11 +884,12 @@ SCENARIO("NoteTransaction")
882884
static int callbackCount = 0;
883885
static int testContext = 99;
884886

885-
auto heartbeatCallback = [](const char *heartbeatJson, void *context) {
887+
auto heartbeatCallback = [](const char *heartbeatJson, void *context) -> bool {
886888
strlcpy(lastReceivedHeartbeat, heartbeatJson, sizeof(lastReceivedHeartbeat));
887889

888890
lastReceivedContext = context;
889891
callbackCount++;
892+
return false; // Continue processing
890893
};
891894

892895
// Reset callback state
@@ -945,10 +948,11 @@ SCENARIO("NoteTransaction")
945948
static void *receivedContext = reinterpret_cast<void*>(0xDEADBEEF); // Non-null sentinel
946949
static int callbackCount = 0;
947950

948-
auto heartbeatCallback = [](const char *heartbeatJson, void *context) {
951+
auto heartbeatCallback = [](const char *heartbeatJson, void *context) -> bool {
949952
strlcpy(receivedHeartbeat, heartbeatJson, sizeof(receivedHeartbeat));
950953
receivedContext = context;
951954
callbackCount++;
955+
return false; // Continue processing
952956
};
953957

954958
// Reset callback state
@@ -981,6 +985,7 @@ SCENARIO("NoteTransaction")
981985
JDelete(req);
982986
JDelete(resp);
983987
}
988+
#endif // NOTE_C_HEARTBEAT_CALLBACK
984989

985990
RESET_FAKE(_crcAdd);
986991
RESET_FAKE(_crcError);

0 commit comments

Comments
 (0)