red_unicorn 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +2 -0
- data/README.rdoc +43 -0
- data/bin/red_unicorn +115 -0
- data/lib/red_unicorn/unicorn.rb +180 -0
- data/lib/red_unicorn/version.rb +17 -0
- data/lib/red_unicorn.rb +1 -0
- metadata +86 -0
data/CHANGELOG.rdoc
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
== RedUnicorn
|
2
|
+
|
3
|
+
RedUnicorn provides easy interaction with unicorn processes. It uses signals
|
4
|
+
to communicate with unicorn to check its status, stop the processes or to
|
5
|
+
provide zero downtime restarts.
|
6
|
+
|
7
|
+
=== Usage
|
8
|
+
|
9
|
+
$ red_unicorn --help
|
10
|
+
RedUnicorn::Unicorn: Unicorn process interactions
|
11
|
+
Usage: red_unicorn [opts] (prolicide | status | slaughter | start | restart | reload | stop | birth)
|
12
|
+
-h, --help Show this help screen
|
13
|
+
-p, --pid /path/to/file.pid Specify path to PID file (default: /var/run/unicorn/unicorn.pid)
|
14
|
+
-x, --unicorn-exec /path/to/unicorn Specify path to unicorn executable (default: /var/www/shared/bundle/bin/unicorn_rails)
|
15
|
+
-c, --unicorn-config /path/to/config Specify path to unicorn configuration file (default: /etc/unicorn/app.rb)
|
16
|
+
-t, --timeout 30 Specify timeout for running actions
|
17
|
+
-e, --env production Specify environment (default: production)
|
18
|
+
Commands:
|
19
|
+
prolicide: Kill single worker process
|
20
|
+
status: Returns current unicorn status
|
21
|
+
slaughter: Kill all worker processes (master remains)
|
22
|
+
start: Start unicorn
|
23
|
+
restart: Gracefully restart unicorn (zero downtime)
|
24
|
+
reload: Reload unicorn configuration
|
25
|
+
stop: Stop unicorn
|
26
|
+
birth: Create new worker process
|
27
|
+
|
28
|
+
=== Zero downtime restarts
|
29
|
+
|
30
|
+
RedUnicorn does its best to provide true zero downtime restarts. After sending
|
31
|
+
unicorn the restart signal, it will wait until the new unicorn process has forked
|
32
|
+
out workers before killing the original unicorn process. If no worker processes
|
33
|
+
are started, the old unicorn process will be left running.
|
34
|
+
|
35
|
+
== Bugs and Feature requests
|
36
|
+
|
37
|
+
* Fork, make updates and send pull request
|
38
|
+
* Report bugs through github's {issues}[http://github.com/chrisroberts/red_unicorn/issues]
|
39
|
+
|
40
|
+
== License
|
41
|
+
|
42
|
+
Copyright (c) 2011 Chris Roberts <chrisroberts.code@gmail.com>
|
43
|
+
Licensed under the MIT license (see LICENSE)
|
data/bin/red_unicorn
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'getoptlong'
|
5
|
+
|
6
|
+
require 'red_unicorn'
|
7
|
+
require 'red_unicorn/unicorn'
|
8
|
+
|
9
|
+
opts = GetoptLong.new(
|
10
|
+
['--pid-file', '-p', GetoptLong::REQUIRED_ARGUMENT],
|
11
|
+
['--unicorn-exec', '-x', GetoptLong::REQUIRED_ARGUMENT],
|
12
|
+
['--unicorn-config', '-c', GetoptLong::REQUIRED_ARGUMENT],
|
13
|
+
['--timeout', '-t', GetoptLong::REQUIRED_ARGUMENT],
|
14
|
+
['--help', '-h', GetoptLong::NO_ARGUMENT],
|
15
|
+
['--env', '-e', GetoptLong::REQUIRED_ARGUMENT]
|
16
|
+
)
|
17
|
+
|
18
|
+
ALLOWED_ACTIONS = {
|
19
|
+
:start => 'Start unicorn',
|
20
|
+
:stop => 'Stop unicorn',
|
21
|
+
:restart => 'Gracefully restart unicorn (zero downtime)',
|
22
|
+
:reload => 'Reload unicorn configuration',
|
23
|
+
:birth => 'Create new worker process',
|
24
|
+
:prolicide => 'Kill single worker process',
|
25
|
+
:slaughter => 'Kill all worker processes (master remains)',
|
26
|
+
:status => 'Returns current unicorn status'
|
27
|
+
}
|
28
|
+
|
29
|
+
RETURN_CODES = {
|
30
|
+
:success => 0,
|
31
|
+
:invalid_action => 1,
|
32
|
+
:action_failed => 2,
|
33
|
+
:not_running => 3
|
34
|
+
}
|
35
|
+
|
36
|
+
def try_block
|
37
|
+
begin
|
38
|
+
yield
|
39
|
+
[RETURN_CODES[:success], nil]
|
40
|
+
rescue => e
|
41
|
+
[RETURN_CODES[:action_failed], e]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def print_help
|
46
|
+
puts 'RedUnicorn::Unicorn: Unicorn process interactions'
|
47
|
+
puts "Usage: red_unicorn [opts] (#{ALLOWED_ACTIONS.keys.join(' | ')})"
|
48
|
+
puts ' -h, --help Show this help screen'
|
49
|
+
puts ' -p, --pid /path/to/file.pid Specify path to PID file (default: /var/run/unicorn/unicorn.pid)'
|
50
|
+
puts ' -x, --unicorn-exec /path/to/unicorn Specify path to unicorn executable (default: /var/www/shared/bundle/bin/unicorn_rails)'
|
51
|
+
puts ' -c, --unicorn-config /path/to/config Specify path to unicorn configuration file (default: /etc/unicorn/app.rb)'
|
52
|
+
puts ' -t, --timeout 30 Specify timeout for running actions'
|
53
|
+
puts ' -e, --env production Specify environment (default: production)'
|
54
|
+
puts 'Commands:'
|
55
|
+
max_width = ALLOWED_ACTIONS.keys.map(&:to_s).map(&:length).max + 8
|
56
|
+
ALLOWED_ACTIONS.each_pair do |action, message|
|
57
|
+
puts " #{action}:#{' ' * (max_width - action.to_s.length)}#{message}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
unicorn_hash = {}
|
62
|
+
opts.each do |opt,arg|
|
63
|
+
case opt
|
64
|
+
when '--timeout'
|
65
|
+
unicorn_hash[:action_timeout] = arg.to_i
|
66
|
+
when '--unicorn-config'
|
67
|
+
unicorn_hash[:unicorn_config] = arg.to_s
|
68
|
+
when '--unicorn-exec'
|
69
|
+
unicorn_hash[:exec_path] = arg.to_s
|
70
|
+
when '--pid'
|
71
|
+
unicorn_hash[:pid] = arg.to_s
|
72
|
+
when '--env'
|
73
|
+
unicorn_hash[:env] = arg.to_s
|
74
|
+
when '--help'
|
75
|
+
print_help
|
76
|
+
exit RETURN_CODES[:success]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
unicorn = RedUnicorn::Unicorn.new(unicorn_hash)
|
81
|
+
|
82
|
+
action = ARGV.first
|
83
|
+
result = nil
|
84
|
+
error = nil
|
85
|
+
|
86
|
+
case action
|
87
|
+
when 'start'
|
88
|
+
result,error = try_block{ unicorn.start }
|
89
|
+
when 'stop'
|
90
|
+
result,error = try_block{ unicorn.stop }
|
91
|
+
when 'restart'
|
92
|
+
result,error = try_block{ unicorn.restart }
|
93
|
+
when 'reload'
|
94
|
+
result,error = try_block{ unicorn.reload }
|
95
|
+
when 'birth'
|
96
|
+
result,error = try_block{ unicorn.add_child }
|
97
|
+
when 'prolicide'
|
98
|
+
result,error = try_block{ unicorn.remove_child }
|
99
|
+
when 'slaughter'
|
100
|
+
result,error = try_block{ unicorn.remove_all_children }
|
101
|
+
when 'status'
|
102
|
+
result,error = try_block{ unicorn.status }
|
103
|
+
result = RETURN_CODES[:not_running] if error
|
104
|
+
else
|
105
|
+
puts "ERROR: Invalid action received: #{action}"
|
106
|
+
exit RETURN_CODES[:invalid_action]
|
107
|
+
end
|
108
|
+
|
109
|
+
if(result == RETURN_CODES[:success])
|
110
|
+
puts "Unicorn action #{action} completed successfully."
|
111
|
+
else
|
112
|
+
puts "ERROR: Unicorn action #{action} failed. Please check error logs.\n#{error.class.name}: #{error}\n#{error.backtrace.join("\n")}"
|
113
|
+
end
|
114
|
+
|
115
|
+
exit result
|
@@ -0,0 +1,180 @@
|
|
1
|
+
#TODO: Add method of checking for rouge unicorn process and killing them
|
2
|
+
module RedUnicorn
|
3
|
+
|
4
|
+
# Descriptive error classes
|
5
|
+
class UnicornError < StandardError
|
6
|
+
end
|
7
|
+
class ActionFailed < UnicornError
|
8
|
+
end
|
9
|
+
class FileNotFound < UnicornError
|
10
|
+
end
|
11
|
+
class NotRunning < UnicornError
|
12
|
+
end
|
13
|
+
class IsRunning < UnicornError
|
14
|
+
end
|
15
|
+
|
16
|
+
class Unicorn
|
17
|
+
# pid:: Path to unicorn PID file
|
18
|
+
# return_codes:: Hash of return codes (defined in executable bin file)
|
19
|
+
# exec_path:: Path to unicorn executable
|
20
|
+
# Create a new instance of unicorn interactor
|
21
|
+
def initialize(opts={})
|
22
|
+
@opts = {
|
23
|
+
:pid => '/var/run/unicorn/unicorn.pid',
|
24
|
+
:exec_path => '/var/www/shared/bundle/bin/unicorn_rails',
|
25
|
+
:config_path => '/etc/unicorn/app.rb',
|
26
|
+
:action_timeout => 30,
|
27
|
+
:env => 'production'
|
28
|
+
}.merge(opts)
|
29
|
+
check_exec_path
|
30
|
+
end
|
31
|
+
|
32
|
+
# Start a new unicorn process
|
33
|
+
def start
|
34
|
+
process_is :stopped do
|
35
|
+
%x{#{@opts[:exec_path]} --daemonize --env #{@opts[:env]} --config-file #{@opts[:config_path]}}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Stop current unicorn process
|
40
|
+
def stop
|
41
|
+
process_is :running do
|
42
|
+
Process.kill('QUIT', pid)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Halts the current unicorn process
|
47
|
+
def halt
|
48
|
+
process_is :running do
|
49
|
+
Process.kill('TERM', pid)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Graceful restart
|
54
|
+
def restart
|
55
|
+
process_is :running do
|
56
|
+
original_pid = pid
|
57
|
+
Process.kill('USR2', pid)
|
58
|
+
sleep(0.1) # give unicorn some breathing room
|
59
|
+
waited = 0
|
60
|
+
until((File.exists?(@opts[:pid]) && is_running? && !child_pids(pid).empty?) || waited > @opts[:action_timeout])
|
61
|
+
sleep_start = Time.now.to_f
|
62
|
+
sleep(0.2)
|
63
|
+
waited += Time.now.to_f - sleep_start
|
64
|
+
end
|
65
|
+
if(pid == original_pid || waited > @opts[:action_timeout])
|
66
|
+
raise UnicornError.new 'Failed to start new process'
|
67
|
+
end
|
68
|
+
Process.kill('QUIT', original_pid)
|
69
|
+
while(is_running?(original_pid) && waited < @opts[:action_timeout])
|
70
|
+
sleep_start = Time.now.to_f
|
71
|
+
sleep(0.2)
|
72
|
+
waited += Time.now.to_f - sleep_start
|
73
|
+
end
|
74
|
+
raise IsRunning.new 'Failed to stop original unicorn process' if is_running?(original_pid)
|
75
|
+
raise NotRunning.new 'Failed to start new unicorn process' unless is_running?
|
76
|
+
reopen_logs
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Reload unicorn configuration
|
81
|
+
def reload
|
82
|
+
process_is :running do
|
83
|
+
Process.kill('HUP', pid)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Add new worker process
|
88
|
+
def add_child
|
89
|
+
process_is :running do
|
90
|
+
Process.kill('TTIN', pid)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Remove worker process
|
95
|
+
def remove_child
|
96
|
+
process_is :running do
|
97
|
+
Process.kill('TTOU', pid)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Stops all worker processes but
|
102
|
+
# keeps the master process alive
|
103
|
+
def remove_all_children
|
104
|
+
process_is :running do
|
105
|
+
Process.kill('WINCH', pid)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Reopen log files
|
110
|
+
def reopen_logs
|
111
|
+
process_is :running do
|
112
|
+
Process.kill('USR1', pid)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Return status of current process
|
117
|
+
def status
|
118
|
+
process_is :running do
|
119
|
+
puts '* unicorn is running'
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
# Return process ID from PID file
|
126
|
+
def pid
|
127
|
+
if(File.exists?(@opts[:pid]))
|
128
|
+
File.read(@opts[:pid]).to_s.strip.to_i
|
129
|
+
else
|
130
|
+
raise FileNotFound.new "PID file not found. Provided path: #{@opts[:pid]}"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Check if unicorn is currently running
|
135
|
+
def is_running?(custom_pid = nil)
|
136
|
+
begin
|
137
|
+
Process.kill(0, custom_pid || pid)
|
138
|
+
true
|
139
|
+
rescue
|
140
|
+
false
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# state:: :running or :stopped
|
145
|
+
# Ensure process is in given state and run some code if
|
146
|
+
# the caller felt like passing some along
|
147
|
+
def process_is(state)
|
148
|
+
case state
|
149
|
+
when :running
|
150
|
+
raise NotRunning.new 'Unicorn is not currently running' unless is_running?
|
151
|
+
when :stopped
|
152
|
+
raise IsRunning.new 'Unicorn is currently running' if is_running?
|
153
|
+
else
|
154
|
+
raise UnicornError.new 'Unknown process state received'
|
155
|
+
end
|
156
|
+
yield if block_given?
|
157
|
+
end
|
158
|
+
|
159
|
+
# parent_pid:: Parent process ID
|
160
|
+
# Returns array of child process IDs for the given parent
|
161
|
+
def child_pids(parent_pid)
|
162
|
+
process_list = %x{ps -eo pid,ppid | grep #{parent_pid}}
|
163
|
+
process_list.map(&:strip).find_all{|pr| pr.split.last == parent_pid.to_s }.map{|pr| pr.split.first.strip.to_i }
|
164
|
+
end
|
165
|
+
|
166
|
+
# Check validity of unicorn exec path
|
167
|
+
def check_exec_path
|
168
|
+
unless(File.exists?(@opts[:exec_path]))
|
169
|
+
test_path = '/var/www/shared/bundle/bin/unicorn_rails'
|
170
|
+
if(File.exists?(test_path))
|
171
|
+
@opts[:exec_path] = test_path
|
172
|
+
else
|
173
|
+
raise FileNotFound.new "Failed to find executable unicorn. Set path is: #{@opts[:exec_path]}"
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module RedUnicorn
|
2
|
+
class Version
|
3
|
+
|
4
|
+
attr_reader :major, :minor, :tiny
|
5
|
+
|
6
|
+
def initialize(version)
|
7
|
+
version = version.split('.')
|
8
|
+
@major, @minor, @tiny = [version[0].to_i, version[1].to_i, version[2].to_i]
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
"#{@major}.#{@minor}.#{@tiny}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
VERSION = Version.new('1.0.0')
|
17
|
+
end
|
data/lib/red_unicorn.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'red_unicorn/version'
|
metadata
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: red_unicorn
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 23
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
- 0
|
10
|
+
version: 1.0.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Chris Roberts
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-06-08 00:00:00 -07:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: unicorn
|
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
|
+
description: Unicorn Process Handler
|
36
|
+
email: chrisroberts.code@gmail.com
|
37
|
+
executables:
|
38
|
+
- red_unicorn
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files:
|
42
|
+
- README.rdoc
|
43
|
+
- CHANGELOG.rdoc
|
44
|
+
files:
|
45
|
+
- README.rdoc
|
46
|
+
- CHANGELOG.rdoc
|
47
|
+
- bin/red_unicorn
|
48
|
+
- lib/red_unicorn.rb
|
49
|
+
- lib/red_unicorn/unicorn.rb
|
50
|
+
- lib/red_unicorn/version.rb
|
51
|
+
has_rdoc: true
|
52
|
+
homepage: http://github.com/chrisroberts/red_unicorn
|
53
|
+
licenses: []
|
54
|
+
|
55
|
+
post_install_message:
|
56
|
+
rdoc_options: []
|
57
|
+
|
58
|
+
require_paths:
|
59
|
+
- lib
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
hash: 3
|
66
|
+
segments:
|
67
|
+
- 0
|
68
|
+
version: "0"
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
hash: 3
|
75
|
+
segments:
|
76
|
+
- 0
|
77
|
+
version: "0"
|
78
|
+
requirements: []
|
79
|
+
|
80
|
+
rubyforge_project:
|
81
|
+
rubygems_version: 1.6.2
|
82
|
+
signing_key:
|
83
|
+
specification_version: 3
|
84
|
+
summary: Unicorn Process Handler
|
85
|
+
test_files: []
|
86
|
+
|