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 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])
@@ -1,6 +1,7 @@
1
- class Magistrate::Process
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('tmp', 'pids', "#{@name}.pid")
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
- LOGGER.info("#{@name} supervising. Is: #{state}. Target: #{@target_state}")
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
- LOGGER.info("#{@name} starting")
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
- LOGGER.info("#{@name} process stopped")
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
- LOGGER.warn("#{@name} still alive after #{@stop_timeout}s; sent SIGKILL")
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
- LOGGER.warn("#{@name} command exited with non-zero code = #{exit_code}")
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
- $0 = command
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
- LOGGER.warn("#{@name} ensuring stop...")
198
+ log "Ensuring stop..."
182
199
 
183
200
  unless self.pid
184
- LOGGER.warn("#{@name} stop called but pid is uknown")
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
- LOGGER.warn("#{@name} still alive after #{@stop_timeout}s; sent SIGKILL")
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
- LOGGER.info("#{@name} sending signal '#{sig}' to pid #{self.pid}")
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
- @pid_path = File.join( 'tmp', 'pids' )
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
- puts "Starting Magistrate [[[#{self.name}]]] talking to [[[#{@config[:monitor_url]}]]]"
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
- puts "Starting: #{worker}"
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
- puts "Stopping: #{worker}"
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
- puts "Worker #{name} not found in local config"
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
- 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)
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
- puts "Server responded with error #{response.code} : [[[#{response.body}]]]. Using saved target states..."
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
- puts "Connection to server #{@config[:monitor_url]} failed."
122
- puts "Error: #{e}"
123
- puts "Using saved target states..."
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
- 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)
165
+ remote_request Net::HTTP::Post, { :status => JSON.generate(status) }
150
166
  rescue StandardError => e
151
- puts "Sending status to #{@config[:monitor_url]} failed"
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] || "#{`hostname`.chomp}-#{`whoami`.chomp}").gsub(/[^a-zA-Z0-9\-\_]/, ' ').gsub(/\s+/, '-').downcase
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
@@ -1,3 +1,3 @@
1
1
  module Magistrate
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/magistrate.rb CHANGED
@@ -5,10 +5,6 @@ require 'magistrate/version'
5
5
  require 'magistrate/supervisor'
6
6
  require 'magistrate/process'
7
7
 
8
- require 'logger'
9
- # App wide logging system
10
- LOGGER = Logger.new(STDOUT)
11
-
12
8
  module Magistrate
13
9
 
14
10
  end
@@ -30,10 +30,6 @@ describe "Magistrate::Process" do
30
30
  @process.state.should == :stopped
31
31
  end
32
32
  end
33
-
34
- it "should " do
35
- true
36
- end
37
33
  end
38
34
 
39
35
  describe 'Self-Daemonizing Worker' do
@@ -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.should == { :daemon_worker=>{:state=>:unmonitored, :target_state=>:unknown},
15
- :rake_like_worker=>{:state=>:unmonitored, :target_state=>:unknown} }
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
- # it 'should'
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
@@ -1,21 +1,28 @@
1
- monitor_url: http://localhost
1
+ # Should have a trailing slash
2
+ monitor_url: http://localhost:3000/magistrate/
2
3
 
3
- #Normal magistrate reports itself as: `hostname`-`user`
4
- #supervisor_name_override: some_other_name
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
- rake_like_worker:
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
- daemon_worker:
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: 27
4
+ hash: 23
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 1
8
+ - 2
9
9
  - 0
10
- version: 0.1.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-16 00:00:00 -05:00
18
+ date: 2011-08-22 00:00:00 -05:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency