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 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