sisyphus 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f454308987e390a7ae0e4795760f41daf32a6643
4
+ data.tar.gz: c37488406b5d180482b9d815d6b912f1eb99f217
5
+ SHA512:
6
+ metadata.gz: 1dbd8435f29ff6bda19c1d379f9009e3bb7276f092577600a9a21a7932bf571d81ffbfd04211fa8f84857911bdcd569b05eb70182c944ad31c748f7c0e08ce7b
7
+ data.tar.gz: e5b74daf39354ba9585e224645a18c7d1789fd897a16fdb1dd5a74c039ecf3371f2afae4a87d32d7e48c50138d65d796877f4788b48483bb2c77eb25ae4dabcb
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in coolie.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,27 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ sisyphus (0.1.1)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ diff-lcs (1.2.4)
10
+ rake (10.1.0)
11
+ rspec (2.14.1)
12
+ rspec-core (~> 2.14.0)
13
+ rspec-expectations (~> 2.14.0)
14
+ rspec-mocks (~> 2.14.0)
15
+ rspec-core (2.14.5)
16
+ rspec-expectations (2.14.2)
17
+ diff-lcs (>= 1.1.3, < 2.0)
18
+ rspec-mocks (2.14.3)
19
+
20
+ PLATFORMS
21
+ ruby
22
+
23
+ DEPENDENCIES
24
+ bundler (~> 1.3)
25
+ sisyphus!
26
+ rake (~> 10.0)
27
+ rspec (~> 2.14)
data/LICENSE.txt ADDED
@@ -0,0 +1,191 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction, and
10
+ distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by the copyright
13
+ owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all other entities
16
+ that control, are controlled by, or are under common control with that entity.
17
+ For the purposes of this definition, "control" means (i) the power, direct or
18
+ indirect, to cause the direction or management of such entity, whether by
19
+ contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
20
+ outstanding shares, or (iii) beneficial ownership of such entity.
21
+
22
+ "You" (or "Your") shall mean an individual or Legal Entity exercising
23
+ permissions granted by this License.
24
+
25
+ "Source" form shall mean the preferred form for making modifications, including
26
+ but not limited to software source code, documentation source, and configuration
27
+ files.
28
+
29
+ "Object" form shall mean any form resulting from mechanical transformation or
30
+ translation of a Source form, including but not limited to compiled object code,
31
+ generated documentation, and conversions to other media types.
32
+
33
+ "Work" shall mean the work of authorship, whether in Source or Object form, made
34
+ available under the License, as indicated by a copyright notice that is included
35
+ in or attached to the work (an example is provided in the Appendix below).
36
+
37
+ "Derivative Works" shall mean any work, whether in Source or Object form, that
38
+ is based on (or derived from) the Work and for which the editorial revisions,
39
+ annotations, elaborations, or other modifications represent, as a whole, an
40
+ original work of authorship. For the purposes of this License, Derivative Works
41
+ shall not include works that remain separable from, or merely link (or bind by
42
+ name) to the interfaces of, the Work and Derivative Works thereof.
43
+
44
+ "Contribution" shall mean any work of authorship, including the original version
45
+ of the Work and any modifications or additions to that Work or Derivative Works
46
+ thereof, that is intentionally submitted to Licensor for inclusion in the Work
47
+ by the copyright owner or by an individual or Legal Entity authorized to submit
48
+ on behalf of the copyright owner. For the purposes of this definition,
49
+ "submitted" means any form of electronic, verbal, or written communication sent
50
+ to the Licensor or its representatives, including but not limited to
51
+ communication on electronic mailing lists, source code control systems, and
52
+ issue tracking systems that are managed by, or on behalf of, the Licensor for
53
+ the purpose of discussing and improving the Work, but excluding communication
54
+ that is conspicuously marked or otherwise designated in writing by the copyright
55
+ owner as "Not a Contribution."
56
+
57
+ "Contributor" shall mean Licensor and any individual or Legal Entity on behalf
58
+ of whom a Contribution has been received by Licensor and subsequently
59
+ incorporated within the Work.
60
+
61
+ 2. Grant of Copyright License.
62
+
63
+ Subject to the terms and conditions of this License, each Contributor hereby
64
+ grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
65
+ irrevocable copyright license to reproduce, prepare Derivative Works of,
66
+ publicly display, publicly perform, sublicense, and distribute the Work and such
67
+ Derivative Works in Source or Object form.
68
+
69
+ 3. Grant of Patent License.
70
+
71
+ Subject to the terms and conditions of this License, each Contributor hereby
72
+ grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
73
+ irrevocable (except as stated in this section) patent license to make, have
74
+ made, use, offer to sell, sell, import, and otherwise transfer the Work, where
75
+ such license applies only to those patent claims licensable by such Contributor
76
+ that are necessarily infringed by their Contribution(s) alone or by combination
77
+ of their Contribution(s) with the Work to which such Contribution(s) was
78
+ submitted. If You institute patent litigation against any entity (including a
79
+ cross-claim or counterclaim in a lawsuit) alleging that the Work or a
80
+ Contribution incorporated within the Work constitutes direct or contributory
81
+ patent infringement, then any patent licenses granted to You under this License
82
+ for that Work shall terminate as of the date such litigation is filed.
83
+
84
+ 4. Redistribution.
85
+
86
+ You may reproduce and distribute copies of the Work or Derivative Works thereof
87
+ in any medium, with or without modifications, and in Source or Object form,
88
+ provided that You meet the following conditions:
89
+
90
+ You must give any other recipients of the Work or Derivative Works a copy of
91
+ this License; and
92
+ You must cause any modified files to carry prominent notices stating that You
93
+ changed the files; and
94
+ You must retain, in the Source form of any Derivative Works that You distribute,
95
+ all copyright, patent, trademark, and attribution notices from the Source form
96
+ of the Work, excluding those notices that do not pertain to any part of the
97
+ Derivative Works; and
98
+ If the Work includes a "NOTICE" text file as part of its distribution, then any
99
+ Derivative Works that You distribute must include a readable copy of the
100
+ attribution notices contained within such NOTICE file, excluding those notices
101
+ that do not pertain to any part of the Derivative Works, in at least one of the
102
+ following places: within a NOTICE text file distributed as part of the
103
+ Derivative Works; within the Source form or documentation, if provided along
104
+ with the Derivative Works; or, within a display generated by the Derivative
105
+ Works, if and wherever such third-party notices normally appear. The contents of
106
+ the NOTICE file are for informational purposes only and do not modify the
107
+ License. You may add Your own attribution notices within Derivative Works that
108
+ You distribute, alongside or as an addendum to the NOTICE text from the Work,
109
+ provided that such additional attribution notices cannot be construed as
110
+ modifying the License.
111
+ You may add Your own copyright statement to Your modifications and may provide
112
+ additional or different license terms and conditions for use, reproduction, or
113
+ distribution of Your modifications, or for any such Derivative Works as a whole,
114
+ provided Your use, reproduction, and distribution of the Work otherwise complies
115
+ with the conditions stated in this License.
116
+
117
+ 5. Submission of Contributions.
118
+
119
+ Unless You explicitly state otherwise, any Contribution intentionally submitted
120
+ for inclusion in the Work by You to the Licensor shall be under the terms and
121
+ conditions of this License, without any additional terms or conditions.
122
+ Notwithstanding the above, nothing herein shall supersede or modify the terms of
123
+ any separate license agreement you may have executed with Licensor regarding
124
+ such Contributions.
125
+
126
+ 6. Trademarks.
127
+
128
+ This License does not grant permission to use the trade names, trademarks,
129
+ service marks, or product names of the Licensor, except as required for
130
+ reasonable and customary use in describing the origin of the Work and
131
+ reproducing the content of the NOTICE file.
132
+
133
+ 7. Disclaimer of Warranty.
134
+
135
+ Unless required by applicable law or agreed to in writing, Licensor provides the
136
+ Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
137
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
138
+ including, without limitation, any warranties or conditions of TITLE,
139
+ NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
140
+ solely responsible for determining the appropriateness of using or
141
+ redistributing the Work and assume any risks associated with Your exercise of
142
+ permissions under this License.
143
+
144
+ 8. Limitation of Liability.
145
+
146
+ In no event and under no legal theory, whether in tort (including negligence),
147
+ contract, or otherwise, unless required by applicable law (such as deliberate
148
+ and grossly negligent acts) or agreed to in writing, shall any Contributor be
149
+ liable to You for damages, including any direct, indirect, special, incidental,
150
+ or consequential damages of any character arising as a result of this License or
151
+ out of the use or inability to use the Work (including but not limited to
152
+ damages for loss of goodwill, work stoppage, computer failure or malfunction, or
153
+ any and all other commercial damages or losses), even if such Contributor has
154
+ been advised of the possibility of such damages.
155
+
156
+ 9. Accepting Warranty or Additional Liability.
157
+
158
+ While redistributing the Work or Derivative Works thereof, You may choose to
159
+ offer, and charge a fee for, acceptance of support, warranty, indemnity, or
160
+ other liability obligations and/or rights consistent with this License. However,
161
+ in accepting such obligations, You may act only on Your own behalf and on Your
162
+ sole responsibility, not on behalf of any other Contributor, and only if You
163
+ agree to indemnify, defend, and hold each Contributor harmless for any liability
164
+ incurred by, or claims asserted against, such Contributor by reason of your
165
+ accepting any such warranty or additional liability.
166
+
167
+ END OF TERMS AND CONDITIONS
168
+
169
+ APPENDIX: How to apply the Apache License to your work
170
+
171
+ To apply the Apache License to your work, attach the following boilerplate
172
+ notice, with the fields enclosed by brackets "[]" replaced with your own
173
+ identifying information. (Don't include the brackets!) The text should be
174
+ enclosed in the appropriate comment syntax for the file format. We also
175
+ recommend that a file or class name and description of purpose be included on
176
+ the same "printed page" as the copyright notice for easier identification within
177
+ third-party archives.
178
+
179
+ Copyright [yyyy] [name of copyright owner]
180
+
181
+ Licensed under the Apache License, Version 2.0 (the "License");
182
+ you may not use this file except in compliance with the License.
183
+ You may obtain a copy of the License at
184
+
185
+ http://www.apache.org/licenses/LICENSE-2.0
186
+
187
+ Unless required by applicable law or agreed to in writing, software
188
+ distributed under the License is distributed on an "AS IS" BASIS,
189
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
190
+ See the License for the specific language governing permissions and
191
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,96 @@
1
+ Sisyphus
2
+ ======
3
+
4
+ Sisyphus provides a really simple way of starting and stopping multiple parallel
5
+ worker processes that are meant to run repeatedly in an efficient way.
6
+
7
+ It requires no frameworks, databases or anything. It just does what you
8
+ want it to - until you tell it to stop doing it.
9
+
10
+ How it works
11
+ ------------
12
+
13
+ The Master takes a job as argument in the initializer, that responds to `setup` and `perform`.
14
+
15
+ When sending the `start_worker` message to the master, it forks a child process where it hands
16
+ the job to a new worker, which then sends the `setup` message to the job during initialization.
17
+ The purpose of the `setup` method is to load and initialize anything necessary to perform the job.
18
+
19
+ The master then starts the worker, which enters a run loop, that forks yet another child process where
20
+ the job receives the `perform` message.
21
+
22
+ Getting started
23
+ ---------------
24
+
25
+ 1. Add `gem 'coolie'` to your Gemfile and run `bundle`
26
+ 2. Subclass `Sisyphus::Job` and implement the `perform` method
27
+ 3. Instantiate the `Sisyphus::Master`, giving it an instance of your job
28
+ and optionally an options hash with the key `:workers` specifying the
29
+ number of workers you need
30
+ 4. You can start workers by doing one of the following things:
31
+ * Send the `start` message to the master, if the `options` hash was
32
+ provided. This starts a run loop which monitors workers and
33
+ restarts them if they encounters uncaught exceptions
34
+ * Otherwise send the `start_worker` message to the master as many
35
+ times as the number of workers needed. This doesn't start any run
36
+ loop, and there is no monitoring of workers.
37
+
38
+ If you need to stop workers, you can do it one at the time by sending
39
+ the `stop_worker` message to the master or stop them all by sending the
40
+ `stop_all` message.
41
+
42
+ When stopping workers, they are allowed to finish what they are doing,
43
+ before they stop. Which means you're screwed right now, if the `perform`
44
+ method in your job never returns.
45
+
46
+ The master registers signal handlers when you send it the `start`
47
+ message and enters a run loop.
48
+
49
+ The signals the master responds to are:
50
+
51
+ - `SIGINT` tells the master to gracefully stop all workers and shut down
52
+ - `SIGTTIN` tells the master to spawn a new worker
53
+ - `SIGTTOU` tells the master to stop a worker
54
+
55
+ Things missing
56
+ --------------
57
+
58
+ Sisyphus is still very much in its infancy, though the ambition isn't to build a [Resque] [resque] clone, but
59
+ instead build as small a tool with as few features as possible.
60
+
61
+ [resque]: https://github.com/resque/resque
62
+
63
+ There are, however, still features that are missing:
64
+
65
+ - Force killing workers
66
+ - Daemonization of the master
67
+ - The master should have a shutdown immediately signal
68
+ - Communication with the master through signals (something like [Unicorn] [unicorn])
69
+ - Basic logging
70
+ - Some sort of error handling
71
+ - Some sort of reaping of worker processes
72
+ - Documentation
73
+
74
+ [unicorn]: http://unicorn.bogomips.org/
75
+
76
+ Contributing
77
+ ------------
78
+
79
+ Feel free to report issues or fork, fix and submit pull requests.
80
+
81
+ License
82
+ -------
83
+
84
+ Copyright 2013 Rasmus Bang Grouleff
85
+
86
+ Licensed under the Apache License, Version 2.0 (the "License");
87
+ you may not use this file except in compliance with the License.
88
+ You may obtain a copy of the License at
89
+
90
+ http://www.apache.org/licenses/LICENSE-2.0
91
+
92
+ Unless required by applicable law or agreed to in writing, software
93
+ distributed under the License is distributed on an "AS IS" BASIS,
94
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
95
+ See the License for the specific language governing permissions and
96
+ limitations under the License.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ desc 'Example task that fires up a sleep job with 2 workers'
4
+ task :sleeper do
5
+ gem 'sisyphus'
6
+ require 'sisyphus'
7
+ require 'sisyphus/sleep'
8
+
9
+ job = Sisyphus::Sleep.new
10
+ master = Sisyphus::Master.new job, workers: 2
11
+ master.start
12
+ end
@@ -0,0 +1,9 @@
1
+ module Sisyphus
2
+ class Job
3
+ def setup
4
+ end
5
+
6
+ def perform
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,188 @@
1
+ require 'timeout'
2
+ require_relative './worker'
3
+
4
+ module Sisyphus
5
+ class Master
6
+ IO_TIMEOUT = 10
7
+
8
+ HANDLED_SIGNALS = [:INT, :TTIN, :TTOU]
9
+
10
+ def initialize(job, options = {})
11
+ @number_of_workers = options.fetch :workers, 0
12
+ @workers = []
13
+ @job = job
14
+
15
+ self_reader, self_writer = IO.pipe
16
+ @selfpipe = { reader: self_reader, writer: self_writer }
17
+
18
+ Thread.main[:signal_queue] = []
19
+ end
20
+
21
+ def start
22
+ trap_signals
23
+ @number_of_workers.times do
24
+ start_worker
25
+ sleep rand(1000).fdiv(1000)
26
+ end
27
+ puts "Sisyphus::Master started with PID: #{Process.pid}"
28
+ watch_for_output
29
+ end
30
+
31
+ def start_worker
32
+ reader, writer = IO.pipe
33
+ if wpid = fork
34
+ writer.close
35
+ @workers << { pid: wpid, reader: reader }
36
+ else
37
+ reader.close
38
+ worker = Worker.new(@job, writer)
39
+ self.process_name = "Worker #{Process.pid}"
40
+ worker.start
41
+ end
42
+ end
43
+
44
+ def stop_worker(wpid)
45
+ if @workers.find { |w| w.fetch(:pid) == wpid }
46
+ Process.kill 'INT', wpid
47
+ else
48
+ raise "Unknown worker PID: #{wpid}"
49
+ end
50
+ end
51
+
52
+ def stop_all
53
+ @workers.each do |worker|
54
+ stop_worker worker.fetch(:pid)
55
+ end
56
+ Timeout.timeout(30) do
57
+ watch_for_shutdown while worker_count > 0
58
+ end
59
+ end
60
+
61
+ def worker_count
62
+ @workers.length
63
+ end
64
+
65
+ private
66
+
67
+ def watch_for_shutdown
68
+ wpid, _ = Process.wait2
69
+ worker = @workers.find { |w| w.fetch(:pid) == wpid }
70
+ worker.fetch(:reader).close
71
+ @workers.delete worker
72
+ wpid
73
+ rescue Errno::ECHILD
74
+ end
75
+
76
+ def watch_for_output
77
+ loop do
78
+ ready = IO.select(worker_pipes + [@selfpipe[:reader]], nil, nil, IO_TIMEOUT)
79
+ if ready
80
+ process_pipes(ready[0])
81
+ process_signal_queue
82
+ end
83
+ end
84
+ end
85
+
86
+ def process_signal_queue
87
+ handle_signal(Thread.main[:signal_queue].shift) until Thread.main[:signal_queue].empty?
88
+ end
89
+
90
+ def process_pipes(pipes)
91
+ begin
92
+ @selfpipe[:reader].read_nonblock(10) if pipes.include?(@selfpipe[:reader])
93
+ rescue Errno::EAGAIN, Errno::EINTR
94
+ # Ignore
95
+ end
96
+ process_output(pipes & worker_pipes) unless stopping?
97
+ end
98
+
99
+ def process_output(pipes)
100
+ pipes.each do |pipe|
101
+ restart_worker worker_pid(pipe) unless stopping?
102
+ end
103
+ end
104
+
105
+ def restart_worker(wpid)
106
+ start_worker
107
+ stop_worker wpid
108
+ watch_for_shutdown
109
+ end
110
+
111
+ def worker_pipes
112
+ if worker_count > 0
113
+ @workers.map { |w| w.fetch(:reader) }
114
+ else
115
+ []
116
+ end
117
+ end
118
+
119
+ def worker_pid(reader)
120
+ if worker = @workers.find { |w| w.fetch(:reader).fileno == reader.fileno }
121
+ worker.fetch(:pid)
122
+ else
123
+ raise 'Unknown worker pipe'
124
+ end
125
+ end
126
+
127
+ def trap_signals
128
+ HANDLED_SIGNALS.each do |signal|
129
+ Signal.trap signal do
130
+ queue_signal signal
131
+ end
132
+ end
133
+ end
134
+
135
+ def queue_signal(signal)
136
+ Thread.main[:signal_queue] << signal
137
+ @selfpipe[:writer].write_nonblock('.')
138
+ rescue Errno::EAGAIN
139
+ # Ignore
140
+ rescue Errno::EINTR
141
+ retry
142
+ end
143
+
144
+ def handle_signal(signal)
145
+ case signal
146
+ when :INT
147
+ handle_int
148
+ when :TTIN
149
+ handle_ttin
150
+ when :TTOU
151
+ handle_ttou
152
+ else
153
+ raise "Unknown signal"
154
+ end
155
+ end
156
+
157
+ def handle_int
158
+ puts "Waiting for workers to stop..."
159
+ stop
160
+ stop_all
161
+ exit 0
162
+ end
163
+
164
+ def handle_ttin
165
+ @number_of_workers += 1
166
+ start_worker
167
+ end
168
+
169
+ def handle_ttou
170
+ if @number_of_workers > 0
171
+ @number_of_workers -= 1
172
+ stop_worker(@workers.first.fetch(:pid))
173
+ end
174
+ end
175
+
176
+ def stop
177
+ @stopping = true
178
+ end
179
+
180
+ def stopping?
181
+ @stopping
182
+ end
183
+
184
+ def process_name=(name)
185
+ $0 = name
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,11 @@
1
+ require_relative './job'
2
+
3
+ module Sisyphus
4
+ class Sleep < Job
5
+ def perform
6
+ sleep 2
7
+ puts "Goodmorning from #{Process.pid}"
8
+ raise "Hest" if rand(10) % 2 == 0
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,65 @@
1
+ module Sisyphus
2
+ class Worker
3
+ UNCAUGHT_ERROR = '.'
4
+
5
+ def initialize(job, output)
6
+ @job = job
7
+ @output = output
8
+ setup
9
+ end
10
+
11
+ def start
12
+ trap_signals
13
+
14
+ loop do
15
+ break if stopped?
16
+ perform_job
17
+ end
18
+
19
+ exit 0
20
+ end
21
+
22
+ private
23
+
24
+ def perform_job
25
+ if child = fork
26
+ _, status = Process.waitpid2 child
27
+ begin
28
+ @output.write UNCAUGHT_ERROR unless status.success? || stopped?
29
+ rescue Errno::EAGAIN, Errno::EINTR
30
+ # Ignore
31
+ end
32
+ else
33
+ self.process_name = "Child of worker #{Process.ppid}"
34
+ begin
35
+ @job.perform
36
+ exit 0
37
+ rescue Exception
38
+ exit 1
39
+ end
40
+ end
41
+ end
42
+
43
+ def trap_signals
44
+ Signal.trap('INT') do
45
+ stop
46
+ end
47
+ end
48
+
49
+ def stop
50
+ @stopped = true
51
+ end
52
+
53
+ def stopped?
54
+ @stopped
55
+ end
56
+
57
+ def setup
58
+ @job.setup if @job.respond_to? :setup
59
+ end
60
+
61
+ def process_name=(name)
62
+ $0 = name
63
+ end
64
+ end
65
+ end
data/lib/sisyphus.rb ADDED
@@ -0,0 +1,8 @@
1
+ require "version"
2
+ require 'sisyphus/job'
3
+ require 'sisyphus/worker'
4
+ require 'sisyphus/master'
5
+
6
+ module Sisyphus
7
+ # Your code goes here...
8
+ end
data/lib/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ module Sisyphus
2
+ VERSION = "0.2.1"
3
+ end
data/sisyphus.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "sisyphus"
8
+ spec.version = Sisyphus::VERSION
9
+ spec.authors = ["Rasmus Bang Grouleff"]
10
+ spec.email = ["rasmusbg@virtualmanager.com"]
11
+ spec.description = %q{A tiny library for spawning worker processes}
12
+ spec.summary = %q{A tiny library for spawning worker processes}
13
+ spec.homepage = "https://github.com/rbgrouleff/sisyphus"
14
+ spec.license = "Apache License 2.0"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "rspec", "~> 2.14"
24
+ end
@@ -0,0 +1,167 @@
1
+ require_relative '../../lib/sisyphus/master'
2
+
3
+ module Sisyphus
4
+ describe Master do
5
+ subject(:master) { Master.new job }
6
+
7
+ before(:each) { master.stub :puts }
8
+
9
+ let(:job) { double(:job) }
10
+ let(:pipes) { [double(:reader_pipe), double(:writer_pipe)] }
11
+
12
+ describe 'when receiving the start_worker message' do
13
+ it 'forks' do
14
+ master.should_receive(:fork) { 666 }
15
+ master.start_worker
16
+ end
17
+
18
+ describe 'in the worker process' do
19
+ let(:worker) { double :worker }
20
+
21
+ before :each do
22
+ master.stub(:fork) { nil }
23
+ IO.stub(:pipe) { pipes }
24
+ pipes.first.stub(:close)
25
+ Process.stub(:pid) { 666 }
26
+ master.stub :exit!
27
+ Worker.stub(:new) { worker }
28
+ worker.stub(:start)
29
+ end
30
+
31
+ it 'should rename the process' do
32
+ master.should_receive(:process_name=).with("Worker #{666}")
33
+ master.start_worker
34
+ end
35
+
36
+ it 'starts a worker after forking' do
37
+ worker.should_receive :start
38
+ master.start_worker
39
+ end
40
+
41
+ it 'gives the writer pipe to the worker' do
42
+ Worker.should_receive(:new).with(job, pipes.last) { worker }
43
+ master.start_worker
44
+ end
45
+
46
+ it 'closes the reader pipe' do
47
+ pipes.first.should_receive :close
48
+ master.start_worker
49
+ end
50
+ end
51
+
52
+ describe 'in the master process' do
53
+ before :each do
54
+ master.stub(:fork) { 666 }
55
+ IO.stub(:pipe) { pipes }
56
+ pipes.last.stub(:close)
57
+ end
58
+
59
+ it 'increases worker_count' do
60
+ master.start_worker
61
+ master.worker_count.should eq(1)
62
+ end
63
+
64
+ it 'should open a pipe' do
65
+ IO.should_receive(:pipe) { pipes }
66
+ master.start_worker
67
+ end
68
+
69
+ it 'should close the writer pipe' do
70
+ pipes.last.should_receive :close
71
+ master.start_worker
72
+ end
73
+ end
74
+ end
75
+
76
+ describe 'when it has running workers' do
77
+ before :each do
78
+ pipes.each { |p| p.stub :close }
79
+ IO.stub(:pipe) { pipes }
80
+ master.stub(:fork) { 666 }
81
+ master.start_worker
82
+ Process.stub(:kill).with('INT', 666)
83
+ Process.stub(:waitpid2).with(666)
84
+ end
85
+
86
+ describe 'and it receives stop_worker message' do
87
+ it 'kills a child with the INT signal' do
88
+ Process.should_receive(:kill).with('INT', 666)
89
+ master.stop_worker(666)
90
+ end
91
+ end
92
+
93
+ it 'stops all workers when receiving stop_all' do
94
+ Process.stub(:kill).with('INT', 666)
95
+ Process.stub(:wait2) { 666 }
96
+
97
+ master.should_receive(:stop_worker).with(666).exactly(master.worker_count).times.and_call_original
98
+
99
+ master.stop_all
100
+ end
101
+ end
102
+
103
+ describe 'when there are no running workers' do
104
+ describe 'and it receives stop_worker' do
105
+ it 'raises an error' do
106
+ expect { master.stop_worker(666) }.to raise_error("Unknown worker PID: #{666}")
107
+ end
108
+ end
109
+
110
+ describe 'and it receives stop_all' do
111
+ it 'does nothing' do
112
+ master.should_not_receive(:stop_worker)
113
+ master.stop_all
114
+ end
115
+ end
116
+ end
117
+
118
+ it 'starts the specified number of workers when started' do
119
+ master = Master.new nil, workers: 3
120
+ master.stub :puts
121
+ master.stub :watch_for_output
122
+ master.should_receive(:start_worker).exactly(3).times
123
+ master.start
124
+ end
125
+
126
+ describe 'when number of workers is zero' do
127
+ let(:master) { Master.new nil, workers: 0 }
128
+
129
+ before(:each) { master.stub :puts }
130
+
131
+ it 'should not start workers' do
132
+ master.stub :watch_for_output
133
+ master.should_not_receive :start_worker
134
+ master.start
135
+ end
136
+ end
137
+
138
+ it 'attaches a signal handler when started' do
139
+ Signal.should_receive(:trap).with(:INT)
140
+ Signal.should_receive(:trap).with(:TTIN)
141
+ Signal.should_receive(:trap).with(:TTOU)
142
+ master.stub :start_worker
143
+ master.stub :watch_for_output
144
+ master.start
145
+ end
146
+
147
+ it 'should watch for output' do
148
+ master.stub :start_worker
149
+ master.should_receive :watch_for_output
150
+ master.start
151
+ end
152
+
153
+ it 'can resolve a wpid from a reader pipe' do
154
+ IO.stub(:pipe) { pipes }
155
+ pipes.each { |p| p.stub(:close) }
156
+ pipes.first.stub(:fileno) { 213 }
157
+ master.stub(:fork) { 666 }
158
+ master.start_worker
159
+
160
+ master.send(:worker_pid, pipes.first).should eq(666)
161
+ end
162
+
163
+ it 'raises if it can\'t resolve a wpid from a reader pipe' do
164
+ expect { master.send(:worker_pid, pipes.first) }.to raise_error("Unknown worker pipe")
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,117 @@
1
+ require_relative '../../lib/sisyphus/worker'
2
+
3
+ module Sisyphus
4
+ describe Worker do
5
+ let(:job) { double :job }
6
+ let(:output) { double :pipe }
7
+ let(:worker) { Worker.new job, output }
8
+
9
+ it 'traps signals when started' do
10
+ worker.stub :exit
11
+ worker.instance_variable_set(:@stopped, true)
12
+ worker.should_receive :trap_signals
13
+ worker.start
14
+ end
15
+
16
+ it 'exits when it has been stopped' do
17
+ worker.instance_variable_set(:@stopped, true)
18
+ worker.should_receive :exit
19
+ worker.start
20
+ end
21
+
22
+ context 'when job does not respond to :setup' do
23
+ it 'does not call job.setup' do
24
+ job.stub(:respond_to?).with(:setup) { false }
25
+ job.should_not_receive :setup
26
+ Worker.new job, output
27
+ end
28
+ end
29
+
30
+ context 'when job responds to :setup' do
31
+ it 'sets up the job upon initialization' do
32
+ job.stub(:respond_to?).with(:setup) { true }
33
+ job.should_receive :setup
34
+ Worker.new job, output
35
+ end
36
+ end
37
+
38
+ context 'in the child process' do
39
+ before :each do
40
+ worker.stub(:fork) { nil }
41
+ end
42
+
43
+ it 'should perform the job' do
44
+ job.should_receive :perform
45
+ worker.stub :exit
46
+ worker.send :perform_job
47
+ end
48
+
49
+ it 'should exit after having performed the job' do
50
+ job.stub :perform
51
+ worker.should_receive(:exit).with 0
52
+ worker.send :perform_job
53
+ end
54
+
55
+ it 'should change process name' do
56
+ job.stub :perform
57
+ worker.stub :exit
58
+ Process.stub(:ppid) { 666 }
59
+ worker.should_receive(:process_name=).with "Child of worker 666"
60
+ worker.send :perform_job
61
+ end
62
+
63
+ context 'when job.perform raises an error' do
64
+ it 'should exit with a non-zero status' do
65
+ job.stub(:perform).and_raise Exception.new "should be rescued!"
66
+ worker.should_receive(:exit).with(1)
67
+ worker.send :perform_job
68
+ end
69
+ end
70
+ end
71
+
72
+ context 'in the worker process' do
73
+ let(:status) { double(:status) }
74
+
75
+ before :each do
76
+ worker.stub(:fork) { 666 }
77
+ end
78
+
79
+ it 'spawns a process and waits for it to finish' do
80
+ worker.should_receive(:fork) { 666 }
81
+ status.stub(:success?) { true }
82
+ Process.should_receive(:waitpid2).with(666) { [666, status] }
83
+ worker.send :perform_job
84
+ end
85
+
86
+ context 'when exit status from the child is non-zero' do
87
+ before :each do
88
+ status.stub(:success?) { false }
89
+ Process.stub(:waitpid2) { [666, status] }
90
+ end
91
+
92
+ it 'writes an error message to the output' do
93
+ output.should_receive(:write).with Worker::UNCAUGHT_ERROR
94
+ worker.send :perform_job
95
+ end
96
+
97
+ it "doesn't write error byte to output when it has been stopped" do
98
+ worker.stub(:stopped?) { true }
99
+ output.should_not_receive :write
100
+ worker.send :perform_job
101
+ end
102
+ end
103
+
104
+ context 'when exit status from child is zero' do
105
+ before :each do
106
+ status.stub(:success?) { true }
107
+ Process.stub(:waitpid2) { [666, status] }
108
+ end
109
+
110
+ it "doesn't write error byte to output" do
111
+ output.should_not_receive :write
112
+ worker.send :perform_job
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sisyphus
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Rasmus Bang Grouleff
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-09-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '2.14'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '2.14'
55
+ description: A tiny library for spawning worker processes
56
+ email:
57
+ - rasmusbg@virtualmanager.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - .gitignore
63
+ - Gemfile
64
+ - Gemfile.lock
65
+ - LICENSE.txt
66
+ - README.md
67
+ - Rakefile
68
+ - lib/sisyphus.rb
69
+ - lib/sisyphus/job.rb
70
+ - lib/sisyphus/master.rb
71
+ - lib/sisyphus/sleep.rb
72
+ - lib/sisyphus/worker.rb
73
+ - lib/version.rb
74
+ - sisyphus.gemspec
75
+ - spec/sisyphus/master_spec.rb
76
+ - spec/sisyphus/worker_spec.rb
77
+ homepage: https://github.com/rbgrouleff/sisyphus
78
+ licenses:
79
+ - Apache License 2.0
80
+ metadata: {}
81
+ post_install_message:
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - '>='
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubyforge_project:
97
+ rubygems_version: 2.0.2
98
+ signing_key:
99
+ specification_version: 4
100
+ summary: A tiny library for spawning worker processes
101
+ test_files:
102
+ - spec/sisyphus/master_spec.rb
103
+ - spec/sisyphus/worker_spec.rb