public
Description: A very fast & simple Ruby web server
Homepage: http://code.macournoyer.com/thin/
Clone URL: git://github.com/macournoyer/thin.git
Search Repo:
* Add --max-persistent-conns option to sets the maximum number of 
persistent connections.
  Set to 0 to disable Keep-Alive.
macournoyer (author)
Wed Feb 20 19:57:05 -0800 2008
commit  2b7d72da3fd20c760eafefa2ad569fa77a6846c0
tree    e4e203a5877815f94922a690ae430f8a0e6793a9
parent  742a7b74aaaad3a6ac81f06dc94f724c24b15594
...
1
2
 
 
 
3
4
5
6
7
8
 
 
9
10
11
...
1
 
2
3
4
5
6
7
8
 
 
9
10
11
12
13
0
@@ -1,11 +1,13 @@
0
 == 0.7.0 Spherical Cow release
0
- * INT signal now force stop and QUIT signal graceful stop.
0
+ * Add --max-persistent-conns option to sets the maximum number of persistent connections.
0
+ Set to 0 to disable Keep-Alive.
0
+ * INT signal now force stop and QUIT signal gracefully stops.
0
  * Warn when descriptors table size can't be set as high as expected.
0
  * Eval Rackup config file using top level bindings.
0
  * Remove daemons gem dependency on Windows plateform, fixes #45.
0
  * Change default timeout from 60 to 30 seconds.
0
- * Add --descriptors option to sets the maximum number of file or socket descriptors that
0
- your process may open, defaults to 4096.
0
+ * Add --max-conns option to sets the maximum number of file or socket descriptors that
0
+ your process may open, defaults to 1024.
0
  * Tail logfile when stopping and restarting a demonized server, fixes #26.
0
  * Wrap application in a Rack::CommonLogger adapter in debug mode.
0
  * --debug (-D) option no longer set $DEBUG so logging will be less verbose
...
75
76
77
 
 
 
 
 
 
 
 
 
 
78
79
80
81
82
 
 
83
84
85
...
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
 
 
91
92
93
94
95
0
@@ -75,11 +75,21 @@
0
       @connector.connection_finished(self)
0
     end
0
     
0
+ # Allows this connection to be persistent.
0
+ def can_persist!
0
+ @can_persist = true
0
+ end
0
+
0
+ # Return +true+ if this connection is allowed to stay open and be persistent.
0
+ def can_persist?
0
+ @can_persist
0
+ end
0
+
0
     # Return +true+ if the connection must be left open
0
     # and ready to be reused for another request.
0
     def persistent?
0
- @response.persistent?
0
- end
0
+ @can_persist && @response.persistent?
0
+ end
0
     
0
     # IP Address of the remote client.
0
     def remote_address
...
11
12
13
 
 
 
 
 
 
14
15
16
 
 
 
 
17
18
19
...
29
30
31
 
 
 
 
 
 
 
32
33
34
...
40
41
42
 
43
44
45
...
11
12
13
14
15
16
17
18
19
20
 
 
21
22
23
24
25
26
27
...
37
38
39
40
41
42
43
44
45
46
47
48
49
...
55
56
57
58
59
60
61
0
@@ -11,9 +11,17 @@
0
       # Maximum time for incoming data to arrive
0
       attr_accessor :timeout
0
       
0
+ # Maximum number of connections that can be persistent
0
+ attr_accessor :maximum_persistent_connections
0
+
0
+ # Number of persistent connections currently opened
0
+ attr_accessor :persistent_connection_count
0
+
0
       def initialize
0
- @connections = []
0
- @timeout = Server::DEFAULT_TIMEOUT
0
+ @connections = []
0
+ @timeout = Server::DEFAULT_TIMEOUT
0
+ @persistent_connection_count = 0
0
+ @maximum_persistent_connections = Server::DEFAULT_MAXIMUM_PERSISTENT_CONNECTIONS
0
       end
0
             
0
       # Free up resources used by the connector.
0
@@ -29,6 +37,13 @@
0
         connection.connector = self
0
         connection.app = @server.app
0
         connection.comm_inactivity_timeout = @timeout
