unicorn 1.1.7 → 2.0.0pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/GIT-VERSION-GEN +1 -1
  2. data/GNUmakefile +14 -5
  3. data/Rakefile +3 -28
  4. data/TODO +7 -0
  5. data/bin/unicorn +9 -13
  6. data/bin/unicorn_rails +12 -14
  7. data/examples/big_app_gc.rb +33 -2
  8. data/ext/unicorn_http/global_variables.h +3 -1
  9. data/ext/unicorn_http/unicorn_http.rl +15 -6
  10. data/lib/unicorn.rb +67 -820
  11. data/lib/unicorn/app/exec_cgi.rb +3 -4
  12. data/lib/unicorn/configurator.rb +20 -25
  13. data/lib/unicorn/const.rb +26 -25
  14. data/lib/unicorn/http_request.rb +64 -57
  15. data/lib/unicorn/http_response.rb +16 -35
  16. data/lib/unicorn/http_server.rb +700 -0
  17. data/lib/unicorn/launcher.rb +4 -3
  18. data/lib/unicorn/oob_gc.rb +50 -61
  19. data/lib/unicorn/socket_helper.rb +4 -4
  20. data/lib/unicorn/tee_input.rb +18 -26
  21. data/lib/unicorn/tmpio.rb +29 -0
  22. data/lib/unicorn/util.rb +51 -85
  23. data/lib/unicorn/worker.rb +40 -0
  24. data/local.mk.sample +0 -9
  25. data/script/isolate_for_tests +43 -0
  26. data/t/GNUmakefile +8 -1
  27. data/t/t0003-working_directory.sh +0 -5
  28. data/t/t0010-reap-logging.sh +55 -0
  29. data/t/t0303-rails3-alt-working_directory_config.ru.sh +0 -5
  30. data/t/test-rails3.sh +1 -1
  31. data/test/exec/test_exec.rb +1 -1
  32. data/test/unit/test_http_parser_ng.rb +11 -0
  33. data/test/unit/test_request.rb +12 -0
  34. data/test/unit/test_response.rb +23 -21
  35. data/test/unit/test_signals.rb +1 -1
  36. data/test/unit/test_tee_input.rb +21 -19
  37. data/unicorn.gemspec +3 -2
  38. metadata +47 -25
  39. data/t/oob_gc.ru +0 -21
  40. data/t/oob_gc_path.ru +0 -21
  41. data/t/t0012-reload-empty-config.sh +0 -82
  42. data/t/t0018-write-on-close.sh +0 -23
  43. data/t/t9001-oob_gc.sh +0 -47
  44. data/t/t9002-oob_gc-path.sh +0 -75
  45. data/t/write-on-close.ru +0 -11
@@ -20,6 +20,7 @@ module Unicorn::Launcher
20
20
  # to pickup code changes if the original deployment directory
21
21
  # is a symlink or otherwise got replaced.
22
22
  def self.daemonize!(options)
23
+ cfg = Unicorn::Configurator
23
24
  $stdin.reopen("/dev/null")
24
25
 
25
26
  # We only start a new process group if we're not being reexecuted
@@ -52,9 +53,9 @@ module Unicorn::Launcher
52
53
  end
53
54
  end
54
55
  # $stderr/$stderr can/will be redirected separately in the Unicorn config
55
- Unicorn::Configurator::DEFAULTS[:stderr_path] ||= "/dev/null"
56
- Unicorn::Configurator::DEFAULTS[:stdout_path] ||= "/dev/null"
57
- Unicorn::Configurator::RACKUP[:daemonized] = true
56
+ cfg::DEFAULTS[:stderr_path] ||= "/dev/null"
57
+ cfg::DEFAULTS[:stdout_path] ||= "/dev/null"
58
+ cfg::RACKUP[:daemonized] = true
58
59
  end
59
60
 
60
61
  end
@@ -1,69 +1,58 @@
1
1
  # -*- encoding: binary -*-
2
+ module Unicorn
2
3
 
