unicorn 0.7.1 → 0.8.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/CHANGELOG CHANGED
@@ -1,3 +1,4 @@
1
+ v0.8.0 - enforce Rack dependency, minor performance improvements and fixes
1
2
  v0.7.1 - minor fixes, cleanups and documentation improvements
2
3
  v0.7.0 - rack.version is 1.0
3
4
  v0.6.0 - cleanups + optimizations, signals to {in,de}crement processes
data/Manifest CHANGED
@@ -120,7 +120,6 @@ test/rails/app-2.3.2.1/public/404.html
120
120
  test/rails/app-2.3.2.1/public/500.html
121
121
  test/rails/test_rails.rb
122
122
  test/test_helper.rb
123
- test/tools/trickletest.rb
124
123
  test/unit/test_configurator.rb
125
124
  test/unit/test_http_parser.rb
126
125
  test/unit/test_request.rb
data/Rakefile CHANGED
@@ -16,6 +16,7 @@ Echoe.new("unicorn") do |p|
16
16
  p.ignore_pattern = /^(pkg|site|projects|doc|log)|CVS|\.log/
17
17
  p.need_tar_gz = false
18
18
  p.need_tgz = true
19
+ p.dependencies = [ 'rack' ]
19
20
 
20
21
  p.extension_pattern = ["ext/**/extconf.rb"]
21
22
 
data/lib/unicorn.rb CHANGED
@@ -1,16 +1,17 @@
1
1
  require 'fcntl'
2
-
3
2
  require 'unicorn/socket_helper'
4
- require 'unicorn/const'
5
- require 'unicorn/http_request'
6
- require 'unicorn/http_response'
7
- require 'unicorn/configurator'
8
- require 'unicorn/util'
3
+ autoload :Rack, 'rack'
9
4
 
10
5
  # Unicorn module containing all of the classes (include C extensions) for running
11
6
  # a Unicorn web server. It contains a minimalist HTTP server with just enough
12
7
  # functionality to service web application requests fast as possible.
13
8
  module Unicorn
9
+ autoload :Const, 'unicorn/const'
10
+ autoload :HttpRequest, 'unicorn/http_request'
11
+ autoload :HttpResponse, 'unicorn/http_response'
12
+ autoload :Configurator, 'unicorn/configurator'
13
+ autoload :Util, 'unicorn/util'
14
+
14
15
  class << self
15
16
  def run(app, options = {})
16
17
  HttpServer.new(app, options).start.join
@@ -377,7 +378,9 @@ module Unicorn
377
378
  # worker.
378
379
  def murder_lazy_workers
379
380
  WORKERS.each_pair do |pid, worker|
380
- Time.now - worker.tempfile.ctime <= @timeout and next
381
+ stat = worker.tempfile.stat
382
+ stat.mode == 0100000 and next
383
+ Time.now - stat.ctime <= @timeout and next
381
384
  logger.error "worker=#{worker.nr} PID:#{pid} is too old, killing"
382
385
  kill_worker(:KILL, pid) # take no prisoners for @timeout violations
383
386
  worker.tempfile.close rescue nil
@@ -414,8 +417,6 @@ module Unicorn
414
417
  # once a client is accepted, it is processed in its entirety here
415
418
  # in 3 easy steps: read request, call app, write app response
416
419
  def process_client(client)
417
- # one syscall less than "client.nonblock = false":
418
- client.fcntl(Fcntl::F_SETFL, File::RDWR)
419
420
  HttpResponse.write(client, @app.call(@request.read(client)))
420
421
  # if we get any error, try to write something back to the client
421
422
  # assuming we haven't closed the socket, but don't get hung up
@@ -423,20 +424,15 @@ module Unicorn
423
424
  # the socket is closed at the end of this function
424
425
  rescue EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,Errno::EBADF
425
426
  client.write_nonblock(Const::ERROR_500_RESPONSE) rescue nil
427
+ client.close rescue nil
426
428
  rescue HttpParserError # try to tell the client they're bad
427
429
  client.write_nonblock(Const::ERROR_400_RESPONSE) rescue nil
430
+ client.close rescue nil
428
431
  rescue Object => e
429
432
  client.write_nonblock(Const::ERROR_500_RESPONSE) rescue nil
433
+ client.close rescue nil
430
434
  logger.error "Read error: #{e.inspect}"
431
435
  logger.error e.backtrace.join("\n")
432
- ensure
433
- begin
434
- client.closed? or client.close
435
- rescue Object => e
436
- logger.error "Client error: #{e.inspect}"
437
- logger.error e.backtrace.join("\n")
438
- end
439
- @request.reset
440
436
  end
441
437
 
442
438
  # gets rid of stuff the worker has no business keeping track of
@@ -475,16 +471,18 @@ module Unicorn
475
471
  nr = 0 # this becomes negative if we need to reopen logs
476
472
  alive = worker.tempfile # tempfile is our lifeline to the master process
477
473
  ready = LISTENERS
478
- client = nil
474
+ t = ti = 0
479
475
 
480
476
  # closing anything we IO.select on will raise EBADF
481
477
  trap(:USR1) { nr = -65536; SELF_PIPE.first.close rescue nil }
482
478
  trap(:QUIT) { alive = nil; LISTENERS.each { |s| s.close rescue nil } }
483
- [:TERM, :INT].each { |sig| trap(sig) { exit(0) } } # instant shutdown
479
+ [:TERM, :INT].each { |sig| trap(sig) { exit!(0) } } # instant shutdown
484
480
  @logger.info "worker=#{worker.nr} ready"
485
481
 
486
- while alive
487
- reopen_worker_logs(worker.nr) if nr < 0
482
+ begin
483
+ nr < 0 and reopen_worker_logs(worker.nr)
484
+ nr = 0
485
+
488
486
  # we're a goner in @timeout seconds anyways if alive.chmod
489
487
  # breaks, so don't trap the exception. Using fchmod() since
490
488
  # futimes() is not available in base Ruby and I very strongly
@@ -493,55 +491,41 @@ module Unicorn
493
491
  # changes with chmod doesn't update ctime on all filesystems; so
494
492
  # we change our counter each and every time (after process_client
495
493
  # and before IO.select).
