secsi: T3 / T4 enforcement moved into the FSM
The SECS-I Protocol FSM now enforces T3 (reply timeout) and T4
(inter-block timeout) directly, instead of leaving them as
upper-layer hooks.
T3: on complete_send, if the block we just acked had W=1, record its
system_bytes in awaiting_reply_sys_ and emit ActionStartTimer{T3}.
deliver_recv cancels T3 when a block arrives whose system_bytes
match the outstanding request. EventTimeout{T3} aborts the FSM with
"T3 reply timeout".
T4: deliver_recv emits ActionStartTimer{T4} whenever the delivered
block has end_block=false. The next block's deliver_recv cancels
the timer; EventTimeout{T4} aborts with "T4 inter-block timeout".
abort() now also cancels T3/T4 and clears the tracking state.
Test changes:
- Old "T3/T4 are FSM-level no-ops" test → REPLACED by four new
tests: T3 arm+expire, T3 arm+matching-reply cancels, T4
arm+expire, T4 arm+next-block cancels.
- Two new observer accessors on Protocol (awaiting_reply,
awaiting_next_block) so the tests can assert tracking state
without poking internals.
COMPLIANCE.md §1a: T3 + T4 rows go ⬜ → ✅.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+48
-1
@@ -40,6 +40,17 @@ void Protocol::retry_send(std::vector<Action>& out) {
|
||||
|
||||
void Protocol::complete_send(std::vector<Action>& out) {
|
||||
out.push_back(ActionCancelTimer{Timer::T2});
|
||||
// If the block we just sent expected a reply (W=1 primary), record the
|
||||
// outstanding system_bytes and arm T3. Single-pending model — SECS-I
|
||||
// is half-duplex on a serial link so multiple in-flight primaries are
|
||||
// structurally rare.
|
||||
if (!send_queue_.empty()) {
|
||||
const auto& sent = send_queue_.front();
|
||||
if (sent.header.w_bit) {
|
||||
awaiting_reply_sys_ = sent.header.system_bytes;
|
||||
out.push_back(ActionStartTimer{Timer::T3});
|
||||
}
|
||||
}
|
||||
send_queue_.pop_front();
|
||||
send_block_bytes_.clear();
|
||||
state_ = State::Idle;
|
||||
@@ -52,6 +63,25 @@ void Protocol::deliver_recv(std::vector<Action>& out) {
|
||||
std::size_t consumed = 0;
|
||||
Block b = Block::decode(recv_buf_.data(), recv_buf_.size(), consumed);
|
||||
out.push_back(ActionTransmit{{kACK}});
|
||||
|
||||
// T3 reply correlation: if we were awaiting a reply with these
|
||||
// system_bytes, cancel T3 — the transaction is closing.
|
||||
if (awaiting_reply_sys_ && *awaiting_reply_sys_ == b.header.system_bytes) {
|
||||
awaiting_reply_sys_.reset();
|
||||
out.push_back(ActionCancelTimer{Timer::T3});
|
||||
}
|
||||
|
||||
// T4 multi-block continuation: cancel any pending T4, then arm a
|
||||
// fresh one if this block is NOT the final one (end_block=false).
|
||||
if (awaiting_next_block_) {
|
||||
awaiting_next_block_ = false;
|
||||
out.push_back(ActionCancelTimer{Timer::T4});
|
||||
}
|
||||
if (!b.header.end_block) {
|
||||
awaiting_next_block_ = true;
|
||||
out.push_back(ActionStartTimer{Timer::T4});
|
||||
}
|
||||
|
||||
out.push_back(ActionDeliverBlock{std::move(b)});
|
||||
} catch (const BlockError& e) {
|
||||
out.push_back(ActionTransmit{{kNAK}});
|
||||
@@ -67,12 +97,16 @@ void Protocol::deliver_recv(std::vector<Action>& out) {
|
||||
void Protocol::abort(std::string reason, std::vector<Action>& out) {
|
||||
out.push_back(ActionCancelTimer{Timer::T1});
|
||||
out.push_back(ActionCancelTimer{Timer::T2});
|
||||
if (awaiting_reply_sys_) out.push_back(ActionCancelTimer{Timer::T3});
|
||||
if (awaiting_next_block_) out.push_back(ActionCancelTimer{Timer::T4});
|
||||
out.push_back(ActionRaiseError{std::move(reason)});
|
||||
send_queue_.clear();
|
||||
send_block_bytes_.clear();
|
||||
recv_buf_.clear();
|
||||
recv_expected_ = 0;
|
||||
rty_ = 0;
|
||||
awaiting_reply_sys_.reset();
|
||||
awaiting_next_block_ = false;
|
||||
state_ = State::Idle;
|
||||
}
|
||||
|
||||
@@ -100,8 +134,21 @@ void Protocol::on_event(const Event& ev, std::vector<Action>& out) {
|
||||
if (state_ == State::RecvBlock) abort("T1 inter-character timeout", out);
|
||||
return;
|
||||
case Timer::T3:
|
||||
// Reply timeout for an outstanding W=1 primary. Abort with a
|
||||
// diagnostic the host can route to its application-level retry
|
||||
// / error path.
|
||||
if (awaiting_reply_sys_) {
|
||||
awaiting_reply_sys_.reset();
|
||||
abort("T3 reply timeout", out);
|
||||
}
|
||||
return;
|
||||
case Timer::T4:
|
||||
// Driven at the higher layer; FSM itself does not enforce.
|
||||
// Inter-block gap exceeded; the peer never sent the next block
|
||||
// of a multi-block message.
|
||||
if (awaiting_next_block_) {
|
||||
awaiting_next_block_ = false;
|
||||
abort("T4 inter-block timeout", out);
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user