unicorn 3.7.0 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/DESIGN CHANGED
@@ -76,14 +76,6 @@
76
76
  Applications that use threads continue to work if Unicorn
77
77
  is only serving LAN or localhost clients.
78
78
 
79
- * Timeout implementation is done via fchmod(2) in each worker
80
- on a shared file descriptor to update st_ctime on the inode.
81
- Master process wakeups for checking on timeouts is throttled
82
- one a second to minimize the performance impact and simplify
83
- the code path within the worker. Neither futimes(2) nor
84
- pwrite(2)/pread(2) are supported by base MRI, nor are they as
85
- portable on UNIX systems as fchmod(2).
86
-
87
79
  * SIGKILL is used to terminate the timed-out workers from misbehaving apps
88
80
  as reliably as possible on a UNIX system. The default timeout is a
89
81
  generous 60 seconds (same default as in Mongrel).
data/GIT-VERSION-GEN CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/bin/sh
2
2
 
3
3
  GVF=GIT-VERSION-FILE
4
- DEF_VER=v3.7.0.GIT
4
+ DEF_VER=v4.0.0.GIT
5
5
 
6
6
  LF='
7
7
  '
data/TODO CHANGED
@@ -2,6 +2,4 @@
2
2
 
3
3
  * improve test suite
4
4
 
5
- * scalability to >= 1024 worker processes for crazy NUMA systems
6
-
7
5
  * Rack 2.x support (when Rack 2.x exists)
data/bin/unicorn CHANGED
@@ -106,6 +106,7 @@ op = OptionParser.new("", 24, ' ') do |opts|
106
106
  end
107
107
 
108
108
  app = Unicorn.builder(ARGV[0] || 'config.ru', op)
109
+ op = nil
109
110
 
110
111
  if $DEBUG
111
112
  require 'pp'
@@ -117,4 +118,4 @@ if $DEBUG
117
118
  end
118
119
 
119
120
  Unicorn::Launcher.daemonize!(options) if rackup_opts[:daemonize]
120
- Unicorn.run(app, options)
121
+ Unicorn::HttpServer.new(app, options).start.join
data/bin/unicorn_rails CHANGED
@@ -186,6 +186,7 @@ def rails_builder(ru, op, daemonize)
186
186
  end
187
187
 
188
188
  app = rails_builder(ARGV[0], op, rackup_opts[:daemonize])
189
+ op = nil
189
190
 
190
191
  if $DEBUG
191
192
  require 'pp'
@@ -205,4 +206,4 @@ if rackup_opts[:daemonize]
205
206
  options[:pid] = "tmp/pids/unicorn.pid"
206
207
  Unicorn::Launcher.daemonize!(options)
207
208
  end
208
- Unicorn.run(app, options)
209
+ Unicorn::HttpServer.new(app, options).start.join
@@ -629,6 +629,34 @@ static VALUE HttpParser_clear(VALUE self)
629
629
  return self;
630
630
  }
631
631
 
632
+ /**
633
+ * call-seq:
634
+ * parser.dechunk! => parser
635
+ *
636
+ * Resets the parser to a state suitable for dechunking response bodies
637
+ *
638
+ */
639
+ static VALUE HttpParser_dechunk_bang(VALUE self)
640
+ {
641
+ struct http_parser *hp = data_get(self);
642
+
643
+ http_parser_init(hp);
644
+
645
+ /*
646
+ * we don't care about trailers in dechunk-only mode,
647
+ * but if we did we'd set UH_FL_HASTRAILER and clear hp->env
648
+ */
649
+ if (0) {
650
+ rb_funcall(hp->env, id_clear, 0);
651
+ hp->flags = UH_FL_HASTRAILER;
652
+ }
653
+
654
+ hp->flags |= UH_FL_HASBODY | UH_FL_INBODY | UH_FL_CHUNKED;
655
+ hp->cs = http_parser_en_ChunkedBody;
656
+
657
+ return self;
658
+ }
659
+
632
660
  /**
633
661
  * call-seq:
634
662
  * parser.reset => nil
@@ -856,72 +884,80 @@ static VALUE HttpParser_env(VALUE self)
856
884
 
857
885
  /**
858
886
  * call-seq:
859
- * parser.filter_body(buf, data) => nil/data
887
+ * parser.filter_body(dst, src) => nil/src
860
888
  *
861
- * Takes a String of +data+, will modify data if dechunking is done.
889
+ * Takes a String of +src+, will modify data if dechunking is done.
862
890
  * Returns +nil+ if there is more data left to process. Returns
863
- * +data+ if body processing is complete. When returning +data+,
864
- * it may modify +data+ so the start of the string points to where
891
+ * +src+ if body processing is complete. When returning +src+,
892
+ * it may modify +src+ so the start of the string points to where
865
893
  * the body ended so that trailer processing can begin.
866
894
  *
867
895
  * Raises HttpParserError if there are dechunking errors.
868
- * Basically this is a glorified memcpy(3) that copies +data+
896
+ * Basically this is a glorified memcpy(3) that copies +src+
869
897
  * into +buf+ while filtering it through the dechunker.
870
898
  */