496
- alive.chmod(nr = 0)
494
+ t == (ti = Time.now.to_i) or alive.chmod(t = ti)
495
+
496
+ ready.each do |sock|
497
+ begin
498
+ process_client(sock.accept_nonblock)
499
+ nr += 1
500
+ t == (ti = Time.now.to_i) or alive.chmod(t = ti)
501
+ rescue Errno::EAGAIN, Errno::ECONNABORTED
502
+ end
503
+ break if nr < 0
504
+ end
505
+
506
+ # make the following bet: if we accepted clients this round,
507
+ # we're probably reasonably busy, so avoid calling select()
508
+ # and do a speculative accept_nonblock on every listener
509
+ # before we sleep again in select().
510
+ redo unless nr == 0 # (nr < 0) => reopen logs
497
511
 
512
+ master_pid == Process.ppid or return
513
+ alive.chmod(t = 0)
498
514
  begin
499
- ready.each do |sock|
500
- begin
501
- client = begin
502
- sock.accept_nonblock
503
- rescue Errno::EAGAIN
504
- next
505
- end
506
- process_client(client)
507
- rescue Errno::ECONNABORTED
508
- # client closed the socket even before accept
509
- client.close rescue nil
510
- ensure
511
- alive.chmod(nr += 1) if client
512
- break if nr < 0
513
- end
514
- end
515
- client = nil
516
-
517
- # make the following bet: if we accepted clients this round,
518
- # we're probably reasonably busy, so avoid calling select()
519
- # and do a speculative accept_nonblock on every listener
520
- # before we sleep again in select().
521
- if nr != 0 # (nr < 0) => reopen logs
522
- ready = LISTENERS
523
- else
524
- master_pid == Process.ppid or exit(0)
525
- alive.chmod(nr += 1)
526
- begin
527
- # timeout used so we can detect parent death:
528
- ret = IO.select(LISTENERS, nil, SELF_PIPE, @timeout) or next
529
- ready = ret.first
530
- rescue Errno::EINTR
531
- ready = LISTENERS
532
- rescue Errno::EBADF => e
533
- nr < 0 or exit(alive ? 1 : 0)
534
- end
535
- end
536
- rescue SignalException, SystemExit => e
537
- raise e
538
- rescue Object => e
539
- if alive
540
- logger.error "Unhandled listen loop exception #{e.inspect}."
541
- logger.error e.backtrace.join("\n")
542
- end
515
+ # timeout used so we can detect parent death:
516
+ ret = IO.select(LISTENERS, nil, SELF_PIPE, @timeout) or redo
517
+ ready = ret.first
518
+ rescue Errno::EINTR
519
+ ready = LISTENERS
520
+ rescue Errno::EBADF
521
+ nr < 0 or return
543
522
  end
544
- end
523
+ rescue Object => e
524
+ if alive
525
+ logger.error "Unhandled listen loop exception #{e.inspect}."
526
+ logger.error e.backtrace.join("\n")
527
+ end
528
+ end while alive
545
529
  end
546
530
 
547
531
  # delivers a signal to a worker and fails gracefully if the worker
@@ -585,6 +569,7 @@ module Unicorn
585
569
  @config.reload
586
570
  @config.commit!(self)
587
571
  kill_each_worker(:QUIT)
572
+ Unicorn::Util.reopen_logs
588
573
  logger.info "done reloading config_file=#{@config.config_file}"
589
574
  rescue Object => e
590
575
  logger.error "error reloading config_file=#{@config.config_file}: " \
@@ -598,7 +583,13 @@ module Unicorn
598
583
  end
599
584
 
600
585
  def build_app!
601
- @app = @app.call if @app.respond_to?(:arity) && @app.arity == 0
586
+ if @app.respond_to?(:arity) && @app.arity == 0
587
+ if defined?(Gem) && Gem.respond_to?(:refresh)
588
+ logger.info "Refreshing Gem list"
589
+ Gem.refresh
590
+ end
591
+ @app = @app.call
592
+ end
602
593
  end
603
594
 
604
595
  def proc_name(tag)
@@ -95,10 +95,15 @@ module Unicorn::App
95
95
  # Allows +out+ to be used as a Rack body.
96
96
  def out.each
97
97
  sysseek(@unicorn_app_exec_cgi_offset)
98
+
99
+ # don't use a preallocated buffer for sysread since we can't
100
+ # guarantee an actual socket is consuming the yielded string
101
+ # (or if somebody is pushing to an array for eventual concatenation
98
102
  begin
99
- loop { yield(sysread(CHUNK_SIZE)) }
103
+ yield(sysread(CHUNK_SIZE))
100
104
  rescue EOFError
101
- end
105
+ return
106
+ end while true
102
107
  end
103
108
 
104
109
  prev = nil
@@ -126,7 +131,8 @@ module Unicorn::App
126
131
  tmp.binmode
127
132
 
128
133
  # Rack::Lint::InputWrapper doesn't allow sysread :(
129
- while buf = inp.read(CHUNK_SIZE)
134
+ buf = ''
135
+ while inp.read(CHUNK_SIZE, buf)
130
136
  tmp.syswrite(buf)
131
137
  end
132
138
  tmp.sysseek(0)
@@ -22,6 +22,8 @@ require 'rack/file'
22
22
  class Unicorn::App::OldRails::Static
23
23
  FILE_METHODS = { 'GET' => true, 'HEAD' => true }.freeze
24
24
  REQUEST_METHOD = 'REQUEST_METHOD'.freeze
25
+ REQUEST_URI = 'REQUEST_URI'.freeze
26
+ PATH_INFO = 'PATH_INFO'.freeze
25
27
 
26
28
  def initialize(app)
27
29
  @app = app
@@ -34,10 +36,10 @@ class Unicorn::App::OldRails::Static
34
36
  FILE_METHODS.include?(env[REQUEST_METHOD]) or return @app.call(env)
35
37
 
36
38
  # first try the path as-is
37
- path_info = env[Unicorn::Const::PATH_INFO].chomp("/")
39
+ path_info = env[PATH_INFO].chomp("/")
38
40
  if File.file?("#@root/#{::Rack::Utils.unescape(path_info)}")
39
41
  # File exists as-is so serve it up
40
- env[Unicorn::Const::PATH_INFO] = path_info
42
+ env[PATH_INFO] = path_info
41
43
  return @file_server.call(env)
42
44
  end
43
45
 
