turbo_tests2 3.2.15 → 3.2.17

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b455a333f0fe9a3521173ff0e73b309b74baa6b37da905f5fcfa1a79a5eb075b
4
- data.tar.gz: 8a2f7d15910049b3b8366fd1ed6e604581a6c7444a6e95babdcb2f344e73b054
3
+ metadata.gz: 4d0f84a37aeb74853501933d16cd9476a1f82800b3d33e9da544af8ef0e0c145
4
+ data.tar.gz: 581d982f38436235925c6bc8180a4ac29513c198599ae841949e47bce114186f
5
5
  SHA512:
6
- metadata.gz: 3cee9299d9a75a189a763621873df30959b68999972212f05f4ea81ba04200c66d9cbb56e2cc402947b58689881b16cdbc25ac9579205c473c4f359a2cab72ca
7
- data.tar.gz: 8ec3ac430a73305ae22bbc58c4ad9e82e0f04f41645853b8da4d50a24f868fe5a6611e5a222774a423df2db89e5176f4f69221ed32adcb85771fab482093b85b
6
+ metadata.gz: 9d0f7b4e2080fd36f4da47ff4bdebed7fce369995e9c1b6cb305bc2caf9ad935ec2d65eaa84483721b361c57a12eb24b36fd3d0c5fc0ce16a62638f14df16636
7
+ data.tar.gz: ffc015acc34a5ad4658a2a5aa53ceb02ec6621eea4e625bc1a7d2b6558c1cc728037ad984c32cc52e08108f8c322011dee41926dfdde3400c5c65ae7021c82dc
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,38 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [3.2.17] - 2026-10-14
34
+
35
+ - TAG: [v3.2.17][3.2.17t]
36
+ - COVERAGE: 95.94% -- 993/1035 lines in 11 files
37
+ - BRANCH COVERAGE: 83.86% -- 265/316 branches in 11 files
38
+ - 39.42% documented
39
+
40
+ ### Changed
41
+
42
+ - README: document the spawn-based (fork-free) worker architecture that makes parallel workers engine-portable to JRuby and TruffleRuby, and add a Resource-Constrained CI (JVM Engines) section with the canonical 2-worker + capped-heap + raised-watchdog setup, matching the kettle-jem CI templates
43
+
44
+ - Stall diagnostics and in-code docs now explain silent worker death: the 900s silence warning and the watchdog stall path point at the OOM killer as the most common cause of total silence in a parallel lane (spawned workers, JVM engines defaulting to 1/4 RAM heap each), with the fix inline (TURBO_TESTS2_MAX_PROCESSES=2 + capped heap); start_subprocess and max_processes_from_environment document the spawn-based engine-portable architecture
45
+
46
+ ## [3.2.16] - 2026-10-14
47
+
48
+ - TAG: [v3.2.16][3.2.16t]
49
+ - COVERAGE: 95.94% -- 993/1035 lines in 11 files
50
+ - BRANCH COVERAGE: 83.86% -- 265/316 branches in 11 files
51
+ - 39.42% documented
52
+
53
+ ### Fixed
54
+
55
+ - Watchdog kills now actually end the run: kill_remaining_workers takes pids from the live wait_threads (the parallel_tests pid file was unreliable — each track/untrack built a fresh Pids instance, so any normal worker exit wiped the file and the watchdog killed nothing), handle_messages stops at the watchdog sentinel instead of waiting for per-worker exits that can never arrive, and post-kill joins are bounded so a wedged worker can no longer hold the run past the watchdog (observed: CI job printed the stall report, then hung 14 more minutes until cancellation)
56
+
57
+ ### Changed
58
+
59
+ - [kc] kettle-jem/template: updated 32 project files:
60
+ - code and tests (1)
61
+ - dependencies (3)
62
+ - other (1)
63
+ - workflows (27)
64
+
33
65
  ## [3.2.15] - 2026-10-13
34
66
 
35
67
  - TAG: [v3.2.15][3.2.15t]
@@ -742,7 +774,11 @@ Please file a bug if you notice a violation of semantic versioning.
742
774
 
743
775
  - Initial release