871
- static VALUE HttpParser_filter_body(VALUE self, VALUE buf, VALUE data)
899
+ static VALUE HttpParser_filter_body(VALUE self, VALUE dst, VALUE src)
872
900
  {
873
901
  struct http_parser *hp = data_get(self);
874
- char *dptr;
875
- long dlen;
902
+ char *srcptr;
903
+ long srclen;
876
904
 
877
- dptr = RSTRING_PTR(data);
878
- dlen = RSTRING_LEN(data);
905
+ srcptr = RSTRING_PTR(src);
906
+ srclen = RSTRING_LEN(src);
879
907
 
880
- StringValue(buf);
881
- rb_str_modify(buf);
882
- rb_str_resize(buf, dlen); /* we can never copy more than dlen bytes */
883
- OBJ_TAINT(buf); /* keep weirdo $SAFE users happy */
908
+ StringValue(dst);
884
909
 
885
910
  if (HP_FL_TEST(hp, CHUNKED)) {
886
911
  if (!chunked_eof(hp)) {
912
+ rb_str_modify(dst);
913
+ rb_str_resize(dst, srclen); /* we can never copy more than srclen bytes */
914
+
887
915
  hp->s.dest_offset = 0;
888
- hp->cont = buf;
889
- hp->buf = data;
890
- http_parser_execute(hp, dptr, dlen);
916
+ hp->cont = dst;
917
+ hp->buf = src;
918
+ http_parser_execute(hp, srcptr, srclen);
891
919
  if (hp->cs == http_parser_error)
892
920
  parser_raise(eHttpParserError, "Invalid HTTP format, parsing fails.");
893
921
 
894
922
  assert(hp->s.dest_offset <= hp->offset &&
895
923
  "destination buffer overflow");
896
- advance_str(data, hp->offset);
897
- rb_str_set_len(buf, hp->s.dest_offset);
924
+ advance_str(src, hp->offset);
925
+ rb_str_set_len(dst, hp->s.dest_offset);
898
926
 
899
- if (RSTRING_LEN(buf) == 0 && chunked_eof(hp)) {
927
+ if (RSTRING_LEN(dst) == 0 && chunked_eof(hp)) {
900
928
  assert(hp->len.chunk == 0 && "chunk at EOF but more to parse");
901
929
  } else {
902
- data = Qnil;
930
+ src = Qnil;
903
931
  }
904
932
  }
905
933
  } else {
906
934
  /* no need to enter the Ragel machine for unchunked transfers */
907
935
  assert(hp->len.content >= 0 && "negative Content-Length");
908
936
  if (hp->len.content > 0) {
909
- long nr = MIN(dlen, hp->len.content);
910
-
911
- hp->buf = data;
912
- memcpy(RSTRING_PTR(buf), dptr, nr);
937
+ long nr = MIN(srclen, hp->len.content);
938
+
939
+ rb_str_modify(dst);
940
+ rb_str_resize(dst, nr);
941
+ /*
942
+ * using rb_str_replace() to avoid memcpy() doesn't help in
943
+ * most cases because a GC-aware programmer will pass an explicit
944
+ * buffer to env["rack.input"].read and reuse the buffer in a loop.
945
+ * This causes copy-on-write behavior to be triggered anyways
946
+ * when the +src+ buffer is modified (when reading off the socket).
947
+ */
948
+ hp->buf = src;
949
+ memcpy(RSTRING_PTR(dst), srcptr, nr);
913
950
  hp->len.content -= nr;
914
951
  if (hp->len.content == 0) {
915
952
  HP_FL_SET(hp, REQEOF);
916
953
  hp->cs = http_parser_first_final;
917
954
  }
918
- advance_str(data, nr);
919
- rb_str_set_len(buf, nr);
920
- data = Qnil;
955
+ advance_str(src, nr);
956
+ src = Qnil;
921
957
  }
922
958
  }
923
959
  hp->offset = 0; /* for trailer parsing */
924
- return data;
960
+ return src;
925
961
  }
926
962
 
927
963
  #define SET_GLOBAL(var,str) do { \
@@ -947,6 +983,7 @@ void Init_unicorn_http(void)
947
983
  rb_define_method(cHttpParser, "initialize", HttpParser_init, 0);
948
984
  rb_define_method(cHttpParser, "clear", HttpParser_clear, 0);
949
985
  rb_define_method(cHttpParser, "reset", HttpParser_reset, 0);
986
+ rb_define_method(cHttpParser, "dechunk!", HttpParser_dechunk_bang, 0);
950
987
  rb_define_method(cHttpParser, "parse", HttpParser_parse, 0);
951
988
  rb_define_method(cHttpParser, "add_parse", HttpParser_add_parse, 1);
952
989
  rb_define_method(cHttpParser, "headers", HttpParser_headers, 2);
data/lib/unicorn.rb CHANGED
@@ -26,17 +26,14 @@ module Unicorn
26
26
  end
27
27
 
28
28
  # :stopdoc:
29
- def self.run(app, options = {})
30
- Unicorn::HttpServer.new(app, options).start.join
31
- end
32
29
 
33
30
  # This returns a lambda to pass in as the app, this does not "build" the
34
31
  # app (which we defer based on the outcome of "preload_app" in the
35
32
  # Unicorn config). The returned lambda will be called when it is
36
33
  # time to build the app.
37
- def self.builder(ru, opts)
34
+ def self.builder(ru, op)
38
35
  # allow Configurator to parse cli switches embedded in the ru file
39
- Unicorn::Configurator::RACKUP.update(:file => ru, :optparse => opts)
36
+ op = Unicorn::Configurator::RACKUP.merge!(:file => ru, :optparse => op)
40
37
 
41
38
  # always called after config file parsing, may be called after forking
42
39
  lambda do ||
@@ -44,7 +41,7 @@ module Unicorn
44
41
  when /\.ru$/
45
42
  raw = File.read(ru)
46
43
  raw.sub!(/^__END__\n.*/, '')
47
- eval("Rack::Builder.new {(#{raw}\n)}.to_app", TOPLEVEL_BINDING, ru)
44
+ eval("Rack::Builder.new {(\n#{raw}\n)}.to_app", TOPLEVEL_BINDING, ru)
48
45
  else
49
46
  require ru
50
47
  Object.const_get(File.basename(ru, '.rb').capitalize)
@@ -80,6 +77,11 @@ module Unicorn
80
77
  Unicorn::SocketHelper.sock_name(io)
81
78
  end
82
79
  end
80
+
81
+ def self.log_error(logger, message, exc)
82
+ logger.error "#{message}: #{exc.message} (#{exc.class})"
83
+ exc.backtrace.each { |line| logger.error(line) }
84
+ end
83
85
  # :startdoc:
84
86
  end
85
87
  # :enddoc:
@@ -264,8 +264,9 @@ class Unicorn::Configurator
264
264
  #
265
265
  # This has no effect on UNIX sockets.
266
266
  #
267
- # Default: +false+ (Nagle's algorithm enabled) in \Unicorn,
268
- # +true+ in Rainbows!
267
+ # Default: +true+ (Nagle's algorithm disabled) in \Unicorn,
268
+ # +true+ in Rainbows! This defaulted to +false+ in \Unicorn
269
+ # 3.x
269
270
  #
270
271
  # [:tcp_nopush => true or false]
271
272
  #
@@ -279,7 +280,8 @@ class Unicorn::Configurator
279
280
  #
280
281
  # This has no effect on UNIX sockets.
281
282
  #
282
- # Default: +true+ in \Unicorn 3.4+, +false+ in Rainbows!
283
+ # Default: +false+
284
+ # This defaulted to +true+ in \Unicorn 3.4 - 3.7
283
285
  #
284
286
  # [:ipv6only => true or false]
285
287
  #
data/lib/unicorn/const.rb CHANGED
@@ -8,8 +8,8 @@
8
8
  # improve things much compared to constants.
9
9
  module Unicorn::Const
10
10
 
11
- # The current version of Unicorn, currently 3.7.0
12
- UNICORN_VERSION = "3.7.0"
11
+ # The current version of Unicorn, currently 4.0.0
12
+ UNICORN_VERSION = "4.0.0"
13
13
 
14
14
  # default TCP listen host address (0.0.0.0, all interfaces)
15
15
  DEFAULT_HOST = "0.0.0.0"
@@ -97,7 +97,7 @@ class Unicorn::HttpServer
97
97
  self.reexec_pid = 0
98
98
  options = options.dup
99
99
  @ready_pipe = options.delete(:ready_pipe)
100
- self.init_listeners = options[:listeners] ? options[:listeners].dup : []
100
+ @init_listeners = options[:listeners] ? options[:listeners].dup : []
101
101
  options[:use_defaults] = true
102
102
  self.config = Unicorn::Configurator.new(options)
103
103
  self.listener_opts = {}
@@ -118,35 +118,7 @@ class Unicorn::HttpServer
118
118
 
119
119
  # Runs the thing. Returns self so you can run join on it
120
120
  def start
121
- BasicSocket.do_not_reverse_lookup = true
122
-
123
- # inherit sockets from parents, they need to be plain Socket objects
124
- # before they become Kgio::UNIXServer or Kgio::TCPServer
125
- inherited = ENV['UNICORN_FD'].to_s.split(/,/).map do |fd|
126
- io = Socket.for_fd(fd.to_i)
127
- set_server_sockopt(io, listener_opts[sock_name(io)])
128
- IO_PURGATORY << io
129
- logger.info "inherited addr=#{sock_name(io)} fd=#{fd}"
130
- server_cast(io)
131
- end
132
-
133
- config_listeners = config[:listeners].dup
134
- LISTENERS.replace(inherited)
135
-
136
- # we start out with generic Socket objects that get cast to either
137
- # Kgio::TCPServer or Kgio::UNIXServer objects; but since the Socket
138
- # objects share the same OS-level file descriptor as the higher-level
139
- # *Server objects; we need to prevent Socket objects from being
140
- # garbage-collected
141
- config_listeners -= listener_names
142
- if config_listeners.empty? && LISTENERS.empty?
143
- config_listeners << Unicorn::Const::DEFAULT_LISTEN
144
- init_listeners << Unicorn::Const::DEFAULT_LISTEN
145
- START_CTX[:argv] << "-l#{Unicorn::Const::DEFAULT_LISTEN}"
146
- end
147
- config_listeners.each { |addr| listen(addr) }
148
- raise ArgumentError, "no listeners" if LISTENERS.empty?
149
-
121
+ inherit_listeners!
150
122
  # this pipe is used to wake us up from select(2) in #join when signals
151
123
  # are trapped. See trap_deferred.
152
124
  init_self_pipe!
@@ -160,7 +132,7 @@ class Unicorn::HttpServer
160
132
 
161
133
  self.master_pid = $$
162
134
  build_app! if preload_app
163
- maintain_worker_count
135
+ spawn_missing_workers
164
136
  self
165
137
  end
166
138
 
@@ -281,8 +253,7 @@ class Unicorn::HttpServer
281
253
  logger.info "master process ready" # test_exec.rb relies on this message
282
254
  if @ready_pipe
283
255
  @ready_pipe.syswrite($$.to_s)
284
- @ready_pipe.close rescue nil
285
- @ready_pipe = nil
256
+ @ready_pipe = @ready_pipe.close rescue nil
286
257
  end
287
258
  begin
288
259
  reap_all_workers
@@ -333,10 +304,8 @@ class Unicorn::HttpServer
333
304
  reexec
334
305
  end
335
306
  end
336
- rescue Errno::EINTR
337
307
  rescue => e
338
- logger.error "Unhandled master loop exception #{e.inspect}."
339
- logger.error e.backtrace.join("\n")
308
+ Unicorn.log_error(@logger, "master loop error", e)
340
309
  end while true
341
310
  stop # gracefully shutdown all workers on our way out
342
311
  logger.info "master complete"
@@ -403,7 +372,7 @@ class Unicorn::HttpServer
403
372
  self.pid = pid.chomp('.oldbin') if pid
404
373
  proc_name 'master'
405
374
  else
406
- worker = WORKERS.delete(wpid) and worker.tmp.close rescue nil
375
+ worker = WORKERS.delete(wpid) and worker.close rescue nil
407
376
  m = "reaped #{status.inspect} worker=#{worker.nr rescue 'unknown'}"
408
377
  status.success? ? logger.info(m) : logger.error(m)
409
378
  end
@@ -460,22 +429,17 @@ class Unicorn::HttpServer
460
429
  proc_name 'master (old)'
461
430
  end
462
431
 
463
- # forcibly terminate all workers that haven't checked in in timeout
464
- # seconds. The timeout is implemented using an unlinked File
465
- # shared between the parent process and each worker. The worker
466
- # runs File#chmod to modify the ctime of the File. If the ctime
467
- # is stale for >timeout seconds, then we'll kill the corresponding
468
- # worker.
432
+ # forcibly terminate all workers that haven't checked in in timeout seconds. The timeout is implemented using an unlinked File
469
433
  def murder_lazy_workers
470
434
  t = @timeout
471
435
  next_sleep = 1
436
+ now = Time.now.to_i
472
437
  WORKERS.dup.each_pair do |wpid, worker|
473
- stat = worker.tmp.stat
474
- # skip workers that disable fchmod or have never fchmod-ed
475
- stat.mode == 0100600 and next
476
- diff = Time.now - stat.ctime
477
- if diff <= t
478
- tmp = t - diff
438
+ tick = worker.tick
439
+ 0 == tick and next # skip workers that are sleeping
440
+ diff = now - tick
441
+ tmp = t - diff
442
+ if tmp >= 0
479
443
  next_sleep < tmp and next_sleep = tmp
480
444
  next
481
445
  end
@@ -488,7 +452,9 @@ class Unicorn::HttpServer
488
452
 
489
453
  def after_fork_internal
490
454
  @ready_pipe.close if @ready_pipe
491
- self.ready_pipe = nil # XXX Rainbows! compat, change for Unicorn 4.x
455
+ Unicorn::Configurator::RACKUP.clear
456
+ @ready_pipe = @init_listeners = @config = @before_exec = @before_fork = nil
457
+
492
458
  srand # http://redmine.ruby-lang.org/issues/4338
493
459
 
494
460
  # The OpenSSL PRNG is seeded with only the pid, and apps with frequently
@@ -497,15 +463,22 @@ class Unicorn::HttpServer
497
463
  end
498
464
 
499
465
  def spawn_missing_workers
500
- (0...worker_processes).each do |worker_nr|
501
- WORKERS.values.include?(worker_nr) and next
502
- worker = Worker.new(worker_nr, Unicorn::TmpIO.new)
466
+ worker_nr = -1
467
+ until (worker_nr += 1) == @worker_processes
468
+ WORKERS.value?(worker_nr) and next
469
+ worker = Worker.new(worker_nr)
503
470
  before_fork.call(self, worker)
504
- WORKERS[fork {
471
+ if pid = fork
472
+ WORKERS[pid] = worker
473
+ else
505
474
  after_fork_internal
506
475
  worker_loop(worker)
507
- }] = worker
476
+ exit
477
+ end
508
478
  end
479
+ rescue => e
480
+ @logger.error(e) rescue nil
481
+ exit!
509
482
  end
510
483
 
511
484
  def maintain_worker_count
@@ -531,8 +504,7 @@ class Unicorn::HttpServer
531
504
  when Unicorn::HttpParserError # try to tell the client they're bad
532
505
  Unicorn::Const::ERROR_400_RESPONSE
533
506
  else
534
- logger.error "Read error: #{e.inspect}"
535
- logger.error e.backtrace.join("\n")
507
+ Unicorn.log_error(@logger, "app error", e)
536
508
  Unicorn::Const::ERROR_500_RESPONSE
537
509
  end
538
510
  client.kgio_trywrite(msg)
@@ -568,10 +540,8 @@ class Unicorn::HttpServer
568
540
  proc_name "worker[#{worker.nr}]"
569
541
  START_CTX.clear
570
542
  init_self_pipe!
571
- WORKERS.values.each { |other| other.tmp.close rescue nil }
572
543
  WORKERS.clear
573
544
  LISTENERS.each { |sock| sock.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) }
