polyrun 2.2.5 → 2.2.6

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/README.md +1 -0
  4. data/docs/SETUP_PROFILE.md +1 -0
  5. data/lib/polyrun/cli/database_commands.rb +21 -7
  6. data/lib/polyrun/cli/default_run.rb +13 -3
  7. data/lib/polyrun/cli/helpers.rb +41 -6
  8. data/lib/polyrun/cli/plan_command.rb +10 -29
  9. data/lib/polyrun/cli/run_queue_command.rb +10 -1
  10. data/lib/polyrun/cli/run_shards_parallel_children.rb +52 -39
  11. data/lib/polyrun/cli/run_shards_parallel_wait.rb +2 -2
  12. data/lib/polyrun/cli/run_shards_planning.rb +7 -29
  13. data/lib/polyrun/cli/run_shards_run.rb +1 -1
  14. data/lib/polyrun/cli/run_shards_worker_interrupt.rb +16 -14
  15. data/lib/polyrun/cli/start_bootstrap.rb +51 -22
  16. data/lib/polyrun/cli.rb +3 -0
  17. data/lib/polyrun/config.rb +9 -4
  18. data/lib/polyrun/coverage/example_diff.rb +6 -7
  19. data/lib/polyrun/coverage/merge/formatters_html.rb +20 -2
  20. data/lib/polyrun/coverage/merge.rb +9 -3
  21. data/lib/polyrun/coverage/merge_fragment_meta.rb +6 -1
  22. data/lib/polyrun/coverage/rails.rb +1 -1
  23. data/lib/polyrun/database/clone_shards.rb +28 -5
  24. data/lib/polyrun/database/provision.rb +15 -9
  25. data/lib/polyrun/database/url_builder/connection/infer.rb +4 -0
  26. data/lib/polyrun/database/url_builder.rb +31 -3
  27. data/lib/polyrun/partition/plan.rb +6 -24
  28. data/lib/polyrun/partition/timing_diagnostics.rb +2 -3
  29. data/lib/polyrun/queue/file_store.rb +133 -30
  30. data/lib/polyrun/queue/file_store_intent.rb +188 -0
  31. data/lib/polyrun/queue/worker_loop.rb +41 -7
  32. data/lib/polyrun/spec_quality/config.rb +1 -1
  33. data/lib/polyrun/spec_quality/profile.rb +0 -1
  34. data/lib/polyrun/spec_quality/report.rb +16 -7
  35. data/lib/polyrun/templates/rails_prepare.polyrun.yml +3 -1
  36. data/lib/polyrun/version.rb +1 -1
  37. data/lib/polyrun/worker_output.rb +30 -13
  38. metadata +2 -1
@@ -39,25 +39,38 @@ module Polyrun
39
39
  batch_size = Integer(batch_size)
40
40
  raise Polyrun::Error, "batch_size must be >= 1" if batch_size < 1
41
41
 
42
- lease_id = SecureRandom.uuid
42
+ lease_id = nil
43
43
  batch = []
44
44
  with_lock do
45
45
  meta = load_meta!
46
46
  batch = take_pending_batch!(meta, batch_size)
47
- leases = read_leases
48
- leases[lease_id] = {
49
- "worker_id" => worker_id.to_s,
50
- "paths" => batch,
51
- "claimed_at" => Time.now.utc.iso8601
52
- }
53
- write_meta!(meta)
54
- write_leases!(leases)
55
- append_ledger(
56
- "CLAIM" => lease_id,
57
- "worker_id" => worker_id.to_s,
58
- "paths" => batch,
59
- "pending_remaining" => meta["pending_count"]
60
- )
47
+ unless batch.empty?
48
+ lease_id = SecureRandom.uuid
49
+ now = Time.now.utc.iso8601
50
+ write_intent!(
51
+ "op" => "claim",
52
+ "lease_id" => lease_id,
53
+ "worker_id" => worker_id.to_s,
54
+ "paths" => batch
55
+ )
56
+ leases = read_leases
57
+ leases[lease_id] = {
58
+ "worker_id" => worker_id.to_s,
59
+ "paths" => batch,
60
+ "claimed_at" => now,
61
+ "heartbeat_at" => now,
62
+ "attempts" => 0
63
+ }
64
+ write_meta!(meta)
65
+ write_leases!(leases)
66
+ clear_intent!
67
+ append_ledger(
68
+ "CLAIM" => lease_id,
69
+ "worker_id" => worker_id.to_s,
70
+ "paths" => batch,
71
+ "pending_remaining" => meta["pending_count"]
72
+ )
73
+ end
61
74
  end
