specwrk 0.15.10 → 0.16.0

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: 1201e277f5b36c6ac9ff9ac20cf2b591f54d42822b071cf61b57a463164c5c4e
4
- data.tar.gz: 0c53e4c86f48062680801f3d0de4ac5f691e55b3afaae98398750b9d8bca865d
3
+ metadata.gz: 1736573f9c3fc84992518a7a2ea486c03eefe4885b8b857a79b15551533deec0
4
+ data.tar.gz: 7106e3b4c856bfd9ff89e5fe562ba3da45ad5981356b295e24b55579aa48609f
5
5
  SHA512:
6
- metadata.gz: 77142f3689fc39ec758869a42611d40b0f2013ebcba1a3c76a3ed4e2d7b6758f80ac0d9e2a93f58fcf58de9d3ada76ed34380d3a81dfc1bc2d1a49cab98fc67c
7
- data.tar.gz: bf4c4f3d3784feaed50f41d84e52dac09d232e6fc9d14e79c00961f79286c25775e4a922c4512a0f17ffd45b4bc863ae61b8fe6a70ab9c233c033303e4c7245f
6
+ metadata.gz: 067222d93a38d847886d40b5a5121e24d68f633115ecfddccdd7c199adf598fd780cb8ca5fd1110028679828436ac3bc8a4335ca54c7a481aeaa7dc85938d0a4
7
+ data.tar.gz: 5d10c755bb71d7ace592e33c5fa8b2c4a2d9a349805870914ca0c9ac3410306765324211820c1283c8c2d8846ddbbcf5d1c88f89f2d5b89899f554a6e5c187dd
data/README.md CHANGED
@@ -34,7 +34,7 @@ Commands:
34
34
  Intended for quick ad-hoc local host development or single-node CI runs. This command starts a queue server, seeds it with examples from the `spec/` directory, and starts `8` worker processes. It will report the ultimate success or failure.
35
35
 
36
36
  ```sh
37
- $ start --help
37
+ $ specwrk start --help
38
38
  Command:
39
39
  specwrk start
40
40
 
@@ -52,6 +52,7 @@ Options:
52
52
  --key=VALUE, -k VALUE # Authentication key clients must use for access. Overrides SPECWRK_SRV_KEY, default: ""
53
53
  --run=VALUE, -r VALUE # The run identifier for this job execution. Overrides SPECWRK_RUN, default: "main"
54
54
  --timeout=VALUE, -t VALUE # The amount of time to wait for the server to respond. Overrides SPECWRK_TIMEOUT, default: "5"
55
+ --network-retries=VALUE # The number of times to retry in the event of a network failure. Overrides SPECWRK_NETWORK_RETRIES, default: "1"
55
56
  --id=VALUE # The identifier for this worker. Overrides SPECWRK_ID. If none provided one in the format of specwrk-worker-8_RAND_CHARS-COUNT_INDEX will be used
56
57
  --count=VALUE, -c VALUE # The number of worker processes you want to start, default: 1
57
58
  --output=VALUE, -o VALUE # Directory where worker output is stored. Overrides SPECWRK_OUT, default: ".specwrk/"
@@ -113,6 +114,7 @@ Options:
113
114
  --key=VALUE, -k VALUE # Authentication key clients must use for access. Overrides SPECWRK_SRV_KEY, default: ""
114
115
  --run=VALUE, -r VALUE # The run identifier for this job execution. Overrides SPECWRK_RUN, default: "main"
115
116
  --timeout=VALUE, -t VALUE # The amount of time to wait for the server to respond. Overrides SPECWRK_TIMEOUT, default: "5"
117
+ --network-retries=VALUE # The number of times to retry in the event of a network failure. Overrides SPECWRK_NETWORK_RETRIES, default: "1"
116
118
  --max-retries=VALUE # Number of times an example will be re-run should it fail, default: 0
117
119
  --help, -h # Print this help
118
120
  ```
