unicorn_horn 0.0.1 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -9,6 +9,7 @@ begin
9
9
  gem.email = "joe@citizenlogistics.com"
10
10
  gem.homepage = "http://github.com/jxe/unicorn_horn"
11
11
  gem.authors = ["Joe Edelman"]
12
+ gem.add_dependency 'configurer'
12
13
  end
13
14
  Jeweler::GemcutterTasks.new
14
15
  rescue LoadError
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.1
1
+ 0.1.1
data/bin/hornrunner ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require 'unicorn_horn'
3
+ require ARGV.shift
4
+ UnicornHorn::Runner.new( eval "[#{ARGV.join(',')}]" ).start
@@ -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 'tmpdir'
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
- ORIG_ZERO = $0
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
- version: 0.0.1
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-07 00:00:00 -04:00
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