magistrate 0.1.0
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.
- data/README.md +94 -0
- data/bin/magistrate +43 -0
- data/lib/magistrate/core_ext.rb +13 -0
- data/lib/magistrate/process.rb +246 -0
- data/lib/magistrate/supervisor.rb +159 -0
- data/lib/magistrate/version.rb +3 -0
- data/lib/magistrate.rb +14 -0
- data/spec/magistrate/process_spec.rb +42 -0
- data/spec/magistrate/supervisor_spec.rb +19 -0
- data/spec/magistrate_spec.rb +11 -0
- data/spec/resources/example.yml +23 -0
- data/spec/resources/rake_like_worker.rb +10 -0
- data/spec/spec_helper.rb +43 -0
- metadata +200 -0
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
|
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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
+
|