fix: treat memory target equality as a no-op success
This commit is contained in:
@@ -38,7 +38,7 @@ function runPythonFixture(body: string): Record<string, unknown> {
|
|||||||
function runRunnerPythonFixture(functionNames: string[], body: string): Record<string, unknown> {
|
function runRunnerPythonFixture(functionNames: string[], body: string): Record<string, unknown> {
|
||||||
const source = Buffer.from(runnerSource, "utf8").toString("base64");
|
const source = Buffer.from(runnerSource, "utf8").toString("base64");
|
||||||
const fixture = [
|
const fixture = [
|
||||||
"import ast, base64, json",
|
"import ast, base64, json, os, re, time",
|
||||||
`source = base64.b64decode(${JSON.stringify(source)}).decode("utf-8")`,
|
`source = base64.b64decode(${JSON.stringify(source)}).decode("utf-8")`,
|
||||||
`wanted = set(${JSON.stringify(functionNames)})`,
|
`wanted = set(${JSON.stringify(functionNames)})`,
|
||||||
"tree = ast.parse(source)",
|
"tree = ast.parse(source)",
|
||||||
@@ -471,6 +471,100 @@ print(json.dumps({"compact": compact, "full": full, "executed": executed}))
|
|||||||
assert.equal(compact.selection.configId, "root-file-cache");
|
assert.equal(compact.selection.configId, "root-file-cache");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("memory-pressure plan and run treat an exact target with zero candidates as a successful no-op", () => {
|
||||||
|
const result = runRunnerPythonFixture([
|
||||||
|
"summarize_memory_candidates",
|
||||||
|
"apply_memory_target_summary",
|
||||||
|
"memory_pressure_selection",
|
||||||
|
"memory_pressure_option_suffix",
|
||||||
|
"plan_payload",
|
||||||
|
"start_memory_pressure_job",
|
||||||
|
], String.raw`
|
||||||
|
PROVIDER_ID = "NC01"
|
||||||
|
OPTIONS = {"memoryPressureOnly": True, "memoryReclaimConfigId": "root-file-cache"}
|
||||||
|
target = 3221225472
|
||||||
|
MEMORY_CONFIG = {"cgroupReclaim": {"targetMemAvailableBytes": target}}
|
||||||
|
def safe_int(value, fallback=0):
|
||||||
|
try: return int(value)
|
||||||
|
except (TypeError, ValueError): return fallback
|
||||||
|
def fmt_bytes(value): return str(value)
|
||||||
|
def now_iso(): return "2026-07-21T00:00:01Z"
|
||||||
|
def status_command(job_id): return "gc remote NC01 status --job-id %s" % job_id
|
||||||
|
snapshot = {"ok": True, "memory": {"availableBytes": target}, "swap": {"freeBytes": 1}}
|
||||||
|
snapshot_count = 0
|
||||||
|
def collect_memory_snapshot():
|
||||||
|
global snapshot_count
|
||||||
|
snapshot_count += 1
|
||||||
|
return snapshot
|
||||||
|
def memory_reclaim_scan_diagnostics(): return {"ok": True, "blocked": False}
|
||||||
|
def job_paths(job_id): return {"state": "/tmp/fixture-state"}
|
||||||
|
written = []
|
||||||
|
def write_json_atomic(path, payload): written.append(payload)
|
||||||
|
def unexpected_fork(): raise RuntimeError("zero-candidate target-met run must not fork")
|
||||||
|
os.fork = unexpected_fork
|
||||||
|
plan = plan_payload("2026-07-21T00:00:00Z", {}, [], [], [])
|
||||||
|
run = start_memory_pressure_job("2026-07-21T00:00:00Z", [])
|
||||||
|
print(json.dumps({"plan": plan, "run": run, "written": written, "snapshotCount": snapshot_count}))
|
||||||
|
`);
|
||||||
|
const plan = result.plan as Record<string, any>;
|
||||||
|
const run = result.run as Record<string, any>;
|
||||||
|
assert.equal(plan.summary.targetMet, true);
|
||||||
|
assert.equal(plan.summary.targetGapBytes, 0);
|
||||||
|
assert.equal(plan.summary.outcome, "memory-target-met");
|
||||||
|
assert.equal(plan.summary.candidateCount, 0);
|
||||||
|
assert.equal(run.ok, true);
|
||||||
|
assert.equal(run.status, "succeeded");
|
||||||
|
assert.equal(run.outcome, "memory-target-met");
|
||||||
|
assert.equal(run.summary.targetMet, true);
|
||||||
|
assert.equal(run.summary.targetGapBytes, 0);
|
||||||
|
assert.equal(run.summary.outcome, "memory-target-met");
|
||||||
|
assert.equal(run.mutation, false);
|
||||||
|
assert.deepEqual(run.results, []);
|
||||||
|
assert.equal(result.snapshotCount, 2);
|
||||||
|
assert.deepEqual(result.written, [run]);
|
||||||
|
|
||||||
|
const config = Bun.YAML.parse(readFileSync(rootPath("config/unidesk-cli.yaml"), "utf8")) as Record<string, any>;
|
||||||
|
const projected = projectRemoteGcResult(plan, config.gc.remote.targets.NC01, {
|
||||||
|
action: "plan",
|
||||||
|
memoryPressureOnly: true,
|
||||||
|
full: false,
|
||||||
|
}) as Record<string, any>;
|
||||||
|
assert.equal(projected.runEligibility.allowed, true);
|
||||||
|
assert.equal(projected.runEligibility.allCandidatesEvaluated, true);
|
||||||
|
assert.deepEqual(projected.runEligibility.reasons, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("memory-pressure zero-candidate run remains blocked below the target", () => {
|
||||||
|
const result = runRunnerPythonFixture([
|
||||||
|
"summarize_memory_candidates",
|
||||||
|
"apply_memory_target_summary",
|
||||||
|
"memory_pressure_selection",
|
||||||
|
"start_memory_pressure_job",
|
||||||
|
], String.raw`
|
||||||
|
PROVIDER_ID = "NC01"
|
||||||
|
OPTIONS = {"memoryPressureOnly": True, "memoryReclaimConfigId": "root-file-cache"}
|
||||||
|
target = 3221225472
|
||||||
|
MEMORY_CONFIG = {"cgroupReclaim": {"targetMemAvailableBytes": target}}
|
||||||
|
def safe_int(value, fallback=0):
|
||||||
|
try: return int(value)
|
||||||
|
except (TypeError, ValueError): return fallback
|
||||||
|
def fmt_bytes(value): return str(value)
|
||||||
|
def now_iso(): return "2026-07-21T00:00:01Z"
|
||||||
|
def status_command(job_id): return "gc remote NC01 status --job-id %s" % job_id
|
||||||
|
def collect_memory_snapshot(): return {"ok": True, "memory": {"availableBytes": target - 1}}
|
||||||
|
def memory_reclaim_scan_diagnostics(): return {"ok": True, "blocked": False}
|
||||||
|
def job_paths(job_id): return {"state": "/tmp/fixture-state"}
|
||||||
|
def write_json_atomic(path, payload): pass
|
||||||
|
run = start_memory_pressure_job("2026-07-21T00:00:00Z", [])
|
||||||
|
print(json.dumps(run))
|
||||||
|
`);
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.equal(result.status, "blocked");
|
||||||
|
assert.equal(result.outcome, "no-memory-reclaim-candidates");
|
||||||
|
assert.equal((result.summary as Record<string, unknown>).targetMet, false);
|
||||||
|
assert.equal((result.summary as Record<string, unknown>).targetGapBytes, 1);
|
||||||
|
});
|
||||||
|
|
||||||
test("default memory-pressure plan projects an actionable YAML-bounded preview below stdout limit", () => {
|
test("default memory-pressure plan projects an actionable YAML-bounded preview below stdout limit", () => {
|
||||||
const processCandidate = (index: number): Record<string, unknown> => ({
|
const processCandidate = (index: number): Record<string, unknown> => ({
|
||||||
id: `browser-orphan-process-tree:${1000 + index}:12345`,
|
id: `browser-orphan-process-tree:${1000 + index}:12345`,
|
||||||
|
|||||||
@@ -1506,6 +1506,21 @@ def summarize_memory_candidates(candidates, returned):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def apply_memory_target_summary(summary, memory_snapshot):
|
||||||
|
target_available = safe_int((MEMORY_CONFIG.get("cgroupReclaim") or {}).get("targetMemAvailableBytes"))
|
||||||
|
snapshot_memory = memory_snapshot.get("memory") if isinstance(memory_snapshot, dict) else None
|
||||||
|
available = safe_int(snapshot_memory.get("availableBytes")) if isinstance(snapshot_memory, dict) else 0
|
||||||
|
target_met = target_available > 0 and available >= target_available
|
||||||
|
summary.update({
|
||||||
|
"targetMemAvailableBytes": target_available,
|
||||||
|
"targetMet": target_met,
|
||||||
|
"targetGapBytes": max(0, target_available - available) if target_available > 0 else None,
|
||||||
|
})
|
||||||
|
if target_met:
|
||||||
|
summary["outcome"] = "memory-target-met"
|
||||||
|
return summary
|
||||||
|
|
||||||
|
|
||||||
def memory_pressure_selection():
|
def memory_pressure_selection():
|
||||||
config_id = OPTIONS.get("memoryReclaimConfigId")
|
config_id = OPTIONS.get("memoryReclaimConfigId")
|
||||||
if not isinstance(config_id, str) or not config_id:
|
if not isinstance(config_id, str) or not config_id:
|
||||||
@@ -1721,7 +1736,8 @@ def returned_results(results):
|
|||||||
def plan_payload(observed_at, preflight, protected, candidates, visible):
|
def plan_payload(observed_at, preflight, protected, candidates, visible):
|
||||||
if bool(OPTIONS.get("memoryPressureOnly")):
|
if bool(OPTIONS.get("memoryPressureOnly")):
|
||||||
scan = memory_reclaim_scan_diagnostics()
|
scan = memory_reclaim_scan_diagnostics()
|
||||||
summary = summarize_memory_candidates(candidates, visible)
|
memory_before = collect_memory_snapshot()
|
||||||
|
summary = apply_memory_target_summary(summarize_memory_candidates(candidates, visible), memory_before)
|
||||||
if scan.get("blocked"):
|
if scan.get("blocked"):
|
||||||
summary["outcome"] = "memory-reclaim-scan-blocked"
|
summary["outcome"] = "memory-reclaim-scan-blocked"
|
||||||
return {
|
return {
|
||||||
@@ -1733,7 +1749,7 @@ def plan_payload(observed_at, preflight, protected, candidates, visible):
|
|||||||
"mutation": False,
|
"mutation": False,
|
||||||
"observedAt": observed_at,
|
"observedAt": observed_at,
|
||||||
"options": OPTIONS,
|
"options": OPTIONS,
|
||||||
"memoryBefore": collect_memory_snapshot(),
|
"memoryBefore": memory_before,
|
||||||
"summary": summary,
|
"summary": summary,
|
||||||
"selection": memory_pressure_selection(),
|
"selection": memory_pressure_selection(),
|
||||||
"candidateScan": scan,
|
"candidateScan": scan,
|
||||||
@@ -2683,7 +2699,8 @@ def start_memory_pressure_job(observed_at, candidates):
|
|||||||
os.getpid(),
|
os.getpid(),
|
||||||
)
|
)
|
||||||
paths = job_paths(job_id)
|
paths = job_paths(job_id)
|
||||||
summary = summarize_memory_candidates(candidates, candidates)
|
memory_before = collect_memory_snapshot()
|
||||||
|
summary = apply_memory_target_summary(summarize_memory_candidates(candidates, candidates), memory_before)
|
||||||
scan = memory_reclaim_scan_diagnostics()
|
scan = memory_reclaim_scan_diagnostics()
|
||||||
base = {
|
base = {
|
||||||
"ok": scan.get("ok") is True,
|
"ok": scan.get("ok") is True,
|
||||||
@@ -2698,6 +2715,7 @@ def start_memory_pressure_job(observed_at, candidates):
|
|||||||
"observedAt": observed_at,
|
"observedAt": observed_at,
|
||||||
"startedAt": observed_at,
|
"startedAt": observed_at,
|
||||||
"summary": summary,
|
"summary": summary,
|
||||||
|
"memoryBefore": memory_before,
|
||||||
"selection": memory_pressure_selection(),
|
"selection": memory_pressure_selection(),
|
||||||
"candidateScan": scan,
|
"candidateScan": scan,
|
||||||
}
|
}
|
||||||
@@ -2713,6 +2731,19 @@ def start_memory_pressure_job(observed_at, candidates):
|
|||||||
write_json_atomic(paths["state"], terminal)
|
write_json_atomic(paths["state"], terminal)
|
||||||
return terminal
|
return terminal
|
||||||
if not candidates:
|
if not candidates:
|
||||||
|
if summary.get("targetMet") is True:
|
||||||
|
terminal = dict(base)
|
||||||
|
terminal.update({
|
||||||
|
"ok": True,
|
||||||
|
"status": "succeeded",
|
||||||
|
"outcome": "memory-target-met",
|
||||||
|
"mutation": False,
|
||||||
|
"finishedAt": now_iso(),
|
||||||
|
"memoryAfter": memory_before,
|
||||||
|
"results": [],
|
||||||
|
})
|
||||||
|
write_json_atomic(paths["state"], terminal)
|
||||||
|
return terminal
|
||||||
terminal = dict(base)
|
terminal = dict(base)
|
||||||
terminal.update({
|
terminal.update({
|
||||||
"status": "blocked",
|
"status": "blocked",
|
||||||
|
|||||||
@@ -707,11 +707,12 @@ export function projectRemoteGcResult(
|
|||||||
const memoryPressure = recordOrEmpty(remoteTarget.memoryPressure);
|
const memoryPressure = recordOrEmpty(remoteTarget.memoryPressure);
|
||||||
const previewLimit = positiveInteger(memoryPressure.planPreviewLimit);
|
const previewLimit = positiveInteger(memoryPressure.planPreviewLimit);
|
||||||
const totalCandidateCount = positiveInteger(summary.candidateCount) ?? 0;
|
const totalCandidateCount = positiveInteger(summary.candidateCount) ?? 0;
|
||||||
const allCandidatesEvaluated = totalCandidateCount > 0 && totalCandidateCount === candidates.length;
|
const targetMet = summary.targetMet === true;
|
||||||
|
const allCandidatesEvaluated = totalCandidateCount === candidates.length;
|
||||||
const reasons: string[] = [];
|
const reasons: string[] = [];
|
||||||
if (payload.ok !== true) reasons.push("plan-not-ok");
|
if (payload.ok !== true) reasons.push("plan-not-ok");
|
||||||
if (scan.ok !== true || scan.blocked === true) reasons.push("candidate-scan-blocked");
|
if (scan.ok !== true || scan.blocked === true) reasons.push("candidate-scan-blocked");
|
||||||
if (totalCandidateCount === 0) reasons.push("no-candidates");
|
if (totalCandidateCount === 0 && !targetMet) reasons.push("no-candidates");
|
||||||
if (!allCandidatesEvaluated) reasons.push("candidate-page-incomplete");
|
if (!allCandidatesEvaluated) reasons.push("candidate-page-incomplete");
|
||||||
if (!candidates.every(memoryPressureCandidateAllowed)) reasons.push("unsupported-or-incomplete-candidate");
|
if (!candidates.every(memoryPressureCandidateAllowed)) reasons.push("unsupported-or-incomplete-candidate");
|
||||||
if (previewLimit === null) reasons.push("yaml-plan-preview-limit-invalid");
|
if (previewLimit === null) reasons.push("yaml-plan-preview-limit-invalid");
|
||||||
|
|||||||
Reference in New Issue
Block a user