mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-07 07:58:36 +00:00
test(docker): allow heavyweight lanes at low parallelism
This commit is contained in:
@@ -17,6 +17,9 @@ const DEFAULT_PREFLIGHT_RUN_TIMEOUT_MS = 60_000;
|
||||
const DEFAULT_TIMINGS_FILE = path.join(ROOT_DIR, ".artifacts/docker-tests/lane-timings.json");
|
||||
const DEFAULT_PROFILE = "all";
|
||||
const RELEASE_PATH_PROFILE = "release-path";
|
||||
const IS_MAIN = process.argv[1]
|
||||
? path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
||||
: false;
|
||||
const LIVE_PROFILE_TIMEOUT_MS = 20 * 60 * 1000;
|
||||
const LIVE_CLI_TIMEOUT_MS = 20 * 60 * 1000;
|
||||
const LIVE_ACP_TIMEOUT_MS = 20 * 60 * 1000;
|
||||
@@ -617,6 +620,32 @@ function laneResources(poolLane) {
|
||||
return ["docker", ...(poolLane.resources ?? [])];
|
||||
}
|
||||
|
||||
export function describeDockerSchedulerLimits(parallelism, options) {
|
||||
return `parallelism=${parallelism} weightLimit=${options.weightLimit} resources=${resourceLimitsSummary(
|
||||
options.resourceLimits,
|
||||
)}`;
|
||||
}
|
||||
|
||||
export function canStartSchedulerLane(candidate, active, parallelism, options) {
|
||||
const weight = laneWeight(candidate);
|
||||
if (active.count >= parallelism) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const exceedsWeightLimit = active.weight + weight > options.weightLimit;
|
||||
const exceedsResourceLimit = laneResources(candidate).some((resource) => {
|
||||
const limit = options.resourceLimits[resource] ?? options.weightLimit;
|
||||
const current = active.resources.get(resource) ?? 0;
|
||||
return current + weight > limit;
|
||||
});
|
||||
|
||||
if (!exceedsWeightLimit && !exceedsResourceLimit) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return active.count === 0;
|
||||
}
|
||||
|
||||
function laneSummary(poolLane) {
|
||||
const resources = laneResources(poolLane).join(",");
|
||||
const timeout = poolLane.timeoutMs ? ` timeout=${Math.round(poolLane.timeoutMs / 1000)}s` : "";
|
||||
@@ -1135,18 +1164,7 @@ async function runLanePool(poolLanes, baseEnv, logDir, parallelism, options) {
|
||||
}
|
||||
|
||||
function canStartLane(candidate) {
|
||||
const weight = laneWeight(candidate);
|
||||
if (active.count >= parallelism || active.weight + weight > options.weightLimit) {
|
||||
return false;
|
||||
}
|
||||
for (const resource of laneResources(candidate)) {
|
||||
const limit = options.resourceLimits[resource] ?? options.weightLimit;
|
||||
const current = active.resources.get(resource) ?? 0;
|
||||
if (current + weight > limit) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return canStartSchedulerLane(candidate, active, parallelism, options);
|
||||
}
|
||||
|
||||
function reserve(candidate) {
|
||||
@@ -1207,7 +1225,12 @@ async function runLanePool(poolLanes, baseEnv, logDir, parallelism, options) {
|
||||
}
|
||||
if (running.size === 0) {
|
||||
const blocked = pending.map(laneSummary).join(", ");
|
||||
throw new Error(`No Docker lanes fit scheduler limits: ${blocked}`);
|
||||
throw new Error(
|
||||
`No Docker lanes fit scheduler limits (${describeDockerSchedulerLimits(
|
||||
parallelism,
|
||||
options,
|
||||
)}): ${blocked}. Tune OPENCLAW_DOCKER_ALL_PARALLELISM, OPENCLAW_DOCKER_ALL_WEIGHT_LIMIT, or OPENCLAW_DOCKER_ALL_<RESOURCE>_LIMIT.`,
|
||||
);
|
||||
}
|
||||
|
||||
const { promise, result } = await Promise.race(running);
|
||||
@@ -1549,7 +1572,9 @@ async function main() {
|
||||
console.log("==> Docker test suite passed");
|
||||
}
|
||||
|
||||
await main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
if (IS_MAIN) {
|
||||
await main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
138
test/scripts/docker-all-scheduler.test.ts
Normal file
138
test/scripts/docker-all-scheduler.test.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
canStartSchedulerLane,
|
||||
describeDockerSchedulerLimits,
|
||||
} from "../../scripts/test-docker-all.mjs";
|
||||
|
||||
const limits = {
|
||||
resourceLimits: {
|
||||
docker: 2,
|
||||
npm: 2,
|
||||
},
|
||||
weightLimit: 2,
|
||||
};
|
||||
|
||||
function activePool({
|
||||
count = 0,
|
||||
resources = {},
|
||||
weight = 0,
|
||||
}: {
|
||||
count?: number;
|
||||
resources?: Record<string, number>;
|
||||
weight?: number;
|
||||
} = {}) {
|
||||
return {
|
||||
count,
|
||||
resources: new Map(Object.entries(resources)),
|
||||
weight,
|
||||
};
|
||||
}
|
||||
|
||||
describe("scripts/test-docker-all scheduler", () => {
|
||||
it("allows an overweight lane to start alone under low parallelism", () => {
|
||||
expect(
|
||||
canStartSchedulerLane(
|
||||
{
|
||||
name: "install-e2e",
|
||||
resources: ["npm"],
|
||||
weight: 4,
|
||||
},
|
||||
activePool(),
|
||||
2,
|
||||
limits,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not co-schedule another lane while an overweight lane is active", () => {
|
||||
expect(
|
||||
canStartSchedulerLane(
|
||||
{
|
||||
name: "package-update",
|
||||
resources: ["npm"],
|
||||
weight: 1,
|
||||
},
|
||||
activePool({
|
||||
count: 1,
|
||||
resources: {
|
||||
docker: 4,
|
||||
npm: 4,
|
||||
},
|
||||
weight: 4,
|
||||
}),
|
||||
2,
|
||||
limits,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("preserves the parallelism count cap", () => {
|
||||
expect(
|
||||
canStartSchedulerLane(
|
||||
{
|
||||
name: "package-update",
|
||||
resources: ["npm"],
|
||||
weight: 1,
|
||||
},
|
||||
activePool({
|
||||
count: 2,
|
||||
resources: {
|
||||
docker: 1,
|
||||
npm: 1,
|
||||
},
|
||||
weight: 1,
|
||||
}),
|
||||
2,
|
||||
limits,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps resource and weight limits as co-scheduling limits", () => {
|
||||
expect(
|
||||
canStartSchedulerLane(
|
||||
{
|
||||
name: "npm-smoke",
|
||||
resources: ["npm"],
|
||||
weight: 1,
|
||||
},
|
||||
activePool({
|
||||
count: 1,
|
||||
resources: {
|
||||
docker: 1,
|
||||
npm: 1,
|
||||
},
|
||||
weight: 1,
|
||||
}),
|
||||
2,
|
||||
limits,
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
canStartSchedulerLane(
|
||||
{
|
||||
name: "npm-heavy",
|
||||
resources: ["npm"],
|
||||
weight: 2,
|
||||
},
|
||||
activePool({
|
||||
count: 1,
|
||||
resources: {
|
||||
docker: 1,
|
||||
npm: 1,
|
||||
},
|
||||
weight: 1,
|
||||
}),
|
||||
2,
|
||||
limits,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("describes effective scheduler limits for operator errors", () => {
|
||||
expect(describeDockerSchedulerLimits(2, limits)).toBe(
|
||||
"parallelism=2 weightLimit=2 resources=docker=2 npm=2",
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user