@@ -140,6 +142,7 @@ Options:
140
142
  --key=VALUE, -k VALUE # Authentication key clients must use for access. Overrides SPECWRK_SRV_KEY, default: ""
141
143
  --run=VALUE, -r VALUE # The run identifier for this job execution. Overrides SPECWRK_RUN, default: "main"
142
144
  --timeout=VALUE, -t VALUE # The amount of time to wait for the server to respond. Overrides SPECWRK_TIMEOUT, default: "5"
145
+ --network-retries=VALUE # The number of times to retry in the event of a network failure. Overrides SPECWRK_NETWORK_RETRIES, default: "1"
143
146
  --help, -h # Print this help
144
147
  ```
145
148
 
@@ -164,13 +167,13 @@ Options:
164
167
  ```
165
168
 
166
169
  ## Configuring your test environment
167
- If you test suite tracks state, starts servers, etc. and you plan on running many processes on the same node, you'll need to make
170
+ If your test suite tracks state, starts servers, etc. and you plan on running many processes on the same node, you'll need to make
168
171
  adjustments to avoid conflicting port usage or database/state mutations.
169
172
 
170
173
  `specwrk` workers will have `TEST_ENV_NUMBER={i}` set to help you configure approriately.
171
174
 
172
175
  ### Rails
173
- Rails has had easy multi-process test setup for a while now by creating unique test databases per process. For my rails v7.2 app which uses PostgreSQL and Capyabara, I made these changes to my `spec/rails_helper.rb`:
176
+ Rails has had easy multi-process test setup for a while now by creating unique test databases per process. For my rails v7.2 app which uses PostgreSQL and Capybara, I made these changes to my `spec/rails_helper.rb`:
174
177
 
175
178
  ```diff
176
179
  ++ if ENV["TEST_ENV_NUMBER"]
@@ -200,7 +203,7 @@ Make sure to persist `$SPECWRK_OUT/report.json` between runs so that subsequent
200
203
  [CircleCI Example](https://github.com/danielwestendorf/specwrk/blob/main/.circleci/config.yml) (specwrk-single-node job)
201
204
 
202
205
  ### Multi-node, multi-process
203
- Multi-node, multi-process works best when have many nodes running tests. This distributes the test execution across the nodes until the queue is for the run is empty, optimizing for slowest specs first. This distributes test execution across all nodes evenly(-ish).
206
+ Multi-node, multi-process works best when you have many nodes running tests. This distributes the test execution across the nodes until the queue is for the run is empty, optimizing for slowest specs first. This distributes test execution across all nodes evenly(-ish).
204
207
 
205
208
  To accomplish this, a central queue server is required, examples must be explicitly seeded, and workers explicitly started.
206
209
 
@@ -221,7 +224,7 @@ Start a persistent Queue Server given one of the following methods
221
224
 
222
225
  ### Configuring your Queue Server
223
226
  - Secure your server with a key either with the `SPECWRK_SRV_KEY` environment variable or `--key` CLI option
224
- - Configure the server output to be a persisted volume so your timings survive between system restarts with the `SPECWRK_SRV_STORE_URI` environment variable or `--store-uri` CLI option. By default, `memory:///` will be used for the run's data stores (so run data will no survive server restarts) while `file://#{Dir.tmpdir}` will be used for run timings. Pass `--store-uri file:///whatever/absolute/path` to store all data on disk (required for multiple server processes).
227
+ - Configure the server output to be a persisted volume so your timings survive between system restarts with the `SPECWRK_SRV_STORE_URI` environment variable or `--store-uri` CLI option. By default, `memory:///` will be used for the run's data stores (so run data will not survive server restarts) while `file://#{Dir.tmpdir}` will be used for run timings. Pass `--store-uri file:///whatever/absolute/path` to store all data on disk (required for multiple server processes).
225
228
 
