Skip to content

Commit 50cff19

Browse files
authored
Merge pull request #511 from Anionix/codex/issue-301-worker-rlimits
Cap bounded robustness worker resources
2 parents 315cc61 + a64942f commit 50cff19

10 files changed

Lines changed: 622 additions & 16 deletions

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ uv run --frozen format-bench report --run-dir runs/fair-local
7676

7777
The native robustness suite records pinned Arrow, Vortex, and FastLanes targets. Arrow requires a checkout at the recorded source commit plus binaries in `native/arrow/build`; Vortex and FastLanes require a checkout whose `HEAD` matches the recorded source commit. FastLanes is recorded as project-seeded rather than coverage-guided. Lance, object JSONL, and TsFile have no confirmed official native target and are retained as `UNSUPPORTED` evidence. Missing binaries or mismatched source checkouts never become a silent pass. Select targets with repeated `--target` options and set the run budget with `--duration-seconds` and `--artifact-budget-mib`.
7878

79-
Robustness reports also aggregate each target's case denominator, pass/fail outcomes, crash and timeout counts, incomplete reasons, duration p50, and artifact/source identities. For generated artifact mutations only, reports add deterministic descriptive counts for the denominator, completed cases, failures, crashes, timeouts, unsupported cases, incomplete cases, and completed percentage. Named boundary cases are excluded; this is not a conventional mutation score and has no ranking or gate effect. The percentage is `N/A` when no generated artifact mutations are present. The manual [`Coverage branch report`](.github/workflows/coverage.yml) layers a pinned coverage.py release over the frozen uv project environment; see the primary [coverage.py branch measurement documentation](https://coverage.readthedocs.io/en/7.10.7/branch.html) and [uv one-off dependency contract](https://docs.astral.sh/uv/reference/cli/#uv-run--with).
79+
Robustness reports also aggregate each target's case denominator, pass/fail outcomes, crash and timeout counts, incomplete reasons, duration p50, and artifact/source identities. Before importing the heavyweight bounded-worker graph, a lightweight bootstrap lowers non-raiseable Unix limits to at most 1 GiB per file, 256 open files, and a 512-process ceiling for the worker's real user; Linux workers also receive an 8 GiB address-space cap. `RLIMIT_NPROC` is a real-user aggregate, not a per-worker child count, so the finite ceiling leaves headroom for the runner's existing processes and library threads. Root and Linux processes with exempt effective capabilities record this ceiling as `UNSUPPORTED` because the kernel does not enforce it there, per the [Linux `getrlimit(2)` contract](https://man7.org/linux/man-pages/man2/getrlimit.2.html). Tighter inherited limits win, and requested, effective, and unsupported caps are recorded in the run plus each launched case. The supported macOS runner rejects finite `RLIMIT_AS` values, so its address-space cap is explicitly `UNSUPPORTED` rather than falsely recorded as enforced; maintainers track the remaining containment risk in [#510](https://github.com/Anionix/data-format-lab/issues/510). These controls follow the primary [Python `resource` contract](https://docs.python.org/3.12/library/resource.html) and complement the existing timeout, output-retention, and total artifact-budget bounds. For generated artifact mutations only, reports add deterministic descriptive counts for the denominator, completed cases, failures, crashes, timeouts, unsupported cases, incomplete cases, and completed percentage. Named boundary cases are excluded; this is not a conventional mutation score and has no ranking or gate effect. The percentage is `N/A` when no generated artifact mutations are present. The manual [`Coverage branch report`](.github/workflows/coverage.yml) layers a pinned coverage.py release over the frozen uv project environment; see the primary [coverage.py branch measurement documentation](https://coverage.readthedocs.io/en/7.10.7/branch.html) and [uv one-off dependency contract](https://docs.astral.sh/uv/reference/cli/#uv-run--with).
8080

