unicorn 0.8.4 → 0.9.0

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/README CHANGED
@@ -131,16 +131,28 @@ regarding this.
131
131
 
132
132
  == Known Issues
133
133
 
134
- * WONTFIX: code reloading with Sinatra 0.3.2 (and likely older
134
+ * WONTFIX: code reloading and restarts with Sinatra 0.3.x (and likely older
135
135
  versions) apps is broken. The workaround is to force production
136
- mode to disable code reloading in your Sinatra application:
136
+ mode to disable code reloading as well as disabling "run" in your
137
+ Sinatra application:
137
138
  set :env, :production
138
- Since this is no longer an issue with Sinatra 0.9.x apps and only
139
- affected non-production instances, this will not be fixed on our end.
140
- Also remember we're capable of replacing the running binary without
141
- dropping any connections regardless of framework :)
139
+ set :run, false
140
+ Since this is no longer an issue with Sinatra 0.9.x apps, this will not be
141
+ fixed on our end. Since Unicorn is itself the application launcher, the
142
+ at_exit handler used in old Sinatra always caused Mongrel to be launched
143
+ whenever a Unicorn worker was about to exit.
144
+
145
+ Also remember we're capable of replacing the running binary without dropping
146
+ any connections regardless of framework :)
142
147
 
143
148
  == Contact
144
149
 
145
- Email Eric Wong at normalperson@yhbt.net for now.
146
- Newsgroup and mailing list maybe coming...
150
+ All feedback (bug reports, user/development dicussion, patches, pull
151
+ requests) go to the mailing list. Patches must be sent inline
152
+ (git format-patch -M + git send-email). No subscription is necessary
153
+ to post on the mailing list. No top posting. Address replies +To:+ (or
154
+ +Cc:+) the original sender and +Cc:+ the mailing list.
155
+
156
+ * email: mongrel-unicorn@rubyforge.org
157
+ * archives: http://rubyforge.org/pipermail/mongrel-unicorn/
158
+ * subscribe: http://rubyforge.org/mailman/listinfo/mongrel-unicorn/
data/TODO CHANGED
@@ -1,17 +1,9 @@
1
- == 1.0.0
1
+ * integration tests with nginx including bad client handling
2
2
 
3
- * integration tests with nginx including bad client handling
3
+ * manpages (why do so few Ruby executables come with proper manpages?)
4
4
 
5
- * manpages (why do so few Ruby executables come with proper manpages?)
5
+ * code cleanups (launchers)
6
6
 
7
- == 1.1.0
7
+ * Pure Ruby HTTP parser
8
8
 
9
- * Transfer-Encoding: chunked request handling. Testcase:
10
-
11
- curl -T- http://host:port/path < file_from_stdin
12
-
13
- * code cleanups (launchers)
14
-
15
- * Pure Ruby HTTP parser
16
-
17
- * Rubinius support?
9
+ * Rubinius support
data/examples/echo.ru ADDED
@@ -0,0 +1,32 @@
1
+ #\-E none
2
+ #
3
+ # Example application that echoes read data back to the HTTP client.
4
+ # This emulates the old echo protocol people used to run.
5
+ #
6
+ # An example of using this in a client would be to run:
7
+ # curl --no-buffer -T- http://host:port/
8
+ #
9
+ # Then type random stuff in your terminal to watch it get echoed back!
10
+
11
+ class EchoBody
12
+ def initialize(input)
13
+ @input = input
14
+ end
15
+
16
+ def each(&block)
17
+ while buf = @input.read(4096)
18
+ yield buf
19
+ end
20
+ self
21
+ end
22
+
23
+ def close
24
+ @input = nil
25
+ end
26
+ end
27
+
28
+ use Rack::Chunked
29
+ run lambda { |env|
30
+ [ 200, { 'Content-Type' => 'application/octet-stream' },
31
+ EchoBody.new(env['rack.input']) ]
32
+ }
data/examples/git.ru ADDED
@@ -0,0 +1,13 @@
1
+ #\-E none
2
+
3
+ # See http://thread.gmane.org/gmane.comp.web.curl.general/10473/raw on
4
+ # how to setup git for this. A better version of the above patch was
5
+ # accepted and committed on June 15, 2009, so you can pull the latest
6
+ # curl CVS snapshot to try this out.
7
+ require 'unicorn/app/inetd'
8
+
9
+ use Rack::Lint
10
+ use Rack::Chunked # important!
11
+ run Unicorn::App::Inetd.new(
12
+ *%w(git daemon --verbose --inetd --export-all --base-path=/home/ew/unicorn)
13
+ )
@@ -324,10 +324,17 @@ static void header_done(void *data, const char *at, size_t length)
324
324
  }