62
75
  {"lease_id" => lease_id, "paths" => batch}
63
76
  end
@@ -72,19 +85,73 @@ module Polyrun
72
85
  raise Polyrun::Error, "lease worker mismatch"
73
86
  end
74
87
 
75
- leases.delete(lease_id)
76
- write_leases!(leases)
77
-
78
88
  paths = lease["paths"] || []
89
+ write_intent!(
90
+ "op" => "ack",
91
+ "lease_id" => lease_id,
92
+ "worker_id" => worker_id.to_s,
93
+ "paths" => paths
94
+ )
95
+ # done.jsonl before removing the lease so a crash still has durable completion evidence.
96
+ append_done_lines!(paths)
79
97
  meta = load_meta!
80
98
  meta["done_count"] = Integer(meta["done_count"]) + paths.size
81
- append_done_lines!(paths)
82
99
  write_meta!(meta)
100
+ leases.delete(lease_id)
101
+ write_leases!(leases)
102
+ clear_intent!
83
103
  append_ledger("ACK" => lease_id, "worker_id" => worker_id.to_s, "paths" => paths)
84
104
  end
85
105
  true
86
106
  end
87
107
 
108
+ # Marks a subset of a lease's paths done; removes the lease when none remain.
109
+ def ack_paths!(lease_id:, worker_id:, paths:)
110
+ paths = Array(paths).map(&:to_s)
111
+ raise Polyrun::Error, "ack_paths! requires at least one path" if paths.empty?
112
+
113
+ with_lock do
114
+ leases = read_leases
115
+ lease = leases[lease_id]
116
+ raise Polyrun::Error, "unknown lease: #{lease_id}" unless lease
117
+
118
+ if lease["worker_id"].to_s != worker_id.to_s
119
+ raise Polyrun::Error, "lease worker mismatch"
120
+ end
121
+
122
+ held = Array(lease["paths"]).map(&:to_s)
123
+ unknown = paths - held
124
+ raise Polyrun::Error, "ack_paths! paths not on lease: #{unknown.join(", ")}" unless unknown.empty?
125
+
126
+ write_intent!(
127
+ "op" => "ack_paths",
128
+ "lease_id" => lease_id,
129
+ "worker_id" => worker_id.to_s,
130
+ "paths" => paths
131
+ )
132
+ apply_ack_side_effects!(leases, lease_id, lease, paths, worker_id)
133
+ clear_intent!
134
+ end
135
+ true
136
+ end
137
+
138
+ def heartbeat!(lease_id:, worker_id:)
139
+ with_lock do
140
+ leases = read_leases
141
+ lease = leases[lease_id]
142
+ raise Polyrun::Error, "unknown lease: #{lease_id}" unless lease
143
+
144
+ if lease["worker_id"].to_s != worker_id.to_s
145
+ raise Polyrun::Error, "lease worker mismatch"
146
+ end
147
+
148
+ lease["heartbeat_at"] = Time.now.utc.iso8601
149
+ leases[lease_id] = lease
150
+ write_leases!(leases)
151
+ end
152
+ true
153
+ end
154
+
88
155
  def status(detailed: false)
89
156
  with_lock do
90
157
  meta = load_meta!
@@ -111,13 +178,23 @@ module Polyrun
111
178
  leases.each do |lease_id, lease|
112
179
  if reclaim_lease?(lease, older_than: older_than, worker_id: worker_id)
113
180
  paths = lease["paths"] || []
114
- return_paths_to_pending!(meta, paths)
181
+ bumped = lease.merge("attempts" => Integer(lease["attempts"] || 0) + 1)
182
+ write_intent!(
183
+ "op" => "reclaim",
184
+ "lease_id" => lease_id,
185
+ "worker_id" => lease["worker_id"],
186
+ "paths" => paths,
187
+ "attempts" => bumped["attempts"]
188
+ )
189
+ quarantine_or_return_paths!(meta, paths, bumped)
115
190
  reclaimed_paths += paths.size
