specwrk 0.10.2 → 0.12.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: 4570e3aaeb01c00647b8671803076848bf28dee5068ec0cde66c88effea85bbe
4
- data.tar.gz: 1d6709fc4b518ca5dcbad15e59820ba855adebb5dc6830ec26cc7f92d59f597d
3
+ metadata.gz: d642db71c542c4b2dbf17038db546a34f713ffccb010dc903e4e7a96c8071016
4
+ data.tar.gz: 00fd8258a2441acd31c4a484c2e039ad5e83028e8c10eef509cdca070daab1c8
5
5
  SHA512:
6
- metadata.gz: 3868e891fc3c14a8ce281b747d8e5b0c271e55f946f1fe1d23aa4d88d940ee40ea966fbf32bce5711ca1597c7870a8a4b1aa1bac610f117a9695888015eee6db
7
- data.tar.gz: 385faa8d85c751eca15e1203ab626ec8d9abe3584e79265550dc52b18c81327f6a837535827dd63d56bc5fb0671fe11c8bd91698a96dc0d9a82d36a23edc7430
6
+ metadata.gz: 6c88c00baa4d0455e8b928e276e6c66cf66fd881f7e9193a4e19f665a93d7b177b18e5f2cd00a8605ef4613e9607634791527898836f85f99bec0d3faf17e3d7
7
+ data.tar.gz: 7370d6c27e8b052a17bcc93d84fdbea18efab35792ec6f010cc21d05a45ed8aad4cc29e1c24e4b5e10aca65787f8fb08bc7bf94fa4e928293e41405a300259d6
data/README.md CHANGED
@@ -51,14 +51,15 @@ Options:
51
51
  --key=VALUE, -k VALUE # Authentication key clients must use for access. Overrides SPECWRK_SRV_KEY, default: ""
52
52
  --run=VALUE, -r VALUE # The run identifier for this job execution. Overrides SPECWRK_RUN, default: "main"
53
53
  --timeout=VALUE, -t VALUE # The amount of time to wait for the server to respond. Overrides SPECWRK_TIMEOUT, default: "5"
54
- --id=VALUE # The identifier for this worker. Default specwrk-worker(-COUNT_INDEX), default: "specwrk-worker"
54
+ --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
55
55
  --count=VALUE, -c VALUE # The number of worker processes you want to start, default: 1
56
56
  --output=VALUE, -o VALUE # Directory where worker output is stored. Overrides SPECWRK_OUT, default: ".specwrk/"
57
57
  --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"
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
+ --store-uri=VALUE # Directory where server state is stored. Required for multi-node or multi-process servers.
60
61
  --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-]verbose # Run in verbose mode. Default false., default: false
62
+ --[no-]verbose # Run in verbose mode, default: false
62
63
  --help, -h # Print this help
63
64
  ```
64
65
 
@@ -80,10 +81,11 @@ Options:
80
81
  --port=VALUE, -p VALUE # Server port. Overrides SPECWRK_SRV_PORT, default: "5138"
81
82
  --bind=VALUE, -b VALUE # Server bind address. Overrides SPECWRK_SRV_BIND, default: "127.0.0.1"
82
83
  --key=VALUE, -k VALUE # Authentication key clients must use for access. Overrides SPECWRK_SRV_KEY, default: ""
83
- --output=VALUE, -o VALUE # Directory where worker output is stored. Overrides SPECWRK_OUT, default: ".specwrk/"
84
+ --output=VALUE, -o VALUE # Directory where worker or server output is stored. Overrides SPECWRK_OUT, default: ".specwrk/"
85
+ --store-uri=VALUE # Directory where server state is stored. Required for multi-node or multi-process servers.
84
86
  --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"
85
- --[no-]verbose # Run in verbose mode. Default false., default: false
86
- --[no-]single-run # Act on shutdown requests from clients. Default: false., default: false
87
+ --[no-]verbose # Run in verbose mode, default: false
88
+ --[no-]single-run # Act on shutdown requests from clients, default: false
87
89
  --help, -h # Print this help
88
90
  ```