8181
Coverage thresholds and source-code mutation scores are intentionally not enforced yet, so regressions can remain ungated. Repository maintainers own this accepted risk under [#506](https://github.com/Anionix/data-format-lab/issues/506); it expires after three successful reports on distinct merged `main` commits or on 2026-08-07, whichever comes first. That issue fixes the evidence fields, numeric threshold decision, serial source-mutation pilot, 20-minute budget, and closeout criteria.
8282

‎src/format_bench/report.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,34 @@ def _robustness(results: dict) -> list[str]:
474474
["Case timeout seconds", config["case_timeout_seconds"]],
475475
["Artifact budget MiB", config["artifact_budget_mib"]],
476476
]
477+
for label, key in (
478+
("Requested", "worker_resource_limits_requested"),
479+
("Effective", "worker_resource_limits_effective"),
480+
):
481+
worker_limits = config.get(key)
482+
if isinstance(worker_limits, dict):
483+
config_rows.extend(
484+
[
485+
[f"{label} worker address-space cap bytes", worker_limits["address_space_bytes"]],
486+
[f"{label} worker file-size cap bytes", worker_limits["file_size_bytes"]],
487+
[f"{label} worker open-file cap", worker_limits["open_files"]],
488+
[
489+
f"{label} real-user process ceiling",
490+
worker_limits["real_user_processes"],
491+
],
492+
]
493+
)
494+
unsupported_limits = config.get("worker_resource_limits_unsupported")
495+
if isinstance(unsupported_limits, list):
496+
config_rows.append(
497+
["Unsupported worker resource caps", ", ".join(unsupported_limits) or "None"]
498+
)
499+
config_rows.append(
500+
[
501+
"Worker resource cap application",
502+
config.get("worker_resource_limits_application"),
503+
]
504+
)
477505

478506
summary_rows = [
479507
[verdict, evidence["summary"].get(verdict.value, 0)]

‎src/format_bench/robustness/profile.py‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@
2929
encode_malformed,
3030
encode_valid,
3131
)
32+
from format_bench.worker_limits import (
33+
DEFAULT_WORKER_RESOURCE_LIMITS,
34+
EffectiveWorkerResourceLimits,
35+
effective_worker_resource_limits,
36+
)
3237

3338
_PER_CASE_OUTPUT_BUDGET_BYTES = 1024 * 1024
3439