574
- worker.tmp.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
575
545
  after_fork.call(self, worker) # can drop perms
576
546
  worker.user(*user) if user.kind_of?(Array) && ! worker.switched
577
547
  self.timeout /= 2.0 # halve it for select()
@@ -595,35 +565,25 @@ class Unicorn::HttpServer
595
565
  ppid = master_pid
596
566
  init_worker_process(worker)
597
567
  nr = 0 # this becomes negative if we need to reopen logs
598
- alive = worker.tmp # tmp is our lifeline to the master process
599
- ready = LISTENERS
568
+ l = LISTENERS.dup
569
+ ready = l.dup
600
570
 
601
571
  # closing anything we IO.select on will raise EBADF
602
572
  trap(:USR1) { nr = -65536; SELF_PIPE[0].close rescue nil }
603
- trap(:QUIT) { alive = nil; LISTENERS.each { |s| s.close rescue nil }.clear }
573
+ trap(:QUIT) { worker = nil; LISTENERS.each { |s| s.close rescue nil }.clear }
604
574
  [:TERM, :INT].each { |sig| trap(sig) { exit!(0) } } # instant shutdown
605
575
  logger.info "worker=#{worker.nr} ready"
606
- m = 0
607
576
 
608
577
  begin
609
578
  nr < 0 and reopen_worker_logs(worker.nr)
