red_unicorn 1.0.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/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
|
+
|