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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +1 -0
- data/docs/SETUP_PROFILE.md +1 -0
- data/lib/polyrun/cli/database_commands.rb +21 -7
- data/lib/polyrun/cli/default_run.rb +13 -3
- data/lib/polyrun/cli/helpers.rb +41 -6
- data/lib/polyrun/cli/plan_command.rb +10 -29
- data/lib/polyrun/cli/run_queue_command.rb +10 -1
- data/lib/polyrun/cli/run_shards_parallel_children.rb +52 -39
- data/lib/polyrun/cli/run_shards_parallel_wait.rb +2 -2
- data/lib/polyrun/cli/run_shards_planning.rb +7 -29
- data/lib/polyrun/cli/run_shards_run.rb +1 -1
- data/lib/polyrun/cli/run_shards_worker_interrupt.rb +16 -14
- data/lib/polyrun/cli/start_bootstrap.rb +51 -22
- data/lib/polyrun/cli.rb +3 -0
- data/lib/polyrun/config.rb +9 -4
- data/lib/polyrun/coverage/example_diff.rb +6 -7
- data/lib/polyrun/coverage/merge/formatters_html.rb +20 -2
- data/lib/polyrun/coverage/merge.rb +9 -3
- data/lib/polyrun/coverage/merge_fragment_meta.rb +6 -1
- data/lib/polyrun/coverage/rails.rb +1 -1
- data/lib/polyrun/database/clone_shards.rb +28 -5
- data/lib/polyrun/database/provision.rb +15 -9
- data/lib/polyrun/database/url_builder/connection/infer.rb +4 -0
- data/lib/polyrun/database/url_builder.rb +31 -3
- data/lib/polyrun/partition/plan.rb +6 -24
- data/lib/polyrun/partition/timing_diagnostics.rb +2 -3
- data/lib/polyrun/queue/file_store.rb +133 -30
- data/lib/polyrun/queue/file_store_intent.rb +188 -0
- data/lib/polyrun/queue/worker_loop.rb +41 -7
- data/lib/polyrun/spec_quality/config.rb +1 -1
- data/lib/polyrun/spec_quality/profile.rb +0 -1
- data/lib/polyrun/spec_quality/report.rb +16 -7
- data/lib/polyrun/templates/rails_prepare.polyrun.yml +3 -1
- data/lib/polyrun/version.rb +1 -1
- data/lib/polyrun/worker_output.rb +30 -13
- 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 =
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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.
|
|
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
|
-
|
|
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
|
|
@@ -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
|
-
|
|
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}"
|
data/lib/polyrun/version.rb
CHANGED
|
@@ -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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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.
|
|
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
|