sisyphus 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 1a96cdf599669e601888f70c06de7ae280b86fc6
4
- data.tar.gz: 601d5544f93601e4b9aac5df9dad122040742300
2
+ SHA256:
3
+ metadata.gz: 5b0d7aeac8761a528b7d084dcf254c284d541f1aabf3c728e35e5f25233fa8fe
4
+ data.tar.gz: 2358bceac26513eeab3f712138bbcd98cfda0fa9e3f3d38a1cdbbabacb762487
5
5
  SHA512:
6
- metadata.gz: 731a337d88e4ca319a824c2fdb44f2343808adfd1a15762bfb6a2417666c7aca8182b2859bf495c33f4c48c170091061b36f18fb0088281258288fe677a5d55a
7
- data.tar.gz: e4c2bcbfb26349558d25d705fb69f370cc05e4040f8f46bc4430794b448b1e621bcece6988b68318a2ee6dba128594001cd42f2212bf52ac9ec79125e9704afc
6
+ metadata.gz: 66317b7e08c7c1de9125e1300811e808641fc76dc29230bcdff93ec66e1a1a1f8841b4565cdbd54709517501f283b81dbcb5e1ba4ceaeefe5f5ccfa7566437bd
7
+ data.tar.gz: cc2fb6929498e3c6ab44eddbd6aa7439cfef3357fdff42e11998f25941c2c44a522b6a7375800fa35068ddd2964bf2869e0f81b01cdafc08ed2546088299f327
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sisyphus (0.2.2)
4
+ sisyphus (0.2.3)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -32,6 +32,10 @@ Getting started
32
32
  master or workers encounter. The logger should quack like a
33
33
  `Logger` instance from the Ruby stdlib. Exceptions are logged with the
34
34
  `Logger::WARN` level.
35
+ * `:execution_strategy` which can either be `Sisyphus::SimpleExecutionStrategy` or
36
+ `Sisyphus::ForkingExecutionStrategy`. This is the strategy used by
37
+ workers when performing the job. The default is
38
+ `Sisyphus::ForkingExecutionStrategy`.
35
39
  4. You can start workers by doing one of the following things:
36
40
  * Send the `start` message to the master, if the `options` hash was
37
41
  provided. This starts a run loop which monitors workers and
@@ -63,8 +67,6 @@ Things missing
63
67
  Sisyphus is still very much in its infancy, though the ambition isn't to build a [Resque] [resque] clone, but
64
68
  instead build as small a tool with as few features as possible.
65
69
 
66
- [resque]: https://github.com/resque/resque
67
-
68
70
  There are, however, still features that are missing:
69
71
 
70
72
  - Force killing workers
@@ -76,6 +78,7 @@ There are, however, still features that are missing:
76
78
  - Some sort of reaping of worker processes
77
79
  - Documentation
78
80
 
81
+ [resque]: https://github.com/resque/resque
79
82
  [unicorn]: http://unicorn.bogomips.org/
80
83
 
81
84
  Contributing
data/Rakefile CHANGED
@@ -5,8 +5,10 @@ task :sleeper do
5
5
  gem 'sisyphus'
6
6
  require 'sisyphus'
7
7
  require 'sisyphus/sleep'
8
+ require 'logger'
8
9
 
10
+ logger = Logger.new(STDOUT)
9
11
  job = Sisyphus::Sleep.new
10
- master = Sisyphus::Master.new job, workers: 2
12
+ master = Sisyphus::Master.new job, workers: 2, logger: logger
11
13
  master.start
12
14
  end