0
+
0
+ # We control the number of persistent connections by keeping
0
+ # a count of the total one allowed yet.
0
+ if @persistent_connection_count < @maximum_persistent_connections
0
+ connection.can_persist!
0
+ @persistent_connection_count += 1
0
+ end
0
 
0
         @connections << connection
0
       end
0
@@ -40,6 +55,7 @@
0
       
0
       # Called by a connection when it's unbinded.
0
       def connection_finished(connection)
0
+ @persistent_connection_count -= 1 if connection.can_persist?
0
         @connections.delete(connection)
0
       end
0
       
...
36
37
38
39
40
41
42
 
 
 
 
 
43
44
45
...
36
37
38
 
 
 
 
39
40
41
42
43
44
45
46
0
@@ -36,10 +36,11 @@
0
           Server.new(@options[:address], @options[:port])
0
         end
0
 
0
- server.pid_file = @options[:pid]
0
- server.log_file = @options[:log]
0
- server.timeout = @options[:timeout]
0
- server.descriptor_table_size = @options[:descriptors]
0
+ server.pid_file = @options[:pid]
0
+ server.log_file = @options[:log]
0
+ server.timeout = @options[:timeout]
0
+ server.maximum_connections = @options[:max_conns]
0
+ server.maximum_persistent_connections = @options[:max_persistent_conns]
0
 
0
         if @options[:daemonize]
0
           server.daemonize
...
32
33
34
35
36
37
38
39
40
41
42
 
 
 
 
 
 
 
 
 
43
44
45
46
...
64
65
66
67
68
69
70
71
72
73
74
75
76
77
...
93
94
95
 
 
 
 
 
 
 
 
 
 
 
 
96
97
98
...
32
33
34
 
 
 
 
 
 
 
 
35
36
37
38
39
40
41
42
43
44
45
46
47
...
65
66
67
 
 
68
69
70
71
 
 
72
73
74
...
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
0
@@ -32,14 +32,15 @@
0
       
0
       # Default options values
0
       @options = {
0
- :chdir => Dir.pwd,
0
- :environment => 'development',
0
- :address => '0.0.0.0',
0
- :port => 3000,
0
- :timeout => 30,
0
- :log => 'log/thin.log',
0
- :pid => 'tmp/pids/thin.pid',
0
- :descriptors => 4096
0
+ :chdir => Dir.pwd,
0
+ :environment => 'development',
0
+ :address => '0.0.0.0',
0
+ :port => Server::DEFAULT_PORT,
0
+ :timeout => Server::DEFAULT_TIMEOUT,
0
+ :log => 'log/thin.log',
0
+ :pid => 'tmp/pids/thin.pid',
0
+ :max_conns => Server::DEFAULT_MAXIMUM_CONNECTIONS,
0
+ :max_persistent_conns => Server::DEFAULT_MAXIMUM_PERSISTENT_CONNECTIONS
0
       }
0
       
0
       parse!
0
0
@@ -64,14 +65,10 @@
0
         opts.on("-e", "--environment ENV", "Rails environment " +
0
                                            "(default: #{@options[:environment]})") { |env| @options[:environment] = env }
0
         opts.on("-c", "--chdir DIR", "Change to dir before starting") { |dir| @options[:chdir] = File.expand_path(dir) }
0
- opts.on("-t", "--timeout SEC", "Request or command timeout in sec " +
0
- "(default: #{@options[:timeout]})") { |sec| @options[:timeout] = sec.to_i }
0
         opts.on("-r", "--rackup FILE", "Load a Rack config file instead of " +
0
                                        "the Rails adapter") { |file| @options[:rackup] = file }
0
         opts.on( "--prefix PATH", "Mount the app under PATH (start with /)") { |path| @options[:prefix] = path }
0
         opts.on( "--stats PATH", "Mount the Stats adapter under PATH") { |path| @options[:stats] = path }
0
- opts.on( "--descriptors NUM", "Descriptor table size " +
0
- "(default: #{@options[:descriptors]})") { |num| @options[:descriptors] = num.to_i }
0
         
0
         unless Thin.win? # Daemonizing not supported on Windows
0
           opts.separator ""
0
@@ -93,6 +90,18 @@
0
           opts.on("-C", "--config FILE", "Load options from config file") { |file| @options[:config] = file }