3
- # Runs GC after requests, after closing the client socket and
4
- # before attempting to accept more connections.
5
- #
6
- # This shouldn't hurt overall performance as long as the server cluster
7
- # is at <50% CPU capacity, and improves the performance of most memory
8
- # intensive requests. This serves to improve _client-visible_
9
- # performance (possibly at the cost of overall performance).
10
- #
11
- # Increasing the number of +worker_processes+ may be necessary to
12
- # improve average client response times because some of your workers
13
- # will be busy doing GC and unable to service clients. Think of
14
- # using more workers with this module as a poor man's concurrent GC.
15
- #
16
- # We'll call GC after each request is been written out to the socket, so
17
- # the client never sees the extra GC hit it.
18
- #
19
- # This middleware is _only_ effective for applications that use a lot
20
- # of memory, and will hurt simpler apps/endpoints that can process
21
- # multiple requests before incurring GC.
22
- #
23
- # This middleware is only designed to work with unicorn, as it harms
24
- # performance with keepalive-enabled servers.
25
- #
26
- # Example (in config.ru):
27
- #
28
- # require 'unicorn/oob_gc'
29
- #
30
- # # GC ever two requests that hit /expensive/foo or /more_expensive/foo
31
- # # in your app. By default, this will GC once every 5 requests
32
- # # for all endpoints in your app
33
- # use Unicorn::OobGC, 2, %r{\A/(?:expensive/foo|more_expensive/foo)}
34
- #
35
- # Feedback from users of early implementations of this module:
36
- # * http://comments.gmane.org/gmane.comp.lang.ruby.unicorn.general/486
37
- # * http://article.gmane.org/gmane.comp.lang.ruby.unicorn.general/596
38
- module Unicorn::OobGC
39
-
40
- # this pretends to be Rack middleware because it used to be
41
- # But we need to hook into unicorn internals so we need to close
42
- # the socket before clearing the request env.
4
+ # Run GC after every request, after closing the client socket and
5
+ # before attempting to accept more connections.
6
+ #
7
+ # This shouldn't hurt overall performance as long as the server cluster
8
+ # is at <50% CPU capacity, and improves the performance of most memory
9
+ # intensive requests. This serves to improve _client-visible_
10
+ # performance (possibly at the cost of overall performance).
11
+ #
12
+ # We'll call GC after each request is been written out to the socket, so
13
+ # the client never sees the extra GC hit it.
14
+ #
15
+ # This middleware is _only_ effective for applications that use a lot
16
+ # of memory, and will hurt simpler apps/endpoints that can process
17
+ # multiple requests before incurring GC.
43
18
  #
44
- # +interval+ is the number of requests matching the +path+ regular
45
- # expression before invoking GC.
46
- def self.new(app, interval = 5, path = %r{\A/})
47
- @@nr = interval
48
- self.const_set :OOBGC_PATH, path
49
- self.const_set :OOBGC_INTERVAL, interval
50
- self.const_set :OOBGC_ENV, Unicorn::HttpRequest::REQ
51
- ObjectSpace.each_object(Unicorn::HttpServer) do |s|
52
- s.extend(self)
19
+ # This middleware is only designed to work with Unicorn, as it harms
20
+ # keepalive performance.
21
+ #
22
+ # Example (in config.ru):
23
+ #
24
+ # require 'unicorn/oob_gc'
25
+ #
26
+ # # GC ever two requests that hit /expensive/foo or /more_expensive/foo
27
+ # # in your app. By default, this will GC once every 5 requests
28
+ # # for all endpoints in your app
29
+ # use Unicorn::OobGC, 2, %r{\A/(?:expensive/foo|more_expensive/foo)}
30
+ class OobGC < Struct.new(:app, :interval, :path, :nr, :env, :body)
31
+
32
+ def initialize(app, interval = 5, path = %r{\A/})
33
+ super(app, interval, path, interval)
53
34
  end
54
- app # pretend to be Rack middleware since it was in the past
55
- end
56
35
 
57
- #:stopdoc:
58
- PATH_INFO = "PATH_INFO"
59
- def process_client(client)
60
- super(client) # Unicorn::HttpServer#process_client
61
- if OOBGC_PATH =~ OOBGC_ENV[PATH_INFO] && ((@@nr -= 1) <= 0)
62
- @@nr = OOBGC_INTERVAL
63
- OOBGC_ENV.clear
64
- GC.start
36
+ def call(env)
37
+ status, headers, self.body = app.call(self.env = env)
38
+ [ status, headers, self ]
39
+ end
40
+
41
+ def each(&block)
42
+ body.each(&block)
65
43
  end
66
- end
67
44
 
68
- # :startdoc:
45
+ # in Unicorn, this is closed _after_ the client socket
46
+ def close
47
+ body.close if body.respond_to?(:close)
48
+
49
+ if path =~ env['PATH_INFO'] && ((self.nr -= 1) <= 0)
50
+ self.nr = interval
51
+ self.body = nil
52
+ env.clear
53
+ GC.start
54
+ end
55
+ end
56
+
57
+ end
69
58
  end
