unicorn 0.8.4 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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