744
776
 
745
- [Unreleased]: https://github.com/galtzo-floss/turbo_tests2/compare/v3.2.15...HEAD
777
+ [Unreleased]: https://github.com/galtzo-floss/turbo_tests2/compare/v3.2.17...HEAD
778
+ [3.2.17]: https://github.com/galtzo-floss/turbo_tests2/compare/v3.2.16...v3.2.17
779
+ [3.2.17t]: https://github.com/galtzo-floss/turbo_tests2/releases/tag/v3.2.17
780
+ [3.2.16]: https://github.com/galtzo-floss/turbo_tests2/compare/v3.2.15...v3.2.16
781
+ [3.2.16t]: https://github.com/galtzo-floss/turbo_tests2/releases/tag/v3.2.16
746
782
  [3.2.15]: https://github.com/galtzo-floss/turbo_tests2/compare/v3.2.14...v3.2.15
747
783
  [3.2.15t]: https://github.com/galtzo-floss/turbo_tests2/releases/tag/v3.2.15
748
784
  [3.2.14]: https://github.com/galtzo-floss/turbo_tests2/compare/v3.2.13...v3.2.14
data/README.md CHANGED
@@ -384,6 +384,60 @@ count at the number of discovered test groups.
384
384
  Known coverage formatter chatter remains collapsed into the concise coverage
385
385
  summary.
386
386
 
387
+ ### Resource-Constrained CI (JVM Engines)
388
+
389
+ Worker parallelism is spawn-based, not fork-based: each worker is started as a
390
+ real subprocess (`Process.spawn` / `Open3.popen3`), never `Kernel#fork`. That
391
+ makes parallel workers engine-portable — MRI, JRuby, and TruffleRuby all run
392
+ the same spawn-based pool, and the default worker count is the machine's core
393
+ count (`Etc.nprocessors`) on every engine. There is no serial-only mode for
394
+ JVM engines unless you ask for one with `TURBO_TESTS2_MAX_PROCESSES=1`.
395
+
396
+ The cost of that portability on JVM engines is memory: each JRuby or
397
+ TruffleRuby worker is a full JVM subprocess, and JRuby defaults its heap to
398
+ one quarter of machine RAM. On a CI runner, `Etc.nprocessors` workers each
399
+ grabbing a quarter of RAM is a recipe for the kernel's OOM killer, which kills
400
+ workers **silently** — every worker stops messaging at once, and the stall
401
+ watchdog then reports total silence. This is a resource problem, not a
402
+ parallelism problem.
403
+
404
+ The fix is to run fewer, smaller workers. This is the canonical
405
+ resource-constrained setup, and the `kettle-jem` CI templates bake it into
406
+ every JRuby workflow they generate:
407
+
408
+ ```bash
409
+ TURBO_TESTS2_MAX_PROCESSES=2 \
410
+ JRUBY_OPTS="-J-Xmx1280m" \
411
+ TURBO_TESTS2_WATCHDOG_SECONDS=1500 \
412
+ bundle exec turbo_tests2
413
+ ```
414
+
415
+ Or in a GitHub Actions workflow:
416
+
417
+ ```yaml
418
+ env:
419
+ # Fewer, smaller JVM workers on constrained runners.
420
+ TURBO_TESTS2_MAX_PROCESSES: 2
421
+ # Cap each worker JVM's heap (JRuby default: 1/4 of runner RAM).
422
+ JRUBY_OPTS: "-J-Xmx1280m"
423
+ # JVM lanes are slow; give the watchdog room without hitting the job timeout.
424
+ TURBO_TESTS2_WATCHDOG_SECONDS: 1500
425
+ ```
426
+
427
+ Guidance:
428
+
429
+ - **Cap workers** (`TURBO_TESTS2_MAX_PROCESSES=2`) when each worker is a heavy
430
+ process: JVM engines, large test suites, or shared/undersized runners. Two
431
+ JVM workers still roughly halve wall-clock time versus one.
432
+ - **Cap the heap** (`JRUBY_OPTS="-J-Xmx1280m"`, or your engine's equivalent)
433
+ so total memory is bounded regardless of runner size.
434
+ - **Raise the watchdog** (1500s) on slow engines: JIT warmup and heavy GC make
435
+ silence windows longer, and you want the watchdog report, not the provider
436
+ timeout, when something genuinely hangs.
437
+ - The same knobs apply to any engine where a worker process is expensive —
438
+ the principle is: bound `workers × per-worker memory` under the runner's
439
+ capacity.
440
+
387
441
  ### Stall watchdog
388
442
 
389
443
  If the worker message queue goes silent — every worker alive but none
@@ -859,7 +913,7 @@ Thanks for RTFM. ☺️
859
913
  [📌gitmoji]: https://gitmoji.dev
860
914
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
861
915
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
862
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-1.022-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
916
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-1.035-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
863
917
  [🔐security]: https://github.com/galtzo-floss/turbo_tests2/blob/main/SECURITY.md
864
918
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
865
919
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -243,6 +243,16 @@ module TurboTests
243
243
  ]
