rainbows 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +11 -0
- data/.gitignore +18 -0
- data/COPYING +339 -0
- data/DEPLOY +29 -0
- data/Documentation/.gitignore +5 -0
- data/Documentation/GNUmakefile +30 -0
- data/Documentation/rainbows.1.txt +159 -0
- data/FAQ +50 -0
- data/GIT-VERSION-GEN +41 -0
- data/GNUmakefile +156 -0
- data/LICENSE +55 -0
- data/README +122 -0
- data/Rakefile +103 -0
- data/SIGNALS +94 -0
- data/TODO +20 -0
- data/TUNING +31 -0
- data/bin/rainbows +166 -0
- data/lib/rainbows.rb +53 -0
- data/lib/rainbows/base.rb +69 -0
- data/lib/rainbows/const.rb +24 -0
- data/lib/rainbows/http_response.rb +35 -0
- data/lib/rainbows/http_server.rb +47 -0
- data/lib/rainbows/revactor.rb +158 -0
- data/lib/rainbows/revactor/tee_input.rb +44 -0
- data/lib/rainbows/thread_pool.rb +96 -0
- data/lib/rainbows/thread_spawn.rb +79 -0
- data/local.mk.sample +54 -0
- data/rainbows.gemspec +47 -0
- data/setup.rb +1586 -0
- data/t/.gitignore +4 -0
- data/t/GNUmakefile +64 -0
- data/t/bin/unused_listen +39 -0
- data/t/sha1.ru +17 -0
- data/t/t0000-basic.sh +18 -0
- data/t/t1000-thread-pool-basic.sh +53 -0
- data/t/t2000-thread-spawn-basic.sh +50 -0
- data/t/t3000-revactor-basic.sh +52 -0
- data/t/t3100-revactor-tee-input.sh +49 -0
- data/t/test-lib.sh +41 -0
- data/vs_Unicorn +48 -0
- metadata +135 -0
data/t/.gitignore
ADDED
data/t/GNUmakefile
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# we can run tests in parallel with GNU make
|
2
|
+
|
3
|
+
all::
|
4
|
+
|
5
|
+
ruby = ruby
|
6
|
+
rainbows_lib := $(shell cd ../lib && pwd)
|
7
|
+
-include ../local.mk
|
8
|
+
ifeq ($(RUBY_VERSION),)
|
9
|
+
RUBY_VERSION := $(shell $(ruby) -e 'puts RUBY_VERSION')
|
10
|
+
endif
|
11
|
+
|
12
|
+
ifeq ($(RUBYLIB),)
|
13
|
+
RUBYLIB := $(rainbows_lib)
|
14
|
+
else
|
15
|
+
RUBYLIB := $(rainbows_lib):$(RUBYLIB)
|
16
|
+
endif
|
17
|
+
export RUBYLIB
|
18
|
+
|
19
|
+
T = $(wildcard t[0-9][0-9][0-9][0-9]-*.sh)
|
20
|
+
|
21
|
+
all:: $(T)
|
22
|
+
|
23
|
+
# can't rely on "set -o pipefail" since we don't require bash or ksh93 :<
|
24
|
+
t_code = $@-$(RUBY_VERSION).code
|
25
|
+
t_log = $@-$(RUBY_VERSION).log
|
26
|
+
t_run = $(TRACER) $(SHELL) $(TEST_OPTS) $@
|
27
|
+
|
28
|
+
# prefix stdout messages with ':', and stderr messages with '!'
|
29
|
+
t_wrap = ( ( ( $(RM) $(t_code); \
|
30
|
+
$(t_run); \
|
31
|
+
echo $$? > $(t_code) ) \
|
32
|
+
| sed 's/^/$(pfx):/' 1>&3 ) 2>&1 \
|
33
|
+
| sed 's/^/$(pfx)!/' 1>&2 ) 3>&1
|
34
|
+
|
35
|
+
ifndef V
|
36
|
+
quiet_pre = @echo '* $@';
|
37
|
+
quiet_post = > $(t_log) 2>&1; exit $$(cat $(t_code))
|
38
|
+
pfx =
|
39
|
+
else
|
40
|
+
quiet_pre = @echo '* $@';
|
41
|
+
quiet_post = 2>&1 | tee $(t_log); exit $$(cat $(t_code))
|
42
|
+
pfx = $@
|
43
|
+
endif
|
44
|
+
|
45
|
+
# TRACER='strace -f -o $@.strace -s 100000'
|
46
|
+
run_test = $(quiet_pre) ( $(t_wrap) ) $(quiet_post)
|
47
|
+
|
48
|
+
test-bin-$(RUBY_VERSION)/rainbows: ruby_bin = $(shell which $(ruby))
|
49
|
+
test-bin-$(RUBY_VERSION)/rainbows: ../bin/rainbows
|
50
|
+
mkdir -p $(@D)
|
51
|
+
install -m 755 $^ $@+
|
52
|
+
$(ruby) -i -p -e '$$_.gsub!(%r{^#!.*$$},"#!$(ruby_bin)")' $@+
|
53
|
+
cmp $@+ $@ || mv $@+ $@
|
54
|
+
$(RM) $@+
|
55
|
+
|
56
|
+
$(T): export ruby := $(ruby)
|
57
|
+
$(T): export PATH := $(CURDIR)/test-bin-$(RUBY_VERSION):$(PATH)
|
58
|
+
$(T): test-bin-$(RUBY_VERSION)/rainbows
|
59
|
+
$(run_test)
|
60
|
+
|
61
|
+
clean:
|
62
|
+
$(RM) -r *.log *.code test-bin-$(RUBY_VERSION)
|
63
|
+
|
64
|
+
.PHONY: $(T) clean
|
data/t/bin/unused_listen
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# this is to remain compatible with the unused_port function in the
|
3
|
+
# Unicorn test/test_helper.rb file
|
4
|
+
require 'socket'
|
5
|
+
require 'tmpdir'
|
6
|
+
|
7
|
+
default_port = 8080
|
8
|
+
addr = ENV['UNICORN_TEST_ADDR'] || '127.0.0.1'
|
9
|
+
retries = 100
|
10
|
+
base = 5000
|
11
|
+
port = sock = lock_path = nil
|
12
|
+
|
13
|
+
begin
|
14
|
+
begin
|
15
|
+
port = base + rand(32768 - base)
|
16
|
+
while port == default_port
|
17
|
+
port = base + rand(32768 - base)
|
18
|
+
end
|
19
|
+
|
20
|
+
sock = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
|
21
|
+
sock.bind(Socket.pack_sockaddr_in(port, addr))
|
22
|
+
sock.listen(5)
|
23
|
+
rescue Errno::EADDRINUSE, Errno::EACCES
|
24
|
+
sock.close rescue nil
|
25
|
+
retry if (retries -= 1) >= 0
|
26
|
+
end
|
27
|
+
|
28
|
+
# since we'll end up closing the random port we just got, there's a race
|
29
|
+
# condition could allow the random port we just chose to reselect itself
|
30
|
+
# when running tests in parallel with gmake. Create a lock file while
|
31
|
+
# we have the port here to ensure that does not happen.
|
32
|
+
lock_path = "#{Dir::tmpdir}/unicorn_test.#{addr}:#{port}.lock"
|
33
|
+
lock = File.open(lock_path, File::WRONLY|File::CREAT|File::EXCL, 0600)
|
34
|
+
rescue Errno::EEXIST
|
35
|
+
sock.close rescue nil
|
36
|
+
retry
|
37
|
+
end
|
38
|
+
sock.close rescue nil
|
39
|
+
puts "listen=#{addr}:#{port} lock_path=#{lock_path}"
|
data/t/sha1.ru
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# SHA1 checksum generator
|
2
|
+
bs = ENV['bs'] ? ENV['bs'].to_i : 4096
|
3
|
+
require 'digest/sha1'
|
4
|
+
use Rack::ContentLength
|
5
|
+
app = lambda do |env|
|
6
|
+
/\A100-continue\z/i =~ env['HTTP_EXPECT'] and
|
7
|
+
return [ 100, {}, [] ]
|
8
|
+
digest = Digest::SHA1.new
|
9
|
+
input = env['rack.input']
|
10
|
+
buf = input.read(bs)
|
11
|
+
begin
|
12
|
+
digest.update(buf)
|
13
|
+
end while input.read(bs, buf)
|
14
|
+
|
15
|
+
[ 200, {'Content-Type' => 'text/plain'}, [ digest.hexdigest << "\n" ] ]
|
16
|
+
end
|
17
|
+
run app
|
data/t/t0000-basic.sh
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
. ./test-lib.sh
|
3
|
+
|
4
|
+
eval $(unused_listen)
|
5
|
+
config_ru=$(mktemp -t rainbows.$$.XXXXXXXX.config.ru)
|
6
|
+
pid=$(mktemp -t rainbows.$$.XXXXXXXX.pid)
|
7
|
+
TEST_RM_LIST="$TEST_RM_LIST $config_ru $lock_path"
|
8
|
+
|
9
|
+
cat > $config_ru <<\EOF
|
10
|
+
use Rack::ContentLength
|
11
|
+
use Rack::ContentType
|
12
|
+
run lambda { |env| [ 200, {}, [ env.inspect << "\n" ] ] }
|
13
|
+
EOF
|
14
|
+
|
15
|
+
rainbows $config_ru -l $listen --pid $pid &
|
16
|
+
wait_for_pid $pid
|
17
|
+
curl -sSfv http://$listen/
|
18
|
+
kill $(cat $pid)
|
@@ -0,0 +1,53 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
. ./test-lib.sh
|
3
|
+
|
4
|
+
eval $(unused_listen)
|
5
|
+
config_ru=$(mktemp -t rainbows.$$.XXXXXXXX.config.ru)
|
6
|
+
unicorn_config=$(mktemp -t rainbows.$$.XXXXXXXX.unicorn.rb)
|
7
|
+
curl_out=$(mktemp -t rainbows.$$.XXXXXXXX.curl.out)
|
8
|
+
curl_err=$(mktemp -t rainbows.$$.XXXXXXXX.curl.err)
|
9
|
+
pid=$(mktemp -t rainbows.$$.XXXXXXXX.pid)
|
10
|
+
TEST_RM_LIST="$TEST_RM_LIST $config_ru $unicorn_config $lock_path"
|
11
|
+
TEST_RM_LIST="$TEST_RM_LIST $curl_out $curl_err"
|
12
|
+
|
13
|
+
cat > $config_ru <<\EOF
|
14
|
+
use Rack::ContentLength
|
15
|
+
use Rack::ContentType
|
16
|
+
run lambda { |env|
|
17
|
+
sleep 1
|
18
|
+
[ 200, {}, [ Thread.current.inspect << "\n" ] ]
|
19
|
+
}
|
20
|
+
EOF
|
21
|
+
|
22
|
+
nr_client=30
|
23
|
+
nr_thread=10
|
24
|
+
|
25
|
+
cat > $unicorn_config <<EOF
|
26
|
+
listen "$listen"
|
27
|
+
pid "$pid"
|
28
|
+
Rainbows! do
|
29
|
+
use :ThreadPool
|
30
|
+
worker_connections $nr_thread
|
31
|
+
end
|
32
|
+
EOF
|
33
|
+
|
34
|
+
rainbows -D $config_ru -c $unicorn_config
|
35
|
+
wait_for_pid $pid
|
36
|
+
|
37
|
+
start=$(date +%s)
|
38
|
+
for i in $(awk "BEGIN{for(i=0;i<$nr_client;++i) print i}" </dev/null)
|
39
|
+
do
|
40
|
+
( curl -sSf http://$listen/$i >> $curl_out 2>> $curl_err ) &
|
41
|
+
done
|
42
|
+
wait
|
43
|
+
echo elapsed=$(( $(date +%s) - $start ))
|
44
|
+
|
45
|
+
kill $(cat $pid)
|
46
|
+
|
47
|
+
! test -s $curl_err
|
48
|
+
test x"$(wc -l < $curl_out)" = x$nr_client
|
49
|
+
|
50
|
+
nr=$(sort < $curl_out | uniq | wc -l)
|
51
|
+
|
52
|
+
test "$nr" -le $nr_thread
|
53
|
+
test "$nr" -gt 1
|
@@ -0,0 +1,50 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
. ./test-lib.sh
|
3
|
+
|
4
|
+
eval $(unused_listen)
|
5
|
+
config_ru=$(mktemp -t rainbows.$$.XXXXXXXX.config.ru)
|
6
|
+
unicorn_config=$(mktemp -t rainbows.$$.XXXXXXXX.unicorn.rb)
|
7
|
+
curl_out=$(mktemp -t rainbows.$$.XXXXXXXX.curl.out)
|
8
|
+
curl_err=$(mktemp -t rainbows.$$.XXXXXXXX.curl.err)
|
9
|
+
pid=$(mktemp -t rainbows.$$.XXXXXXXX.pid)
|
10
|
+
TEST_RM_LIST="$TEST_RM_LIST $config_ru $unicorn_config $lock_path"
|
11
|
+
TEST_RM_LIST="$TEST_RM_LIST $curl_out $curl_err"
|
12
|
+
|
13
|
+
cat > $config_ru <<\EOF
|
14
|
+
use Rack::ContentLength
|
15
|
+
use Rack::ContentType
|
16
|
+
run lambda { |env|
|
17
|
+
sleep 1
|
18
|
+
[ 200, {}, [ Thread.current.inspect << "\n" ] ]
|
19
|
+
}
|
20
|
+
EOF
|
21
|
+
|
22
|
+
nr_client=30
|
23
|
+
nr_thread=10
|
24
|
+
|
25
|
+
cat > $unicorn_config <<EOF
|
26
|
+
listen "$listen"
|
27
|
+
pid "$pid"
|
28
|
+
Rainbows! do
|
29
|
+
use :ThreadSpawn
|
30
|
+
worker_connections $nr_thread
|
31
|
+
end
|
32
|
+
EOF
|
33
|
+
|
34
|
+
rainbows -D $config_ru -c $unicorn_config
|
35
|
+
wait_for_pid $pid
|
36
|
+
|
37
|
+
start=$(date +%s)
|
38
|
+
for i in $(awk "BEGIN{for(i=0;i<$nr_client;++i) print i}" </dev/null)
|
39
|
+
do
|
40
|
+
( curl -sSf http://$listen/$i >> $curl_out 2>> $curl_err ) &
|
41
|
+
done
|
42
|
+
wait
|
43
|
+
echo elapsed=$(( $(date +%s) - $start ))
|
44
|
+
|
45
|
+
kill $(cat $pid)
|
46
|
+
|
47
|
+
! test -s $curl_err
|
48
|
+
test x"$(wc -l < $curl_out)" = x$nr_client
|
49
|
+
nr=$(sort < $curl_out | uniq | wc -l)
|
50
|
+
test "$nr" -eq $nr_client
|
@@ -0,0 +1,52 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
. ./test-lib.sh
|
3
|
+
require_revactor
|
4
|
+
|
5
|
+
eval $(unused_listen)
|
6
|
+
config_ru=$(mktemp -t rainbows.$$.XXXXXXXX.config.ru)
|
7
|
+
unicorn_config=$(mktemp -t rainbows.$$.XXXXXXXX.unicorn.rb)
|
8
|
+
curl_out=$(mktemp -t rainbows.$$.XXXXXXXX.curl.out)
|
9
|
+
curl_err=$(mktemp -t rainbows.$$.XXXXXXXX.curl.err)
|
10
|
+
pid=$(mktemp -t rainbows.$$.XXXXXXXX.pid)
|
11
|
+
TEST_RM_LIST="$TEST_RM_LIST $config_ru $unicorn_config $lock_path"
|
12
|
+
TEST_RM_LIST="$TEST_RM_LIST $curl_out $curl_err"
|
13
|
+
|
14
|
+
cat > $config_ru <<\EOF
|
15
|
+
use Rack::ContentLength
|
16
|
+
use Rack::ContentType
|
17
|
+
run lambda { |env|
|
18
|
+
Actor.sleep 1
|
19
|
+
[ 200, {}, [ Thread.current.inspect << "\n" ] ]
|
20
|
+
}
|
21
|
+
EOF
|
22
|
+
|
23
|
+
nr_client=30
|
24
|
+
nr_actor=10
|
25
|
+
|
26
|
+
cat > $unicorn_config <<EOF
|
27
|
+
listen "$listen"
|
28
|
+
pid "$pid"
|
29
|
+
Rainbows! do
|
30
|
+
use :Revactor
|
31
|
+
worker_connections $nr_actor
|
32
|
+
end
|
33
|
+
EOF
|
34
|
+
|
35
|
+
rainbows -D $config_ru -c $unicorn_config
|
36
|
+
wait_for_pid $pid
|
37
|
+
|
38
|
+
start=$(date +%s)
|
39
|
+
for i in $(awk "BEGIN{for(i=0;i<$nr_client;++i) print i}" </dev/null)
|
40
|
+
do
|
41
|
+
( curl -sSf http://$listen/$i >> $curl_out 2>> $curl_err ) &
|
42
|
+
done
|
43
|
+
wait
|
44
|
+
echo elapsed=$(( $(date +%s) - $start ))
|
45
|
+
|
46
|
+
kill $(cat $pid)
|
47
|
+
|
48
|
+
! test -s $curl_err
|
49
|
+
test x"$(wc -l < $curl_out)" = x$nr_client
|
50
|
+
nr=$(sort < $curl_out | uniq | wc -l)
|
51
|
+
|
52
|
+
test "$nr" -eq 1
|
@@ -0,0 +1,49 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
nr_client=${nr_client-25}
|
3
|
+
nr_actor=${nr_actor-50}
|
4
|
+
|
5
|
+
. ./test-lib.sh
|
6
|
+
require_revactor
|
7
|
+
|
8
|
+
eval $(unused_listen)
|
9
|
+
unicorn_config=$(mktemp -t rainbows.$$.unicorn.rb.XXXXXXXX)
|
10
|
+
curl_out=$(mktemp -t rainbows.$$.curl.out.XXXXXXXX)
|
11
|
+
curl_err=$(mktemp -t rainbows.$$.curl.err.XXXXXXXX)
|
12
|
+
r_err=$(mktemp -t rainbows.$$.r.err.XXXXXXXX)
|
13
|
+
r_out=$(mktemp -t rainbows.$$.r.out.XXXXXXXX)
|
14
|
+
pid=$(mktemp -t rainbows.$$.pid.XXXXXXXX)
|
15
|
+
blob=$(mktemp -t rainbows.$$.blob.XXXXXXXX)
|
16
|
+
TEST_RM_LIST="$TEST_RM_LIST $unicorn_config $lock_path $r_err $r_out"
|
17
|
+
TEST_RM_LIST="$TEST_RM_LIST $curl_out $curl_err $blob"
|
18
|
+
|
19
|
+
cat > $unicorn_config <<EOF
|
20
|
+
listen "$listen"
|
21
|
+
pid "$pid"
|
22
|
+
stderr_path "$r_err"
|
23
|
+
stdout_path "$r_out"
|
24
|
+
Rainbows! do
|
25
|
+
use :Revactor
|
26
|
+
worker_connections $nr_actor
|
27
|
+
end
|
28
|
+
EOF
|
29
|
+
|
30
|
+
echo pid=$pid
|
31
|
+
rainbows -D sha1.ru -c $unicorn_config
|
32
|
+
wait_for_pid $pid
|
33
|
+
|
34
|
+
dd if=/dev/urandom bs=1M count=10 of=$blob 2>/dev/null
|
35
|
+
|
36
|
+
start=$(date +%s)
|
37
|
+
for i in $(awk "BEGIN{for(i=0;i<$nr_client;++i) print i}" </dev/null)
|
38
|
+
do
|
39
|
+
( curl -sSf -T- < $blob http://$listen/$i >> $curl_out 2>> $curl_err ) &
|
40
|
+
done
|
41
|
+
wait
|
42
|
+
echo elapsed=$(( $(date +%s) - $start ))
|
43
|
+
|
44
|
+
kill $(cat $pid)
|
45
|
+
test $nr_client -eq $(wc -l < $curl_out)
|
46
|
+
test 1 -eq $(sort < $curl_out | uniq | wc -l)
|
47
|
+
blob_sha1=$( expr "$(sha1sum < $blob)" : '\([a-f0-9]\+\)')
|
48
|
+
echo blob_sha1=$blob_sha1
|
49
|
+
test x"$blob_sha1" = x"$(sort < $curl_out | uniq)"
|
data/t/test-lib.sh
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
# Copyright (c) 2009 Eric Wong
|
3
|
+
set -e
|
4
|
+
set -u
|
5
|
+
T=$(basename $0)
|
6
|
+
ruby="${ruby-ruby}"
|
7
|
+
|
8
|
+
# ensure a sane environment
|
9
|
+
TZ=UTC LC_ALL=C LANG=C
|
10
|
+
export LANG LC_ALL TZ
|
11
|
+
unset CDPATH
|
12
|
+
|
13
|
+
die () {
|
14
|
+
echo >&2 "$@"
|
15
|
+
exit 1
|
16
|
+
}
|
17
|
+
|
18
|
+
TEST_RM_LIST=""
|
19
|
+
trap 'rm -f $TEST_RM_LIST' 0
|
20
|
+
PATH=$PWD/bin:$PATH
|
21
|
+
export PATH
|
22
|
+
|
23
|
+
test -x $PWD/bin/unused_listen || die "must be run in 't' directory"
|
24
|
+
|
25
|
+
wait_for_pid () {
|
26
|
+
path="$1"
|
27
|
+
nr=30
|
28
|
+
while ! test -s "$path" && test $nr -gt 0
|
29
|
+
do
|
30
|
+
nr=$(($nr - 1))
|
31
|
+
sleep 1
|
32
|
+
done
|
33
|
+
}
|
34
|
+
|
35
|
+
require_revactor () {
|
36
|
+
if ! $ruby -rrevactor -e "puts Revactor::VERSION" >/dev/null 2>&1
|
37
|
+
then
|
38
|
+
echo >&2 "skipping $T since we don't have Revactor"
|
39
|
+
exit 0
|
40
|
+
fi
|
41
|
+
}
|
data/vs_Unicorn
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
= \Rainbows! is like Unicorn, but Different...
|
2
|
+
|
3
|
+
While \Rainbows! depends on Unicorn for its process/socket management,
|
4
|
+
HTTP parser and configuration language; \Rainbows! is more ambitious.
|
5
|
+
|
6
|
+
== Differences from Unicorn
|
7
|
+
|
8
|
+
* log rotation is handled immediately in \Rainbows! whereas Unicorn has
|
9
|
+
the luxury of delaying it until the current request is finished
|
10
|
+
processing to prevent log entries for one request to be split across
|
11
|
+
files.
|
12
|
+
|
13
|
+
* load balancing between workers is imperfect, certain worker processes
|
14
|
+
may be servicing more requests than others so it is important to not
|
15
|
+
set +worker_connections+ too high. Unicorn worker processes can never
|
16
|
+
be servicing more than one request at once.
|
17
|
+
|
18
|
+
* speculative, non-blocking accept() is not used, this is to help
|
19
|
+
load balance between multiple worker processes.
|
20
|
+
|
21
|
+
* HTTP pipelining and keepalive may be used for GET and HEAD requests.
|
22
|
+
|
23
|
+
* Less heavily-tested and inherently more complex.
|
24
|
+
|
25
|
+
|
26
|
+
== Similarities with Unicorn
|
27
|
+
|
28
|
+
While some similarities are obvious (we depend on and subclass of
|
29
|
+
Unicorn code), some things are not:
|
30
|
+
|
31
|
+
* Does not attempt to accept() connections when pre-configured limits
|
32
|
+
are hit (+worker_connections+). This will first help balance load
|
33
|
+
to different worker processes, and if your listen() +:backlog+ is
|
34
|
+
overflowing: to other machines in your cluster.
|
35
|
+
|
36
|
+
* Accepts the same {signals}[http://unicorn.bogomips.org/SIGNALS.html]
|
37
|
+
for process management, so you can share scripts to manage them (and
|
38
|
+
nginx, too).
|
39
|
+
|
40
|
+
* supports per-process listeners, allowing an external load balancer
|
41
|
+
like haproxy or nginx to be used to balance between multiple
|
42
|
+
worker processes.
|
43
|
+
|
44
|
+
* Exposes a streaming "rack.input" to the Rack application that reads
|
45
|
+
data off the socket as the application reads it (while retaining
|
46
|
+
rewindable semantics as required by Rack). This allows Rack-compliant
|
47
|
+
apps/middleware to implement things such as real-time upload progress
|
48
|
+
monitoring.
|