0
           opts.on( "--all [DIR]", "Send command to each config files in DIR") { |dir| @options[:all] = dir } if Thin.linux?
0
         end
0
+
0
+ opts.separator ""
0
+ opts.separator "Tuning options:"
0
+
0
+ opts.on("-t", "--timeout SEC", "Request or command timeout in sec " +
0
+ "(default: #{@options[:timeout]})") { |sec| @options[:timeout] = sec.to_i }
0
+ opts.on( "--max-conns NUM",
0
+ "Maximum number of connections (default: #{@options[:max_conns]})",
0
+ "Might require superuser privileges to set higher then 1024") { |num| @options[:max_conns] = num.to_i }
0
+ opts.on( "--max-persistent-conns NUM",
0
+ "Maximum number of persistent connections " +
0
+ "(default: #{@options[:max_persistent_conns]})") { |num| @options[:max_persistent_conns] = num.to_i }
0
         
0
         opts.separator ""
0
         opts.separator "Common options:"
...
47
48
49
50
51
 
 
 
 
52
53
54
55
...
57
58
59
60
 
61
62
63
64
65
 
 
 
 
 
 
66
67
68
69
70
...
80
81
82
83
84
 
85
 
 
 
 
 
86
87
88
...
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
 
120
121
122
...
132
133
134
 
 
135
136
137
...
202
203
204
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
206
...
47
48
49
 
 
50
51
52
53
54
55
56
57
...
59
60
61
 
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
...
88
89
90
 
91
92
93
94
95
96
97
98
99
100
101
...
114
115
116
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
118
119
120
...
130
131
132
133
134
135
136
137
...
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
0
@@ -47,8 +47,10 @@
0
     extend Forwardable
0
     
0
     # Default values
0
- DEFAULT_TIMEOUT = 30 #sec
0
- DEFAULT_PORT = 3000
0
+ DEFAULT_TIMEOUT = 30 #sec
0
+ DEFAULT_PORT = 3000
0
+ DEFAULT_MAXIMUM_CONNECTIONS = 1024
0
+ DEFAULT_MAXIMUM_PERSISTENT_CONNECTIONS = 512
0
         
0
     # Application (Rack adapter) called with the request that produces the response.
0
     attr_accessor :app
0
0
@@ -57,12 +59,18 @@
0
     attr_accessor :connector
0
     
0
     # Maximum number of file or socket descriptors that the server may open.
0
- attr_reader :descriptor_table_size
0
+ attr_accessor :maximum_connections
0
     
0
     # Maximum number of seconds for incoming data to arrive before the connection
0
     # is dropped.
0
     def_delegators :@connector, :timeout, :timeout=
0
     
0
+ # Maximum number of connection that can be persistent at the same time.
0
+ # Most browser never close the connection so most of the time they are closed
0
+ # when the timeout occur. If we don't control the number of persistent connection,
0
+ # if would be very easy to overflow the server for a DoS attack.
0
+ def_delegators :@connector, :maximum_persistent_connections, :maximum_persistent_connections=
0
+
0
     # Address and port on which the server is listening for connections.
0
     def_delegators :@connector, :host, :port
0
     
0
0
0
@@ -80,9 +88,14 @@
0
         Connectors::TcpServer.new(host_or_socket_or_connector, port.to_i)
0
       end
0
 
0
- @connector.server = self
0
       @app = app
0
+ @connector.server = self
0
       
0
+ # Set defaults
0
+ @maximum_connections = DEFAULT_MAXIMUM_CONNECTIONS
0
+ @connector.maximum_persistent_connections = DEFAULT_MAXIMUM_PERSISTENT_CONNECTIONS
0
+ @connector.timeout = DEFAULT_TIMEOUT
0
+
0
       # Allow using Rack builder as a block
0
       @app = Rack::Builder.new(&block).to_app if block
0
       
0
@@ -101,22 +114,7 @@
0
     def self.start(*args, &block)
0
       new(*args, &block).start!
0
     end
