-
Notifications
You must be signed in to change notification settings - Fork 1
fix(testmon): run resumable shards with bounded argv #3960
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6042be0
0e1cbf9
3278437
50f102e
a4297fc
92ca98f
1da9c07
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2631,7 +2631,19 @@ def _prepare_testmon_seed_shards( | |
| shards = ( | ||
| prior_shards | ||
| if prior_shards is not None | ||
| else (seed_shard_plan(expected, shard_size=TESTMON_SEED_SHARD_SIZE) if expected else []) | ||
| else ( | ||
| seed_shard_plan( | ||
| expected, | ||
| shard_size=TESTMON_SEED_SHARD_SIZE, | ||
| serial_nodeids=[ | ||
| nodeid | ||
| for nodeid, markers in (selection or {}).get("selected_node_markers", {}).items() | ||
| if "load_sensitive" in markers or "tui" in markers | ||
| ], | ||
| ) | ||
| if expected | ||
| else [] | ||
| ) | ||
| ) | ||
| payload = { | ||
| **dict(prepared), | ||
|
|
@@ -2646,16 +2658,64 @@ def _prepare_testmon_seed_shards( | |
| return payload | ||
|
|
||
|
|
||
| def _seed_shard_command(collection_command: Sequence[str], shard: Mapping[str, Any]) -> list[str]: | ||
| """Build a dynamically balanced explicit-node pytest-testmon invocation.""" | ||
| def _seed_shard_command( | ||
| collection_command: Sequence[str], | ||
| shard: Mapping[str, Any], | ||
| *, | ||
| nodeids_file: Path, | ||
| ) -> list[str]: | ||
| """Build a bounded-argv, dynamically balanced pytest-testmon invocation. | ||
|
|
||
| A full shard's node IDs can exceed the host's ``execve`` argument budget | ||
| once ``systemd-run`` and the managed environment are included. Pytest's | ||
| response-file syntax keeps the authoritative node list in the run | ||
| artifact while making the child command size independent of shard size. | ||
| """ | ||
| nodeids = shard.get("nodeids") | ||
| if not isinstance(nodeids, list) or not nodeids: | ||
| raise ValueError("testmon seed shard is missing nodeids") | ||
| command = [argument for argument in collection_command if argument != "--collect-only"] | ||
| command.extend(["--dist=worksteal", "--testmon", "--testmon-noselect", *nodeids]) | ||
| nodeids_file.parent.mkdir(parents=True, exist_ok=True) | ||
| nodeids_file.write_text("\n".join(nodeids) + "\n", encoding="utf-8") | ||
|
Comment on lines
+2677
to
+2678
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the checkout filesystem fills or becomes unwritable between collection and shard execution, the newly added AGENTS.md reference: AGENTS.md:L521-L524 Useful? React with 👍 / 👎. |
||
| command: list[str] = [] | ||
| skip_next = False | ||
| for argument in collection_command: | ||
| if skip_next: | ||
| skip_next = False | ||
| continue | ||
| if argument == "--collect-only": | ||
| continue | ||
| if argument in {"-n", "--numprocesses"}: | ||
| skip_next = True | ||
| continue | ||
| if argument.startswith("--numprocesses=") or (argument.startswith("-n") and len(argument) > 2): | ||
| continue | ||
| command.append(argument) | ||
| # Collection is deliberately serial, but execution is not. pytest-testmon | ||
| # has an xdist-aware controller database; retaining the managed worker pool | ||
| # here avoids turning a 20k-node seed into hours of serial fixture setup. | ||
| if shard.get("execution_mode") == "serial": | ||
| command.extend(["-n", "0", "--testmon", "--testmon-noselect", f"@{nodeids_file}"]) | ||
| else: | ||
| command.extend( | ||
| [ | ||
| "--dist=loadgroup", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a shard contains any existing AGENTS.md reference: AGENTS.md:L338-L341 Useful? React with 👍 / 👎. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a parallel shard contains an AGENTS.md reference: AGENTS.md:L338-L341 Useful? React with 👍 / 👎. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
With the default adaptive worker count above zero, AGENTS.md reference: AGENTS.md:L338-L341 Useful? React with 👍 / 👎. |
||
| *_pytest_worker_args(maximum=10), | ||
| "--testmon", | ||
| "--testmon-noselect", | ||
| f"@{nodeids_file}", | ||
| ] | ||
| ) | ||
| return command | ||
|
|
||
|
|
||
| def _canonical_seed_nodeid(nodeid: str, expected_nodeids: Sequence[str]) -> str: | ||
| """Map xdist's ``nodeid@group`` reports back to the collected node ID.""" | ||
| if nodeid in expected_nodeids: | ||
| return nodeid | ||
| candidates = [expected for expected in expected_nodeids if nodeid.startswith(expected + "@")] | ||
| return max(candidates, key=len, default=nodeid) | ||
|
|
||
|
|
||
| def _seed_shard_outcomes(shards: Sequence[Mapping[str, Any]]) -> list[dict[str, Any]]: | ||
| """Flatten the shard ledger in canonical node order for legacy readers.""" | ||
| outcomes: dict[str, dict[str, Any]] = {} | ||
|
|
@@ -2684,7 +2744,10 @@ def _checkpoint_testmon_seed_shard( | |
| nodeids = shard["nodeids"] | ||
| artifact_dir = _safe_testmon_artifact_dir(step.get("artifact_dir")) | ||
| selection = _read_json_artifact(artifact_dir / "selection.json") if artifact_dir is not None else None | ||
| selected = _seed_selection_nodeids(selection) if isinstance(selection, Mapping) else None | ||
| selected_raw = _seed_selection_nodeids(selection) if isinstance(selection, Mapping) else None | ||
| selected = ( | ||
| sorted(_canonical_seed_nodeid(nodeid, nodeids) for nodeid in selected_raw) if selected_raw is not None else None | ||
| ) | ||
| database = _testmon_database_state(nodeids) | ||
| prior = { | ||
| str(item["nodeid"]): item | ||
|
|
@@ -2781,6 +2844,7 @@ def _seed_node_outcomes_from_events( | |
| nodeid = event.get("nodeid") | ||
| if not isinstance(nodeid, str) or not nodeid: | ||
| continue | ||
| nodeid = _canonical_seed_nodeid(nodeid, expected_nodeids) | ||
| if event.get("event") == "test_started": | ||
| started.add(nodeid) | ||
| elif event.get("event") == "test_finished": | ||
|
|
@@ -3422,7 +3486,32 @@ def main(argv: list[str] | None = None) -> int: | |
| continue | ||
| shard_index = int(shard["index"]) | ||
| shard_label = f"pytest seed-testmon shard {shard_index}/{len(shards)}" | ||
| shard_cmd = _seed_shard_command(cmd, shard) | ||
| shard_args_path = verify_run.run_dir / "seed-shards" / f"{shard_index:04d}.args" | ||
| try: | ||
| shard_cmd = _seed_shard_command(cmd, shard, nodeids_file=shard_args_path) | ||
| except (OSError, PytestResourceError) as exc: | ||
| resource_failure_result = { | ||
| "name": shard_label, | ||
| "duration_s": 0.0, | ||
| "exit": 125, | ||
| "diagnosis": ( | ||
| "pytest_resource_refusal" | ||
| if isinstance(exc, PytestResourceError) | ||
| else "testmon_seed_args_file_write_failed" | ||
| ), | ||
| "error": str(exc), | ||
| "shard_index": shard_index, | ||
| "shard_count": len(shards), | ||
| "shard_nodeid_count": len(shard["nodeids"]), | ||
| } | ||
| step_results.append(resource_failure_result) | ||
| prepared_seed_attempt = _checkpoint_testmon_seed_shard( | ||
| prepared=prepared_seed_attempt, | ||
| shard_index=shard_index, | ||
| step=resource_failure_result, | ||
| ) | ||
|
Comment on lines
+3508
to
+3512
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When response-file creation raises AGENTS.md reference: AGENTS.md:L521-L524 Useful? React with 👍 / 👎. |
||
| exit_code = 125 | ||
| break | ||
| _warn_low_memory() | ||
| shard_rc, shard_elapsed, shard_metadata = _run(shard_label, shard_cmd, run=verify_run) | ||
| shard_result: dict[str, Any] = { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When an interrupted or malformed attempt repeats a node in two otherwise valid shard records, updating a set hides the overlap, so this validator accepts a ledger that violates its stated disjoint-shard contract. Resume/finalization can then process the node twice, and
_seed_shard_outcomessilently uses the later result; a passing duplicate can replace an earlier failure. Reject a node already present inobserved, or additionally require the total observed count to equal the expected count.Useful? React with 👍 / 👎.