Skip to content

Commit ae53234

Browse files
committed
feat(notecard): Exclusive() + keep_ready() operation-session guards
Two independent RAII guards an app holds across a group of requests: exclusive() holds the (recursive) request lock so the group is atomic against other threads; keep_ready() holds the RTX/CTX readiness scope so the card is asserted-ready once for the whole group instead of per request. Both are no-ops when their seam is unset. Inner requests nest via the recursive lock / op_depth_ counter. Mirrored on StaticNotecard<Stack, Lock>: ExclusiveSession reuses the StaticNcOpGuard path (zero-cost for NullLock), ReadySession reuses StaticNcTxnOpGuard (gated by NOTE_TXN_HANDSHAKE). AVR baselines unchanged (+0 flash, +0 RAM on all four envs). Tests: threaded exclusive() group test (5 runs, 0 violations, TSan-clean) and keep_ready() coalescing test (start/stop fires once not twice across a two-request group). StaticNotecard smoke tests added for both guards. Full suite 2123/2123 passes.
1 parent b029241 commit ae53234

3 files changed

Lines changed: 488 additions & 0 deletions

File tree

src/note/notecard.hpp

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -963,6 +963,132 @@ class Notecard {
963963
return p;
964964
}
965965

966+
// ── Operation-session guards ──────────────────────────────────────────
967+
//
968+
// Two independent RAII guards an app holds across a GROUP of requests.
969+
//
970+
// exclusive() — holds the recursive request lock for the guard's lifetime
971+
// so a group of requests is atomic against other threads (exclusion only).
972+
// Inner run_operation() calls re-acquire the lock harmlessly (recursive).
973+
// No-op when no request lock is configured.
974+
//
975+
// keep_ready() — holds the RTX/CTX readiness scope (op_depth_ /
976+
// begin_operation / end_operation) for the guard's lifetime so the card
977+
// is asserted-ready once for the whole group instead of per request
978+
// (readiness only). Inner run_operation() calls see op_depth_ > 0 and
979+
// skip begin_operation. When NOTE_TXN_HANDSHAKE is off, returns a trivial
980+
// empty guard with no op_depth_ touch. No-op when no transport is set.
981+
//
982+
// Both guards: non-copyable, non-movable. C++17 guaranteed copy elision
983+
// (NRVO) makes `auto g = nc.exclusive();` work without any copy or move.
984+
//
985+
// The two guards are INDEPENDENT — an app may use either, both, or neither.
986+
// exclusive() = exclusion; keep_ready() = readiness. They compose correctly:
987+
//
988+
// auto ex = nc.exclusive(); // lock the group
989+
// auto kr = nc.keep_ready(); // hold RTX-ready for the group
990+
// nc.execute(req1);
991+
// nc.execute(req2);
992+
993+
/// RAII guard returned by exclusive(). Holds the recursive request lock
994+
/// for its lifetime; non-copyable, non-movable.
995+
struct ExclusiveSession {
996+
ExclusiveSession(const ExclusiveSession&) = delete;
997+
ExclusiveSession& operator=(const ExclusiveSession&) = delete;
998+
ExclusiveSession(ExclusiveSession&&) = delete;
999+
ExclusiveSession& operator=(ExclusiveSession&&) = delete;
1000+
1001+
~ExclusiveSession() {
1002+
if (lock_) lock_->unlock();
1003+
}
1004+
private:
1005+
friend class Notecard;
1006+
explicit ExclusiveSession(IBusLock* lock) : lock_(lock) {
1007+
if (lock_) lock_->lock();
1008+
}
1009+
IBusLock* lock_;
1010+
};
1011+
1012+
/// RAII guard returned by keep_ready(). Holds the RTX/CTX readiness scope
1013+
/// (op_depth_ / begin_operation / end_operation) for its lifetime so the
1014+
/// card is asserted-ready once for the whole group; non-copyable, non-movable.
1015+
/// When NOTE_TXN_HANDSHAKE is off this is a trivial empty struct.
1016+
struct ReadySession {
1017+
ReadySession(const ReadySession&) = delete;
1018+
ReadySession& operator=(const ReadySession&) = delete;
1019+
ReadySession(ReadySession&&) = delete;
1020+
ReadySession& operator=(ReadySession&&) = delete;
1021+
1022+
#if NOTE_TXN_HANDSHAKE
1023+
~ReadySession() {
1024+
if (outermost_ && nc_->transport_) nc_->transport_->end_operation();
1025+
--nc_->op_depth_;
1026+
}
1027+
#else
1028+
~ReadySession() = default;
1029+
#endif
1030+
1031+
private:
1032+
friend class Notecard;
1033+
#if NOTE_TXN_HANDSHAKE
1034+
explicit ReadySession(Notecard* nc)
1035+
: nc_(nc)
1036+
, outermost_(nc_->op_depth_++ == 0)
1037+
{
1038+
if (outermost_ && nc_->transport_)
1039+
nc_->transport_->begin_operation(nc_->default_timeout_ms_);
1040+
}
1041+
Notecard* nc_;
1042+
bool outermost_;
1043+
#else
1044+
explicit ReadySession(Notecard*) {}
1045+
#endif
1046+
};
1047+
1048+
/// Hold the recursive request lock across a group of requests, making the
1049+
/// group atomic against other threads (exclusion only — does NOT affect
1050+
/// the RTX/CTX readiness scope).
1051+
///
1052+
/// The request lock must be set via set_request_lock() and must be
1053+
/// recursive (see set_request_lock docs). When no lock is configured this
1054+
/// is a no-op. Inner run_operation() calls within the guard re-acquire the
1055+
/// lock on the same thread harmlessly (recursive re-entry); other threads
1056+
/// block on the lock for the lifetime of the guard.
1057+
///
1058+
/// auto ex = nc.exclusive(); // lock held here
1059+
/// nc.execute(req1); // inner lock re-acquire: no-op (recursive)
1060+
/// nc.execute(req2); // still inside the exclusive group
1061+
/// // ex destructs → lock released
1062+
///
1063+
/// Note: exclusive() = exclusion only. To hold RTX readiness across the
1064+
/// group as well, combine with keep_ready() (the two are independent).
1065+
[[nodiscard]] ExclusiveSession exclusive() {
1066+
return ExclusiveSession{request_lock_};
1067+
}
1068+
1069+
/// Hold the RTX/CTX readiness scope across a group of requests so the
1070+
/// card is asserted-ready once for the whole group instead of per request
1071+
/// (readiness only — does NOT acquire the request lock).
1072+
///
1073+
/// When NOTE_TXN_HANDSHAKE is enabled, the guard calls begin_operation()
1074+
/// on the outermost entry and end_operation() on destruction. Inner
1075+
/// run_operation() calls see op_depth_ > 0 and skip begin_operation —
1076+
/// the card stays asserted-ready for all requests in the group.
1077+
///
1078+
/// When NOTE_TXN_HANDSHAKE is off (the default for AVR/minimal builds)
1079+
/// this returns a trivial empty guard with no op_depth_ touch.
1080+
///
1081+
/// auto kr = nc.keep_ready(); // begin_operation once (RTX asserted)
1082+
/// nc.execute(req1); // op_depth_ > 0 → no inner begin_operation
1083+
/// nc.execute(req2); // same
1084+
/// // kr destructs → end_operation (RTX released)
1085+
///
1086+
/// Note: keep_ready() = readiness only. To atomically exclude other threads
1087+
/// as well, combine with exclusive() (the two are independent).
1088+
[[nodiscard]] ReadySession keep_ready() {
1089+
return ReadySession{this};
1090+
}
1091+
9661092
/// Operation gate. Acquires the operation lock (if any) before doing
9671093
/// anything else, so that cross-thread exclusion is immediate: a second
9681094
/// thread entering any operation blocks here until the first thread's