0
-
0
- # Set the maximum number of socket descriptors that the server may open.
0
- # The process needs to have required privilege to set it higher the 1024 on
0
- # some systems.
0
- def descriptor_table_size=(size)
0
- @descriptor_table_size = EventMachine.set_descriptor_table_size(size)
0
-
0
- log ">> Setting descriptor table size to #{@descriptor_table_size}"
0
- if @descriptor_table_size < size
0
- log "!! descriptor table size smaller then requested, " +
0
- "run with sudo to set higher"
0
- end
0
-
0
- @descriptor_table_size
0
- end
0
-
0
+
0
     # Start the server and listen for connections.
0
     # Also register signals:
0
     # * INT calls +stop+ to shutdown gracefully.
0
@@ -132,6 +130,8 @@
0
       log ">> Thin web server (v#{VERSION::STRING} codename #{VERSION::CODENAME})"
0
       debug ">> Debugging ON"
0
       trace ">> Tracing ON"
0
+
0
+ set_descriptor_table_size
0
       
0
       log ">> Listening on #{@connector}, CTRL+C to stop"
0
       
0
@@ -202,6 +202,22 @@
0
         trap('INT') { stop! }
0
         trap('TERM') { stop! }
0
       end
0
+
0
+ # Set the maximum number of socket descriptors that the server may open.
0
+ # The process needs to have required privilege to set it higher the 1024 on
0
+ # some systems.
0
+ def set_descriptor_table_size
0
+ requested_maximum_connections = @maximum_connections
0
+ @maximum_connections = EventMachine.set_descriptor_table_size(requested_maximum_connections)
0
+
0
+ log ">> Setting maximum connections to #{@maximum_connections}"
0
+ if @maximum_connections < requested_maximum_connections
0
+ log "!! Maximum connections smaller then requested, " +
0
+ "run with sudo to set higher"
0
+ end
0
+
0
+ @maximum_connections
0
+ end
0
   end
0
 end
...
54
55
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
...
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
0
@@ -54,5 +54,20 @@
0
     @connection.stub!(:get_peername).and_return("\020\002?E\177\000\000\001\000\000\000\000\000\000\000\000")
0
     @connection.remote_address.should == '127.0.0.1'
0
   end
0
+
0
+ it "should not be persistent" do
0
+ @connection.should_not be_persistent
0
+ end
0
+
0
+ it "should be persistent when response is and allowed" do
0
+ @connection.response.stub!(:persistent?).and_return(true)
0
+ @connection.can_persist!
0
+ @connection.should be_persistent
0
+ end
0
+
0
+ it "should not be persistent when response is but not allowed" do
0
+ @connection.response.persistent!
0
+ @connection.should_not be_persistent
0
+ end
0
 end
...
4
5
6
7
 
 
 
 
 
 
 
8
9
10
...
19
20
21
22
 
 
23
24
25
...
4
5
6
 
7
8
9
10
11
12
13
14
15
16
...
25
26
27
 
28
29
30
31
32
0
@@ -4,7 +4,13 @@
0
 
0
 describe Controller, 'start' do
0
   before do
0
- @controller = Controller.new(:address => '0.0.0.0', :port => 3000, :pid => 'thin.pid', :log => 'thin.log', :timeout => 60)
0
+ @controller = Controller.new(:address => '0.0.0.0',
0
+ :port => 3000,
0
+ :pid => 'thin.pid',
0
+ :log => 'thin.log',
0
+ :timeout => 60,
0
+ :max_conns => 2000,
0
+ :max_persistent_conns => 1000)
0
     
0
     @server = OpenStruct.new
0
     @adapter = OpenStruct.new
0
@@ -19,7 +25,8 @@
0
     @server.app.should == @adapter
0
     @server.pid_file.should == 'thin.pid'
0
     @server.log_file.should == 'thin.log'
0
- @server.timeout.should == 60
0
+ @server.maximum_connections.should == 2000
0
+ @server.maximum_persistent_connections.should == 1000
0
   end
0
   
0
   it "should start as daemon" do
...
8
9
10
 
11
12
13
...
18
19
20
 
 
21
22
23
...
29
30
31
 
 
32
33
34
...
40
41
42
 
 
43
44
45
46
47
...
52
53
54
 
 
55
56
57
58
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
60
61
 
 
 
 
 