@@ -97,6 +102,7 @@ def _case_result_reserve(
97102
mutation: Mapping[str, object] | None,
98103
timeout: float,
99104
output_budget_bytes: int,
105+
worker_resource_limits_effective: EffectiveWorkerResourceLimits,
100106
) -> int:
101107
placeholder = {
102108
"case_id": case_id,
@@ -127,6 +133,12 @@ def _case_result_reserve(
127133
"input_canonical_hash": "0" * 64,
128134
"input_arrow": _record(input_record),
129135
"artifact_records": [_record(item) for item in artifact_records],
136+
"worker_resource_limits_requested": DEFAULT_WORKER_RESOURCE_LIMITS.evidence(),
137+
"worker_resource_limits_effective": worker_resource_limits_effective.evidence(),
138+
"worker_resource_limits_unsupported": list(
139+
worker_resource_limits_effective.unsupported_resources
140+
),
141+
"worker_resource_limits_application": "APPLIED",
130142
**({"mutation": mutation} if mutation is not None else {}),
131143
}
132144
return len(_json(placeholder))
@@ -146,7 +158,11 @@ def _execute(
146158
mutation_index: int | None = None,
147159
mutation_count: int = 0,
148160
seed: int = 0,
161+
worker_resource_limits_effective: EffectiveWorkerResourceLimits | None = None,
149162
) -> dict[str, object]:
163+
worker_resource_limits_effective = (
164+
worker_resource_limits_effective or effective_worker_resource_limits()
165+
)
150166
with (
151167
tempfile.TemporaryDirectory() as temporary,
152168
tempfile.TemporaryDirectory(dir=run_dir) as process_directory,
@@ -218,6 +234,7 @@ def _execute(
218234
mutation,
219235
timeout,
220236
_PER_CASE_OUTPUT_BUDGET_BYTES,
237+
worker_resource_limits_effective,
221238
)
222239
if remaining < result_reserve:
223240
raise ArtifactBudgetExceeded(
@@ -305,6 +322,7 @@ def run_bounded(
305322
]
306323
mutation_count = min(mutations_per_target, 1) if run["fixture"] else mutations_per_target
307324
store = EvidenceStore(run_dir / "robustness", artifact_budget_mib * 1024 * 1024)
325+
worker_resource_limits_effective = effective_worker_resource_limits()
308326
observations: list[dict] = []
309327
exhausted = False
310328
for target in targets or core_targets():
@@ -347,6 +365,7 @@ def run_bounded(
347365
mutation_index=mutation_index,
348366
mutation_count=mutation_count,
349367
seed=seed,
368+
worker_resource_limits_effective=worker_resource_limits_effective,
350369
)
351370
)
352371
except ArtifactBudgetExceeded as error:
@@ -370,6 +389,30 @@ def run_bounded(
370389
summary = {verdict.value: 0 for verdict in RobustnessVerdict}
371390
for item in observations:
372391
summary[item["verdict"].value] += 1
392+
launched = [
393+
item
394+
for item in observations
395+
if "worker_resource_limits_requested" in item
396+
]
397+
applied = [
398+
item
399+
for item in launched
400+
if item.get("worker_resource_limits_application") == "APPLIED"
401+
]
402+
if launched and len(applied) == len(launched):
403+
resource_limit_application = "APPLIED"
404+
elif applied:
405+
resource_limit_application = "PARTIAL"
406+
else:
407+
resource_limit_application = "UNCONFIRMED"
408+
effective_limit_evidence = (
409+
applied[0]["worker_resource_limits_effective"]
410+
if applied
411+
else {
412+
key: None
413+
for key in DEFAULT_WORKER_RESOURCE_LIMITS.evidence()
414+
}
415+
)
373416
evidence = {
374417
"robustness_v1": {
375418
"contract_version": "1",
@@ -385,6 +428,16 @@ def run_bounded(
385428
"effective_mutations_per_target": mutation_count,
386429
"case_timeout_seconds": timeout_seconds,
387430
"artifact_budget_mib": artifact_budget_mib,
431+
"worker_resource_limits_requested": (
432+
DEFAULT_WORKER_RESOURCE_LIMITS.evidence()
433+
),
434+
"worker_resource_limits_effective": (
435+
effective_limit_evidence
436+
),
437+
"worker_resource_limits_unsupported": list(
438+
worker_resource_limits_effective.unsupported_resources
439+
),
440+
"worker_resource_limits_application": resource_limit_application,
388441
},
389442
"cases": observations,
390443
"summary": summary,

‎src/format_bench/robustness/runner.py‎

Lines changed: 123 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
)
2020
from format_bench.json_contract import strict_json_dumps, strict_json_loads
2121
from format_bench.robustness.paths import reject_symlink_tree
22+
from format_bench.worker_limits import (
23+
DEFAULT_WORKER_RESOURCE_LIMITS,
24+
effective_worker_resource_limits,
25+
)
2226

2327
MAX_WORKER_DETAILS_BYTES = 4096
2428
DEFAULT_OUTPUT_RETENTION_BYTES = 1024 * 1024
@@ -52,6 +56,8 @@ class RequestPayload(TypedDict):
5256
class WorkerResponse(TypedDict):
5357
observed: str
5458
details: dict[str, object]
59+
worker_resource_limits_effective: NotRequired[dict[str, int | None]]
60+
worker_resource_limits_unsupported: NotRequired[list[str]]
5561

5662

5763
class CaseResult(TypedDict):
@@ -71,6 +77,10 @@ class CaseResult(TypedDict):
7177
input_arrow: NotRequired[object]
7278
artifact_records: NotRequired[object]
7379
mutation: NotRequired[object]
80+
worker_resource_limits_requested: NotRequired[dict[str, int]]
81+
worker_resource_limits_effective: NotRequired[dict[str, int | None]]
82+
worker_resource_limits_unsupported: NotRequired[list[str]]
83+
worker_resource_limits_application: NotRequired[str]
7484

7585

7686
def _json_object(value: object, label: str) -> dict[str, object]:
@@ -107,10 +117,49 @@ def _worker_response(stdout: str) -> WorkerResponse:
107117
value = strict_json_loads(stdout.strip())
108118
response = _json_object(value, "worker response")
109119
details: object = response.get("details", {})
110-
return {
120+
parsed: WorkerResponse = {
111121
"observed": _string_field(response, "observed"),
112122
"details": _bounded_details(_json_object(details, "worker response details")),
113123
}
124+
effective = response.get("worker_resource_limits_effective")
125+
unsupported = response.get("worker_resource_limits_unsupported")
126+
if effective is None and unsupported is None:
127+
return parsed
128+
if not isinstance(unsupported, list):
129+
raise TypeError("worker unsupported resource limits must be strings")
130+
unsupported_strings: list[str] = []
131+
for item in cast(list[object], unsupported):
132+
if not isinstance(item, str):
133+
raise TypeError("worker unsupported resource limits must be strings")
134+
unsupported_strings.append(item)
135+
effective_object = _json_object(
136+
effective, "worker effective resource limits"
137+
)
138+
fields = (
139+
"address_space_bytes",
140+
"file_size_bytes",
141+
"open_files",
142+
"real_user_processes",
143+
)
144+
if set(effective_object) != set(fields):
145+
raise TypeError("worker effective resource limit fields do not match")
146+
if not all(
147+
value is None
148+
or (
149+
isinstance(value, int)
150+
and not isinstance(value, bool)
151+
and value >= 0
152+
)
153+
for value in effective_object.values()
154+
):
155+
raise TypeError("worker effective resource limits must be non-negative integers")
156+
if not set(unsupported_strings).issubset(fields):
157+
raise TypeError("worker unsupported resource limit is unknown")
158+
parsed["worker_resource_limits_effective"] = cast(
159+
dict[str, int | None], effective_object
160+
)
161+
parsed["worker_resource_limits_unsupported"] = unsupported_strings
162+
return parsed
114163

115164

116165
def _bounded_details(details: dict[str, object]) -> dict[str, object]:
@@ -318,30 +367,30 @@ def cleanup_group() -> None:
318367

319368
def _outcome(
320369
process: ProcessEvidence, stdout: str
321-
) -> tuple[ObservedOutcome, dict[str, object]]:
370+
) -> tuple[ObservedOutcome, dict[str, object], WorkerResponse | None]:
322371
if process["timed_out"]:
323372
details: dict[str, object] = (
324373
{"cleanup_incomplete": True}
325374
if process["cleanup_incomplete"]
326375
else {}
327376
)
328-
return ObservedOutcome.TIMED_OUT, details
377+
return ObservedOutcome.TIMED_OUT, details, None
329378
signal_number = process["signal"]
330379
if signal_number is not None:
331380
try:
332381
name = signal.Signals(signal_number).name
333382
except ValueError:
334383
name = None
335-
return ObservedOutcome.CRASHED, {"signal_name": name}
384+
return ObservedOutcome.CRASHED, {"signal_name": name}, None
336385
if process["output_exhausted"]:
337-
return ObservedOutcome.BUDGET_EXHAUSTED, {}
386+
return ObservedOutcome.BUDGET_EXHAUSTED, {}, None
338387
if process["exit_code"] != 0:
339-
return ObservedOutcome.HARNESS_FAILED, {}
388+
return ObservedOutcome.HARNESS_FAILED, {}, None
340389
try:
341390
response = _worker_response(stdout)
342-
return ObservedOutcome(response["observed"]), response["details"]
391+
return ObservedOutcome(response["observed"]), response["details"], response
343392
except (json.JSONDecodeError, KeyError, RecursionError, ValueError, TypeError):
344-
return ObservedOutcome.HARNESS_FAILED, {}
393+
return ObservedOutcome.HARNESS_FAILED, {}, None
345394

346395

347396
def run_case(
@@ -359,17 +408,55 @@ def run_case(
359408
_relative(root, payload["manifest"])
360409
_relative(root, payload["artifact"])
361410
expectation = RobustnessExpectation(payload["expectation"])
362-
command = command or (
363-
sys.executable, "-m", "format_bench.robustness.worker", "--request", Path(request).as_posix()
364-
)
411+
default_worker = command is None
412+
planned_resource_limits = None
413+
if default_worker:
414+
planned_resource_limits = effective_worker_resource_limits()
415+
command = (
416+
sys.executable,
417+
"-m",
418+
"format_bench.robustness_worker_bootstrap",
419+
"--request",
420+
Path(request).as_posix(),
421+
)
422+
assert command is not None
365423
process, stdout, stderr = _process(
366424
command, root, timeout_seconds, output_budget_bytes
367425
)
368426
stdout_path = _save(root, Path(output_dir) / "stdout.txt", stdout)
369427
stderr_path = _save(root, Path(output_dir) / "stderr.txt", stderr)
370-
observed, details = _outcome(process, stdout)
428+
outcome = _outcome(process, stdout)
429+
observed = outcome[0]
430+
details: dict[str, object] = outcome[1]
431+
worker_response = outcome[2]
432+
resource_limits_confirmed = False
433+
confirmed_effective_limits: dict[str, int | None] | None = None
434+
confirmed_unsupported_limits: list[str] | None = None
435+
if default_worker and worker_response is not None:
436+
assert planned_resource_limits is not None
437+
response_effective_limits = worker_response.get(
438+
"worker_resource_limits_effective"
439+
)
440+
response_unsupported_limits = worker_response.get(
441+
"worker_resource_limits_unsupported"
442+
)
443+
resource_limits_confirmed = (
444+
response_effective_limits == planned_resource_limits.evidence()
445+
and response_unsupported_limits
446+
== list(planned_resource_limits.unsupported_resources)
447+
)
448+
if resource_limits_confirmed:
449+
assert response_effective_limits is not None
450+
assert response_unsupported_limits is not None
451+
confirmed_effective_limits = response_effective_limits
452+
confirmed_unsupported_limits = response_unsupported_limits
453+
else:
454+
observed = ObservedOutcome.HARNESS_FAILED
455+
details = {
456+
"error_type": "WorkerResourceLimitEvidenceMismatch",
457+
}
371458
verdict = robustness_verdict(expectation, observed)
372-
return {
459+
result: CaseResult = {
373460
"case_id": payload["case_id"],
374461
"target": payload["target"],
375462
"expectation": expectation,
@@ -380,3 +467,26 @@ def run_case(
380467
"stdout": stdout_path,
381468
"stderr": stderr_path,
382469
}
470+
if default_worker:
471+
assert planned_resource_limits is not None
472+
result["worker_resource_limits_requested"] = (
473+
DEFAULT_WORKER_RESOURCE_LIMITS.evidence()
474+
)
475+
if resource_limits_confirmed:
476+
assert confirmed_effective_limits is not None
477+
assert confirmed_unsupported_limits is not None
478+
result["worker_resource_limits_effective"] = confirmed_effective_limits
479+
result["worker_resource_limits_unsupported"] = (
480+
confirmed_unsupported_limits
481+
)
482+
result["worker_resource_limits_application"] = "APPLIED"
483+
else:
484+
result["worker_resource_limits_effective"] = {
485+
key: None
486+
for key in DEFAULT_WORKER_RESOURCE_LIMITS.evidence()
487+
}
488+
result["worker_resource_limits_unsupported"] = list(
489+
planned_resource_limits.unsupported_resources
490+
)
491+
result["worker_resource_limits_application"] = "UNCONFIRMED"
492+
return result

‎src/format_bench/robustness/worker.py‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
read_target,
1414
target_map,
1515
)
16+
from format_bench.worker_limits import (
17+
EffectiveWorkerResourceLimits,
18+
apply_worker_resource_limits,
19+
)
1620

1721

1822
# LLM contract: DISCOVERED -> ENCODED -> ROUNDTRIP_VERIFIED -> BENCHMARKED -> REPORTED.
@@ -131,11 +135,17 @@ def run_request(request_path: Path) -> dict:
131135
return {"schema_version": "1", "case_id": case_id, "observed": observed, "details": details}
132136

133137

134-
def main() -> None:
138+
def main(effective_limits: EffectiveWorkerResourceLimits | None = None) -> None:
135139
parser = argparse.ArgumentParser()
136140
parser.add_argument("--request", type=Path, required=True)
137141
args = parser.parse_args()
138-
print(strict_json_dumps(run_request(args.request), separators=(",", ":")))
142+
effective_limits = effective_limits or apply_worker_resource_limits()
143+
response = run_request(args.request)
144+
response["worker_resource_limits_effective"] = effective_limits.evidence()
145+
response["worker_resource_limits_unsupported"] = list(
146+
effective_limits.unsupported_resources
147+
)
148+
print(strict_json_dumps(response, separators=(",", ":")))
139149

140150

141151
if __name__ == "__main__":

0 commit comments

Comments
 (0)