@@ -45,11 +47,11 @@ class Unicorn::App::OldRails::Static
45
47
 
46
48
  # grab the semi-colon REST operator used by old versions of Rails
47
49
  # this is the reason we didn't just copy the new Rails::Rack::Static
48
- env[Unicorn::Const::REQUEST_URI] =~ /^#{Regexp.escape(path_info)}(;[^\?]+)/
50
+ env[REQUEST_URI] =~ /^#{Regexp.escape(path_info)}(;[^\?]+)/
49
51
  path_info << "#$1#{ActionController::Base.page_cache_extension}"
50
52
 
51
53
  if File.file?("#@root/#{::Rack::Utils.unescape(path_info)}")
52
- env[Unicorn::Const::PATH_INFO] = path_info
54
+ env[PATH_INFO] = path_info
53
55
  return @file_server.call(env)
54
56
  end
55
57
 
data/lib/unicorn/const.rb CHANGED
@@ -1,65 +1,11 @@
1
-
2
1
  module Unicorn
3
2
 
4
- # Every standard HTTP code mapped to the appropriate message. These are
5
- # used so frequently that they are placed directly in Unicorn for easy
6
- # access rather than Unicorn::Const itself.
7
- HTTP_STATUS_CODES = {
8
- 100 => 'Continue',
9
- 101 => 'Switching Protocols',
10
- 200 => 'OK',
11
- 201 => 'Created',
12
- 202 => 'Accepted',
13
- 203 => 'Non-Authoritative Information',
14
- 204 => 'No Content',
15
- 205 => 'Reset Content',
16
- 206 => 'Partial Content',
17
- 300 => 'Multiple Choices',
18
- 301 => 'Moved Permanently',
19
- 302 => 'Moved Temporarily',
20
- 303 => 'See Other',
21
- 304 => 'Not Modified',
22
- 305 => 'Use Proxy',
23
- 400 => 'Bad Request',
24
- 401 => 'Unauthorized',
25
- 402 => 'Payment Required',
26
- 403 => 'Forbidden',
27
- 404 => 'Not Found',
28
- 405 => 'Method Not Allowed',
29
- 406 => 'Not Acceptable',
30
- 407 => 'Proxy Authentication Required',
31
- 408 => 'Request Time-out',
32
- 409 => 'Conflict',
33
- 410 => 'Gone',
34
- 411 => 'Length Required',
35
- 412 => 'Precondition Failed',
36
- 413 => 'Request Entity Too Large',
37
- 414 => 'Request-URI Too Large',
38
- 415 => 'Unsupported Media Type',
39
- 500 => 'Internal Server Error',
40
- 501 => 'Not Implemented',
41
- 502 => 'Bad Gateway',
42
- 503 => 'Service Unavailable',
43
- 504 => 'Gateway Time-out',
44
- 505 => 'HTTP Version not supported'
45
- }.inject({}) { |hash,(code,msg)|
46
- hash[code] = "#{code} #{msg}"
47
- hash
48
- }
49
-
50
3
  # Frequently used constants when constructing requests or responses. Many times
51
4
  # the constant just refers to a string with the same contents. Using these constants
52
5
  # gave about a 3% to 10% performance improvement over using the strings directly.
53
6
  # Symbols did not really improve things much compared to constants.
54
7
  module Const
55
- # This is the part of the path after the SCRIPT_NAME.
56
- PATH_INFO="PATH_INFO".freeze
57
-
58
- # The original URI requested by the client.
59
- REQUEST_URI='REQUEST_URI'.freeze
60
- REQUEST_PATH='REQUEST_PATH'.freeze
61
-
62
- UNICORN_VERSION="0.7.1".freeze
8
+ UNICORN_VERSION="0.8.0".freeze
63
9
 
64
10
  DEFAULT_HOST = "0.0.0.0".freeze # default TCP listen host address
65
11
  DEFAULT_PORT = "8080".freeze # default TCP listen port
@@ -12,19 +12,22 @@ module Unicorn
12
12
  #
13
13
  class HttpRequest
14
14
 
15
- # default parameters we merge into the request env for Rack handlers
16
- DEF_PARAMS = {
17
- "rack.errors" => $stderr,
18
- "rack.multiprocess" => true,
19
- "rack.multithread" => false,
20
- "rack.run_once" => false,
21
- "rack.version" => [1, 0].freeze,
22
- "SCRIPT_NAME" => "".freeze,
23
-
24
- # this is not in the Rack spec, but some apps may rely on it
25
- "SERVER_SOFTWARE" => "Unicorn #{Const::UNICORN_VERSION}".freeze
26
- }.freeze
27
-
15
+ # default parameters we merge into the request env for Rack handlers
16
+ DEFAULTS = {
17
+ "rack.errors" => $stderr,
18
+ "rack.multiprocess" => true,
19
+ "rack.multithread" => false,
20
+ "rack.run_once" => false,
21
+ "rack.version" => [1, 0].freeze,
22
+ "SCRIPT_NAME" => "".freeze,
23
+
24
+ # this is not in the Rack spec, but some apps may rely on it
25
+ "SERVER_SOFTWARE" => "Unicorn #{Const::UNICORN_VERSION}".freeze
26
+ }
27
+
28
+ # Optimize for the common case where there's no request body
29
+ # (GET/HEAD) requests.
30
+ NULL_IO = StringIO.new
28
31
  LOCALHOST = '127.0.0.1'.freeze
29
32
 
30
33
  # Being explicitly single-threaded, we have certain advantages in
@@ -35,14 +38,6 @@ module Unicorn
35
38
 
36
39
  def initialize(logger)
37
40
  @logger = logger
38
- reset
39
- end
40
-
41
- def reset
42
- PARAMS[Const::RACK_INPUT].close rescue nil
43
- PARAMS[Const::RACK_INPUT].close! rescue nil
44
- PARSER.reset
45
- PARAMS.clear
46
41
  end
47
42
 
48
43
  # Does the majority of the IO processing. It has been written in
@@ -59,6 +54,14 @@ module Unicorn
59
54
  # This does minimal exception trapping and it is up to the caller
60
55
  # to handle any socket errors (e.g. user aborted upload).
61
56
  def read(socket)