62
...
8
9
10
11
12
13
14
...
19
20
21
22
23
24
25
26
...
32
33
34
35
36
37
38
39
...
45
46
47
48
49
50
51
52
53
54
...
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
0
@@ -8,6 +8,7 @@
0
       body = env['PATH_INFO'] + '-' + calls.to_s
0
       [200, { 'Content-Type' => 'text/html', 'Content-Length' => body.size.to_s }, body]
0
     end
0
+ @server.maximum_persistent_connections = 1024
0
   end
0
   
0
   it "should pipeline request on same socket" do
0
@@ -18,6 +19,8 @@
0
     response = socket.read
0
     socket.close
0
     
0
+ wait_for_requests_to_complete!
0
+
0
     response.should include('/first-1', '/second-2')
0
   end
0
   
0
@@ -29,6 +32,8 @@
0
     response = socket.read
0
     socket.close
0
     
0
+ wait_for_requests_to_complete!
0
+
0
     response.should include('/first-1', '/second-2')
0
   end
0
   
0
@@ -40,6 +45,8 @@
0
     response = socket.read
0
     socket.close
0
     
0
+ wait_for_requests_to_complete!
0
+
0
     response.should include('/first-1')
0
     response.should_not include('/second-2')
0
   end
0
0
0
@@ -52,12 +59,51 @@
0
     response = socket.read
0
     socket.close
0
     
0
+ wait_for_requests_to_complete!
0
+
0
     response.should include('/first-1')
0
     response.should_not include('/second-2')
0
   end
0
   
0
+ it "should not allow more persistent connection then maximum" do
0
+ @server.maximum_persistent_connections = 1
0
+
0
+ socket1 = TCPSocket.new('0.0.0.0', 3333)
0
+ socket1.write "GET / HTTP/1.1\r\n\r\n"
0
+ socket1.flush
0
+ socket2 = TCPSocket.new('0.0.0.0', 3333)
0
+ socket2.write "GET / HTTP/1.1\r\n\r\n"
0
+ socket2.flush
0
+
0
+ @server.connector.persistent_connection_count.should == 1
0
+ @server.connector.size.should == 2
0
+
0
+ socket1.close
0
+ socket2.close
0
+ end
0
+
0
+ it "should decrement persistent connection on close" do
0
+ socket = TCPSocket.new('0.0.0.0', 3333)
0
+ socket.write "GET / HTTP/1.1\r\n\r\n"
0
+ socket.flush
0
+
0
+ @server.connector.persistent_connection_count.should == 1
0
+
0
+ socket.write "GET / HTTP/1.1\r\nConnection: close\r\n\r\n"
0
+ socket.close
0
+
0
+ wait_for_requests_to_complete!
0
+
0
+ @server.connector.persistent_connection_count.should == 0
0
+ end
0
+
0
   after do
0
     stop_server
0
   end
0
+
0
+ private
0
+ def wait_for_requests_to_complete!
0
+ sleep 0.1 until @server.connector.size == 0
0
+ end
0
 end
...
7
8
9
10
11
 
 
 
12
13
14
15
16
17
18
 
 
 
 
19
20
...
7
8
9
 
 
10
11
12
13
14
15
16
 
 
 
17
18
19
20
21
22
0
@@ -7,15 +7,17 @@
0
   
0
   it "should set descriptor table size" do
0
     @server.should_receive(:log).once
0
- @server.descriptor_table_size = 100
0
- @server.descriptor_table_size.should == 100
0
+ @server.maximum_connections = 100
0
+ @server.send(:set_descriptor_table_size)
0
+ @server.maximum_connections.should == 100
0
   end
0
 
0
   it "should warn when descriptor table size too large" do
0
     @server.stub!(:log)
0
- @server.should_receive(:log).with(/^!! descriptor table size smaller then requested/)
0
- @server.descriptor_table_size = 100_000
0
- @server.descriptor_table_size.should < 100_000
0
+ @server.should_receive(:log).with(/^!!/)
0
+ @server.maximum_connections = 100_000
0
+ @server.send(:set_descriptor_table_size)
0
+ @server.maximum_connections.should < 100_000
0
   end
0
 end

Comments

    No one has commented yet.