226
229
  See [specwrk-store-redis_adapter](https://github.com/danielwestendorf/specwrk-store-redis_adapter) for Redis-compatible backed storage.
227
230
 
@@ -245,7 +248,7 @@ map(/_spec\.rb$/) do |spec_path|
245
248
  spec_path
246
249
  end
247
250
 
248
- # If a file in lib changes, map it to the spec folder for it's spec file
251
+ # If a file in lib changes, map it to the spec folder for its spec file
249
252
  map(/lib\/.*\.rb$/) do |path|
250
253
  path.gsub(/lib\/(.+)\.rb/, "spec/\\1_spec.rb")
251
254
  end
@@ -255,7 +258,7 @@ end
255
258
  # path.gsub(/app\/models\/(.+)\.rb/, "spec/models/\\1_spec.rb")
256
259
  # end
257
260
  #
258
- # If a controlelr file changes (assuming rails app structure), run the controller and system specs file
261
+ # If a controller file changes (assuming rails app structure), run the controller and system specs file
259
262
  # map(/app\/controllers\/.*.rb$/) do |path|
260
263
  # [
261
264
  # path.gsub(/app\/controllers\/(.+)\.rb/, "spec/controllers/\\1_spec.rb"),
@@ -265,7 +268,7 @@ end
265
268
  ```
266
269
 
267
270
  ## Prior/other works
268
- There are many prior works for running rspec tests across multiple processes. Most of them combine process output making failures hard to grok. Some are good at running tests locally, but not on CI, while others are inversely true. Others are comercial or impactical without making a purchase.
271
+ There are many prior works for running rspec tests across multiple processes. Most of them combine process output making failures hard to grok. Some are good at running tests locally, but not on CI, while others are inversely true. Others are commercial or impractical without making a purchase.
269
272
 
270
273
  specwrk is different because it:
271
274
  1. Puts your developer experience first. Easy execution. No messy outputs. Retries built in. Easy(er) debugging of flaky tests.
@@ -287,7 +290,7 @@ specwrk is different because it:
287
290
 
288
291
  ## Contributing
289
292
 
290
- Bug reports and pull requests are welcome on GitHub at https://github.com/dwestendorf/specwrk.
293
+ Bug reports and pull requests are welcome on GitHub at https://github.com/danielwestendorf/specwrk.
291
294
 
292
295
  ## License
293
296
 
data/lib/specwrk/cli.rb CHANGED
@@ -20,13 +20,15 @@ module Specwrk
20
20
  base.unique_option :key, type: :string, default: ENV.fetch("SPECWRK_SRV_KEY", ""), aliases: ["-k"], desc: "Authentication key clients must use for access. Overrides SPECWRK_SRV_KEY"
21
21
  base.unique_option :run, type: :string, default: ENV.fetch("SPECWRK_RUN", "main"), aliases: ["-r"], desc: "The run identifier for this job execution. Overrides SPECWRK_RUN"
22
22
  base.unique_option :timeout, type: :integer, default: ENV.fetch("SPECWRK_TIMEOUT", "5"), aliases: ["-t"], desc: "The amount of time to wait for the server to respond. Overrides SPECWRK_TIMEOUT"
23
+ base.unique_option :network_retries, type: :integer, default: ENV.fetch("SPECWRK_NETWORK_RETRIES", "1"), desc: "The number of times to retry in the event of a network failure. Overrides SPECWRK_NETWORK_RETRIES"
23
24
  end
24
25
 
25
- on_setup do |uri:, key:, run:, timeout:, **|
26
+ on_setup do |uri:, key:, run:, timeout:, network_retries:, **|
26
27
  ENV["SPECWRK_SRV_URI"] = uri
27
28
  ENV["SPECWRK_SRV_KEY"] = key
28
29
  ENV["SPECWRK_RUN"] = run
29
30
  ENV["SPECWRK_TIMEOUT"] = timeout
31
+ ENV["SPECWRK_NETWORK_RETRIES"] = network_retries
30
32
  end
31
33
  end
32
34
 
@@ -115,16 +115,6 @@ module Specwrk
115
115
  else
116
116
  raise UnhandledResponseError.new("#{response.code}: #{response.body}")
117
117
  end
118
- rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout => e
119
- @retry_count ||= 0
120
- @retry_count += 1
121
-
122
- raise e if @retry_count == 5
123
-
124
- warn e
125
- sleep @retry_count
126
-
127
- retry
128
118
  end
129
119
 
130
120
  def seed(examples, max_retries)
@@ -172,6 +162,16 @@ module Specwrk
172
162
  @worker_status = response["x-specwrk-status"].to_i if response["x-specwrk-status"]
173
163
  end
174
164
  end
165
+ rescue Net::ReadTimeout, Net::WriteTimeout => e
166
+ @retry_count ||= 0
167
+
168
+ raise e if @retry_count == ENV["SPECWRK_NETWORK_RETRIES"].to_i
169
+ @retry_count += 1
170
+
171
+ warn e
172
+ sleep @retry_count
173
+
174
+ retry
175
175
  end
176
176
 
177
177
  def default_headers
data/lib/specwrk/store.rb CHANGED
@@ -105,6 +105,47 @@ module Specwrk
105
105
  end
106
106
  end
107
107
 
108
+ class WorkerStore < Store
109
+ FIRST_SEEN_AT_KEY = :____first_seen_at_key
110
+ LAST_SEEN_AT_KEY = :____last_seen_at_key
111
+
112
+ def first_seen_at=(val)
113
+ @first_seen_at = nil
114
+
115
+ self[FIRST_SEEN_AT_KEY] = val.to_i
116
+ end
117
+
118
+ def first_seen_at
119
+ @first_seen_at ||= begin
120
+ value = self[FIRST_SEEN_AT_KEY]
121
+ return @first_seen_at = value unless value
122
+
123
+ @first_seen_at = Time.at(value.to_i)
124
+ end
125
+ end
126
+
127
+ def last_seen_at=(val)
128
+ @last_seen_at = nil
129
+
130
+ self[LAST_SEEN_AT_KEY] = val.to_i
131
+ end
132
+
133
+ def last_seen_at
134
+ @last_seen_at ||= begin
135
+ value = self[LAST_SEEN_AT_KEY]
136
+ return @last_seen_at = value unless value
137
+
138
+ @last_seen_at = Time.at(value.to_i)
139
+ end
140
+ end
141
+
142
+ def reload
143
+ @last_seen_at = nil
144
+ @first_seen_at = nil
145
+ super
146
+ end
147
+ end
148
+
108
149
  class PendingStore < Store
109
150
  RUN_TIME_BUCKET_MAXIMUM_KEY = :____run_time_bucket_maximum
110
151
  ORDER_KEY = :____order
@@ -234,22 +275,6 @@ module Specwrk
234
275
  end
235
276
 
236
277
  class ProcessingStore < Store
237
- def expired
238
- @expired ||= begin
239
- bucket = []
240
-
241
- keys.each_slice(24).each do |key_group|
242
- examples = multi_read(*key_group)
243
- examples.each do |id, example|
244
- next if example[:completion_threshold].nil?
245
-
246
- bucket << [id, example] if example[:completion_threshold] < Time.now.to_i
247
- end
248
- end
249
-
250
- bucket.to_h
251
- end
252
- end
253
278
  end
254
279
 
255
280
  class CompletedStore < Store
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Specwrk
4
- VERSION = "0.15.10"
4
+ VERSION = "0.16.0"
5
5
  end
@@ -21,8 +21,8 @@ module Specwrk
21
21
 
22
22
  before_lock
23
23
 
24
- worker[:first_seen_at] ||= Time.now.iso8601
25
- worker[:last_seen_at] = Time.now.iso8601
24
+ worker.first_seen_at ||= Time.now
25
+ worker.last_seen_at = Time.now
26
26
 
27
27
  final_response = with_lock do
28
28
  started_at = metadata[:started_at] ||= Time.now.iso8601
@@ -109,7 +109,11 @@ module Specwrk
109
109
  end
110
110
 
111
111
  def worker
112
- @worker ||= Store.new(ENV.fetch("SPECWRK_SRV_STORE_URI", "memory:///"), File.join(run_id, "workers", request.get_header("HTTP_X_SPECWRK_ID").to_s))
112
+ @worker ||= worker_store_for(worker_id)
113
+ end
114
+
115
+ def worker_id
116
+ request.get_header("HTTP_X_SPECWRK_ID").to_s
113
117
  end
114
118
 
115
119
  def worker_status
@@ -118,6 +122,10 @@ module Specwrk
118
122
  worker[:failed] || 1
119
123
  end
120
124
 
125
+ def worker_store_for(id)
126
+ WorkerStore.new(ENV.fetch("SPECWRK_SRV_STORE_URI", "memory:///"), File.join(run_id, "workers", id))
127
+ end
128
+
121
129
  def run_id
122
130
  request.get_header("HTTP_X_SPECWRK_RUN")
123
131
  end
@@ -15,9 +15,9 @@ module Specwrk
15
15
  [204, {"content-type" => "text/plain"}, ["Waiting for sample to be seeded."]]
16
16
  elsif completed.any? && processing.empty?
17
17
  [410, {"content-type" => "text/plain"}, ["That's a good lad. Run along now and go home."]]
18
- elsif processing.any? && processing.expired.keys.any?
19
- pending.merge!(processing.expired)
20
- processing.delete(*processing.expired.keys)
18
+ elsif expired_examples.length.positive?
19
+ pending.merge!(expired_examples.each { |_id, example| example[:worker_id] = worker_id })
20
+ processing.delete(*expired_examples.keys)
21
21
  @examples = nil
22
22
 
23
23
  [200, {"content-type" => "application/json"}, [JSON.generate(examples)]]
@@ -29,13 +29,10 @@ module Specwrk
29
29
  def examples
30
30
  @examples ||= begin
31
31
  examples = pending.shift_bucket
32
- bucket_run_time_total = examples.map { |example| example.fetch(:expected_run_time, 10.0) }.compact.sum * 2
33
- maximum_completion_threshold = (pending.run_time_bucket_maximum || 30.0) * 2
34
- completion_threshold = Time.now + [bucket_run_time_total, maximum_completion_threshold, 20.0].max
35
32
 
36
33
  processing_data = examples.map do |example|
37
34
  [
38
- example[:id], example.merge(completion_threshold: completion_threshold.to_f)
35
+ example[:id], example.merge(worker_id: worker_id, processing_started_at: Time.now.to_i)
39
36
  ]
40
37
  end
41
38
 
@@ -44,6 +41,27 @@ module Specwrk
44
41
  examples
45
42
  end
46
43
  end
44
+
45
+ def expired_examples
46
+ return unless processing.any?
47
+
48
+ @expired_examples ||= processing.to_h.select { |_id, example| expired?(example) }
49
+ end
50
+
51
+ # Has the worker missed two heartbeat check-ins?
52
+ def expired?(example)
53
+ return false unless example[:worker_id]
54
+ return false unless example[:processing_started_at]
55
+ return false unless example[:processing_started_at] < (Time.now - 20).to_i
56
+
57
+ workers_last_heartbeats[example[:worker_id]] < Time.now - 20
58
+ end
59
+
60
+ def workers_last_heartbeats
61
+ @workers_last_heartbeats ||= Hash.new do |h, k|
62
+ h[k] = worker_store_for(k).last_seen_at || Time.at(0)
63
+ end
64
+ end
47
65
  end
48
66
  end
49
67
  end
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.15.10
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Westendorf