magistrate 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/magistrate +6 -1
- data/lib/magistrate/process.rb +31 -13
- data/lib/magistrate/supervisor.rb +61 -30
- data/lib/magistrate/version.rb +1 -1
- data/lib/magistrate.rb +0 -4
- data/spec/magistrate/process_spec.rb +0 -4
- data/spec/magistrate/supervisor_spec.rb +21 -3
- data/spec/resources/example.yml +12 -5
- data/spec/spec_helper.rb +6 -0
- metadata +4 -4
data/bin/magistrate
CHANGED
@@ -8,6 +8,7 @@ require "optparse"
|
|
8
8
|
|
9
9
|
action = :start
|
10
10
|
config_file = nil
|
11
|
+
verbose = false
|
11
12
|
|
12
13
|
ARGV.options do |opts|
|
13
14
|
opts.banner = "Usage: #{File.basename($PROGRAM_NAME)} COMMAND [OPTIONS]"
|
@@ -28,6 +29,10 @@ ARGV.options do |opts|
|
|
28
29
|
config_file = f
|
29
30
|
end
|
30
31
|
|
32
|
+
opts.on( '-v', '--verbose') do
|
33
|
+
verbose = true
|
34
|
+
end
|
35
|
+
|
31
36
|
begin
|
32
37
|
opts.parse!
|
33
38
|
rescue
|
@@ -40,4 +45,4 @@ config_file ||= File.join('config', 'magistrate.yaml')
|
|
40
45
|
|
41
46
|
ARGV[0] ||= 'run'
|
42
47
|
|
43
|
-
Magistrate::Supervisor.new(config_file).send(ARGV[0], ARGV[1])
|
48
|
+
Magistrate::Supervisor.new(config_file, :verbose => verbose).send(ARGV[0], ARGV[1])
|
data/lib/magistrate/process.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
-
|
1
|
+
module Magistrate
|
2
|
+
class Process
|
2
3
|
|
3
|
-
attr_reader :name, :daemonize, :start_cmd, :stop_cmd, :pid_file, :working_dir, :env
|
4
|
+
attr_reader :name, :daemonize, :start_cmd, :stop_cmd, :pid_file, :working_dir, :env, :logs
|
4
5
|
attr_accessor :target_state, :monitored
|
5
6
|
|
6
7
|
def initialize(name, options = {})
|
@@ -8,9 +9,10 @@ class Magistrate::Process
|
|
8
9
|
@daemonize = options[:daemonize]
|
9
10
|
@working_dir = options[:working_dir]
|
10
11
|
@start_cmd = options[:start_cmd]
|
12
|
+
@pid_path = options[:pid_path]
|
11
13
|
|
12
14
|
if @daemonize
|
13
|
-
@pid_file = File.join(
|
15
|
+
@pid_file = File.join(@pid_path, "#{@name}.pid")
|
14
16
|
@stop_signal = options[:stop_signal] || 'TERM'
|
15
17
|
else
|
16
18
|
@stop_cmd = options[:end_cmd]
|
@@ -23,6 +25,20 @@ class Magistrate::Process
|
|
23
25
|
@env = {}
|
24
26
|
|
25
27
|
@target_state = :unknown
|
28
|
+
@logs = []
|
29
|
+
end
|
30
|
+
|
31
|
+
def log(str)
|
32
|
+
@logs << str
|
33
|
+
end
|
34
|
+
|
35
|
+
def status
|
36
|
+
{
|
37
|
+
:state => self.state,
|
38
|
+
:target_state => self.target_state,
|
39
|
+
:pid => self.pid,
|
40
|
+
:logs => @logs
|
41
|
+
}
|
26
42
|
end
|
27
43
|
|
28
44
|
def running?
|
@@ -44,7 +60,7 @@ class Magistrate::Process
|
|
44
60
|
# It will check if the pid exists and if so, is the process responding OK?
|
45
61
|
# It will take action based on the target state
|
46
62
|
def supervise!
|
47
|
-
|
63
|
+
log "Supervising. Is: #{state}. Target: #{@target_state}"
|
48
64
|
if state != @target_state
|
49
65
|
if @target_state == :running
|
50
66
|
start
|
@@ -55,7 +71,7 @@ class Magistrate::Process
|
|
55
71
|
end
|
56
72
|
|
57
73
|
def start
|
58
|
-
|
74
|
+
log "#{@name} starting"
|
59
75
|
if @daemonize
|
60
76
|
@pid = double_fork(@start_cmd)
|
61
77
|
# TODO: Should check if the pid really exists as we expect
|
@@ -76,7 +92,7 @@ class Magistrate::Process
|
|
76
92
|
::Process.kill(0, pid)
|
77
93
|
rescue Errno::ESRCH
|
78
94
|
# It died. Good.
|
79
|
-
|
95
|
+
log "Process stopped"
|
80
96
|
return
|
81
97
|
end
|
82
98
|
|
@@ -84,7 +100,7 @@ class Magistrate::Process
|
|
84
100
|
end
|
85
101
|
|
86
102
|
signal('KILL', pid)
|
87
|
-
|
103
|
+
log "Still alive after #{@stop_timeout}s; sent SIGKILL"
|
88
104
|
else
|
89
105
|
single_fork(@stop_cmd)
|
90
106
|
ensure_stop
|
@@ -99,7 +115,7 @@ class Magistrate::Process
|
|
99
115
|
exit_code = status[1] >> 8
|
100
116
|
|
101
117
|
if exit_code != 0
|
102
|
-
|
118
|
+
log "Command exited with non-zero code = #{exit_code}"
|
103
119
|
end
|
104
120
|
pid
|
105
121
|
end
|
@@ -140,7 +156,8 @@ class Magistrate::Process
|
|
140
156
|
dir = @working_dir || '/'
|
141
157
|
Dir.chdir dir
|
142
158
|
|
143
|
-
|
159
|
+
#$0 = command
|
160
|
+
$0 = "Magistrate Worker: #{@name}"
|
144
161
|
STDIN.reopen "/dev/null"
|
145
162
|
|
146
163
|
STDOUT.reopen '/dev/null'
|
@@ -178,10 +195,10 @@ class Magistrate::Process
|
|
178
195
|
#
|
179
196
|
# Returns nothing
|
180
197
|
def ensure_stop
|
181
|
-
|
198
|
+
log "Ensuring stop..."
|
182
199
|
|
183
200
|
unless self.pid
|
184
|
-
|
201
|
+
log "Stop called but pid is unknown"
|
185
202
|
return
|
186
203
|
end
|
187
204
|
|
@@ -199,7 +216,7 @@ class Magistrate::Process
|
|
199
216
|
|
200
217
|
# last resort
|
201
218
|
signal('KILL')
|
202
|
-
|
219
|
+
log "Still alive after #{@stop_timeout}s; sent SIGKILL"
|
203
220
|
end
|
204
221
|
|
205
222
|
# Send the given signal to this process.
|
@@ -208,7 +225,7 @@ class Magistrate::Process
|
|
208
225
|
def signal(sig, target_pid = nil)
|
209
226
|
target_pid ||= self.pid
|
210
227
|
sig = sig.to_i if sig.to_i != 0
|
211
|
-
|
228
|
+
log "Sending signal '#{sig}' to pid #{target_pid}"
|
212
229
|
::Process.kill(sig, target_pid) rescue nil
|
213
230
|
end
|
214
231
|
|
@@ -244,3 +261,4 @@ class Magistrate::Process
|
|
244
261
|
end
|
245
262
|
|
246
263
|
end
|
264
|
+
end
|
@@ -10,42 +10,57 @@ require 'uri'
|
|
10
10
|
|
11
11
|
module Magistrate
|
12
12
|
class Supervisor
|
13
|
-
def initialize(config_file)
|
13
|
+
def initialize(config_file, overrides = {})
|
14
14
|
@workers = {}
|
15
15
|
|
16
16
|
#File.expand_path('~')
|
17
|
-
@
|
18
|
-
|
19
|
-
FileUtils.mkdir_p(@pid_path) unless File.directory? @pid_path
|
20
|
-
|
17
|
+
@config_file = config_file
|
21
18
|
@config = File.open(config_file) { |file| YAML.load(file) }
|
22
19
|
@config.recursive_symbolize_keys!
|
23
20
|
|
24
21
|
@uri = URI.parse @config[:monitor_url]
|
22
|
+
@pid_path = @config[:pid_path] || File.join( 'tmp', 'pids' )
|
23
|
+
|
24
|
+
FileUtils.mkdir_p(@pid_path) unless File.directory? @pid_path
|
25
25
|
|
26
26
|
@config[:workers].each do |k,v|
|
27
|
+
v[:pid_path] ||= @pid_path
|
27
28
|
@workers[k] = Process.new(k,v)
|
28
29
|
end
|
29
30
|
|
30
31
|
@loaded_from = nil
|
32
|
+
@logs = []
|
33
|
+
@verbose = overrides[:verbose]
|
34
|
+
|
35
|
+
if @verbose
|
36
|
+
require 'pp'
|
37
|
+
end
|
31
38
|
end
|
32
39
|
|
33
40
|
def run(params = nil)
|
34
|
-
|
41
|
+
log "Run started at: #{Time.now}"
|
42
|
+
|
43
|
+
log "Starting Magistrate [[[#{self.name}]]] talking to [[[#{@config[:monitor_url]}]]]"
|
35
44
|
set_target_states!
|
36
45
|
|
37
46
|
# Pull in all already-running workers and set their target states
|
38
47
|
@workers.each do |k, worker|
|
39
48
|
worker.supervise!
|
49
|
+
if @verbose
|
50
|
+
puts "==== Worker: #{k}"
|
51
|
+
worker.logs.join("\n")
|
52
|
+
end
|
40
53
|
end
|
41
54
|
|
42
55
|
send_status
|
56
|
+
|
57
|
+
log "Run Complete at: #{Time.now}" #This is only good in verbose mode, but that's ok
|
43
58
|
end
|
44
59
|
|
45
60
|
#
|
46
61
|
def start(params = nil)
|
47
62
|
worker = params
|
48
|
-
|
63
|
+
log "Starting: #{worker}"
|
49
64
|
@workers[worker.to_sym].supervise!
|
50
65
|
|
51
66
|
# Save that we've requested this to be started
|
@@ -53,7 +68,7 @@ module Magistrate
|
|
53
68
|
|
54
69
|
def stop(params = nil)
|
55
70
|
worker = params
|
56
|
-
|
71
|
+
log "Stopping: #{worker}"
|
57
72
|
@workers[worker.to_sym].stop
|
58
73
|
|
59
74
|
# Save that we've requested this to be stopped
|
@@ -68,18 +83,27 @@ module Magistrate
|
|
68
83
|
|
69
84
|
# Returns the actual hash of all workers and their status
|
70
85
|
def status
|
71
|
-
s = {
|
86
|
+
s = {
|
87
|
+
:name => self.name,
|
88
|
+
:pid_path => @pid_path,
|
89
|
+
:monitor_url => @config[:monitor_url],
|
90
|
+
:config_file => @config_file,
|
91
|
+
:logs => @logs,
|
92
|
+
:workers => {}
|
93
|
+
}
|
72
94
|
|
73
95
|
@workers.each do |k,process|
|
74
|
-
s[k] =
|
75
|
-
:state => process.state,
|
76
|
-
:target_state => process.target_state
|
77
|
-
}
|
96
|
+
s[:workers][k] = process.status
|
78
97
|
end
|
79
98
|
|
80
99
|
s
|
81
100
|
end
|
82
101
|
|
102
|
+
def log(str)
|
103
|
+
@logs << str
|
104
|
+
puts str if @verbose
|
105
|
+
end
|
106
|
+
|
83
107
|
protected
|
84
108
|
|
85
109
|
# Loads the @target_states from either the remote server or local cache
|
@@ -93,7 +117,7 @@ module Magistrate
|
|
93
117
|
if @workers[name]
|
94
118
|
@workers[name].target_state = target['target_state'].to_sym if target['target_state']
|
95
119
|
else
|
96
|
-
|
120
|
+
log "Worker #{name} has an entry in the target_state but it's not listed in the local config file and will be ignored."
|
97
121
|
end
|
98
122
|
end
|
99
123
|
end
|
@@ -102,25 +126,21 @@ module Magistrate
|
|
102
126
|
# Gets and sets @target_states from the server
|
103
127
|
# Automatically falls back to the local cache
|
104
128
|
def load_remote_target_states!
|
105
|
-
|
106
|
-
http.read_timeout = 30
|
107
|
-
request = Net::HTTP::Get.new(@uri.request_uri + "api/status/#{self.name}")
|
108
|
-
|
109
|
-
response = http.request(request)
|
129
|
+
response = remote_request(Net::HTTP::Get)
|
110
130
|
|
111
131
|
if response.code == '200'
|
112
132
|
@loaded_from = :server
|
113
133
|
@target_states = JSON.parse(response.body)
|
114
134
|
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
135
|
else
|
116
|
-
|
136
|
+
log "Server responded with error #{response.code} : [[[#{response.body}]]]. Using saved target states..."
|
117
137
|
load_saved_target_states!
|
118
138
|
end
|
119
139
|
|
120
140
|
rescue StandardError => e
|
121
|
-
|
122
|
-
|
123
|
-
|
141
|
+
log "Connection to server #{@config[:monitor_url]} failed."
|
142
|
+
log "Error: #{e}"
|
143
|
+
log "Using saved target states..."
|
124
144
|
load_saved_target_states!
|
125
145
|
end
|
126
146
|
|
@@ -142,18 +162,29 @@ module Magistrate
|
|
142
162
|
# Currently only sends basic worker info, but could start sending lots more:
|
143
163
|
#
|
144
164
|
def send_status
|
145
|
-
|
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)
|
165
|
+
remote_request Net::HTTP::Post, { :status => JSON.generate(status) }
|
150
166
|
rescue StandardError => e
|
151
|
-
|
167
|
+
log "Sending status to #{@config[:monitor_url]} failed"
|
168
|
+
log "Error: #{e}"
|
152
169
|
end
|
153
170
|
|
154
171
|
# This is the name that the magistrate_monitor will identify us as
|
155
172
|
def name
|
156
|
-
@_name ||= (@config[:supervisor_name_override] || "#{
|
173
|
+
@_name ||= (@config[:supervisor_name_override] || "#{@config[:root_name]}-#{`hostname`.chomp}").gsub(/[^a-zA-Z0-9\-\_]/, ' ').gsub(/\s+/, '-').downcase
|
174
|
+
end
|
175
|
+
|
176
|
+
# Wrapper method for easy remote requests
|
177
|
+
def remote_request(klass, form_data = nil)
|
178
|
+
http = Net::HTTP.new(@uri.host, @uri.port)
|
179
|
+
http.read_timeout = 30
|
180
|
+
request = klass.new(@uri.request_uri + "api/status/#{self.name}")
|
181
|
+
request.set_form_data(form_data) if form_data
|
182
|
+
|
183
|
+
if @config[:http_username] && @config[:http_password]
|
184
|
+
request.basic_auth @config[:http_username], @config[:http_password]
|
185
|
+
end
|
186
|
+
|
187
|
+
http.request(request)
|
157
188
|
end
|
158
189
|
end
|
159
190
|
end
|
data/lib/magistrate/version.rb
CHANGED
data/lib/magistrate.rb
CHANGED
@@ -11,9 +11,27 @@ describe "Magistrate::Supervisor" do
|
|
11
11
|
end
|
12
12
|
|
13
13
|
it "should show basic status for its workers" do
|
14
|
-
@supervisor.status
|
15
|
-
|
14
|
+
s = @supervisor.status
|
15
|
+
|
16
|
+
s[:name].should == 'test_name'
|
17
|
+
s[:pid_path].should == File.join('tmp','pids')
|
18
|
+
|
16
19
|
end
|
17
20
|
|
18
|
-
|
21
|
+
it 'should run successfully' do
|
22
|
+
body = {}
|
23
|
+
body = JSON.generate(body)
|
24
|
+
stub_request(:get, "http://localhost:3000/magistrate/api/status/test_name").
|
25
|
+
to_return(:status => 200, :body => body, :headers => {})
|
26
|
+
|
27
|
+
stub_request(:post, "http://localhost:3000/magistrate/api/status/test_name").
|
28
|
+
to_return(:status => 200, :body => 'OK', :headers => {})
|
29
|
+
|
30
|
+
|
31
|
+
|
32
|
+
@supervisor.run
|
33
|
+
|
34
|
+
a_request(:get, "http://localhost:3000/magistrate/api/status/test_name").should have_been_made
|
35
|
+
a_request(:post, "http://localhost:3000/magistrate/api/status/test_name").should have_been_made
|
36
|
+
end
|
19
37
|
end
|
data/spec/resources/example.yml
CHANGED
@@ -1,21 +1,28 @@
|
|
1
|
-
|
1
|
+
# Should have a trailing slash
|
2
|
+
monitor_url: http://localhost:3000/magistrate/
|
2
3
|
|
3
|
-
#
|
4
|
-
#
|
4
|
+
# By default, if this isn't set, it'll use tmp/pids
|
5
|
+
# pid_path: /var/www/app/current/tmp/pids
|
6
|
+
|
7
|
+
#Normal magistrate reports itself as: root_name-`hostname`
|
8
|
+
root_name: super1
|
9
|
+
|
10
|
+
#Use this to avoid using the hostname at all. The supervisor name will be this instead of root_name-`hostname`
|
11
|
+
supervisor_name_override: test_name
|
5
12
|
|
6
13
|
workers:
|
7
14
|
# If daemonize is true, then Magistrate will daemonize this process (it doesn't daemonize itself)
|
8
15
|
# Magistrate will track the pid of the underlying process
|
9
16
|
# And will stop it by killing the pid
|
10
17
|
# It will ping the status by sending USR1 signal to the process
|
11
|
-
|
18
|
+
rake_like_worker1:
|
12
19
|
daemonize: true
|
13
20
|
working_dir: /data/app/
|
14
21
|
start_cmd: rake my:task RAILS_ENV=production
|
15
22
|
|
16
23
|
# If daemonize is false, then Magistrate will use a separate start and stop command
|
17
24
|
# You must also manually specify the pid that this daemonized process creates
|
18
|
-
|
25
|
+
daemon_worker1:
|
19
26
|
daemonize: false
|
20
27
|
working_dir: /data/app/
|
21
28
|
start_cmd: mongrel_rails start -d
|
data/spec/spec_helper.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require "rubygems"
|
2
|
+
require 'webmock/rspec'
|
2
3
|
require "rspec"
|
3
4
|
#require "fakefs/safe"
|
4
5
|
#require "fakefs/spec_helpers"
|
@@ -40,4 +41,9 @@ RSpec.configure do |config|
|
|
40
41
|
config.color_enabled = true
|
41
42
|
#config.include FakeFS::SpecHelpers
|
42
43
|
config.mock_with :rr
|
44
|
+
|
45
|
+
config.before(:suite) do
|
46
|
+
f = 'tmp/pids/target_states.yml'
|
47
|
+
File.delete(f) if File.exist?(f)
|
48
|
+
end
|
43
49
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: magistrate
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 23
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
8
|
+
- 2
|
9
9
|
- 0
|
10
|
-
version: 0.
|
10
|
+
version: 0.2.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Drew Blas
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-08-
|
18
|
+
date: 2011-08-22 00:00:00 -05:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|