116
191
  append_ledger(
117
192
  "RECLAIM" => lease_id,
118
193
  "worker_id" => lease["worker_id"],
119
- "paths" => paths
194
+ "paths" => paths,
195
+ "attempts" => bumped["attempts"]
120
196
  )
197
+ clear_intent!
121
198
  else
122
199
  keep[lease_id] = lease
123
200
  end
@@ -135,14 +212,28 @@ module Polyrun
135
212
  lease = leases[lease_id]
136
213
  return 0 unless lease
137
214
 
215
+ paths = lease["paths"] || []
216
+ bumped = lease.merge("attempts" => Integer(lease["attempts"] || 0) + 1)
217
+ write_intent!(
218
+ "op" => "reclaim_lease",
219
+ "lease_id" => lease_id,
220
+ "worker_id" => lease["worker_id"],
221
+ "paths" => paths,
222
+ "attempts" => bumped["attempts"]
223
+ )
138
224
  leases.delete(lease_id)
139
225
  write_leases!(leases)
140
226
  meta = load_meta!
141
- paths = lease["paths"] || []
142
- return_paths_to_pending!(meta, paths)
227
+ quarantine_or_return_paths!(meta, paths, bumped)
143
228
  write_meta!(meta)
144
229
  reclaimed = paths.size
145
- append_ledger("RECLAIM" => lease_id, "worker_id" => lease["worker_id"], "paths" => paths)
230
+ append_ledger(
231
+ "RECLAIM" => lease_id,
232
+ "worker_id" => lease["worker_id"],
233
+ "paths" => paths,
234
+ "attempts" => bumped["attempts"]
235
+ )
236
+ clear_intent!
146
237
  end
147
238
  reclaimed
148
239
  end
@@ -154,8 +245,10 @@ module Polyrun
154
245
  return false
155
246
  end
156
247
  if older_than
157
- claimed = Time.parse(lease["claimed_at"].to_s)
158
- return (Time.now - claimed) >= older_than
248
+ stamp = lease["heartbeat_at"].to_s
249
+ stamp = lease["claimed_at"].to_s if stamp.empty?
250
+ touched = Time.parse(stamp)
251
+ return (Time.now - touched) >= older_than
159
252
  end
160
253
  !!worker_id
161
254
  rescue ArgumentError
@@ -165,13 +258,16 @@ module Polyrun
165
258
  def lease_details(leases)
166
259
  now = Time.now
167
260
  leases.map do |lease_id, lease|
168
- claimed = Time.parse(lease["claimed_at"].to_s)
261
+ stamp = lease["heartbeat_at"].to_s
262
+ stamp = lease["claimed_at"].to_s if stamp.empty?
263
+ touched = Time.parse(stamp)
169
264
  {
170
265
  "lease_id" => lease_id,
171
266
  "worker_id" => lease["worker_id"],
172
267
  "paths_count" => (lease["paths"] || []).size,
173
268
  "claimed_at" => lease["claimed_at"],
174
- "age_seconds" => (now - claimed).round(1)
269
+ "heartbeat_at" => lease["heartbeat_at"],
270
+ "age_seconds" => (now - touched).round(1)
175
271
  }
176
272
  rescue ArgumentError
177
273
  {
@@ -179,6 +275,7 @@ module Polyrun
179
275
  "worker_id" => lease["worker_id"],
180
276
  "paths_count" => (lease["paths"] || []).size,
181
277
  "claimed_at" => lease["claimed_at"],
278
+ "heartbeat_at" => lease["heartbeat_at"],
182
279
  "age_seconds" => nil
183
280
  }
184
281
  end
@@ -228,6 +325,7 @@ module Polyrun
228
325
  FileUtils.mkdir_p(@root)
229
326
  File.open(lock_path, File::CREAT | File::RDWR) do |f|
230
327
  f.flock(File::LOCK_EX)
328
+ recover_intent!
231
329
  yield
232
330
  end
233
331
  end
