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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9da02a0ce1009b7c0fc4c03cd33af10e5e2984b706ef46aca34dd8f33c593267
4
- data.tar.gz: 94563ca34708d9d7563f36291eb781ab6f48654a09a0fe7d7912d04aae008a5b
3
+ metadata.gz: 944469a1460443f84117dcdacbe79ec133c7605e83cc2b2882faaf573661a458
4
+ data.tar.gz: cc120e2ad04cf8baabf001921f16a03c67f15dd81884a6df83d097c06af420ae
5
5
  SHA512:
6
- metadata.gz: 9ac813bab61e13d630fde64a875e57eb973fd74c6a59d48772ab1ec2d5a688a99899b70e4874c9fcdd71348f88a4b668a97955fdea156c667968fe2ca63523fe
7
- data.tar.gz: d7af5aa56a6ffd45043834440c144f8baebc7c7b59a6fc1d80e93dddc959d20f3db3b6cb66cd183e3f055898abbcd09ec9864266ed91f8df361efae15f70671e
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
- ## Planned Features
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
- * Use heuristics to run your slowest tests first
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
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
- lib = File.expand_path('../../lib', __FILE__)
3
- $:.unshift lib unless $:.include? lib
2
+ lib = File.expand_path('../lib', __dir__)
3
+ $LOAD_PATH.unshift lib unless $LOAD_PATH.include? lib
4
4
 
5
5
  require 'flatware'
6
6
  Flatware::CLI.start
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
@@ -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
- formatters.each do |formatter|
11
- formatter.send name, *args if formatter.respond_to? name
12
- end
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/pids'
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 ||= ProcessorInfo.count
11
+ @processors ||= Etc.nprocessors
9
12
  end
10
13
 
11
14
  def self.worker_option
12
- method_option :workers, aliases: '-w', type: :numeric, default: processors, desc: 'Number of concurent processes to run'
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 :log, aliases: '-l', type: :boolean, desc: 'Print debug messages to $stderr'
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 - [$PROCESS_ID]).each do |pid|
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 jobs: jobs, formatter: Flatware::Broadcaster.new([formatter]), sink: options['sink-endpoint'], dispatch: options['dispatch-endpoint'], worker_count: workers
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
- begin
63
- require flatware_gem
64
- rescue LoadError
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
@@ -4,7 +4,19 @@ module Flatware
4
4
  attr_writer :failed
5
5
 
6
6
  def failed?
7
- !!@failed
7
+ @failed == true
8
+ end
9
+
10
+ def failed!
11
+ @failed = true
12
+ end
13
+
14
+ def sentinel?
15
+ id == 'seppuku'
16
+ end
17
+
18
+ def self.sentinel
19
+ new 'seppuku'
8
20
  end
9
21
  end
10
22
  end
@@ -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
- def initialize(klass, message, backtrace, cause='')
6
- @class, @message, @backtrace, @cause = serialized(klass), message, backtrace, cause
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 exception.class, exception.message, exception.backtrace, exception.cause
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); @name = name end
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 'flatware'
1
+ require 'drb/drb'
2
+
2
3
  module Flatware
3
4
  module Sink
4
- extend self
5
+ module_function
6
+
7
+ def start_server(**args)
8
+ Server.new(**args).start
9
+ end
5
10
 
6
- def start_server(*args)
7
- Server.new(*args).start
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 :sink, :dispatch, :poller, :workers, :checkpoints, :jobs, :formatter
20
+ attr_reader :checkpoints, :completed_jobs, :formatter, :jobs, :queue, :sink, :workers
12
21
 
13
- def initialize(jobs:, formatter:, dispatch:, sink:, worker_count: 0)
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
- @sink = Flatware.socket(ZMQ::PULL, bind: sink)
17
- @dispatch = Flatware.socket(ZMQ::REP, bind: dispatch)
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
- trap 'INT' do
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
- listen.tap do
36
- Flatware.close
37
- end
35
+ DRb.start_service(sink, self, verbose: Flatware.verbose?)
36
+ DRb.thread.join
37
+ !failures?
38
38
  end
39
39
 
40
- def listen
41
- que = jobs.dup
42
- poller.each do |socket|
43
- message, content = socket.recv
44
-
45
- case message
46
- when :ready
47
- workers << content
48
- job = que.shift
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
- formatter.summarize(checkpoints)
66
- !failures?
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 failures?
72
- checkpoints.any?(&:failures?) || completed_jobs.any?(&:failed?)
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 summarize_remaining
76
- return if remaining_work.empty?
77
- formatter.summarize_remaining remaining_work
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 log(*args)
81
- Flatware.log *args
94
+ def interruped?
95
+ signals = Thread.main[:signals]
96
+ signals && !signals.empty?
82
97
  end
83
98
 
84
- def completed_jobs
85
- @completed_jobs ||= []
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 done?
89
- remaining_work.empty?
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
- jobs.group_by.with_index do |_,i|
99
- i % worker_count
100
- end.values.map do |jobs|
101
- Job.new(jobs.map(&:id).flatten, jobs.first.args)
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
@@ -1,15 +1,17 @@
1
- require 'flatware/socket'
1
+ require 'drb/drb'
2
+
2
3
  module Flatware
3
4
  module Sink
4
- extend self
5
+ module_function
6
+
5
7
  attr_accessor :client
6
8
 
7
9
  class Client
8
10
  def initialize(sink_endpoint)
9
- @socket = Flatware.socket ZMQ::PUSH, connect: sink_endpoint
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
- @socket.send message
23
+ @sink.public_send message
22
24
  end
23
25
  end
24
26
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Flatware
4
- VERSION = '1.2.0'
4
+ VERSION = '2.0.0.rc1'
5
5
  end
@@ -1,57 +1,76 @@
1
- require 'flatware/sink/client'
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, dispatch_endpoint, sink_endpoint)
11
+ def initialize(id, runner, sink_endpoint)
7
12
  @id = id
8
13
  @runner = runner
9
- @sink = Sink::Client.new sink_endpoint
10
- @task = Flatware.socket ZMQ::REQ, connect: dispatch_endpoint
14
+ @sink = DRbObject.new_with_uri sink_endpoint
15
+ Flatware::Sink.client = @sink
11
16
  end
12
17
 
13
- def self.spawn(count:, runner:, dispatch:, sink:)
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
- new(i, runner, dispatch, sink).listen
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
- trap 'INT' do
25
- Flatware.close!
26
- @want_to_quit = true
27
- exit(1)
28
- end
29
-
30
- Sink.client = sink
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
- attr_reader :task, :sink, :runner
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 report_for_duty
54
- task.send [:ready, id]
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: 1.2.0
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: 2019-08-23 00:00:00.000000000 Z
11
+ date: 2021-03-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: ffi-rzmq
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/pids.rb
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: '2.1'
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: '0'
70
+ version: 1.3.1
112
71
  requirements: []
113
- rubygems_version: 3.0.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
@@ -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
@@ -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