thin 0.7.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of thin might be problematic. Click here for more details.

Files changed (57) hide show
  1. data/CHANGELOG +15 -0
  2. data/README +10 -4
  3. data/benchmark/abc +0 -0
  4. data/benchmark/runner +0 -0
  5. data/bin/thin +0 -0
  6. data/example/thin_solaris_smf.erb +36 -0
  7. data/example/thin_solaris_smf.readme.txt +150 -0
  8. data/ext/thin_parser/common.rl +3 -2
  9. data/ext/thin_parser/parser.c +15 -38
  10. data/lib/rack/adapter/loader.rb +69 -0
  11. data/lib/rack/adapter/rails.rb +17 -8
  12. data/lib/thin.rb +1 -0
  13. data/lib/thin/backends/base.rb +17 -0
  14. data/lib/thin/backends/swiftiply_client.rb +3 -2
  15. data/lib/thin/backends/tcp_server.rb +1 -1
  16. data/lib/thin/connection.rb +38 -6
  17. data/lib/thin/controllers/controller.rb +43 -17
  18. data/lib/thin/logging.rb +1 -1
  19. data/lib/thin/request.rb +8 -1
  20. data/lib/thin/runner.rb +29 -7
  21. data/lib/thin/server.rb +58 -28
  22. data/lib/thin/version.rb +3 -3
  23. data/lib/thin_backend.bundle +0 -0
  24. data/lib/thin_parser.bundle +0 -0
  25. data/spec/connection_spec.rb +7 -0
  26. data/spec/controllers/controller_spec.rb +9 -1
  27. data/spec/rack/loader_spec.rb +29 -0
  28. data/spec/{rack_rails_spec.rb → rack/rails_adapter_spec.rb} +15 -3
  29. data/spec/rails_app/public/dispatch.cgi +0 -0
  30. data/spec/rails_app/public/dispatch.fcgi +0 -0
  31. data/spec/rails_app/public/dispatch.rb +0 -0
  32. data/spec/rails_app/script/about +0 -0
  33. data/spec/rails_app/script/console +0 -0
  34. data/spec/rails_app/script/destroy +0 -0
  35. data/spec/rails_app/script/generate +0 -0
  36. data/spec/rails_app/script/performance/benchmarker +0 -0
  37. data/spec/rails_app/script/performance/profiler +0 -0
  38. data/spec/rails_app/script/performance/request +0 -0
  39. data/spec/rails_app/script/plugin +0 -0
  40. data/spec/rails_app/script/process/inspector +0 -0
  41. data/spec/rails_app/script/process/reaper +0 -0
  42. data/spec/rails_app/script/process/spawner +0 -0
  43. data/spec/rails_app/script/runner +0 -0
  44. data/spec/rails_app/script/server +0 -0
  45. data/spec/request/parser_spec.rb +22 -0
  46. data/spec/request/processing_spec.rb +9 -10
  47. data/spec/runner_spec.rb +14 -1
  48. data/spec/server/builder_spec.rb +3 -2
  49. data/spec/server/robustness_spec.rb +34 -0
  50. data/spec/server/swiftiply_spec.rb +1 -1
  51. data/spec/server/threaded_spec.rb +27 -0
  52. data/spec/server_spec.rb +69 -0
  53. data/spec/spec_helper.rb +10 -5
  54. data/tasks/gem.rake +2 -2
  55. data/tasks/spec.rake +24 -16
  56. metadata +13 -5
  57. data/doc/benchmarks.txt +0 -86
@@ -3,13 +3,18 @@ require 'cgi'
3
3
  # Adapter to run a Rails app with any supported Rack handler.
4
4
  # By default it will try to load the Rails application in the
5
5
  # current directory in the development environment.
6
+ #
6
7
  # Options:
7
8
  # root: Root directory of the Rails app
8
- # env: Rails environment to run in (development, production or test)
9
+ # environment: Rails environment to run in (development [default], production or test)
10
+ # prefix: Set the relative URL root.
11
+ #
9
12
  # Based on http://fuzed.rubyforge.org/ Rails adapter
