magistrate 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # Magistrate
2
+
3
+ ## Installation
4
+
5
+ gem install magistrate
6
+
7
+ ## Description
8
+
9
+ Magistrate is a manager for workers that is cluster and network aware.
10
+ It interacts with a centralized server to get its management and marching orders.
11
+ It's designed to be a process manager that runs entirely in userspace: no root access needed.
12
+
13
+ ## Manual
14
+
15
+ The magistrate command line tool utilizes the following from the filesystem:
16
+
17
+ * config/magistrate.yml - The configuration file (override the path with the --config option)
18
+ * tmp/pids - stores the pids of itself and all managed workers
19
+
20
+ These are meant to coincide with easy running from a Rails app root (so that the worker config can be kept together with the app)
21
+ If you're using capistrano, then the tmp/pids directory is persisted across deploys, so Magistrate will continue to run
22
+ (with an updated config) even after a deploy.
23
+
24
+ Your user-space cron job should look like this:
25
+
26
+ */5 0 0 0 0 magistrate run --config ~/my_app/current/config/magistrate.yml
27
+
28
+ ### What if the server is down?
29
+
30
+ The magistrate request will time out after 30 seconds and then use its previously stored target_states.yml file
31
+
32
+ ## Command line options
33
+
34
+ --config path/to/config.yml
35
+
36
+ Sets the config file path. See example_config.yml for an example config
37
+
38
+ ### run
39
+
40
+ `magistrate run`
41
+
42
+ run is the primary command used. It's intended to be run as a cron job periodically. Each time it's run it'll:
43
+
44
+ * Download the target state for each worker from the server
45
+ * Check each worker
46
+ * Try to get it to its target state
47
+ * POST back to the server its state
48
+
49
+ ### list
50
+
51
+ `magistrate list`
52
+
53
+ Will return a string like:
54
+
55
+ {:test1=>{:state=>:unmonitored, :target_state=>:running}}
56
+
57
+ This is the status string that is sent to the remote server during a run
58
+
59
+ ### start / stop
60
+
61
+ `magistrate start WORKER_NAME`
62
+ `magistrate stop WORKER_NAME`
63
+
64
+ Allows you to manually start/stop a worker process. This has the side effect of writing the new target_state to the cached target state. It will
65
+ then continue to use this requested state UNTIL it next gets a different state from the server.
66
+
67
+ ## License
68
+
69
+ Copyright (C) 2011 by Drew Blas <drew.blas@gmail.com>
70
+
71
+ Permission is hereby granted, free of charge, to any person obtaining a copy
72
+ of this software and associated documentation files (the "Software"), to deal
73
+ in the Software without restriction, including without limitation the rights
74
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
75
+ copies of the Software, and to permit persons to whom the Software is
76
+ furnished to do so, subject to the following conditions:
77
+
78
+ The above copyright notice and this permission notice shall be included in
79
+ all copies or substantial portions of the Software.
80
+
81
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
82
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
83
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
84
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
85
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
86
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
87
+
88
+ ## Attribution
89
+
90
+ Inspiration and thanks to:
91
+
92
+ * foreman
93
+ * resque
94
+ * god
data/bin/magistrate ADDED
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.expand_path("../../lib", __FILE__)
4
+
5
+ require "magistrate"
6
+
7
+ require "optparse"
8
+
9
+ action = :start
10
+ config_file = nil
11
+
12
+ ARGV.options do |opts|
13
+ opts.banner = "Usage: #{File.basename($PROGRAM_NAME)} COMMAND [OPTIONS]"
14
+
15
+ opts.separator "COMMAND: run, list, start WORKER, stop WORKER"
16
+
17
+ opts.separator "Specific Options:"
18
+
19
+ opts.separator "Common Options:"
20
+
21
+ opts.on( "-h", "--help",
22
+ "Show this message." ) do
23
+ puts opts
24
+ exit
25
+ end
26
+
27
+ opts.on( '-c', '--config FILE', String, 'Specify Config file') do |f|
28
+ config_file = f
29
+ end
30
+
31
+ begin
32
+ opts.parse!
33
+ rescue
34
+ puts opts
35
+ exit
36
+ end
37
+ end
38
+
39
+ config_file ||= File.join('config', 'magistrate.yaml')
40
+
41
+ ARGV[0] ||= 'run'
42
+
43
+ Magistrate::Supervisor.new(config_file).send(ARGV[0], ARGV[1])
@@ -0,0 +1,13 @@
1
+ class Hash
2
+ def symbolize_keys!
3
+ keys.each do |key|
4
+ self[(key.to_sym rescue key) || key] = delete(key)
5
+ end
6
+ self
7
+ end
8
+
9
+ def recursive_symbolize_keys!
10
+ symbolize_keys!
11
+ values.select { |v| v.is_a?(Hash) }.each { |h| h.recursive_symbolize_keys! }
12
+ end
13
+ end
@@ -0,0 +1,246 @@
1
+ class Magistrate::Process
2
+
3
+ attr_reader :name, :daemonize, :start_cmd, :stop_cmd, :pid_file, :working_dir, :env
4
+ attr_accessor :target_state, :monitored
5
+
6
+ def initialize(name, options = {})
7
+ @name = name
8
+ @daemonize = options[:daemonize]
9
+ @working_dir = options[:working_dir]
10
+ @start_cmd = options[:start_cmd]
11
+
12
+ if @daemonize
13
+ @pid_file = File.join('tmp', 'pids', "#{@name}.pid")
14
+ @stop_signal = options[:stop_signal] || 'TERM'
15
+ else
16
+ @stop_cmd = options[:end_cmd]
17
+ @pid_file = options[:pid_file]
18
+ end
19
+
20
+ @stop_timeout = 5
21
+ @start_timeout = 5
22
+
23
+ @env = {}
24
+
25
+ @target_state = :unknown
26
+ end
27
+
28
+ def running?
29
+ end
30
+
31
+ def state
32
+ if @target_state == :unmonitored || @target_state == :unknown
33
+ :unmonitored
34
+ else
35
+ if self.alive?
36
+ :running
37
+ else
38
+ :stopped
39
+ end
40
+ end
41
+ end
42
+
43
+ # This is to be called when we first start managing a worker
44
+ # It will check if the pid exists and if so, is the process responding OK?
45
+ # It will take action based on the target state
46
+ def supervise!
47
+ LOGGER.info("#{@name} supervising. Is: #{state}. Target: #{@target_state}")
48
+ if state != @target_state
49
+ if @target_state == :running
50
+ start
51
+ elsif @target_state == :stopped
52
+ stop
53
+ end
54
+ end
55
+ end
56
+
57
+ def start
58
+ LOGGER.info("#{@name} starting")
59
+ if @daemonize
60
+ @pid = double_fork(@start_cmd)
61
+ # TODO: Should check if the pid really exists as we expect
62
+ write_pid
63
+ else
64
+ @pid = single_fork(@start_cmd)
65
+ end
66
+ @pid
67
+ end
68
+
69
+ def stop
70
+ if @daemonize
71
+ signal(@stop_signal, pid)
72
+
73
+ # Poll to see if it's dead
74
+ @stop_timeout.times do
75
+ begin
76
+ ::Process.kill(0, pid)
77
+ rescue Errno::ESRCH
78
+ # It died. Good.
79
+ LOGGER.info("#{@name} process stopped")
80
+ return
81
+ end
82
+
83
+ sleep 1
84
+ end
85
+
86
+ signal('KILL', pid)
87
+ LOGGER.warn("#{@name} still alive after #{@stop_timeout}s; sent SIGKILL")
88
+ else
89
+ single_fork(@stop_cmd)
90
+ ensure_stop
91
+ end
92
+ end
93
+
94
+ # single fork self-daemonizing processes
95
+ # we want to wait for them to finish
96
+ def single_fork(command)
97
+ pid = self.spawn(command)
98
+ status = ::Process.waitpid2(pid, 0)
99
+ exit_code = status[1] >> 8
100
+
101
+ if exit_code != 0
102
+ LOGGER.warn("#{@name} command exited with non-zero code = #{exit_code}")
103
+ end
104
+ pid
105
+ end
106
+
107
+ def double_fork(command)
108
+ pid = nil
109
+ # double fork daemonized processes
110
+ # we don't want to wait for them to finish
111
+ r, w = IO.pipe
112
+ begin
113
+ opid = fork do
114
+ STDOUT.reopen(w)
115
+ r.close
116
+ pid = self.spawn(command)
117
+ puts pid.to_s # send pid back to forker
118
+ end
119
+
120
+ ::Process.waitpid(opid, 0)
121
+ w.close
122
+ pid = r.gets.chomp
123
+ ensure
124
+ # make sure the file descriptors get closed no matter what
125
+ r.close rescue nil
126
+ w.close rescue nil
127
+ end
128
+
129
+ pid
130
+ end
131
+
132
+ # Fork/exec the given command, returns immediately
133
+ # +command+ is the String containing the shell command
134
+ #
135
+ # Returns nothing
136
+ def spawn(command)
137
+ fork do
138
+ ::Process.setsid
139
+
140
+ dir = @working_dir || '/'
141
+ Dir.chdir dir
142
+
143
+ $0 = command
144
+ STDIN.reopen "/dev/null"
145
+
146
+ STDOUT.reopen '/dev/null'
147
+ STDERR.reopen STDOUT
148
+
149
+ # if self.log_cmd
150
+ # STDOUT.reopen IO.popen(self.log_cmd, "a")
151
+ # else
152
+ # STDOUT.reopen file_in_chroot(self.log), "a"
153
+ # end
154
+ #
155
+ # if err_log_cmd
156
+ # STDERR.reopen IO.popen(err_log_cmd, "a")
157
+ # elsif err_log && (log_cmd || err_log != log)
158
+ # STDERR.reopen file_in_chroot(err_log), "a"
159
+ # else
160
+ # STDERR.reopen STDOUT
161
+ # end
162
+
163
+ # close any other file descriptors
164
+ 3.upto(256){|fd| IO::new(fd).close rescue nil}
165
+
166
+ if @env && @env.is_a?(Hash)
167
+ @env.each do |key, value|
168
+ ENV[key] = value.to_s
169
+ end
170
+ end
171
+
172
+ exec command unless command.empty?
173
+ end
174
+ end
175
+
176
+ # Ensure that a stop command actually stops the process. Force kill
177
+ # if necessary.
178
+ #
179
+ # Returns nothing
180
+ def ensure_stop
181
+ LOGGER.warn("#{@name} ensuring stop...")
182
+
183
+ unless self.pid
184
+ LOGGER.warn("#{@name} stop called but pid is uknown")
185
+ return
186
+ end
187
+
188
+ # Poll to see if it's dead
189
+ @stop_timeout.times do
190
+ begin
191
+ signal(0)
192
+ rescue Errno::ESRCH
193
+ # It died. Good.
194
+ return
195
+ end
196
+
197
+ sleep 1
198
+ end
199
+
200
+ # last resort
201
+ signal('KILL')
202
+ LOGGER.warn("#{@name} still alive after #{@stop_timeout}s; sent SIGKILL")
203
+ end
204
+
205
+ # Send the given signal to this process.
206
+ #
207
+ # Returns nothing
208
+ def signal(sig, target_pid = nil)
209
+ target_pid ||= self.pid
210
+ sig = sig.to_i if sig.to_i != 0
211
+ LOGGER.info("#{@name} sending signal '#{sig}' to pid #{self.pid}")
212
+ ::Process.kill(sig, target_pid) rescue nil
213
+ end
214
+
215
+ # Fetch the PID from pid_file. If the pid_file does not
216
+ # exist, then use the PID from the last time it was read.
217
+ # If it has never been read, then return nil.
218
+ #
219
+ # Returns Integer(pid) or nil
220
+ def pid
221
+ contents = File.read(@pid_file).strip rescue ''
222
+ real_pid = contents =~ /^\d+$/ ? contents.to_i : nil
223
+
224
+ if real_pid
225
+ @pid = real_pid
226
+ real_pid
227
+ else
228
+ @pid
229
+ end
230
+ end
231
+
232
+ def write_pid
233
+ File.open(@pid_file, 'w') do |f|
234
+ f.write @pid
235
+ end
236
+ end
237
+
238
+ def alive?
239
+ if p = self.pid
240
+ !!::Process.kill(0, p) rescue false
241
+ else
242
+ false
243
+ end
244
+ end
245
+
246
+ end
@@ -0,0 +1,159 @@
1
+ require 'rubygems'
2
+ # Ugh, yaml for local serialization and json for over-the-wire serialization sucks.
3
+ # But YAML doesn't go over the wire well and json doesn't support comments and makes for really bad local config files
4
+ # Almost makes me want to use XML ;)
5
+ require 'yaml'
6
+ require 'json'
7
+ require 'fileutils'
8
+ require 'net/http'
9
+ require 'uri'
10
+
11
+ module Magistrate
12
+ class Supervisor
13
+ def initialize(config_file)
14
+ @workers = {}
15
+
16
+ #File.expand_path('~')
17
+ @pid_path = File.join( 'tmp', 'pids' )
18
+
19
+ FileUtils.mkdir_p(@pid_path) unless File.directory? @pid_path
20
+
21
+ @config = File.open(config_file) { |file| YAML.load(file) }
22
+ @config.recursive_symbolize_keys!
23
+
24
+ @uri = URI.parse @config[:monitor_url]
25
+
26
+ @config[:workers].each do |k,v|
27
+ @workers[k] = Process.new(k,v)
28
+ end
29
+
30
+ @loaded_from = nil
31
+ end
32
+
33
+ def run(params = nil)
34
+ puts "Starting Magistrate [[[#{self.name}]]] talking to [[[#{@config[:monitor_url]}]]]"
35
+ set_target_states!
36
+
37
+ # Pull in all already-running workers and set their target states
38
+ @workers.each do |k, worker|
39
+ worker.supervise!
40
+ end
41
+
42
+ send_status
43
+ end
44
+
45
+ #
46
+ def start(params = nil)
47
+ worker = params
48
+ puts "Starting: #{worker}"
49
+ @workers[worker.to_sym].supervise!
50
+
51
+ # Save that we've requested this to be started
52
+ end
53
+
54
+ def stop(params = nil)
55
+ worker = params
56
+ puts "Stopping: #{worker}"
57
+ @workers[worker.to_sym].stop
58
+
59
+ # Save that we've requested this to be stopped
60
+ end
61
+
62
+ def list(params = nil)
63
+ set_target_states!
64
+
65
+ require 'pp'
66
+ pp status
67
+ end
68
+
69
+ # Returns the actual hash of all workers and their status
70
+ def status
71
+ s = {}
72
+
73
+ @workers.each do |k,process|
74
+ s[k] = {
75
+ :state => process.state,
76
+ :target_state => process.target_state
77
+ }
78
+ end
79
+
80
+ s
81
+ end
82
+
83
+ protected
84
+
85
+ # Loads the @target_states from either the remote server or local cache
86
+ # Then sets all the worker target_states to the loaded values
87
+ def set_target_states!(local_only = false)
88
+ local_only ? load_saved_target_states! : load_remote_target_states!
89
+
90
+ if @target_states && @target_states['workers']
91
+ @target_states['workers'].each do |name, target|
92
+ name = name.to_sym
93
+ if @workers[name]
94
+ @workers[name].target_state = target['target_state'].to_sym if target['target_state']
95
+ else
96
+ puts "Worker #{name} not found in local config"
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ # Gets and sets @target_states from the server
103
+ # Automatically falls back to the local cache
104
+ def load_remote_target_states!
105
+ http = Net::HTTP.new(@uri.host, @uri.port)
106
+ http.read_timeout = 30
107
+ request = Net::HTTP::Get.new(@uri.request_uri + "api/status/#{self.name}")
108
+
109
+ response = http.request(request)
110
+
111
+ if response.code == '200'
112
+ @loaded_from = :server
113
+ @target_states = JSON.parse(response.body)
114
+ save_target_states! # The double serialization here might not be best for performance, but will guarantee that the locally stored file is internally consistent
115
+ else
116
+ puts "Server responded with error #{response.code} : [[[#{response.body}]]]. Using saved target states..."
117
+ load_saved_target_states!
118
+ end
119
+
120
+ rescue StandardError => e
121
+ puts "Connection to server #{@config[:monitor_url]} failed."
122
+ puts "Error: #{e}"
123
+ puts "Using saved target states..."
124
+ load_saved_target_states!
125
+ end
126
+
127
+ # Loads the @target_states variable from the cache file
128
+ def load_saved_target_states!
129
+ @loaded_from = :file
130
+ @target_states = File.open(target_states_file) { |file| YAML.load(file) }
131
+ end
132
+
133
+ def save_target_states!
134
+ File.open(target_states_file, "w") { |file| YAML.dump(@target_states, file) }
135
+ end
136
+
137
+ def target_states_file
138
+ File.join @pid_path, 'target_states.yml'
139
+ end
140
+
141
+ # Sends the current system status back to the server
142
+ # Currently only sends basic worker info, but could start sending lots more:
143
+ #
144
+ def send_status
145
+ http = Net::HTTP.new(@uri.host, @uri.port)
146
+ http.read_timeout = 30
147
+ request = Net::HTTP::Post.new(@uri.request_uri + "api/status/#{self.name}")
148
+ request.set_form_data({ :status => JSON.generate(status) })
149
+ response = http.request(request)
150
+ rescue StandardError => e
151
+ puts "Sending status to #{@config[:monitor_url]} failed"
152
+ end
153
+
154
+ # This is the name that the magistrate_monitor will identify us as
155
+ def name
156
+ @_name ||= (@config[:supervisor_name_override] || "#{`hostname`.chomp}-#{`whoami`.chomp}").gsub(/[^a-zA-Z0-9\-\_]/, ' ').gsub(/\s+/, '-').downcase
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,3 @@
1
+ module Magistrate
2
+ VERSION = "0.1.0"
3
+ end
data/lib/magistrate.rb ADDED
@@ -0,0 +1,14 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'magistrate/core_ext'
4
+ require 'magistrate/version'
5
+ require 'magistrate/supervisor'
6
+ require 'magistrate/process'
7
+
8
+ require 'logger'
9
+ # App wide logging system
10
+ LOGGER = Logger.new(STDOUT)
11
+
12
+ module Magistrate
13
+
14
+ end
@@ -0,0 +1,42 @@
1
+ require "spec_helper"
2
+ require "magistrate/process"
3
+
4
+ describe "Magistrate::Process" do
5
+ describe 'Rake-Like Worker' do
6
+ before(:each) do
7
+ @process = Magistrate::Process.new(
8
+ :name => 'rake_like_worker',
9
+ :daemonize => true,
10
+ :start_cmd => 'ruby spec/resources/rake_like_worker.rb'
11
+ )
12
+
13
+ stub(@process).spawn do
14
+ raise "Unexpected spawn call made...you don't want your specs actually spawning stuff, right?"
15
+ end
16
+ end
17
+
18
+ describe 'state' do
19
+ it 'should be unmonitored by default' do
20
+ @process.state.should == :unmonitored
21
+ end
22
+
23
+ it 'should be unmonitored when unmonitored is the target state' do
24
+ @process.target_state = :unmonitored
25
+ @process.state.should == :unmonitored
26
+ end
27
+
28
+ it 'should be stopped when target state other that unmonitored' do
29
+ @process.target_state = :foo
30
+ @process.state.should == :stopped
31
+ end
32
+ end
33
+
34
+ it "should " do
35
+ true
36
+ end
37
+ end
38
+
39
+ describe 'Self-Daemonizing Worker' do
40
+
41
+ end
42
+ end
@@ -0,0 +1,19 @@
1
+ require "spec_helper"
2
+ require "magistrate/supervisor"
3
+
4
+ describe "Magistrate::Supervisor" do
5
+ before(:each) do
6
+ @supervisor = Magistrate::Supervisor.new("spec/resources/example.yml")
7
+ end
8
+
9
+ it "should initialize correctly" do
10
+ lambda { Magistrate::Supervisor.new("spec/resources/example.yml") }.should_not raise_error
11
+ end
12
+
13
+ it "should show basic status for its workers" do
14
+ @supervisor.status.should == { :daemon_worker=>{:state=>:unmonitored, :target_state=>:unknown},
15
+ :rake_like_worker=>{:state=>:unmonitored, :target_state=>:unknown} }
16
+ end
17
+
18
+ # it 'should'
19
+ end
@@ -0,0 +1,11 @@
1
+ require "spec_helper"
2
+ require "magistrate"
3
+
4
+ describe Magistrate do
5
+
6
+ describe "VERSION" do
7
+ subject { Magistrate::VERSION }
8
+ it { should be_a String }
9
+ end
10
+
11
+ end
@@ -0,0 +1,23 @@
1
+ monitor_url: http://localhost
2
+
3
+ #Normal magistrate reports itself as: `hostname`-`user`
4
+ #supervisor_name_override: some_other_name
5
+
6
+ workers:
7
+ # If daemonize is true, then Magistrate will daemonize this process (it doesn't daemonize itself)
8
+ # Magistrate will track the pid of the underlying process
9
+ # And will stop it by killing the pid
10
+ # It will ping the status by sending USR1 signal to the process
11
+ rake_like_worker:
12
+ daemonize: true
13
+ working_dir: /data/app/
14
+ start_cmd: rake my:task RAILS_ENV=production
15
+
16
+ # If daemonize is false, then Magistrate will use a separate start and stop command
17
+ # You must also manually specify the pid that this daemonized process creates
18
+ daemon_worker:
19
+ daemonize: false
20
+ working_dir: /data/app/
21
+ start_cmd: mongrel_rails start -d
22
+ stop_cmd: mongrel_rails stop
23
+ pid_file: tmp/pids/mongrel.pid
@@ -0,0 +1,10 @@
1
+ trap('USR1') {
2
+ puts 'Doing good'
3
+ }
4
+
5
+ while true do
6
+ puts '1'
7
+ sleep(3)
8
+ end
9
+
10
+
@@ -0,0 +1,43 @@
1
+ require "rubygems"
2
+ require "rspec"
3
+ #require "fakefs/safe"
4
+ #require "fakefs/spec_helpers"
5
+
6
+ $:.unshift File.expand_path("../../lib", __FILE__)
7
+
8
+ def mock_error(subject, message)
9
+ mock_exit do
10
+ mock(subject).puts("ERROR: #{message}")
11
+ yield
12
+ end
13
+ end
14
+
15
+ def mock_exit(&block)
16
+ block.should raise_error(SystemExit)
17
+ end
18
+ #
19
+ # def load_export_templates_into_fakefs(type)
20
+ # FakeFS.deactivate!
21
+ # files = Dir[File.expand_path("../../data/export/#{type}/**", __FILE__)].inject({}) do |hash, file|
22
+ # hash.update(file => File.read(file))
23
+ # end
24
+ # FakeFS.activate!
25
+ # files.each do |filename, contents|
26
+ # File.open(filename, "w") do |f|
27
+ # f.puts contents
28
+ # end
29
+ # end
30
+ # end
31
+ #
32
+ # def example_export_file(filename)
33
+ # FakeFS.deactivate!
34
+ # data = File.read(File.expand_path("../resources/export/#{filename}", __FILE__))
35
+ # FakeFS.activate!
36
+ # data
37
+ # end
38
+
39
+ RSpec.configure do |config|
40
+ config.color_enabled = true
41
+ #config.include FakeFS::SpecHelpers
42
+ config.mock_with :rr
43
+ end
metadata ADDED
@@ -0,0 +1,200 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: magistrate
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Drew Blas
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-08-16 00:00:00 -05:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: json
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: rake
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :development
48
+ version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: ronn
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ hash: 3
58
+ segments:
59
+ - 0
60
+ version: "0"
61
+ type: :development
62
+ version_requirements: *id003
63
+ - !ruby/object:Gem::Dependency
64
+ name: fakefs
65
+ prerelease: false
66
+ requirement: &id004 !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ~>
70
+ - !ruby/object:Gem::Version
71
+ hash: 21
72
+ segments:
73
+ - 0
74
+ - 2
75
+ - 1
76
+ version: 0.2.1
77
+ type: :development
78
+ version_requirements: *id004
79
+ - !ruby/object:Gem::Dependency
80
+ name: rcov
81
+ prerelease: false
82
+ requirement: &id005 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ~>
86
+ - !ruby/object:Gem::Version
87
+ hash: 43
88
+ segments:
89
+ - 0
90
+ - 9
91
+ - 8
92
+ version: 0.9.8
93
+ type: :development
94
+ version_requirements: *id005
95
+ - !ruby/object:Gem::Dependency
96
+ name: rr
97
+ prerelease: false
98
+ requirement: &id006 !ruby/object:Gem::Requirement
99
+ none: false
100
+ requirements:
101
+ - - ~>
102
+ - !ruby/object:Gem::Version
103
+ hash: 19
104
+ segments:
105
+ - 1
106
+ - 0
107
+ - 2
108
+ version: 1.0.2
109
+ type: :development
110
+ version_requirements: *id006
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec
113
+ prerelease: false
114
+ requirement: &id007 !ruby/object:Gem::Requirement
115
+ none: false
116
+ requirements:
117
+ - - ~>
118
+ - !ruby/object:Gem::Version
119
+ hash: 23
120
+ segments:
121
+ - 2
122
+ - 6
123
+ - 0
124
+ version: 2.6.0
125
+ type: :development
126
+ version_requirements: *id007
127
+ - !ruby/object:Gem::Dependency
128
+ name: webmock
129
+ prerelease: false
130
+ requirement: &id008 !ruby/object:Gem::Requirement
131
+ none: false
132
+ requirements:
133
+ - - ~>
134
+ - !ruby/object:Gem::Version
135
+ hash: 7
136
+ segments:
137
+ - 1
138
+ - 6
139
+ - 4
140
+ version: 1.6.4
141
+ type: :development
142
+ version_requirements: *id008
143
+ description: Cluster-based process / worker manager
144
+ email: drew.blas@gmail.com
145
+ executables:
146
+ - magistrate
147
+ extensions: []
148
+
149
+ extra_rdoc_files: []
150
+
151
+ files:
152
+ - bin/magistrate
153
+ - lib/magistrate/core_ext.rb
154
+ - lib/magistrate/process.rb
155
+ - lib/magistrate/supervisor.rb
156
+ - lib/magistrate/version.rb
157
+ - lib/magistrate.rb
158
+ - README.md
159
+ - spec/magistrate/process_spec.rb
160
+ - spec/magistrate/supervisor_spec.rb
161
+ - spec/magistrate_spec.rb
162
+ - spec/resources/example.yml
163
+ - spec/resources/rake_like_worker.rb
164
+ - spec/spec_helper.rb
165
+ has_rdoc: true
166
+ homepage: http://github.com/drewblas/magistrate
167
+ licenses: []
168
+
169
+ post_install_message:
170
+ rdoc_options: []
171
+
172
+ require_paths:
173
+ - lib
174
+ required_ruby_version: !ruby/object:Gem::Requirement
175
+ none: false
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ hash: 3
180
+ segments:
181
+ - 0
182
+ version: "0"
183
+ required_rubygems_version: !ruby/object:Gem::Requirement
184
+ none: false
185
+ requirements:
186
+ - - ">="
187
+ - !ruby/object:Gem::Version
188
+ hash: 3
189
+ segments:
190
+ - 0
191
+ version: "0"
192
+ requirements: []
193
+
194
+ rubyforge_project:
195
+ rubygems_version: 1.6.2
196
+ signing_key:
197
+ specification_version: 3
198
+ summary: Cluster-based process / worker manager
199
+ test_files: []
200
+