@@ -297,9 +395,13 @@ module Polyrun
297
395
  File.open(ledger_path, "a") { |f| f.write(line) }
298
396
  end
299
397
 
300
- def atomic_write(path, body)
398
+ def atomic_write(path, body, fsync: false)
301
399
  tmp = "#{path}.tmp.#{$$}"
302
- File.write(tmp, body)
400
+ File.open(tmp, File::WRONLY | File::CREAT | File::TRUNC) do |io|
401
+ io.write(body)
402
+ io.flush
403
+ io.fsync if fsync
404
+ end
303
405
  File.rename(tmp, path)
304
406
  end
305
407
  end
@@ -307,4 +409,5 @@ module Polyrun
307
409
  end
308
410
 
309
411
  require_relative "file_store_pending"
412
+ require_relative "file_store_intent"
310
413
  # rubocop:enable Polyrun/FileLength, Metrics/ClassLength
@@ -0,0 +1,188 @@
1
+ # rubocop:disable Metrics/ModuleLength -- claim/ack/reclaim intent recovery beside FileStore
2
+ module Polyrun
3
+ module Queue
4
+ class FileStore
5
+ INTENT_BASENAME = "intent.json"
6
+
7
+ private
8
+
9
+ def intent_path
10
+ File.join(@root, INTENT_BASENAME)
11
+ end
12
+
13
+ def write_intent!(payload)
14
+ atomic_write(intent_path, JSON.generate(payload), fsync: true)
15
+ end
16
+
17
+ def clear_intent!
18
+ File.unlink(intent_path)
19
+ rescue Errno::ENOENT
20
+ nil
21
+ end
22
+
23
+ def recover_intent!
24
+ return unless File.file?(intent_path)
25
+
26
+ intent = JSON.parse(File.read(intent_path))
27
+ case intent["op"].to_s
28
+ when "claim"
29
+ recover_claim_intent!(intent)
30
+ when "ack", "ack_paths"
31
+ recover_ack_intent!(intent)
32
+ when "reclaim", "reclaim_lease"
33
+ recover_reclaim_intent!(intent)
34
+ else
35
+ Polyrun::Log.warn "polyrun queue: dropping unknown intent op=#{intent["op"].inspect}"
36
+ end
37
+ clear_intent!
38
+ rescue JSON::ParserError, TypeError
39
+ Polyrun::Log.warn "polyrun queue: dropping corrupt intent at #{intent_path}"
40
+ clear_intent!
41
+ end
42
+
43
+ def recover_claim_intent!(intent)
44
+ lease_id = intent["lease_id"].to_s
45
+ leases = read_leases
46
+ return if leases.key?(lease_id)
47
+
48
+ paths = Array(intent["paths"])
49
+ meta = load_meta!
50
+ return_paths_to_pending!(meta, paths)
51
+ write_meta!(meta)
52
+ append_ledger(
53
+ "RECOVER_CLAIM" => lease_id,
54
+ "worker_id" => intent["worker_id"],
55
+ "paths" => paths
56
+ )
57
+ end
58
+
59
+ def recover_ack_intent!(intent)
60
+ lease_id = intent["lease_id"].to_s
61
+ leases = read_leases
62
+ lease = leases[lease_id]
63
+ return unless lease
64
+
65
+ paths = Array(intent["paths"])
66
+ paths = Array(lease["paths"]) if paths.empty?
67
+ apply_ack_side_effects!(leases, lease_id, lease, paths, intent["worker_id"])
68
+ end
69
+
70
+ def recover_reclaim_intent!(intent)
71
+ lease_id = intent["lease_id"].to_s
72
+ leases = read_leases
73
+ lease = leases[lease_id]
74
+ return unless lease
75
+
76
+ paths = Array(intent["paths"])
77
+ paths = Array(lease["paths"]) if paths.empty?
78
+ bumped = lease.merge("attempts" => intent["attempts"] || lease["attempts"])
79
+ leases.delete(lease_id)
80
+ write_leases!(leases)
81
+ meta = load_meta!
82
+ quarantine_or_return_paths!(meta, paths, bumped)
83
+ write_meta!(meta)
84
+ append_ledger(
85
+ "RECOVER_RECLAIM" => lease_id,
86
+ "worker_id" => lease["worker_id"],
87
+ "paths" => paths,
88
+ "attempts" => bumped["attempts"]
89
+ )
90
+ end
91
+
92
+ def apply_ack_side_effects!(leases, lease_id, lease, paths, worker_id)
93
+ append_done_lines!(paths)
94
+ clear_path_attempts!(paths)
95
+ meta = load_meta!
96
+ meta["done_count"] = Integer(meta["done_count"]) + paths.size
97
+ write_meta!(meta)
98
+ remaining = Array(lease["paths"]).map(&:to_s) - paths.map(&:to_s)
99
+ if remaining.empty?
100
+ leases.delete(lease_id)
101
+ else
102
+ lease["paths"] = remaining
103
+ leases[lease_id] = lease
104
+ end
105
+ write_leases!(leases)
106
+ append_ledger("ACK" => lease_id, "worker_id" => worker_id.to_s, "paths" => paths)
107
+ end
108
+
109
+ def quarantine_or_return_paths!(meta, paths, lease)
110
+ attempts_h = read_path_attempts
111
+ to_pending = []
112
+ poisoned = []
113
+ max_attempts = queue_max_attempts
114
+ Array(paths).each do |path|
115
+ key = path.to_s
116
+ n = Integer(attempts_h[key] || 0) + 1
117
+ attempts_h[key] = n
118
+ if max_attempts.positive? && n >= max_attempts
119
+ poisoned << [key, n]
120
+ else
121
+ to_pending << key
122
+ end
123
+ end
124
+ write_path_attempts!(attempts_h)
125
+ poisoned.each do |key, n|
126
+ append_failed_lines!(
127
+ [key],
128
+ "worker_id" => lease["worker_id"],
129
+ "attempts" => n
130
+ )
131
+ append_ledger("POISON" => lease["worker_id"], "attempts" => n, "paths" => [key])
132
+ end
133
+ return_paths_to_pending!(meta, to_pending)
134
+ end
135
+
136
+ def clear_path_attempts!(paths)
137
+ attempts_h = read_path_attempts
138
+ Array(paths).each { |p| attempts_h.delete(p.to_s) }
139
+ write_path_attempts!(attempts_h)
140
+ end
141
+
142
+ def queue_max_attempts
143
+ raw = ENV["POLYRUN_QUEUE_MAX_ATTEMPTS"].to_s.strip
144
+ return 3 if raw.empty?
145
+
146
+ n = Integer(raw, exception: false)
147
+ (n && n >= 1) ? n : 3
148
+ end
149
+
150
+ def path_attempts_path
151
+ File.join(@root, "path_attempts.json")
152
+ end
153
+
154
+ def read_path_attempts
155
+ return {} unless File.file?(path_attempts_path)
156
+
157
+ data = JSON.parse(File.read(path_attempts_path))
158
+ data.is_a?(Hash) ? data : {}
159
+ rescue JSON::ParserError
160
+ {}
161
+ end
162
+
163
+ def write_path_attempts!(hash)
164
+ atomic_write(path_attempts_path, JSON.generate(hash))
165
+ end
166
+
167
+ def failed_path
168
+ File.join(@root, "failed.jsonl")
169
+ end
170
+
171
+ def append_failed_lines!(paths, lease)
172
+ return if paths.empty?
173
+
174
+ File.open(failed_path, "a") do |io|
175
+ paths.each do |p|
176
+ io.puts(JSON.generate(
177
+ "path" => p.to_s,
178
+ "worker_id" => lease["worker_id"],
179
+ "attempts" => Integer(lease["attempts"] || 0),
180
+ "at" => Time.now.utc.iso8601
181
+ ))
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
188
+ # rubocop:enable Metrics/ModuleLength
@@ -1,6 +1,9 @@
1
1
  module Polyrun