610
579
  nr = 0
611
580
 
612
- # we're a goner in timeout seconds anyways if alive.chmod
613
- # breaks, so don't trap the exception. Using fchmod() since
614
- # futimes() is not available in base Ruby and I very strongly
615
- # prefer temporary files to be unlinked for security,
616
- # performance and reliability reasons, so utime is out. No-op
617
- # changes with chmod doesn't update ctime on all filesystems; so
618
- # we change our counter each and every time (after process_client
619
- # and before IO.select).
620
- alive.chmod(m = 0 == m ? 1 : 0)
621
-
622
- ready.each do |sock|
581
+ worker.tick = Time.now.to_i
582
+ while sock = ready.shift
623
583
  if client = sock.kgio_tryaccept
624
584
  process_client(client)
625
585
  nr += 1
626
- alive.chmod(m = 0 == m ? 1 : 0)
586
+ worker.tick = Time.now.to_i
627
587
  end
628
588
  break if nr < 0
629
589
  end
@@ -632,23 +592,21 @@ class Unicorn::HttpServer
632
592
  # we're probably reasonably busy, so avoid calling select()
633
593
  # and do a speculative non-blocking accept() on ready listeners
634
594
  # before we sleep again in select().
635
- redo unless nr == 0 # (nr < 0) => reopen logs
595
+ unless nr == 0 # (nr < 0) => reopen logs (unlikely)
596
+ ready = l.dup
597
+ redo
598
+ end
636
599
 
637
600
  ppid == Process.ppid or return
638
- alive.chmod(m = 0 == m ? 1 : 0)
639
601
 
640
602
  # timeout used so we can detect parent death:
641
- ret = IO.select(LISTENERS, nil, SELF_PIPE, timeout) and ready = ret[0]
642
- rescue Errno::EINTR
643
- ready = LISTENERS
603
+ worker.tick = Time.now.to_i
604
+ ret = IO.select(l, nil, SELF_PIPE, @timeout) and ready = ret[0]
644
605
  rescue Errno::EBADF
645
606
  nr < 0 or return
646
607
  rescue => e
647
- if alive
648
- logger.error "Unhandled listen loop exception #{e.inspect}."
649
- logger.error e.backtrace.join("\n")
650
- end
651
- end while alive
608
+ Unicorn.log_error(@logger, "listen loop error", e) if worker
609
+ end while worker
652
610
  end
653
611
 
654
612
  # delivers a signal to a worker and fails gracefully if the worker
@@ -656,7 +614,7 @@ class Unicorn::HttpServer
656
614
  def kill_worker(signal, wpid)
657
615
  Process.kill(signal, wpid)
658
616
  rescue Errno::ESRCH
659
- worker = WORKERS.delete(wpid) and worker.tmp.close rescue nil
617
+ worker = WORKERS.delete(wpid) and worker.close rescue nil
660
618
  end
661
619
 
662
620
  # delivers a signal to each worker
@@ -686,7 +644,7 @@ class Unicorn::HttpServer
686
644
  def load_config!
687
645
  loaded_app = app
688
646
  logger.info "reloading config_file=#{config.config_file}"
689
- config[:listeners].replace(init_listeners)
647
+ config[:listeners].replace(@init_listeners)
690
648
  config.reload
691
649
  config.commit!(self)
692
650
  kill_each_worker(:QUIT)
@@ -695,8 +653,8 @@ class Unicorn::HttpServer
695
653
  build_app! if preload_app
696
654
  logger.info "done reloading config_file=#{config.config_file}"
697
655
  rescue StandardError, LoadError, SyntaxError => e
698
- logger.error "error reloading config_file=#{config.config_file}: " \
699
- "#{e.class} #{e.message} #{e.backtrace}"
656
+ Unicorn.log_error(@logger,
657
+ "error reloading config_file=#{config.config_file}", e)
700
658
  self.app = loaded_app
701
659
  end
702
660
 
@@ -730,5 +688,33 @@ class Unicorn::HttpServer
730
688
  SELF_PIPE.replace(Kgio::Pipe.new)
731
689
  SELF_PIPE.each { |io| io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) }
732
690
  end
733
- end
734
691
 
