polyrun 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +58 -0
- data/lib/polyrun/cli/ci_shard_hooks.rb +121 -0
- data/lib/polyrun/cli/ci_shard_run_command.rb +6 -22
- data/lib/polyrun/cli/failure_commands.rb +92 -0
- data/lib/polyrun/cli/help.rb +4 -1
- data/lib/polyrun/cli/hooks_command.rb +97 -0
- data/lib/polyrun/cli/run_shards_command.rb +4 -1
- data/lib/polyrun/cli/run_shards_parallel_children.rb +99 -0
- data/lib/polyrun/cli/run_shards_plan_boot_phases.rb +37 -2
- data/lib/polyrun/cli/run_shards_plan_options.rb +10 -2
- data/lib/polyrun/cli/run_shards_run.rb +73 -64
- data/lib/polyrun/cli.rb +8 -2
- data/lib/polyrun/config.rb +10 -0
- data/lib/polyrun/hooks/dsl.rb +128 -0
- data/lib/polyrun/hooks/worker_runner.rb +27 -0
- data/lib/polyrun/hooks/worker_shell.rb +50 -0
- data/lib/polyrun/hooks.rb +185 -0
- data/lib/polyrun/reporting/failure_merge.rb +135 -0
- data/lib/polyrun/reporting/rspec_failure_fragment_formatter.rb +95 -0
- data/lib/polyrun/rspec.rb +14 -0
- data/lib/polyrun/version.rb +1 -1
- data/lib/polyrun.rb +1 -0
- metadata +11 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7666af9186562083f29dc56e6c867e48b877acdff6ad28ff8c351e8d3c308582
|
|
4
|
+
data.tar.gz: 503f5435deb22112044f7841a82728e6782a770eb656859419e8412d623dcff0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d0d5d248f1e072c446049bafff111db6e29d784a4a0992528214a6e29cca7b156b67144e6b9c22fc71fa9143f110ac443cda2953219aa816c5562d3247b5e02b
|
|
7
|
+
data.tar.gz: d0be776ce5d4a7a5acacfe237a4d07a47c078562aee6a054fb5b63a31a78e199bc2b5ee276b0a8e77a0f0cacd3e4f03cb72d8fd5037baf782d0efb8a1cdf1b04
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 1.4.1 (2026-04-16)
|
|
4
|
+
|
|
5
|
+
- Add `polyrun merge-failures` and `run-shards --merge-failures` / `--merge-failures-output` / `--merge-failures-format`; merge per-worker JSONL under `tmp/polyrun_failures/polyrun-failure-fragment-*.jsonl` (or RSpec JSON via `-i`). Run merge after all workers exit, including when a shard failed (`--merge-coverage` still runs only after all shards succeed).
|
|
6
|
+
- Add `Polyrun::Reporting::FailureMerge`, `Polyrun::RSpec.install_failure_fragments!`, and `Polyrun::Reporting::RspecFailureFragmentFormatter`; parent sets `POLYRUN_FAILURE_FRAGMENTS=1` on workers when merge-failures is enabled.
|
|
7
|
+
- Add optional `reporting:` in `polyrun.yml` and `Polyrun::Config#reporting` for merge-failures toggles and paths; honor `POLYRUN_MERGE_FAILURES`, `POLYRUN_MERGED_FAILURES_OUT`, `POLYRUN_MERGED_FAILURES_FORMAT`; set `POLYRUN_MERGED_FAILURES_PATH` for `after_suite` when merge wrote a file.
|
|
8
|
+
- On bad JSONL or non-RSpec JSON, raise `Polyrun::Error` with path and line where applicable; `merge-failures` exits 1 without a full stack trace; a failed merge after successful workers forces `run-shards` exit 1.
|
|
9
|
+
- Fix `run_shards_plan_ready_log` to take `cfg` so debug logging for merge-failures does not raise `NameError`.
|
|
10
|
+
|
|
11
|
+
## 1.4.0 (2026-04-16)
|
|
12
|
+
|
|
13
|
+
- Add `hooks:` in `polyrun.yml` — shell commands for `before_suite` / `after_suite`, `before_shard` / `after_shard`, `before_worker` / `after_worker` (RSpec-style YAML keys `before(:suite)`, `before(:all)`, `before(:each)` accepted). Wire hooks into `run-shards`, `parallel-rspec`, and `ci-shard-*`.
|
|
14
|
+
- Add `hooks.ruby` / `hooks.ruby_file` and `Polyrun::Hooks::Dsl` (`before(:suite)` … `after(:each)` blocks); worker Ruby hooks run in the child via `ruby -e` + `POLYRUN_HOOKS_RUBY_FILE`.
|
|
15
|
+
- Add `polyrun hook run <phase>` (`--shard` / `--total` optional). Set `POLYRUN_HOOKS_DISABLE=1` to skip hooks during orchestration only; `hook run` still executes.
|
|
16
|
+
- On `ci-shard-run` / `ci-shard-rspec`, skip automatic `before_suite` / `after_suite` when `POLYRUN_SHARD_TOTAL` > 1 (matrix); run suite hooks once via `polyrun hook run` or set `POLYRUN_HOOKS_SUITE_PER_MATRIX_JOB=1` to run them on every matrix job.
|
|
17
|
+
- Document hook phases, matrix vs suite, and `after_shard` ordering in `README.md`; list `Polyrun::Hooks` and `Polyrun::Hooks::Dsl` in the library section.
|
|
18
|
+
|
|
3
19
|
## 1.3.0 (2026-04-15)
|
|
4
20
|
|
|
5
21
|
- Add safe parsing for `ci-shard-run` / `ci-shard-rspec` `--shard-processes` and `--workers` (warn + exit 2 on missing or non-integer values).
|
data/README.md
CHANGED
|
@@ -25,6 +25,61 @@ Capybara and Playwright stay in your application; Polyrun does not replace brows
|
|
|
25
25
|
4. Run workers with `bin/polyrun run-shards --workers N -- bundle exec rspec`: N separate OS processes, each running RSpec with its own file list from `partition.paths_file`, or `spec/spec_paths.txt`, or else `spec/**/*_spec.rb`. Stderr shows where paths came from; after a successful multi-worker run it reminds you to run merge-coverage unless you use `parallel-rspec` or `run-shards --merge-coverage`.
|
|
26
26
|
5. Merge artifacts with `bin/polyrun merge-coverage` on `coverage/polyrun-fragment-*.json` (one fragment per `POLYRUN_SHARD_INDEX` when coverage is on), or use `bin/polyrun parallel-rspec` or `run-shards --merge-coverage` so Polyrun runs merge for you. Optional: `merge-timing`, `report-timing`, `report-junit`.
|
|
27
27
|
|
|
28
|
+
### Hooks (`hooks:` in `polyrun.yml`)
|
|
29
|
+
|
|
30
|
+
Optional **shell** commands and/or a **Ruby DSL** file for instrumentation (telemetry, Slack, logging, manual debugging). Names mirror RSpec’s API (`before(:suite)`, `before(:all)`, `before(:each)`), but **Polyrun hooks are about process orchestration**, not RSpec example groups. Below, **suite / shard / worker** mean Polyrun’s model unless stated otherwise.
|
|
31
|
+
|
|
32
|
+
#### What “suite”, “shard”, and “worker” mean
|
|
33
|
+
|
|
34
|
+
| Term | Process | Meaning |
|
|
35
|
+
|------|---------|---------|
|
|
36
|
+
| **Suite** | **Parent** only | One **orchestration run** on a single machine: a single `polyrun run-shards` / `parallel-rspec` / `ci-shard-run` (with **one** global shard, see below). `before_suite` runs once before any worker is started; `after_suite` runs once after all workers have exited and (when used) merge-coverage has finished. This is **not** the same as “the whole RSpec suite in one process”—with `--workers N`, RSpec runs in **N separate processes**, each with its own examples. |
|
|
37
|
+
| **Shard** | **Parent** only | One **partition** of the path list for this run, identified by `POLYRUN_SHARD_INDEX` / `POLYRUN_SHARD_TOTAL` **for that parallel layout** (0 … N−1 for `run-shards` with N workers). `before_shard` runs in the parent **immediately before** `Process.spawn` for that index; `after_shard` runs **after** that child has exited. The parent **waits workers in shard index order** (0, then 1, …), so `after_shard` runs in that order—not in “who finished first” order if workers overlap in time. Empty partitions are skipped (no spawn, no hooks). |
|
|
38
|
+
| **Worker** | **Child** OS process | The process that runs your command after `--` (e.g. `bundle exec rspec …`). `before_worker` / `after_worker` run **in that child**, directly before and after the test command. **Individual examples run only after `before_worker` completes** (inside the same process as RSpec/Minitest). |
|
|
39
|
+
|
|
40
|
+
**CI matrix** (`POLYRUN_SHARD_TOTAL` > 1, one job per index): each job is a **global shard**, not a full “suite” in the pipeline sense. **`ci-shard-run` / `ci-shard-rspec` with one process per job do not run `before_suite` / `after_suite` automatically**—otherwise they would run once per matrix cell. Put pipeline-wide setup/teardown in a **separate CI step** (e.g. `bin/polyrun hook run before_suite` and `hook run after_suite` once), or set `POLYRUN_HOOKS_SUITE_PER_MATRIX_JOB=1` to restore the old behaviour (suite hooks on every matrix job). **`before_shard` / `after_shard` / worker hooks still run** per job, with `POLYRUN_SHARD_INDEX` / `POLYRUN_SHARD_TOTAL` set from the matrix. A **single** non-matrix `ci-shard-run` (`POLYRUN_SHARD_TOTAL` is 1) still runs suite hooks like `run-shards --workers 1`. Fan-out on one host (`--shard-processes` > 1) still runs **`before_suite` / `after_suite` once** for that job, around the local workers.
|
|
41
|
+
|
|
42
|
+
#### Lifecycle (typical `run-shards` with multiple workers)
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
[Parent] before_suite
|
|
46
|
+
[Parent] for each shard index i that has paths:
|
|
47
|
+
before_shard(i) → spawn worker i
|
|
48
|
+
[Child i] before_worker → test runner starts → examples run → runner exits
|
|
49
|
+
[Parent] after_shard(i) (parent waits children in shard index order 0…N−1, not global finish order)
|
|
50
|
+
[Parent] merge-coverage (if requested)
|
|
51
|
+
[Parent] after_suite
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`after_worker` runs in the child after the test command exits, before the parent’s `after_shard`.
|
|
55
|
+
|
|
56
|
+
#### Order and priority within one phase
|
|
57
|
+
|
|
58
|
+
1. **Ruby DSL, then shell (YAML)** — For the same phase (e.g. `before_suite`), all **Ruby** blocks from `hooks.ruby` run first, then every **shell** command from YAML for that phase.
|
|
59
|
+
2. **Multiple Ruby blocks** — In one DSL file, registrations run in **source order** (e.g. two `before(:suite)` blocks run top to bottom).
|
|
60
|
+
3. **Multiple shell commands** — Use a **YAML list**; entries run in list order:
|
|
61
|
+
```yaml
|
|
62
|
+
before_suite:
|
|
63
|
+
- echo first
|
|
64
|
+
- echo second
|
|
65
|
+
```
|
|
66
|
+
4. **Duplicate YAML keys** — Do **not** repeat the same key (e.g. two `before_suite:` lines). Parsers may keep only one value; behaviour is undefined. Prefer a **list** under a single key.
|
|
67
|
+
5. **Failure** — If any step in a phase fails (non-zero exit from shell; uncaught error in Ruby), orchestration stops or marks failure per existing `run-shards` rules; `after_worker` shell steps use `|| true` so a failing teardown does not mask the test exit code (Ruby `after_worker` is wrapped similarly in the worker script).
|
|
68
|
+
|
|
69
|
+
Environment includes `POLYRUN_HOOK_PHASE`, `POLYRUN_HOOK=1`, `POLYRUN_HOOK_ORCHESTRATOR` (`1` in parent, `0` in workers), `POLYRUN_SHARD_*`, and `POLYRUN_SUITE_EXIT_STATUS` on `after_suite`. Worker children get `POLYRUN_HOOKS_RUBY_FILE` when using the Ruby DSL. Set `POLYRUN_HOOKS_DISABLE=1` to skip hooks during `run-shards` / `parallel-rspec` / `ci-shard-*` (orchestration only); `polyrun hook run` still executes hooks. **`POLYRUN_HOOKS_SUITE_PER_MATRIX_JOB=1`** — when set, run `before_suite` / `after_suite` on every CI matrix job (not recommended for expensive global setup).
|
|
70
|
+
|
|
71
|
+
**YAML keys (shell) and RSpec-style names in YAML:**
|
|
72
|
+
|
|
73
|
+
| YAML key | DSL in `hooks.ruby` | When |
|
|
74
|
+
|----------|----------------------|------|
|
|
75
|
+
| `before_suite` / `after_suite` | `before(:suite)` / `after(:suite)` | Parent: once per orchestration on one host; **skipped** for `ci-shard-run` when `POLYRUN_SHARD_TOTAL` > 1 unless `POLYRUN_HOOKS_SUITE_PER_MATRIX_JOB=1` (see matrix paragraph above) |
|
|
76
|
+
| `before_shard` / `after_shard` | `before(:all)` / `after(:all)` | Parent: per shard index (spawn / after exit) |
|
|
77
|
+
| `before_worker` / `after_worker` | `before(:each)` / `after(:each)` | Child: around the test command |
|
|
78
|
+
|
|
79
|
+
**Ruby DSL (`hooks.ruby` or `hooks.ruby_file`):** path to a `.rb` file (relative to the project root). Blocks receive a `Hash` with **string keys** (same env as shell hooks). Worker-phase Ruby hooks run in the child via `ruby -e 'require "polyrun"; …'`.
|
|
80
|
+
|
|
81
|
+
Run one phase by hand: `bin/polyrun hook run before_suite` (optional `--shard N --total M`). YAML may use quoted keys such as `"before(:suite)"` instead of `before_suite`.
|
|
82
|
+
|
|
28
83
|
Quick CLI samples:
|
|
29
84
|
|
|
30
85
|
If the current directory already has `polyrun.yml` or `config/polyrun.yml`, you can omit `-c` (same as `Config.load` default discovery). Pass `-c PATH` or set `POLYRUN_CONFIG` when the file lives elsewhere or uses another name.
|
|
@@ -41,6 +96,7 @@ bin/polyrun env --shard 0 --total 4 # print DATABASE_URL exports from polyrun.
|
|
|
41
96
|
bin/polyrun init --list
|
|
42
97
|
bin/polyrun init --profile gem -o polyrun.yml # starter YAML; see docs/SETUP_PROFILE.md
|
|
43
98
|
bin/polyrun quick # Polyrun::Quick examples under spec/polyrun_quick/ or test/polyrun_quick/
|
|
99
|
+
bin/polyrun hook run before_suite # run hooks.before_suite from polyrun.yml (manual / CI)
|
|
44
100
|
```
|
|
45
101
|
|
|
46
102
|
### Matrix shards and timing
|
|
@@ -87,6 +143,8 @@ That single require loads the CLI and core library **without** loading RSpec or
|
|
|
87
143
|
| `Polyrun::Prepare::Assets` | Digest trees, marker file, `assets:precompile`. |
|
|
88
144
|
| `Polyrun::Database::Shard` | Shard env map, `%{shard}` DB names, URL path suffix for `postgres://`, `mysql2://`, `mongodb://`, etc. |
|
|
89
145
|
| `Polyrun::Database::UrlBuilder` | URLs from `polyrun.yml` `databases:` — nested blocks or `adapter:` for common Rails stacks (`postgresql`, `mysql`/`mysql2`, `trilogy`, `sqlserver`/`mssql`, `sqlite3`/`sqlite`, `mongodb`/`mongo`). |
|
|
146
|
+
| `Polyrun::Hooks` | Load from `Config#hooks`; `run_phase` / `run_phase_if_enabled`; `build_worker_shell_script` wraps the worker command. |
|
|
147
|
+
| `Polyrun::Hooks::Dsl` | Ruby hook file (`hooks.ruby`); `before(:suite)` / `after(:each)` etc. in `config/polyrun_hooks.rb` (see README). |
|
|
90
148
|
|
|
91
149
|
## Development
|
|
92
150
|
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
module Polyrun
|
|
2
|
+
class CLI
|
|
3
|
+
# Suite / shard / worker shell hooks for +ci-shard-run+ / +ci-shard-rspec+.
|
|
4
|
+
module CiShardHooks
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
# rubocop:disable Metrics/AbcSize -- suite hooks + spawn/wait + failure paths
|
|
8
|
+
def ci_shard_run_fanout!(ctx)
|
|
9
|
+
hook_cfg = Polyrun::Hooks.from_config(ctx[:cfg])
|
|
10
|
+
suite_started = false
|
|
11
|
+
exit_code = 1
|
|
12
|
+
|
|
13
|
+
begin
|
|
14
|
+
env_suite = ENV.to_h.merge(
|
|
15
|
+
"POLYRUN_HOOK_ORCHESTRATOR" => "1",
|
|
16
|
+
"POLYRUN_SHARD_TOTAL" => ctx[:workers].to_s
|
|
17
|
+
)
|
|
18
|
+
code = hook_cfg.run_phase_if_enabled(:before_suite, env_suite)
|
|
19
|
+
return code if code != 0
|
|
20
|
+
|
|
21
|
+
suite_started = true
|
|
22
|
+
|
|
23
|
+
pids, spawn_err = run_shards_spawn_workers(ctx, hook_cfg)
|
|
24
|
+
if spawn_err
|
|
25
|
+
exit_code = spawn_err
|
|
26
|
+
return spawn_err
|
|
27
|
+
end
|
|
28
|
+
return 1 if pids.empty?
|
|
29
|
+
|
|
30
|
+
run_shards_warn_interleaved(ctx[:parallel], pids.size)
|
|
31
|
+
shard_results, wait_hook_err = run_shards_wait_all_children(pids, hook_cfg, ctx)
|
|
32
|
+
failed = shard_results.reject { |r| r[:success] }.map { |r| r[:shard] }
|
|
33
|
+
|
|
34
|
+
if failed.any?
|
|
35
|
+
Polyrun::Log.warn "polyrun ci-shard: finished #{pids.size} worker(s) (some failed)"
|
|
36
|
+
run_shards_log_failed_reruns(failed, shard_results, ctx[:plan], ctx[:parallel], ctx[:workers], ctx[:cmd])
|
|
37
|
+
exit_code = 1
|
|
38
|
+
exit_code = 1 if wait_hook_err != 0
|
|
39
|
+
return exit_code
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
exit_code = (wait_hook_err == 0) ? 0 : 1
|
|
43
|
+
Polyrun::Log.warn "polyrun ci-shard: finished #{pids.size} worker(s) (exit 0)" if exit_code == 0
|
|
44
|
+
exit_code
|
|
45
|
+
ensure
|
|
46
|
+
if suite_started
|
|
47
|
+
env_after = ENV.to_h.merge(
|
|
48
|
+
"POLYRUN_HOOK_ORCHESTRATOR" => "1",
|
|
49
|
+
"POLYRUN_SHARD_TOTAL" => ctx[:workers].to_s,
|
|
50
|
+
"POLYRUN_SUITE_EXIT_STATUS" => exit_code.to_s
|
|
51
|
+
)
|
|
52
|
+
hook_cfg.run_phase_if_enabled(:after_suite, env_after)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
# rubocop:enable Metrics/AbcSize
|
|
57
|
+
|
|
58
|
+
# One matrix shard, one OS process: same hook phases as +run-shards+ with +--workers 1+ (no +exec+ when hooks exist).
|
|
59
|
+
# rubocop:disable Metrics/AbcSize -- suite / shard / worker lifecycle
|
|
60
|
+
def ci_shard_run_single!(cmd, paths, cfg, pc, _config_path)
|
|
61
|
+
hook_cfg = Polyrun::Hooks.from_config(cfg)
|
|
62
|
+
if hook_cfg.empty? || Polyrun::Hooks.disabled?
|
|
63
|
+
exec(*cmd, *paths)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
si = Polyrun::Config::Resolver.resolve_shard_index(pc)
|
|
67
|
+
st = Polyrun::Config::Resolver.resolve_shard_total(pc)
|
|
68
|
+
suite_started = false
|
|
69
|
+
exit_code = 1
|
|
70
|
+
# Distributed CI matrix (N > 1 global shards): each job is one shard; suite hooks are pipeline-wide.
|
|
71
|
+
# Run them once via +polyrun hook run before_suite+ / +after_suite+ (e.g. dedicated job), or set
|
|
72
|
+
# +POLYRUN_HOOKS_SUITE_PER_MATRIX_JOB=1+ to run suite hooks on every matrix job.
|
|
73
|
+
matrix_shards = st > 1
|
|
74
|
+
run_suite_hooks = !matrix_shards || Polyrun::Hooks.suite_per_matrix_job?
|
|
75
|
+
|
|
76
|
+
begin
|
|
77
|
+
env_orch = ENV.to_h.merge(
|
|
78
|
+
"POLYRUN_HOOK_ORCHESTRATOR" => "1",
|
|
79
|
+
"POLYRUN_SHARD_INDEX" => si.to_s,
|
|
80
|
+
"POLYRUN_SHARD_TOTAL" => st.to_s
|
|
81
|
+
)
|
|
82
|
+
if run_suite_hooks
|
|
83
|
+
code = hook_cfg.run_phase_if_enabled(:before_suite, env_orch)
|
|
84
|
+
return code if code != 0
|
|
85
|
+
|
|
86
|
+
suite_started = true
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
code = hook_cfg.run_phase_if_enabled(:before_shard, env_orch)
|
|
90
|
+
return code if code != 0
|
|
91
|
+
|
|
92
|
+
mx, mt = ci_shard_matrix_context(pc, 1)
|
|
93
|
+
child_env = shard_child_env(cfg: cfg, workers: 1, shard: 0, matrix_index: mx, matrix_total: mt)
|
|
94
|
+
child_env = child_env.merge("POLYRUN_HOOK_ORCHESTRATOR" => "0")
|
|
95
|
+
child_env = hook_cfg.merge_worker_ruby_env(child_env)
|
|
96
|
+
|
|
97
|
+
if hook_cfg.worker_hooks? && !Polyrun::Hooks.disabled?
|
|
98
|
+
system(child_env, "sh", "-c", hook_cfg.build_worker_shell_script(cmd, paths))
|
|
99
|
+
else
|
|
100
|
+
system(child_env, *cmd, *paths)
|
|
101
|
+
end
|
|
102
|
+
exit_code = $?.exitstatus
|
|
103
|
+
|
|
104
|
+
rc = hook_cfg.run_phase_if_enabled(:after_shard, env_orch.merge(
|
|
105
|
+
"POLYRUN_WORKER_EXIT_STATUS" => exit_code.to_s
|
|
106
|
+
))
|
|
107
|
+
exit_code = rc if rc != 0
|
|
108
|
+
|
|
109
|
+
exit_code
|
|
110
|
+
ensure
|
|
111
|
+
if suite_started
|
|
112
|
+
hook_cfg.run_phase_if_enabled(:after_suite, env_orch.merge(
|
|
113
|
+
"POLYRUN_SUITE_EXIT_STATUS" => exit_code.to_s
|
|
114
|
+
))
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
# rubocop:enable Metrics/AbcSize
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
require "shellwords"
|
|
2
2
|
|
|
3
|
+
require_relative "ci_shard_hooks"
|
|
4
|
+
|
|
3
5
|
module Polyrun
|
|
4
6
|
class CLI
|
|
5
7
|
# One CI matrix job = one global shard (POLYRUN_SHARD_INDEX / POLYRUN_SHARD_TOTAL), not +run-shards+
|
|
@@ -14,6 +16,8 @@ module Polyrun
|
|
|
14
16
|
# After +--+, prefer **multiple argv tokens** (+bundle+, +exec+, +rspec+, …). A single token that
|
|
15
17
|
# contains spaces is split with +Shellwords+ (not a full shell); exotic quoting differs from +sh -c+.
|
|
16
18
|
module CiShardRunCommand
|
|
19
|
+
include CiShardHooks
|
|
20
|
+
|
|
17
21
|
private
|
|
18
22
|
|
|
19
23
|
# @return [Array(Array<String>, Integer)] [paths, 0] on success, or [nil, exit_code] on failure
|
|
@@ -47,24 +51,6 @@ module Polyrun
|
|
|
47
51
|
[resolve_shard_index(pc), n]
|
|
48
52
|
end
|
|
49
53
|
|
|
50
|
-
def ci_shard_run_fanout!(ctx)
|
|
51
|
-
pids = run_shards_spawn_workers(ctx)
|
|
52
|
-
return 1 if pids.empty?
|
|
53
|
-
|
|
54
|
-
run_shards_warn_interleaved(ctx[:parallel], pids.size)
|
|
55
|
-
shard_results = run_shards_wait_all_children(pids)
|
|
56
|
-
failed = shard_results.reject { |r| r[:success] }.map { |r| r[:shard] }
|
|
57
|
-
|
|
58
|
-
if failed.any?
|
|
59
|
-
Polyrun::Log.warn "polyrun ci-shard: finished #{pids.size} worker(s) (some failed)"
|
|
60
|
-
run_shards_log_failed_reruns(failed, shard_results, ctx[:plan], ctx[:parallel], ctx[:workers], ctx[:cmd])
|
|
61
|
-
return 1
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
Polyrun::Log.warn "polyrun ci-shard: finished #{pids.size} worker(s) (exit 0)"
|
|
65
|
-
0
|
|
66
|
-
end
|
|
67
|
-
|
|
68
54
|
def ci_shard_fanout_context(cfg:, pc:, paths:, shard_processes:, cmd:, config_path:)
|
|
69
55
|
plan = ci_shard_local_plan!(paths, shard_processes)
|
|
70
56
|
mx, mt = ci_shard_matrix_context(pc, shard_processes)
|
|
@@ -113,8 +99,7 @@ module Polyrun
|
|
|
113
99
|
return code if code != 0
|
|
114
100
|
|
|
115
101
|
if shard_processes <= 1
|
|
116
|
-
|
|
117
|
-
return 0
|
|
102
|
+
return ci_shard_run_single!(cmd, paths, cfg, pc, config_path)
|
|
118
103
|
end
|
|
119
104
|
|
|
120
105
|
ctx = ci_shard_fanout_context(
|
|
@@ -145,8 +130,7 @@ module Polyrun
|
|
|
145
130
|
cmd = ["bundle", "exec", "rspec", *rspec_argv]
|
|
146
131
|
|
|
147
132
|
if shard_processes <= 1
|
|
148
|
-
|
|
149
|
-
return 0
|
|
133
|
+
return ci_shard_run_single!(cmd, paths, cfg, pc, config_path)
|
|
150
134
|
end
|
|
151
135
|
|
|
152
136
|
ctx = ci_shard_fanout_context(
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
require_relative "../reporting/failure_merge"
|
|
6
|
+
|
|
7
|
+
module Polyrun
|
|
8
|
+
class CLI
|
|
9
|
+
module FailureCommands
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def cmd_merge_failures(argv, _config_path)
|
|
13
|
+
inputs, output, format = merge_failures_parse_argv(argv)
|
|
14
|
+
if inputs.empty?
|
|
15
|
+
Polyrun::Log.warn "merge-failures: need at least one existing -i FILE (after glob expansion)"
|
|
16
|
+
return 2
|
|
17
|
+
end
|
|
18
|
+
Polyrun::Log.warn "merge-failures: merging #{inputs.size} fragment(s)" if @verbose
|
|
19
|
+
n = merge_failures_merge_files_or_warn!(inputs, output: output, format: format)
|
|
20
|
+
return 1 if n.nil?
|
|
21
|
+
|
|
22
|
+
Polyrun::Log.puts File.expand_path(output)
|
|
23
|
+
Polyrun::Log.warn "merge-failures: #{n} failure row(s)" if @verbose
|
|
24
|
+
0
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def merge_failures_merge_files_or_warn!(inputs, output:, format:)
|
|
28
|
+
Polyrun::Reporting::FailureMerge.merge_files!(inputs, output: output, format: format)
|
|
29
|
+
rescue Polyrun::Error => e
|
|
30
|
+
Polyrun::Log.warn e.message.to_s
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def merge_failures_parse_argv(argv)
|
|
35
|
+
inputs = []
|
|
36
|
+
output = File.join("tmp", "polyrun_failures", "merged.jsonl")
|
|
37
|
+
format = "jsonl"
|
|
38
|
+
OptionParser.new do |opts|
|
|
39
|
+
opts.banner = "usage: polyrun merge-failures -i FILE [-i FILE] [-o PATH] [--format jsonl|json]"
|
|
40
|
+
opts.on("-i", "--input FILE", "JSONL fragment or RSpec JSON (repeatable; globs ok)") do |f|
|
|
41
|
+
expand_merge_input_pattern(f).each { |x| inputs << x }
|
|
42
|
+
end
|
|
43
|
+
opts.on("-o", "--output PATH", String) { |v| output = v }
|
|
44
|
+
opts.on("--format VAL", "jsonl (default) or json") { |v| format = v }
|
|
45
|
+
end.parse!(argv)
|
|
46
|
+
inputs.uniq!
|
|
47
|
+
inputs.select! { |p| File.file?(p) }
|
|
48
|
+
[inputs, output, format]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# After run-shards workers exit: merge polyrun failure fragments when requested.
|
|
52
|
+
# Runs even when shards failed (unlike --merge-coverage).
|
|
53
|
+
# @return [String, nil] absolute path to merged file, or nil when skipped / nothing written
|
|
54
|
+
def merge_failures_after_shards(ctx)
|
|
55
|
+
return nil unless ctx[:merge_failures]
|
|
56
|
+
|
|
57
|
+
pattern = Polyrun::Reporting::FailureMerge.default_fragment_glob
|
|
58
|
+
files = Dir.glob(pattern).sort
|
|
59
|
+
if files.empty?
|
|
60
|
+
Polyrun::Log.warn "polyrun run-shards: --merge-failures: no #{Polyrun::Reporting::FailureMerge::FRAGMENT_GLOB} under fragment dir (enable Polyrun::RSpec.install_failure_fragments! in spec_helper?)"
|
|
61
|
+
return nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
fmt = merge_failures_resolved_format(ctx)
|
|
65
|
+
out = merge_failures_resolved_output_path(ctx, fmt)
|
|
66
|
+
Polyrun::Log.warn "polyrun run-shards: merging #{files.size} failure fragment(s) → #{out} (#{fmt})"
|
|
67
|
+
Polyrun::Debug.log_kv(merge_failures: "start", output: out, inputs: files, format: fmt)
|
|
68
|
+
n = Polyrun::Reporting::FailureMerge.merge_files!(files, output: out, format: fmt)
|
|
69
|
+
Polyrun::Debug.log_kv(merge_failures: "done", rows: n, output: File.expand_path(out))
|
|
70
|
+
File.expand_path(out)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def merge_failures_resolved_format(ctx)
|
|
74
|
+
f = ctx[:merge_failures_format].to_s.strip.downcase
|
|
75
|
+
return "jsonl" if f.empty?
|
|
76
|
+
return "jsonl" if f == "jsonl"
|
|
77
|
+
return "json" if f == "json"
|
|
78
|
+
|
|
79
|
+
Polyrun::Log.warn "polyrun run-shards: unknown merge_failures_format=#{ctx[:merge_failures_format].inspect}; using jsonl"
|
|
80
|
+
"jsonl"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def merge_failures_resolved_output_path(ctx, fmt)
|
|
84
|
+
raw = ctx[:merge_failures_output]
|
|
85
|
+
return File.expand_path(raw) if raw && !raw.to_s.strip.empty?
|
|
86
|
+
|
|
87
|
+
ext = (fmt == "json") ? "json" : "jsonl"
|
|
88
|
+
File.expand_path(File.join("tmp", "polyrun_failures", "merged.#{ext}"))
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
data/lib/polyrun/cli/help.rb
CHANGED
|
@@ -21,6 +21,7 @@ module Polyrun
|
|
|
21
21
|
Skip start auto-prepare / auto DB provision: POLYRUN_START_SKIP_PREPARE=1, POLYRUN_START_SKIP_DATABASES=1
|
|
22
22
|
Skip writing paths_file from partition.paths_build: POLYRUN_SKIP_PATHS_BUILD=1
|
|
23
23
|
Warn if merge-coverage wall time exceeds N seconds (default 10): POLYRUN_MERGE_SLOW_WARN_SECONDS (0 disables)
|
|
24
|
+
Failure fragments (run-shards --merge-failures): POLYRUN_MERGE_FAILURES=1; parent sets POLYRUN_FAILURE_FRAGMENTS=1 in workers; POLYRUN_FAILURE_FRAGMENT_DIR, POLYRUN_MERGED_FAILURES_OUT, POLYRUN_MERGED_FAILURES_FORMAT; after_suite sets POLYRUN_MERGED_FAILURES_PATH when merge ran
|
|
24
25
|
Parallel RSpec workers: POLYRUN_WORKERS default 5, max 10 (run-shards / parallel-rspec / start); distinct from POLYRUN_SHARD_PROCESSES / ci-shard --shard-processes (local processes per CI matrix job)
|
|
25
26
|
Partition timing granularity (default file): POLYRUN_TIMING_GRANULARITY=file|example (experimental per-example; see partition.timing_granularity)
|
|
26
27
|
|
|
@@ -29,7 +30,8 @@ module Polyrun
|
|
|
29
30
|
plan emit partition manifest JSON
|
|
30
31
|
prepare run prepare recipe: default | assets (optional prepare.command overrides bin/rails assets:precompile) | shell (prepare.command required)
|
|
31
32
|
merge-coverage merge SimpleCov JSON fragments (json/lcov/cobertura/console)
|
|
32
|
-
|
|
33
|
+
merge-failures merge per-shard failure JSONL fragments or RSpec JSON files (jsonl/json)
|
|
34
|
+
run-shards fan out N parallel OS processes (POLYRUN_SHARD_*; not Ruby threads); optional --merge-coverage / --merge-failures
|
|
33
35
|
parallel-rspec run-shards + merge-coverage (defaults to: bundle exec rspec after --)
|
|
34
36
|
start parallel-rspec; auto-runs prepare (shell/assets) and db:setup-* when polyrun.yml configures them; legacy script/build_spec_paths.rb if paths_build absent
|
|
35
37
|
ci-shard-run CI matrix: build-paths + plan for POLYRUN_SHARD_INDEX / POLYRUN_SHARD_TOTAL (or config), then run your command with that shard's paths after --; optional --shard-processes M or --workers M (POLYRUN_SHARD_PROCESSES; not POLYRUN_WORKERS) for N×M jobs × processes on this host
|
|
@@ -38,6 +40,7 @@ module Polyrun
|
|
|
38
40
|
init write a starter polyrun.yml or POLYRUN.md from built-in templates (see docs/SETUP_PROFILE.md)
|
|
39
41
|
queue file-backed batch queue: init (optional --shard/--total etc. as plan, then claim/ack); M workers share one dir; no duplicate paths across claims
|
|
40
42
|
quick run Polyrun::Quick (describe/it, before/after, let, expect…to, assert_*; optional capybara!)
|
|
43
|
+
hook run <phase> run one shell hook from polyrun.yml hooks: (e.g. before_suite); optional --shard/--total
|
|
41
44
|
report-coverage write all coverage formats from one JSON file
|
|
42
45
|
report-junit RSpec JSON or Polyrun testcase JSON → JUnit XML (CI)
|
|
43
46
|
report-timing print slow-file summary from merged timing JSON
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
module Polyrun
|
|
2
|
+
class CLI
|
|
3
|
+
# +polyrun hook run <phase>+ — run one lifecycle phase from +polyrun.yml+ +hooks:+ (manual debugging / CI).
|
|
4
|
+
module HooksCommand
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def cmd_hook(argv, config_path)
|
|
8
|
+
sub = argv.shift
|
|
9
|
+
case sub
|
|
10
|
+
when "run"
|
|
11
|
+
cmd_hook_run(argv, config_path)
|
|
12
|
+
when nil, "help", "-h", "--help"
|
|
13
|
+
print_hook_help
|
|
14
|
+
0
|
|
15
|
+
else
|
|
16
|
+
Polyrun::Log.warn "polyrun hook: unknown subcommand #{sub.inspect} (try: polyrun hook run <phase>)"
|
|
17
|
+
print_hook_help
|
|
18
|
+
2
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def cmd_hook_run(argv, config_path)
|
|
23
|
+
phase = argv.shift
|
|
24
|
+
if phase.nil? || phase == "-h" || phase == "--help"
|
|
25
|
+
print_hook_help
|
|
26
|
+
return 2
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
shard, total = hook_run_parse_shard_flags!(argv)
|
|
30
|
+
|
|
31
|
+
unless argv.empty?
|
|
32
|
+
Polyrun::Log.warn "polyrun hook run: unexpected arguments: #{argv.inspect}"
|
|
33
|
+
return 2
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
phase_sym = Polyrun::Hooks.parse_phase(phase)
|
|
37
|
+
unless phase_sym && Polyrun::Hooks::PHASES.include?(phase_sym)
|
|
38
|
+
Polyrun::Log.warn "polyrun hook run: unknown phase #{phase.inspect} (expected: #{Polyrun::Hooks::PHASES.join(", ")})"
|
|
39
|
+
return 2
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
cfg = Polyrun::Config.load(path: config_path || ENV["POLYRUN_CONFIG"])
|
|
43
|
+
hook_cfg = Polyrun::Hooks.from_config(cfg)
|
|
44
|
+
env = hook_run_env(shard, total)
|
|
45
|
+
|
|
46
|
+
hook_cfg.run_phase(phase_sym, env)
|
|
47
|
+
rescue ArgumentError => e
|
|
48
|
+
Polyrun::Log.warn "polyrun hook run: #{e.message}"
|
|
49
|
+
2
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def hook_run_parse_shard_flags!(argv)
|
|
53
|
+
shard = nil
|
|
54
|
+
total = nil
|
|
55
|
+
while (a = argv.first)
|
|
56
|
+
case a
|
|
57
|
+
when "--shard"
|
|
58
|
+
argv.shift
|
|
59
|
+
shard = Integer(argv.shift || (raise ArgumentError, "--shard needs a value"))
|
|
60
|
+
when "--total"
|
|
61
|
+
argv.shift
|
|
62
|
+
total = Integer(argv.shift || (raise ArgumentError, "--total needs a value"))
|
|
63
|
+
else
|
|
64
|
+
break
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
[shard, total]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def hook_run_env(shard, total)
|
|
71
|
+
env = ENV.to_h.merge(
|
|
72
|
+
"POLYRUN_HOOK_ORCHESTRATOR" => "1",
|
|
73
|
+
"POLYRUN_HOOK_CLI" => "1"
|
|
74
|
+
)
|
|
75
|
+
env["POLYRUN_SHARD_INDEX"] = shard.to_s unless shard.nil?
|
|
76
|
+
env["POLYRUN_SHARD_TOTAL"] = total.to_s unless total.nil?
|
|
77
|
+
env
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def print_hook_help
|
|
81
|
+
Polyrun::Log.puts <<~HELP
|
|
82
|
+
usage: polyrun hook run <phase> [--shard N] [--total M]
|
|
83
|
+
|
|
84
|
+
Runs hook(s) from polyrun.yml: Ruby DSL (+hooks.ruby+) then shell strings for <phase> (same names as RSpec lifecycle:
|
|
85
|
+
before_suite / after_suite as before(:suite) / after(:suite); before_shard / after_shard as
|
|
86
|
+
before(:all) / after(:all); before_worker / after_worker as before(:each) / after(:each)).
|
|
87
|
+
|
|
88
|
+
Phases: #{Polyrun::Hooks::PHASES.join(", ")}
|
|
89
|
+
|
|
90
|
+
Optional --shard / --total set POLYRUN_SHARD_INDEX / POLYRUN_SHARD_TOTAL for the hook process.
|
|
91
|
+
POLYRUN_HOOKS_DISABLE=1 skips hooks during run-shards / ci-shard only; polyrun hook run still executes.
|
|
92
|
+
For CI matrix (POLYRUN_SHARD_TOTAL > 1), ci-shard-run skips before_suite / after_suite unless POLYRUN_HOOKS_SUITE_PER_MATRIX_JOB=1; run those phases here or in a dedicated CI job.
|
|
93
|
+
HELP
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -2,12 +2,14 @@ require "optparse"
|
|
|
2
2
|
require "rbconfig"
|
|
3
3
|
|
|
4
4
|
require_relative "start_bootstrap"
|
|
5
|
+
require_relative "failure_commands"
|
|
5
6
|
require_relative "run_shards_run"
|
|
6
7
|
|
|
7
8
|
module Polyrun
|
|
8
9
|
class CLI
|
|
9
10
|
module RunShardsCommand
|
|
10
11
|
include StartBootstrap
|
|
12
|
+
include FailureCommands
|
|
11
13
|
include RunShardsRun
|
|
12
14
|
|
|
13
15
|
private
|
|
@@ -114,10 +116,11 @@ module Polyrun
|
|
|
114
116
|
# ENV for a worker process: POLYRUN_SHARD_* plus per-shard database URLs from polyrun.yml or DATABASE_URL.
|
|
115
117
|
# When +matrix_total+ > 1 with multiple local workers, sets +POLYRUN_SHARD_MATRIX_INDEX+ / +POLYRUN_SHARD_MATRIX_TOTAL+
|
|
116
118
|
# so {Coverage::Collector} can name fragments uniquely across CI matrix jobs (NxM sharding).
|
|
117
|
-
def shard_child_env(cfg:, workers:, shard:, matrix_index: nil, matrix_total: nil)
|
|
119
|
+
def shard_child_env(cfg:, workers:, shard:, matrix_index: nil, matrix_total: nil, failure_fragments: false)
|
|
118
120
|
child_env = ENV.to_h.merge(
|
|
119
121
|
Polyrun::Database::Shard.env_map(shard_index: shard, shard_total: workers)
|
|
120
122
|
)
|
|
123
|
+
child_env["POLYRUN_FAILURE_FRAGMENTS"] = "1" if failure_fragments
|
|
121
124
|
mt = matrix_total.nil? ? 0 : Integer(matrix_total)
|
|
122
125
|
if mt > 1
|
|
123
126
|
if matrix_index.nil?
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
module Polyrun
|
|
2
|
+
class CLI
|
|
3
|
+
# Spawns and waits on worker processes for +run-shards+ / +ci-shard-*+ fan-out.
|
|
4
|
+
module RunShardsParallelChildren
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
# @return [Array(Array, Integer, nil)] +[pids, spawn_error_code]+; +spawn_error_code+ is +nil+ when all spawns succeeded
|
|
8
|
+
# rubocop:disable Metrics/AbcSize -- shard loop: spawn + shard hooks + env
|
|
9
|
+
def run_shards_spawn_workers(ctx, hook_cfg)
|
|
10
|
+
workers = ctx[:workers]
|
|
11
|
+
cmd = ctx[:cmd]
|
|
12
|
+
cfg = ctx[:cfg]
|
|
13
|
+
plan = ctx[:plan]
|
|
14
|
+
parallel = ctx[:parallel]
|
|
15
|
+
mx = ctx[:matrix_shard_index]
|
|
16
|
+
mt = ctx[:matrix_shard_total]
|
|
17
|
+
|
|
18
|
+
pids = []
|
|
19
|
+
workers.times do |shard|
|
|
20
|
+
paths = plan.shard(shard)
|
|
21
|
+
if paths.empty?
|
|
22
|
+
Polyrun::Log.warn "polyrun run-shards: shard #{shard} skipped (no paths)" if @verbose || parallel
|
|
23
|
+
next
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
env_shard = ENV.to_h.merge(
|
|
27
|
+
"POLYRUN_HOOK_ORCHESTRATOR" => "1",
|
|
28
|
+
"POLYRUN_SHARD_INDEX" => shard.to_s,
|
|
29
|
+
"POLYRUN_SHARD_TOTAL" => workers.to_s
|
|
30
|
+
)
|
|
31
|
+
code = hook_cfg.run_phase_if_enabled(:before_shard, env_shard)
|
|
32
|
+
if code != 0
|
|
33
|
+
run_shards_terminate_children!(pids)
|
|
34
|
+
return [pids, code]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
child_env = shard_child_env(
|
|
38
|
+
cfg: cfg,
|
|
39
|
+
workers: workers,
|
|
40
|
+
shard: shard,
|
|
41
|
+
matrix_index: mx,
|
|
42
|
+
matrix_total: mt,
|
|
43
|
+
failure_fragments: ctx[:merge_failures]
|
|
44
|
+
)
|
|
45
|
+
child_env = child_env.merge("POLYRUN_HOOK_ORCHESTRATOR" => "0")
|
|
46
|
+
child_env = hook_cfg.merge_worker_ruby_env(child_env)
|
|
47
|
+
|
|
48
|
+
Polyrun::Log.warn "polyrun run-shards: shard #{shard} → #{paths.size} file(s)" if @verbose
|
|
49
|
+
pid = run_shards_spawn_one_worker(child_env, cmd, paths, hook_cfg)
|
|
50
|
+
pids << {pid: pid, shard: shard}
|
|
51
|
+
Polyrun::Debug.log("[parent pid=#{$$}] run-shards: Process.spawn shard=#{shard} child_pid=#{pid} spec_files=#{paths.size}")
|
|
52
|
+
Polyrun::Log.warn "polyrun run-shards: started shard #{shard} pid=#{pid} (#{paths.size} file(s))" if parallel
|
|
53
|
+
end
|
|
54
|
+
[pids, nil]
|
|
55
|
+
end
|
|
56
|
+
# rubocop:enable Metrics/AbcSize
|
|
57
|
+
|
|
58
|
+
def run_shards_spawn_one_worker(child_env, cmd, paths, hook_cfg)
|
|
59
|
+
if hook_cfg.worker_hooks? && !Polyrun::Hooks.disabled?
|
|
60
|
+
Process.spawn(child_env, "sh", "-c", hook_cfg.build_worker_shell_script(cmd, paths))
|
|
61
|
+
else
|
|
62
|
+
Process.spawn(child_env, *cmd, *paths)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @return [Array(Array, Integer)] +[shard_results, after_shard_hook_error_code]+ (0 when all +after_shard+ hooks passed)
|
|
67
|
+
def run_shards_wait_all_children(pids, hook_cfg, ctx)
|
|
68
|
+
workers = ctx[:workers]
|
|
69
|
+
shard_results = []
|
|
70
|
+
after_hook_err = 0
|
|
71
|
+
Polyrun::Debug.time("Process.wait (#{pids.size} worker process(es))") do
|
|
72
|
+
pids.each do |h|
|
|
73
|
+
Process.wait(h[:pid])
|
|
74
|
+
exitstatus = $?.exitstatus
|
|
75
|
+
ok = $?.success?
|
|
76
|
+
Polyrun::Debug.log("[parent pid=#{$$}] run-shards: Process.wait child_pid=#{h[:pid]} shard=#{h[:shard]} exit=#{exitstatus} success=#{ok}")
|
|
77
|
+
env_after = ENV.to_h.merge(
|
|
78
|
+
"POLYRUN_HOOK_ORCHESTRATOR" => "1",
|
|
79
|
+
"POLYRUN_SHARD_INDEX" => h[:shard].to_s,
|
|
80
|
+
"POLYRUN_SHARD_TOTAL" => workers.to_s,
|
|
81
|
+
"POLYRUN_WORKER_EXIT_STATUS" => exitstatus.to_s
|
|
82
|
+
)
|
|
83
|
+
rc = hook_cfg.run_phase_if_enabled(:after_shard, env_after)
|
|
84
|
+
after_hook_err = rc if rc != 0 && after_hook_err == 0
|
|
85
|
+
shard_results << {shard: h[:shard], exitstatus: exitstatus, success: ok}
|
|
86
|
+
end
|
|
87
|
+
rescue Interrupt
|
|
88
|
+
# Do not trap SIGINT: Process.wait raises Interrupt; a trap races and prints Interrupt + SystemExit traces.
|
|
89
|
+
run_shards_shutdown_on_signal!(pids, 130)
|
|
90
|
+
rescue SignalException => e
|
|
91
|
+
raise unless e.signm == "SIGTERM"
|
|
92
|
+
|
|
93
|
+
run_shards_shutdown_on_signal!(pids, 143)
|
|
94
|
+
end
|
|
95
|
+
[shard_results, after_hook_err]
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|