specwrk 0.4.11 → 0.6.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: e85bdff31c7f1312b12a9a823200a5da5b36653433496556cc1bce274a3dc90a
4
- data.tar.gz: fa73aebb69753d7b528d333f2fca589e0e5525bd12b71e4c5b7815df0932b9a1
3
+ metadata.gz: bf78cf2d1c82bc0878e1e89777eb8de0ab04af441e210c64a15c2bad1277c1fa
4
+ data.tar.gz: 3701d6f676e4c31c3fb787f4ca59a8aaa7aa412118371470b069471da4e6c933
5
5
  SHA512:
6
- metadata.gz: 9467744c0cd676bdeb312b3f1deb4190113601cb01bb9ce939acdfa1c6170897904f3125b43c90dde536459ed56b2623be132bfeed84725c1f6b098ea65ae6a9
7
- data.tar.gz: f8cd7800b9f117567c1aa53404f2ca7f9a99a907c4b46c1c05da34d7534a2743c68960c50ffd9555de82430f582cb6a042a70579e4a8a44f53dea136f72cd8ae
6
+ metadata.gz: debda5ab7128ae03e74652196da8783b4d78ddf3648ccfd2c3e62104fe8a3fb87b98abbc68deb7d66565d66474a504a8d778ca0de44eef311e14188a4258f856
7
+ data.tar.gz: 1a4089b567a0840f91a133fef5580b6c5bd3c43190eb7cd1156fe3439e042f727b62bdf69ee1624815ac741f738907e43c92559be1f4c4d55268b2db1625b074
data/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## v0.4.11
4
+
5
+ - Move logic out of CLI methods ([#64](https://github.com/danielwestendorf/specwrk/issues/64)) by @danielwestendorf
6
+ - Add thruster in front of puma ([#65](https://github.com/danielwestendorf/specwrk/issues/65)) by @danielwestendorf
7
+ - Revise heartbeats logic ([#66](https://github.com/danielwestendorf/specwrk/issues/66)) by @danielwestendorf
8
+
9
+ ## v0.4.9
10
+
11
+ - Remove single-run env var (it was not what I wanted) by @danielwestendorf
12
+
13
+ ## v0.4.8
14
+
15
+ - Fix `config.ru` by @danielwestendorf
16
+
17
+ ## v0.4.7
18
+
19
+ - Switch to Puma for the Docker image (#59) by @danielwestendorf
20
+
21
+ ## v0.4.6
22
+
23
+ - Nil env var that will prevent start command from completing ([#56](https://github.com/danielwestendorf/specwrk/issues/56)) by @danielwestendorf
24
+ - Better handling of seed failing ([#57](https://github.com/danielwestendorf/specwrk/issues/57)) by @danielwestendorf
25
+ - Silence health logging ([#58](https://github.com/danielwestendorf/specwrk/issues/58)) by @danielwestendorf
26
+ - Add missing CCI caching of `report.json` ([#55](https://github.com/danielwestendorf/specwrk/issues/55)) by @danielwestendorf
27
+ - Add CircleCI examples ([#50](https://github.com/danielwestendorf/specwrk/issues/50)) by @danielwestendorf
28
+ - Better GHA Examples ([#49](https://github.com/danielwestendorf/specwrk/issues/49)) by @danielwestendorf
29
+ - Skip key lookup and rely on the result of `Hash#delete` instead by @danielwestendorf
30
+
3
31
  ## v0.4.5
4
32
 
5
33
  - Set ENV var when generating seed examples [#47](https://github.com/danielwestendorf/specwrk/issues/47). by @danielwestendorf
data/README.md CHANGED
@@ -129,7 +129,7 @@ Description:
129
129
  Start one or more worker processes
130
130
 
131
131
  Options:
132
- --id=VALUE # The identifier for this worker. Default specwrk-worker(-COUNT_INDEX), default: "specwrk-worker"
132
+ --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
133
133
  --count=VALUE, -c VALUE # The number of worker processes you want to start, default: 1
134
134
  --output=VALUE, -o VALUE # Directory where worker output is stored. Overrides SPECWRK_OUT, default: ".specwrk/"
135
135
  --seed-waits=VALUE, -w VALUE # Number of times the worker will wait for examples to be seeded to the server. 1sec between attempts. Overrides SPECWRK_SEED_WAITS, default: "10"
data/lib/specwrk/cli.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "pathname"
4
+ require "securerandom"
4
5
 
5
6
  require "dry/cli"
6
7
 
@@ -33,14 +34,15 @@ module Specwrk
33
34
  extend Hookable
34
35
 
35
36
  on_included do |base|
36
- base.unique_option :id, type: :string, default: "specwrk-worker", desc: "The identifier for this worker. Default specwrk-worker(-COUNT_INDEX)"
37
+ base.unique_option :id, type: :string, desc: "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"
37
38
  base.unique_option :count, type: :integer, default: 1, aliases: ["-c"], desc: "The number of worker processes you want to start"
38
39
  base.unique_option :output, type: :string, default: ENV.fetch("SPECWRK_OUT", ".specwrk/"), aliases: ["-o"], desc: "Directory where worker output is stored. Overrides SPECWRK_OUT"
39
40
  base.unique_option :seed_waits, type: :integer, default: ENV.fetch("SPECWRK_SEED_WAITS", "10"), aliases: ["-w"], desc: "Number of times the worker will wait for examples to be seeded to the server. 1sec between attempts. Overrides SPECWRK_SEED_WAITS"
40
41
  end
41
42
 
42
- on_setup do |id:, count:, output:, seed_waits:, **|
43
- ENV["SPECWRK_ID"] = id
43
+ on_setup do |count:, output:, seed_waits:, id: "specwrk-worker-#{SecureRandom.uuid[0, 8]}", **|
44
+ ENV["SPECWRK_ID"] ||= id # Unique default. Don't override the ENV value here
45
+
44
46
  ENV["SPECWRK_COUNT"] = count.to_s
45
47
  ENV["SPECWRK_SEED_WAITS"] = seed_waits.to_s
46
48
  ENV["SPECWRK_OUT"] = Pathname.new(output).expand_path(Dir.pwd).to_s
@@ -115,6 +117,8 @@ module Specwrk
115
117
 
116
118
  Client.wait_for_server!
117
119
  Client.new.seed(examples)
120
+ file_count = examples.group_by { |e| e[:file_path] }.keys.size
121
+ puts "🌱 Seeded #{examples.size} examples across #{file_count} files"
118
122
  rescue Errno::ECONNREFUSED
119
123
  puts "Server at #{ENV.fetch("SPECWRK_SRV_URI", "http://localhost:5138")} is refusing connections, exiting...#{ENV["SPECWRK_FLUSH_DELIMINATOR"]}"
120
124
  exit 1
@@ -11,7 +11,10 @@ require "rspec/core/formatters/console_codes"
11
11
  module Specwrk
12
12
  class CLIReporter
13
13
  def report
14
- return 1 unless Client.connect?
14
+ unless Client.connect?
15
+ puts colorizer.wrap("\nCannot connect to server to generate report. Assuming failure.", :red)
16
+ return 1
17
+ end
15
18
 
16
19
  puts "\nFinished in #{total_duration} " \
17
20
  "(total execution time of #{total_run_time})\n"
@@ -28,8 +31,11 @@ module Specwrk
28
31
  puts colorizer.wrap(totals_line, :green)
29
32
  0
30
33
  end
31
- rescue Specwrk::UnhandledResponseError
32
- puts colorizer.wrap("No examples run.", :red)
34
+ rescue Specwrk::UnhandledResponseError => e
35
+ puts colorizer.wrap("\nCannot report, #{e.message}.", :red)
36
+
37
+ client.shutdown
38
+
33
39
  1
34
40
  end
35
41
 
@@ -43,28 +49,28 @@ module Specwrk
43
49
  summary
44
50
  end
45
51
 
46
- def stats
47
- @stats ||= client.stats
52
+ def report_data
53
+ @report_data ||= client.report
48
54
  end
49
55
 
50
56
  def total_duration
51
- Time.parse(stats.dig(:completed, :meta, :last_finished_at)) - Time.parse(stats.dig(:completed, :meta, :first_started_at))
57
+ Time.parse(report_data.dig(:meta, :last_finished_at)) - Time.parse(report_data.dig(:meta, :first_started_at))
52
58
  end
53
59
 
54
60
  def total_run_time
55
- stats.dig(:completed, :meta, :total_run_time)
61
+ report_data.dig(:meta, :total_run_time)
56
62
  end
57
63
 
58
64
  def failure_count
59
- stats.dig(:completed, :meta, :failures)
65
+ report_data.dig(:meta, :failures)
60
66
  end
61
67
 
62
68
  def pending_count
63
- stats.dig(:completed, :meta, :pending)
69
+ report_data.dig(:meta, :pending)
64
70
  end
65
71
 
66
72
  def example_count
67
- stats.dig(:completed, :examples).length
73
+ report_data.dig(:examples).length
68
74
  end
69
75
 
70
76
  def client
@@ -62,8 +62,8 @@ module Specwrk
62
62
  response.code == "200"
63
63
  end
64
64
 
65
- def stats
66
- response = get "/stats"
65
+ def report
66
+ response = get "/report"
67
67
 
68
68
  if response.code == "200"
69
69
  JSON.parse(response.body, symbolize_names: true)
@@ -150,7 +150,9 @@ module Specwrk
150
150
 
151
151
  def default_headers
152
152
  @default_headers ||= {}.tap do |h|
153
+ h["User-Agent"] = "Specwrk/#{VERSION}"
153
154
  h["Authorization"] = "Bearer #{ENV["SPECWRK_SRV_KEY"]}" if ENV["SPECWRK_SRV_KEY"]
155
+ h["X-Specwrk-Id"] = ENV.fetch("SPECWRK_ID", "specwrk-client")
154
156
  h["X-Specwrk-Run"] = ENV["SPECWRK_RUN"] if ENV["SPECWRK_RUN"]
155
157
  h["Content-Type"] = "application/json"
156
158
  end
data/lib/specwrk/queue.rb CHANGED
@@ -6,7 +6,11 @@ require "json"
6
6
  module Specwrk
7
7
  # Thread-safe Hash access
8
8
  class Queue
9
+ attr_reader :created_at
10
+
9
11
  def initialize(hash = {})
12
+ @created_at = Time.now
13
+
10
14
  if block_given?
11
15
  @mutex = Monitor.new # Reentrant locking is required here
12
16
  # It's possible to enter the proc from two threads, so we need to ||= in case
@@ -40,8 +44,6 @@ module Specwrk
40
44
  end
41
45
 
42
46
  class PendingQueue < Queue
43
- attr_reader :previous_run_times
44
-
45
47
  def shift_bucket
46
48
  return bucket_by_file unless previous_run_times
47
49
 
@@ -59,17 +61,22 @@ module Specwrk
59
61
  previous_run_times.dig(:meta, :average_run_time)
60
62
  end
61
63
 
62
- # TODO: move reading the file to the getter method
63
- def previous_run_times_file=(path)
64
- return unless path
65
- return unless File.exist? path
64
+ def previous_run_times
65
+ return unless ENV["SPECWRK_OUT"]
66
66
 
67
- File.open(path, "r") do |file|
68
- file.flock(File::LOCK_EX)
67
+ @previous_run_times ||= begin
68
+ return unless previous_run_times_file_path
69
+ return unless File.exist? previous_run_times_file_path
69
70
 
70
- @previous_run_times = JSON.parse(file.read, symbolize_names: true)
71
+ raw_data = File.open(previous_run_times_file_path, "r") do |file|
72
+ file.flock(File::LOCK_SH)
73
+ file.read
74
+ end
71
75
 
72
- file.flock(File::LOCK_UN)
76
+ @previous_run_times = JSON.parse(raw_data, symbolize_names: true)
77
+ rescue JSON::ParserError => e
78
+ warn "#{e.inspect} in file #{previous_run_times_file_path}"
79
+ nil
73
80
  end
74
81
  end
75
82
 
@@ -84,6 +91,15 @@ module Specwrk
84
91
 
85
92
  private
86
93
 
94
+ # We want the most recently modified run time file
95
+ # report files are prefixed with a timestamp, and Dir.glob should order
96
+ # alphanumericly
97
+ def previous_run_times_file_path
98
+ return unless ENV["SPECWRK_OUT"]
99
+
100
+ @previous_run_times_file_path ||= Dir.glob(File.join(ENV["SPECWRK_OUT"], "*-report-*.json")).last
101
+ end
102
+
87
103
  # Take elements from the hash where the file_path is the same
88
104
  def bucket_by_file
89
105
  bucket = []
@@ -163,7 +179,7 @@ module Specwrk
163
179
  @hash.values.each { |example| calculate(example) }
164
180
 
165
181
  @output[:meta][:total_run_time] = @run_times.sum
166
- @output[:meta][:average_run_time] = @output[:meta][:total_run_time] / @run_times.length.to_f
182
+ @output[:meta][:average_run_time] = @output[:meta][:total_run_time] / [@run_times.length, 1].max.to_f
167
183
  @output[:meta][:first_started_at] = @first_started_at.iso8601(6)
168
184
  @output[:meta][:last_finished_at] = @last_finished_at.iso8601(6)
169
185
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Specwrk
4
- VERSION = "0.4.11"
4
+ VERSION = "0.6.0"
5
5
  end
@@ -13,6 +13,7 @@ rescue LoadError
13
13
  require "rack/handler/webrick"
14
14
  end
15
15
 
16
+ require "specwrk/web"
16
17
  require "specwrk/web/logger"
17
18
  require "specwrk/web/auth"
18
19
  require "specwrk/web/endpoints"
@@ -20,6 +21,8 @@ require "specwrk/web/endpoints"
20
21
  module Specwrk
21
22
  class Web
22
23
  class App
24
+ REAP_INTERVAL = 330 # HTTP connection timeout + some buffer
25
+
23
26
  class << self
24
27
  def run!
25
28
  Process.setproctitle "specwrk-server"
@@ -49,10 +52,8 @@ module Specwrk
49
52
 
50
53
  def setup!
51
54
  if ENV["SPECWRK_OUT"]
52
-
53
55
  FileUtils.mkdir_p(ENV["SPECWRK_OUT"])
54
56
  ENV["SPECWRK_SRV_LOG"] ||= Pathname.new(File.join(ENV["SPECWRK_OUT"], "server.log")).to_s unless ENV["SPECWRK_SRV_VERBOSE"]
55
- ENV["SPECWRK_SRV_OUTPUT"] ||= Pathname.new(File.join(ENV["SPECWRK_OUT"], "report.json")).expand_path(Dir.pwd).to_s
56
57
  end
57
58
 
58
59
  if ENV["SPECWRK_SRV_LOG"]
@@ -73,6 +74,10 @@ module Specwrk
73
74
  end
74
75
  end
75
76
 
77
+ def initialize
78
+ @reaper_thread = Thread.new { reaper } unless ENV["SPECWRK_SRV_SINGLE_RUN"]
79
+ end
80
+
76
81
  def call(env)
77
82
  env[:request] ||= Rack::Request.new(env)
78
83
 
@@ -93,14 +98,34 @@ module Specwrk
93
98
  Endpoints::Complete
94
99
  when ["POST", "/seed"]
95
100
  Endpoints::Seed
96
- when ["GET", "/stats"]
97
- Endpoints::Stats
101
+ when ["GET", "/report"]
102
+ Endpoints::Report
98
103
  when ["DELETE", "/shutdown"]
99
104
  Endpoints::Shutdown
100
105
  else
101
106
  Endpoints::NotFound
102
107
  end
103
108
  end
109
+
110
+ def reaper
111
+ until Specwrk.force_quit
112
+ sleep REAP_INTERVAL
113
+
114
+ reap
115
+ end
116
+ end
117
+
118
+ def reap
119
+ Web::WORKERS.each do |run, workers|
120
+ most_recent_last_seen_at = workers.map { |id, worker| worker[:last_seen_at] }.max
121
+ next unless most_recent_last_seen_at
122
+
123
+ # Don't consider runs which aren't at least REAP_INTERVAL sec stale
124
+ if most_recent_last_seen_at < Time.now - REAP_INTERVAL
125
+ Web.clear_run_queues(run)
126
+ end
127
+ end
128
+ end
104
129
  end
105
130
  end
106
131
  end
@@ -8,6 +8,9 @@ module Specwrk
8
8
  class Base
9
9
  def initialize(request)
10
10
  @request = request
11
+
12
+ worker[:first_seen_at] ||= Time.now
13
+ worker[:last_seen_at] = Time.now
11
14
  end
12
15
 
13
16
  def response
@@ -35,7 +38,7 @@ module Specwrk
35
38
  end
36
39
 
37
40
  def pending_queue
38
- Web::PENDING_QUEUES[request.get_header("HTTP_X_SPECWRK_RUN")]
41
+ Web::PENDING_QUEUES[run_id]
39
42
  end
40
43
 
41
44
  def processing_queue
@@ -45,6 +48,22 @@ module Specwrk
45
48
  def completed_queue
46
49
  Web::COMPLETED_QUEUES[request.get_header("HTTP_X_SPECWRK_RUN")]
47
50
  end
51
+
52
+ def workers
53
+ Web::WORKERS[request.get_header("HTTP_X_SPECWRK_RUN")]
54
+ end
55
+
56
+ def worker
57
+ workers[request.get_header("HTTP_X_SPECWRK_ID")]
58
+ end
59
+
60
+ def run_id
61
+ request.get_header("HTTP_X_SPECWRK_RUN")
62
+ end
63
+
64
+ def run_report_file_path
65
+ @run_report_file_path ||= File.join(ENV["SPECWRK_OUT"], "#{completed_queue.created_at.strftime("%Y%m%dT%H%M%S")}-report-#{run_id}.json").to_s
66
+ end
48
67
  end
49
68
 
50
69
  # Base default response is 404
@@ -87,8 +106,8 @@ module Specwrk
87
106
  end
88
107
  end
89
108
 
90
- if pending_queue.length.zero? && processing_queue.length.zero? && completed_queue.length.positive? && ENV["SPECWRK_SRV_OUTPUT"]
91
- completed_queue.dump_and_write(ENV["SPECWRK_SRV_OUTPUT"])
109
+ if pending_queue.length.zero? && processing_queue.length.zero? && completed_queue.length.positive? && ENV["SPECWRK_OUT"]
110
+ completed_queue.dump_and_write(run_report_file_path)
92
111
  end
93
112
 
94
113
  ok
@@ -117,43 +136,37 @@ module Specwrk
117
136
  end
118
137
  end
119
138
 
120
- class Stats < Base
139
+ class Report < Base
121
140
  def response
122
- data = {
123
- pending: {count: pending_queue.length},
124
- processing: {count: processing_queue.length},
125
- completed: completed_queue.dump
126
- }
127
-
128
- if data.dig(:completed, :examples).length.positive?
129
- [200, {"Content-Type" => "application/json"}, [JSON.generate(data)]]
141
+ if data
142
+ [200, {"Content-Type" => "application/json"}, [data]]
130
143
  else
131
- not_found
144
+ [404, {"Content-Type" => "text/plain"}, ["Unable to report on run #{run_id}; no file matching #{"*-report-#{run_id}.json"}"]]
132
145
  end
133
146
  end
134
- end
135
147
 
136
- class Shutdown < Base
137
- def response
138
- if ENV["SPECWRK_SRV_SINGLE_RUN"]
139
- interupt!
140
- elsif processing_queue.length.positive?
141
- # Push any processing jobs back into the pending queue
142
- processing_queue.synchronize do |processing_queue_hash|
143
- pending_queue.synchronize do |pending_queue_hash|
144
- processing_queue_hash.each do |id, example|
145
- pending_queue_hash[id] = example
146
- processing_queue_hash.delete(id)
147
- end
148
- end
149
- end
148
+ private
150
149
 
151
- elsif processing_queue.length.zero? && pending_queue.length.zero?
152
- # All done, we can clear the completed queue
153
- completed_queue.clear
150
+ def data
151
+ return @data if defined? @data
152
+
153
+ return unless most_recent_run_report_file
154
+ return unless File.exist?(most_recent_run_report_file)
155
+
156
+ @data = File.open(most_recent_run_report_file, "r") do |file|
157
+ file.flock(File::LOCK_SH)
158
+ file.read
154
159
  end
160
+ end
155
161
 
156
- # TODO: clear any zombie queues
162
+ def most_recent_run_report_file
163
+ @most_recent_run_report_file ||= Dir.glob(File.join(ENV["SPECWRK_OUT"], "*-report-#{run_id}.json")).last
164
+ end
165
+ end
166
+
167
+ class Shutdown < Base
168
+ def response
169
+ interupt! if ENV["SPECWRK_SRV_SINGLE_RUN"]
157
170
 
158
171
  [200, {"Content-Type" => "text/plain"}, ["✌️"]]
159
172
  end
data/lib/specwrk/web.rb CHANGED
@@ -4,12 +4,19 @@ require "specwrk/queue"
4
4
 
5
5
  module Specwrk
6
6
  class Web
7
- PENDING_QUEUES = Queue.new { |h, key| h[key] = PendingQueue.new.tap { |q| q.previous_run_times_file = ENV["SPECWRK_SRV_OUTPUT"] } }
7
+ PENDING_QUEUES = Queue.new { |h, key| h[key] = PendingQueue.new }
8
8
  PROCESSING_QUEUES = Queue.new { |h, key| h[key] = Queue.new }
9
9
  COMPLETED_QUEUES = Queue.new { |h, key| h[key] = CompletedQueue.new }
10
+ WORKERS = Hash.new { |h, key| h[key] = Hash.new { |h, key| h[key] = {} } }
10
11
 
11
12
  def self.clear_queues
12
- [PENDING_QUEUES, PROCESSING_QUEUES, COMPLETED_QUEUES].each(&:clear)
13
+ [PENDING_QUEUES, PROCESSING_QUEUES, COMPLETED_QUEUES, WORKERS].each(&:clear)
14
+ end
15
+
16
+ def self.clear_run_queues(run)
17
+ [PENDING_QUEUES, PROCESSING_QUEUES, COMPLETED_QUEUES, WORKERS].each do |queue|
18
+ queue.delete(run)
19
+ end
13
20
  end
14
21
  end
15
22
  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.4.11
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Westendorf