692
+ def inherit_listeners!
693
+ # inherit sockets from parents, they need to be plain Socket objects
694
+ # before they become Kgio::UNIXServer or Kgio::TCPServer
695
+ inherited = ENV['UNICORN_FD'].to_s.split(/,/).map do |fd|
696
+ io = Socket.for_fd(fd.to_i)
697
+ set_server_sockopt(io, listener_opts[sock_name(io)])
698
+ IO_PURGATORY << io
699
+ logger.info "inherited addr=#{sock_name(io)} fd=#{fd}"
700
+ server_cast(io)
701
+ end
702
+
703
+ config_listeners = config[:listeners].dup
704
+ LISTENERS.replace(inherited)
705
+
706
+ # we start out with generic Socket objects that get cast to either
707
+ # Kgio::TCPServer or Kgio::UNIXServer objects; but since the Socket
708
+ # objects share the same OS-level file descriptor as the higher-level
709
+ # *Server objects; we need to prevent Socket objects from being
710
+ # garbage-collected
711
+ config_listeners -= listener_names
712
+ if config_listeners.empty? && LISTENERS.empty?
713
+ config_listeners << Unicorn::Const::DEFAULT_LISTEN
714
+ @init_listeners << Unicorn::Const::DEFAULT_LISTEN
715
+ START_CTX[:argv] << "-l#{Unicorn::Const::DEFAULT_LISTEN}"
716
+ end
717
+ config_listeners.each { |addr| listen(addr) }
718
+ raise ArgumentError, "no listeners" if LISTENERS.empty?
719
+ end
720
+ end
@@ -27,9 +27,9 @@ module Unicorn
27
27
  # same default value as Mongrel
28
28
  :backlog => 1024,
29
29
 
30
- # since we don't do keepalive, we'll always flush-on-close and
31
- # this saves packets for everyone.
32
- :tcp_nopush => true,
30
+ # favor latency over bandwidth savings
31
+ :tcp_nopush => false,
32
+ :tcp_nodelay => true,
33
33
  }
34
34
  #:startdoc:
35
35
 
@@ -101,8 +101,7 @@ module Unicorn
101
101
  end
102
102
  sock.listen(opt[:backlog])
103
103
  rescue => e
104
- logger.error "error setting socket options: #{e.inspect}"
105
- logger.error e.backtrace.join("\n")
104
+ Unicorn.log_error(logger, message, e)
106
105
  end
107
106
 
108
107
  def log_buffer_sizes(sock, pfx = '')
@@ -1,4 +1,5 @@
1
1
  # -*- encoding: binary -*-
2
+ require "raindrops"
2
3
 
3
4
  # This class and its members can be considered a stable interface
4
5
  # and will not change in a backwards-incompatible fashion between
@@ -7,13 +8,53 @@
7
8
  #
8
9
  # Some users may want to access it in the before_fork/after_fork hooks.
9
10
  # See the Unicorn::Configurator RDoc for examples.
10
- class Unicorn::Worker < Struct.new(:nr, :tmp, :switched)
11
+ class Unicorn::Worker
12
+ # :stopdoc:
13
+ attr_accessor :nr, :switched
14
+ attr_writer :tmp
15
+
16
+ PER_DROP = Raindrops::PAGE_SIZE / Raindrops::SIZE
17
+ DROPS = []
18
+
19
+ def initialize(nr)
20
+ drop_index = nr / PER_DROP
21
+ @raindrop = DROPS[drop_index] ||= Raindrops.new(PER_DROP)
22
+ @offset = nr % PER_DROP
23
+ @raindrop[@offset] = 0
24
+ @nr = nr
25
+ @tmp = @switched = false
26
+ end
11
27
 
12
28
  # worker objects may be compared to just plain Integers
13
29
  def ==(other_nr) # :nodoc:
14
- nr == other_nr
30
+ @nr == other_nr
31
+ end
32
+
33
+ # called in the worker process
34
+ def tick=(value) # :nodoc:
35
+ @raindrop[@offset] = value
36
+ end
37
+
38
+ # called in the master process
39
+ def tick # :nodoc:
40
+ @raindrop[@offset]
15
41
  end
16
42
 
43
+ # only exists for compatibility
44
+ def tmp # :nodoc:
45
+ @tmp ||= begin
46
+ tmp = Unicorn::TmpIO.new
47
+ tmp.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
48
+ tmp
49
+ end
50
+ end
51
+
52
+ def close # :nodoc:
53
+ @tmp.close if @tmp
54
+ end
55
+
56
+ # :startdoc:
57
+
17
58
  # In most cases, you should be using the Unicorn::Configurator#user
18
59
  # directive instead. This method should only be used if you need
19
60
  # fine-grained control of exactly when you want to change permissions
@@ -36,12 +77,12 @@ class Unicorn::Worker < Struct.new(:nr, :tmp, :switched)
36
77
  uid = Etc.getpwnam(user).uid
37
78
  gid = Etc.getgrnam(group).gid if group
38
79
  Unicorn::Util.chown_logs(uid, gid)
39
- tmp.chown(uid, gid)
80
+ @tmp.chown(uid, gid) if @tmp
40
81
  if gid && Process.egid != gid
41
82
  Process.initgroups(user, gid)
42
83
  Process::GID.change_privilege(gid)
43
84
  end
44
85
  Process.euid != uid and Process::UID.change_privilege(uid)
45
- self.switched = true
86
+ @switched = true
46
87
  end
47
88
  end
