macournoyer-thin 1.0.1

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.
Files changed (135) hide show
  1. data/CHANGELOG +233 -0
  2. data/COPYING +18 -0
  3. data/README +77 -0
  4. data/Rakefile +13 -0
  5. data/benchmark/abc +51 -0
  6. data/benchmark/benchmarker.rb +80 -0
  7. data/benchmark/runner +79 -0
  8. data/bin/thin +6 -0
  9. data/example/adapter.rb +32 -0
  10. data/example/config.ru +23 -0
  11. data/example/monit_sockets +20 -0
  12. data/example/monit_unixsock +20 -0
  13. data/example/myapp.rb +1 -0
  14. data/example/ramaze.ru +12 -0
  15. data/example/thin.god +80 -0
  16. data/example/thin_solaris_smf.erb +36 -0
  17. data/example/thin_solaris_smf.readme.txt +150 -0
  18. data/example/vlad.rake +64 -0
  19. data/ext/thin_parser/common.rl +55 -0
  20. data/ext/thin_parser/ext_help.h +14 -0
  21. data/ext/thin_parser/extconf.rb +6 -0
  22. data/ext/thin_parser/parser.c +452 -0
  23. data/ext/thin_parser/parser.h +49 -0
  24. data/ext/thin_parser/parser.rl +157 -0
  25. data/ext/thin_parser/thin.c +433 -0
  26. data/lib/rack/adapter/loader.rb +79 -0
  27. data/lib/rack/adapter/rails.rb +173 -0
  28. data/lib/rack/handler/thin.rb +18 -0
  29. data/lib/thin.rb +50 -0
  30. data/lib/thin/backends/base.rb +141 -0
  31. data/lib/thin/backends/swiftiply_client.rb +56 -0
  32. data/lib/thin/backends/tcp_server.rb +29 -0
  33. data/lib/thin/backends/unix_server.rb +51 -0
  34. data/lib/thin/command.rb +52 -0
  35. data/lib/thin/connection.rb +186 -0
  36. data/lib/thin/controllers/cluster.rb +127 -0
  37. data/lib/thin/controllers/controller.rb +183 -0
  38. data/lib/thin/controllers/service.rb +75 -0
  39. data/lib/thin/controllers/service.sh.erb +39 -0
  40. data/lib/thin/daemonizing.rb +163 -0
  41. data/lib/thin/headers.rb +39 -0
  42. data/lib/thin/logging.rb +54 -0
  43. data/lib/thin/request.rb +147 -0
  44. data/lib/thin/response.rb +99 -0
  45. data/lib/thin/runner.rb +208 -0
  46. data/lib/thin/server.rb +241 -0
  47. data/lib/thin/stats.html.erb +216 -0
  48. data/lib/thin/stats.rb +52 -0
  49. data/lib/thin/statuses.rb +43 -0
  50. data/lib/thin/version.rb +32 -0
  51. data/spec/backends/swiftiply_client_spec.rb +66 -0
  52. data/spec/backends/tcp_server_spec.rb +33 -0
  53. data/spec/backends/unix_server_spec.rb +37 -0
  54. data/spec/command_spec.rb +20 -0
  55. data/spec/configs/cluster.yml +9 -0
  56. data/spec/configs/single.yml +9 -0
  57. data/spec/connection_spec.rb +105 -0
  58. data/spec/controllers/cluster_spec.rb +212 -0
  59. data/spec/controllers/controller_spec.rb +129 -0
  60. data/spec/controllers/service_spec.rb +50 -0
  61. data/spec/daemonizing_spec.rb +192 -0
  62. data/spec/headers_spec.rb +40 -0
  63. data/spec/logging_spec.rb +46 -0
  64. data/spec/perf/request_perf_spec.rb +50 -0
  65. data/spec/perf/response_perf_spec.rb +19 -0
  66. data/spec/perf/server_perf_spec.rb +39 -0
  67. data/spec/rack/loader_spec.rb +29 -0
  68. data/spec/rack/rails_adapter_spec.rb +106 -0
  69. data/spec/rails_app/app/controllers/application.rb +10 -0
  70. data/spec/rails_app/app/controllers/simple_controller.rb +19 -0
  71. data/spec/rails_app/app/helpers/application_helper.rb +3 -0
  72. data/spec/rails_app/app/views/simple/index.html.erb +15 -0
  73. data/spec/rails_app/config/boot.rb +109 -0
  74. data/spec/rails_app/config/environment.rb +64 -0
  75. data/spec/rails_app/config/environments/development.rb +18 -0
  76. data/spec/rails_app/config/environments/production.rb +19 -0
  77. data/spec/rails_app/config/environments/test.rb +22 -0
  78. data/spec/rails_app/config/initializers/inflections.rb +10 -0
  79. data/spec/rails_app/config/initializers/mime_types.rb +5 -0
  80. data/spec/rails_app/config/routes.rb +35 -0
  81. data/spec/rails_app/public/404.html +30 -0
  82. data/spec/rails_app/public/422.html +30 -0
  83. data/spec/rails_app/public/500.html +30 -0
  84. data/spec/rails_app/public/dispatch.cgi +10 -0
  85. data/spec/rails_app/public/dispatch.fcgi +24 -0
  86. data/spec/rails_app/public/dispatch.rb +10 -0
  87. data/spec/rails_app/public/favicon.ico +0 -0
  88. data/spec/rails_app/public/images/rails.png +0 -0
  89. data/spec/rails_app/public/index.html +277 -0
  90. data/spec/rails_app/public/javascripts/application.js +2 -0
  91. data/spec/rails_app/public/javascripts/controls.js +963 -0
  92. data/spec/rails_app/public/javascripts/dragdrop.js +972 -0
  93. data/spec/rails_app/public/javascripts/effects.js +1120 -0
  94. data/spec/rails_app/public/javascripts/prototype.js +4225 -0
  95. data/spec/rails_app/public/robots.txt +5 -0
  96. data/spec/rails_app/script/about +3 -0
  97. data/spec/rails_app/script/console +3 -0
  98. data/spec/rails_app/script/destroy +3 -0
  99. data/spec/rails_app/script/generate +3 -0
  100. data/spec/rails_app/script/performance/benchmarker +3 -0
  101. data/spec/rails_app/script/performance/profiler +3 -0
  102. data/spec/rails_app/script/performance/request +3 -0
  103. data/spec/rails_app/script/plugin +3 -0
  104. data/spec/rails_app/script/process/inspector +3 -0
  105. data/spec/rails_app/script/process/reaper +3 -0
  106. data/spec/rails_app/script/process/spawner +3 -0
  107. data/spec/rails_app/script/runner +3 -0
  108. data/spec/rails_app/script/server +3 -0
  109. data/spec/request/mongrel_spec.rb +39 -0
  110. data/spec/request/parser_spec.rb +215 -0
  111. data/spec/request/persistent_spec.rb +35 -0
  112. data/spec/request/processing_spec.rb +45 -0
  113. data/spec/response_spec.rb +83 -0
  114. data/spec/runner_spec.rb +167 -0
  115. data/spec/server/builder_spec.rb +44 -0
  116. data/spec/server/pipelining_spec.rb +109 -0
  117. data/spec/server/robustness_spec.rb +34 -0
  118. data/spec/server/stopping_spec.rb +45 -0
  119. data/spec/server/swiftiply.yml +6 -0
  120. data/spec/server/swiftiply_spec.rb +32 -0
  121. data/spec/server/tcp_spec.rb +57 -0
  122. data/spec/server/threaded_spec.rb +27 -0
  123. data/spec/server/unix_socket_spec.rb +26 -0
  124. data/spec/server_spec.rb +96 -0
  125. data/spec/spec_helper.rb +219 -0
  126. data/tasks/announce.rake +22 -0
  127. data/tasks/deploy.rake +16 -0
  128. data/tasks/email.erb +30 -0
  129. data/tasks/ext.rake +42 -0
  130. data/tasks/gem.rake +108 -0
  131. data/tasks/rdoc.rake +25 -0
  132. data/tasks/site.rake +15 -0
  133. data/tasks/spec.rake +48 -0
  134. data/tasks/stats.rake +28 -0
  135. metadata +248 -0