325
325
  rb_hash_aset(req, global_server_name, server_name);
326
326
  rb_hash_aset(req, global_server_port, server_port);
327
+ rb_hash_aset(req, global_server_protocol, global_server_protocol_value);
327
328
 
328
329
  /* grab the initial body and stuff it into the hash */
329
- rb_hash_aset(req, sym_http_body, rb_str_new(at, length));
330
- rb_hash_aset(req, global_server_protocol, global_server_protocol_value);
330
+ temp = rb_hash_aref(req, global_request_method);
331
+ if (temp != Qnil) {
332
+ long len = RSTRING_LEN(temp);
333
+ char *ptr = RSTRING_PTR(temp);
334
+
335
+ if (memcmp(ptr, "HEAD", len) && memcmp(ptr, "GET", len))
336
+ rb_hash_aset(req, sym_http_body, rb_str_new(at, length));
337
+ }
331
338
  }
332
339
 
333
340
  static void HttpParser_free(void *data) {
data/lib/unicorn.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'fcntl'
2
+ require 'tempfile'
2
3
  require 'unicorn/socket_helper'
3
4
  autoload :Rack, 'rack'
4
5
 
@@ -10,8 +11,15 @@ module Unicorn
10
11
  autoload :HttpRequest, 'unicorn/http_request'
11
12
  autoload :HttpResponse, 'unicorn/http_response'
12
13
  autoload :Configurator, 'unicorn/configurator'
14
+ autoload :TeeInput, 'unicorn/tee_input'
15
+ autoload :ChunkedReader, 'unicorn/chunked_reader'
16
+ autoload :TrailerParser, 'unicorn/trailer_parser'
13
17
  autoload :Util, 'unicorn/util'
14
18
 
19
+ Z = '' # the stock empty string we use everywhere...
20
+ Z.force_encoding(Encoding::BINARY) if Z.respond_to?(:force_encoding)
21
+ Z.freeze
22
+
15
23
  class << self
16
24
  def run(app, options = {})
17
25
  HttpServer.new(app, options).start.join
@@ -51,10 +59,11 @@ module Unicorn
51
59
  # don't rely on Dir.pwd here since it's not symlink-aware, and
52
60
  # symlink dirs are the default with Capistrano...
53
61
  :cwd => `/bin/sh -c pwd`.chomp("\n"),
54
- 0 => $0.dup,
62
+ :zero => $0.dup,
55
63
  }
56
64
 
57
- class Worker < Struct.new(:nr, :tmp)
65
+ Worker = Struct.new(:nr, :tempfile) unless defined?(Worker)
66
+ class Worker
58
67
  # worker objects may be compared to just plain numbers
59
68
  def ==(other_nr)
60
69
  self.nr == other_nr
@@ -322,7 +331,7 @@ module Unicorn
322
331
  self.pid = @pid.chomp('.oldbin') if @pid
323
332
  proc_name 'master'
324
333
  else
325
- worker = WORKERS.delete(pid) and worker.tmp.close rescue nil
334
+ worker = WORKERS.delete(pid) and worker.tempfile.close rescue nil
326
335
  logger.info "reaped #{status.inspect} " \
327
336
  "worker=#{worker.nr rescue 'unknown'}"
328
337
  end
@@ -362,7 +371,7 @@ module Unicorn
362
371
  listener_fds = LISTENERS.map { |sock| sock.fileno }
363
372
  ENV['UNICORN_FD'] = listener_fds.join(',')
364
373
  Dir.chdir(START_CTX[:cwd])
365
- cmd = [ START_CTX[0] ].concat(START_CTX[:argv])
374
+ cmd = [ START_CTX[:zero] ] + START_CTX[:argv]
366
375
 
367
376
  # avoid leaking FDs we don't know about, but let before_exec
368
377
  # unset FD_CLOEXEC, if anything else in the app eventually
@@ -382,16 +391,16 @@ module Unicorn
382
391
  end
383
392
 
384
393
  # forcibly terminate all workers that haven't checked in in @timeout
385
- # seconds. The timeout is implemented using an unlinked File
394
+ # seconds. The timeout is implemented using an unlinked tempfile
386
395
  # shared between the parent process and each worker. The worker