2
2
  module Queue
3
3
  # Claim / run / ack loop for +polyrun run-queue+ workers (forked or direct).
4
+ #
5
+ # Successful batches stay one command invocation. On failure, paths are re-run one at a time so
6
+ # already-passing paths can be acked without reclaiming them with a poison neighbor.
4
7
  module WorkerLoop
5
8
  module_function
6
9
 
@@ -12,18 +15,25 @@ module Polyrun
12
15
  paths = claim["paths"] || []
13
16
  break if paths.empty?
14
17
 
18
+ store.heartbeat!(lease_id: claim["lease_id"], worker_id: worker_id)
15
19
  code = run_batch(cmd, paths)
16
20
  if code == 0
17
21
  store.ack!(lease_id: claim["lease_id"], worker_id: worker_id)
18
22
  batches_ok += 1
19
- elsif on_failure.to_s == "requeue"
20
- store.reclaim_lease!(claim["lease_id"])
21
- batches_fail += 1
22
- return {ok: batches_ok, fail: batches_fail, exit_code: 1}
23
- else
24
- batches_fail += 1
25
- return {ok: batches_ok, fail: batches_fail, exit_code: code.zero? ? 1 : code}
23
+ next
26
24
  end
25
+
26
+ outcome = isolate_failed_batch!(
27
+ store: store,
28
+ worker_id: worker_id,
29
+ lease_id: claim["lease_id"],
30
+ paths: paths,
31
+ cmd: cmd,
32
+ on_failure: on_failure
33
+ )
34
+ batches_ok += outcome[:ok]
35
+ batches_fail += outcome[:fail]
36
+ return {ok: batches_ok, fail: batches_fail, exit_code: outcome[:exit_code]} if outcome[:stop]
27
37
  end