10
13
  module Rack
11
14
  module Adapter
12
15
  class Rails
16
+ FILE_METHODS = %w(GET HEAD).freeze
17
+
13
18
  def initialize(options={})
14
19
  @root = options[:root] || Dir.pwd
15
20
  @env = options[:environment] || 'development'
@@ -53,16 +58,20 @@ module Rack
53
58
 
54
59
  def call(env)
55
60
  path = env['PATH_INFO'].chomp('/')
61
+ method = env['REQUEST_METHOD']
56
62
  cached_path = (path.empty? ? 'index' : path) + ActionController::Base.page_cache_extension
57
63
 
58
- if file_exist?(path) # Serve the file if it's there
59
- serve_file(env)
60
- elsif file_exist?(cached_path) # Serve the page cache if it's there
61
- env['PATH_INFO'] = cached_path
62
- serve_file(env)
63
- else # No static file, let Rails handle it
64
- serve_rails(env)
64
+ if FILE_METHODS.include?(method)
65
+ if file_exist?(path) # Serve the file if it's there
66
+ return serve_file(env)
67
+ elsif file_exist?(cached_path) # Serve the page cache if it's there
68
+ env['PATH_INFO'] = cached_path
69
+ return serve_file(env)
70
+ end
65
71
  end
72
+
73
+ # No static file, let Rails handle it
74
+ serve_rails(env)
66
75
  end
67
76
 
68
77
  protected
data/lib/thin.rb CHANGED
@@ -37,6 +37,7 @@ module Thin
37
37
  end
38
38
 
39
39
  require 'rack'
40
+ require 'rack/adapter/loader'
40
41
 
41
42
  module Rack
42
43
  module Handler
@@ -4,6 +4,12 @@ module Thin
4
4
  # * connection/disconnection to the server
5
5
  # * initialization of the connections
6
6
  # * manitoring of the active connections.
7
+ #
8
+ # == Implementing your own backend
9
+ # You can create your own minimal backend by inheriting this class and
10
+ # defining the +connect+ and +disconnect+ method.
11
+ # If your backend is not based on EventMachine you also need to redefine
12
+ # the +start+, +stop+, <tt>stop!</tt> and +config+ methods.
7
13
  class Base
8
14
  # Server serving the connections throught the backend
9
15
  attr_accessor :server
@@ -17,6 +23,10 @@ module Thin
17
23
  # Maximum number of connections that can be persistent
18
24
  attr_accessor :maximum_persistent_connections
19
25
 
26
+ # Allow using threads in the backend.
27
+ attr_writer :threaded
28
+ def threaded?; @threaded end
29
+
20
30
  # Number of persistent connections currently opened
21
31
  attr_accessor :persistent_connection_count
22
32
 
@@ -28,6 +38,7 @@ module Thin
28
38
  @maximum_persistent_connections = Server::DEFAULT_MAXIMUM_PERSISTENT_CONNECTIONS
29
39
  end
30
40
 
41
+ # Start the backend and connect it.
31
42
  def start
32
43
  @stopping = false
33
44
 
@@ -37,6 +48,7 @@ module Thin
37
48
  end
38
49
  end
39
50
 
51
+ # Stop of the backend from accepting new connections.
40
52
  def stop
41
53
  @running = false
42
54
  @stopping = true
@@ -46,6 +58,7 @@ module Thin
46
58
  stop! if @connections.empty?
47
59
  end
48
60
 
61
+ # Force stop of the backend NOW, too bad for the current connections.
49
62
  def stop!
50
63
  @running = false
51
64
  @stopping = false
@@ -55,6 +68,8 @@ module Thin
55
68
  close
56
69
  end
57
70
 
71
+ # Configure the backend. This method will be called before droping superuser privileges,
72
+ # so you can do crazy stuff that require godlike powers here.
58
73
  def config