387
- # runs File#chmod to modify the ctime of the File. If the ctime
396
+ # runs File#chmod to modify the ctime of the tempfile. If the ctime
388
397
  # is stale for >@timeout seconds, then we'll kill the corresponding
389
398
  # worker.
390
399
  def murder_lazy_workers
391
400
  diff = stat = nil
392
401
  WORKERS.dup.each_pair do |pid, worker|
393
402
  stat = begin
394
- worker.tmp.stat
403
+ worker.tempfile.stat
395
404
  rescue => e
396
405
  logger.warn "worker=#{worker.nr} PID:#{pid} stat error: #{e.inspect}"
397
406
  kill_worker(:QUIT, pid)
@@ -415,7 +424,9 @@ module Unicorn
415
424
  SIG_QUEUE << :QUIT # forcibly emulate SIGQUIT
416
425
  return
417
426
  end
418
- worker = Worker.new(worker_nr, Unicorn::Util.tmpio)
427
+ tempfile = Tempfile.new(nil) # as short as possible to save dir space
428
+ tempfile.unlink # don't allow other processes to find or see it
429
+ worker = Worker.new(worker_nr, tempfile)
419
430
  @before_fork.call(self, worker)
420
431
  pid = fork { worker_loop(worker) }
421
432
  WORKERS[pid] = worker
@@ -433,8 +444,15 @@ module Unicorn
433
444
  # once a client is accepted, it is processed in its entirety here
434
445
  # in 3 easy steps: read request, call app, write app response
435
446
  def process_client(app, client)
436
- client.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
437
- HttpResponse.write(client, app.call(REQUEST.read(client)))
447
+ response = app.call(env = REQUEST.read(client))
448
+
449
+ if 100 == response.first.to_i
450
+ client.write(Const::EXPECT_100_RESPONSE)
451
+ env.delete(Const::HTTP_EXPECT)
452
+ response = app.call(env)
453
+ end
454
+
455
+ HttpResponse.write(client, response)
438
456
  # if we get any error, try to write something back to the client
439
457
  # assuming we haven't closed the socket, but don't get hung up
440
458
  # if the socket is already closed or broken. We'll always ensure
@@ -457,16 +475,16 @@ module Unicorn
457
475
  # traps for USR1, USR2, and HUP may be set in the @after_fork Proc
458
476
  # by the user.
459
477
  def init_worker_process(worker)
460
- QUEUE_SIGS.each { |sig| trap(sig, nil) }
478
+ QUEUE_SIGS.each { |sig| trap(sig, 'IGNORE') }
461
479
  trap(:CHLD, 'DEFAULT')
462
480
  SIG_QUEUE.clear
463
481
  proc_name "worker[#{worker.nr}]"
464
482
  START_CTX.clear
465
483
  init_self_pipe!
466
- WORKERS.values.each { |other| other.tmp.close rescue nil }
484
+ WORKERS.values.each { |other| other.tempfile.close! rescue nil }
467
485
  WORKERS.clear
468
486
  LISTENERS.each { |sock| sock.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) }
469
- worker.tmp.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
487
+ worker.tempfile.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
470
488
  @after_fork.call(self, worker) # can drop perms
471
489
  @timeout /= 2.0 # halve it for select()
472
490
  build_app! unless @preload_app
@@ -486,7 +504,7 @@ module Unicorn
486
504
  master_pid = Process.ppid # slightly racy, but less memory usage
487
505
  init_worker_process(worker)
488
506
  nr = 0 # this becomes negative if we need to reopen logs
489
- alive = worker.tmp # tmp is our lifeline to the master process
507
+ alive = worker.tempfile # tempfile is our lifeline to the master process
490
508
  ready = LISTENERS
491
509
  t = ti = 0
492
510
 
@@ -552,7 +570,7 @@ module Unicorn
552
570
  begin
553
571
  Process.kill(signal, pid)
554
572
  rescue Errno::ESRCH
555
- worker = WORKERS.delete(pid) and worker.tmp.close rescue nil
573
+ worker = WORKERS.delete(pid) and worker.tempfile.close rescue nil
556
574
  end
557
575
  end
558
576
 
@@ -613,8 +631,8 @@ module Unicorn
613
631
  end
614
632
 
615
633
  def proc_name(tag)
616
- $0 = ([ File.basename(START_CTX[0]), tag
617
- ]).concat(START_CTX[:argv]).join(' ')
634
+ $0 = ([ File.basename(START_CTX[:zero]), tag ] +
635
+ START_CTX[:argv]).join(' ')
618
636
  end
619
637
 
