flatware 1.2.0 → 2.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +28 -48
- data/bin/flatware +2 -2
- data/lib/flatware.rb +32 -3
- data/lib/flatware/broadcaster.rb +20 -3
- data/lib/flatware/cli.rb +28 -16
- data/lib/flatware/configuration.rb +40 -0
- data/lib/flatware/job.rb +13 -1
- data/lib/flatware/pid.rb +41 -0
- data/lib/flatware/serialized_exception.rb +16 -4
- data/lib/flatware/sink.rb +97 -65
- data/lib/flatware/sink/client.rb +7 -5
- data/lib/flatware/version.rb +1 -1
- data/lib/flatware/worker.rb +50 -31
- metadata +18 -59
- data/lib/flatware/pids.rb +0 -25
- data/lib/flatware/poller.rb +0 -35
- data/lib/flatware/processor_info.rb +0 -24
- data/lib/flatware/socket.rb +0 -176
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 944469a1460443f84117dcdacbe79ec133c7605e83cc2b2882faaf573661a458
|
4
|
+
data.tar.gz: cc120e2ad04cf8baabf001921f16a03c67f15dd81884a6df83d097c06af420ae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 80d9ae698fd2f8c9c01c78e09d65fa36a1989b6bbdbee62cac5b74c5d69003734025cec6862f89415ca1e79ced1fbce97a17d37b31238f617b5d4fc5e160e74f
|
7
|
+
data.tar.gz: 35a22fe1de1df1a761ef16114729d10e51ddd797c3a7590293d49465f9ab9e86a77e22572d89abb4b603e56093da0d2ec8b1dccccd4c7cbc666339aa693d987d
|
data/README.md
CHANGED
@@ -7,39 +7,6 @@
|
|
7
7
|
|
8
8
|
Flatware parallelizes your test suite to significantly reduce test time.
|
9
9
|
|
10
|
-
## Requirements
|
11
|
-
|
12
|
-
* ZeroMQ > 4.0
|
13
|
-
|
14
|
-
## Installation
|
15
|
-
|
16
|
-
### ZeroMQ
|
17
|
-
|
18
|
-
#### Linux Ubuntu
|
19
|
-
|
20
|
-
```sh
|
21
|
-
sudo apt-get install -qq libzmq3-dev
|
22
|
-
```
|
23
|
-
|
24
|
-
(Never you mind the 3. This package contains ZMQ version 4.)
|
25
|
-
|
26
|
-
#### Mac OSX
|
27
|
-
|
28
|
-
|
29
|
-
If you're on macOS 10.12, use the custom hashrocket ZMQ homebrew formula.
|
30
|
-
|
31
|
-
```sh
|
32
|
-
brew tap hashrocket/formulas
|
33
|
-
brew install hashrocket/formulas/zeromq
|
34
|
-
```
|
35
|
-
|
36
|
-
The stock homebrew version will likely work on older versions of macOS.
|
37
|
-
|
38
|
-
|
39
|
-
```sh
|
40
|
-
brew install zeromq
|
41
|
-
```
|
42
|
-
|
43
10
|
### Flatware
|
44
11
|
|
45
12
|
Add the runners you need to your Gemfile:
|
@@ -81,8 +48,9 @@ For this to work the configuration option must be loaded before any specs are ru
|
|
81
48
|
|
82
49
|
--require spec_helper
|
83
50
|
|
84
|
-
But beware, if you're using ActiveRecord in your suite you'll need to avoid doing things that cause it to establish a database connection in `spec_helper.rb`. If ActiveRecord connects before flatware forks off workers, each will die messily. All of this will just work if you're following [the recomended pattern of splitting your helpers into `spec_helper` and `rails_helper`](https://github.com/rspec/rspec-rails/blob/v3.8.2/lib/generators/rspec/install/templates/spec/rails_helper.rb).
|
85
|
-
|
51
|
+
But beware, if you're using ActiveRecord in your suite you'll need to avoid doing things that cause it to establish a database connection in `spec_helper.rb`. If ActiveRecord connects before flatware forks off workers, each will die messily. All of this will just work if you're following [the recomended pattern of splitting your helpers into `spec_helper` and `rails_helper`](https://github.com/rspec/rspec-rails/blob/v3.8.2/lib/generators/rspec/install/templates/spec/rails_helper.rb). Another option is to use [the configurable hooks](
|
52
|
+
#faster-startup-with-activerecord
|
53
|
+
).
|
86
54
|
|
87
55
|
### Options
|
88
56
|
|
@@ -134,9 +102,31 @@ Now you are ready to rock:
|
|
134
102
|
$ flatware rspec && flatware cucumber
|
135
103
|
```
|
136
104
|
|
137
|
-
|
105
|
+
### Faster Startup With ActiveRecord
|
106
|
+
|
107
|
+
Flatware has a couple lifecycle callbacks that you can use to avoid booting your app
|
108
|
+
over again on every core. One way to take advantage of this via a `spec/flatware_helper.rb` file like so:
|
138
109
|
|
139
|
-
|
110
|
+
```ruby
|
111
|
+
Flatware.configure do |conf|
|
112
|
+
conf.before_fork do
|
113
|
+
require 'rails_helper'
|
114
|
+
|
115
|
+
ActiveRecord::Base.connection.disconnect!
|
116
|
+
end
|
117
|
+
|
118
|
+
conf.after_fork do |test_env_number|
|
119
|
+
config = ActiveRecord::Base.connection_config
|
120
|
+
|
121
|
+
ActiveRecord::Base.establish_connection(
|
122
|
+
config.merge(
|
123
|
+
database: config.fetch(:database) + test_env_number.to_s
|
124
|
+
)
|
125
|
+
)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
```
|
129
|
+
Now when I run `bundle exec flatware rspec -r ./spec/flatware_helper` My app only boots once, rather than once per core.
|
140
130
|
|
141
131
|
## Design Goals
|
142
132
|
|
@@ -167,20 +157,10 @@ directory. CD there and `flatware` will be in your path so you can tinker away.
|
|
167
157
|
|
168
158
|
## How it works
|
169
159
|
|
170
|
-
Flatware relies on a message passing system to enable concurrency.
|
171
|
-
The main process declares a worker for each cpu in the computer. Each
|
172
|
-
worker forks from the main process and is then assigned a portion of the
|
173
|
-
test suite. As the worker runs the test suite it sends progress
|
174
|
-
messages to the main process. These messages are collected and when
|
175
|
-
the last worker is finished the main process provides a report on the
|
176
|
-
collected progress messages.
|
160
|
+
Flatware relies on a message passing system to enable concurrency. The main process forks a worker for each cpu in the computer. These workers are each given a chunk of the tests to run. The workers report back to the main process about their progress. The main process prints those progress messages. When the last worker is finished the main process prints the results.
|
177
161
|
|
178
162
|
## Resources
|
179
163
|
|
180
|
-
To learn more about the messaging system that Flatware uses, take a look at the
|
181
|
-
[excellent ZeroMQ guide][z].
|
182
|
-
|
183
|
-
[z]: http://zguide.zeromq.org/page:all
|
184
164
|
[a]: https://github.com/cucumber/aruba
|
185
165
|
|
186
166
|
## Contributing to Flatware
|
data/bin/flatware
CHANGED
data/lib/flatware.rb
CHANGED
@@ -1,10 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
1
5
|
module Flatware
|
2
|
-
require 'flatware/processor_info'
|
3
6
|
require 'flatware/job'
|
4
7
|
require 'flatware/cli'
|
5
|
-
require 'flatware/poller'
|
6
8
|
require 'flatware/sink'
|
7
|
-
require 'flatware/socket'
|
8
9
|
require 'flatware/worker'
|
9
10
|
require 'flatware/broadcaster'
|
11
|
+
|
12
|
+
module_function
|
13
|
+
|
14
|
+
def logger
|
15
|
+
@logger ||= Logger.new($stderr, level: :fatal)
|
16
|
+
end
|
17
|
+
|
18
|
+
def logger=(logger)
|
19
|
+
@logger = logger
|
20
|
+
end
|
21
|
+
|
22
|
+
def log(*message)
|
23
|
+
case message.first
|
24
|
+
when Exception
|
25
|
+
logger.error message.first
|
26
|
+
else
|
27
|
+
logger.info(([$PROGRAM_NAME] + message).join(' '))
|
28
|
+
end
|
29
|
+
message
|
30
|
+
end
|
31
|
+
|
32
|
+
def verbose=(bool)
|
33
|
+
logger.level = bool ? :debug : :fatal
|
34
|
+
end
|
35
|
+
|
36
|
+
def verbose?
|
37
|
+
logger.level < Logger::SEV_LABEL.index('FATAL')
|
38
|
+
end
|
10
39
|
end
|
data/lib/flatware/broadcaster.rb
CHANGED
@@ -1,5 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Flatware
|
4
|
+
# sends messages to all formatters
|
2
5
|
class Broadcaster
|
6
|
+
FORMATTER_MESSAGES = %i[
|
7
|
+
jobs
|
8
|
+
started
|
9
|
+
progress
|
10
|
+
finished
|
11
|
+
summarize
|
12
|
+
summarize_remaining
|
13
|
+
].freeze
|
14
|
+
|
3
15
|
attr_reader :formatters
|
4
16
|
|
5
17
|
def initialize(formatters)
|
@@ -7,9 +19,14 @@ module Flatware
|
|
7
19
|
end
|
8
20
|
|
9
21
|
def method_missing(name, *args)
|
10
|
-
|
11
|
-
|
12
|
-
|
22
|
+
return super unless FORMATTER_MESSAGES.include? name
|
23
|
+
|
24
|
+
formatters.select { |formatter| formatter.respond_to? name }
|
25
|
+
.each { |formatter| formatter.send name, *args }
|
26
|
+
end
|
27
|
+
|
28
|
+
def respond_to_missing?(name, _include_all)
|
29
|
+
FORMATTER_MESSAGES.include? name
|
13
30
|
end
|
14
31
|
end
|
15
32
|
end
|
data/lib/flatware/cli.rb
CHANGED
@@ -1,18 +1,32 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'thor'
|
4
|
-
require 'flatware/
|
4
|
+
require 'flatware/pid'
|
5
|
+
require 'etc'
|
6
|
+
|
5
7
|
module Flatware
|
8
|
+
# shared flatware cli
|
6
9
|
class CLI < Thor
|
7
10
|
def self.processors
|
8
|
-
@processors ||=
|
11
|
+
@processors ||= Etc.nprocessors
|
9
12
|
end
|
10
13
|
|
11
14
|
def self.worker_option
|
12
|
-
method_option
|
15
|
+
method_option(
|
16
|
+
:workers,
|
17
|
+
aliases: '-w',
|
18
|
+
type: :numeric,
|
19
|
+
default: processors,
|
20
|
+
desc: 'Number of concurent processes to run'
|
21
|
+
)
|
13
22
|
end
|
14
23
|
|
15
|
-
class_option
|
24
|
+
class_option(
|
25
|
+
:log,
|
26
|
+
aliases: '-l',
|
27
|
+
type: :boolean,
|
28
|
+
desc: 'Print debug messages to $stderr'
|
29
|
+
)
|
16
30
|
|
17
31
|
worker_option
|
18
32
|
desc 'fan [COMMAND]', 'executes the given job on all of the workers'
|
@@ -32,7 +46,7 @@ module Flatware
|
|
32
46
|
|
33
47
|
desc 'clear', 'kills all flatware processes'
|
34
48
|
def clear
|
35
|
-
(Flatware.pids - [
|
49
|
+
(Flatware.pids - [Process.pid]).each do |pid|
|
36
50
|
Process.kill 6, pid
|
37
51
|
end
|
38
52
|
end
|
@@ -42,14 +56,15 @@ module Flatware
|
|
42
56
|
def start_sink(jobs:, workers:, formatter:)
|
43
57
|
$0 = 'flatware sink'
|
44
58
|
Process.setpgrp
|
45
|
-
passed = Sink.start_server
|
59
|
+
passed = Sink.start_server(
|
60
|
+
jobs: jobs,
|
61
|
+
formatter: Flatware::Broadcaster.new([formatter]),
|
62
|
+
sink: options['sink-endpoint'],
|
63
|
+
worker_count: workers
|
64
|
+
)
|
46
65
|
exit passed ? 0 : 1
|
47
66
|
end
|
48
67
|
|
49
|
-
def log(*args)
|
50
|
-
Flatware.log(*args)
|
51
|
-
end
|
52
|
-
|
53
68
|
def workers
|
54
69
|
options[:workers]
|
55
70
|
end
|
@@ -59,11 +74,9 @@ end
|
|
59
74
|
flatware_gems = %w[flatware-rspec flatware-cucumber]
|
60
75
|
|
61
76
|
loaded_flatware_gem_count = flatware_gems.map do |flatware_gem|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
nil
|
66
|
-
end
|
77
|
+
require flatware_gem
|
78
|
+
rescue LoadError
|
79
|
+
nil
|
67
80
|
end.compact.size
|
68
81
|
|
69
82
|
if loaded_flatware_gem_count.zero?
|
@@ -72,5 +85,4 @@ if loaded_flatware_gem_count.zero?
|
|
72
85
|
The flatware gem is a dependency of flatware runners for rspec and cucumber.
|
73
86
|
Install %<gem_list>s for more usefull commands.
|
74
87
|
MESSAGE
|
75
|
-
|
76
88
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Flatware
|
4
|
+
class Configuration
|
5
|
+
def initialize
|
6
|
+
reset!
|
7
|
+
end
|
8
|
+
|
9
|
+
def before_fork(&block)
|
10
|
+
if block_given?
|
11
|
+
@before_fork = block
|
12
|
+
else
|
13
|
+
@before_fork
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def after_fork(&block)
|
18
|
+
if block_given?
|
19
|
+
@after_fork = block
|
20
|
+
else
|
21
|
+
@after_fork
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def reset!
|
26
|
+
@before_fork = -> {}
|
27
|
+
@after_fork = ->(_) {}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module_function
|
32
|
+
|
33
|
+
def configuration
|
34
|
+
@configuration ||= Configuration.new
|
35
|
+
end
|
36
|
+
|
37
|
+
def configure(&_block)
|
38
|
+
yield configuration
|
39
|
+
end
|
40
|
+
end
|
data/lib/flatware/job.rb
CHANGED
data/lib/flatware/pid.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'etc'
|
4
|
+
|
5
|
+
module Flatware
|
6
|
+
module_function
|
7
|
+
|
8
|
+
# All the pids of all the processes called flatware on this machine
|
9
|
+
def pids
|
10
|
+
Pid.pids { |pid| pid.command =~ /flatware/ }
|
11
|
+
end
|
12
|
+
|
13
|
+
def pids_of_group(group_pid)
|
14
|
+
Pid.pids { |pid| pid.pgid == group_pid }
|
15
|
+
end
|
16
|
+
|
17
|
+
Pid = Struct.new(:pid, :pgid, :ppid, :command) do
|
18
|
+
def self.pids(&block)
|
19
|
+
ps.select(&block).map(&:pid)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.ps
|
23
|
+
args = ['-o', members.join(',')]
|
24
|
+
args += { 'Darwin' => %w[-c] }.fetch(Etc.uname.fetch(:sysname), [])
|
25
|
+
|
26
|
+
IO
|
27
|
+
.popen(['ps', *args])
|
28
|
+
.readlines
|
29
|
+
.map do |row|
|
30
|
+
fields = row.strip.split(/\s+/, 4)
|
31
|
+
new(*fields.take(3).map(&:to_i), fields.last)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def pids_of_group(group_pid)
|
36
|
+
ps
|
37
|
+
.select { |pid| pid.pgid == group_pid }
|
38
|
+
.map(&:pid)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -2,15 +2,25 @@ module Flatware
|
|
2
2
|
class SerializedException
|
3
3
|
attr_reader :class, :message, :cause
|
4
4
|
attr_accessor :backtrace
|
5
|
-
|
6
|
-
|
5
|
+
|
6
|
+
def initialize(klass, message, backtrace, cause = '')
|
7
|
+
@class = serialized(klass)
|
8
|
+
@message = message
|
9
|
+
@backtrace = backtrace
|
10
|
+
@cause = cause
|
7
11
|
end
|
8
12
|
|
9
13
|
def self.from(exception)
|
10
|
-
new
|
14
|
+
new(
|
15
|
+
exception.class,
|
16
|
+
exception.message,
|
17
|
+
exception.backtrace,
|
18
|
+
exception.cause
|
19
|
+
)
|
11
20
|
end
|
12
21
|
|
13
22
|
private
|
23
|
+
|
14
24
|
def serialized(klass)
|
15
25
|
SerializedClass.new(klass.to_s)
|
16
26
|
end
|
@@ -19,6 +29,8 @@ module Flatware
|
|
19
29
|
class SerializedClass
|
20
30
|
attr_reader :name
|
21
31
|
alias to_s name
|
22
|
-
def initialize(name)
|
32
|
+
def initialize(name)
|
33
|
+
@name = name
|
34
|
+
end
|
23
35
|
end
|
24
36
|
end
|
data/lib/flatware/sink.rb
CHANGED
@@ -1,104 +1,136 @@
|
|
1
|
-
require '
|
1
|
+
require 'drb/drb'
|
2
|
+
|
2
3
|
module Flatware
|
3
4
|
module Sink
|
4
|
-
|
5
|
+
module_function
|
6
|
+
|
7
|
+
def start_server(**args)
|
8
|
+
Server.new(**args).start
|
9
|
+
end
|
5
10
|
|
6
|
-
def
|
7
|
-
|
11
|
+
def client
|
12
|
+
@client
|
13
|
+
end
|
14
|
+
|
15
|
+
def client=(client)
|
16
|
+
@client = client
|
8
17
|
end
|
9
18
|
|
10
19
|
class Server
|
11
|
-
attr_reader :
|
20
|
+
attr_reader :checkpoints, :completed_jobs, :formatter, :jobs, :queue, :sink, :workers
|
12
21
|
|
13
|
-
def initialize(jobs:, formatter:,
|
22
|
+
def initialize(jobs:, formatter:, sink:, worker_count: 0, **)
|
23
|
+
@checkpoints = []
|
24
|
+
@completed_jobs = []
|
14
25
|
@formatter = formatter
|
15
|
-
@jobs = group_jobs(jobs, worker_count)
|
16
|
-
@
|
17
|
-
@
|
18
|
-
@poller = Poller.new(@sink, @dispatch)
|
26
|
+
@jobs = group_jobs(jobs, worker_count).freeze
|
27
|
+
@queue = @jobs.dup
|
28
|
+
@sink = sink
|
19
29
|
@workers = Set.new(worker_count.times.to_a)
|
20
|
-
@checkpoints = []
|
21
30
|
end
|
22
31
|
|
23
32
|
def start
|
24
|
-
|
25
|
-
puts "Interrupted!"
|
26
|
-
formatter.summarize checkpoints
|
27
|
-
summarize_remaining
|
28
|
-
puts "\n\nCleaning up. Please wait...\n"
|
29
|
-
Flatware.close!
|
30
|
-
Process.waitall
|
31
|
-
puts "thanks."
|
32
|
-
exit 1
|
33
|
-
end
|
33
|
+
trap_interrupt
|
34
34
|
formatter.jobs jobs
|
35
|
-
|
36
|
-
|
37
|
-
|
35
|
+
DRb.start_service(sink, self, verbose: Flatware.verbose?)
|
36
|
+
DRb.thread.join
|
37
|
+
!failures?
|
38
38
|
end
|
39
39
|
|
40
|
-
def
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
if job and not done?
|
50
|
-
dispatch.send job
|
51
|
-
else
|
52
|
-
workers.delete content
|
53
|
-
dispatch.send 'seppuku'
|
54
|
-
end
|
55
|
-
when :checkpoint
|
56
|
-
checkpoints << content
|
57
|
-
when :finished
|
58
|
-
completed_jobs << content
|
59
|
-
formatter.finished content
|
60
|
-
else
|
61
|
-
formatter.send message, content
|
62
|
-
end
|
63
|
-
break if workers.empty? and done?
|
40
|
+
def ready(worker)
|
41
|
+
job = queue.shift
|
42
|
+
if job && !(remaining_work.empty? || interruped?)
|
43
|
+
workers << worker
|
44
|
+
job
|
45
|
+
else
|
46
|
+
workers.delete worker
|
47
|
+
check_finished!
|
48
|
+
Job.sentinel
|
64
49
|
end
|
65
|
-
|
66
|
-
|
50
|
+
end
|
51
|
+
|
52
|
+
def checkpoint(checkpoint)
|
53
|
+
checkpoints << checkpoint
|
54
|
+
end
|
55
|
+
|
56
|
+
def finished(job)
|
57
|
+
completed_jobs << job
|
58
|
+
formatter.finished(job)
|
59
|
+
check_finished!
|
60
|
+
end
|
61
|
+
|
62
|
+
def method_missing(name, *args)
|
63
|
+
super unless formatter.respond_to?(name)
|
64
|
+
Flatware.log(name, *args)
|
65
|
+
formatter.send(name, *args)
|
66
|
+
end
|
67
|
+
|
68
|
+
def respond_to_missing?(name, include_all)
|
69
|
+
formatter.respond_to?(name, include_all)
|
67
70
|
end
|
68
71
|
|
69
72
|
private
|
70
73
|
|
71
|
-
def
|
72
|
-
|
74
|
+
def trap_interrupt
|
75
|
+
Thread.main[:signals] = Queue.new
|
76
|
+
|
77
|
+
Thread.new(&method(:handle_interrupt))
|
78
|
+
|
79
|
+
trap 'INT' do
|
80
|
+
Thread.main[:signals] << :int
|
81
|
+
end
|
73
82
|
end
|
74
83
|
|
75
|
-
def
|
76
|
-
|
77
|
-
|
84
|
+
def handle_interrupt
|
85
|
+
Thread.main[:signals].pop
|
86
|
+
puts 'Interrupted!'
|
87
|
+
summarize_remaining
|
88
|
+
puts "\n\nCleaning up. Please wait...\n"
|
89
|
+
Process.waitall
|
90
|
+
puts 'done.'
|
91
|
+
abort
|
78
92
|
end
|
79
93
|
|
80
|
-
def
|
81
|
-
|
94
|
+
def interruped?
|
95
|
+
signals = Thread.main[:signals]
|
96
|
+
signals && !signals.empty?
|
82
97
|
end
|
83
98
|
|
84
|
-
def
|
85
|
-
|
99
|
+
def check_finished!
|
100
|
+
return unless [workers, remaining_work].all?(&:empty?)
|
101
|
+
|
102
|
+
DRb.stop_service
|
103
|
+
summarize
|
86
104
|
end
|
87
105
|
|
88
|
-
def
|
89
|
-
|
106
|
+
def failures?
|
107
|
+
checkpoints.any?(&:failures?) || completed_jobs.any?(&:failed?)
|
108
|
+
end
|
109
|
+
|
110
|
+
def summarize_remaining
|
111
|
+
summarize
|
112
|
+
return if remaining_work.empty?
|
113
|
+
|
114
|
+
formatter.summarize_remaining remaining_work
|
90
115
|
end
|
91
116
|
|
92
117
|
def remaining_work
|
93
118
|
jobs - completed_jobs
|
94
119
|
end
|
95
120
|
|
121
|
+
def summarize
|
122
|
+
formatter.summarize(checkpoints)
|
123
|
+
end
|
124
|
+
|
96
125
|
def group_jobs(jobs, worker_count)
|
97
126
|
return jobs unless worker_count > 1
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
127
|
+
|
128
|
+
jobs
|
129
|
+
.group_by
|
130
|
+
.with_index { |_, i| i % worker_count }
|
131
|
+
.values
|
132
|
+
.map do |job_group|
|
133
|
+
Job.new(job_group.map(&:id).flatten, jobs.first.args)
|
102
134
|
end
|
103
135
|
end
|
104
136
|
end
|
data/lib/flatware/sink/client.rb
CHANGED
@@ -1,15 +1,17 @@
|
|
1
|
-
require '
|
1
|
+
require 'drb/drb'
|
2
|
+
|
2
3
|
module Flatware
|
3
4
|
module Sink
|
4
|
-
|
5
|
+
module_function
|
6
|
+
|
5
7
|
attr_accessor :client
|
6
8
|
|
7
9
|
class Client
|
8
10
|
def initialize(sink_endpoint)
|
9
|
-
@
|
11
|
+
@sink = DRbObject.new_with_uri sink_endpoint
|
10
12
|
end
|
11
13
|
|
12
|
-
%w[finished started progress checkpoint].each do |message|
|
14
|
+
%w[ready finished started progress checkpoint].each do |message|
|
13
15
|
define_method message do |content|
|
14
16
|
push [message.to_sym, content]
|
15
17
|
end
|
@@ -18,7 +20,7 @@ module Flatware
|
|
18
20
|
private
|
19
21
|
|
20
22
|
def push(message)
|
21
|
-
@
|
23
|
+
@sink.public_send message
|
22
24
|
end
|
23
25
|
end
|
24
26
|
end
|
data/lib/flatware/version.rb
CHANGED
data/lib/flatware/worker.rb
CHANGED
@@ -1,57 +1,76 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'drb/drb'
|
4
|
+
|
2
5
|
module Flatware
|
6
|
+
require 'flatware/configuration'
|
7
|
+
# executes tests and sends results to the sink
|
3
8
|
class Worker
|
4
|
-
attr_reader :id
|
9
|
+
attr_reader :sink, :runner, :id
|
5
10
|
|
6
|
-
def initialize(id, runner,
|
11
|
+
def initialize(id, runner, sink_endpoint)
|
7
12
|
@id = id
|
8
13
|
@runner = runner
|
9
|
-
@sink =
|
10
|
-
|
14
|
+
@sink = DRbObject.new_with_uri sink_endpoint
|
15
|
+
Flatware::Sink.client = @sink
|
11
16
|
end
|
12
17
|
|
13
|
-
def self.spawn(count:, runner:,
|
18
|
+
def self.spawn(count:, runner:, sink:, **)
|
19
|
+
Flatware.configuration.before_fork.call
|
14
20
|
count.times do |i|
|
15
21
|
fork do
|
16
22
|
$0 = "flatware worker #{i}"
|
17
23
|
ENV['TEST_ENV_NUMBER'] = i.to_s
|
18
|
-
|
24
|
+
Flatware.configuration.after_fork.call(i)
|
25
|
+
new(i, runner, sink).listen
|
19
26
|
end
|
20
27
|
end
|
21
28
|
end
|
22
29
|
|
23
30
|
def listen
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
report_for_duty
|
32
|
-
loop do
|
33
|
-
job = task.recv
|
34
|
-
break if job == 'seppuku' or @want_to_quit
|
35
|
-
job.worker = id
|
36
|
-
sink.started job
|
37
|
-
begin
|
38
|
-
runner.run job.id, job.args
|
39
|
-
rescue => e
|
40
|
-
Flatware.log e
|
41
|
-
job.failed = true
|
31
|
+
retrying(times: 10, wait: 0.1) do
|
32
|
+
job = sink.ready id
|
33
|
+
until want_to_quit? || job.sentinel?
|
34
|
+
job.worker = id
|
35
|
+
sink.started job
|
36
|
+
run job
|
37
|
+
job = sink.ready id
|
42
38
|
end
|
43
|
-
sink.finished job
|
44
|
-
report_for_duty
|
45
39
|
end
|
46
|
-
Flatware.close unless @want_to_quit
|
47
40
|
end
|
48
41
|
|
49
42
|
private
|
50
43
|
|
51
|
-
|
44
|
+
def run(job)
|
45
|
+
runner.run job.id, job.args
|
46
|
+
sink.finished job
|
47
|
+
rescue Interrupt
|
48
|
+
want_to_quit!
|
49
|
+
rescue StandardError => e
|
50
|
+
Flatware.log e
|
51
|
+
job.failed!
|
52
|
+
sink.finished job
|
53
|
+
end
|
52
54
|
|
53
|
-
def
|
54
|
-
|
55
|
+
def want_to_quit!
|
56
|
+
@want_to_quit = true
|
57
|
+
end
|
58
|
+
|
59
|
+
def want_to_quit?
|
60
|
+
@want_to_quit == true
|
61
|
+
end
|
62
|
+
|
63
|
+
def retrying(times:, wait:)
|
64
|
+
tries = 0
|
65
|
+
begin
|
66
|
+
yield unless want_to_quit?
|
67
|
+
rescue DRb::DRbConnError => e
|
68
|
+
raise if (tries += 1) >= times
|
69
|
+
|
70
|
+
sleep wait
|
71
|
+
Flatware.log('retrying', e.message)
|
72
|
+
retry
|
73
|
+
end
|
55
74
|
end
|
56
75
|
end
|
57
76
|
end
|
metadata
CHANGED
@@ -1,71 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: flatware
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0.rc1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brian Dunn
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-03-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: thor
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - "<"
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '2.0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - "<"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '2.0'
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: thor
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - "~>"
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '0.13'
|
34
|
-
type: :runtime
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - "~>"
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '0.13'
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: aruba
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - "~>"
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '0.14'
|
48
|
-
type: :development
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - "~>"
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: '0.14'
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: rake
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - "~>"
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: 10.1.0
|
62
|
-
type: :development
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - "~>"
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: 10.1.0
|
69
27
|
description: A distributed rspec and cucumber runner
|
70
28
|
email: brian@hashrocket.com
|
71
29
|
executables:
|
@@ -81,37 +39,38 @@ files:
|
|
81
39
|
- lib/flatware.rb
|
82
40
|
- lib/flatware/broadcaster.rb
|
83
41
|
- lib/flatware/cli.rb
|
42
|
+
- lib/flatware/configuration.rb
|
84
43
|
- lib/flatware/job.rb
|
85
|
-
- lib/flatware/
|
86
|
-
- lib/flatware/poller.rb
|
87
|
-
- lib/flatware/processor_info.rb
|
44
|
+
- lib/flatware/pid.rb
|
88
45
|
- lib/flatware/serialized_exception.rb
|
89
46
|
- lib/flatware/sink.rb
|
90
47
|
- lib/flatware/sink/client.rb
|
91
|
-
- lib/flatware/socket.rb
|
92
48
|
- lib/flatware/version.rb
|
93
49
|
- lib/flatware/worker.rb
|
94
50
|
homepage: http://github.com/briandunn/flatware
|
95
51
|
licenses:
|
96
52
|
- MIT
|
97
53
|
metadata: {}
|
98
|
-
post_install_message:
|
54
|
+
post_install_message:
|
99
55
|
rdoc_options: []
|
100
56
|
require_paths:
|
101
57
|
- lib
|
102
58
|
required_ruby_version: !ruby/object:Gem::Requirement
|
103
59
|
requirements:
|
104
|
-
- - "
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '2.6'
|
63
|
+
- - "<"
|
105
64
|
- !ruby/object:Gem::Version
|
106
|
-
version: '
|
65
|
+
version: '3.1'
|
107
66
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
108
67
|
requirements:
|
109
|
-
- - "
|
68
|
+
- - ">"
|
110
69
|
- !ruby/object:Gem::Version
|
111
|
-
version:
|
70
|
+
version: 1.3.1
|
112
71
|
requirements: []
|
113
|
-
rubygems_version: 3.
|
114
|
-
signing_key:
|
72
|
+
rubygems_version: 3.2.3
|
73
|
+
signing_key:
|
115
74
|
specification_version: 4
|
116
75
|
summary: A distributed rspec and cucumber runner
|
117
76
|
test_files: []
|
data/lib/flatware/pids.rb
DELETED
@@ -1,25 +0,0 @@
|
|
1
|
-
require 'flatware/processor_info'
|
2
|
-
module Flatware
|
3
|
-
extend self
|
4
|
-
# All the pids of all the processes called flatware on this machine
|
5
|
-
def pids
|
6
|
-
pids_command.map do |row|
|
7
|
-
row =~ /(\d+).*flatware/ and $1.to_i
|
8
|
-
end.compact
|
9
|
-
end
|
10
|
-
|
11
|
-
def pids_command
|
12
|
-
case ProcessorInfo.operating_system
|
13
|
-
when 'Darwin'
|
14
|
-
`ps -c -opid,pgid,command`
|
15
|
-
when 'Linux'
|
16
|
-
`ps -opid,pgid,command`
|
17
|
-
end.split("\n")[1..-1]
|
18
|
-
end
|
19
|
-
|
20
|
-
def pids_of_group(group_pid)
|
21
|
-
pids_command.map(&:split).map do |pid, pgid, _|
|
22
|
-
pid.to_i if pgid.to_i == group_pid
|
23
|
-
end.compact
|
24
|
-
end
|
25
|
-
end
|
data/lib/flatware/poller.rb
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
module Flatware
|
2
|
-
class Poller
|
3
|
-
attr_reader :sockets, :zmq_poller
|
4
|
-
def initialize(*sockets)
|
5
|
-
@sockets = sockets
|
6
|
-
@zmq_poller = ZMQ::Poller.new
|
7
|
-
register_sockets
|
8
|
-
end
|
9
|
-
|
10
|
-
def each(&block)
|
11
|
-
while (result = zmq_poller.poll) != 0
|
12
|
-
raise Error, ZMQ::Util.error_string, caller if result == -1
|
13
|
-
for socket in zmq_poller.readables.map &find_wrapped_socket
|
14
|
-
yield socket
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
private
|
20
|
-
|
21
|
-
def find_wrapped_socket
|
22
|
-
->(s) do
|
23
|
-
sockets.find do |socket|
|
24
|
-
socket.socket == s
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
def register_sockets
|
30
|
-
sockets.each do |socket|
|
31
|
-
zmq_poller.register_readable socket.socket
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
@@ -1,24 +0,0 @@
|
|
1
|
-
module Flatware
|
2
|
-
class ProcessorInfo
|
3
|
-
def count
|
4
|
-
case operating_system
|
5
|
-
when 'Darwin'
|
6
|
-
`hostinfo`.match(/^(?<processors>\d+) processors are logically available\.$/)[:processors].to_i
|
7
|
-
when 'Linux'
|
8
|
-
`grep --count '^processor' /proc/cpuinfo`.to_i
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
12
|
-
def operating_system
|
13
|
-
`uname`.chomp
|
14
|
-
end
|
15
|
-
|
16
|
-
def self.count
|
17
|
-
new.count
|
18
|
-
end
|
19
|
-
|
20
|
-
def self.operating_system
|
21
|
-
new.operating_system
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
data/lib/flatware/socket.rb
DELETED
@@ -1,176 +0,0 @@
|
|
1
|
-
require 'ffi-rzmq'
|
2
|
-
require 'securerandom'
|
3
|
-
require 'logger'
|
4
|
-
|
5
|
-
module Flatware
|
6
|
-
Error = Class.new StandardError
|
7
|
-
|
8
|
-
extend self
|
9
|
-
|
10
|
-
def logger
|
11
|
-
@logger ||= Logger.new($stderr)
|
12
|
-
end
|
13
|
-
|
14
|
-
def logger=(logger)
|
15
|
-
@logger = logger
|
16
|
-
end
|
17
|
-
|
18
|
-
def socket(*args)
|
19
|
-
context.socket(*args)
|
20
|
-
end
|
21
|
-
|
22
|
-
def close(force: false)
|
23
|
-
@ignore_errors = true if force
|
24
|
-
context.close(force: force)
|
25
|
-
@context = nil
|
26
|
-
end
|
27
|
-
|
28
|
-
def close!
|
29
|
-
close force: true
|
30
|
-
end
|
31
|
-
|
32
|
-
def socket_error(name=nil)
|
33
|
-
raise(Error, [$0,name,ZMQ::Util.error_string].compact.join(' - '), caller) unless @ignore_errors
|
34
|
-
end
|
35
|
-
|
36
|
-
def log(*message)
|
37
|
-
if Exception === message.first
|
38
|
-
logger.error message.first
|
39
|
-
elsif verbose?
|
40
|
-
logger.info ([$0] + message).join(' ')
|
41
|
-
end
|
42
|
-
message
|
43
|
-
end
|
44
|
-
|
45
|
-
attr_writer :verbose
|
46
|
-
def verbose?
|
47
|
-
!!@verbose
|
48
|
-
end
|
49
|
-
|
50
|
-
def context
|
51
|
-
@context ||= begin
|
52
|
-
@ignore_errors = nil
|
53
|
-
Context.new
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
class Context
|
58
|
-
attr_reader :sockets, :c
|
59
|
-
|
60
|
-
def initialize
|
61
|
-
@c = ZMQ::Context.new
|
62
|
-
@sockets = []
|
63
|
-
end
|
64
|
-
|
65
|
-
def socket(zmq_type, options={})
|
66
|
-
Socket.new(c.socket(zmq_type)).tap do |socket|
|
67
|
-
sockets.push socket
|
68
|
-
if port = options[:connect]
|
69
|
-
socket.connect port
|
70
|
-
end
|
71
|
-
if port = options[:bind]
|
72
|
-
socket.bind port
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
def close(force: false)
|
78
|
-
sockets.each do |socket|
|
79
|
-
socket.setsockopt ZMQ::LINGER, 0
|
80
|
-
end if force
|
81
|
-
sockets.each(&:close)
|
82
|
-
Flatware::socket_error unless LibZMQ.zmq_term(c.context) == 0
|
83
|
-
Flatware.log "terminated context"
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
class Socket
|
88
|
-
attr_reader :socket
|
89
|
-
|
90
|
-
def initialize(socket)
|
91
|
-
@socket = socket
|
92
|
-
end
|
93
|
-
|
94
|
-
def setsockopt(*args)
|
95
|
-
socket.setsockopt(*args)
|
96
|
-
end
|
97
|
-
|
98
|
-
def name
|
99
|
-
socket.name
|
100
|
-
end
|
101
|
-
|
102
|
-
def send(message)
|
103
|
-
result = socket.send_string(Marshal.dump(message))
|
104
|
-
error if result == -1
|
105
|
-
Flatware.log "#@type #@port send #{message}"
|
106
|
-
message
|
107
|
-
end
|
108
|
-
|
109
|
-
def connect(port)
|
110
|
-
@type = 'connected'
|
111
|
-
@port = port
|
112
|
-
error unless socket.connect(port) == 0
|
113
|
-
Flatware.log "connect #@port"
|
114
|
-
end
|
115
|
-
|
116
|
-
def monitor
|
117
|
-
name = "inproc://monitor#{SecureRandom.hex(10)}"
|
118
|
-
LibZMQ.zmq_socket_monitor(socket.socket, name, ZMQ::EVENT_ALL)
|
119
|
-
Monitor.new(name)
|
120
|
-
end
|
121
|
-
|
122
|
-
class Monitor
|
123
|
-
def initialize(port)
|
124
|
-
@socket = Flatware.socket ZMQ::PAIR
|
125
|
-
@socket.connect port
|
126
|
-
end
|
127
|
-
|
128
|
-
def recv
|
129
|
-
bytes = @socket.recv marshal: false
|
130
|
-
data = LibZMQ::EventData.new FFI::MemoryPointer.from_string bytes
|
131
|
-
event[data.event]
|
132
|
-
end
|
133
|
-
|
134
|
-
private
|
135
|
-
|
136
|
-
def event
|
137
|
-
ZMQ.constants.select do |c|
|
138
|
-
c.to_s =~ /^EVENT/
|
139
|
-
end.map do |s|
|
140
|
-
{s => ZMQ.const_get(s)}
|
141
|
-
end.reduce(:merge).invert
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
|
-
def bind(port)
|
146
|
-
@type = 'bound'
|
147
|
-
@port = port
|
148
|
-
error unless socket.bind(port) == 0
|
149
|
-
Flatware.log "bind #@port"
|
150
|
-
end
|
151
|
-
|
152
|
-
def close
|
153
|
-
error unless socket.close == 0
|
154
|
-
Flatware.log "close #@type #@port"
|
155
|
-
end
|
156
|
-
|
157
|
-
def error
|
158
|
-
Flatware::socket_error name
|
159
|
-
end
|
160
|
-
|
161
|
-
def recv(block: true, marshal: true)
|
162
|
-
message = ''
|
163
|
-
if block
|
164
|
-
result = socket.recv_string(message)
|
165
|
-
error if result == -1
|
166
|
-
else
|
167
|
-
socket.recv_string(message, ZMQ::NOBLOCK)
|
168
|
-
end
|
169
|
-
if message != '' and marshal
|
170
|
-
message = Marshal.load(message)
|
171
|
-
end
|
172
|
-
Flatware.log "#@type #@port recv #{message}"
|
173
|
-
message
|
174
|
-
end
|
175
|
-
end
|
176
|
-
end
|