57
+ # reset the parser
58
+ unless NULL_IO == (input = PARAMS[Const::RACK_INPUT]) # unlikely
59
+ input.close rescue nil
60
+ input.close! rescue nil
61
+ end
62
+ PARAMS.clear
63
+ PARSER.reset
64
+
62
65
  # From http://www.ietf.org/rfc/rfc3875:
63
66
  # "Script authors should be aware that the REMOTE_ADDR and
64
67
  # REMOTE_HOST meta-variables (see sections 4.1.8 and 4.1.9)
@@ -70,15 +73,15 @@ module Unicorn
70
73
  TCPSocket === socket ? socket.peeraddr.last : LOCALHOST
71
74
 
72
75
  # short circuit the common case with small GET requests first
73
- PARSER.execute(PARAMS, read_socket(socket)) and
76
+ PARSER.execute(PARAMS, socket.readpartial(Const::CHUNK_SIZE, BUFFER)) and
74
77
  return handle_body(socket)
75
78
 
76
- data = BUFFER.dup # read_socket will clobber BUFFER
79
+ data = BUFFER.dup # socket.readpartial will clobber BUFFER
77
80
 
78
81
  # Parser is not done, queue up more data to read and continue parsing
79
82
  # an Exception thrown from the PARSER will throw us out of the loop
80
83
  begin
81
- data << read_socket(socket)
84
+ data << socket.readpartial(Const::CHUNK_SIZE, BUFFER)
82
85
  PARSER.execute(PARAMS, data) and return handle_body(socket)
83
86
  end while true
84
87
  rescue HttpParserError => e
@@ -99,8 +102,8 @@ module Unicorn
99
102
  content_length = PARAMS[Const::CONTENT_LENGTH].to_i
100
103
 
101
104
  if content_length == 0 # short circuit the common case
102
- PARAMS[Const::RACK_INPUT] = StringIO.new
103
- return PARAMS.update(DEF_PARAMS)
105
+ PARAMS[Const::RACK_INPUT] = NULL_IO.closed? ? NULL_IO.reopen : NULL_IO
106
+ return PARAMS.update(DEFAULTS)
104
107
  end
105
108
 
106
109
  # must read more data to complete body
@@ -110,21 +113,19 @@ module Unicorn
110
113
  StringIO.new : Tempfile.new('unicorn')
111
114
 
112
115
  body.binmode
113
- body.sync = true
114
- body.syswrite(http_body)
116
+ body.write(http_body)
115
117
 
116
118
  # Some clients (like FF1.0) report 0 for body and then send a body.
117
119
  # This will probably truncate them but at least the request goes through
118
120
  # usually.
119
121
  read_body(socket, remain, body) if remain > 0
120
122
  body.rewind
121
- body.sysseek(0) if body.respond_to?(:sysseek)
122
123
 
123
124
  # in case read_body overread because the client tried to pipeline
124
125
  # another request, we'll truncate it. Again, we don't do pipelining
125
126
  # or keepalive
126
127
  body.truncate(content_length)
127
- PARAMS.update(DEF_PARAMS)
128
+ PARAMS.update(DEFAULTS)
128
129
  end
129
130
 
130
131
  # Does the heavy lifting of properly reading the larger body
@@ -133,10 +134,10 @@ module Unicorn
133
134
  # of the body that has been read to be in the PARAMS['rack.input']
134
135
  # already. It will return true if successful and false if not.
135
136
  def read_body(socket, remain, body)
136
- while remain > 0
137
- # writes always write the requested amount on a POSIX filesystem
138
- remain -= body.syswrite(read_socket(socket))
139
- end
137
+ begin
138
+ # write always writes the requested amount on a POSIX filesystem
139
+ remain -= body.write(socket.readpartial(Const::CHUNK_SIZE, BUFFER))
140
+ end while remain > 0
140
141
  rescue Object => e
141
142
  @logger.error "Error reading HTTP body: #{e.inspect}"
142
143
 
@@ -147,14 +148,5 @@ module Unicorn
147
148
  raise e
148
149
  end
149
150
 
150
- # read(2) on "slow" devices like sockets can be interrupted by signals
151
- def read_socket(socket)
152
- begin
153
- socket.sysread(Const::CHUNK_SIZE, BUFFER)
154
- rescue Errno::EINTR
155
- retry
156
- end
157
- end
158
-
159
151
  end
160
152
  end
@@ -21,6 +21,12 @@ module Unicorn
21
21
 
22
22
  class HttpResponse
23
23
 
24
+ # Every standard HTTP code mapped to the appropriate message.
25
+ CODES = Rack::Utils::HTTP_STATUS_CODES.inject({}) { |hash,(code,msg)|
26
+ hash[code] = "#{code} #{msg}"
27
+ hash
28
+ }
29
+
24
30
  # Rack does not set/require a Date: header. We always override the
25
31
  # Connection: and Date: headers no matter what (if anything) our
26
32
  # Rack application sent us.
@@ -31,7 +37,7 @@ module Unicorn
31
37
  # writes the rack_response to socket as an HTTP response
32
38
  def self.write(socket, rack_response)
33
39
  status, headers, body = rack_response
34
- status = HTTP_STATUS_CODES[status.to_i]
40
+ status = CODES[status.to_i]
35
41
  OUT.clear
36
42
 
37
43
  # Don't bother enforcing duplicate supression, it's a Hash most of
@@ -49,30 +55,16 @@ module Unicorn
49
55
  # so don't worry or care about them.
50
56
  # Date is required by HTTP/1.1 as long as our clock can be trusted.
51
57
  # Some broken clients require a "Status" header so we accomodate them
