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 +4 -4
- data/README.md +8 -6
- data/docker/pitchfork.conf +2 -2
- data/lib/specwrk/cli.rb +6 -4
- data/lib/specwrk/cli_reporter.rb +2 -2
- data/lib/specwrk/client.rb +12 -2
- data/lib/specwrk/store/base_adapter.rb +62 -0
- data/lib/specwrk/store/file_adapter.rb +33 -38
- data/lib/specwrk/store/memory_adapter.rb +59 -0
- data/lib/specwrk/store.rb +83 -11
- data/lib/specwrk/version.rb +1 -1
- data/lib/specwrk/web/auth.rb +6 -2
- data/lib/specwrk/web/endpoints.rb +57 -58
- data/lib/specwrk/worker/executor.rb +1 -0
- data/lib/specwrk.rb +16 -0
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d642db71c542c4b2dbf17038db546a34f713ffccb010dc903e4e7a96c8071016
|
4
|
+
data.tar.gz: 00fd8258a2441acd31c4a484c2e039ad5e83028e8c10eef509cdca070daab1c8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
|
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
|
86
|
-
--[no-]single-run # Act on shutdown requests from clients
|
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
|
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
|
|
data/docker/pitchfork.conf
CHANGED
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
|
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
|
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
|
data/lib/specwrk/cli_reporter.rb
CHANGED
@@ -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
|
|
data/lib/specwrk/client.rb
CHANGED
@@ -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(
|
64
|
-
FileUtils.mkdir_p(
|
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|
|
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?
|
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
|
-
|
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
|
-
|
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
|
153
|
-
|
154
|
-
|
155
|
-
|
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)
|
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
|
-
|
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(
|
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
|
-
|
11
|
-
|
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
|
-
|
76
|
-
@adapter ||= FileAdapter.new(@path)
|
77
|
-
end
|
92
|
+
attr_reader :uri, :scope
|
78
93
|
|
79
|
-
def
|
80
|
-
@
|
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(
|
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 = []
|
data/lib/specwrk/version.rb
CHANGED
data/lib/specwrk/web/auth.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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(
|
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 ||=
|
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(
|
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(
|
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(
|
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(
|
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
|
-
|
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
|
-
|
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
|
226
|
-
|
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
|
-
|
233
|
-
|
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
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
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
|
-
|
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
|
-
|
253
|
-
|
245
|
+
[
|
246
|
+
example[:id], example.merge(completion_threshold: [maximum_completion_threshold, example_run_time_completion_threshold].compact.max)
|
247
|
+
]
|
248
|
+
end
|
254
249
|
|
255
|
-
|
256
|
-
|
257
|
-
|
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
|
-
|
257
|
+
class Pop < Popable
|
258
|
+
def with_response
|
259
|
+
with_pop_response
|
260
|
+
end
|
261
|
+
end
|
267
262
|
|
268
|
-
|
269
|
-
|
270
|
-
|
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
|
-
|
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.
|
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
|