unicorn 1.1.7 → 2.0.0pre1

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