52
- socket_write(socket,
53
- "HTTP/1.1 #{status}\r\n" \
58
+ socket.write("HTTP/1.1 #{status}\r\n" \
54
59
  "Date: #{Time.now.httpdate}\r\n" \
55
60
  "Status: #{status}\r\n" \
56
61
  "Connection: close\r\n" \
57
62
  "#{OUT.join(EMPTY)}\r\n")
58
- body.each { |chunk| socket_write(socket, chunk) }
59
- socket.close # uncorks the socket immediately
63
+ body.each { |chunk| socket.write(chunk) }
64
+ socket.close # flushes and uncorks the socket immediately
60
65
  ensure
61
66
  body.respond_to?(:close) and body.close rescue nil
62
67
  end
63
68
 
64
- private
65
-
66
- # write(2) can return short on slow devices like sockets as well
67
- # as fail with EINTR if a signal was caught.
68
- def self.socket_write(socket, buffer)
69
- begin
70
- written = socket.syswrite(buffer)
71
- return written if written == buffer.length
72
- buffer = buffer[written..-1]
73
- rescue Errno::EINTR
74
- end while true
75
- end
76
-
77
69
  end
78
70
  end
@@ -8,7 +8,11 @@ length = bs * count
8
8
  slice = (' ' * bs).freeze
9
9
 
10
10
  big = Tempfile.new('')
11
- def big.unicorn_peeraddr; '127.0.0.1'; end
11
+
12
+ def big.unicorn_peeraddr # old versions of Unicorn used this
13
+ '127.0.0.1'
14
+ end
15
+
12
16
  big.syswrite(
13
17
  "PUT /hello/world/puturl?abcd=efg&hi#anchor HTTP/1.0\r\n" \
14
18
  "Host: localhost\r\n" \
@@ -22,6 +26,11 @@ big.fsync
22
26
 
23
27
  include Unicorn
24
28
  request = HttpRequest.new(Logger.new($stderr))
29
+ unless request.respond_to?(:reset)
30
+ def request.reset
31
+ # no-op
32
+ end
33
+ end
25
34
 
26
35
  Benchmark.bmbm do |x|
27
36
  x.report("big") do
@@ -10,6 +10,9 @@ class TestClient
10
10
  buf.replace(@response)
11
11
  end
12
12
 
13
+ alias readpartial sysread
14
+
15
+ # old versions of Unicorn used this
13
16
  def unicorn_peeraddr
14
17
  '127.0.0.1'
15
18
  end
@@ -31,6 +34,12 @@ medium = TestClient.new([
31
34
 
32
35
  include Unicorn
33
36
  request = HttpRequest.new(Logger.new($stderr))
37
+ unless request.respond_to?(:reset)
38
+ def request.reset
39
+ # no-op
40
+ end
41
+ end
42
+
34
43
  Benchmark.bmbm do |x|
35
44
  x.report("small") do
36
45
  for i in 1..nr
@@ -3,6 +3,7 @@ require 'unicorn'
3
3
 
4
4
  class NullWriter
5
5
  def syswrite(buf); buf.size; end
6
+ alias write syswrite
6
7
  def close; end
7
8
  end
8
9
 
data/test/test_helper.rb CHANGED
@@ -25,7 +25,9 @@ require 'stringio'
25
25
  require 'pathname'
26
26
  require 'tempfile'
27
27
  require 'fileutils'
28
+ require 'logger'
28
29
  require 'unicorn'
30
+ require 'unicorn/http11'
29
31
 
30
32
  if ENV['DEBUG']
31
33
  require 'ruby-debug'
@@ -14,7 +14,9 @@ include Unicorn
14
14
 
15
15
  class RequestTest < Test::Unit::TestCase
16
16
 
17
- class MockRequest < StringIO; end
17
+ class MockRequest < StringIO
18
+ alias_method :readpartial, :sysread
19
+ end
18
20
 
19
21
  def setup
20
22
  @request = HttpRequest.new(Logger.new($stderr))
@@ -75,7 +77,6 @@ class RequestTest < Test::Unit::TestCase
75
77
  client = MockRequest.new("GET #{abs_uri} HTTP/1.1\r\n" \
76
78
  "Host: foo\r\n\r\n")
77
79
  assert_raises(HttpParserError) { @request.read(client) }
78
- @request.reset
79
80
  end
80
81
  end
81
82
 
@@ -37,6 +37,87 @@ class SignalsTest < Test::Unit::TestCase
37
37
  @server = nil
38
38
  end
39
39
 
40
+ def test_worker_dies_on_dead_master
41
+ pid = fork {
42
+ app = lambda { |env| [ 200, {'X-Pid' => "#$$" }, [] ] }
43
+ opts = @server_opts.merge(:timeout => 3)
44
+ redirect_test_io { HttpServer.new(app, opts).start.join }
45
+ }
46
+ child = sock = buf = t0 = nil
47
+ assert_nothing_raised do
48
+ wait_workers_ready("test_stderr.#{pid}.log", 1)
49
+ sock = TCPSocket.new('127.0.0.1', @port)
50
+ sock.syswrite("GET / HTTP/1.0\r\n\r\n")
51
+ buf = sock.readpartial(4096)
52
+ sock.close
53
+ buf =~ /\bX-Pid: (\d+)\b/ or raise Exception
54
+ child = $1.to_i
55
+ wait_master_ready("test_stderr.#{pid}.log")
56
+ Process.kill(:KILL, pid)
57
+ Process.waitpid(pid)
58
+ t0 = Time.now
59
+ end
60
+ assert child
61
+ assert t0
62
+ assert_raises(Errno::ESRCH) { loop { Process.kill(0, child); sleep 0.2 } }
63
+ assert((Time.now - t0) < 60)
64
+ end
65
+
66
+ def test_sleepy_kill
67
+ rd, wr = IO.pipe
68
+ pid = fork {
69
+ rd.close
70
+ app = lambda { |env| wr.syswrite('.'); sleep; [ 200, {}, [] ] }
71
+ redirect_test_io { HttpServer.new(app, @server_opts).start.join }
72
+ }
73
+ sock = buf = nil
74
+ wr.close
75
+ assert_nothing_raised do
76
+ wait_workers_ready("test_stderr.#{pid}.log", 1)
77
+ sock = TCPSocket.new('127.0.0.1', @port)
78
+ sock.syswrite("GET / HTTP/1.0\r\n\r\n")
79
+ buf = rd.readpartial(1)
80
+ wait_master_ready("test_stderr.#{pid}.log")
81
+ Process.kill(:INT, pid)
82
+ Process.waitpid(pid)
83
+ end
84
+ assert_equal '.', buf
85
+ buf = nil
86
+ assert_raises(EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,
87
+ Errno::EBADF) do
88
+ buf = sock.sysread(4096)
89
+ end
90
+ assert_nil buf
91
+ ensure
92
+ end
93
+
94
+ def test_timeout_slow_response
95
+ pid = fork {
96
+ app = lambda { |env| sleep }
97
+ opts = @server_opts.merge(:timeout => 3)
98
+ redirect_test_io { HttpServer.new(app, opts).start.join }
99
+ }
100
+ t0 = Time.now
101
+ sock = nil
102
+ assert_nothing_raised do
103
+ wait_workers_ready("test_stderr.#{pid}.log", 1)
104
+ sock = TCPSocket.new('127.0.0.1', @port)
105
+ sock.syswrite("GET / HTTP/1.0\r\n\r\n")
106
+ end
107
+
108
+ buf = nil
109
+ assert_raises(EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,
110
+ Errno::EBADF) do
111
+ buf = sock.sysread(4096)
112
+ end
113
+ diff = Time.now - t0
114
+ assert_nil buf
115
+ assert diff > 1.0, "diff was #{diff.inspect}"
116
+ assert diff < 60.0
117
+ ensure
118
+ Process.kill(:QUIT, pid) rescue nil
119
+ end
120
+
40
121
  def test_response_write
41
122
  app = lambda { |env|
42
123
  [ 200, { 'Content-Type' => 'text/plain', 'X-Pid' => Process.pid.to_s },
@@ -45,6 +126,7 @@ class SignalsTest < Test::Unit::TestCase
45
126
  redirect_test_io { @server = HttpServer.new(app, @server_opts).start }
46
127
  sock = nil
47
128
  assert_nothing_raised do
129
+ wait_workers_ready("test_stderr.#{$$}.log", 1)
48
130
  sock = TCPSocket.new('127.0.0.1', @port)
49
131
  sock.syswrite("GET / HTTP/1.0\r\n\r\n")
50
132
  end
@@ -82,6 +164,7 @@ class SignalsTest < Test::Unit::TestCase
82
164
  pid = nil
83
165
 
84
166
  assert_nothing_raised do
167
+ wait_workers_ready("test_stderr.#{$$}.log", 1)
85
168
  sock = TCPSocket.new('127.0.0.1', @port)
86
169
  sock.syswrite("GET / HTTP/1.0\r\n\r\n")
87
170
  pid = sock.sysread(4096)[/\r\nX-Pid: (\d+)\r\n/, 1].to_i
data/unicorn.gemspec CHANGED
@@ -2,17 +2,17 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{unicorn}
5
- s.version = "0.7.1"
5
+ s.version = "0.8.0"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["Eric Wong"]
9
- s.date = %q{2009-05-22}
9
+ s.date = %q{2009-05-26}
10
10
  s.description = %q{A small fast HTTP library and server for Rack applications.}
11
11
  s.email = %q{normalperson@yhbt.net}
12
12
  s.executables = ["unicorn", "unicorn_rails"]
13
13
  s.extensions = ["ext/unicorn/http11/extconf.rb"]
14
14
  s.extra_rdoc_files = ["CHANGELOG", "LICENSE", "README", "TODO", "bin/unicorn", "bin/unicorn_rails", "ext/unicorn/http11/ext_help.h", "ext/unicorn/http11/extconf.rb", "ext/unicorn/http11/http11.c", "ext/unicorn/http11/http11_parser.h", "ext/unicorn/http11/http11_parser.rl", "ext/unicorn/http11/http11_parser_common.rl", "lib/unicorn.rb", "lib/unicorn/app/exec_cgi.rb", "lib/unicorn/app/old_rails.rb", "lib/unicorn/app/old_rails/static.rb", "lib/unicorn/cgi_wrapper.rb", "lib/unicorn/configurator.rb", "lib/unicorn/const.rb", "lib/unicorn/http_request.rb", "lib/unicorn/http_response.rb", "lib/unicorn/launcher.rb", "lib/unicorn/socket_helper.rb", "lib/unicorn/util.rb"]
15
- s.files = [".document", ".gitignore", "CHANGELOG", "CONTRIBUTORS", "DESIGN", "GNUmakefile", "LICENSE", "Manifest", "PHILOSOPHY", "README", "Rakefile", "SIGNALS", "TODO", "TUNING", "bin/unicorn", "bin/unicorn_rails", "examples/init.sh", "ext/unicorn/http11/ext_help.h", "ext/unicorn/http11/extconf.rb", "ext/unicorn/http11/http11.c", "ext/unicorn/http11/http11_parser.h", "ext/unicorn/http11/http11_parser.rl", "ext/unicorn/http11/http11_parser_common.rl", "lib/unicorn.rb", "lib/unicorn/app/exec_cgi.rb", "lib/unicorn/app/old_rails.rb", "lib/unicorn/app/old_rails/static.rb", "lib/unicorn/cgi_wrapper.rb", "lib/unicorn/configurator.rb", "lib/unicorn/const.rb", "lib/unicorn/http_request.rb", "lib/unicorn/http_response.rb", "lib/unicorn/launcher.rb", "lib/unicorn/socket_helper.rb", "lib/unicorn/util.rb", "local.mk.sample", "setup.rb", "test/aggregate.rb", "test/benchmark/README", "test/benchmark/big_request.rb", "test/benchmark/dd.ru", "test/benchmark/request.rb", "test/benchmark/response.rb", "test/exec/README", "test/exec/test_exec.rb", "test/rails/app-1.2.3/.gitignore", "test/rails/app-1.2.3/Rakefile", "test/rails/app-1.2.3/app/controllers/application.rb", "test/rails/app-1.2.3/app/controllers/foo_controller.rb", "test/rails/app-1.2.3/app/helpers/application_helper.rb", "test/rails/app-1.2.3/config/boot.rb", "test/rails/app-1.2.3/config/database.yml", "test/rails/app-1.2.3/config/environment.rb", "test/rails/app-1.2.3/config/environments/development.rb", "test/rails/app-1.2.3/config/environments/production.rb", "test/rails/app-1.2.3/config/routes.rb", "test/rails/app-1.2.3/db/.gitignore", "test/rails/app-1.2.3/log/.gitignore", "test/rails/app-1.2.3/public/404.html", "test/rails/app-1.2.3/public/500.html", "test/rails/app-2.0.2/.gitignore", "test/rails/app-2.0.2/Rakefile", "test/rails/app-2.0.2/app/controllers/application.rb", "test/rails/app-2.0.2/app/controllers/foo_controller.rb", "test/rails/app-2.0.2/app/helpers/application_helper.rb", "test/rails/app-2.0.2/config/boot.rb", "test/rails/app-2.0.2/config/database.yml", "test/rails/app-2.0.2/config/environment.rb", "test/rails/app-2.0.2/config/environments/development.rb", "test/rails/app-2.0.2/config/environments/production.rb", "test/rails/app-2.0.2/config/routes.rb", "test/rails/app-2.0.2/db/.gitignore", "test/rails/app-2.0.2/log/.gitignore", "test/rails/app-2.0.2/public/404.html", "test/rails/app-2.0.2/public/500.html", "test/rails/app-2.1.2/.gitignore", "test/rails/app-2.1.2/Rakefile", "test/rails/app-2.1.2/app/controllers/application.rb", "test/rails/app-2.1.2/app/controllers/foo_controller.rb", "test/rails/app-2.1.2/app/helpers/application_helper.rb", "test/rails/app-2.1.2/config/boot.rb", "test/rails/app-2.1.2/config/database.yml", "test/rails/app-2.1.2/config/environment.rb", "test/rails/app-2.1.2/config/environments/development.rb", "test/rails/app-2.1.2/config/environments/production.rb", "test/rails/app-2.1.2/config/routes.rb", "test/rails/app-2.1.2/db/.gitignore", "test/rails/app-2.1.2/log/.gitignore", "test/rails/app-2.1.2/public/404.html", "test/rails/app-2.1.2/public/500.html", "test/rails/app-2.2.2/.gitignore", "test/rails/app-2.2.2/Rakefile", "test/rails/app-2.2.2/app/controllers/application.rb", "test/rails/app-2.2.2/app/controllers/foo_controller.rb", "test/rails/app-2.2.2/app/helpers/application_helper.rb", "test/rails/app-2.2.2/config/boot.rb", "test/rails/app-2.2.2/config/database.yml", "test/rails/app-2.2.2/config/environment.rb", "test/rails/app-2.2.2/config/environments/development.rb", "test/rails/app-2.2.2/config/environments/production.rb", "test/rails/app-2.2.2/config/routes.rb", "test/rails/app-2.2.2/db/.gitignore", "test/rails/app-2.2.2/log/.gitignore", "test/rails/app-2.2.2/public/404.html", "test/rails/app-2.2.2/public/500.html", "test/rails/app-2.3.2.1/.gitignore", "test/rails/app-2.3.2.1/Rakefile", "test/rails/app-2.3.2.1/app/controllers/application_controller.rb", "test/rails/app-2.3.2.1/app/controllers/foo_controller.rb", "test/rails/app-2.3.2.1/app/helpers/application_helper.rb", "test/rails/app-2.3.2.1/config/boot.rb", "test/rails/app-2.3.2.1/config/database.yml", "test/rails/app-2.3.2.1/config/environment.rb", "test/rails/app-2.3.2.1/config/environments/development.rb", "test/rails/app-2.3.2.1/config/environments/production.rb", "test/rails/app-2.3.2.1/config/routes.rb", "test/rails/app-2.3.2.1/db/.gitignore", "test/rails/app-2.3.2.1/log/.gitignore", "test/rails/app-2.3.2.1/public/404.html", "test/rails/app-2.3.2.1/public/500.html", "test/rails/test_rails.rb", "test/test_helper.rb", "test/tools/trickletest.rb", "test/unit/test_configurator.rb", "test/unit/test_http_parser.rb", "test/unit/test_request.rb", "test/unit/test_response.rb", "test/unit/test_server.rb", "test/unit/test_signals.rb", "test/unit/test_socket_helper.rb", "test/unit/test_upload.rb", "test/unit/test_util.rb", "unicorn.gemspec"]
15
+ s.files = [".document", ".gitignore", "CHANGELOG", "CONTRIBUTORS", "DESIGN", "GNUmakefile", "LICENSE", "Manifest", "PHILOSOPHY", "README", "Rakefile", "SIGNALS", "TODO", "TUNING", "bin/unicorn", "bin/unicorn_rails", "examples/init.sh", "ext/unicorn/http11/ext_help.h", "ext/unicorn/http11/extconf.rb", "ext/unicorn/http11/http11.c", "ext/unicorn/http11/http11_parser.h", "ext/unicorn/http11/http11_parser.rl", "ext/unicorn/http11/http11_parser_common.rl", "lib/unicorn.rb", "lib/unicorn/app/exec_cgi.rb", "lib/unicorn/app/old_rails.rb", "lib/unicorn/app/old_rails/static.rb", "lib/unicorn/cgi_wrapper.rb", "lib/unicorn/configurator.rb", "lib/unicorn/const.rb", "lib/unicorn/http_request.rb", "lib/unicorn/http_response.rb", "lib/unicorn/launcher.rb", "lib/unicorn/socket_helper.rb", "lib/unicorn/util.rb", "local.mk.sample", "setup.rb", "test/aggregate.rb", "test/benchmark/README", "test/benchmark/big_request.rb", "test/benchmark/dd.ru", "test/benchmark/request.rb", "test/benchmark/response.rb", "test/exec/README", "test/exec/test_exec.rb", "test/rails/app-1.2.3/.gitignore", "test/rails/app-1.2.3/Rakefile", "test/rails/app-1.2.3/app/controllers/application.rb", "test/rails/app-1.2.3/app/controllers/foo_controller.rb", "test/rails/app-1.2.3/app/helpers/application_helper.rb", "test/rails/app-1.2.3/config/boot.rb", "test/rails/app-1.2.3/config/database.yml", "test/rails/app-1.2.3/config/environment.rb", "test/rails/app-1.2.3/config/environments/development.rb", "test/rails/app-1.2.3/config/environments/production.rb", "test/rails/app-1.2.3/config/routes.rb", "test/rails/app-1.2.3/db/.gitignore", "test/rails/app-1.2.3/log/.gitignore", "test/rails/app-1.2.3/public/404.html", "test/rails/app-1.2.3/public/500.html", "test/rails/app-2.0.2/.gitignore", "test/rails/app-2.0.2/Rakefile", "test/rails/app-2.0.2/app/controllers/application.rb", "test/rails/app-2.0.2/app/controllers/foo_controller.rb", "test/rails/app-2.0.2/app/helpers/application_helper.rb", "test/rails/app-2.0.2/config/boot.rb", "test/rails/app-2.0.2/config/database.yml", "test/rails/app-2.0.2/config/environment.rb", "test/rails/app-2.0.2/config/environments/development.rb", "test/rails/app-2.0.2/config/environments/production.rb", "test/rails/app-2.0.2/config/routes.rb", "test/rails/app-2.0.2/db/.gitignore", "test/rails/app-2.0.2/log/.gitignore", "test/rails/app-2.0.2/public/404.html", "test/rails/app-2.0.2/public/500.html", "test/rails/app-2.1.2/.gitignore", "test/rails/app-2.1.2/Rakefile", "test/rails/app-2.1.2/app/controllers/application.rb", "test/rails/app-2.1.2/app/controllers/foo_controller.rb", "test/rails/app-2.1.2/app/helpers/application_helper.rb", "test/rails/app-2.1.2/config/boot.rb", "test/rails/app-2.1.2/config/database.yml", "test/rails/app-2.1.2/config/environment.rb", "test/rails/app-2.1.2/config/environments/development.rb", "test/rails/app-2.1.2/config/environments/production.rb", "test/rails/app-2.1.2/config/routes.rb", "test/rails/app-2.1.2/db/.gitignore", "test/rails/app-2.1.2/log/.gitignore", "test/rails/app-2.1.2/public/404.html", "test/rails/app-2.1.2/public/500.html", "test/rails/app-2.2.2/.gitignore", "test/rails/app-2.2.2/Rakefile", "test/rails/app-2.2.2/app/controllers/application.rb", "test/rails/app-2.2.2/app/controllers/foo_controller.rb", "test/rails/app-2.2.2/app/helpers/application_helper.rb", "test/rails/app-2.2.2/config/boot.rb", "test/rails/app-2.2.2/config/database.yml", "test/rails/app-2.2.2/config/environment.rb", "test/rails/app-2.2.2/config/environments/development.rb", "test/rails/app-2.2.2/config/environments/production.rb", "test/rails/app-2.2.2/config/routes.rb", "test/rails/app-2.2.2/db/.gitignore", "test/rails/app-2.2.2/log/.gitignore", "test/rails/app-2.2.2/public/404.html", "test/rails/app-2.2.2/public/500.html", "test/rails/app-2.3.2.1/.gitignore", "test/rails/app-2.3.2.1/Rakefile", "test/rails/app-2.3.2.1/app/controllers/application_controller.rb", "test/rails/app-2.3.2.1/app/controllers/foo_controller.rb", "test/rails/app-2.3.2.1/app/helpers/application_helper.rb", "test/rails/app-2.3.2.1/config/boot.rb", "test/rails/app-2.3.2.1/config/database.yml", "test/rails/app-2.3.2.1/config/environment.rb", "test/rails/app-2.3.2.1/config/environments/development.rb", "test/rails/app-2.3.2.1/config/environments/production.rb", "test/rails/app-2.3.2.1/config/routes.rb", "test/rails/app-2.3.2.1/db/.gitignore", "test/rails/app-2.3.2.1/log/.gitignore", "test/rails/app-2.3.2.1/public/404.html", "test/rails/app-2.3.2.1/public/500.html", "test/rails/test_rails.rb", "test/test_helper.rb", "test/unit/test_configurator.rb", "test/unit/test_http_parser.rb", "test/unit/test_request.rb", "test/unit/test_response.rb", "test/unit/test_server.rb", "test/unit/test_signals.rb", "test/unit/test_socket_helper.rb", "test/unit/test_upload.rb", "test/unit/test_util.rb", "unicorn.gemspec"]
16
16
  s.has_rdoc = true
17
17
  s.homepage = %q{http://unicorn.bogomips.org}
18
18
  s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Unicorn", "--main", "README"]
@@ -27,8 +27,11 @@ Gem::Specification.new do |s|
27
27
  s.specification_version = 3
28
28
 
29
29
  if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
30
+ s.add_runtime_dependency(%q<rack>, [">= 0"])
30
31
  else
32
+ s.add_dependency(%q<rack>, [">= 0"])
31
33
  end
32
34
  else
35
+ s.add_dependency(%q<rack>, [">= 0"])
33
36
  end
34
37
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: unicorn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Wong
@@ -9,10 +9,19 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-05-22 00:00:00 -07:00
12
+ date: 2009-05-26 00:00:00 -07:00
13
13
  default_executable:
14
- dependencies: []
15
-
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rack
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
16
25
  description: A small fast HTTP library and server for Rack applications.
17
26
  email: normalperson@yhbt.net
18
27
  executables:
@@ -168,7 +177,6 @@ files:
168
177
  - test/rails/app-2.3.2.1/public/500.html
169
178
  - test/rails/test_rails.rb
170
179
  - test/test_helper.rb
171
- - test/tools/trickletest.rb
172
180
  - test/unit/test_configurator.rb
173
181
  - test/unit/test_http_parser.rb
174
182
  - test/unit/test_request.rb
@@ -1,45 +0,0 @@
1
- require 'socket'
2
- require 'stringio'
3
-
4
- def do_test(st, chunk)
5
- s = TCPSocket.new('127.0.0.1',ARGV[0].to_i);
6
- req = StringIO.new(st)
7
- nout = 0
8
- randstop = rand(st.length / 10)
9
- STDERR.puts "stopping after: #{randstop}"
10
-
11
- begin
12
- while data = req.read(chunk)
13
- nout += s.write(data)
14
- s.flush
15
- sleep 0.1
16
- if nout > randstop
17
- STDERR.puts "BANG! after #{nout} bytes."
18
- break
19
- end
20
- end
21
- rescue Object => e
22
- STDERR.puts "ERROR: #{e}"
23
- ensure
24
- s.close
25
- end
26
- end
27
-
28
- content = "-" * (1024 * 240)
29
- st = "GET / HTTP/1.1\r\nHost: www.zedshaw.com\r\nContent-Type: text/plain\r\nContent-Length: #{content.length}\r\n\r\n#{content}"
30
-
31
- puts "length: #{content.length}"
32
-
33
- threads = []
34
- ARGV[1].to_i.times do
35
- t = Thread.new do
36
- size = 100
37
- puts ">>>> #{size} sized chunks"
38
- do_test(st, size)
39
- end
40
-
41
- t.abort_on_exception = true
42
- threads << t
43
- end
44
-
45
- threads.each {|t| t.join}