@@ -0,0 +1,52 @@
1
+ module Sisyphus
2
+ class ForkingExecutionStrategy
3
+
4
+ def execute(job, error_handler = ->(process_name, error) {})
5
+ if @child_pid = fork
6
+ ChildProcess.new(@child_pid).success?
7
+ else
8
+ perform job, error_handler
9
+ end
10
+ end
11
+
12
+ private
13
+
14
+ def perform(job, error_handler)
15
+ self.process_name = "Child of worker #{::Process.ppid}"
16
+ begin
17
+ job.perform
18
+ exit! 0
19
+ rescue ::Exception => e
20
+ error_handler.call process_name, e
21
+ exit! 1
22
+ end
23
+ end
24
+
25
+ def process_name
26
+ $0
27
+ end
28
+
29
+ def process_name=(name)
30
+ $0 = name
31
+ end
32
+
33
+ class ChildProcess
34
+ attr_reader :pid
35
+
36
+ def initialize(pid)
37
+ @pid = pid
38
+ end
39
+
40
+ def success?
41
+ status.success?
42
+ end
43
+
44
+ def status
45
+ _, status = ::Process.waitpid2 pid
46
+ status
47
+ end
48
+
49
+ end
50
+
51
+ end
52
+ end
@@ -1,6 +1,8 @@
1
1
  require 'timeout'
2
2
  require_relative './worker'
3
+ require_relative './forking_execution_strategy'
3
4
  require_relative './null_logger'
5
+ require_relative './worker_pool'
4
6
 
5
7
  module Sisyphus
6
8
  class Master
@@ -8,12 +10,15 @@ module Sisyphus
8
10
 
9
11
  HANDLED_SIGNALS = [:INT, :TTIN, :TTOU]
10
12
 
11
- attr_reader :logger
13
+ attr_reader :logger, :job, :number_of_workers, :execution_strategy
12
14
 
13
15
  def initialize(job, options = {})
14
- @number_of_workers = options.fetch :workers, 0
15
- @logger = options.fetch :logger, NullLogger.new
16
- @workers = []
16
+ self.number_of_workers = options.fetch :workers, 0
17
+ @logger = options.fetch(:logger) { NullLogger.new }
18
+ @execution_strategy = options.fetch(:execution_strategy) { ForkingExecutionStrategy.new }
19
+
20
+ @worker_pool = options.fetch(:worker_pool) { WorkerPool.new self }
21
+
17
22
  @job = job
18
23
 
19
24
  self_reader, self_writer = IO.pipe
@@ -24,60 +29,54 @@ module Sisyphus
24
29
 
25
30
  def start
26
31
  trap_signals
27
- @number_of_workers.times do
28
- start_worker
32
+ number_of_workers.times do
33
+ @worker_pool.spawn_worker
29
34
  sleep rand(1000).fdiv(1000)
30
35
  end
31
36
  puts "Sisyphus::Master started with PID: #{Process.pid}"
32
37
  watch_for_output
33
38
  end
34
39
 
35
- def start_worker
36
- reader, writer = IO.pipe
37
- if wpid = fork
38
- writer.close
39
- @workers << { pid: wpid, reader: reader }
40
- else
41
- reader.close
42
- self.process_name = "Worker #{Process.pid}"
43
- begin
44
- worker = Worker.new(@job, writer, logger)
45
- worker.setup
46
- worker.start
47
- rescue Exception => e
48
- writer.write Worker::UNCAUGHT_ERROR
49
- logger.warn(process_name) { e }
50
- exit! 0
51
- end
52
- end
40
+ def create_worker
41
+ Worker.new(job, execution_strategy, logger)
53
42
  end
54
43
 
55
44
  def stop_worker(wpid)
56
- if @workers.find { |w| w.fetch(:pid) == wpid }
45
+ if workers.find { |w| w.fetch(:pid) == wpid }
57
46
  Process.kill 'INT', wpid rescue Errno::ESRCH # Ignore if the process is already gone
58
47
  end
59
48
  end
60
49
 
61
50
  def stop_all
62
- @workers.each do |worker|
51
+ workers.each do |worker|
63
52
  stop_worker worker.fetch(:pid)
64
53
  end
65
- Timeout.timeout(30) do
66
- watch_for_shutdown while worker_count > 0
54
+ begin
55
+ Timeout.timeout(30) do
56
+ watch_for_shutdown while worker_count > 0
57
+ end
58
+ rescue Timeout::Error => e
59
+ p "Timeout reached:", e
67
60
  end
68
61
  end
69
62
 
70
63
  def worker_count
71
- @workers.length
64
+ workers.length
72
65
  end
73
66
 
74
67
  private
75
68
 