620
638
  def redirect_io(io, path)
@@ -42,8 +42,11 @@ module Unicorn::App
42
42
 
43
43
  # Calls the app
44
44
  def call(env)
45
- out, err = Unicorn::Util.tmpio, Unicorn::Util.tmpio
45
+ out, err = Tempfile.new(nil), Tempfile.new(nil)
46
+ out.unlink
47
+ err.unlink
46
48
  inp = force_file_input(env)
49
+ out.sync = err.sync = true
47
50
  pid = fork { run_child(inp, out, err, env) }
48
51
  inp.close
49
52
  pid, status = Process.waitpid2(pid)
@@ -118,15 +121,15 @@ module Unicorn::App
118
121
  # ensures rack.input is a file handle that we can redirect stdin to
119
122
  def force_file_input(env)
120
123
  inp = env['rack.input']
121
- if inp.respond_to?(:fileno) && Integer === inp.fileno
122
- inp
123
- elsif inp.size == 0 # inp could be a StringIO or StringIO-like object
124
+ if inp.size == 0 # inp could be a StringIO or StringIO-like object
124
125
  ::File.open('/dev/null', 'rb')
125
126
  else
126
- tmp = Unicorn::Util.tmpio
127
+ tmp = Tempfile.new(nil)
128
+ tmp.unlink
129
+ tmp.binmode
130
+ tmp.sync = true
127
131
 
128
- # Rack::Lint::InputWrapper doesn't allow sysread :(
129
- buf = Unicorn::Z.dup
132
+ buf = Z.dup
130
133
  while inp.read(CHUNK_SIZE, buf)
131
134
  tmp.syswrite(buf)
132
135
  end