src/note/static_notecard.hpp

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,70 @@ class StaticNotecard : private Lock {
406406
timeout_ms);
407407
}
408408

409+
// ── Operation-session guards ──────────────────────────────────────────
410+
//
411+
// Mirror of Notecard::exclusive() / Notecard::keep_ready().
412+
// See the Notecard counterparts for the full usage documentation.
413+
//
414+
// StaticNotecard differences:
415+
// exclusive(): the lock is the EBO base class `Lock`. A `NullLock`
416+
// base produces a zero-cost no-op guard (no lock/unlock calls and
417+
// no stored pointer — the specialization has no members).
418+
// keep_ready(): gated by NOTE_TXN_HANDSHAKE exactly like the
419+
// polymorphic version; trivial empty guard when the flag is off.
420+
421+
/// RAII guard returned by exclusive(). Holds the compile-time lock for
422+
/// its lifetime; non-copyable, non-movable. Zero-cost when Lock=NullLock
423+
/// (the NullLock specialization of StaticNcOpGuard has no members and
424+
/// trivial ctor/dtor, so the entire guard is eliminated by the compiler).
425+
struct ExclusiveSession {
426+
ExclusiveSession(const ExclusiveSession&) = delete;
427+
ExclusiveSession& operator=(const ExclusiveSession&) = delete;
428+
ExclusiveSession(ExclusiveSession&&) = delete;
429+
ExclusiveSession& operator=(ExclusiveSession&&) = delete;
430+
~ExclusiveSession() = default;
431+
432+
private:
433+
friend class StaticNotecard;
434+
using OpGuard = detail::StaticNcOpGuard<Lock>;
435+
OpGuard guard_;
436+
explicit ExclusiveSession(Lock* lock) : guard_(lock) {}
437+
};
438+
439+
/// RAII guard returned by keep_ready(). Holds the RTX/CTX readiness
440+
/// scope for its lifetime; non-copyable, non-movable. Trivial empty
441+
/// guard when NOTE_TXN_HANDSHAKE is off.
442+
struct ReadySession {
443+
ReadySession(const ReadySession&) = delete;
444+
ReadySession& operator=(const ReadySession&) = delete;
445+
ReadySession(ReadySession&&) = delete;
446+
ReadySession& operator=(ReadySession&&) = delete;
447+
~ReadySession() = default;
448+
449+
private:
450+
friend class StaticNotecard;
451+
#if NOTE_TXN_HANDSHAKE
452+
using TxnGuard = detail::StaticNcTxnOpGuard<Stack, Lock>;
453+
TxnGuard guard_;
454+
explicit ReadySession(StaticNotecard& nc) : guard_(nc) {}
455+
#else
456+
explicit ReadySession(StaticNotecard&) {}
457+
#endif
458+
};
459+
460+
/// Hold the compile-time lock across a group of requests (exclusion only).
461+
/// Zero-cost no-op when Lock=NullLock. See Notecard::exclusive() for details.
462+
[[nodiscard]] ExclusiveSession exclusive() {
463+
return ExclusiveSession{static_cast<Lock*>(this)};
464+
}
465+
466+
/// Hold the RTX/CTX readiness scope across a group of requests (readiness
467+
/// only). Zero-cost no-op when NOTE_TXN_HANDSHAKE is off. See
468+
/// Notecard::keep_ready() for details.
469+
[[nodiscard]] ReadySession keep_ready() {
470+
return ReadySession{*this};
471+
}
472+
409473
/// One-shot `echo` connectivity probe. Mirror of `Notecard::ping()` —
410474
/// see the description there for the wire shape, timing, error
411475
/// semantics, and the meaning of `seed_fn`. The two implementations

0 commit comments

Comments
 (0)