unicorn_horn 0.0.1 → 0.1.1
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/Rakefile +1 -0
- data/VERSION +1 -1
- data/bin/hornrunner +4 -0
- data/lib/unicorn_horn/runner.rb +53 -0
- data/lib/unicorn_horn/self_pipe_daemon.rb +52 -0
- data/lib/unicorn_horn/utils.rb +24 -0
- data/lib/unicorn_horn/worker.rb +64 -0
- data/lib/unicorn_horn.rb +6 -176
- metadata +24 -8
data/Rakefile
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.1.1
|
data/bin/hornrunner
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
module UnicornHorn
|
2
|
+
class Runner < SelfPipeDaemon
|
3
|
+
|
4
|
+
def initialize handlers
|
5
|
+
super()
|
6
|
+
@workers = handlers.map{ |handler| Worker.new handler }
|
7
|
+
end
|
8
|
+
|
9
|
+
def start
|
10
|
+
register :QUIT, :INT, :TERM, :CHLD
|
11
|
+
AFTER_FORK << proc{ @workers.clear; forget }
|
12
|
+
@workers.each(&:launch!)
|
13
|
+
|
14
|
+
ploop do |signal|
|
15
|
+
reap
|
16
|
+
case signal
|
17
|
+
when nil
|
18
|
+
@workers.each(&:kill_if_idle)
|
19
|
+
@workers.each{ |w| w.wpid or w.launch! }
|
20
|
+
psleep 1
|
21
|
+
when :CHLD; next
|
22
|
+
when :QUIT; raze(:QUIT, 60); break
|
23
|
+
when :TERM, :INT; raze(:TERM, 5); break
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def reap
|
32
|
+
begin
|
33
|
+
wpid, status = Process.waitpid2(-1, Process::WNOHANG)
|
34
|
+
wpid or return
|
35
|
+
next unless worker = @workers.detect{ |w| w.wpid == wpid }
|
36
|
+
worker.reap(status)
|
37
|
+
rescue Errno::ECHILD
|
38
|
+
break
|
39
|
+
end while true
|
40
|
+
end
|
41
|
+
|
42
|
+
def raze(sig, timeframe)
|
43
|
+
limit = Time.now + timeframe
|
44
|
+
until @workers.empty? || Time.now > limit
|
45
|
+
@workers.each{ |w| w.kill(sig) }
|
46
|
+
sleep(0.1)
|
47
|
+
reap
|
48
|
+
end
|
49
|
+
@workers.each{ |w| w.kill(:KILL) }
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module UnicornHorn
|
2
|
+
|
3
|
+
class SelfPipeDaemon
|
4
|
+
extend Configurer
|
5
|
+
config :logger do Logger.new(STDERR) end
|
6
|
+
|
7
|
+
SELF_PIPE = []
|
8
|
+
SIG_QUEUE = []
|
9
|
+
|
10
|
+
def psleep(sec)
|
11
|
+
IO.select([ SELF_PIPE[0] ], nil, nil, sec) or return
|
12
|
+
SELF_PIPE[0].read_nonblock(16*1024, "")
|
13
|
+
rescue Errno::EAGAIN, Errno::EINTR
|
14
|
+
end
|
15
|
+
|
16
|
+
def pwake
|
17
|
+
SELF_PIPE[1].write_nonblock('.') # wakeup master process from select
|
18
|
+
rescue Errno::EAGAIN, Errno::EINTR
|
19
|
+
end
|
20
|
+
|
21
|
+
def register *signals
|
22
|
+
@signals = signals
|
23
|
+
signals.each { |sig| trap(sig){ |sig_nr| SIG_QUEUE << sig; pwake } }
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize
|
27
|
+
SELF_PIPE.replace(IO.pipe)
|
28
|
+
SELF_PIPE.each { |io| io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def ploop
|
32
|
+
Utils.proc_name 'master'
|
33
|
+
logger.info "master process ready"
|
34
|
+
|
35
|
+
begin
|
36
|
+
yield SIG_QUEUE.shift
|
37
|
+
rescue => e
|
38
|
+
logger.error "Unhandled master loop exception #{e.inspect}."
|
39
|
+
logger.error e.backtrace.join("\n")
|
40
|
+
end while true
|
41
|
+
|
42
|
+
logger.info "master complete"
|
43
|
+
end
|
44
|
+
|
45
|
+
def forget
|
46
|
+
@signals.each { |sig| trap(sig, nil) }
|
47
|
+
SIG_QUEUE.clear
|
48
|
+
SELF_PIPE.each { |io| io.close rescue nil }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'tmpdir'
|
2
|
+
|
3
|
+
module UnicornHorn
|
4
|
+
module Utils
|
5
|
+
module_function
|
6
|
+
|
7
|
+
def tmpio
|
8
|
+
fp = File.open("#{Dir::tmpdir}/#{rand}",
|
9
|
+
File::RDWR|File::CREAT|File::EXCL, 0600)
|
10
|
+
File.unlink(fp.path)
|
11
|
+
fp.binmode
|
12
|
+
fp.sync = true
|
13
|
+
fp
|
14
|
+
rescue Errno::EEXIST
|
15
|
+
retry
|
16
|
+
end
|
17
|
+
|
18
|
+
def proc_name foo
|
19
|
+
@orig_zero ||= $0
|
20
|
+
@orig_argv ||= ARGV.join(' ')
|
21
|
+
$0 = "#{@orig_zero} #{foo} #{@orig_argv}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module UnicornHorn
|
2
|
+
|
3
|
+
class Worker
|
4
|
+
extend Configurer
|
5
|
+
config :idle_timeout do 60; end
|
6
|
+
config :logger do Logger.new(STDERR); end
|
7
|
+
|
8
|
+
attr_reader :name, :wpid
|
9
|
+
|
10
|
+
def initialize handler
|
11
|
+
@name = handler.respond_to?(:name) ? handler.name : handler.inspect
|
12
|
+
@handler = handler.respond_to?(:new) ? handler.new : handler
|
13
|
+
end
|
14
|
+
|
15
|
+
def launch!
|
16
|
+
@tmp = Utils.tmpio
|
17
|
+
@wpid = fork do
|
18
|
+
|
19
|
+
# the prep work
|
20
|
+
Utils.proc_name "worker[#{name}]"
|
21
|
+
AFTER_FORK.each(&:call)
|
22
|
+
@tmp.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
|
23
|
+
[:TERM, :INT].each { |sig| trap(sig) { exit!(0) } }
|
24
|
+
alive = @tmp
|
25
|
+
m = 0
|
26
|
+
logger.info "worker=#{name} ready"
|
27
|
+
|
28
|
+
# the actual loop
|
29
|
+
while Process.ppid && alive
|
30
|
+
alive.chmod(m = 0 == m ? 1 : 0)
|
31
|
+
@handler.call
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def kill_if_idle
|
38
|
+
return unless @tmp and @wpid
|
39
|
+
stat = @tmp.stat
|
40
|
+
stat.mode == 0100600 and return
|
41
|
+
idle_timeout ||= 60
|
42
|
+
(diff = (Time.now - stat.ctime)) <= idle_timeout and return
|
43
|
+
logger.error "worker=#{name} PID:#{@wpid} timeout " \
|
44
|
+
"(#{diff}s > #{idle_timeout}s), killing"
|
45
|
+
kill(:KILL)
|
46
|
+
end
|
47
|
+
|
48
|
+
def kill(signal)
|
49
|
+
return unless @wpid
|
50
|
+
Process.kill(signal, @wpid)
|
51
|
+
rescue Errno::ESRCH
|
52
|
+
@wpid = nil
|
53
|
+
@tmp.close rescue nil
|
54
|
+
end
|
55
|
+
|
56
|
+
def reap(status)
|
57
|
+
@wpid = nil
|
58
|
+
@tmp.close rescue nil
|
59
|
+
m = "reaped #{status.inspect} worker=#{name}"
|
60
|
+
status.success? ? logger.info(m) : logger.error(m)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
data/lib/unicorn_horn.rb
CHANGED
@@ -1,180 +1,10 @@
|
|
1
1
|
require 'fcntl'
|
2
|
-
require '
|
2
|
+
require 'configurer'
|
3
|
+
require 'unicorn_horn/utils'
|
4
|
+
require 'unicorn_horn/self_pipe_daemon'
|
5
|
+
require 'unicorn_horn/worker'
|
6
|
+
require 'unicorn_horn/runner'
|
3
7
|
|
4
8
|
module UnicornHorn
|
5
|
-
|
6
|
-
ARGVS = ARGV.join(' ')
|
7
|
-
|
8
|
-
class Worker
|
9
|
-
attr_accessor :name, :logger, :idle_timeout
|
10
|
-
attr_reader :wpid
|
11
|
-
attr_writer :master
|
12
|
-
|
13
|
-
def initialize name, idle_timeout = 60, &blk
|
14
|
-
@name = name
|
15
|
-
@idle_timeout = idle_timeout
|
16
|
-
@blk = blk
|
17
|
-
end
|
18
|
-
|
19
|
-
def launch!
|
20
|
-
@tmp = tmpio
|
21
|
-
@wpid = fork do
|
22
|
-
$0 = "#{ORIG_ZERO} worker[#{name}] #{ARGVS}"
|
23
|
-
@master.forget; @master = nil
|
24
|
-
@tmp.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
|
25
|
-
[:TERM, :INT].each { |sig| trap(sig) { exit!(0) } }
|
26
|
-
alive = @tmp
|
27
|
-
m = 0
|
28
|
-
logger.info "worker=#{name} ready"
|
29
|
-
@blk.call(proc{
|
30
|
-
if Process.ppid && alive
|
31
|
-
alive.chmod(m = 0 == m ? 1 : 0) or true
|
32
|
-
end
|
33
|
-
})
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
def tmpio
|
38
|
-
fp = File.open("#{Dir::tmpdir}/#{rand}",
|
39
|
-
File::RDWR|File::CREAT|File::EXCL, 0600)
|
40
|
-
File.unlink(fp.path)
|
41
|
-
fp.binmode
|
42
|
-
fp.sync = true
|
43
|
-
fp
|
44
|
-
rescue Errno::EEXIST
|
45
|
-
retry
|
46
|
-
end
|
47
|
-
|
48
|
-
def kill_if_idle
|
49
|
-
return unless @tmp and @wpid
|
50
|
-
stat = @tmp.stat
|
51
|
-
stat.mode == 0100600 and return
|
52
|
-
@idle_timeout ||= 60
|
53
|
-
(diff = (Time.now - stat.ctime)) <= @idle_timeout and return
|
54
|
-
@logger.error "worker=#{name} PID:#{@wpid} timeout " \
|
55
|
-
"(#{diff}s > #{@idle_timeout}s), killing"
|
56
|
-
kill(:KILL)
|
57
|
-
end
|
58
|
-
|
59
|
-
def kill(signal)
|
60
|
-
return unless @wpid
|
61
|
-
Process.kill(signal, @wpid)
|
62
|
-
rescue Errno::ESRCH
|
63
|
-
@wpid = nil
|
64
|
-
@tmp.close rescue nil
|
65
|
-
end
|
66
|
-
|
67
|
-
def reap(status)
|
68
|
-
@wpid = nil
|
69
|
-
@tmp.close rescue nil
|
70
|
-
m = "reaped #{status.inspect} worker=#{name}"
|
71
|
-
status.success? ? @logger.info(m) : @logger.error(m)
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
|
76
|
-
class SelfPipeDaemon
|
77
|
-
attr_accessor :logger
|
78
|
-
|
79
|
-
SELF_PIPE = []
|
80
|
-
SIG_QUEUE = []
|
81
|
-
|
82
|
-
def psleep(sec)
|
83
|
-
IO.select([ SELF_PIPE[0] ], nil, nil, sec) or return
|
84
|
-
SELF_PIPE[0].read_nonblock(16*1024, "")
|
85
|
-
rescue Errno::EAGAIN, Errno::EINTR
|
86
|
-
end
|
87
|
-
|
88
|
-
def pwake
|
89
|
-
SELF_PIPE[1].write_nonblock('.') # wakeup master process from select
|
90
|
-
rescue Errno::EAGAIN, Errno::EINTR
|
91
|
-
end
|
92
|
-
|
93
|
-
def register *signals
|
94
|
-
@signals = signals
|
95
|
-
signals.each { |sig| trap(sig){ |sig_nr| SIG_QUEUE << sig; pwake } }
|
96
|
-
end
|
97
|
-
|
98
|
-
def initialize options = {}
|
99
|
-
SELF_PIPE.replace(IO.pipe)
|
100
|
-
SELF_PIPE.each { |io| io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) }
|
101
|
-
options.each_pair{ |k,v| send("#{k}=", v) }
|
102
|
-
yield self if block_given?
|
103
|
-
self
|
104
|
-
end
|
105
|
-
|
106
|
-
def ploop
|
107
|
-
$0 = "#{ORIG_ZERO} master #{ARGVS}"
|
108
|
-
logger.info "master process ready"
|
109
|
-
|
110
|
-
begin
|
111
|
-
yield SIG_QUEUE.shift
|
112
|
-
rescue => e
|
113
|
-
logger.error "Unhandled master loop exception #{e.inspect}."
|
114
|
-
logger.error e.backtrace.join("\n")
|
115
|
-
end while true
|
116
|
-
|
117
|
-
logger.info "master complete"
|
118
|
-
end
|
119
|
-
|
120
|
-
def forget
|
121
|
-
@signals.each { |sig| trap(sig, nil) }
|
122
|
-
SIG_QUEUE.clear
|
123
|
-
SELF_PIPE.each { |io| io.close rescue nil }
|
124
|
-
end
|
125
|
-
end
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
class Monitor < SelfPipeDaemon
|
130
|
-
attr_accessor :workers, :kill_timeout
|
131
|
-
|
132
|
-
def start
|
133
|
-
workers.each{ |w| w.master = self; w.logger = logger }
|
134
|
-
register :QUIT, :INT, :TERM, :CHLD
|
135
|
-
workers.each(&:launch!)
|
136
|
-
|
137
|
-
ploop do |signal|
|
138
|
-
reap
|
139
|
-
case signal
|
140
|
-
when nil
|
141
|
-
workers.each(&:kill_if_idle)
|
142
|
-
workers.each{ |w| w.wpid or w.launch! }
|
143
|
-
psleep 1
|
144
|
-
when :CHLD; next
|
145
|
-
when :QUIT; raze(:QUIT); break
|
146
|
-
when :TERM, :INT; raze(:TERM); break
|
147
|
-
end
|
148
|
-
end
|
149
|
-
end
|
150
|
-
|
151
|
-
def forget
|
152
|
-
super
|
153
|
-
workers.clear
|
154
|
-
end
|
155
|
-
|
156
|
-
|
157
|
-
private
|
158
|
-
|
159
|
-
def reap
|
160
|
-
begin
|
161
|
-
wpid, status = Process.waitpid2(-1, Process::WNOHANG)
|
162
|
-
wpid or return
|
163
|
-
next unless worker = workers.detect{ |w| w.wpid == wpid }
|
164
|
-
worker.reap(status)
|
165
|
-
rescue Errno::ECHILD
|
166
|
-
break
|
167
|
-
end while true
|
168
|
-
end
|
169
|
-
|
170
|
-
def raze(sig)
|
171
|
-
limit = Time.now + (@kill_timeout ||= 60)
|
172
|
-
until workers.empty? || Time.now > limit
|
173
|
-
workers.each{ |w| w.kill(sig) }
|
174
|
-
sleep(0.1)
|
175
|
-
reap
|
176
|
-
end
|
177
|
-
workers.each{ |w| w.kill(:KILL) }
|
178
|
-
end
|
179
|
-
end
|
9
|
+
AFTER_FORK = []
|
180
10
|
end
|
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 0
|
7
|
-
- 0
|
8
7
|
- 1
|
9
|
-
|
8
|
+
- 1
|
9
|
+
version: 0.1.1
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Joe Edelman
|
@@ -14,14 +14,25 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-09-
|
18
|
-
default_executable:
|
19
|
-
dependencies:
|
20
|
-
|
17
|
+
date: 2010-09-09 00:00:00 -04:00
|
18
|
+
default_executable: hornrunner
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: configurer
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 0
|
29
|
+
version: "0"
|
30
|
+
type: :runtime
|
31
|
+
version_requirements: *id001
|
21
32
|
description:
|
22
33
|
email: joe@citizenlogistics.com
|
23
|
-
executables:
|
24
|
-
|
34
|
+
executables:
|
35
|
+
- hornrunner
|
25
36
|
extensions: []
|
26
37
|
|
27
38
|
extra_rdoc_files:
|
@@ -34,7 +45,12 @@ files:
|
|
34
45
|
- README.rdoc
|
35
46
|
- Rakefile
|
36
47
|
- VERSION
|
48
|
+
- bin/hornrunner
|
37
49
|
- lib/unicorn_horn.rb
|
50
|
+
- lib/unicorn_horn/runner.rb
|
51
|
+
- lib/unicorn_horn/self_pipe_daemon.rb
|
52
|
+
- lib/unicorn_horn/utils.rb
|
53
|
+
- lib/unicorn_horn/worker.rb
|
38
54
|
- test/helper.rb
|
39
55
|
- test/test_unicorn_horn.rb
|
40
56
|
has_rdoc: true
|