59
74
  # See http://rubyeventmachine.com/pub/rdoc/files/EPOLL.html
60
75
  EventMachine.epoll
@@ -69,6 +84,7 @@ module Thin
69
84
  def close
70
85
  end
71
86
 
87
+ # Returns +true+ if the backend is connected and running.
72
88
  def running?
73
89
  @running
74
90
  end
@@ -98,6 +114,7 @@ module Thin
98
114
  connection.backend = self
99
115
  connection.app = @server.app
100
116
  connection.comm_inactivity_timeout = @timeout
117
+ connection.threaded = @threaded
101
118
 
102
119
  # We control the number of persistent connections by keeping
103
120
  # a count of the total one allowed yet.
@@ -1,14 +1,15 @@
1
1
  module Thin
2
2
  module Backends
3
+ # Backend to act as a Swiftiply client (http://swiftiply.swiftcore.org).
3
4
  class SwiftiplyClient < Base
4
5
  attr_accessor :key
5
6
 
6
7
  attr_accessor :host, :port
7
8
 
8
- def initialize(host, port, key=nil)
9
+ def initialize(host, port, options={})
9
10
  @host = host
10
11
  @port = port.to_i
11
- @key = key || ''
12
+ @key = options[:swiftiply].to_s
12
13
  super()
13
14
  end
14
15
 
@@ -1,6 +1,6 @@
1
1
  module Thin
2
2
  module Backends
3
- # Connectior to act as a TCP socket server.
3
+ # Backend to act as a TCP socket server.
4
4
  class TcpServer < Base
5
5
  # Address and port on which the server is listening for connections.
6
6
  attr_accessor :host, :port
@@ -7,7 +7,7 @@ module Thin
7
7
  class Connection < EventMachine::Connection
8
8
  include Logging
9
9
 
10
- # Rack application served by this connection.
10
+ # Rack application (adapter) served by this connection.
11
11
  attr_accessor :app
12
12
 
13
13
  # Backend to the server
@@ -16,13 +16,19 @@ module Thin
16
16
  # Current request served by the connection
17
17
  attr_accessor :request
18
18
 
19
- # Next response sent through connection
19
+ # Next response sent through the connection
20
20
  attr_accessor :response
21
21
 
22
+ # Calling the application in a threaded allowing
23
+ # concurrent processing of requests.
24
+ attr_accessor :threaded
25
+
22
26
  # Get the connection ready to process a request.
23
27
  def post_init
24
28
  @request = Request.new
25
29
  @response = Response.new
30
+
31
+ @request.threaded = threaded
26
32
  end
27
33
 
28
34
  # Called when data is received from the client.
@@ -36,13 +42,31 @@ module Thin
36
42
  end
37
43
 
38
44
  # Called when all data was received and the request
39
- # is ready to being processed.
45
+ # is ready to be processed.
40
46
  def process
47
+ if @threaded
48
+ EventMachine.defer(method(:pre_process), method(:post_process))
49
+ else
50
+ post_process(pre_process)
51
+ end
52
+ end
53
+
54
+ def pre_process
41
55
  # Add client info to the request env
42
56
  @request.remote_address = remote_address
43
57
 
44
- # Process the request
45
- @response.status, @response.headers, @response.body = @app.call(@request.env)
58
+ # Process the request calling the Rack adapter
59
+ @app.call(@request.env)
60
+ rescue
61
+ handle_error
62
+ terminate_request
63
+ nil # Signal to post_process that the request could not be processed
64
+ end
65
+
66
+ def post_process(result)
67
+ return unless result
68
+
69
+ @response.status, @response.headers, @response.body = result
46
70
 
47
71
  # Make the response persistent if requested by the client
48
72
  @response.persistent! if @request.persistent?
@@ -57,10 +81,18 @@ module Thin
57
81
  close_connection_after_writing unless persistent?
58
82
 
59
83
  rescue