@@ -17,8 +17,9 @@ opts = {
17
17
  pid = fork do
18
18
  Isolate.now!(opts) do
19
19
  gem 'sqlite3-ruby', '1.2.5'
20
- gem 'kgio', '2.3.3'
21
- gem 'rack', '1.2.2'
20
+ gem 'raindrops', '0.6.1'
21
+ gem 'kgio', '2.5.0'
22
+ gem 'rack', '1.3.0'
22
23
  end
23
24
  end
24
25
  _, status = Process.waitpid2(pid)
data/t/broken-app.ru ADDED
@@ -0,0 +1,12 @@
1
+ # we do not want Rack::Lint or anything to protect us
2
+ use Rack::ContentLength
3
+ use Rack::ContentType, "text/plain"
4
+ map "/" do
5
+ run lambda { |env| [ 200, {}, [ "OK\n" ] ] }
6
+ end
7
+ map "/raise" do
8
+ run lambda { |env| raise "BAD" }
9
+ end
10
+ map "/nil" do
11
+ run lambda { |env| nil }
12
+ end
@@ -0,0 +1,12 @@
1
+ use Rack::ContentLength
2
+ headers = { 'Content-Type' => 'text/plain' }
3
+ run lambda { |env|
4
+ case env['PATH_INFO']
5
+ when "/block-forever"
6
+ Process.kill(:STOP, $$)
7
+ sleep # in case STOP signal is not received in time
8
+ [ 500, headers, [ "Should never get here\n" ] ]
9
+ else
10
+ [ 200, headers, [ "#$$\n" ] ]
11
+ end
12
+ }
@@ -34,6 +34,7 @@ t_begin "reload signal succeeds" && {
34
34
  done
35
35
 
36
36
  grep 'error reloading' $r_err >/dev/null
37
+ > $r_err
37
38
  }
38
39
 
39
40
  t_begin "hit with curl" && {
@@ -0,0 +1,69 @@
1
+ #!/bin/sh
2
+ . ./test-lib.sh
3
+
4
+ t_plan 11 "heartbeat/timeout test"
5
+
6
+ t_begin "setup and startup" && {
7
+ unicorn_setup
8
+ echo timeout 3 >> $unicorn_config
9
+ echo preload_app true >> $unicorn_config
10
+ unicorn -D heartbeat-timeout.ru -c $unicorn_config
11
+ unicorn_wait_start
12
+ }
13
+
14
+ t_begin "read worker PID" && {
15
+ worker_pid=$(curl -sSf http://$listen/)
16
+ t_info "worker_pid=$worker_pid"
17
+ }
18
+
19
+ t_begin "sleep for a bit, ensure worker PID does not change" && {
20
+ sleep 4
21
+ test $(curl -sSf http://$listen/) -eq $worker_pid
22
+ }
23
+
24
+ t_begin "block the worker process to force it to die" && {
25
+ rm $ok
26
+ t0=$(date +%s)
27
+ err="$(curl -sSf http://$listen/block-forever 2>&1 || > $ok)"
28
+ t1=$(date +%s)
29
+ elapsed=$(($t1 - $t0))
30
+ t_info "elapsed=$elapsed err=$err"
31
+ test x"$err" != x"Should never get here"
32
+ test x"$err" != x"$worker_pid"
33
+ }
34
+
35
+ t_begin "ensure worker was killed" && {
36
+ test -e $ok
37
+ test 1 -eq $(grep timeout $r_err | grep killing | wc -l)
38
+ }
39
+
40
+ t_begin "ensure timeout took at least 3 seconds" && {
41
+ test $elapsed -ge 3
42
+ }
43
+
44
+ t_begin "we get a fresh new worker process" && {
45
+ new_worker_pid=$(curl -sSf http://$listen/)
46
+ test $new_worker_pid -ne $worker_pid
47
+ }
48
+
49
+ t_begin "truncate the server error log" && {
50
+ > $r_err
51
+ }
52
+
53
+ t_begin "SIGSTOP and SIGCONT on unicorn master does not kill worker" && {
54
+ kill -STOP $unicorn_pid
55
+ sleep 4
56
+ kill -CONT $unicorn_pid
57
+ sleep 2
58
+ test $new_worker_pid -eq $(curl -sSf http://$listen/)
59
+ }
60
+
61
+ t_begin "stop server" && {
62
+ kill -QUIT $unicorn_pid
63
+ }
64
+
65
+ t_begin "check stderr" && check_stderr
66
+
67
+ dbgcat r_err
68
+
69
+ t_done
@@ -0,0 +1,56 @@
1
+ #!/bin/sh
2
+ . ./test-lib.sh
3
+
4
+ t_plan 9 "graceful handling of broken apps"
5
+
6
+ t_begin "setup and start" && {
7
+ unicorn_setup
8
+ unicorn -E none -D broken-app.ru -c $unicorn_config
9
+ unicorn_wait_start
10
+ }
11
+
12
+ t_begin "normal response is alright" && {
13
+ test xOK = x"$(curl -sSf http://$listen/)"
14
+ }
15
+
16
+ t_begin "app raised exception" && {
17
+ curl -sSf http://$listen/raise 2> $tmp || :
18
+ grep -F 500 $tmp
19
+ > $tmp
20
+ }
21
+
22
+ t_begin "app exception logged and backtrace not swallowed" && {
23
+ grep -F 'app error' $r_err
24
+ grep -A1 -F 'app error' $r_err | tail -1 | grep broken-app.ru:
25
+ dbgcat r_err
26
+ > $r_err
27
+ }
28
+
29
+ t_begin "trigger bad response" && {
30
+ curl -sSf http://$listen/nil 2> $tmp || :
31
+ grep -F 500 $tmp
32
+ > $tmp
33
+ }
34
+
35
+ t_begin "app exception logged" && {
36
+ grep -F 'app error' $r_err
37
+ > $r_err
38
+ }
39
+
40
+ t_begin "normal responses alright afterwards" && {
41
+ > $tmp
42
+ curl -sSf http://$listen/ >> $tmp &
43
+ curl -sSf http://$listen/ >> $tmp &
44
+ curl -sSf http://$listen/ >> $tmp &
45
+ curl -sSf http://$listen/ >> $tmp &
46
+ wait
47
+ test xOK = x$(sort < $tmp | uniq)
48
+ }
49
+
50
+ t_begin "teardown" && {
51
+ kill $unicorn_pid
52
+ }
53
+
54
+ t_begin "check stderr" && check_stderr
55
+
56
+ t_done
@@ -0,0 +1,49 @@
1
+ #!/bin/sh
2
+ . ./test-lib.sh
3
+
4
+ t_plan 5 "at_exit/END handlers work as expected"
5
+
6
+ t_begin "setup and startup" && {
7
+ unicorn_setup
8
+ cat >> $unicorn_config <<EOF
9
+ at_exit { \$stdout.syswrite("#{Process.pid} BOTH\\n") }
10
+ END { \$stdout.syswrite("#{Process.pid} END BOTH\\n") }
11
+ after_fork do |_,_|
12
+ at_exit { \$stdout.syswrite("#{Process.pid} WORKER ONLY\\n") }
13
+ END { \$stdout.syswrite("#{Process.pid} END WORKER ONLY\\n") }
14
+ end
15
+ EOF
16
+
17
+ unicorn -D pid.ru -c $unicorn_config
18
+ unicorn_wait_start
19
+ }
20
+
21
+ t_begin "read worker PID" && {
22
+ worker_pid=$(curl -sSf http://$listen/)
23
+ t_info "worker_pid=$worker_pid"
24
+ }
25
+
26
+ t_begin "issue graceful shutdown (SIGQUIT) and wait for termination" && {
27
+ kill -QUIT $unicorn_pid
28
+
29
+ while kill -0 $unicorn_pid >/dev/null 2>&1
30
+ do
31
+ sleep 1
32
+ done
33
+ }
34
+
35
+ t_begin "check stderr" && check_stderr
36
+
37
+ dbgcat r_err
38
+ dbgcat r_out
39
+
40
+ t_begin "all at_exit handlers ran" && {
41
+ grep "$worker_pid BOTH" $r_out
42
+ grep "$unicorn_pid BOTH" $r_out
43
+ grep "$worker_pid END BOTH" $r_out
44
+ grep "$unicorn_pid END BOTH" $r_out
45
+ grep "$worker_pid WORKER ONLY" $r_out
46
+ grep "$worker_pid END WORKER ONLY" $r_out
47
+ }
48
+
49
+ t_done
@@ -0,0 +1,8 @@
1
+ run(lambda { |env|
2
+ body = "#{caller.size}\n"
3
+ h = {
4
+ "Content-Length" => body.size.to_s,
5
+ "Content-Type" => "text/plain",
6
+ }
7
+ [ 200, h, [ body ] ]
8
+ })
@@ -0,0 +1,28 @@
1
+ require 'test/unit'
2
+ require 'unicorn'
3
+
4
+ class TestDroplet < Test::Unit::TestCase
5
+ def test_create_many_droplets
6
+ now = Time.now.to_i
7
+ tmp = (0..1024).map do |i|
8
+ droplet = Unicorn::Worker.new(i)
9
+ assert droplet.respond_to?(:tick)
10
+ assert_equal 0, droplet.tick
11
+ assert_equal(now, droplet.tick = now)
12
+ assert_equal now, droplet.tick
13
+ assert_equal(0, droplet.tick = 0)
14
+ assert_equal 0, droplet.tick
15
+ end
16
+ end
17
+
18
+ def test_shared_process
19
+ droplet = Unicorn::Worker.new(0)
20
+ _, status = Process.waitpid2(fork { droplet.tick += 1; exit!(0) })
21
+ assert status.success?, status.inspect
22
+ assert_equal 1, droplet.tick
23
+
24
+ _, status = Process.waitpid2(fork { droplet.tick += 1; exit!(0) })
25
+ assert status.success?, status.inspect
26
+ assert_equal 2, droplet.tick
27
+ end
28
+ end
@@ -740,7 +740,7 @@ class HttpParserTest < Test::Unit::TestCase
740
740
  # then that large header names are caught
741
741
  10.times do |c|
742
742
  get = "GET /#{rand_data(10,120)} HTTP/1.1\r\nX-#{rand_data(1024, 1024+(c*1024))}: Test\r\n\r\n"
743
- assert_raises Unicorn::HttpParserError do
743
+ assert_raises(Unicorn::HttpParserError,Unicorn::RequestURITooLongError) do
744
744
  parser.buf << get
745
745
  parser.parse
746
746
  parser.clear
@@ -750,7 +750,7 @@ class HttpParserTest < Test::Unit::TestCase
750
750
  # then that large mangled field values are caught
751
751
  10.times do |c|
752
752
  get = "GET /#{rand_data(10,120)} HTTP/1.1\r\nX-Test: #{rand_data(1024, 1024+(c*1024), false)}\r\n\r\n"
753
- assert_raises Unicorn::HttpParserError do
753
+ assert_raises(Unicorn::HttpParserError,Unicorn::RequestURITooLongError) do
754
754
  parser.buf << get
755
755
  parser.parse
756
756
  parser.clear
@@ -761,7 +761,7 @@ class HttpParserTest < Test::Unit::TestCase
761
761
  get = "GET /#{rand_data(10,120)} HTTP/1.1\r\n"
762
762
  get << "X-Test: test\r\n" * (80 * 1024)
763
763
  parser.buf << get
764
- assert_raises Unicorn::HttpParserError do
764
+ assert_raises(Unicorn::HttpParserError,Unicorn::RequestURITooLongError) do
765
765
  parser.parse
766
766
  end
767
767
  parser.clear
@@ -769,7 +769,7 @@ class HttpParserTest < Test::Unit::TestCase
769
769
  # finally just that random garbage gets blocked all the time
770
770
  10.times do |c|
771
771
  get = "GET #{rand_data(1024, 1024+(c*1024), false)} #{rand_data(1024, 1024+(c*1024), false)}\r\n\r\n"
772
- assert_raises Unicorn::HttpParserError do
772
+ assert_raises(Unicorn::HttpParserError,Unicorn::RequestURITooLongError) do
773
773
  parser.buf << get
774
774
  parser.parse
775
775
  parser.clear
@@ -237,6 +237,17 @@ class HttpParserNgTest < Test::Unit::TestCase
237
237
  assert @parser.keepalive?
238
238
  end
239
239
 
240
+ def test_chunked_empty
241
+ str = @parser.buf
242
+ req = @parser.env
243
+ str << "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n"
244
+ assert_equal req, @parser.parse, "msg=#{str}"
245
+ assert_equal 0, str.size
246
+ tmp = ""
247
+ assert_equal str, @parser.filter_body(tmp, str << "0\r\n\r\n")
248
+ assert_equal "", tmp
249
+ end
250
+
240
251
  def test_two_chunks
241
252
  str = @parser.buf
242
253
  str << "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n"
@@ -651,4 +662,47 @@ class HttpParserNgTest < Test::Unit::TestCase
651
662
  assert_equal expect, @parser.parse
652
663
  assert ! @parser.next?
653
664
  end
665
+
666
+ def test_chunk_only
667
+ tmp = ""
668
+ assert_equal @parser, @parser.dechunk!
669
+ assert_nil @parser.filter_body(tmp, "6\r\n")
670
+ assert_equal "", tmp
671
+ assert_nil @parser.filter_body(tmp, "abcdef")
672
+ assert_equal "abcdef", tmp
673
+ assert_nil @parser.filter_body(tmp, "\r\n")
674
+ assert_equal "", tmp
675
+ src = "0\r\n\r\n"
676
+ assert_equal src.object_id, @parser.filter_body(tmp, src).object_id
677
+ assert_equal "", tmp
678
+ end
679
+
680
+ def test_chunk_only_bad_align
681
+ tmp = ""
682
+ assert_equal @parser, @parser.dechunk!
683
+ assert_nil @parser.filter_body(tmp, "6\r\na")
684
+ assert_equal "a", tmp
685
+ assert_nil @parser.filter_body(tmp, "bcde")
686
+ assert_equal "bcde", tmp
687
+ assert_nil @parser.filter_body(tmp, "f\r")
688
+ assert_equal "f", tmp
689
+ src = "\n0\r\n\r\n"
690
+ assert_equal src.object_id, @parser.filter_body(tmp, src).object_id
691
+ assert_equal "", tmp
692
+ end
693
+
694
+ def test_chunk_only_reset_ok
695
+ tmp = ""
696
+ assert_equal @parser, @parser.dechunk!
697
+ src = "1\r\na\r\n0\r\n\r\n"
698
+ assert_nil @parser.filter_body(tmp, src)
699
+ assert_equal "a", tmp
700
+ assert_equal src.object_id, @parser.filter_body(tmp, src).object_id
701
+
702
+ assert_equal @parser, @parser.dechunk!
703
+ src = "0\r\n\r\n"
704
+ assert_equal src.object_id, @parser.filter_body(tmp, src).object_id
705
+ assert_equal "", tmp
706
+ assert_equal src, @parser.filter_body(tmp, src)
707
+ end
654
708
  end
@@ -15,7 +15,7 @@ class ResponseTest < Test::Unit::TestCase
15
15
  include Unicorn::HttpResponse
16
16
 
17
17
  def test_httpdate
18
- before = Time.now.to_i
18
+ before = Time.now.to_i - 1
19
19
  str = httpdate
20
20
  assert_kind_of(String, str)
21
21
  middle = Time.parse(str).to_i
@@ -39,7 +39,7 @@ class WebServerTest < Test::Unit::TestCase
39
39
  redirect_test_io do
40
40
  wait_workers_ready("test_stderr.#$$.log", 1)
41
41
  File.truncate("test_stderr.#$$.log", 0)
42
- @server.stop(true)
42
+ @server.stop(false)
43
43
  end
44
44
  reset_sig_handlers
45
45
  end
@@ -124,7 +124,7 @@ class SignalsTest < Test::Unit::TestCase
124
124
  assert diff > 1.0, "diff was #{diff.inspect}"
125
125
  assert diff < 60.0
126
126
  ensure
127
- Process.kill(:QUIT, pid) rescue nil
127
+ Process.kill(:TERM, pid) rescue nil
128
128
  end
129
129
 
130
130
  def test_response_write
@@ -53,7 +53,7 @@ class UploadTest < Test::Unit::TestCase
53
53
  end
54
54
 
55
55
  def teardown
56
- redirect_test_io { @server.stop(true) } if @server
56
+ redirect_test_io { @server.stop(false) } if @server
57
57
  @random.close
58
58
  reset_sig_handlers
59
59
  end
data/unicorn.gemspec CHANGED
@@ -34,7 +34,8 @@ Gem::Specification.new do |s|
34
34
  # commented out. Nevertheless, upgrading to Rails 2.3.4 or later is
35
35
  # *strongly* recommended for security reasons.
36
36
  s.add_dependency(%q<rack>)
37
- s.add_dependency(%q<kgio>, '~> 2.3')
37
+ s.add_dependency(%q<kgio>, '~> 2.4')
38
+ s.add_dependency(%q<raindrops>, '~> 0.6')
38
39
 
39
40
  s.add_development_dependency('isolate', '~> 3.1')
40
41
  s.add_development_dependency('wrongdoc', '~> 1.5')
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: unicorn
3
3
  version: !ruby/object:Gem::Version
4
- hash: 27
4
+ hash: 63
5
5
  prerelease:
6
6
  segments:
7
- - 3
8
- - 7
7
+ - 4
9
8
  - 0
10
- version: 3.7.0
9
+ - 0
10
+ version: 4.0.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Unicorn hackers
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-06-09 00:00:00 Z
18
+ date: 2011-06-27 00:00:00 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: rack
@@ -39,17 +39,32 @@ dependencies:
39
39
  requirements:
40
40
  - - ~>
41
41
  - !ruby/object:Gem::Version
42
- hash: 5
42
+ hash: 11
43
43
  segments:
44
44
  - 2
45
- - 3
46
- version: "2.3"
45
+ - 4
46
+ version: "2.4"
47
47
  type: :runtime
48
48
  version_requirements: *id002
49
49
  - !ruby/object:Gem::Dependency
50
- name: isolate
50
+ name: raindrops
51
51
  prerelease: false
52
52
  requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ~>
56
+ - !ruby/object:Gem::Version
57
+ hash: 7
58
+ segments:
59
+ - 0
60
+ - 6
61
+ version: "0.6"
62
+ type: :runtime
63
+ version_requirements: *id003
64
+ - !ruby/object:Gem::Dependency
65
+ name: isolate
66
+ prerelease: false
67
+ requirement: &id004 !ruby/object:Gem::Requirement
53
68
  none: false
54
69
  requirements:
55
70
  - - ~>
@@ -60,11 +75,11 @@ dependencies:
60
75
  - 1
61
76
  version: "3.1"
62
77
  type: :development
63
- version_requirements: *id003
78
+ version_requirements: *id004
64
79
  - !ruby/object:Gem::Dependency
65
80
  name: wrongdoc
66
81
  prerelease: false
67
- requirement: &id004 !ruby/object:Gem::Requirement
82
+ requirement: &id005 !ruby/object:Gem::Requirement
68
83
  none: false
69
84
  requirements:
70
85
  - - ~>
@@ -75,7 +90,7 @@ dependencies:
75
90
  - 5
76
91
  version: "1.5"
77
92
  type: :development
78
- version_requirements: *id004
93
+ version_requirements: *id005
79
94
  description: |-
80
95
  \Unicorn is an HTTP server for Rack applications designed to only serve
81
96
  fast clients on low-latency, high-bandwidth connections and take
@@ -212,7 +227,9 @@ files:
212
227
  - t/bin/sha1sum.rb
213
228
  - t/bin/unused_listen
214
229
  - t/bin/utee
230
+ - t/broken-app.ru
215
231
  - t/env.ru
232
+ - t/heartbeat-timeout.ru
216
233
  - t/my-tap-lib.sh
217
234
  - t/oob_gc.ru
218
235
  - t/oob_gc_path.ru
@@ -255,12 +272,14 @@ files:
255
272
  - t/t0002-config-conflict.sh
256
273
  - t/t0002-parser-error.sh
257
274
  - t/t0003-working_directory.sh
275
+ - t/t0004-heartbeat-timeout.sh
258
276
  - t/t0004-working_directory_broken.sh
259
277
  - t/t0005-working_directory_app.rb.sh
260
278
  - t/t0006-reopen-logs.sh
261
279
  - t/t0006.ru
262
280
  - t/t0007-working_directory_no_embed_cli.sh
263
281
  - t/t0008-back_out_of_upgrade.sh
282
+ - t/t0009-broken-app.sh
264
283
  - t/t0009-winch_ttin.sh
265
284
  - t/t0010-reap-logging.sh
266
285
  - t/t0011-active-unix-socket.sh
@@ -274,6 +293,7 @@ files:
274
293
  - t/t0017-trust-x-forwarded-true.sh
275
294
  - t/t0018-write-on-close.sh
276
295
  - t/t0019-max_header_len.sh
296
+ - t/t0020-at_exit-handler.sh
277
297
  - t/t0100-rack-input-tests.sh
278
298
  - t/t0116-client_body_buffer_size.sh
279
299
  - t/t0116.ru
@@ -291,6 +311,7 @@ files:
291
311
  - test/aggregate.rb
292
312
  - test/benchmark/README
293
313
  - test/benchmark/dd.ru
314
+ - test/benchmark/stack.ru
294
315
  - test/exec/README
295
316
  - test/exec/test_exec.rb
296
317
  - test/rails/app-1.2.3/.gitignore
@@ -356,6 +377,7 @@ files:
356
377
  - test/rails/test_rails.rb
357
378
  - test/test_helper.rb
358
379
  - test/unit/test_configurator.rb
380
+ - test/unit/test_droplet.rb
359
381
  - test/unit/test_http_parser.rb
360
382
  - test/unit/test_http_parser_ng.rb
361
383
  - test/unit/test_http_parser_xftrust.rb