@@ -0,0 +1,108 @@
1
+ # Copyright (c) 2009 Eric Wong
2
+ # You can redistribute it and/or modify it under the same terms as Ruby.
3
+
4
+ # this class *must* be used with Rack::Chunked
5
+
6
+ module Unicorn::App
7
+ class Inetd
8
+
9
+ class CatBody
10
+ def initialize(env, cmd)
11
+ @cmd = cmd
12
+ @input, @errors = env['rack.input'], env['rack.errors']
13
+ in_rd, in_wr = IO.pipe
14
+ @err_rd, err_wr = IO.pipe
15
+ @out_rd, out_wr = IO.pipe
16
+
17
+ @cmd_pid = fork {
18
+ inp, out, err = (0..2).map { |i| IO.new(i) }
19
+ inp.reopen(in_rd)
20
+ out.reopen(out_wr)
21
+ err.reopen(err_wr)
22
+ [ in_rd, in_wr, @err_rd, err_wr, @out_rd, out_wr ].each { |io|
23
+ io.close
24
+ }
25
+ exec(*cmd)
26
+ }
27
+ [ in_rd, err_wr, out_wr ].each { |io| io.close }
28
+ [ in_wr, @err_rd, @out_rd ].each { |io| io.binmode }
29
+ in_wr.sync = true
30
+
31
+ # Unfortunately, input here must be processed inside a seperate
32
+ # thread/process using blocking I/O since env['rack.input'] is not
33
+ # IO.select-able and attempting to make it so would trip Rack::Lint
34
+ @inp_pid = fork {
35
+ [ @err_rd, @out_rd ].each { |io| io.close }
36
+ buf = Unicorn::Z.dup
37
+
38
+ # this is dependent on @input.read having readpartial semantics:
39
+ while @input.read(16384, buf)
40
+ in_wr.write(buf)
41
+ end
42
+ in_wr.close
43
+ }
44
+ in_wr.close
45
+ end
46
+
47
+ def each(&block)
48
+ buf = Unicorn::Z.dup
49
+ begin
50
+ rd, = IO.select([@err_rd, @out_rd])
51
+ rd && rd.first or next
52
+
53
+ if rd.include?(@err_rd)
54
+ begin
55
+ @errors.write(@err_rd.read_nonblock(16384, buf))
56
+ rescue Errno::EINTR
57
+ rescue Errno::EAGAIN
58
+ break
59
+ end while true
60
+ end
61
+
62
+ rd.include?(@out_rd) or next
63
+
64
+ begin
65
+ yield @out_rd.read_nonblock(16384, buf)
66
+ rescue Errno::EINTR
67
+ rescue Errno::EAGAIN
68
+ break
69
+ end while true
70
+ rescue EOFError,Errno::EPIPE,Errno::EBADF,Errno::EINVAL
71
+ break
72
+ end while true
73
+
74
+ self
75
+ end
76
+
77
+ def close
78
+ @input = nil
79
+ [ [ @cmd.inspect, @cmd_pid ], [ 'input streamer', @inp_pid ]
80
+ ].each { |str, pid|
81
+ begin
82
+ pid, status = Process.waitpid2(pid)
83
+ status.success? or
84
+ @errors.write("#{str}: #{status.inspect} (PID:#{pid})\n")
85
+ rescue Errno::ECHILD
86
+ @errors.write("Failed to reap #{str} (PID:#{pid})\n")
87
+ end
88
+ }
89
+ end
90
+
91
+ end
92
+
93
+ def initialize(*cmd)
94
+ @cmd = cmd
95
+ end
96
+
97
+ def call(env)
98
+ expect = env[Unicorn::Const::HTTP_EXPECT] and
99
+ /\A100-continue\z/i =~ expect and
100
+ return [ 100, {} , [] ]
101
+
102
+ [ 200, { 'Content-Type' => 'application/octet-stream' },
103
+ CatBody.new(env, @cmd) ]
104
+ end
105
+
106
+ end
107
+
108
+ end
@@ -0,0 +1,94 @@
1
+ # Copyright (c) 2009 Eric Wong
2
+ # You can redistribute it and/or modify it under the same terms as Ruby.
3
+
4
+ require 'unicorn'
5
+ require 'unicorn/http11'
6
+
7
+ module Unicorn
8
+ class ChunkedReader
9
+
10
+ def initialize(env, input, buf)
11
+ @env, @input, @buf = env, input, buf
12
+ @chunk_left = 0
13
+ parse_chunk_header
14
+ end
15
+
16
+ def readpartial(max, buf = Z.dup)
17
+ while @input && @chunk_left <= 0 && ! parse_chunk_header
18
+ @buf << @input.readpartial(Const::CHUNK_SIZE, buf)
19
+ end
20
+
21
+ if @input
22
+ begin
23
+ @buf << @input.read_nonblock(Const::CHUNK_SIZE, buf)
24
+ rescue Errno::EAGAIN, Errno::EINTR
25
+ end
26
+ end
27
+
28
+ max = @chunk_left if max > @chunk_left
29
+ buf.replace(last_block(max) || Z)
30
+ @chunk_left -= buf.size
31
+ (0 == buf.size && @input.nil?) and raise EOFError
32
+ buf
33
+ end
34
+
35
+ def gets
36
+ line = nil
37
+ begin
38
+ line = readpartial(Const::CHUNK_SIZE)
39
+ begin
40
+ if line.sub!(%r{\A(.*?#{$/})}, Z)
41
+ @chunk_left += line.size
42
+ @buf = @buf ? (line << @buf) : line
43
+ return $1.dup
44
+ end
45
+ line << readpartial(Const::CHUNK_SIZE)
46
+ end while true
47
+ rescue EOFError
48
+ return line
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def last_block(max = nil)
55
+ rv = @buf
56
+ if max && rv && max < rv.size
57
+ @buf = rv[max - rv.size, rv.size - max]
58
+ return rv[0, max]
59
+ end
60
+ @buf = Z.dup
61
+ rv
62
+ end
63
+
64
+ def parse_chunk_header
65
+ buf = @buf
66
+ # ignoring chunk-extension info for now, I haven't seen any use for it
67
+ # (or any users, and TE:chunked sent by clients is rare already)
68
+ # if there was not enough data in buffer to parse length of the chunk
69
+ # then just return
70
+ if buf.sub!(/\A(?:\r\n)?([a-fA-F0-9]{1,8})[^\r]*?\r\n/, Z)
71
+ @chunk_left = $1.to_i(16)
72
+ if 0 == @chunk_left # EOF
73
+ buf.sub!(/\A\r\n(?:\r\n)?/, Z) # cleanup for future requests
74
+ if trailer = @env[Const::HTTP_TRAILER]
75
+ tp = TrailerParser.new(trailer)
76
+ while ! tp.execute!(@env, buf)
77
+ buf << @input.readpartial(Const::CHUNK_SIZE)
78
+ end
79
+ end
80
+ @input = nil
81
+ end
82
+ return @chunk_left
83
+ end
84
+
85
+ buf.size > 256 and
86
+ raise HttpParserError,
87
+ "malformed chunk, chunk-length not found in buffer: " \
88
+ "#{buf.inspect}"
89
+ nil
90
+ end
91
+
92
+ end
93
+
94
+ end