84
+ handle_error
85
+ ensure
86
+ terminate_request
87
+ end
88
+
89
+ def handle_error
60
90
  log "!! Unexpected error while processing request: #{$!.message}"
61
91
  log_error
62
92
  close_connection rescue nil
63
- ensure
93
+ end
94
+
95
+ def terminate_request
64
96
  @request.close rescue nil
65
97
  @response.close rescue nil
66
98
 
@@ -1,15 +1,23 @@
1
1
  require 'yaml'
2
2
 
3
3
  module Thin
4
- module Controllers
5
- # Raised when a mandatory option is missing to run a command.
6
- class OptionRequired < RuntimeError
7
- def initialize(option)
8
- super("#{option} option required")
9
- end
4
+ # Error raised that will abort the process and print not backtrace.
5
+ class RunnerError < RuntimeError; end
6
+
7
+ # Raised when a mandatory option is missing to run a command.
8
+ class OptionRequired < RunnerError
9
+ def initialize(option)
10
+ super("#{option} option required")
10
11
  end
12
+ end
11
13
 
12
- # Controls a Thin server.
14
+ # Raised when an option is not valid.
15
+ class InvalidOption < RunnerError; end
16
+
17
+ # Build and control Thin servers.
18
+ # Hey Controller pattern is not only for web apps yo!
19
+ module Controllers
20
+ # Controls one Thin server.
13
21
  # Allow to start, stop, restart and configure a single thin server.
14
22
  class Controller
15
23
  include Logging
@@ -27,35 +35,38 @@ module Thin
27
35
  end
28
36
 
29
37
  def start
38
+ # Select proper backend
30
39
  server = case
40
+ when @options.has_key?(:backend)
41
+ Server.new(@options[:address], @options[:port], :backend => eval(@options[:backend], TOPLEVEL_BINDING))
31
42
  when @options.has_key?(:socket)
32
43
  Server.new(@options[:socket])
33
- when @options.has_key?(:swiftiply)
34
- Server.new(Backends::SwiftiplyClient.new(@options[:address], @options[:port], @options[:swiftiply]))
35
44
  else
36
45
  Server.new(@options[:address], @options[:port])
37
46
  end
38
-
47
+
48
+ # Set options
39
49
  server.pid_file = @options[:pid]
40
50
  server.log_file = @options[:log]
41
51
  server.timeout = @options[:timeout]
42
52
  server.maximum_connections = @options[:max_conns]
43
53
  server.maximum_persistent_connections = @options[:max_persistent_conns]
54
+ server.threaded = @options[:threaded]
44
55
 
56
+ # Detach the process, after this line the current process returns
45
57
  server.daemonize if @options[:daemonize]
46
58
 
47
- server.config # Must be called before changing privileges since it might require superuser power.
59
+ # +config+ must be called before changing privileges since it might require superuser power.
60
+ server.config
48
61
 
49
62
  server.change_privilege @options[:user], @options[:group] if @options[:user] && @options[:group]
50
63
 
51
64
  # If a Rack config file is specified we eval it inside a Rack::Builder block to create
52
- # a Rack adapter from it. DHH was hacker of the year a couple years ago so we default
53
- # to Rails adapter.
65
+ # a Rack adapter from it. Or else we guess which adapter to use and load it.
54
66
  if @options[:rackup]
55
- rackup_code = File.read(@options[:rackup])
56
- server.app = eval("Rack::Builder.new {( #{rackup_code}\n )}.to_app", TOPLEVEL_BINDING, @options[:rackup])
67
+ server.app = load_rackup_config
57
68
  else
58
- server.app = Rack::Adapter::Rails.new(@options.merge(:root => @options[:chdir]))
69
+ server.app = load_adapter
59
70
  end
60
71
 
61
72
  # If a prefix is required, wrap in Rack URL mapper
@@ -64,7 +75,8 @@ module Thin
64
75
  # If a stats URL is specified, wrap in Stats adapter
65
76
  server.app = Stats::Adapter.new(server.app, @options[:stats]) if @options[:stats]