89
91
 
@@ -196,7 +198,7 @@ Start a persistent Queue Server given one of the following methods
196
198
 
197
199
  ### Configuring your Queue Server
198
200
  - Secure your server with a key either with the `SPECWRK_SRV_KEY` environment variable or `--key` CLI option
199
- - Configure the server output to be a persisted volume so your timings survive between restarts with the `SPECWRK_OUT` environment variable or `--out` CLI option
201
+ - 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).
200
202
 
201
203
  See [specwrk serve --help](#specwrk-serve) for all possible configuration options.
202
204
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- worker_processes 1
3
+ worker_processes ENV.fetch("PITCHFORK_WORKERS", "1").to_i
4
4
  listen "localhost:3000", backlog: 2048
5
- timeout 301
5
+ timeout ENV.fetch("IDLE_TIMEOUT", "300").to_i + 1
data/lib/specwrk/cli.rb CHANGED
@@ -74,13 +74,15 @@ module Specwrk
74
74
  base.unique_option :port, type: :integer, default: ENV.fetch("SPECWRK_SRV_PORT", "5138"), aliases: ["-p"], desc: "Server port. Overrides SPECWRK_SRV_PORT"
75
75
  base.unique_option :bind, type: :string, default: ENV.fetch("SPECWRK_SRV_BIND", "127.0.0.1"), aliases: ["-b"], desc: "Server bind address. Overrides SPECWRK_SRV_BIND"
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
- base.unique_option :output, type: :string, default: ENV.fetch("SPECWRK_OUT", ".specwrk/"), aliases: ["-o"], desc: "Directory where worker output is stored. Overrides SPECWRK_OUT"
77
+ base.unique_option :output, type: :string, default: ENV.fetch("SPECWRK_OUT", ".specwrk/"), aliases: ["-o"], desc: "Directory where worker or server output is stored. Overrides SPECWRK_OUT"
78
+ base.unique_option :store_uri, type: :string, desc: "Directory where server state is stored. Required for multi-node or multi-process servers."
78
79
  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 :verbose, type: :boolean, default: false, desc: "Run in verbose mode. Default false."
80
+ base.unique_option :verbose, type: :boolean, default: false, desc: "Run in verbose mode"
80
81
  end
81
82
 
82
- on_setup do |port:, bind:, output:, key:, group_by:, verbose:, **|
83
+ on_setup do |port:, bind:, output:, key:, group_by:, verbose:, **opts|
83
84
  ENV["SPECWRK_OUT"] = Pathname.new(output).expand_path(Dir.pwd).to_s
85
+ ENV["SPECWRK_SRV_STORE_URI"] = opts[:store_uri] if opts.key? :store_uri
84
86
  ENV["SPECWRK_SRV_VERBOSE"] = "1" if verbose
85
87
 
86
88
  ENV["SPECWRK_SRV_PORT"] = port
@@ -158,7 +160,7 @@ module Specwrk
158
160
  include Servable
159
161
 
160
162
  desc "Start a queue server"
161
- option :single_run, type: :boolean, default: false, desc: "Act on shutdown requests from clients. Default: false."
163
+ option :single_run, type: :boolean, default: false, desc: "Act on shutdown requests from clients"
162
164
 
163
165
  def call(single_run:, **args)
164
166
  ENV["SPECWRK_SRV_SINGLE_RUN"] = "1" if single_run
@@ -16,8 +16,8 @@ module Specwrk
16
16
  return 1
17
17
  end
18
18
 
19
- puts "\nFinished in #{total_duration} " \
20
- "(total execution time of #{total_run_time})\n"
19
+ puts "\nFinished in #{Specwrk.human_readable_duration total_duration} " \
20
+ "(total execution time of #{Specwrk.human_readable_duration total_run_time})\n"
21
21
 
22
22
  client.shutdown
23
23
 
@@ -44,7 +44,7 @@ module Specwrk
44
44
  raise Errno::ECONNREFUSED unless connected
45
45
  end
46
46
 
47
- attr_reader :last_request_at
47
+ attr_reader :last_request_at, :retry_count
48
48
 
49
49
  def initialize
50
50
  @mutex = Mutex.new
@@ -120,6 +120,16 @@ module Specwrk
120
120
  else
121
121
  raise UnhandledResponseError.new("#{response.code}: #{response.body}")
122
122
  end
123
+ rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout => e
124
+ @retry_count ||= 0
125
+ @retry_count += 1
126
+
127
+ raise e if @retry_count == 5
128
+
129
+ warn e
130
+ sleep @retry_count
131
+
132
+ retry
123
133
  end
124
134
 
125
135
  def seed(examples)
@@ -161,7 +171,7 @@ module Specwrk
161
171
  def make_request(request)
162
172
  @mutex.synchronize do
163
173
  @last_request_at = Time.now
164
- @http.request(request)
174
+ @http.request(request).tap { @retry_count = 0 }
165
175
  end
166
176
  end
167
177
 
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ require "specwrk/store"
6
+
7
+ module Specwrk
8
+ class Store
9
+ class BaseAdapter
10
+ class << self
11
+ def with_lock(_uri, _key)
12
+ yield
13
+ end
14
+ end
15
+
16
+ def initialize(uri, scope)
17
+ @uri = uri
18
+ @scope = scope
19
+ end
20
+
21
+ def [](key)
22
+ raise "Not implemented"
23
+ end
24
+
25
+ def []=(key, value)
26
+ raise "Not implemented"
27
+ end
28
+
29
+ def keys
30
+ raise "Not implemented"
31
+ end
32
+
33
+ def clear
34
+ raise "Not implemented"
35
+ end
36
+
37
+ def delete(*keys)
38
+ raise "Not implemented"
39
+ end
40
+
41
+ def merge!(h2)
42
+ raise "Not implemented"
43
+ end
44
+
45
+ def multi_read(*read_keys)
46
+ raise "Not implemented"
47
+ end
48
+
49
+ def multi_write(hash)
50
+ raise "Not implemented"
51
+ end
52
+
53
+ def empty?
54
+ raise "Not implemented"
55
+ end
56
+
57
+ private
58
+
59
+ attr_reader :uri, :scope
60
+ end
61
+ end
62
+ end
@@ -3,17 +3,31 @@
3
3
  require "json"
4
4
  require "base64"
5
5
 
6
- require "specwrk/store"
6
+ require "specwrk/store/base_adapter"
7
7
 
8
8
  module Specwrk
9
9
  class Store
10
- class FileAdapter
10
+ class FileAdapter < BaseAdapter
11
11
  EXT = ".wrk.json"
12
12
 
13
13
  @work_queue = Queue.new
14
14
  @threads = []
15
15
 
16
16
  class << self
17
+ def with_lock(uri, key)
18
+ lock_file_path = File.join(uri.path, "#{key}.lock").tap do |path|
19
+ FileUtils.mkdir_p(uri.path)
20
+ end
21
+
22
+ lock_file = File.open(lock_file_path, "a")
23
+
24
+ Thread.pass until lock_file.flock(File::LOCK_EX)
25
+
26
+ yield
27
+ ensure
28
+ lock_file.flock(File::LOCK_UN)
29
+ end
30
+
17
31
  def schedule_work(&blk)
18
32
  start_threads!
19
33
  @work_queue.push blk
@@ -32,11 +46,6 @@ module Specwrk
32
46
  end
33
47
  end
34
48
 
35
- def initialize(path)
36
- @path = path
37
- FileUtils.mkdir_p(@path)
38
- end
39
-
40
49
  def [](key)
41
50
  content = read(key.to_s)
42
51
  return unless content
@@ -51,7 +60,6 @@ module Specwrk
51
60
  else
52
61
  filename = filename_for_key(key_string)
53
62
  write(filename, JSON.generate(value))
54
- known_key_pairs[key_string] = filename
55
63
  end
56
64
  end
57
65
 
@@ -60,18 +68,14 @@ module Specwrk
60
68
  end
61
69
 
62
70
  def clear
63
- FileUtils.rm_rf(@path)
64
- FileUtils.mkdir_p(@path)
65
-
66
- @known_key_pairs = nil
71
+ FileUtils.rm_rf(path)
72
+ FileUtils.mkdir_p(path)
67
73
  end
68
74
 
69
75
  def delete(*keys)
70
- filenames = keys.map { |key| known_key_pairs[key] }.compact
76
+ filenames = keys.map { |key| filename_for_key key }.compact
71
77
 
72
78
  FileUtils.rm_f(filenames)
73
-
74
- keys.each { |key| known_key_pairs.delete(key) }
75
79
  end
76
80
 
77
81
  def merge!(h2)
@@ -79,8 +83,6 @@ module Specwrk
79
83
  end
80
84
 
81
85
  def multi_read(*read_keys)
82
- known_key_pairs # precache before each thread tries to look them up
83
-
84
86
  result_queue = Queue.new
85
87
 
86
88
  read_keys.each do |key|
@@ -103,8 +105,6 @@ module Specwrk
103
105
  end
104
106
 
105
107
  def multi_write(hash)
106
- known_key_pairs # precache before each thread tries to look them up
107
-
108
108
  result_queue = Queue.new
109
109
 
110
110
  hash_with_filenames = hash.map { |key, value| [key.to_s, [filename_for_key(key.to_s), value]] }.to_h
@@ -117,11 +117,10 @@ module Specwrk
117
117
  end
118
118
 
119
119
  Thread.pass until result_queue.length == hash.length
120
- hash_with_filenames.each { |key, (filename, _value)| known_key_pairs[key] = filename }
121
120
  end
122
121
 
123
122
  def empty?
124
- Dir.empty? @path
123
+ Dir.empty? path
125
124
  end
126
125
 
127
126
  private
@@ -136,27 +135,23 @@ module Specwrk
136
135
  end
137
136
 
138
137
  def read(key)
139
- File.read(known_key_pairs[key]) if known_key_pairs.key? key
138
+ filename = filename_for_key key
139
+ File.read(filename)
140
+ rescue Errno::ENOENT
141
+ nil
140
142
  end
141
143
 
142
144
  def filename_for_key(key)
143
145
  File.join(
144
- @path,
145
- [
146
- counter_prefix(key),
147
- encode_key(key)
148
- ].join("_")
146
+ path,
147
+ encode_key(key)
149
148
  ) + EXT
150
149
  end
151
150
 
152
- def counter_prefix(key)
153
- count = keys.index(key) || counter.tap { @counter += 1 }
154
-
155
- "%012d" % count
156
- end
157
-
158
- def counter
159
- @counter ||= keys.length
151
+ def path
152
+ @path ||= File.join(uri.path, scope).tap do |full_path|
153
+ FileUtils.mkdir_p(full_path)
154
+ end
160
155
  end
161
156
 
162
157
  def encode_key(key)
@@ -164,18 +159,18 @@ module Specwrk
164
159
  end
165
160
 
166
161
  def decode_key(key)
167
- encoded_key_part = File.basename(key).delete_suffix(EXT).split(/\A\d+_/).last
162
+ encoded_key_part = File.basename(key).delete_suffix(EXT)
168
163
  padding_count = (4 - encoded_key_part.length % 4) % 4
169
164
 
170
165
  Base64.urlsafe_decode64(encoded_key_part + ("=" * padding_count))
171
166
  end
172
167
 
173
168
  def known_key_pairs
174
- @known_key_pairs ||= Dir.entries(@path).sort.map do |filename|
169
+ Dir.entries(path).sort.map do |filename|
175
170
  next if filename.start_with? "."
176
171
  next unless filename.end_with? EXT
177
172
 
178
- file_path = File.join(@path, filename)
173
+ file_path = File.join(path, filename)
179
174
  [decode_key(filename), file_path]
180
175
  end.compact.to_h
181
176
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "specwrk/store/base_adapter"
4
+
5
+ module Specwrk
6
+ class Store
7
+ class MemoryAdapter < BaseAdapter
8
+ @@stores = Hash.new { |hash, key| hash[key] = {} }
9
+
10
+ class << self
11
+ def clear
12
+ @@stores.values.each(&:clear)
13
+ end
14
+ end
15
+
16
+ def [](key)
17
+ store[key]
18
+ end
19
+
20
+ def []=(key, value)
21
+ store[key] = value
22
+ end
23
+
24
+ def keys
25
+ store.keys
26
+ end
27
+
28
+ def clear
29
+ store.clear
30
+ end
31
+
32
+ def delete(*keys)
33
+ keys.each { |key| store.delete(key) }
34
+ end
35
+
36
+ def merge!(h2)
37
+ store.merge!(h2)
38
+ end
39
+
40
+ def multi_read(*read_keys)
41
+ store.slice(*read_keys)
42
+ end
43
+
44
+ def multi_write(hash)
45
+ merge!(hash)
46
+ end
47
+
48
+ def empty?
49
+ store.keys.length.zero?
50
+ end
51
+
52
+ private
53
+
54
+ def store
55
+ @store ||= @@stores[scope]
56
+ end
57
+ end
58
+ end
59
+ end
data/lib/specwrk/store.rb CHANGED
@@ -1,14 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "time"
4
- require "json"
5
-
6
- require "specwrk/store/file_adapter"
7
4
 
8
5
  module Specwrk
9
6
  class Store
10
- def initialize(path)
11
- @path = path
7
+ class << self
8
+ def with_lock(uri, key)
9
+ adapter_klass(uri).with_lock(uri, key) { yield }
10
+ end
11
+
12
+ def adapter_klass(uri)
13
+ case uri.scheme
14
+ when "memory"
15
+ require "specwrk/store/memory_adapter" unless defined?(MemoryAdapter)
16
+
17
+ MemoryAdapter
18
+ when "file"
19
+ require "specwrk/store/file_adapter" unless defined?(FileAdapter)
20
+
21
+ FileAdapter
22
+ end
23
+ end
24
+ end
25
+
26
+ def initialize(uri_string, scope)
27
+ @uri = URI(uri_string)
28
+ @scope = scope
12
29
  end
13
30
 
14
31
  def [](key)
@@ -72,17 +89,16 @@ module Specwrk
72
89
 
73
90
  private
74
91
 
75
- def adapter
76
- @adapter ||= FileAdapter.new(@path)
77
- end
92
+ attr_reader :uri, :scope
78
93
 
79
- def mutex
80
- @mutex ||= self.class.mutex_for(@path)
94
+ def adapter
95
+ @adapter ||= self.class.adapter_klass(uri).new uri, scope
81
96
  end
82
97
  end
83
98
 
84
99
  class PendingStore < Store
85
100
  RUN_TIME_BUCKET_MAXIMUM_KEY = :____run_time_bucket_maximum
101
+ ORDER_KEY = :____order
86
102
 
87
103
  def run_time_bucket_maximum=(val)
88
104
  @run_time_bucket_maximum = self[RUN_TIME_BUCKET_MAXIMUM_KEY] = val
@@ -92,6 +108,41 @@ module Specwrk
92
108
  @run_time_bucket_maximum ||= self[RUN_TIME_BUCKET_MAXIMUM_KEY]
93
109
  end
94
110
 
111
+ def order=(val)
112
+ @order = nil
113
+
114
+ self[ORDER_KEY] = if val.nil? || val.length.zero?
115
+ nil
116
+ else
117
+ val
118
+ end
119
+ end
120
+
121
+ def order
122
+ @order ||= self[ORDER_KEY] || []
123
+ end
124
+
125
+ def keys
126
+ return super if order.length.zero?
127
+
128
+ order
129
+ end
130
+
131
+ def merge!(hash)
132
+ super
133
+ self.order = hash.keys
134
+ end
135
+
136
+ def clear
137
+ @order = nil
138
+ super
139
+ end
140
+
141
+ def reload
142
+ @order = nil
143
+ super
144
+ end
145
+
95
146
  def shift_bucket
96
147
  return bucket_by_file unless run_time_bucket_maximum&.positive?
97
148
 
@@ -131,6 +182,7 @@ module Specwrk
131
182
  end
132
183
 
133
184
  delete(*consumed_keys)
185
+ self.order = order - consumed_keys
134
186
  bucket
135
187
  end
136
188
 
@@ -142,7 +194,7 @@ module Specwrk
142
194
  estimated_run_time_total = 0
143
195
 
144
196
  catch(:full) do
145
- keys.each_slice(25).each do |key_group|
197
+ keys.each_slice(24).each do |key_group|
146
198
  examples = multi_read(*key_group)
147
199
 
148
200
  examples.each do |key, example|
@@ -156,10 +208,30 @@ module Specwrk
156
208
  end
157
209
 
158
210
  delete(*consumed_keys)
211
+ self.order = order - consumed_keys
159
212
  bucket
160
213
  end
161
214
  end
162
215
 
216
+ class ProcessingStore < Store
217
+ def expired
218
+ @expired ||= begin
219
+ bucket = []
220
+
221
+ keys.each_slice(24).each do |key_group|
222
+ examples = multi_read(*key_group)
223
+ examples.each do |id, example|
224
+ next if example[:completion_threshold].nil?
225
+
226
+ bucket << [id, example] if example[:completion_threshold] < Time.now.to_i
227
+ end
228
+ end
229
+
230
+ bucket.to_h
231
+ end
232
+ end
233
+ end
234
+
163
235
  class CompletedStore < Store
164
236
  def dump
165
237
  @run_times = []
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Specwrk
4
- VERSION = "0.10.2"
4
+ VERSION = "0.12.0"
5
5
  end
@@ -11,7 +11,7 @@ module Specwrk
11
11
  end
12
12
 
13
13
  def call(env)
14
- env[:request] ||= Rack::Request.new(env)
14
+ @request = env[:request] ||= Rack::Request.new(env)
15
15
 
16
16
  return @app.call(env) if [nil, ""].include? ENV["SPECWRK_SRV_KEY"]
17
17
  return @app.call(env) if @excluded_paths.include? env[:request].path_info
@@ -28,7 +28,11 @@ module Specwrk
28
28
  private
29
29
 
30
30
  def unauthorized
31
- [401, {"content-type" => "application/json"}, ["Unauthorized"]]
31
+ if @request.head?
32
+ [401, {}, []]
33
+ else
34
+ [401, {"content-type" => "application/json"}, ["Unauthorized"]]
35
+ end
32
36
  end
33
37
  end
34
38
  end
@@ -51,11 +51,19 @@ 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
+ if request.head?
55
+ [404, {}, []]
56
+ else
57
+ [404, {"content-type" => "text/plain"}, ["This is not the path you're looking for, 'ol chap..."]]
58
+ end
55
59
  end
56
60
 
57
61
  def ok
58
- [200, {"content-type" => "text/plain"}, ["OK, 'ol chap"]]
62
+ if request.head?
63
+ [200, {}, []]
64
+ else
65
+ [200, {"content-type" => "text/plain"}, ["OK, 'ol chap"]]
66
+ end
59
67
  end
60
68
 
61
69
  def payload
@@ -71,52 +79,35 @@ module Specwrk
71
79
  end
72
80
 
73
81
  def pending
74
- @pending ||= PendingStore.new(File.join(datastore_path, "pending"))
82
+ @pending ||= PendingStore.new(ENV.fetch("SPECWRK_SRV_STORE_URI", "memory:///"), File.join(run_id, "pending"))
75
83
  end
76
84
 
77
85
  def processing
78
- @processing ||= Store.new(File.join(datastore_path, "processing"))
86
+ @processing ||= ProcessingStore.new(ENV.fetch("SPECWRK_SRV_STORE_URI", "memory:///"), File.join(run_id, "processing"))
79
87
  end
80
88
 
81
89
  def completed
82
- @completed ||= CompletedStore.new(File.join(datastore_path, "completed"))
90
+ @completed ||= CompletedStore.new(ENV.fetch("SPECWRK_SRV_STORE_URI", "memory:///"), File.join(run_id, "completed"))
83
91
  end
84
92
 
85
93
  def metadata
86
- @metadata ||= Store.new(File.join(datastore_path, "metadata"))
94
+ @metadata ||= Store.new(ENV.fetch("SPECWRK_SRV_STORE_URI", "memory:///"), File.join(run_id, "metadata"))
87
95
  end
88
96
 
89
97
  def run_times
90
- @run_times ||= Store.new(File.join(ENV["SPECWRK_OUT"], "run_times"))
98
+ @run_times ||= Store.new(ENV.fetch("SPECWRK_SRV_STORE_URI", "file://#{File.join(Dir.tmpdir, "specwrk")}"), "run_times")
91
99
  end
92
100
 
93
101
  def worker
94
- @worker ||= Store.new(File.join(datastore_path, "workers", request.get_header("HTTP_X_SPECWRK_ID").to_s))
102
+ @worker ||= Store.new(ENV.fetch("SPECWRK_SRV_STORE_URI", "memory:///"), File.join(run_id, "workers", request.get_header("HTTP_X_SPECWRK_ID").to_s))
95
103
  end
96
104
 
97
105
  def run_id
98
106
  request.get_header("HTTP_X_SPECWRK_RUN")
99
107
  end
100
108
 
101
- def run_report_file_path
102
- @run_report_file_path ||= File.join(datastore_path, "#{started_at.strftime("%Y%m%dT%H%M%S")}-report.json").to_s
103
- end
104
-
105
- def datastore_path
106
- @datastore_path ||= File.join(ENV["SPECWRK_OUT"], run_id).to_s.tap do |path|
107
- FileUtils.mkdir_p(path) unless File.directory?(path)
108
- end
109
- end
110
-
111
109
  def with_lock
112
- Thread.pass until lock_file.flock(File::LOCK_EX)
113
- yield
114
- ensure
115
- lock_file.flock(File::LOCK_UN)
116
- end
117
-
118
- def lock_file
119
- @lock_file ||= File.open(File.join(datastore_path, "lock"), "a")
110
+ Store.with_lock(URI(ENV.fetch("SPECWRK_SRV_STORE_URI", "memory:///")), "server") { yield }
120
111
  end
121
112
  end
122
113
 
@@ -125,7 +116,7 @@ module Specwrk
125
116
 
126
117
  class Health < Base
127
118
  def with_response
128
- [200, {}, []]
119
+ ok
129
120
  end
130
121
  end
131
122
 
@@ -222,54 +213,63 @@ module Specwrk
222
213
  end
223
214
  end
224
215
 
225
- class Pop < Base
226
- def with_response
227
- @examples = pending.shift_bucket
228
-
229
- processing_data = @examples.map { |example| [example[:id], example] }.to_h
230
- processing.merge!(processing_data)
216
+ class Popable < Base
217
+ private
231
218
 
232
- if @examples.any?
233
- [200, {"content-type" => "application/json"}, [JSON.generate(@examples)]]
219
+ def with_pop_response
220
+ if examples.any?
221
+ [200, {"content-type" => "application/json"}, [JSON.generate(examples)]]
234
222
  elsif pending.empty? && processing.empty? && completed.empty?
235
223
  [204, {"content-type" => "text/plain"}, ["Waiting for sample to be seeded."]]
236
224
  elsif completed.any? && processing.empty?
237
225
  [410, {"content-type" => "text/plain"}, ["That's a good lad. Run along now and go home."]]
226
+ elsif processing.any? && processing.expired.keys.any?
227
+ pending.merge!(processing.expired)
228
+ processing.delete(*processing.expired.keys)
229
+ @examples = nil
230
+
231
+ [200, {"content-type" => "application/json"}, [JSON.generate(examples)]]
238
232
  else
239
233
  not_found
240
234
  end
241
235
  end
242
- end
243
236
 
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)
237
+ def examples
238
+ @examples ||= begin
239
+ examples = pending.shift_bucket
240
+ maximum_completion_threshold = (Time.now + ((pending.run_time_bucket_maximum || 30) * 2)).to_i
249
241
 
