Keep Companion replies alive for the official app timeout window

This commit is contained in:
mikecarper committed 2026-10-04 14:34:28 -07:00
1 parent f515e19a7a
commit e3033b364e
3 files changed
+108 -4

No files matched your search

+20 -4
View File
@@ -32,16 +32,31 @@ void CompanionDelayedReplies::reset(Reply& reply) {
reply.sent_pending = false;
}
uint32_t CompanionDelayedReplies::radioBudget(const Reply& reply) {
// Leave room for retirement grace within the signed half of millis range.
const uint32_t maximum = 0x7FFFFFFFU - DELIVERY_GRACE_MS;
const uint32_t extra = reply.timeout / 5;
uint32_t budget = reply.timeout > maximum - extra
? maximum : reply.timeout + extra;
if (!reply.terminal) {
// Official apps start an estimate + 5s timer when they receive SENT.
const uint32_t host_budget = reply.timeout > maximum - HOST_EXTRA_TIMEOUT_MS
? maximum : reply.timeout + HOST_EXTRA_TIMEOUT_MS;
if (host_budget > budget) budget = host_budget;
}
return budget;
}
void CompanionDelayedReplies::arm(Reply& reply, uint32_t timeout,
bool flood, uint32_t now) {
reply.timeout = timeout;
// Wrap-safe deadlines must stay in the signed half of the millis range.
const uint32_t maximum = 0x7FFFFFFFU - DELIVERY_GRACE_MS;
uint32_t budget = timeout > maximum / 6 * 5 ? maximum : timeout + timeout / 5;
reply.radio_deadline = now + budget;
reply.flood = flood;
reply.sent_pending = !reply.terminal;
reply.sent_deadline = now + DELIVERY_GRACE_MS;
// Before SENT admission only the bounded pre-SENT grace applies. Terminal
// output has no SENT frame and retains its printed arm-time radio budget.
reply.radio_deadline = reply.terminal
? now + radioBudget(reply) : reply.sent_deadline;
reply.frame_len = 0;
reply.phase = AwaitRadio;
}
@@ -80,6 +95,7 @@ bool CompanionDelayedReplies::service(Reply& reply,
return false;
}
reply.sent_pending = false;
reply.radio_deadline = now + radioBudget(reply);
}
return reply.phase == AwaitAdmission
&& serial->writeFrameToRoute(reply.route, reply.frame, reply.frame_len)
+2
View File
@@ -16,6 +16,7 @@ public:
static constexpr uint8_t HISTORY_SIZE = 8;
static constexpr uint8_t NO_HISTORY = 255;
static constexpr uint32_t DELIVERY_GRACE_MS = 10000;
static constexpr uint32_t HOST_EXTRA_TIMEOUT_MS = 5000;
static constexpr uint32_t LOGIN_QUARANTINE_MS = 30000;
struct Reply {
@@ -50,6 +51,7 @@ private:
static bool due(uint32_t now, uint32_t deadline);
static uint32_t retirementDeadline(uint32_t now, uint32_t radio_deadline,
uint32_t floor);
static uint32_t radioBudget(const Reply& reply);
static void reset(Reply& reply);
static void arm(Reply& reply, uint32_t timeout, bool flood, uint32_t now);
static bool store(Reply& reply, const uint8_t* frame, size_t len, uint32_t now);
+86
View File
@@ -270,6 +270,14 @@ struct Fixture {
memcpy(mesh.cmd_frame + offset, mesh.recipient.id.pub_key, 32);
mesh.handleRequestFrame(len);
}
void aclCommand() {
memset(mesh.cmd_frame, 0, sizeof(mesh.cmd_frame));
mesh.cmd_frame[0] = CMD_SEND_BINARY_REQ;
memcpy(mesh.cmd_frame + 1, mesh.recipient.id.pub_key, 32);
const uint8_t request[] = {5, 0, 0, 0x12, 0x34, 0x56, 0x78};
memcpy(mesh.cmd_frame + 33, request, sizeof(request));
mesh.handleRequestFrame(33 + sizeof(request));
}
};
static const Kind kinds[] = {Tracker::Login, Tracker::Status, Tracker::Telemetry,
@@ -443,6 +451,73 @@ static void testReservedHistorySurvivesIdle(bool request_service, uint32_t start
int main() {
unsigned checks = 0;
// Exercise the real CMD50 producer and callback with a legacy one-admin ACL
// reply after the old estimate + 20% cutoff but inside the app's +5s window.
for (bool tcp : {false, true}) for (uint32_t start : {100U, 0xfffffff0U}) {
Fixture f(tcp); g_mock_millis = start; f.mesh.direct_timeout = 2000;
f.aclCommand(); const uint32_t tag = slot(f, Tracker::Binary).tag;
assert(!slot(f, Tracker::Binary).sent_pending);
assert(slot(f, Tracker::Binary).radio_deadline == start + 7000U);
f.otherRequest(); g_mock_millis = start + 3000U;
uint8_t acl[11]; memcpy(acl, &tag, 4);
memcpy(acl + 4, f.mesh.recipient.id.pub_key, 6); acl[10] = 3;
f.mesh.onContactResponse(f.mesh.recipient, acl, sizeof(acl)); f.drain();
assertSentBeforeFinal(f.wire(), Tracker::Binary, tag);
Bytes expected = {PUSH_CODE_BINARY_RESPONSE, 0};
expected.insert(expected.end(), acl, acl + sizeof(acl));
assert(frameWith(f.wire(), PUSH_CODE_BINARY_RESPONSE) == expected);
assert(f.other_stream.output.empty()); ++checks;
}
// Every nonterminal kind shares the host budget. A pending SENT is bounded
// for 10s, then successful admission starts the full host window exactly once.
for (bool tcp : {false, true}) for (Kind kind : kinds)
for (uint32_t start : {100U, 0xfffffff0U}) {
Fixture f(tcp); g_mock_millis = start; assert(f.fill() == 4);
f.arm(kind, 17, 2000);
assert(slot(f, kind).sent_pending);
assert(slot(f, kind).radio_deadline == start + Tracker::DELIVERY_GRACE_MS);
g_mock_millis = start + 9000U; f.service();
assert(slot(f, kind).phase == Tracker::AwaitRadio && slot(f, kind).sent_pending);
assert(f.mesh.hasFiniteDelayedReplyForRoute(f.owner));
f.drain(); assert(!slot(f, kind).sent_pending);
const uint32_t admitted_deadline = start + 9000U + 7000U;
assert(slot(f, kind).radio_deadline == admitted_deadline);
g_mock_millis = start + 12000U; f.service();
assert(slot(f, kind).radio_deadline == admitted_deadline);
reply(f, kind); f.drain(); assertSentBeforeFinal(f.wire(), kind); ++checks;
}
// Radio callbacks may arrive before backpressured SENT admission. Retain the
// response without extending the existing pre-SENT/delivery grace.
for (bool tcp : {false, true}) for (Kind kind : kinds) {
Fixture f(tcp); assert(f.fill() == 4); f.arm(kind, 17, 2000);
const uint32_t pre_sent_deadline = slot(f, kind).sent_deadline;
g_mock_millis += 9000U; reply(f, kind);
assert(slot(f, kind).phase == Tracker::AwaitAdmission);
assert(slot(f, kind).delivery_deadline == pre_sent_deadline);
f.drain(); assertSentBeforeFinal(f.wire(), kind); ++checks;
}
// Independent wide arithmetic checks saturation and both exact boundaries.
const uint32_t ceiling = 0x7fffffffU - Tracker::DELIVERY_GRACE_MS;
for (uint32_t start : {100U, 0xfffffff0U})
for (uint32_t timeout : {0U, 1U, 2000U, 25000U, 30000U, ceiling, 0xffffffffU}) {
const uint64_t extra = uint64_t(timeout) + timeout / 5U;
const uint64_t host = uint64_t(timeout) + 5000U;
const uint32_t budget = uint32_t(std::min<uint64_t>(ceiling, std::max(extra, host)));
Fixture f; g_mock_millis = start; f.arm(Tracker::Binary, 17, timeout);
assert(slot(f, Tracker::Binary).radio_deadline == start + budget);
g_mock_millis = start + budget - 1U; f.service();
assert(slot(f, Tracker::Binary).phase == Tracker::AwaitRadio);
g_mock_millis += 1; reply(f, Tracker::Binary); f.drain();
assert(slot(f, Tracker::Binary).phase == Tracker::Empty);
assert(count(f.wire(), PUSH_CODE_BINARY_RESPONSE) == 0); ++checks;
Fixture blocked; g_mock_millis = start; assert(blocked.fill() == 4);
blocked.arm(Tracker::Binary, 17, timeout);
g_mock_millis = start + Tracker::DELIVERY_GRACE_MS - 1U; blocked.service();
assert(slot(blocked, Tracker::Binary).phase == Tracker::AwaitRadio);
g_mock_millis += 1; reply(blocked, Tracker::Binary); blocked.drain();
assert(count(blocked.wire(), RESP_CODE_SENT) == 0);
assert(count(blocked.wire(), PUSH_CODE_BINARY_RESPONSE) == 0); ++checks;
}
for (bool tcp : {false, true}) for (Kind kind : kinds) {
Fixture f(tcp); f.arm(kind); assert(f.fill() == 3); f.otherRequest();
reply(f, kind); const auto& held = slot(f, kind);
@@ -685,6 +760,17 @@ int main() {
assert(f.other_stream.output.empty()); ++checks;
}
#if COMPANION_FEATURE_TEXT_TERMINAL
// Terminal login keeps the arm-time +20% deadline printed to the operator.
for (uint32_t start : {100U, 0xfffffff0U}) {
Fixture f; g_mock_millis = start; f.mesh.direct_timeout = 2000;
f.mesh.sendTerminalLogin(f.mesh.recipient, "x");
assert(f.mesh._terminal_login_expires_at == start + 2400U);
assert(slot(f, Tracker::Login).radio_deadline == start + 2400U);
assert(!slot(f, Tracker::Login).sent_pending);
g_mock_millis = start + 2400U; reply(f, Tracker::Login);
assert(!f.mesh._terminal_login_pending && !f.mesh.hasPendingReqs());
f.drain(); assert(f.wire().empty()); ++checks;
}
// The timeout plus retirement grace must stay in the signed half-range.
// Test the last reachable tripled base estimate and the next estimate,
// including a millis wrap while a maximum-duration trace is pending.