244
244
  end
245
245
 
246
+ # Inclusive ceiling on the worker pool when neither --count nor --workers
247
+ # is passed. Explicit CLI counts always win. Two main uses:
248
+ #
249
+ # 1. Outer orchestrators (kettle-family) budgeting a machine-wide process
250
+ # pool across concurrent family jobs.
251
+ # 2. Resource-constrained CI, where each worker is an expensive process —
252
+ # JVM engines (JRuby, TruffleRuby) spawn a full JVM per worker, so cap
253
+ # the pool (typically 2) AND the per-worker heap (e.g. JRUBY_OPTS=
254
+ # "-J-Xmx1280m") to stay under runner RAM. See README
255
+ # "Resource-Constrained CI (JVM Engines)".
246
256
  def max_processes_from_environment
247
257
  value = ENV.fetch("TURBO_TESTS2_MAX_PROCESSES", "").strip
248
258
  return if value.empty?
@@ -318,24 +318,41 @@ module TurboTests
318
318
 
319
319
  handle_messages
320
320
 
321
- @threads.each(&:join)
321
+ if @watchdog_fired
322
+ # Workers were SIGKILLed by the watchdog; their reader threads end
323
+ # at EOF and waiters return as the OS reaps them — but if a kill
324
+ # failed (permissions, zombie, pid reuse) an unbounded join here
325
+ # would hang the run anyway, defeating the watchdog. Bound every
326
+ # join and treat any worker that still has not exited as failed.
327
+ @threads.each do |thread|
328
+ thread.join(FORCE_STOP_JOIN)
329
+ thread.kill if thread.alive?
330
+ end
331
+ @wait_threads.each do |wait_thr|
332
+ wait_thr.join(FORCE_STOP_JOIN)
333
+ end
334
+ flush_worker_output unless stream_worker_output?
335
+ exit_status = 1
336
+ else
337
+ @threads.each(&:join)
322
338
 
323
- report_workers_without_results
339
+ report_workers_without_results
324
340
 
325
- report_failed_group(tests_in_groups) if @print_failed_group
341
+ report_failed_group(tests_in_groups) if @print_failed_group
326
342
 
327
- Signal.trap(:INT, old_signal)
343
+ Signal.trap(:INT, old_signal)
328
344
 
329
- statuses = @wait_threads.map(&:value)
345
+ statuses = @wait_threads.map(&:value)
330
346
 
331
- if @reporter.failed_examples.empty? && statuses.all?(&:success?)
332
- flush_successful_worker_output
333
- report_coverage = true
334
- exit_status = 0
335
- else
336
- flush_worker_output unless stream_worker_output?
337
- # From https://github.com/galtzo-floss/turbo_tests2/pull/20/
338
- exit_status = statuses.map(&:exitstatus).max
347
+ if @reporter.failed_examples.empty? && statuses.all?(&:success?)
348
+ flush_successful_worker_output
349
+ report_coverage = true
350
+ exit_status = 0
351
+ else
352
+ flush_worker_output unless stream_worker_output?
353
+ # From https://github.com/galtzo-floss/turbo_tests2/pull/20/
354
+ exit_status = statuses.map(&:exitstatus).max
355
+ end
339
356
  end