250
- @examples = pending.shift_bucket
242
+ processing_data = examples.map do |example|
243
+ example_run_time_completion_threshold = (Time.now + example[:expected_run_time].to_f * 2).to_i
251
244
 
252
- processing_data = @examples.map { |example| [example[:id], example] }.to_h
253
- processing.merge!(processing_data)
245
+ [
246
+ example[:id], example.merge(completion_threshold: [maximum_completion_threshold, example_run_time_completion_threshold].compact.max)
247
+ ]
248
+ end
254
249
 
255
- if @examples.any?
256
- [200, {"content-type" => "application/json"}, [JSON.generate(@examples)]]
257
- elsif pending.empty? && processing.empty? && completed.empty?
258
- [204, {"content-type" => "text/plain"}, ["Waiting for sample to be seeded."]]
259
- elsif completed.any? && processing.empty?
260
- [410, {"content-type" => "text/plain"}, ["That's a good lad. Run along now and go home."]]
261
- else
262
- not_found
250
+ processing.merge!(processing_data.to_h)
251
+
252
+ examples
263
253
  end
264
254
  end
255
+ end
265
256
 
266
- private
257
+ class Pop < Popable
258
+ def with_response
259
+ with_pop_response
260
+ end
261
+ end
267
262
 
268
- def before_lock
269
- completed_examples
270
- run_time_data
263
+ class CompleteAndPop < Popable
264
+ def with_response
265
+ completed.merge!(completed_examples)
266
+ processing.delete(*completed_examples.keys)
267
+
268
+ with_pop_response
271
269
  end
