specwrk 0.9.1 → 0.10.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/README.md +0 -2
- data/docker/Dockerfile.server +7 -11
- data/docker/entrypoint.server.sh +2 -2
- data/docker/pitchfork.conf +5 -0
- data/lib/specwrk/cli.rb +2 -2
- data/lib/specwrk/client.rb +17 -0
- data/lib/specwrk/store.rb +17 -49
- data/lib/specwrk/version.rb +1 -1
- data/lib/specwrk/web/app.rb +2 -0
- data/lib/specwrk/web/auth.rb +1 -1
- data/lib/specwrk/web/endpoints.rb +60 -20
- data/lib/specwrk/worker.rb +9 -3
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9dfd4ef167bef2dc36de450f5855ff2ff376255ddd1e471426e08cb20d91a615
|
4
|
+
data.tar.gz: 0107c130003c627c5f3cc50949d9dc6cd3563e4f810b799527e6aa2234a19822
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1ffb03334f25bb2ca19a45e3004feb80ab3565e12c2e8aad43565000e741b37b2aa296683c1cf5215571cd25a79f5f53934cdfc427fea89a44e92d49d8c0a076
|
7
|
+
data.tar.gz: 4eb0f0e1a9469b1d0f23952d06b24b873d98e25ec3777bedf94a0b2be904038b225f39e9395556350c8c1cd057086f7af8866fbb38bf79ea5937b531be690993
|
data/README.md
CHANGED
@@ -58,7 +58,6 @@ Options:
|
|
58
58
|
--port=VALUE, -p VALUE # Server port. Overrides SPECWRK_SRV_PORT, default: "5138"
|
59
59
|
--bind=VALUE, -b VALUE # Server bind address. Overrides SPECWRK_SRV_BIND, default: "127.0.0.1"
|
60
60
|
--group-by=VALUE # How examples will be grouped for workers; fallback to file if no timings are found. Overrides SPECWERK_SRV_GROUP_BY: (file/timings), default: "timings"
|
61
|
-
--[no-]single-seed-per-run # Only allow one seed per run. Useful for CI where many nodes may seed at the same time, default: false
|
62
61
|
--[no-]verbose # Run in verbose mode. Default false., default: false
|
63
62
|
--help, -h # Print this help
|
64
63
|
```
|
@@ -83,7 +82,6 @@ Options:
|
|
83
82
|
--key=VALUE, -k VALUE # Authentication key clients must use for access. Overrides SPECWRK_SRV_KEY, default: ""
|
84
83
|
--output=VALUE, -o VALUE # Directory where worker output is stored. Overrides SPECWRK_OUT, default: ".specwrk/"
|
85
84
|
--group-by=VALUE # How examples will be grouped for workers; fallback to file if no timings are found. Overrides SPECWERK_SRV_GROUP_BY: (file/timings), default: "timings"
|
86
|
-
--[no-]single-seed-per-run # Only allow one seed per run. Useful for CI where many nodes may seed at the same time, default: false
|
87
85
|
--[no-]verbose # Run in verbose mode. Default false., default: false
|
88
86
|
--[no-]single-run # Act on shutdown requests from clients. Default: false., default: false
|
89
87
|
--help, -h # Print this help
|
data/docker/Dockerfile.server
CHANGED
@@ -1,11 +1,6 @@
|
|
1
1
|
FROM ruby:3.4-alpine
|
2
2
|
|
3
|
-
RUN apk add --no-cache
|
4
|
-
build-base \
|
5
|
-
ruby-dev \
|
6
|
-
linux-headers \
|
7
|
-
zlib-dev \
|
8
|
-
libffi-dev
|
3
|
+
RUN apk add --no-cache build-base
|
9
4
|
|
10
5
|
WORKDIR /app
|
11
6
|
|
@@ -13,14 +8,15 @@ RUN mkdir .specwrk/
|
|
13
8
|
|
14
9
|
ARG SPECWRK_SRV_PORT=5138
|
15
10
|
ARG SPECWRK_VERSION=latest
|
16
|
-
ARG
|
11
|
+
ARG GEM_FILE=specwrk-$SPECWRK_VERSION.gem
|
17
12
|
|
18
|
-
COPY $
|
19
|
-
RUN gem install ./$
|
20
|
-
RUN rm ./$
|
13
|
+
COPY $GEM_FILE ./
|
14
|
+
RUN gem install ./$GEM_FILE --no-document
|
15
|
+
RUN rm ./$GEM_FILE
|
21
16
|
|
22
|
-
RUN gem install
|
17
|
+
RUN gem install pitchfork thruster
|
23
18
|
COPY config.ru ./
|
19
|
+
COPY docker/pitchfork.conf ./
|
24
20
|
|
25
21
|
COPY docker/entrypoint.server.sh /usr/local/bin/entrypoint
|
26
22
|
RUN chmod +x /usr/local/bin/entrypoint
|
data/docker/entrypoint.server.sh
CHANGED
@@ -2,6 +2,6 @@
|
|
2
2
|
|
3
3
|
export THRUSTER_HTTP_PORT=${PORT:-5138}
|
4
4
|
export THRUSTER_TARGET_PORT=3000
|
5
|
-
export THRUSTER_HTTP_IDLE_TIMEOUT=${IDLE_TIMEOUT:-
|
5
|
+
export THRUSTER_HTTP_IDLE_TIMEOUT=${IDLE_TIMEOUT:-300}
|
6
6
|
|
7
|
-
exec thrust
|
7
|
+
exec thrust pitchfork -c pitchfork.conf
|
data/lib/specwrk/cli.rb
CHANGED
@@ -76,7 +76,6 @@ module Specwrk
|
|
76
76
|
base.unique_option :key, type: :string, aliases: ["-k"], default: ENV.fetch("SPECWRK_SRV_KEY", ""), desc: "Authentication key clients must use for access. Overrides SPECWRK_SRV_KEY"
|
77
77
|
base.unique_option :output, type: :string, default: ENV.fetch("SPECWRK_OUT", ".specwrk/"), aliases: ["-o"], desc: "Directory where worker output is stored. Overrides SPECWRK_OUT"
|
78
78
|
base.unique_option :group_by, values: %w[file timings], default: ENV.fetch("SPECWERK_SRV_GROUP_BY", "timings"), desc: "How examples will be grouped for workers; fallback to file if no timings are found. Overrides SPECWERK_SRV_GROUP_BY"
|
79
|
-
base.unique_option :single_seed_per_run, type: :boolean, default: false, desc: "Only allow one seed per run. Useful for CI where many nodes may seed at the same time"
|
80
79
|
base.unique_option :verbose, type: :boolean, default: false, desc: "Run in verbose mode. Default false."
|
81
80
|
end
|
82
81
|
|
@@ -211,7 +210,8 @@ module Specwrk
|
|
211
210
|
status "Server responding ✓"
|
212
211
|
status "Seeding #{examples.length} examples..."
|
213
212
|
Client.new.seed(examples)
|
214
|
-
|
213
|
+
file_count = examples.group_by { |e| e[:file_path] }.keys.size
|
214
|
+
status "🌱 Seeded #{examples.size} examples across #{file_count} files"
|
215
215
|
end
|
216
216
|
|
217
217
|
if Specwrk.wait_for_pids_exit([seed_pid]).value?(1)
|
data/lib/specwrk/client.rb
CHANGED
@@ -105,6 +105,23 @@ module Specwrk
|
|
105
105
|
(response.code == "200") ? true : raise(UnhandledResponseError.new("#{response.code}: #{response.body}"))
|
106
106
|
end
|
107
107
|
|
108
|
+
def complete_and_fetch_examples(examples)
|
109
|
+
response = post "/complete_and_pop", body: examples.to_json
|
110
|
+
|
111
|
+
case response.code
|
112
|
+
when "200"
|
113
|
+
JSON.parse(response.body, symbolize_names: true)
|
114
|
+
when "204"
|
115
|
+
raise WaitingForSeedError
|
116
|
+
when "404"
|
117
|
+
raise NoMoreExamplesError
|
118
|
+
when "410"
|
119
|
+
raise CompletedAllExamplesError
|
120
|
+
else
|
121
|
+
raise UnhandledResponseError.new("#{response.code}: #{response.body}")
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
108
125
|
def seed(examples)
|
109
126
|
response = post "/seed", body: examples.to_json
|
110
127
|
|
data/lib/specwrk/store.rb
CHANGED
@@ -7,40 +7,24 @@ require "specwrk/store/file_adapter"
|
|
7
7
|
|
8
8
|
module Specwrk
|
9
9
|
class Store
|
10
|
-
|
11
|
-
MUTEXES_MUTEX = Mutex.new # 🐢🐢🐢🐢
|
12
|
-
|
13
|
-
class << self
|
14
|
-
def mutex_for(path)
|
15
|
-
MUTEXES_MUTEX.synchronize do
|
16
|
-
MUTEXES[path] ||= Mutex.new
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
def initialize(path, thread_safe_reads: true)
|
10
|
+
def initialize(path)
|
22
11
|
@path = path
|
23
|
-
@thread_safe_reads = thread_safe_reads
|
24
12
|
end
|
25
13
|
|
26
14
|
def [](key)
|
27
|
-
|
15
|
+
adapter[key.to_s]
|
28
16
|
end
|
29
17
|
|
30
18
|
def multi_read(*keys)
|
31
|
-
|
19
|
+
adapter.multi_read(*keys)
|
32
20
|
end
|
33
21
|
|
34
22
|
def []=(key, value)
|
35
|
-
|
36
|
-
adapter[key.to_s] = value
|
37
|
-
end
|
23
|
+
adapter[key.to_s] = value
|
38
24
|
end
|
39
25
|
|
40
26
|
def keys
|
41
|
-
all_keys =
|
42
|
-
adapter.keys
|
43
|
-
end
|
27
|
+
all_keys = adapter.keys
|
44
28
|
|
45
29
|
all_keys.reject { |k| k.start_with? "____" }
|
46
30
|
end
|
@@ -54,28 +38,24 @@ module Specwrk
|
|
54
38
|
end
|
55
39
|
|
56
40
|
def empty?
|
57
|
-
|
58
|
-
adapter.empty?
|
59
|
-
end
|
41
|
+
adapter.empty?
|
60
42
|
end
|
61
43
|
|
62
44
|
def delete(*keys)
|
63
|
-
|
45
|
+
adapter.delete(*keys)
|
64
46
|
end
|
65
47
|
|
66
48
|
def merge!(h2)
|
67
49
|
h2.transform_keys!(&:to_s)
|
68
|
-
|
50
|
+
adapter.merge!(h2)
|
69
51
|
end
|
70
52
|
|
71
53
|
def clear
|
72
|
-
|
54
|
+
adapter.clear
|
73
55
|
end
|
74
56
|
|
75
57
|
def to_h
|
76
|
-
|
77
|
-
adapter.multi_read(*keys).transform_keys!(&:to_sym)
|
78
|
-
end
|
58
|
+
adapter.multi_read(*keys).transform_keys!(&:to_sym)
|
79
59
|
end
|
80
60
|
|
81
61
|
def inspect
|
@@ -92,16 +72,6 @@ module Specwrk
|
|
92
72
|
|
93
73
|
private
|
94
74
|
|
95
|
-
attr_reader :thread_safe_reads
|
96
|
-
|
97
|
-
def sync(thread_safe: true)
|
98
|
-
if !thread_safe || mutex.owned?
|
99
|
-
yield
|
100
|
-
else
|
101
|
-
mutex.synchronize { yield }
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
75
|
def adapter
|
106
76
|
@adapter ||= FileAdapter.new(@path)
|
107
77
|
end
|
@@ -123,15 +93,13 @@ module Specwrk
|
|
123
93
|
end
|
124
94
|
|
125
95
|
def shift_bucket
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
bucket_by_timings
|
134
|
-
end
|
96
|
+
return bucket_by_file unless run_time_bucket_maximum&.positive?
|
97
|
+
|
98
|
+
case ENV["SPECWRK_SRV_GROUP_BY"]
|
99
|
+
when "file"
|
100
|
+
bucket_by_file
|
101
|
+
else
|
102
|
+
bucket_by_timings
|
135
103
|
end
|
136
104
|
end
|
137
105
|
|
data/lib/specwrk/version.rb
CHANGED
data/lib/specwrk/web/app.rb
CHANGED
data/lib/specwrk/web/auth.rb
CHANGED
@@ -51,11 +51,11 @@ module Specwrk
|
|
51
51
|
end
|
52
52
|
|
53
53
|
def not_found
|
54
|
-
[404, {"
|
54
|
+
[404, {"content-type" => "text/plain"}, ["This is not the path you're looking for, 'ol chap..."]]
|
55
55
|
end
|
56
56
|
|
57
57
|
def ok
|
58
|
-
[200, {"
|
58
|
+
[200, {"content-type" => "text/plain"}, ["OK, 'ol chap"]]
|
59
59
|
end
|
60
60
|
|
61
61
|
def payload
|
@@ -83,11 +83,11 @@ module Specwrk
|
|
83
83
|
end
|
84
84
|
|
85
85
|
def metadata
|
86
|
-
@metadata ||= Store.new(File.join(datastore_path, "metadata")
|
86
|
+
@metadata ||= Store.new(File.join(datastore_path, "metadata"))
|
87
87
|
end
|
88
88
|
|
89
89
|
def run_times
|
90
|
-
@run_times ||= Store.new(File.join(ENV["SPECWRK_OUT"], "run_times")
|
90
|
+
@run_times ||= Store.new(File.join(ENV["SPECWRK_OUT"], "run_times"))
|
91
91
|
end
|
92
92
|
|
93
93
|
def worker
|
@@ -137,17 +137,15 @@ module Specwrk
|
|
137
137
|
|
138
138
|
class Seed < Base
|
139
139
|
def before_lock
|
140
|
-
examples_with_run_times
|
140
|
+
examples_with_run_times
|
141
141
|
end
|
142
142
|
|
143
143
|
def with_response
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
pending.merge!(examples_with_run_times)
|
149
|
-
end
|
144
|
+
pending.clear
|
145
|
+
new_run_time_bucket_maximums = [pending.run_time_bucket_maximum, @seeds_run_time_bucket_maximum.to_f].compact
|
146
|
+
pending.run_time_bucket_maximum = new_run_time_bucket_maximums.sum.to_f / new_run_time_bucket_maximums.length.to_f
|
150
147
|
|
148
|
+
pending.merge!(examples_with_run_times)
|
151
149
|
processing.clear
|
152
150
|
completed.clear
|
153
151
|
|
@@ -192,10 +190,6 @@ module Specwrk
|
|
192
190
|
(mean + Math.sqrt(variance)).round(2)
|
193
191
|
end
|
194
192
|
|
195
|
-
def persist_seeds?
|
196
|
-
ENV["SPECWRK_SRV_SINGLE_SEED_PER_RUN"].nil? || pending.empty?
|
197
|
-
end
|
198
|
-
|
199
193
|
def sort_by
|
200
194
|
if ENV["SPECWRK_SRV_GROUP_BY"] == "file" || run_times.empty?
|
201
195
|
:file
|
@@ -207,6 +201,7 @@ module Specwrk
|
|
207
201
|
|
208
202
|
class Complete < Base
|
209
203
|
def with_response
|
204
|
+
warn "[DEPRECATED] This endpoint will be retired in favor of CompleteAndPop. Upgrade your clients."
|
210
205
|
completed.merge!(completed_examples)
|
211
206
|
processing.delete(*completed_examples.keys)
|
212
207
|
|
@@ -235,20 +230,65 @@ module Specwrk
|
|
235
230
|
processing.merge!(processing_data)
|
236
231
|
|
237
232
|
if @examples.any?
|
238
|
-
[200, {"
|
233
|
+
[200, {"content-type" => "application/json"}, [JSON.generate(@examples)]]
|
234
|
+
elsif pending.empty? && processing.empty? && completed.empty?
|
235
|
+
[204, {"content-type" => "text/plain"}, ["Waiting for sample to be seeded."]]
|
236
|
+
elsif completed.any? && processing.empty?
|
237
|
+
[410, {"content-type" => "text/plain"}, ["That's a good lad. Run along now and go home."]]
|
238
|
+
else
|
239
|
+
not_found
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
class CompleteAndPop < Base
|
245
|
+
def with_response
|
246
|
+
completed.merge!(completed_examples)
|
247
|
+
run_times.merge! run_time_data
|
248
|
+
processing.delete(*completed_examples.keys)
|
249
|
+
|
250
|
+
@examples = pending.shift_bucket
|
251
|
+
|
252
|
+
processing_data = @examples.map { |example| [example[:id], example] }.to_h
|
253
|
+
processing.merge!(processing_data)
|
254
|
+
|
255
|
+
if @examples.any?
|
256
|
+
[200, {"content-type" => "application/json"}, [JSON.generate(@examples)]]
|
239
257
|
elsif pending.empty? && processing.empty? && completed.empty?
|
240
|
-
[204, {"
|
258
|
+
[204, {"content-type" => "text/plain"}, ["Waiting for sample to be seeded."]]
|
241
259
|
elsif completed.any? && processing.empty?
|
242
|
-
[410, {"
|
260
|
+
[410, {"content-type" => "text/plain"}, ["That's a good lad. Run along now and go home."]]
|
243
261
|
else
|
244
262
|
not_found
|
245
263
|
end
|
246
264
|
end
|
265
|
+
|
266
|
+
private
|
267
|
+
|
268
|
+
def before_lock
|
269
|
+
completed_examples
|
270
|
+
run_time_data
|
271
|
+
end
|
272
|
+
|
273
|
+
def completed_examples
|
274
|
+
@completed_data ||= payload.map { |example| [example[:id], example] if processing[example[:id]] }.compact.to_h
|
275
|
+
end
|
276
|
+
|
277
|
+
# We don't care about exact values here, just approximate run times are fine
|
278
|
+
# So if we overwrite run times from another process it is nbd
|
279
|
+
def after_lock
|
280
|
+
# run_time_data = payload.map { |example| [example[:id], example[:run_time]] }.to_h
|
281
|
+
# run_times.merge! run_time_data
|
282
|
+
end
|
283
|
+
|
284
|
+
def run_time_data
|
285
|
+
@run_time_data ||= payload.map { |example| [example[:id], example[:run_time]] }.to_h
|
286
|
+
end
|
247
287
|
end
|
248
288
|
|
249
289
|
class Report < Base
|
250
290
|
def with_response
|
251
|
-
[200, {"
|
291
|
+
[200, {"content-type" => "application/json"}, [JSON.generate(completed.dump)]]
|
252
292
|
end
|
253
293
|
end
|
254
294
|
|
@@ -259,7 +299,7 @@ module Specwrk
|
|
259
299
|
|
260
300
|
interupt! if ENV["SPECWRK_SRV_SINGLE_RUN"]
|
261
301
|
|
262
|
-
[200, {"
|
302
|
+
[200, {"content-type" => "text/plain"}, ["✌️"]]
|
263
303
|
end
|
264
304
|
|
265
305
|
def interupt!
|
data/lib/specwrk/worker.rb
CHANGED
@@ -69,15 +69,21 @@ module Specwrk
|
|
69
69
|
end
|
70
70
|
|
71
71
|
def execute
|
72
|
-
executor.run
|
72
|
+
executor.run next_examples
|
73
73
|
complete_examples
|
74
74
|
rescue UnhandledResponseError => e
|
75
|
-
# If fetching examples fails we can just try again so warn and return
|
75
|
+
# If fetching examples via next_exampels fails we can just try again so warn and return
|
76
|
+
# Expects complete_examples to rescue this error if raised in that method
|
76
77
|
warn e.message
|
77
78
|
end
|
78
79
|
|
80
|
+
def next_examples
|
81
|
+
return @next_examples if @next_examples&.length&.positive?
|
82
|
+
client.fetch_examples
|
83
|
+
end
|
84
|
+
|
79
85
|
def complete_examples
|
80
|
-
client.
|
86
|
+
@next_examples = client.complete_and_fetch_examples executor.examples
|
81
87
|
rescue UnhandledResponseError => e
|
82
88
|
# I do not think we should so lightly abandon the completion of executed examples
|
83
89
|
# try to complete until successful or terminated
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: specwrk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Westendorf
|
@@ -180,6 +180,7 @@ files:
|
|
180
180
|
- config.ru
|
181
181
|
- docker/Dockerfile.server
|
182
182
|
- docker/entrypoint.server.sh
|
183
|
+
- docker/pitchfork.conf
|
183
184
|
- examples/circleci/config.yml
|
184
185
|
- examples/github/specwrk-multi-node.yml
|
185
186
|
- examples/github/specwrk-single-node.yml
|