28
38
  {ok: batches_ok, fail: batches_fail, exit_code: 0}
29
39
  rescue Polyrun::Error => e
@@ -34,6 +44,30 @@ module Polyrun
34
44
  def run_batch(cmd, paths)
35
45
  system(*cmd, *paths) ? 0 : ($?.exitstatus || 1)
36
46
  end
47
+
48
+ def isolate_failed_batch!(store:, worker_id:, lease_id:, paths:, cmd:, on_failure:)
49
+ ok = 0
50
+ paths.each do |path|
51
+ store.heartbeat!(lease_id: lease_id, worker_id: worker_id)
52
+ code = run_batch(cmd, [path])
53
+ if code == 0
54
+ store.ack_paths!(lease_id: lease_id, worker_id: worker_id, paths: [path])
55
+ ok += 1
56
+ next
57
+ end
58
+
59
+ if on_failure.to_s == "requeue"
60
+ store.reclaim_lease!(lease_id)
61
+ return {ok: ok, fail: 1, exit_code: 1, stop: true}
62
+ end
63
+
64
+ return {ok: ok, fail: 1, exit_code: code.zero? ? 1 : code, stop: true}
65
+ end
66
+
67
+ # Every path passed on the isolating re-run; lease should already be empty.
68
+ {ok: ok.positive? ? 1 : 0, fail: 0, exit_code: 0, stop: false}
69
+ end
70
+ private_class_method :isolate_failed_batch!
37
71
  end
38
72
  end
39
73
  end
@@ -53,7 +53,7 @@ module Polyrun
53
53
  path = File.expand_path(path)
54
54
  return {} unless File.file?(path)
55
55
 
56
- data = YAML.load_file(path)
56
+ data = YAML.safe_load_file(path, permitted_classes: [Symbol], aliases: true)
57
57
  data.is_a?(Hash) ? data : {}
58
58
  end
59
59
 
@@ -41,7 +41,6 @@ module Polyrun
41
41
 
42
42
  delta = numeric(a) - numeric(b)
43
43
  out[k] = delta if delta.positive? || k.start_with?("cpu")
44
- out[k] = delta
45
44
  end
46
45
  out
47
46
  end
@@ -158,13 +158,7 @@ module Polyrun
158
158
  violations << "zero_hit_examples=#{analysis[:zero_hit].size} max=#{max_zero}"
159
159
  end
160
160
 
161
- min_unique = cfg["minimum_unique_lines_per_example"]
162
- if min_unique
163
- bad = analysis[:zero_hit].keys
164
- if bad.size.positive?
165
- violations << "examples_below_minimum_unique_lines=#{bad.size} minimum=#{min_unique}"
166
- end
167
- end
161
+ violations.concat(minimum_unique_line_violations(merged, cfg))
168
162
 
169
163
  max_hot = cfg["max_hot_line_overlap"]