340
357
  end
341
358
 
@@ -391,6 +408,14 @@ module TurboTests
391
408
  )
392
409
  end
393
410
 
411
+ # Spawns one worker as a real OS subprocess via Open3.popen3 — never
412
+ # Kernel#fork. This is what makes the worker pool engine-portable: MRI,
413
+ # JRuby, and TruffleRuby all spawn subprocesses the same way, and the
414
+ # default pool size is Etc.nprocessors on every engine. Remember each
415
+ # worker is a full interpreter/VM process: on JVM engines that is a whole
416
+ # JVM with its own heap, so bound the pool (TURBO_TESTS2_MAX_PROCESSES)
417
+ # and the per-worker heap (e.g. JRUBY_OPTS) on memory-constrained
418
+ # runners — see README "Resource-Constrained CI (JVM Engines)".
394
419
  def start_subprocess(env, extra_args, tests, process_id, record_runtime:)
395
420
  if tests.empty?
396
421
  @messages << {
@@ -739,6 +764,14 @@ module TurboTests
739
764
  # on the queue forever — CI jobs hung for hours with no diagnostics.
740
765
  # On a stall we kill the surviving worker pids, report which groups
741
766
  # never finished, and fail the run instead.
767
+ #
768
+ # Note the OOM killer is the most common silent worker death on CI: the
769
+ # kernel kills the process with no stderr, no exit message, and no
770
+ # partial output. Workers are spawn-based real processes (never forked),
771
+ # so on JVM engines (JRuby, TruffleRuby) each one is a full JVM whose
772
+ # default heap can be 1/4 of machine RAM — a core-count pool of those
773
+ # exhausts a CI runner. Bound `workers x per-worker memory` on
774
+ # constrained runners (see README "Resource-Constrained CI").
742
775
  def pop_message_with_watchdog
743
776
  # 0 (the default) disables the watchdog: block indefinitely, exactly the
744
777
  # pre-watchdog behavior.
@@ -759,6 +792,11 @@ module TurboTests
759
792
  "turbo_tests2: watchdog fired after #{@watchdog_seconds}s without worker messages; workers killed\n#{stall_report}"
760
793
  )
761
794
  @error = true
795
+ # The run is over: workers were just SIGKILLed. handle_messages must
796
+ # not keep waiting for per-worker exit messages that can never
797
+ # arrive, and the caller must not re-enter this watchdog (which would
798
+ # warn-and-kill in a 1-second spin).
799
+ @watchdog_fired = true
762
800
  return {type: "exit", process_id: :watchdog}
763
801
  end
764
802
  end
@@ -792,7 +830,19 @@ module TurboTests
792
830
  end
793
831
 
794
832
  def kill_remaining_workers
795
- tracked_pids.each do |pid|
833
+ # The live waiter objects are authoritative: the parallel_tests pid file
834
+ # loses entries because every track/untrack call builds a fresh Pids
835
+ # instance whose in-memory list starts empty, so a save overwrites (or,
836
+ # for untrack, empties) the file. After any normal worker exit the file
837
+ # is guaranteed useless — exactly when the watchdog needs it most.
838
+ pids = (@wait_threads || []).map do |wait_thr|
839
+ begin
840
+ wait_thr.pid
841
+ rescue
842
+ nil
843
+ end
844
+ end.compact
845
+ (pids | tracked_pids).each do |pid|
796
846
  begin
797
847
  Process.kill("KILL", pid)
798
848
  rescue
@@ -822,7 +872,10 @@ module TurboTests
822
872
  "outer process watcher (a CI job timeout, a container orchestrator, or a local supervisor) will then " \
823
873
  "be what eventually ends the run, usually without a useful stall report. A single long test only " \
824
874
  "trips this in serial lanes (TURBO_TESTS2_MAX_PROCESSES=1); in parallel lanes other workers keep " \