69
+ attr_writer :number_of_workers
70
+
71
+ def workers
72
+ @worker_pool.workers
73
+ end
74
+
76
75
  def watch_for_shutdown
77
76
  wpid, _ = Process.wait2
78
- worker = @workers.find { |w| w.fetch(:pid) == wpid }
77
+ worker = workers.find { |w| w.fetch(:pid) == wpid }
79
78
  worker.fetch(:reader).close
80
- @workers.delete worker
79
+ workers.delete worker
81
80
  wpid
82
81
  rescue Errno::ECHILD
83
82
  end
@@ -93,7 +92,7 @@ module Sisyphus
93
92
  end
94
93
 
95
94
  def process_signal_queue
96
- handle_signal(Thread.main[:signal_queue].shift) until Thread.main[:signal_queue].empty?
95
+ handle_signal(signal_queue.shift) until signal_queue.empty?
97
96
  end
98
97
 
99
98
  def process_pipes(pipes)
@@ -107,26 +106,26 @@ module Sisyphus
107
106
 
108
107
  def process_output(pipes)
109
108
  pipes.each do |pipe|
110
- restart_worker worker_pid(pipe) unless stopping?
109
+ respawn_worker worker_pid(pipe) unless stopping?
111
110
  end
112
111
  end
113
112
 
114
- def restart_worker(wpid)
115
- start_worker
113
+ def respawn_worker(wpid)
114
+ @worker_pool.spawn_worker
116
115
  stop_worker wpid
117
116
  watch_for_shutdown
118
117
  end
119
118
 
120
119
  def worker_pipes
121
120
  if worker_count > 0
122
- @workers.map { |w| w.fetch(:reader) }
121
+ workers.map { |w| w.fetch(:reader) }
123
122
  else
124
123
  []
125
124
  end
126
125
  end
127
126
 
128
127
  def worker_pid(reader)
129
- if worker = @workers.find { |w| w.fetch(:reader).fileno == reader.fileno }
128
+ if worker = workers.find { |w| w.fetch(:reader).fileno == reader.fileno }
130
129
  worker.fetch(:pid)
131
130
  else
132
131
  raise 'Unknown worker pipe'
@@ -142,7 +141,7 @@ module Sisyphus
142
141
  end
143
142
 
144
143
  def queue_signal(signal)
145
- Thread.main[:signal_queue] << signal
144
+ signal_queue << signal
146
145
  @selfpipe[:writer].write_nonblock('.')
147
146
  rescue Errno::EAGAIN
148
147
  # Ignore
@@ -171,14 +170,14 @@ module Sisyphus
171
170
  end
172
171
 
173
172
  def handle_ttin
174
- @number_of_workers += 1
175
- start_worker
173
+ self.number_of_workers += 1
174
+ @worker_pool.spawn_worker
176
175
  end
177
176
 
178
177
  def handle_ttou
179
- if @number_of_workers > 0
180
- @number_of_workers -= 1
181
- stop_worker(@workers.first.fetch(:pid))
178
+ if number_of_workers > 0
179
+ self.number_of_workers -= 1
180
+ stop_worker(workers.first.fetch(:pid))
182
181
  end
183
182
  end
184
183
 
@@ -190,12 +189,8 @@ module Sisyphus
190
189
  @stopping
191
190
  end
192
191
 
193
- def process_name=(name)
194
- $0 = name
195
- end
196
-
197
- def process_name
198
- $0
192
+ def signal_queue
193
+ Thread.main[:signal_queue]
199
194
  end
200
195
  end
201
196
  end
@@ -0,0 +1,17 @@
1
+ module Sisyphus
2
+ class SimpleExecutionStrategy
3
+
4
+ def execute(job, error_handler = ->(name, error) {})
5
+ job.perform
6
+ rescue Exception => e
7
+ error_handler.call process_name, e
8
+ end
9
+
10
+ private
11
+
12
+ def process_name
13
+ $0
14
+ end
15
+
16
+ end
17
+ end
@@ -3,7 +3,7 @@ require_relative './job'
3
3
  module Sisyphus
4
4
  class Sleep < Job
5
5
  def perform
6
- sleep 2
6
+ sleep 40
7
7
  puts "Goodmorning from #{Process.pid}"