170
164
  if max_hot
@@ -175,6 +169,21 @@ module Polyrun
175
169
  violations
176
170
  end
177
171
 
172
+ def minimum_unique_line_violations(merged, cfg)
173
+ violations = []
174
+ min_unique = cfg["minimum_unique_lines_per_example"]
175
+ if min_unique
176
+ threshold = min_unique.to_i
177
+ examples = merged["examples"] || {}
178
+ bad = examples.select { |_loc, row| row["unique_lines"].to_i < threshold }.keys
179
+ if bad.size.positive?
180
+ violations << "examples_below_minimum_unique_lines=#{bad.size} minimum=#{min_unique}"
181
+ end
182
+ end
183
+
184
+ violations
185
+ end
186
+
178
187
  def emit_warnings!(merged, cfg = {})
179
188
  analyze(merged, cfg).fetch(:line_churn, {}).each do |loc, row|
180
189
  Polyrun::Log.warn "polyrun spec-quality line_churn: #{loc} churn=#{row["line_churn"]}"
@@ -22,7 +22,9 @@ prepare:
22
22
  # merge_spec_quality: true
23
23
  # merge_spec_quality_output: coverage/polyrun-spec-quality.json
24
24
 
25
- # Uncomment and edit when using Postgres template + per-shard DB names from polyrun env:
25
+ # Uncomment and edit when using Postgres template + per-shard DB names from polyrun env.
26
+ # CloneShards / polyrun start auto provision is PostgreSQL-only. For mysql2 (or other adapters),
27
+ # keep template_db + shard_db_pattern for URL shaping, set start.databases: false, and provision shards in prepare.
26
28
  # databases:
27
29
  # template_db: my_app_template
28
30
  # shard_db_pattern: "my_app_test_%{shard}"
@@ -1,3 +1,3 @@
1
1
  module Polyrun
2
- VERSION = "2.2.5"
2
+ VERSION = "2.2.6"
3
3
  end
@@ -55,20 +55,37 @@ module Polyrun
55
55
  out_read, out_write = IO.pipe
56
56
  err_read, err_write = IO.pipe
57
57
 
58
- pid =
59
- if hook_cfg.worker_hooks? && !Polyrun::Hooks.disabled?
60
- Process.spawn(
61
- child_env,
62
- "sh",
63
- "-c",
64
- hook_cfg.build_worker_shell_script(cmd, paths),
65
- out: out_write,
66
- err: err_write
67
- )
68
- else
69
- # Array(cmd) so a String command is one argv element (not character-splatted).
70
- Process.spawn(child_env, *Array(cmd), *Array(paths), out: out_write, err: err_write)
58
+ begin
59
+ pid =
60
+ if hook_cfg.worker_hooks? && !Polyrun::Hooks.disabled?
61
+ Process.spawn(
62
+ child_env,
63
+ "sh",
64
+ "-c",
65
+ hook_cfg.build_worker_shell_script(cmd, paths),
66
+ out: out_write,
67
+ err: err_write,
68
+ pgroup: true
69
+ )
70
+ else
71
+ # Array(cmd) so a String command is one argv element (not character-splatted).
72
+ Process.spawn(
73
+ child_env,
74
+ *Array(cmd),
75
+ *Array(paths),
76
+ out: out_write,
77
+ err: err_write,
78
+ pgroup: true
79
+ )
80
+ end
81
+ rescue
82
+ [out_read, out_write, err_read, err_write].each do |io|
83
+ io.close unless io.closed?
84
+ rescue IOError
85
+ nil
71
86
  end
87
+ raise
88
+ end
72
89
 
73
90
  out_write.close
74
91
  err_write.close
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: polyrun
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.5
4
+ version: 2.2.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
@@ -321,6 +321,7 @@ files:
321
321
  - lib/polyrun/process_stdio.rb
322
322
  - lib/polyrun/queue/duration.rb
323
323
  - lib/polyrun/queue/file_store.rb
324
+ - lib/polyrun/queue/file_store_intent.rb
324
325
  - lib/polyrun/queue/file_store_pending.rb
325
326
  - lib/polyrun/queue/worker_loop.rb
326
327
  - lib/polyrun/quick.rb