mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 19:35:28 -06:00
fix(scripts): keep every cgroup mount view, not just the last one seen
One hierarchy can be visible through several mounts and only some expose a subtree containing this process. Retaining only the last view dropped the budget whenever a non-representable bind view came later, sending the build back to host MemTotal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+33
-20
@@ -83,6 +83,8 @@ type MemoryLimitParams = {
|
||||
procSelfMountinfoPath?: string;
|
||||
};
|
||||
|
||||
type CgroupMount = { mountPoint: string; root: string };
|
||||
|
||||
type TsdownBuildParams = MemoryLimitParams & {
|
||||
args?: string[];
|
||||
comSpec?: string;
|
||||
@@ -493,8 +495,10 @@ function resolveCgroupMountPoints(params: MemoryLimitParams = {}) {
|
||||
// Unreadable off Linux; the documented defaults still apply.
|
||||
}
|
||||
|
||||
let unified = { mountPoint: DEFAULT_CGROUP_V2_MOUNT_PATH, root: "/" };
|
||||
let v1Memory = { mountPoint: DEFAULT_CGROUP_V1_MEMORY_MOUNT_PATH, root: "/" };
|
||||
// One hierarchy can be visible through several mounts, and only some of them expose a subtree
|
||||
// containing this process, so every view is kept as a candidate rather than the last one seen.
|
||||
const unified: CgroupMount[] = [];
|
||||
const v1Memory: CgroupMount[] = [];
|
||||
for (const line of rawMountinfo.split("\n")) {
|
||||
// mountinfo separates its variable optional fields from the fstype with a lone "-".
|
||||
const [fields, describe] = line.split(" - ");
|
||||
@@ -504,12 +508,19 @@ function resolveCgroupMountPoints(params: MemoryLimitParams = {}) {
|
||||
continue;
|
||||
}
|
||||
if (fsType === "cgroup2") {
|
||||
unified = { mountPoint, root };
|
||||
unified.push({ mountPoint, root });
|
||||
} else if (fsType === "cgroup" && (superOptions ?? "").split(",").includes("memory")) {
|
||||
v1Memory = { mountPoint, root };
|
||||
v1Memory.push({ mountPoint, root });
|
||||
}
|
||||
}
|
||||
return { unified, v1Memory };
|
||||
return {
|
||||
unified:
|
||||
unified.length > 0 ? unified : [{ mountPoint: DEFAULT_CGROUP_V2_MOUNT_PATH, root: "/" }],
|
||||
v1Memory:
|
||||
v1Memory.length > 0
|
||||
? v1Memory
|
||||
: [{ mountPoint: DEFAULT_CGROUP_V1_MEMORY_MOUNT_PATH, root: "/" }],
|
||||
};
|
||||
}
|
||||
|
||||
// mountinfo field 4 is the subtree a cgroupfs mount exposes, so /proc/self/cgroup records are
|
||||
@@ -540,19 +551,17 @@ function resolveCgroupMemoryLimitPaths(params: MemoryLimitParams = {}) {
|
||||
}
|
||||
|
||||
const paths: string[] = [];
|
||||
const addHierarchy = (
|
||||
mount: { mountPoint: string; root: string },
|
||||
limitFiles: string[],
|
||||
cgroupPath: string,
|
||||
) => {
|
||||
const relative = relativeCgroupPath(mount.root, cgroupPath);
|
||||
if (relative === null) {
|
||||
return;
|
||||
}
|
||||
const segments = relative.split("/").filter(Boolean);
|
||||
for (let depth = segments.length; depth >= 0; depth -= 1) {
|
||||
for (const limitFile of limitFiles) {
|
||||
paths.push(path.join(mount.mountPoint, ...segments.slice(0, depth), limitFile));
|
||||
const addHierarchy = (mounts: CgroupMount[], limitFiles: string[], cgroupPath: string) => {
|
||||
for (const mount of mounts) {
|
||||
const relative = relativeCgroupPath(mount.root, cgroupPath ?? mount.root);
|
||||
if (relative === null) {
|
||||
continue;
|
||||
}
|
||||
const segments = relative.split("/").filter(Boolean);
|
||||
for (let depth = segments.length; depth >= 0; depth -= 1) {
|
||||
for (const limitFile of limitFiles) {
|
||||
paths.push(path.join(mount.mountPoint, ...segments.slice(0, depth), limitFile));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -576,8 +585,12 @@ function resolveCgroupMemoryLimitPaths(params: MemoryLimitParams = {}) {
|
||||
// Only probe the mounts blind when this process has no memory cgroup record at all; a record
|
||||
// that no mount can represent means the limit is unreadable here, not that the root applies.
|
||||
if (!sawMemoryRecord) {
|
||||
addHierarchy(mounts.unified, CGROUP_V2_MEMORY_LIMIT_FILES, mounts.unified.root);
|
||||
addHierarchy(mounts.v1Memory, CGROUP_V1_MEMORY_LIMIT_FILES, mounts.v1Memory.root);
|
||||
for (const mount of mounts.unified) {
|
||||
addHierarchy([mount], CGROUP_V2_MEMORY_LIMIT_FILES, mount.root);
|
||||
}
|
||||
for (const mount of mounts.v1Memory) {
|
||||
addHierarchy([mount], CGROUP_V1_MEMORY_LIMIT_FILES, mount.root);
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
@@ -514,6 +514,39 @@ describe("resolveTsdownBuildInvocation", () => {
|
||||
expect(result.options.env.NODE_OPTIONS).toBe("--max-old-space-size=4352");
|
||||
});
|
||||
|
||||
it("keeps a representable cgroup mount when a later view cannot represent it", () => {
|
||||
// Several mounts can expose one hierarchy; only the first covers this process here, so
|
||||
// retaining just the last-seen view would lose the budget entirely.
|
||||
const cgroupFiles = new Map([
|
||||
["/proc/self/cgroup", "0::/docker/abc123/openclaw-main-update.service\n"],
|
||||
[
|
||||
"/proc/self/mountinfo",
|
||||
"30 25 0:26 /docker/abc123 /sys/fs/cgroup rw - cgroup2 cgroup2 rw\n" +
|
||||
"31 25 0:26 /other/branch /mnt/peer-cgroup rw - cgroup2 cgroup2 rw\n",
|
||||
],
|
||||
["/sys/fs/cgroup/openclaw-main-update.service/memory.max", `${5 * 1024 * 1024 * 1024}\n`],
|
||||
["/test/meminfo", "MemTotal: 7340032 kB\n"],
|
||||
]);
|
||||
|
||||
const result = resolveTsdownBuildInvocation({
|
||||
nodeExecPath: "/usr/bin/node",
|
||||
npmExecPath: "/tmp/pnpm.cjs",
|
||||
env: {},
|
||||
procMeminfoPath: "/test/meminfo",
|
||||
fs: {
|
||||
readFileSync(filePath: string) {
|
||||
const contents = cgroupFiles.get(filePath);
|
||||
if (contents === undefined) {
|
||||
throw new Error(`ENOENT: ${filePath}`);
|
||||
}
|
||||
return contents;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.options.env.NODE_OPTIONS).toBe("--max-old-space-size=4352");
|
||||
});
|
||||
|
||||
it("ignores a cgroup mount that cannot represent this process's cgroup", () => {
|
||||
// An inherited namespace can leave a mount whose subtree holds someone else's cgroup.
|
||||
// Sizing the build from it would apply an unrelated limit, so it must be skipped.
|
||||
|
||||
Reference in New Issue
Block a user