825
- "messages flowing."
875
+ "messages flowing. Total silence in a PARALLEL lane usually means workers died rather than slowed " \
876
+ "down — the most common cause is the kernel OOM killer (each worker is a real spawned process; on " \
877
+ "JVM engines each is a full JVM whose default heap is 1/4 of RAM). Cap workers x per-worker memory: " \
878
+ "TURBO_TESTS2_MAX_PROCESSES=2 with a capped heap (e.g. JRUBY_OPTS=\"-J-Xmx1280m\")."
826
879
  )
827
880
  warned_silence = true
828
881
  end
@@ -870,6 +923,10 @@ module TurboTests
870
923
  nil
871
924
  when "exit"
872
925
  process_id = message[:process_id]
926
+ # Watchdog exit: workers were just SIGKILLed and no further per-worker
927
+ # exit messages can be relied upon — stop the loop immediately
928
+ # instead of waiting for process_count exits that never arrive.
929
+ break if process_id == :watchdog
873
930
  next if exited_process_ids.key?(process_id)
874
931
 
875
932
  exited_process_ids[process_id] = true
@@ -4,7 +4,7 @@ module TurboTests
4
4
  # Version namespace for this gem.
5
5
  module Version
6
6
  # Current gem version.
7
- VERSION = "3.2.15"
7
+ VERSION = "3.2.17"
8
8
  end
9
9
  # Current gem version exposed at the traditional constant location.
10
10
  VERSION = Version::VERSION # Traditional Constant Location
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: turbo_tests2
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.15
4
+ version: 3.2.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Illia
@@ -111,7 +111,7 @@ dependencies:
111
111
  version: '3.1'
112
112
  - - ">="
113
113
  - !ruby/object:Gem::Version
114
- version: 3.1.11
114
+ version: 3.1.12
115
115
  type: :development
116
116
  prerelease: false
117
117
  version_requirements: !ruby/object:Gem::Requirement
@@ -121,7 +121,7 @@ dependencies:
121
121
  version: '3.1'
122
122
  - - ">="
123
123
  - !ruby/object:Gem::Version
124
- version: 3.1.11
124
+ version: 3.1.12
125
125
  - !ruby/object:Gem::Dependency
126
126
  name: bundler-audit
127
127
  requirement: !ruby/object:Gem::Requirement
@@ -219,7 +219,7 @@ dependencies:
219
219
  version: '2.0'
220
220
  - - ">="
221
221
  - !ruby/object:Gem::Version
222
- version: 2.0.24
222
+ version: 2.0.25
223
223
  type: :development
224
224
  prerelease: false
225
225
  version_requirements: !ruby/object:Gem::Requirement
@@ -229,7 +229,7 @@ dependencies:
229
229
  version: '2.0'
230
230
  - - ">="
231
231
  - !ruby/object:Gem::Version
232
- version: 2.0.24
232
+ version: 2.0.25
233
233
  - !ruby/object:Gem::Dependency
234
234
  name: ruby-progressbar
235
235
  requirement: !ruby/object:Gem::Requirement
@@ -316,10 +316,10 @@ licenses:
316
316
  - MIT
317
317
  metadata:
318
318
  homepage_uri: https://turbo-tests2.galtzo.com
319
- source_code_uri: https://github.com/galtzo-floss/turbo_tests2/tree/v3.2.15
320
- changelog_uri: https://github.com/galtzo-floss/turbo_tests2/blob/v3.2.15/CHANGELOG.md
319
+ source_code_uri: https://github.com/galtzo-floss/turbo_tests2/tree/v3.2.17
320
+ changelog_uri: https://github.com/galtzo-floss/turbo_tests2/blob/v3.2.17/CHANGELOG.md
321
321
  bug_tracker_uri: https://github.com/galtzo-floss/turbo_tests2/issues
322
- documentation_uri: https://www.rubydoc.info/gems/turbo_tests2/3.2.15
322
+ documentation_uri: https://www.rubydoc.info/gems/turbo_tests2/3.2.17
323
323
  funding_uri: https://github.com/sponsors/pboling
324
324
  wiki_uri: https://github.com/galtzo-floss/turbo_tests2/wiki
325
325
  news_uri: https://www.railsbling.com/tags/turbo_tests2
metadata.gz.sig CHANGED
Binary file