sisyphus 0.2.1

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 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