diff --git a/lib/tdeck_ui/UI/LXMF/NomadNetCache.cpp b/lib/tdeck_ui/UI/LXMF/NomadNetCache.cpp index 6b9ea245..249ad640 100644 --- a/lib/tdeck_ui/UI/LXMF/NomadNetCache.cpp +++ b/lib/tdeck_ui/UI/LXMF/NomadNetCache.cpp @@ -952,8 +952,11 @@ void NomadNetCache::service(std::uint64_t now_ms) { if (transient_stall_count_ < MAX_TRANSIENT_STALL_TICKS) { ++transient_stall_count_; if (transient_stall_count_ == 1) transient_stall_start_ms_ = now_ms; - } else if (now_ms >= transient_stall_start_ms_ && - now_ms - transient_stall_start_ms_ >= MAX_TRANSIENT_STALL_MS) { + } else if (static_cast(now_ms - transient_stall_start_ms_) >= + static_cast(MAX_TRANSIENT_STALL_MS)) { + // 32-bit unsigned subtraction is wrap-safe across the millis() + // wraparound, so the 10s window measures true elapsed time even + // when the stall spans the 49.7-day counter rollover. transient_stall_count_ = 0; transient_bail(); } diff --git a/tests/native/test_nomadnet_cache.cpp b/tests/native/test_nomadnet_cache.cpp index db240f6b..9684be02 100644 --- a/tests/native/test_nomadnet_cache.cpp +++ b/tests/native/test_nomadnet_cache.cpp @@ -85,6 +85,19 @@ int main(){int f=0;auto ck=[&](bool x,const char*n){if(!x){++f;std::cerr<<"FAIL MemoryStorage slow_boot;slow_boot.available=false;NomadNetCache sc(slow_boot,cfg); uint64_t flat=0;for(int i=0;i<600&&sc.busy();++i){flat+=1;sc.service(flat);} ck(sc.busy(),"fast ticks alone do not disable the session cache"); + // F3/wrap: the wall-time window must measure true elapsed time across the + // 32-bit millis() rollover. millis() is a 32-bit counter zero-extended at the + // call site, so model it that way: a stall that starts 7.8s below the counter + // max and wraps must still bail after 10s of elapsed ticks, not wait ~49.7 + // days for the counter to lap the pre-wrap start value. + MemoryStorage wrap_boot;wrap_boot.available=false;NomadNetCache wc(wrap_boot,cfg); + const uint64_t wrap_pre=4294959500ULL; + for(int i=0;i<500&&wc.busy();++i)wc.service(wrap_pre+i); + ck(wc.busy(),"stall still active at tick floor before wrap"); + uint64_t now=4294960000ULL;int wcap=0; + while(wc.busy()&&wcap<200000){wc.service(static_cast(now));++now;++wcap;} + ck(!wc.busy(),"stall spanning the millis wrap still bails"); + ck(wcap<200000,"wrap-spanning stall bails in bounded ticks, not ~49.7 days"); // A seam that heals within the window terminates recovery cleanly. Boot // recovery transients do not degrade the namespace (the RECOVERY_BEGIN/END // paths keep it authoritative), so a healable transient still completes