8
8
  raise "Hest" if rand(10) % 2 == 0
9
9
  end
@@ -2,49 +2,74 @@ module Sisyphus
2
2
  class Worker
3
3
  UNCAUGHT_ERROR = '.'
4
4
 
5
- attr_reader :logger
5
+ attr_reader :logger, :execution_strategy, :job, :output, :to_master
6
6
 
7
- def initialize(job, output, logger)
7
+ def initialize(job, execution_strategy, logger)
8
8
  @job = job
9
- @output = output
9
+ @to_master, @output = IO.pipe
10
+ @execution_strategy = execution_strategy
10
11
  @logger = logger
12
+ @set_up = false
11
13
  end
12
14
 
13
- def setup
14
- @job.setup if @job.respond_to? :setup
15
+ def start
16
+ setup
17
+ run
15
18
  end
16
19
 
17
- def start
20
+ def setup
18
21
  trap_signals
22
+ job.setup if job.respond_to? :setup
23
+ setup_done
24
+ rescue Exception => e
25
+ error_handler.call "Setup", e
26
+ end
19
27
 
28
+ def run
20
29
  loop do
21
30
  break if stopped?
22
31
  perform_job
23
- end
32
+ end if set_up?
24
33
 
25
34
  exit! 0
26
35
  end
27
36
 
28
- private
29
-
30
37
  def perform_job
31
- if child = fork
32
- _, status = Process.waitpid2 child
38
+ execution_strategy.execute job, error_handler
39
+ end
40
+
41
+ def error_handler
42
+ -> (name, error) {
43
+ return if stopped?
33
44
  begin
34
- @output.write UNCAUGHT_ERROR unless status.success? || stopped?
45
+ logger.warn(name) { error }
46
+ output.write UNCAUGHT_ERROR
35
47
  rescue Errno::EAGAIN, Errno::EINTR
36
48
  # Ignore
37
49
  end
38
- else
39
- self.process_name = "Child of worker #{Process.ppid}"
40
- begin
41
- @job.perform
42
- exit! 0
43
- rescue Exception => e
44
- logger.warn(process_name) { e }
45
- exit! 1
46
- end
47
- end
50
+ }
51
+ end
52
+
53
+ def stop
54
+ @stopped = true
55
+ end
56
+
57
+ def atfork_parent
58
+ output.close
59
+ end
60
+
61
+ def atfork_child
62
+ to_master.close
63
+ end
64
+
65
+ private
66
+
67
+ def set_up?
68
+ @set_up
69
+ end
70
+
71
+ def setup_done
72
+ @set_up = true
48
73
  end
49
74
 
50
75
  def trap_signals
@@ -53,20 +78,9 @@ module Sisyphus
53
78
  end
54
79
  end
55
80
 
56
- def stop
57
- @stopped = true
58
- end
59
-
60
81
  def stopped?
61
82
  @stopped
62
83
  end
63
84
 
64
- def process_name=(name)
65
- $0 = name
66
- end
67
-
68
- def process_name
69
- $0
70
- end
71
85
  end
72
86
  end
@@ -0,0 +1,36 @@
1
+ require_relative './worker'
2
+
3
+ module Sisyphus
4
+ class WorkerPool
5
+
6
+ attr_reader :workers, :worker_factory
7
+
8
+ def initialize(worker_factory)
9
+ @worker_factory = worker_factory
10
+ @workers = []
11
+ end
12
+
13
+ def spawn_worker
14
+ worker = create_worker
15
+ if wpid = fork
16
+ worker.atfork_parent
17
+ workers << { pid: wpid, reader: worker.to_master }
18
+ else
19
+ worker.atfork_child
20
+ self.process_name = "Worker #{Process.pid}"
21
+ worker.start
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def create_worker
28
+ worker_factory.create_worker
29
+ end
30
+
31
+ def process_name=(name)
32
+ $0 = name
33
+ end
34
+
35
+ end
36
+ end
data/lib/sisyphus.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require "version"
2
+ require 'sisyphus/simple_execution_strategy'
2
3
  require 'sisyphus/job'
3
- require 'sisyphus/worker'
4
4
  require 'sisyphus/master'