66
77
 
67
- # Register restart procedure
78
+ # Register restart procedure which just start another process with same options,
79
+ # so that's why this is done here.
68
80
  server.on_restart { Command.run(:start, @options) }
69
81
 
70
82
  server.start
@@ -141,6 +153,20 @@ module Thin
141
153
  sleep 1 if File.exist?(file) # HACK Give the thread a little time to open the file
142
154
  tail_thread
143
155
  end
156
+
157
+ private
158
+ def load_adapter
159
+ adapter = @options[:adapter] || Rack::Adapter.guess(@options[:chdir])
160
+ log ">> Using #{adapter} adapter"
161
+ Rack::Adapter.for(adapter, @options)
162
+ rescue Rack::AdapterNotFound => e
163
+ raise InvalidOption, e.message
164
+ end
165
+
166
+ def load_rackup_config
167
+ rackup_code = File.read(@options[:rackup])
168
+ eval("Rack::Builder.new {( #{rackup_code}\n )}.to_app", TOPLEVEL_BINDING, @options[:rackup])
169
+ end
144
170
  end
145
171
  end
146
172
  end
data/lib/thin/logging.rb CHANGED
@@ -15,7 +15,7 @@ module Thin
15
15
  def silent?; @silent end
16
16
  end
17
17
 
18
- # Deprecated silencer methods, those are now a module methods
18
+ # Deprecated silencer methods, those are now module methods
19
19
  def silent
20
20
  warn "`#{self.class.name}\#silent` deprecated, use `Thin::Logging.silent?` instead"
21
21
  Logging.silent?
data/lib/thin/request.rb CHANGED
@@ -12,6 +12,7 @@ module Thin
12
12
  # and into a tempfile for reading.
13
13
  MAX_BODY = 1024 * (80 + 32)
14
14
  BODY_TMPFILE = 'thin-body'.freeze
15
+ MAX_HEADER = 1024 * (80 + 32)
15
16
 
16
17
  # Freeze some HTTP header names & values
17
18
  SERVER_SOFTWARE = 'SERVER_SOFTWARE'.freeze
@@ -60,7 +61,7 @@ module Thin
60
61
  RACK_RUN_ONCE => false
61
62
  }
62
63
  end
63
-
64
+
64
65
  # Parse a chunk of data into the request environment
65
66
  # Raises a +InvalidRequest+ if invalid.
66
67
  # Returns +true+ if the parsing is complete.
@@ -69,6 +70,8 @@ module Thin
69
70
  body << data
70
71
  else # Parse more header using the super parser
71
72
  @data << data
73
+ raise InvalidRequest, 'Header longer than allowed' if @data.size > MAX_HEADER
74
+
72
75
  @nparsed = @parser.execute(@env, @data, @nparsed)
73
76
 
74
77
  # Transfert to a tempfile if body is very big
@@ -119,6 +122,10 @@ module Thin
119
122
  @env[FORWARDED_FOR]
120
123
  end
121
124
 
125
+ def threaded=(value)
126
+ @env[RACK_MULTITHREAD] = value
127
+ end
128
+
122
129
  # Close any resource used by the request
123
130
  def close
124
131
  @body.delete if @body.class == Tempfile
data/lib/thin/runner.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'optparse'
2
2
  require 'yaml'
3
3
 
4
- module Thin
4
+ module Thin
5
5
  # CLI runner.
6
6
  # Parse options and send command to the correct Controller.
7
7
  class Runner
@@ -62,14 +62,19 @@ module Thin
62
62
  opts.on("-p", "--port PORT", "use PORT (default: #{@options[:port]})") { |port| @options[:port] = port.to_i }
63
63
  opts.on("-S", "--socket FILE", "bind to unix domain socket") { |file| @options[:socket] = file }
64
64
  opts.on("-y", "--swiftiply [KEY]", "Run using swiftiply") { |key| @options[:swiftiply] = key }
