diff --git a/lix/legacy/build-remote.cc b/lix/legacy/build-remote.cc index 98dba8733..e97e6cfe7 100644 --- a/lix/legacy/build-remote.cc +++ b/lix/legacy/build-remote.cc @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #if __APPLE__ @@ -75,10 +76,11 @@ static bool allSupportedLocally(Store & store, const std::set& requ * (machineHasRoom -> true, liveLoadPenalty -> 0). * ------------------------------------------------------------------------ */ -// Default estimate for a drv that does not match the heavy-crate table. -// "Light" means: we have NO confident signal that this drv is memory-heavy, -// so machineHasRoom never filters on account of it. -static const uint64_t adaptiveLightMib = 512; +// A drv that does not match the heavy-crate table is treated as "light": +// we have NO confident signal that it is memory-heavy, so adaptiveEstPeakRSS +// returns nullopt and machineHasRoom never filters on account of it. (There is +// deliberately no numeric light default - an unmatched drv must always permit, +// so keying it on a free-RAM threshold would wrongly filter light drvs.) // name-substring -> estimated peak RSS in MiB (from LIX_ADAPTIVE_RSS_TABLE). static std::map adaptiveRssTable; @@ -108,8 +110,12 @@ static void adaptiveLoadConfig() f >> j; if (j.is_object()) for (auto & [k, v] : j.items()) - if (v.is_number()) - adaptiveRssTable[k] = v.get(); + // Per-entry guard: one bad value skips only that entry, + // it does not discard the whole (otherwise valid) table. + try { + if (v.is_number_unsigned() || (v.is_number_integer() && v.get() >= 0)) + adaptiveRssTable[k] = v.get(); + } catch (...) { continue; } } } } catch (...) { adaptiveRssTable.clear(); } @@ -120,8 +126,11 @@ static void adaptiveLoadConfig() auto j = nlohmann::json::parse(*m); if (j.is_object()) for (auto & [k, v] : j.items()) - if (v.is_string()) - adaptiveMetricsMap[k] = v.get(); + // Per-entry guard: skip one bad value, keep the rest. + try { + if (v.is_string()) + adaptiveMetricsMap[k] = v.get(); + } catch (...) { continue; } } } catch (...) { adaptiveMetricsMap.clear(); } } @@ -143,8 +152,12 @@ static std::optional adaptiveEstPeakRSS(const StorePath & drvPath) /* TCP-connect the metrics endpoint and read one line: * "MemAvail_kB psi_mem psi_io psi_cpu load1 nproc" - * Short (500ms) connect+read timeout so selection NEVER hangs. Any failure - * returns an AdaptiveProbe with ok=false. */ + * A single ~500ms wall-clock deadline bounds the WHOLE probe (resolve + + * connect + read) so selection NEVER hangs, regardless of a slow or + * byte-dribbling peer. The endpoint MUST be a numeric IP:port - resolution is + * pinned to AI_NUMERICHOST|AI_NUMERICSERV so getaddrinfo never does network + * I/O (a hostname simply fails fast -> fail-open). Any failure returns an + * AdaptiveProbe with ok=false. */ static AdaptiveProbe adaptiveProbeEndpoint(const std::string & hostport) { AdaptiveProbe r; @@ -154,10 +167,20 @@ static AdaptiveProbe adaptiveProbeEndpoint(const std::string & hostport) std::string host = hostport.substr(0, colon); std::string port = hostport.substr(colon + 1); + // Single wall-clock budget for the entire probe. + auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(500); + auto remainingMs = [&]() -> int { + auto d = std::chrono::duration_cast( + deadline - std::chrono::steady_clock::now()).count(); + return d <= 0 ? 0 : (int) d; + }; + struct addrinfo hints; memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; + // Numeric-only: no DNS, no resolver blocking. Non-IP endpoint -> fail-open. + hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV; struct addrinfo * res = nullptr; if (getaddrinfo(host.c_str(), port.c_str(), &hints, &res) != 0 || !res) return r; @@ -170,7 +193,7 @@ static AdaptiveProbe adaptiveProbeEndpoint(const std::string & hostport) struct pollfd pfd; pfd.fd = fd; pfd.events = POLLOUT; - if (poll(&pfd, 1, 500) <= 0) { close(fd); freeaddrinfo(res); return r; } + if (poll(&pfd, 1, remainingMs()) <= 0) { close(fd); freeaddrinfo(res); return r; } int soerr = 0; socklen_t sl = sizeof soerr; if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &soerr, &sl) < 0 || soerr != 0) { @@ -181,16 +204,27 @@ static AdaptiveProbe adaptiveProbeEndpoint(const std::string & hostport) } freeaddrinfo(res); - struct timeval tv; - tv.tv_sec = 0; - tv.tv_usec = 500000; - setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); - + /* Read one short line. Keep the socket non-blocking and gate every recv on + poll(POLLIN) against the shared deadline, so the total read time is + bounded even if the peer drips one byte at a time. A valid reply is tiny, + so also cap the number of reads. */ std::string line; char buf[512]; - while (line.size() < 4096) { + for (int iter = 0; iter < 16 && line.size() < 4096; ++iter) { + int rem = remainingMs(); + if (rem == 0) break; + struct pollfd pfd; + pfd.fd = fd; + pfd.events = POLLIN; + int pr = poll(&pfd, 1, rem); + if (pr <= 0) break; // timeout or error -> fail-open + if (!(pfd.revents & POLLIN)) break; // POLLHUP/POLLERR with no data ssize_t n = recv(fd, buf, sizeof buf, 0); - if (n <= 0) break; + if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) continue; + break; + } + if (n == 0) break; // peer closed line.append(buf, n); if (line.find('\n') != std::string::npos) break; }