5
5
 
6
6
  module Sisyphus
data/lib/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Sisyphus
2
- VERSION = "0.2.3"
2
+ VERSION = "0.2.5"
3
3
  end
data/sisyphus.gemspec CHANGED
@@ -18,6 +18,8 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
+ spec.required_ruby_version = '>= 1.9.3'
22
+
21
23
  spec.add_development_dependency "bundler", "~> 1.3"
22
24
  spec.add_development_dependency "rake", "~> 10.0"
23
25
  spec.add_development_dependency "rspec", "~> 2.14"
@@ -0,0 +1,98 @@
1
+ require 'spec_helper'
2
+ require_relative '../../lib/sisyphus/forking_execution_strategy'
3
+
4
+ module Sisyphus
5
+ describe ForkingExecutionStrategy do
6
+
7
+ let(:job) { double :job }
8
+ let(:error_handler) { ->(name, raised_error) {} }
9
+ let(:strategy) { ForkingExecutionStrategy.new }
10
+ let(:child_pid) { 1 }
11
+ let(:status) { double :process_status }
12
+
13
+ it 'forks on execution' do
14
+ allow(Process).to receive(:waitpid2).with(child_pid) { [child_pid, status] }
15
+ allow(status).to receive(:success?) { true }
16
+ expect(strategy).to receive(:fork) { child_pid }
17
+ strategy.execute job
18
+ end
19
+
20
+ context 'in the parent process' do
21
+
22
+ before :each do
23
+ allow(strategy).to receive(:fork) { child_pid }
24
+ allow(status).to receive(:success?) { true }
25
+ end
26
+
27
+ it 'waits for the execution to finish' do
28
+ expect(Process).to receive(:waitpid2).with(child_pid) { [child_pid, status] }
29
+ strategy.execute(job, error_handler)
30
+ end
31
+
32
+ it 'gets the status of the child process' do
33
+ allow(Process).to receive(:waitpid2) { [child_pid, status] }
34
+ expect(strategy.execute job, error_handler).to eq(true)
35
+ end
36
+
37
+ end
38
+
39
+ context 'in the execution process' do
40
+
41
+ let(:ppid) { 2 }
42
+
43
+ before :each do
44
+ allow(strategy).to receive(:fork) { nil }
45
+ allow(job).to receive(:perform)
46
+ allow(strategy).to receive(:exit!)
47
+ end
48
+
49
+ it 'updates the process name' do
50
+ allow(Process).to receive(:ppid) { ppid }
51
+ expect(strategy).to receive(:process_name=).with("Child of worker #{ppid}")
52
+ strategy.execute job
53
+ end
54
+
55
+ it 'performs the job' do
56
+ expect(job).to receive(:perform)
57
+ strategy.execute job
58
+ end
59
+
60
+ it 'exits with a 0 status if job is performed without failing' do
61
+ expect(strategy).to receive(:exit!).with(0)
62
+ strategy.execute job
63
+ end
64
+
65
+ it 'does not call error_handler if execution is successful' do
66
+ allow(strategy).to receive(:exit!).with(0)
67
+ strategy.execute job, ->(n, e) { fail "Should not be called" }
68
+ end
69
+
70
+ context 'when the job#perform fails' do
71
+
72
+ let(:process_name) { "foobarbaz" }
73
+ let(:exception) { Exception.new("foo") }
74
+
75
+ before :each do
76
+ allow(strategy).to receive(:process_name) { process_name }
77
+ allow(job).to receive(:perform).and_raise(exception)
78
+ end
79
+
80
+ it 'exits with a 1 status if job is performed and it fails' do
81
+ expect(strategy).to receive(:exit!).with(1)
82
+ strategy.execute job, error_handler
83
+ end
84
+
85
+ it 'calls error_handler if execution is unsuccessful' do
86
+ allow(strategy).to receive(:exit!)
87
+ strategy.execute job, ->(name, raised_error) {
88
+ expect(name).to eq(process_name)
89
+ expect(raised_error).to eq(exception)
90
+ }
91
+ end
92
+
93
+ end
94
+
95
+ end
96
+
97
+ end
98
+ end