Skip to content

Commit 57f7346

Browse files
authored
cpu_utilization: gracefully handle cgroup v2 CPU detection in CONTAINER mode (#47485)
Commit Message: cpu_utilization: fix CONTAINER mode in the host cgroup namespace In CONTAINER mode the cpu_utilization resource monitor read the cgroup v2 files straight from /sys/fs/cgroup, assuming it is the process's own cgroup, and required cpu.stat, cpu.max and cpuset.cpus.effective to all exist. When Envoy shares the host cgroup namespace, as privileged containers do, /sys/fs/cgroup is the cgroup v2 root: its cpu.stat covers the whole machine and it has no cpu.max. Detection failed and config initialization aborted the server. Resolve the process's own cgroup from /proc/self/cgroup and /proc/self/mountinfo, reusing the parsing in CgroupCpuUtil, before probing the cgroup v2 files, so the monitor reports container rather than host usage. Key cgroup v2 detection on cpu.stat. An absent cpu.max means no CPU limit and an absent cpuset.cpus.effective falls back to the CPU affinity count, while a file that exists but cannot be read fails the sample. When no supported cgroup CPU implementation is found, config initialization still fails as before. HOST mode is unchanged. Additional Description: Measured inside an affected container (privileged, Kubernetes with containerd, cgroup v2): ``` /proc/self/cgroup 0::/kubepods.slice/kubepods-pod<uid>.slice/cri-containerd-<id>.scope /proc/self/mountinfo ... 0:30 / /sys/fs/cgroup ... - cgroup2 cgroup rw /sys/fs/cgroup/cpu.stat present usage_usec 17341693371 <- whole machine /sys/fs/cgroup/cpu.max absent <- none at the root /sys/fs/cgroup/cpuset.cpus.effective present <own cgroup>/cpu.stat present usage_usec 12356042342 <- this container <own cgroup>/cpu.max present max 100000 <own cgroup>/cpuset.cpus.effective present 0-31 ``` `CgroupCpuUtil::getCurrentCgroupInfo()` is a new public wrapper around the existing `discoverCgroupMount()` and `constructCgroupPath()`, so the monitor shares the mount root stripping and `mountinfo` unescaping already used for concurrency detection. Related to #39978. This supersedes #47503. AI assistance disclosure: this change was prepared with AI assistance. The submitter has reviewed and fully understands all of the submitted code, per the project's generative AI policy. Risk Level: Low. Any resolution failure falls back to the previous `/sys/fs/cgroup` behavior. Testing: Unit tests in `test/extensions/resource_monitors/cpu_utilization/` and `test/server/cgroup_cpu_util_test.cc` cover resolution in the host cgroup namespace, `0::/` resolving to the mount point, the fallback when the resolved cgroup has no `cpu.stat`, absent versus unreadable optional files (stat errors other than `ENOENT`, and read failures after a successful stat), the affinity fallback, and the config-init error when no cgroup CPU support is found. A native macOS build of Envoy was not possible for unrelated toolchain reasons, so build and test verification relies on CI. Docs Changes: N/A Release Notes: Updated `changelogs/current/bug_fixes/cpu_utilization__cgroup-v2-graceful-detection.rst`. Platform Specific Features: Linux cgroup detection only. The affinity fallback is compiled on Linux only; other platforms keep `hardware_concurrency()`. --------- Signed-off-by: William Dauchy <william.dauchy@datadoghq.com>
1 parent 587588d commit 57f7346

11 files changed

Lines changed: 352 additions & 77 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Fixed the ``envoy.resource_monitors.cpu_utilization`` monitor in ``CONTAINER`` mode when Envoy shares
2+
the host cgroup namespace, for example in a privileged container. ``/sys/fs/cgroup`` is then the
3+
cgroup v2 root, which has no ``cpu.max``, so detection failed and config initialization aborted
4+
the server. The monitor now resolves its own cgroup from ``/proc/self/cgroup`` and
5+
``/proc/self/mountinfo`` and reports container rather than host usage. cgroup v2 detection is keyed
6+
on ``cpu.stat``: an absent ``cpu.max`` means no CPU limit and an absent ``cpuset.cpus.effective``
7+
falls back to the CPU affinity count, while a file that exists but cannot be read fails the sample.

‎source/extensions/resource_monitors/cpu_utilization/BUILD‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ envoy_cc_library(
5151
"//source/common/common:assert_lib",
5252
"//source/common/common:logger_lib",
5353
"//source/common/common:thread_lib",
54+
"//source/server:cgroup_cpu_util_lib",
55+
"//source/server:options_base",
5456
"@abseil-cpp//absl/strings",
5557
"@envoy_api//envoy/extensions/resource_monitors/cpu_utilization/v3:pkg_cc_proto",
5658
],

‎source/extensions/resource_monitors/cpu_utilization/cpu_paths.h‎

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "envoy/filesystem/filesystem.h"
66

77
#include "absl/strings/str_cat.h"
8+
#include "absl/strings/string_view.h"
89

910
namespace Envoy {
1011
namespace Extensions {
@@ -39,15 +40,22 @@ struct CpuPaths {
3940
};
4041

4142
struct V2 {
43+
// Returns the cgroup v2 mount point, used when the process's own cgroup cannot be resolved.
44+
static absl::string_view getBasePath() { return CGROUP_V2_BASE; }
45+
4246
// Returns the full path to the CPU stat file (cpu.stat).
43-
static std::string getStatPath() { return absl::StrCat(CGROUP_V2_BASE, STAT); }
47+
static std::string getStatPath(absl::string_view base = CGROUP_V2_BASE) {
48+
return absl::StrCat(base, STAT);
49+
}
4450

4551
// Returns the full path to the CPU max file (cpu.max).
46-
static std::string getMaxPath() { return absl::StrCat(CGROUP_V2_BASE, MAX); }
52+
static std::string getMaxPath(absl::string_view base = CGROUP_V2_BASE) {
53+
return absl::StrCat(base, MAX);
54+
}
4755

4856
// Returns the full path to the effective CPUs file (cpuset.cpus.effective).
49-
static std::string getEffectiveCpusPath() {
50-
return absl::StrCat(CGROUP_V2_BASE, EFFECTIVE_CPUS);
57+
static std::string getEffectiveCpusPath(absl::string_view base = CGROUP_V2_BASE) {
58+
return absl::StrCat(base, EFFECTIVE_CPUS);
5159
}
5260

5361
private:
@@ -57,10 +65,10 @@ struct CpuPaths {
5765
static constexpr const char* const EFFECTIVE_CPUS = "/cpuset.cpus.effective";
5866
};
5967

60-
// Returns whether cgroup v2 CPU subsystem is available.
61-
static bool isV2(Filesystem::Instance& fs) {
62-
return fs.fileExists(V2::getStatPath()) && fs.fileExists(V2::getMaxPath()) &&
63-
fs.fileExists(V2::getEffectiveCpusPath());
68+
// Keyed only on cpu.stat: cpu.max and cpuset.cpus.effective are optional (often
69+
// absent on Kubernetes v2 pods) and handled with fallbacks by the reader.
70+
static bool isV2(Filesystem::Instance& fs, absl::string_view base = V2::getBasePath()) {
71+
return fs.fileExists(V2::getStatPath(base));
6472
}
6573

6674
// Returns whether cgroup v1 CPU subsystem is available.

‎source/extensions/resource_monitors/cpu_utilization/linux_cpu_stats_reader.cc‎

Lines changed: 99 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
#include "source/extensions/resource_monitors/cpu_utilization/linux_cpu_stats_reader.h"
22

33
#include <algorithm>
4+
#include <cerrno>
45
#include <chrono>
6+
#include <optional>
57
#include <sstream>
8+
#include <thread>
69
#include <vector>
710

811
#include "envoy/common/exception.h"
@@ -11,9 +14,16 @@
1114
#include "source/common/common/assert.h"
1215
#include "source/common/common/fmt.h"
1316
#include "source/common/common/thread.h"
17+
#include "source/server/cgroup_cpu_util.h"
18+
19+
#ifdef __linux__
20+
#include "source/server/options_impl_platform_linux.h"
21+
#endif
1422

1523
#include "absl/strings/numbers.h"
24+
#include "absl/strings/str_cat.h"
1625
#include "absl/strings/str_split.h"
26+
#include "absl/strings/string_view.h"
1727
#include "absl/strings/strip.h"
1828

1929
namespace Envoy {
@@ -26,6 +36,37 @@ constexpr uint64_t NUMBER_OF_CPU_TIMES_TO_PARSE =
2636

2737
namespace {
2838

39+
// Fallback CPU count when cpuset.cpus.effective is absent. Honors the affinity mask
40+
// and floors at 1 to avoid divide-by-zero in downstream utilization math.
41+
int defaultCpuCount() {
42+
const unsigned int hw_threads = std::max(1u, std::thread::hardware_concurrency());
43+
#ifdef __linux__
44+
return static_cast<int>(OptionsImplPlatformLinux::getCpuAffinityCount(hw_threads));
45+
#else
46+
return static_cast<int>(hw_threads);
47+
#endif
48+
}
49+
50+
// Reads an optional cgroup interface file. std::nullopt means the file does not
51+
// exist; any other failure is an error, so that a permission problem is not
52+
// mistaken for an absent controller.
53+
absl::StatusOr<std::optional<std::string>> readOptionalFile(Filesystem::Instance& fs,
54+
const std::string& path) {
55+
const Api::IoCallResult<Filesystem::FileInfo> info = fs.stat(path);
56+
if (!info.ok()) {
57+
if (info.err_->getSystemErrorCode() == ENOENT) {
58+
return std::optional<std::string>();
59+
}
60+
return absl::UnavailableError(
61+
absl::StrCat("unable to stat ", path, ": ", info.err_->getErrorDetails()));
62+
}
63+
absl::StatusOr<std::string> contents = fs.fileReadToEnd(path);
64+
if (!contents.ok()) {
65+
return contents.status();
66+
}
67+
return std::optional<std::string>(std::move(contents).value());
68+
}
69+
2970
absl::StatusOr<int> parseEffectiveCpus(absl::string_view effective_cpu_list,
3071
const std::string& effective_path) {
3172
int cpu_count = 0;
@@ -165,6 +206,18 @@ absl::StatusOr<double> LinuxCpuStatsReader::getUtilization() {
165206

166207
absl::StatusOr<LinuxContainerCpuStatsReader::ContainerStatsReaderPtr>
167208
LinuxContainerCpuStatsReader::create(Filesystem::Instance& fs, TimeSource& time_source) {
209+
// Prefer the process's own cgroup: in the host cgroup namespace the mount point is
210+
// the cgroup root, which reports whole-machine usage and has no cpu.max.
211+
const std::optional<CgroupInfo> cgroup = CgroupCpuUtil::getCurrentCgroupInfo(fs);
212+
if (cgroup.has_value() && cgroup->version == "v2") {
213+
const std::string base(absl::StripSuffix(cgroup->full_path, "/"));
214+
if (CpuPaths::isV2(fs, base)) {
215+
return std::make_unique<CgroupV2CpuStatsReader>(fs, time_source, base);
216+
}
217+
ENVOY_LOG_MISC(debug, "No cpu.stat in cgroup {}, falling back to {}", base,
218+
CpuPaths::V2::getBasePath());
219+
}
220+
168221
if (CpuPaths::isV2(fs)) {
169222
return std::make_unique<CgroupV2CpuStatsReader>(fs, time_source);
170223
}
@@ -261,9 +314,14 @@ absl::StatusOr<double> CgroupV1CpuStatsReader::getUtilization() {
261314
}
262315

263316
CgroupV2CpuStatsReader::CgroupV2CpuStatsReader(Filesystem::Instance& fs, TimeSource& time_source)
264-
: LinuxContainerCpuStatsReader(fs, time_source), stat_path_(CpuPaths::V2::getStatPath()),
265-
max_path_(CpuPaths::V2::getMaxPath()), effective_path_(CpuPaths::V2::getEffectiveCpusPath()) {
266-
}
317+
: CgroupV2CpuStatsReader(fs, time_source, CpuPaths::V2::getBasePath()) {}
318+
319+
CgroupV2CpuStatsReader::CgroupV2CpuStatsReader(Filesystem::Instance& fs, TimeSource& time_source,
320+
absl::string_view base_path)
321+
: LinuxContainerCpuStatsReader(fs, time_source),
322+
stat_path_(CpuPaths::V2::getStatPath(base_path)),
323+
max_path_(CpuPaths::V2::getMaxPath(base_path)),
324+
effective_path_(CpuPaths::V2::getEffectiveCpusPath(base_path)) {}
267325

268326
CgroupV2CpuStatsReader::CgroupV2CpuStatsReader(Filesystem::Instance& fs, TimeSource& time_source,
269327
const std::string& stat_path,
@@ -306,35 +364,50 @@ CpuTimesV2 CgroupV2CpuStatsReader::getCpuTimes() {
306364
return {false, 0, 0, 0};
307365
}
308366

309-
// Read cpuset.cpus.effective
310-
auto effective_result = fs_.fileReadToEnd(effective_path_);
367+
// CPU count from cpuset.cpus.effective, falling back to the affinity CPU count
368+
// when absent. If present it must be readable and well-formed.
369+
// Format can be: "0", "0-3", "0,2,4", "0-2,4", "0-3,5-7", etc.
370+
int N = 0;
371+
const absl::StatusOr<std::optional<std::string>> effective_result =
372+
readOptionalFile(fs_, effective_path_);
311373
if (!effective_result.ok()) {
312-
ENVOY_LOG(error, "Unable to read effective CPUs file at {}", effective_path_);
374+
ENVOY_LOG(error, "Unable to read effective CPUs file {}: {}", effective_path_,
375+
effective_result.status().message());
313376
return {false, 0, 0, 0};
314377
}
315-
316-
// Parse effective CPUs
317-
// Format can be: "0", "0-3", "0,2,4", "0-2,4", "0-3,5-7", etc.
318-
absl::StatusOr<int> cpu_count = parseEffectiveCpus(effective_result.value(), effective_path_);
319-
if (!cpu_count.ok()) {
320-
ENVOY_LOG(error, "Failed to parse effective CPUs file {}: {}", effective_path_,
321-
cpu_count.status().message());
322-
return {false, 0, 0, 0};
378+
if (effective_result->has_value()) {
379+
absl::StatusOr<int> cpu_count = parseEffectiveCpus(**effective_result, effective_path_);
380+
if (!cpu_count.ok()) {
381+
ENVOY_LOG(error, "Failed to parse effective CPUs file {}: {}", effective_path_,
382+
cpu_count.status().message());
383+
return {false, 0, 0, 0};
384+
}
385+
N = cpu_count.value();
386+
} else {
387+
N = defaultCpuCount();
388+
ENVOY_LOG(trace, "cpuset.cpus.effective absent at {}, falling back to {} CPU(s)",
389+
effective_path_, N);
323390
}
324-
const int N = cpu_count.value();
325391

326-
// Read cpu.max
327-
auto max_result = fs_.fileReadToEnd(max_path_);
392+
// Effective core count from cpu.max. An absent file or a "max" quota means no
393+
// limit, i.e. all available CPUs; a present file must be readable and well-formed.
394+
double effective_cores = static_cast<double>(N);
395+
const absl::StatusOr<std::optional<std::string>> max_result = readOptionalFile(fs_, max_path_);
328396
if (!max_result.ok()) {
329-
ENVOY_LOG(error, "Unable to read CPU max file at {}", max_path_);
397+
ENVOY_LOG(error, "Unable to read cpu.max file {}: {}", max_path_,
398+
max_result.status().message());
330399
return {false, 0, 0, 0};
331400
}
332-
333-
absl::StatusOr<double> effective_cores = parseEffectiveCores(max_result.value(), N);
334-
if (!effective_cores.ok()) {
335-
ENVOY_LOG(error, "Failed to parse cpu.max file {}: {}", max_path_,
336-
effective_cores.status().message());
337-
return {false, 0, 0, 0};
401+
if (max_result->has_value()) {
402+
absl::StatusOr<double> parsed_cores = parseEffectiveCores(**max_result, N);
403+
if (!parsed_cores.ok()) {
404+
ENVOY_LOG(error, "Failed to parse cpu.max file {}: {}", max_path_,
405+
parsed_cores.status().message());
406+
return {false, 0, 0, 0};
407+
}
408+
effective_cores = parsed_cores.value();
409+
} else {
410+
ENVOY_LOG(trace, "cpu.max absent at {}, assuming no CPU limit ({} core(s))", max_path_, N);
338411
}
339412

340413
// Convert usage from usec to match our time units
@@ -344,9 +417,9 @@ CpuTimesV2 CgroupV2CpuStatsReader::getCpuTimes() {
344417
.count();
345418

346419
ENVOY_LOG(trace, "cgroupv2 usage_usec: {}, effective_cores: {}, current_time: {}", usage_usec,
347-
effective_cores.value(), current_time);
420+
effective_cores, current_time);
348421

349-
return {true, cpu_times_value_us, current_time, effective_cores.value()};
422+
return {true, cpu_times_value_us, current_time, effective_cores};
350423
}
351424

352425
absl::StatusOr<double> CgroupV2CpuStatsReader::getUtilization() {

‎source/extensions/resource_monitors/cpu_utilization/linux_cpu_stats_reader.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ class CgroupV2CpuStatsReader : public LinuxContainerCpuStatsReader,
9898
public:
9999
explicit CgroupV2CpuStatsReader(Filesystem::Instance& fs, TimeSource& time_source);
100100

101+
// Reads the interface files of a specific cgroup directory.
102+
CgroupV2CpuStatsReader(Filesystem::Instance& fs, TimeSource& time_source,
103+
absl::string_view base_path);
104+
101105
// Test-friendly constructor that accepts custom file paths
102106
CgroupV2CpuStatsReader(Filesystem::Instance& fs, TimeSource& time_source,
103107
const std::string& stat_path, const std::string& max_path,

‎source/server/cgroup_cpu_util.cc‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,14 @@ std::optional<CgroupPathInfo> CgroupCpuUtil::getCurrentCgroupPath(Filesystem::In
225225
return CgroupPathInfo{v2_path, "v2"};
226226
}
227227

228+
std::optional<CgroupInfo> CgroupCpuUtil::getCurrentCgroupInfo(Filesystem::Instance& fs) {
229+
const std::optional<CgroupMount> mount = discoverCgroupMount(fs);
230+
if (!mount.has_value()) {
231+
return std::nullopt;
232+
}
233+
return constructCgroupPath(*mount, fs);
234+
}
235+
228236
// Constructs complete cgroup path by combining mount metadata and process assignment.
229237
std::optional<CgroupInfo> CgroupCpuUtil::constructCgroupPath(const CgroupMount& mount,
230238
Filesystem::Instance& fs) {

‎source/server/cgroup_cpu_util.h‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,15 @@ class CgroupCpuUtil {
117117
static std::optional<uint32_t> getCpuLimit(Filesystem::Instance& fs,
118118
CgroupDetectionDiagnostic* diag = nullptr);
119119

120+
/**
121+
* Resolves the calling process's own `cgroup` directory by combining the `cgroup` mount from
122+
* `/proc/self/mountinfo` with the path from `/proc/self/cgroup`. The mount point alone is the
123+
* `cgroup` root when the process shares the host `cgroup` namespace.
124+
* @param fs Filesystem instance for file operations.
125+
* @return CgroupInfo with the directory and version, nullopt if it cannot be determined.
126+
*/
127+
static std::optional<CgroupInfo> getCurrentCgroupInfo(Filesystem::Instance& fs);
128+
120129
private:
121130
/**
122131
* Reads CPU limit from specific `cgroup` `v1` paths.

‎test/extensions/resource_monitors/cpu_utilization/BUILD‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ envoy_extension_cc_test(
3636
"//test/mocks/server:options_mocks",
3737
"//test/test_common:environment_lib",
3838
"//test/test_common:status_utility_lib",
39+
"@abseil-cpp//absl/status",
40+
"@abseil-cpp//absl/strings",
3941
],
4042
)
4143

‎test/extensions/resource_monitors/cpu_utilization/cpu_paths_test.cc‎

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@ TEST(CpuPathsV2Test, GetEffectiveCpusPathReturnsCorrectPath) {
4949
EXPECT_EQ(CpuPaths::V2::getEffectiveCpusPath(), "/sys/fs/cgroup/cpuset.cpus.effective");
5050
}
5151

52+
TEST(CpuPathsV2Test, GetBasePathReturnsMountPoint) {
53+
EXPECT_EQ(CpuPaths::V2::getBasePath(), "/sys/fs/cgroup");
54+
}
55+
56+
TEST(CpuPathsV2Test, PathsAreRelativeToSuppliedBase) {
57+
const std::string base = "/sys/fs/cgroup/kubepods.slice/pod.slice/container.scope";
58+
EXPECT_EQ(CpuPaths::V2::getStatPath(base), base + "/cpu.stat");
59+
EXPECT_EQ(CpuPaths::V2::getMaxPath(base), base + "/cpu.max");
60+
EXPECT_EQ(CpuPaths::V2::getEffectiveCpusPath(base), base + "/cpuset.cpus.effective");
61+
}
62+
5263
// =============================================================================
5364
// CpuPaths::isV1() Detection Tests
5465
// =============================================================================
@@ -90,45 +101,40 @@ TEST(CpuPathsDetectionTest, IsV1ReturnsFalseWhenUsageFileMissing) {
90101
// CpuPaths::isV2() Detection Tests
91102
// =============================================================================
92103

93-
TEST(CpuPathsDetectionTest, IsV2ReturnsTrueWhenAllFilesExist) {
104+
TEST(CpuPathsDetectionTest, IsV2ReturnsTrueWhenStatFileExists) {
94105
Filesystem::MockInstance mock_fs;
95106

96-
// Mock all three required V2 files as existing
107+
// Keyed only on cpu.stat; the optional v2 files must not be probed.
97108
EXPECT_CALL(mock_fs, fileExists("/sys/fs/cgroup/cpu.stat")).WillOnce(Return(true));
98-
EXPECT_CALL(mock_fs, fileExists("/sys/fs/cgroup/cpu.max")).WillOnce(Return(true));
99-
EXPECT_CALL(mock_fs, fileExists("/sys/fs/cgroup/cpuset.cpus.effective")).WillOnce(Return(true));
109+
110+
EXPECT_TRUE(CpuPaths::isV2(mock_fs));
111+
}
112+
113+
TEST(CpuPathsDetectionTest, IsV2ReturnsTrueWhenOnlyStatFileExists) {
114+
Filesystem::MockInstance mock_fs;
115+
116+
// A normal v2 pod may expose only cpu.stat; detection must still succeed.
117+
EXPECT_CALL(mock_fs, fileExists("/sys/fs/cgroup/cpu.stat")).WillOnce(Return(true));
118+
EXPECT_CALL(mock_fs, fileExists("/sys/fs/cgroup/cpu.max")).Times(0);
119+
EXPECT_CALL(mock_fs, fileExists("/sys/fs/cgroup/cpuset.cpus.effective")).Times(0);
100120

101121
EXPECT_TRUE(CpuPaths::isV2(mock_fs));
102122
}
103123

104124
TEST(CpuPathsDetectionTest, IsV2ReturnsFalseWhenStatFileMissing) {
105125
Filesystem::MockInstance mock_fs;
106126

107-
// Mock stat file missing
108127
EXPECT_CALL(mock_fs, fileExists("/sys/fs/cgroup/cpu.stat")).WillOnce(Return(false));
109-
// Short-circuit evaluation - other file checks may not be called
110-
EXPECT_CALL(mock_fs, fileExists("/sys/fs/cgroup/cpu.max"))
111-
.Times(testing::AtMost(1))
112-
.WillRepeatedly(Return(true));
113-
EXPECT_CALL(mock_fs, fileExists("/sys/fs/cgroup/cpuset.cpus.effective"))
114-
.Times(testing::AtMost(1))
115-
.WillRepeatedly(Return(true));
116128

117129
EXPECT_FALSE(CpuPaths::isV2(mock_fs));
118130
}
119131

120-
TEST(CpuPathsDetectionTest, IsV2ReturnsFalseWhenMaxFileMissing) {
132+
TEST(CpuPathsDetectionTest, IsV2ProbesSuppliedBase) {
121133
Filesystem::MockInstance mock_fs;
122134

123-
// Mock max file missing
124-
EXPECT_CALL(mock_fs, fileExists("/sys/fs/cgroup/cpu.stat")).WillOnce(Return(true));
125-
EXPECT_CALL(mock_fs, fileExists("/sys/fs/cgroup/cpu.max")).WillOnce(Return(false));
126-
// Short-circuit evaluation - effective_cpus check may not be called
127-
EXPECT_CALL(mock_fs, fileExists("/sys/fs/cgroup/cpuset.cpus.effective"))
128-
.Times(testing::AtMost(1))
129-
.WillRepeatedly(Return(true));
135+
EXPECT_CALL(mock_fs, fileExists("/sys/fs/cgroup/pod.slice/cpu.stat")).WillOnce(Return(true));
130136

131-
EXPECT_FALSE(CpuPaths::isV2(mock_fs));
137+
EXPECT_TRUE(CpuPaths::isV2(mock_fs, "/sys/fs/cgroup/pod.slice"));
132138
}
133139

134140
} // namespace

0 commit comments

Comments
 (0)