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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 657cd8e611ea7ea6344dc5bd540892e69d801ea5afd410b1be2e7a6d3f73658e
4
- data.tar.gz: c066775a6f57452cd55c49eec7604b8e264881ad085d7847ff176240a9ecbe05
3
+ metadata.gz: 9dfd4ef167bef2dc36de450f5855ff2ff376255ddd1e471426e08cb20d91a615
4
+ data.tar.gz: 0107c130003c627c5f3cc50949d9dc6cd3563e4f810b799527e6aa2234a19822
5
5
  SHA512:
6
- metadata.gz: 076a41a98119ccfe0108c76c32341d8cbf6642da3f37f80276a61ed885fbca9ddaf31c597b8462ddeb608c67d7e8787f5cefd606cb4b977aeef715e9a5cdb0bb
7
- data.tar.gz: 39891db9a0400196ddaba587e7014aa00e485c65ce37efc997998f854dd98d8cabfef39806657bec15e71c43a29743dda4081b35c2022a917a5503e4e161cea4
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
@@ -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 GEMFILE=specwrk-$SPECWRK_VERSION.gem
11
+ ARG GEM_FILE=specwrk-$SPECWRK_VERSION.gem
17
12
 
18
- COPY $GEMFILE ./
19
- RUN gem install ./$GEMFILE --no-document
20
- RUN rm ./$GEMFILE
13
+ COPY $GEM_FILE ./
14
+ RUN gem install ./$GEM_FILE --no-document
15
+ RUN rm ./$GEM_FILE
21
16
 
22
- RUN gem install puma thruster
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
@@ -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:-305}
5
+ export THRUSTER_HTTP_IDLE_TIMEOUT=${IDLE_TIMEOUT:-300}
6
6
 
7
- exec thrust puma --workers 0 --bind tcp://127.0.0.1:3000 --threads ${PUMA_THREADS:-1} --workers ${PUMA_WORKERS:-0}
7
+ exec thrust pitchfork -c pitchfork.conf
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ worker_processes 1
4
+ listen "localhost:3000", backlog: 2048
5
+ timeout 301
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
- status "Samples seeded ✓"
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)
@@ -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
- MUTEXES = {}
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
- sync(thread_safe: thread_safe_reads) { adapter[key.to_s] }
15
+ adapter[key.to_s]
28
16
  end
29
17
 
30
18
  def multi_read(*keys)
31
- sync(thread_safe: thread_safe_reads) { adapter.multi_read(*keys) }
19
+ adapter.multi_read(*keys)
32
20
  end
33
21
 
34
22
  def []=(key, value)
35
- sync do
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 = sync(thread_safe: thread_safe_reads) do
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
- sync(thread_safe: thread_safe_reads) do
58
- adapter.empty?
59
- end
41
+ adapter.empty?
60
42
  end
61
43
 
62
44
  def delete(*keys)
63
- sync { adapter.delete(*keys) }
45
+ adapter.delete(*keys)
64
46
  end
65
47
 
66
48
  def merge!(h2)
67
49
  h2.transform_keys!(&:to_s)
68
- sync { adapter.merge!(h2) }
50
+ adapter.merge!(h2)
69
51
  end
70
52
 
71
53
  def clear
72
- sync { adapter.clear }
54
+ adapter.clear
73
55
  end
74
56
 
75
57
  def to_h
76
- sync(thread_safe: thread_safe_reads) do
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
- sync do
127
- return bucket_by_file unless run_time_bucket_maximum&.positive?
128
-
129
- case ENV["SPECWRK_SRV_GROUP_BY"]
130
- when "file"
131
- bucket_by_file
132
- else
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Specwrk
4
- VERSION = "0.9.1"
4
+ VERSION = "0.10.1"
5
5
  end
@@ -90,6 +90,8 @@ module Specwrk
90
90
  Endpoints::Pop
91
91
  when ["POST", "/complete"]
92
92
  Endpoints::Complete
93
+ when ["POST", "/complete_and_pop"]
94
+ Endpoints::CompleteAndPop
93
95
  when ["POST", "/seed"]
94
96
  Endpoints::Seed
95
97
  when ["GET", "/report"]
@@ -28,7 +28,7 @@ module Specwrk
28
28
  private
29
29
 
30
30
  def unauthorized
31
- [401, {"Content-Type" => "application/json"}, ["Unauthorized"]]
31
+ [401, {"content-type" => "application/json"}, ["Unauthorized"]]
32
32
  end
33
33
  end
34
34
  end
@@ -51,11 +51,11 @@ module Specwrk
51
51
  end
52
52
 
53
53
  def not_found
54
- [404, {"Content-Type" => "text/plain"}, ["This is not the path you're looking for, 'ol chap..."]]
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, {"Content-Type" => "text/plain"}, ["OK, 'ol chap"]]
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"), thread_safe_reads: false)
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"), thread_safe_reads: false)
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 if persist_seeds?
140
+ examples_with_run_times
141
141
  end
142
142
 
143
143
  def with_response
144
- if persist_seeds?
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
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, {"Content-Type" => "application/json"}, [JSON.generate(@examples)]]
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, {"Content-Type" => "text/plain"}, ["Waiting for sample to be seeded."]]
258
+ [204, {"content-type" => "text/plain"}, ["Waiting for sample to be seeded."]]
241
259
  elsif completed.any? && processing.empty?
242
- [410, {"Content-Type" => "text/plain"}, ["That's a good lad. Run along now and go home."]]
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, {"Content-Type" => "application/json"}, [JSON.generate(completed.dump)]]
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, {"Content-Type" => "text/plain"}, ["✌️"]]
302
+ [200, {"content-type" => "text/plain"}, ["✌️"]]
263
303
  end
264
304
 
265
305
  def interupt!
@@ -69,15 +69,21 @@ module Specwrk
69
69
  end
70
70
 
71
71
  def execute
72
- executor.run client.fetch_examples
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.complete_examples executor.examples
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.9.1
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