magistrate 0.1.0 → 0.2.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/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
|