272
270
 
271
+ private
272
+
273
273
  def completed_examples
274
274
  @completed_data ||= payload.map { |example| [example[:id], example] if processing[example[:id]] }.compact.to_h
275
275
  end
@@ -277,8 +277,7 @@ module Specwrk
277
277
  # We don't care about exact values here, just approximate run times are fine
278
278
  # So if we overwrite run times from another process it is nbd
279
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
280
+ run_times.merge! run_time_data
282
281
  end
283
282
 
284
283
  def run_time_data
@@ -40,6 +40,7 @@ module Specwrk
40
40
  completion_formatter.examples.clear
41
41
 
42
42
  RSpec.clear_examples
43
+ RSpec.configuration.backtrace_formatter.filter_gem "specwrk"
43
44
 
44
45
  # see https://github.com/rspec/rspec-core/pull/2723
45
46
  if Gem::Version.new(RSpec::Core::Version::STRING) <= Gem::Version.new("3.9.1")
data/lib/specwrk.rb CHANGED
@@ -38,5 +38,21 @@ module Specwrk
38
38
 
39
39
  exited_pids
40
40
  end
41
+
42
+ def human_readable_duration(total_seconds, precision: 2)
43
+ secs = total_seconds.to_f
44
+ hours = (secs / 3600).to_i
45
+ mins = ((secs % 3600) / 60).to_i
46
+ seconds = secs % 60
47
+
48
+ parts = []
49
+ parts << "#{hours}h" if hours.positive?
50
+ parts << "#{mins}m" if mins.positive?
51
+ if seconds.positive?
52
+ sec_str = format("%0.#{precision}f", seconds).sub(/\.?0+$/, "")
53
+ parts << "#{sec_str}s"
54
+ end
55
+ parts.join(" ")
56
+ end
41
57
  end
42
58
  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.10.2
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Westendorf
@@ -192,7 +192,9 @@ files:
192
192
  - lib/specwrk/hookable.rb
193
193
  - lib/specwrk/list_examples.rb
194
194
  - lib/specwrk/store.rb
195
+ - lib/specwrk/store/base_adapter.rb
195
196
  - lib/specwrk/store/file_adapter.rb
197
+ - lib/specwrk/store/memory_adapter.rb
196
198
  - lib/specwrk/version.rb
197
199
  - lib/specwrk/web.rb
198
200
  - lib/specwrk/web/app.rb