@@ -126,12 +126,12 @@ module Unicorn
126
126
  end
127
127
  old_umask = File.umask(opt[:umask] || 0)
128
128
  begin
129
- UNIXServer.new(address)
129
+ Kgio::UNIXServer.new(address)
130
130
  ensure
131
131
  File.umask(old_umask)
132
132
  end
133
133
  elsif address =~ /^(\d+\.\d+\.\d+\.\d+):(\d+)$/
134
- TCPServer.new($1, $2.to_i)
134
+ Kgio::TCPServer.new($1, $2.to_i)
135
135
  else
136
136
  raise ArgumentError, "Don't know how to bind: #{address}"
137
137
  end
@@ -166,9 +166,9 @@ module Unicorn
166
166
  def server_cast(sock)
167
167
  begin
168
168
  Socket.unpack_sockaddr_in(sock.getsockname)
169
- TCPServer.for_fd(sock.fileno)
169
+ Kgio::TCPServer.for_fd(sock.fileno)
170
170
  rescue ArgumentError
171
- UNIXServer.for_fd(sock.fileno)
171
+ Kgio::UNIXServer.for_fd(sock.fileno)
172
172
  end
173
173
  end
174
174
 
@@ -25,11 +25,14 @@ class Unicorn::TeeInput < Struct.new(:socket, :req, :parser,
25
25
 
26
26
  # Initializes a new TeeInput object. You normally do not have to call
27
27
  # this unless you are writing an HTTP server.
28
- def initialize(*args)
29
- super(*args)
28
+ def initialize(socket, request)
29
+ self.socket = socket
30
+ self.req = request.env
31
+ self.parser = request.parser
32
+ self.buf = request.buf
30
33
  self.len = parser.content_length
31
34
  self.tmp = len && len < @@client_body_buffer_size ?
32
- StringIO.new("") : Unicorn::Util.tmpio
35
+ StringIO.new("") : Unicorn::TmpIO.new
33
36
  self.buf2 = ""
34
37
  if buf.size > 0
35
38
  parser.filter_body(buf2, buf) and finalize_input
@@ -168,44 +171,25 @@ class Unicorn::TeeInput < Struct.new(:socket, :req, :parser,
168
171
 
169
172
  private
170
173
 
171
- def client_error(e)
172
- case e
173
- when EOFError
174
- # in case client only did a premature shutdown(SHUT_WR)
175
- # we do support clients that shutdown(SHUT_WR) after the
176
- # _entire_ request has been sent, and those will not have
177
- # raised EOFError on us.
178
- socket.close if socket
179
- raise Unicorn::ClientShutdown, "bytes_read=#{tmp.size}", []
180
- when Unicorn::HttpParserError
181
- e.set_backtrace([])
182
- end
183
- raise e
184
- end
185
-
186
174
  # tees off a +length+ chunk of data from the input into the IO
187
175
  # backing store as well as returning it. +dst+ must be specified.
188
176
  # returns nil if reading from the input returns nil
189
177
  def tee(length, dst)
190
178
  unless parser.body_eof?
191
- if parser.filter_body(dst, socket.readpartial(length, buf)).nil?
179
+ r = socket.kgio_read(length, buf) or eof!
180
+ unless parser.filter_body(dst, r)
192
181
  tmp.write(dst)
193
182
  tmp.seek(0, IO::SEEK_END) # workaround FreeBSD/OSX + MRI 1.8.x bug
194
183
  return dst
195
184
  end
196
185
  end
197
186
  finalize_input
198
- rescue => e
199
- client_error(e)
200
187
  end
201
188
 
202
189
  def finalize_input
203
190
  while parser.trailers(req, buf).nil?
204
- # Don't worry about raising ClientShutdown here on EOFError, tee()
205
- # will catch EOFError when app is processing it, otherwise in
206
- # initialize we never get any chance to enter the app so the
207
- # EOFError will just get trapped by Unicorn and not the Rack app
208
- buf << socket.readpartial(@@io_chunk_size)
191
+ r = socket.kgio_read(@@io_chunk_size) or eof!
192
+ buf << r
209
193
  end
210
194
  self.socket = nil
211
195
  end
@@ -229,4 +213,12 @@ private
229
213
  dst
230
214
  end
231
215
 
216
+ def eof!
217
+ # in case client only did a premature shutdown(SHUT_WR)
218
+ # we do support clients that shutdown(SHUT_WR) after the
219
+ # _entire_ request has been sent, and those will not have
220
+ # raised EOFError on us.
221
+ socket.close if socket
222
+ raise Unicorn::ClientShutdown, "bytes_read=#{tmp.size}", []
223
+ end
232
224
  end
@@ -0,0 +1,29 @@
1
+ # -*- encoding: binary -*-
2
+ # :stopdoc:
3
+ require 'tmpdir'
4
+
5
+ # some versions of Ruby had a broken Tempfile which didn't work
6
+ # well with unlinked files. This one is much shorter, easier
7
+ # to understand, and slightly faster.
8
+ class Unicorn::TmpIO < File
9
+
10
+ # creates and returns a new File object. The File is unlinked
11
+ # immediately, switched to binary mode, and userspace output
12
+ # buffering is disabled
13
+ def self.new
14
+ fp = begin
15
+ super("#{Dir::tmpdir}/#{rand}", RDWR|CREAT|EXCL, 0600)
16
+ rescue Errno::EEXIST
17
+ retry
18
+ end
19
+ unlink(fp.path)
20
+ fp.binmode
21
+ fp.sync = true
22
+ fp
23
+ end
24
+
25
+ # for easier env["rack.input"] compatibility with Rack <= 1.1
26
+ def size
27
+ stat.size
28
+ end
29
+ end
data/lib/unicorn/util.rb CHANGED
@@ -1,101 +1,67 @@
1
1
  # -*- encoding: binary -*-
2
2
 
3
- require 'fcntl'
4
- require 'tmpdir'
5
-
6
- module Unicorn
7
-
8
- class TmpIO < ::File
3
+ module Unicorn::Util
4
+
5
+ # :stopdoc:
6
+ def self.is_log?(fp)
7
+ append_flags = File::WRONLY | File::APPEND
8
+
9
+ ! fp.closed? &&
10
+ fp.sync &&
11
+ fp.path[0] == ?/ &&
12
+ (fp.fcntl(Fcntl::F_GETFL) & append_flags) == append_flags
13
+ rescue IOError, Errno::EBADF
14
+ false
15
+ end
9
16
 
10
- # for easier env["rack.input"] compatibility
11
- def size
12
- # flush if sync
13
- stat.size
17
+ def self.chown_logs(uid, gid)
18
+ ObjectSpace.each_object(File) do |fp|
19
+ fp.chown(uid, gid) if is_log?(fp)
14
20
  end
15
21
  end
16
-
17
- module Util
18
- class << self
19
-
20
- def is_log?(fp)
21
- append_flags = File::WRONLY | File::APPEND
22
-
23
- ! fp.closed? &&
24
- fp.sync &&
25
- fp.path[0] == ?/ &&
26
- (fp.fcntl(Fcntl::F_GETFL) & append_flags) == append_flags
27
- rescue IOError, Errno::EBADF
28
- false
22
+ # :startdoc:
23
+
24
+ # This reopens ALL logfiles in the process that have been rotated
25
+ # using logrotate(8) (without copytruncate) or similar tools.
26
+ # A +File+ object is considered for reopening if it is:
27
+ # 1) opened with the O_APPEND and O_WRONLY flags
28
+ # 2) opened with an absolute path (starts with "/")
29
+ # 3) the current open file handle does not match its original open path
30
+ # 4) unbuffered (as far as userspace buffering goes, not O_SYNC)
31
+ # Returns the number of files reopened
32
+ def self.reopen_logs
33
+ to_reopen = []
34
+ nr = 0
35
+ ObjectSpace.each_object(File) { |fp| is_log?(fp) and to_reopen << fp }
36
+
37
+ to_reopen.each do |fp|
38
+ orig_st = begin
39
+ fp.stat
40
+ rescue IOError, Errno::EBADF
41
+ next
29
42
  end
30
43
 
31
- def chown_logs(uid, gid)
32
- ObjectSpace.each_object(File) do |fp|
33
- fp.chown(uid, gid) if is_log?(fp)
34
- end
44
+ begin
45
+ b = File.stat(fp.path)
46
+ next if orig_st.ino == b.ino && orig_st.dev == b.dev
47
+ rescue Errno::ENOENT
35
48
  end
36
49
 
37
- # This reopens ALL logfiles in the process that have been rotated
38
- # using logrotate(8) (without copytruncate) or similar tools.
39
- # A +File+ object is considered for reopening if it is:
40
- # 1) opened with the O_APPEND and O_WRONLY flags
41
- # 2) opened with an absolute path (starts with "/")
42
- # 3) the current open file handle does not match its original open path
43
- # 4) unbuffered (as far as userspace buffering goes, not O_SYNC)
44
- # Returns the number of files reopened
45
- def reopen_logs
46
- to_reopen = []
47
- nr = 0
48
- ObjectSpace.each_object(File) { |fp| is_log?(fp) and to_reopen << fp }
49
-
50
- to_reopen.each do |fp|
51
- orig_st = begin
52
- fp.stat
53
- rescue IOError, Errno::EBADF
54
- next
55
- end
56
-
57
- begin
58
- b = File.stat(fp.path)
59
- next if orig_st.ino == b.ino && orig_st.dev == b.dev
60
- rescue Errno::ENOENT
61
- end
62
-
63
- begin
64
- File.open(fp.path, 'a') { |tmpfp| fp.reopen(tmpfp) }
65
- fp.sync = true
66
- new_st = fp.stat
67
-
68
- # this should only happen in the master:
69
- if orig_st.uid != new_st.uid || orig_st.gid != new_st.gid
70
- fp.chown(orig_st.uid, orig_st.gid)
71
- end
50
+ begin
51
+ File.open(fp.path, 'a') { |tmpfp| fp.reopen(tmpfp) }
52
+ fp.sync = true
53
+ new_st = fp.stat
72
54
 
73
- nr += 1
74
- rescue IOError, Errno::EBADF
75
- # not much we can do...
76
- end
55
+ # this should only happen in the master:
56
+ if orig_st.uid != new_st.uid || orig_st.gid != new_st.gid
57
+ fp.chown(orig_st.uid, orig_st.gid)
77
58
  end
78
59
 
79
- nr
60
+ nr += 1
61
+ rescue IOError, Errno::EBADF
62
+ # not much we can do...
80
63
  end
81
-
82
- # creates and returns a new File object. The File is unlinked
83
- # immediately, switched to binary mode, and userspace output
84
- # buffering is disabled
85
- def tmpio
86
- fp = begin
87
- TmpIO.open("#{Dir::tmpdir}/#{rand}",
88
- File::RDWR|File::CREAT|File::EXCL, 0600)
89
- rescue Errno::EEXIST
90
- retry
91
- end
92
- File.unlink(fp.path)
93
- fp.binmode
94
- fp.sync = true
95
- fp
96
- end
97
-
98
64
  end
99
-
65
+ nr
100
66
  end
101
67
  end
@@ -0,0 +1,40 @@
1
+ # -*- encoding: binary -*-
2
+
3
+ # This class and its members can be considered a stable interface
4
+ # and will not change in a backwards-incompatible fashion between
5
+ # releases of Unicorn. You may need to access it in the
6
+ # before_fork/after_fork hooks. See the Unicorn::Configurator RDoc
7
+ # for examples.
8
+ class Unicorn::Worker < Struct.new(:nr, :tmp, :switched)
9
+
10
+ # worker objects may be compared to just plain numbers
11
+ def ==(other_nr)
12
+ self.nr == other_nr
13
+ end
14
+
15
+ # Changes the worker process to the specified +user+ and +group+
16
+ # This is only intended to be called from within the worker
17
+ # process from the +after_fork+ hook. This should be called in
18
+ # the +after_fork+ hook after any priviledged functions need to be
19
+ # run (e.g. to set per-worker CPU affinity, niceness, etc)
20
+ #
21
+ # Any and all errors raised within this method will be propagated
22
+ # directly back to the caller (usually the +after_fork+ hook.
23
+ # These errors commonly include ArgumentError for specifying an
24
+ # invalid user/group and Errno::EPERM for insufficient priviledges
25
+ def user(user, group = nil)
26
+ # we do not protect the caller, checking Process.euid == 0 is
27
+ # insufficient because modern systems have fine-grained
28
+ # capabilities. Let the caller handle any and all errors.
29
+ uid = Etc.getpwnam(user).uid
30
+ gid = Etc.getgrnam(group).gid if group
31
+ Unicorn::Util.chown_logs(uid, gid)
32
+ tmp.chown(uid, gid)
33
+ if gid && Process.egid != gid
34
+ Process.initgroups(user, gid)
35
+ Process::GID.change_privilege(gid)
36
+ end
37
+ Process.euid != uid and Process::UID.change_privilege(uid)
38
+ self.switched = true
39
+ end
40
+ end