65
- opts.on("-e", "--environment ENV", "Rails environment " +
66
- "(default: #{@options[:environment]})") { |env| @options[:environment] = env }
65
+ opts.on("-A", "--adapter NAME", "Rack adapter to use (default: autodetect)",
66
+ "(#{Rack::ADAPTERS.keys.join(', ')})") { |name| @options[:adapter] = name }
67
+ opts.on("-R", "--rackup FILE", "Load a Rack config file instead of " +
68
+ "Rack adapter") { |file| @options[:rackup] = file }
67
69
  opts.on("-c", "--chdir DIR", "Change to dir before starting") { |dir| @options[:chdir] = File.expand_path(dir) }
68
- opts.on("-r", "--rackup FILE", "Load a Rack config file instead of " +
69
- "Rails adapter") { |file| @options[:rackup] = file }
70
- opts.on( "--prefix PATH", "Mount the app under PATH (start with /)") { |path| @options[:prefix] = path }
71
70
  opts.on( "--stats PATH", "Mount the Stats adapter under PATH") { |path| @options[:stats] = path }
72
71
 
72
+ opts.separator ""
73
+ opts.separator "Adapter options:"
74
+ opts.on("-e", "--environment ENV", "Framework environment " +
75
+ "(default: #{@options[:environment]})") { |env| @options[:environment] = env }
76
+ opts.on( "--prefix PATH", "Mount the app under PATH (start with /)") { |path| @options[:prefix] = path }
77
+
73
78
  unless Thin.win? # Daemonizing not supported on Windows
74
79
  opts.separator ""
75
80
  opts.separator "Daemon options:"
@@ -94,6 +99,7 @@ module Thin
94
99
  opts.separator ""
95
100
  opts.separator "Tuning options:"
96
101
 
102
+ opts.on("-b", "--backend CLASS", "Backend to use, full classname") { |name| @options[:backend] = name }
97
103
  opts.on("-t", "--timeout SEC", "Request or command timeout in sec " +
98
104
  "(default: #{@options[:timeout]})") { |sec| @options[:timeout] = sec.to_i }
99
105
  opts.on( "--max-conns NUM", "Maximum number of connections " +
@@ -102,10 +108,13 @@ module Thin
102
108
  opts.on( "--max-persistent-conns NUM",
103
109
  "Maximum number of persistent connections",
104
110
  "(default: #{@options[:max_persistent_conns]})") { |num| @options[:max_persistent_conns] = num.to_i }
111
+ opts.on( "--threaded", "Call the Rack application in threads " +
112
+ "[experimental]") { @options[:threaded] = true }
105
113
 
106
114
  opts.separator ""
107
115
  opts.separator "Common options:"
108
116
 
117
+ opts.on_tail("-r", "--require FILE", "require the library") { |file| ruby_require file }
109
118
  opts.on_tail("-D", "--debug", "Set debbuging on") { Logging.debug = true }
110
119
  opts.on_tail("-V", "--trace", "Set tracing on (log raw request/response)") { Logging.trace = true }
111
120
  opts.on_tail("-h", "--help", "Show this message") { puts opts; exit }
@@ -153,7 +162,11 @@ module Thin
153
162
  end
154
163
 
155
164
  if controller.respond_to?(@command)
156
- controller.send(@command, *@arguments)
165
+ begin
166
+ controller.send(@command, *@arguments)
167
+ rescue RunnerError => e
168
+ abort e.message
169
+ end
157
170
  else
158
171
  abort "Invalid options for command: #{@command}"
159
172
  end
@@ -175,5 +188,14 @@ module Thin
175
188
  YAML.load_file(file).each { |key, value| @options[key.to_sym] = value }
176
189
  end
177
190
  end
191
+
192
+ def ruby_require(file)
193
+ if File.extname(file) == '.ru'
194
+ warn 'WARNING: Use the -R option to load a Rack config file'
195
+ @options[:rackup] = file
196
+ else
197
+ require file
198
+ end
199
+ end
178
200
  end
179
201
  end