Files
pyxis/lib/tdeck_ui/UI/LXMF/NomadNetCacheFlow.cpp
T
Torlando 32a09938d8 fix(nomadnet): address Greploop round 1 on the cache transient-stall guard
Three findings on the transient-stall bail, all valid:

- Reload invalidation: a bail recorded BYPASS, but NomadNetCacheFlow::service()
  accepts only MISS as a successful invalidation, so a bail during an admitted
  reload reported 'Page cache invalidation failed' instead of falling through
  to a live fetch. The bail now records MISS.

- Open-resource leak: the bail cleared read_open_/write_open_ (and abandoned an
  open directory enumeration) without calling endRead()/abortWrite()/endList(),
  leaking SD handles. The bail now releases each in-flight resource via the
  seam's own teardown (bounded best-effort; a still-transient close is
  accepted rather than re-pinning the op).

- Tick-vs-time: the not-ready UNAVAILABLE path returns immediately (no bus
  wait), so a pure 500-tick budget could expire during a legitimate SD mount
  window and disable caching for the whole session. The bail is now gated on
  BOTH the tick floor AND a 10s wall-time window (service() takes a monotonic
  ms clock; production passes millis(), 0 is a safe default for tests).

Regression tests: reload-invalidation bail -> NEED_LIVE (flow), flat-clock does
not bail, healable transient keeps authority, list-open (RECOVERY_END) stall
bails and releases the handle (cache).
2026-09-15 06:27:14 +00:00

114 lines
3.9 KiB
C++

// Copyright (c) 2026 Pyxis contributors
// SPDX-License-Identifier: MIT
#include "NomadNetCacheFlow.h"
namespace UI { namespace LXMF { namespace NomadNet {
CacheFlowState NomadNetCacheFlow::begin(const CacheKey& key, std::uint64_t now,
bool reload) {
cancel();
key_ = key;
now_ = now;
ExternalVector<std::uint8_t>().swap(page_);
status_ = reload ? "Invalidating cached page..." : "Checking page cache...";
lookup_admitted_ = false;
invalidation_admitted_ = false;
if (reload) {
state_ = CacheFlowState::INVALIDATE;
if (!cache_.busy()) {
invalidation_admitted_ = cache_.invalidate(key_) == CacheResult::PENDING;
}
return state_;
}
state_ = CacheFlowState::LOOKUP;
if (!cache_.busy()) {
lookup_admitted_ = cache_.beginLookup(key_, now, false) == CacheResult::PENDING;
if (!lookup_admitted_) {
state_ = CacheFlowState::NEED_LIVE;
status_ = "Requesting page...";
}
}
return state_;
}
void NomadNetCacheFlow::service(std::uint64_t now_ms) {
if (state_ == CacheFlowState::CANCELLED || state_ == CacheFlowState::FAILED)
return;
if (cache_.busy()) cache_.service(now_ms);
if (state_ == CacheFlowState::INVALIDATE) {
if (cache_.busy()) return;
if (!invalidation_admitted_) {
invalidation_admitted_ = cache_.invalidate(key_) == CacheResult::PENDING;
return;
}
if (cache_.lastResult() == CacheResult::MISS) {
state_ = CacheFlowState::NEED_LIVE;
status_ = "Requesting page...";
} else {
state_ = CacheFlowState::FAILED;
status_ = "Page cache invalidation failed";
}
return;
}
if (state_ != CacheFlowState::LOOKUP || cache_.busy()) return;
if (!lookup_admitted_) {
lookup_admitted_ = cache_.beginLookup(key_, now_, false) == CacheResult::PENDING;
if (!lookup_admitted_) {
state_ = CacheFlowState::NEED_LIVE;
status_ = "Requesting page...";
}
return;
}
if (cache_.lastResult() == CacheResult::HIT && cache_.takeBody(page_)) {
state_ = CacheFlowState::READY;
status_ = "Cached page; current reachability not checked";
} else {
state_ = CacheFlowState::NEED_LIVE;
status_ = "Requesting page...";
}
}
bool NomadNetCacheFlow::acceptLive(const std::vector<std::uint8_t>& body,
const CacheEligibility& eligibility,
std::uint64_t now) {
if (state_ != CacheFlowState::NEED_LIVE || !eligibility.successful ||
!eligibility.valid || eligibility.partial || eligibility.malformed ||
eligibility.truncated || eligibility.error || body.empty()) {
state_ = CacheFlowState::FAILED;
status_ = "Malformed NomadNet response";
return false;
}
try {
page_.assign(body.begin(), body.end());
} catch (const std::bad_alloc&) {
state_ = CacheFlowState::FAILED;
status_ = "Page is too large for available memory";
return false;
}
state_ = CacheFlowState::READY;
status_ = "Page loaded (live)";
if (cache_eligible(eligibility)) {
const auto directive = parse_cache_directive(body.data(), body.size());
if (directive.valid && directive.ttl)
cache_.beginCommit(key_, body, now, directive.ttl);
else
cache_.invalidate(key_);
}
return true;
}
void NomadNetCacheFlow::cancel() {
if (cache_.busy()) cache_.cancel();
ExternalVector<std::uint8_t>().swap(page_);
if (state_ != CacheFlowState::IDLE) state_ = CacheFlowState::CANCELLED;
status_.clear();
}
bool NomadNetCacheFlow::takePage(ExternalVector<std::uint8_t>& output) {
if (!pageReady()) return false;
output.swap(page_);
return true;
}
}}} // namespace UI::LXMF::NomadNet