@@ -0,0 +1,56 @@
1
+ module Thin
2
+ module Backends
3
+ # Backend to act as a Swiftiply client (http://swiftiply.swiftcore.org).
4
+ class SwiftiplyClient < Base
5
+ attr_accessor :key
6
+
7
+ attr_accessor :host, :port
8
+
9
+ def initialize(host, port, options={})
10
+ @host = host
11
+ @port = port.to_i
12
+ @key = options[:swiftiply].to_s
13
+ super()
14
+ end
15
+
16
+ # Connect the server
17
+ def connect
18
+ EventMachine.connect(@host, @port, SwiftiplyConnection, &method(:initialize_connection))
19
+ end
20
+
21
+ # Stops the server
22
+ def disconnect
23
+ EventMachine.stop
24
+ end
25
+
26
+ def to_s
27
+ "#{@host}:#{@port} swiftiply"
28
+ end
29
+ end
30
+ end
31
+
32
+ class SwiftiplyConnection < Connection
33
+ def connection_completed
34
+ send_data swiftiply_handshake(@backend.key)
35
+ end
36
+
37
+ def persistent?
38
+ true
39
+ end
40
+
41
+ def unbind
42
+ super
43
+ EventMachine.add_timer(rand(2)) { reconnect(@backend.host, @backend.port) } if @backend.running?
44
+ end
45
+
46
+ protected
47
+ def swiftiply_handshake(key)
48
+ 'swiftclient' << host_ip.collect { |x| sprintf('%02x', x.to_i)}.join << sprintf('%04x', @backend.port) << sprintf('%02x', key.length) << key
49
+ end
50
+
51
+ # For some reason Swiftiply request the current host
52
+ def host_ip
53
+ Socket.gethostbyname(@backend.host)[3].unpack('CCCC') rescue [0,0,0,0]
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,29 @@
1
+ module Thin
2
+ module Backends
3
+ # Backend to act as a TCP socket server.
4
+ class TcpServer < Base
5
+ # Address and port on which the server is listening for connections.
6
+ attr_accessor :host, :port
7
+
8
+ def initialize(host, port)
9
+ @host = host
10
+ @port = port
11
+ super()
12
+ end
13
+
14
+ # Connect the server
15
+ def connect
16
+ @signature = EventMachine.start_server(@host, @port, Connection, &method(:initialize_connection))
17
+ end
18
+
19
+ # Stops the server
20
+ def disconnect
21
+ EventMachine.stop_server(@signature)
22
+ end
23
+
24
+ def to_s
25
+ "#{@host}:#{@port}"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,51 @@
1
+ module Thin
2
+ module Backends
3
+ # Backend to act as a UNIX domain socket server.
4
+ class UnixServer < Base
5
+ # UNIX domain socket on which the server is listening for connections.
6
+ attr_accessor :socket
7
+
8
+ def initialize(socket)
9
+ raise PlatformNotSupported, 'UNIX domain sockets not available on Windows' if Thin.win?
10
+ @socket = socket
11
+ super()
12
+ end
13
+
14
+ # Connect the server
15
+ def connect
16
+ at_exit { remove_socket_file } # In case it crashes
17
+ EventMachine.start_unix_domain_server(@socket, UnixConnection, &method(:initialize_connection))
18
+ # HACK EventMachine.start_unix_domain_server doesn't return the connection signature
19
+ # so we have to go in the internal stuff to find it.
20
+ @signature = EventMachine.instance_eval{@acceptors.keys.first}
21
+ end
22
+
23
+ # Stops the server
24
+ def disconnect
25
+ EventMachine.stop_server(@signature)
26
+ end
27
+
28
+ # Free up resources used by the backend.
29
+ def close
30
+ remove_socket_file
31
+ end
32
+
33
+ def to_s
34
+ @socket
35
+ end
36
+
37
+ protected
38
+ def remove_socket_file
39
+ File.delete(@socket) if @socket && File.exist?(@socket)
40
+ end
41
+ end
42
+ end
43
+
44
+ # Connection through a UNIX domain socket.
45
+ class UnixConnection < Connection
46
+ protected
47
+ def socket_address
48
+ '127.0.0.1' # Unix domain sockets can only be local
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,52 @@
1
+ require 'open3'
2
+
3
+ module Thin
4
+ # Run a command through the +thin+ command-line script.
5
+ class Command
6
+ include Logging
7
+
8
+ class << self
9
+ # Path to the +thin+ script used to control the servers.
10
+ # Leave this to default to use the one in the path.
11
+ attr_accessor :script
12
+ end
13
+
14
+ def initialize(name, options={})
15
+ @name = name
16
+ @options = options
17
+ end
18
+
19
+ def self.run(*args)
20
+ new(*args).run
21
+ end
22
+
23
+ # Send the command to the +thin+ script
24
+ def run
25
+ shell_cmd = shellify
26
+ trace shell_cmd
27
+ trap('INT') {} # Ignore INT signal to pass CTRL+C to subprocess
28
+ Open3.popen3(shell_cmd) do |stdin, stdout, stderr|
29
+ log stdout.gets until stdout.eof?
30
+ log stderr.gets until stderr.eof?
31
+ end
32
+ end
33
+
34
+ # Turn into a runnable shell command
35
+ def shellify
36
+ shellified_options = @options.inject([]) do |args, (name, value)|
37
+ case value
38
+ when NilClass,
39
+ TrueClass then args << "--#{name}"
40
+ when FalseClass
41
+ when Array then value.each { |v| args << "--#{name}=#{v.inspect}" }
42
+ else args << "--#{name.to_s.tr('_', '-')}=#{value.inspect}"
43
+ end
44
+ args
45
+ end
46
+
47
+ raise ArgumentError, "Path to thin script can't be found, set Command.script" unless self.class.script
48
+
49
+ "#{self.class.script} #{@name} #{shellified_options.compact.join(' ')}"
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,186 @@
1
+ require 'socket'
2
+
3
+ module Thin
4
+ # Connection between the server and client.
5
+ # This class is instanciated by EventMachine on each new connection
6
+ # that is opened.
7
+ class Connection < EventMachine::Connection
8
+ CONTENT_LENGTH = 'Content-Length'.freeze
9
+ TRANSFER_ENCODING = 'Transfer-Encoding'.freeze
10
+ CHUNKED_REGEXP = /\bchunked\b/i.freeze
11
+
12
+ include Logging
13
+
14
+ # Rack application (adapter) served by this connection.
15
+ attr_accessor :app
16
+
17
+ # Backend to the server
18
+ attr_accessor :backend
19
+
20
+ # Current request served by the connection
21
+ attr_accessor :request
22
+
23
+ # Next response sent through the connection
24
+ attr_accessor :response
25
+
26
+ # Calling the application in a threaded allowing
27
+ # concurrent processing of requests.
28
+ attr_writer :threaded
29
+
30
+ # Get the connection ready to process a request.
31
+ def post_init
32
+ @request = Request.new
33
+ @response = Response.new
34
+ end
35
+
36
+ # Called when data is received from the client.
37
+ def receive_data(data)
38
+ trace { data }
39
+ process if @request.parse(data)
40
+ rescue InvalidRequest => e
41
+ log "!! Invalid request"
42
+ log_error e
43
+ close_connection
44
+ end
45
+
46
+ # Called when all data was received and the request
47
+ # is ready to be processed.
48
+ def process
49
+ if threaded?
50
+ @request.threaded = true
51
+ EventMachine.defer(method(:pre_process), method(:post_process))
52
+ else
53
+ @request.threaded = false
54
+ post_process(pre_process)
55
+ end
56
+ end
57
+
58
+ def pre_process
59
+ # Add client info to the request env
60
+ @request.remote_address = remote_address
61
+
62
+ # Process the request calling the Rack adapter
63
+ @app.call(@request.env)
64
+ rescue Exception
65
+ handle_error
66
+ terminate_request
67
+ nil # Signal to post_process that the request could not be processed
68
+ end
69
+
70
+ def post_process(result)
71
+ return unless result
72
+
73
+ # Set the Content-Length header if possible
74
+ set_content_length(result) if need_content_length?(result)
75
+
76
+ @response.status, @response.headers, @response.body = result
77
+
78
+ log "!! Rack application returned nil body. Probably you wanted it to be an empty string?" if @response.body.nil?
79
+ # Make the response persistent if requested by the client
80
+ @response.persistent! if @request.persistent?
81
+
82
+ # Send the response
83
+ @response.each do |chunk|
84
+ trace { chunk }
85
+ send_data chunk
86
+ end
87
+
88
+ # If no more request on that same connection, we close it.
89
+ close_connection_after_writing unless persistent?
90
+
91
+ rescue Exception
92
+ handle_error
93
+ ensure
94
+ terminate_request
95
+ end
96
+
97
+ # Logs catched exception and closes the connection.
98
+ def handle_error
99
+ log "!! Unexpected error while processing request: #{$!.message}"
100
+ log_error
101
+ close_connection rescue nil
102
+ end
103
+
104
+ # Does request and response cleanup (closes open IO streams and
105
+ # deletes created temporary files).
106
+ # Re-initializes response and request if client supports persistent
107
+ # connection.
108
+ def terminate_request
109
+ @request.close rescue nil
110
+ @response.close rescue nil
111
+
112
+ # Prepare the connection for another request if the client
113
+ # supports HTTP pipelining (persistent connection).
114
+ post_init if persistent?
115
+ end
116
+
117
+ # Called when the connection is unbinded from the socket
118
+ # and can no longer be used to process requests.
119
+ def unbind
120
+ @backend.connection_finished(self)
121
+ end
122
+
123
+ # Allows this connection to be persistent.
124
+ def can_persist!
125
+ @can_persist = true
126
+ end
127
+
128
+ # Return +true+ if this connection is allowed to stay open and be persistent.
129
+ def can_persist?
130
+ @can_persist
131
+ end
132
+
133
+ # Return +true+ if the connection must be left open
134
+ # and ready to be reused for another request.
135
+ def persistent?
136
+ @can_persist && @response.persistent?
137
+ end
138
+
139
+ # +true+ if <tt>app.call</tt> will be called inside a thread.
140
+ # You can set all requests as threaded setting <tt>Connection#threaded=true</tt>
141
+ # or on a per-request case returning +true+ in <tt>app.deferred?</tt>.
142
+ def threaded?
143
+ @threaded || (@app.respond_to?(:deferred?) && @app.deferred?(@request.env))
144
+ end
145
+
146
+ # IP Address of the remote client.
147
+ def remote_address
148
+ @request.forwarded_for || socket_address
149
+ rescue Exception
150
+ log_error
151
+ nil
152
+ end
153
+
154
+ protected
155
+
156
+ # Returns IP address of peer as a string.
157
+ def socket_address
158
+ Socket.unpack_sockaddr_in(get_peername)[1]
159
+ end
160
+
161
+ private
162
+ def need_content_length?(result)
163
+ status, headers, body = result
164
+ return false if headers.has_key?(CONTENT_LENGTH)
165
+ return false if (100..199).include?(status) || status == 204 || status == 304
166
+ return false if headers.has_key?(TRANSFER_ENCODING) && headers[TRANSFER_ENCODING] =~ CHUNKED_REGEXP
167
+ return false unless body.kind_of?(String) || body.kind_of?(Array)
168
+ true
169
+ end
170
+
171
+ def set_content_length(result)
172
+ headers, body = result[1..2]
173
+ case body
174
+ when String
175
+ # See http://redmine.ruby-lang.org/issues/show/203
176
+ headers[CONTENT_LENGTH] = (body.respond_to?(:bytesize) ? body.bytesize : body.size).to_s
177
+ when Array
178
+ bytes = 0
179
+ body.each do |p|
180
+ bytes += p.respond_to?(:bytesize) ? p.bytesize : p.size
181
+ end
182
+ headers[CONTENT_LENGTH] = bytes.to_s
183
+ end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,127 @@
1
+ module Thin
2
+ module Controllers
3
+ # Control a set of servers.
4
+ # * Generate start and stop commands and run them.
5
+ # * Inject the port or socket number in the pid and log filenames.
6
+ # Servers are started throught the +thin+ command-line script.
7
+ class Cluster < Controller
8
+ # Cluster only options that should not be passed in the command sent
9
+ # to the indiviual servers.
10
+ CLUSTER_OPTIONS = [:servers, :only]
11
+
12
+ # Create a new cluster of servers launched using +options+.
13
+ def initialize(options)
14
+ super
15
+ # Cluster can only contain daemonized servers
16
+ @options.merge!(:daemonize => true)
17
+ end
18
+
19
+ def first_port; @options[:port] end
20
+ def address; @options[:address] end
21
+ def socket; @options[:socket] end
22
+ def pid_file; @options[:pid] end
23
+ def log_file; @options[:log] end
24
+ def size; @options[:servers] end
25
+ def only; @options[:only] end
26
+
27
+ def swiftiply?
28
+ @options.has_key?(:swiftiply)
29
+ end
30
+
31
+ # Start the servers
32
+ def start
33
+ with_each_server { |n| start_server n }
34
+ end
35
+
36
+ # Start a single server
37
+ def start_server(number)
38
+ log "Starting server on #{server_id(number)} ... "
39
+
40
+ run :start, number
41
+ end
42
+
43
+ # Stop the servers
44
+ def stop
45
+ with_each_server { |n| stop_server n }
46
+ end
47
+
48
+ # Stop a single server
49
+ def stop_server(number)
50
+ log "Stopping server on #{server_id(number)} ... "
51
+
52
+ run :stop, number
53
+ end
54
+
55
+ # Stop and start the servers.
56
+ def restart
57
+ stop
58
+ sleep 0.1 # Let's breath a bit shall we ?
59
+ start
60
+ end
61
+
62
+ def server_id(number)
63
+ if socket
64
+ socket_for(number)
65
+ elsif swiftiply?
66
+ [address, first_port, number].join(':')
67
+ else
68
+ [address, number].join(':')
69
+ end
70
+ end
71
+
72
+ def log_file_for(number)
73
+ include_server_number log_file, number
74
+ end
75
+
76
+ def pid_file_for(number)
77
+ include_server_number pid_file, number
78
+ end
79
+
80
+ def socket_for(number)
81
+ include_server_number socket, number
82
+ end
83
+
84
+ def pid_for(number)
85
+ File.read(pid_file_for(number)).chomp.to_i
86
+ end
87
+
88
+ private
89
+ # Send the command to the +thin+ script
90
+ def run(cmd, number)
91
+ cmd_options = @options.reject { |option, value| CLUSTER_OPTIONS.include?(option) }
92
+ cmd_options.merge!(:pid => pid_file_for(number), :log => log_file_for(number))
93
+ if socket
94
+ cmd_options.merge!(:socket => socket_for(number))
95
+ elsif swiftiply?
96
+ cmd_options.merge!(:port => first_port)
97
+ else
98
+ cmd_options.merge!(:port => number)
99
+ end
100
+ Command.run(cmd, cmd_options)
101
+ end
102
+
103
+ def with_each_server
104
+ if only
105
+ if only < 80
106
+ # interpret +only+ as a sequence number
107
+ yield(first_port + only)
108
+ else
109
+ # interpret +only+ as an absolute port number
110
+ yield only
111
+ end
112
+ elsif socket || swiftiply?
113
+ size.times { |n| yield n }
114
+ else
115
+ size.times { |n| yield first_port + n }
116
+ end
117
+ end
118
+
119
+ # Add the server port or number in the filename
120
+ # so each instance get its own file
121
+ def include_server_number(path, number)
122
+ ext = File.extname(path)
123
+ path.gsub(/#{ext}$/, ".#{number}#{ext}")
124
+ end
125
+ end
126
+ end
127
+ end