goliath 0.9.2 → 0.9.4

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

Potentially problematic release.


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

Files changed (49) hide show
  1. data/Gemfile +1 -1
  2. data/{HISTORY → HISTORY.md} +26 -12
  3. data/README.md +17 -10
  4. data/examples/api_proxy.rb +28 -0
  5. data/examples/async_aroundware_demo.rb +14 -10
  6. data/examples/auth_and_rate_limit.rb +160 -38
  7. data/examples/config/auth_and_rate_limit.rb +8 -5
  8. data/examples/config/content_stream.rb +5 -9
  9. data/examples/early_abort.rb +37 -0
  10. data/examples/env_use_statements.rb +3 -0
  11. data/examples/favicon.rb +40 -0
  12. data/examples/http_log.rb +2 -1
  13. data/examples/public/favicon.ico +0 -0
  14. data/examples/rack_routes.rb +19 -0
  15. data/examples/rasterize/rasterize.rb +2 -1
  16. data/examples/rasterize/rasterize_and_shorten.rb +10 -5
  17. data/goliath.gemspec +7 -9
  18. data/lib/goliath/api.rb +16 -4
  19. data/lib/goliath/connection.rb +8 -7
  20. data/lib/goliath/deprecated/async_aroundware.rb +133 -0
  21. data/lib/goliath/{synchrony → deprecated}/mongo_receiver.rb +28 -8
  22. data/lib/goliath/deprecated/response_receiver.rb +97 -0
  23. data/lib/goliath/env.rb +5 -0
  24. data/lib/goliath/rack.rb +6 -1
  25. data/lib/goliath/rack/async_middleware.rb +34 -12
  26. data/lib/goliath/rack/barrier_aroundware.rb +228 -0
  27. data/lib/goliath/rack/barrier_aroundware_factory.rb +60 -0
  28. data/lib/goliath/rack/builder.rb +22 -6
  29. data/lib/goliath/rack/heartbeat.rb +8 -5
  30. data/lib/goliath/rack/simple_aroundware.rb +114 -0
  31. data/lib/goliath/rack/simple_aroundware_factory.rb +121 -0
  32. data/lib/goliath/rack/validation/required_param.rb +9 -2
  33. data/lib/goliath/request.rb +7 -0
  34. data/lib/goliath/runner.rb +17 -5
  35. data/lib/goliath/server.rb +11 -3
  36. data/lib/goliath/test_helper.rb +14 -14
  37. data/lib/goliath/version.rb +1 -1
  38. data/spec/integration/early_abort_spec.rb +50 -0
  39. data/spec/integration/keepalive_spec.rb +2 -2
  40. data/spec/integration/pipelining_spec.rb +2 -2
  41. data/spec/integration/rack_routes_spec.rb +25 -0
  42. data/spec/integration/template_spec.rb +2 -0
  43. data/spec/unit/rack/heartbeat_spec.rb +11 -1
  44. data/spec/unit/rack/validation/required_param_spec.rb +10 -0
  45. data/spec/unit/runner_spec.rb +13 -0
  46. data/spec/unit/server_spec.rb +4 -0
  47. metadata +218 -265
  48. data/lib/goliath/rack/async_aroundware.rb +0 -56
  49. data/lib/goliath/synchrony/response_receiver.rb +0 -64
@@ -0,0 +1,60 @@
1
+ module Goliath
2
+ module Rack
3
+ #
4
+ # Include this to enable middleware that can perform pre- and
5
+ # post-processing, orchestrating multiple concurrent requests.
6
+ #
7
+ # For internal reasons, you can't do the following as you would in Rack:
8
+ #
9
+ # def call(env)
10
+ # # ... do pre-processing
11
+ # status, headers, body = @app.call(env)
12
+ # new_body = make_totally_awesome(body) ## !! BROKEN !!
13
+ # [status, headers, new_body]
14
+ # end
15
+ #
16
+ # This class creates an "aroundware" helper to do that kind of
17
+ # processing. Goliath proceeds asynchronously, but will still "unwind" the
18
+ # request by walking up the callback chain. Delegating out to the aroundware
19
+ # also lets you carry state around -- the ban on instance variables no
20
+ # longer applies, as each aroundware is unique per request.
21
+ #
22
+ # The strategy here is similar to that in EM::Multi. Figuring out what goes
23
+ # on there will help you understand this.
24
+ #
25
+ # @see EventMachine::Multi
26
+ # @see Goliath::Rack::SimpleAroundware
27
+ # @see Goliath::Rack::SimpleAroundwareFactory
28
+ # @see Goliath::Rack::BarrierAroundware
29
+ #
30
+ class BarrierAroundwareFactory < Goliath::Rack::SimpleAroundwareFactory
31
+ include Goliath::Rack::Validator
32
+
33
+ # Put aroundware in the middle of the async_callback chain:
34
+ # * save the old callback chain;
35
+ # * have the downstream callback send results to the aroundware (possibly
36
+ # completing it)
37
+ # * set the old callback chain to fire when the aroundware completes
38
+ def hook_into_callback_chain(env, aroundware)
39
+ async_callback = env['async.callback']
40
+
41
+ # The response from the downstream app is accepted by the aroundware...
42
+ downstream_callback = Proc.new do |resp|
43
+ safely(env){ aroundware.accept_response(:downstream_resp, true, resp) }
44
+ end
45
+
46
+ # .. but the upstream chain is only invoked when the aroundware completes
47
+ invoke_upstream_chain = Proc.new do
48
+ new_resp = safely(env){ aroundware.post_process }
49
+ async_callback.call(new_resp)
50
+ end
51
+
52
+ env['async.callback'] = downstream_callback
53
+ aroundware.add_to_pending(:downstream_resp)
54
+ aroundware.callback(&invoke_upstream_chain)
55
+ aroundware.errback(&invoke_upstream_chain)
56
+ end
57
+
58
+ end
59
+ end
60
+ end
@@ -1,11 +1,22 @@
1
1
  require 'http_router'
2
2
 
3
+ class HttpRouter::Route
4
+ attr_accessor :api_class
5
+ end
6
+
3
7
  module Goliath
4
8
  module Rack
5
9
  class Builder < ::Rack::Builder
6
10
  attr_accessor :params
11
+ attr_reader :inner_app
7
12
  include Params::Parser
8
13
 
14
+ alias_method :original_run, :run
15
+ def run(app)
16
+ @inner_app = app
17
+ original_run(app)
18
+ end
19
+
9
20
  # Builds the rack middleware chain for the given API
10
21
  #
11
22
  # @param klass [Class] The API class to build the middlewares for
@@ -18,15 +29,21 @@ module Goliath
18
29
  end
19
30
  if klass.maps?
20
31
  klass.maps.each do |path, route_klass, opts, blk|
21
- blk ||= Proc.new {
22
- run Builder.build(route_klass, route_klass.new)
23
- }
24
- klass.router.add(path, opts.dup).to {|env|
32
+ route = klass.router.add(path, opts.dup)
33
+ route.api_class = route_klass
34
+ route.to {|env|
25
35
  builder = Builder.new
26
36
  env['params'] ||= {}
27
37
  env['params'].merge!(env['router.params']) if env['router.params']
28
38
  builder.params = builder.retrieve_params(env)
29
- builder.instance_eval(&blk)
39
+ builder.instance_eval(&blk) if blk
40
+ route_klass.middlewares.each do |mw|
41
+ builder.instance_eval { use mw[0], *mw[1], &mw[2] }
42
+ end if route_klass
43
+ if route_klass or blk.nil?
44
+ raise "You cannot use `run' and supply a routing class at the same time" if builder.inner_app
45
+ builder.instance_eval { run env.event_handler }
46
+ end
30
47
  builder.to_app.call(env)
31
48
  }
32
49
  end
@@ -36,7 +53,6 @@ module Goliath
36
53
  end
37
54
  end
38
55
  end
39
-
40
56
  end
41
57
  end
42
58
  end
@@ -7,17 +7,20 @@ module Goliath
7
7
  # use Goliath::Rack::Heartbeat
8
8
  #
9
9
  class Heartbeat
10
- def initialize(app)
11
- @app = app
10
+ def initialize(app, opts = {})
11
+ @app = app
12
+ @opts = opts
13
+ @opts[:path] ||= '/status'
14
+ @opts[:response] ||= [200, {}, 'OK']
12
15
  end
13
16
 
14
17
  def call(env)
15
- if env['PATH_INFO'] == '/status'
16
- [200, {}, 'OK']
18
+ if env['PATH_INFO'] == @opts[:path]
19
+ @opts[:response]
17
20
  else
18
21
  @app.call(env)
19
22
  end
20
23
  end
21
24
  end
22
25
  end
23
- end
26
+ end
@@ -0,0 +1,114 @@
1
+ module Goliath
2
+ module Rack
3
+
4
+ #
5
+ # This module gives you ergonomics similar to traditional Rack middleware:
6
+ #
7
+ # * Use instance variables! Each SimpleAroundware is unique to its request.
8
+ # * You have accessors for env and (once in post_process) status, headers,
9
+ # body -- no more shipping them around to every method.
10
+ #
11
+ # If in your traditional rack middleware you'd do this:
12
+ #
13
+ # class MyRackMiddleware
14
+ # def call(env)
15
+ # get_ready_to_be_totally_awesome()
16
+ # status, headers, body = @app.call(env)
17
+ # new_body = make_totally_awesome(body)
18
+ # [status, headers, new_body]
19
+ # end
20
+ # end
21
+ #
22
+ # You'd now do this:
23
+ #
24
+ # class MyAwesomeAroundware
25
+ # include Goliath::Rack::SimpleAroundware
26
+ # def pre_process
27
+ # get_ready_to_be_totally_awesome()
28
+ # end
29
+ # def post_process
30
+ # new_body = make_totally_awesome(body)
31
+ # [status, headers, new_body]
32
+ # end
33
+ # end
34
+ #
35
+ # And you'd include it in your endpoint like this:
36
+ #
37
+ # class AwesomeApi < Goliath::API
38
+ # use Goliath::Rack::SimpleAroundwareFactory, MyAwesomeAroundware
39
+ # end
40
+ #
41
+ # @example
42
+ # # count incoming requests, outgoing responses, and
43
+ # # outgoing responses by status code
44
+ # class StatsdLogger
45
+ # include Goliath::Rack::SimpleAroundware
46
+ # def pre_process
47
+ # statsd_count("reqs.#{config['statsd_name']}.in")
48
+ # Goliath::Connection::AsyncResponse
49
+ # end
50
+ # def post_process
51
+ # statsd_count("reqs.#{config['statsd_name']}.out")
52
+ # statsd_count("reqs.#{config['statsd_name']}.#{status}")
53
+ # [status, headers, body]
54
+ # end
55
+ # def statsd_count(name, count=1, sampling_frac=nil)
56
+ # # ...
57
+ # end
58
+ # end
59
+ #
60
+ # class AwesomeApiWithLogging < Goliath::API
61
+ # use Goliath::Rack::Params
62
+ # use Goliath::Rack::SimpleAroundwareFactory, StatsdLogger
63
+ # def response(env)
64
+ # # ... do something awesome
65
+ # end
66
+ # end
67
+ #
68
+ module SimpleAroundware
69
+ include Goliath::Rack::Validator
70
+
71
+ # The request environment, set in the initializer
72
+ attr_reader :env
73
+ # The response, set by the SimpleAroundware's downstream
74
+ attr_accessor :status, :headers, :body
75
+
76
+ # @param env [Goliath::Env] The request environment
77
+ # @return [Goliath::Rack::SimpleAroundware]
78
+ def initialize(env)
79
+ @env = env
80
+ end
81
+
82
+ # Override this method in your middleware to perform any preprocessing
83
+ # (launching a deferred request, perhaps).
84
+ #
85
+ # You must return Goliath::Connection::AsyncResponse if you want processing to continue
86
+ #
87
+ # @return [Array] array contains [status, headers, body]
88
+ def pre_process
89
+ Goliath::Connection::AsyncResponse
90
+ end
91
+
92
+ # Override this method in your middleware to perform any postprocessing.
93
+ # This will only be invoked when all deferred requests (including the
94
+ # response) have completed.
95
+ #
96
+ # @return [Array] array contains [status, headers, body]
97
+ def post_process
98
+ [status, headers, body]
99
+ end
100
+
101
+ # Virtual setter for the downstream middleware/endpoint response
102
+ def downstream_resp=(status_headers_body)
103
+ @status, @headers, @body = status_headers_body
104
+ end
105
+
106
+ # On receipt of an async result,
107
+ # * call the setter for that handle if any (on receipt of :shortened_url,
108
+ def accept_response(handle, resp_succ, resp)
109
+ self.downstream_resp = resp
110
+ end
111
+
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,121 @@
1
+ module Goliath
2
+ module Rack
3
+ #
4
+ # Include this to enable middleware that can perform pre- and
5
+ # post-processing.
6
+ #
7
+ # For internal reasons, you can't do the following as you would in Rack:
8
+ #
9
+ # def call(env)
10
+ # # ... do pre-processing
11
+ # status, headers, body = @app.call(env)
12
+ # new_body = make_totally_awesome(body) ## !! BROKEN !!
13
+ # [status, headers, new_body]
14
+ # end
15
+ #
16
+ # This class creates a "aroundware" helper to do that kind of
17
+ # processing. Goliath proceeds asynchronously, but will still "unwind" the
18
+ # request by walking up the callback chain. Delegating out to the aroundware
19
+ # also lets you carry state around -- the ban on instance variables no
20
+ # longer applies, as each aroundware is unique per request.
21
+ #
22
+ # @see Goliath::Rack::AsyncMiddleware
23
+ # @see Goliath::Rack::SimpleAroundware
24
+ # @see Goliath::Rack::BarrierAroundware
25
+ #
26
+ class SimpleAroundwareFactory
27
+ include Goliath::Rack::Validator
28
+
29
+ # Called by the framework to create the middleware.
30
+ #
31
+ # Any extra args passed to the use statement are sent to each
32
+ # aroundware_klass as it is created.
33
+ #
34
+ # @example
35
+ # class Awesomizer2011
36
+ # include Goliath::Rack::SimpleAroundware
37
+ # def initialize(env, aq)
38
+ # @awesomeness_quotient = aq
39
+ # super(env)
40
+ # end
41
+ # # ... define pre_process and post_process ...
42
+ # end
43
+ #
44
+ # class AwesomeApiWithShortening < Goliath::API
45
+ # use Goliath::Rack::SimpleAroundwareFactory, Awesomizer2011, 3
46
+ # # ... stuff ...
47
+ # end
48
+ #
49
+ # @param app [#call] the downstream app
50
+ # @param aroundware_klass a class that quacks like a
51
+ # Goliath::Rack::SimpleAroundware and an EM::Deferrable
52
+ # @param *args [Array] extra args to pass to the aroundware
53
+ # @return [Goliath::Rack::AroundwareFactory]
54
+ def initialize app, aroundware_klass, *args
55
+ @app = app
56
+ @aroundware_klass = aroundware_klass
57
+ @aroundware_args = args
58
+ end
59
+
60
+ # Coordinates aroundware to process a request.
61
+ #
62
+ # We hook the aroundware in the middle of the async_callback chain:
63
+ # * send the downstream response to the aroundware, whether received directly
64
+ # from @app.call or via async callback
65
+ # * have the upstream callback chain be invoked when the aroundware completes
66
+ #
67
+ # @param env [Goliath::Env] The goliath environment
68
+ # @return [Array] The [status_code, headers, body] tuple
69
+ def call(env)
70
+ aroundware = new_aroundware(env)
71
+
72
+ aroundware_resp = aroundware.pre_process
73
+ return aroundware_resp if final_response?(aroundware_resp)
74
+
75
+ hook_into_callback_chain(env, aroundware)
76
+
77
+ downstream_resp = @app.call(env)
78
+
79
+ # if downstream resp is final, pass it to the aroundware; it will invoke
80
+ # the callback chain at its leisure. Our response is *always* async.
81
+ if final_response?(downstream_resp)
82
+ aroundware.accept_response(:downstream_resp, true, downstream_resp)
83
+ end
84
+ return Goliath::Connection::AsyncResponse
85
+ end
86
+
87
+ # Put aroundware in the middle of the async_callback chain:
88
+ # * save the old callback chain;
89
+ # * have the downstream callback send results to the aroundware (possibly
90
+ # completing it)
91
+ # * set the old callback chain to fire when the aroundware completes
92
+ def hook_into_callback_chain(env, aroundware)
93
+ async_callback = env['async.callback']
94
+
95
+ # The response from the downstream app is accepted by the aroundware...
96
+ # ... and we immediately call post_process and hand it upstream
97
+ downstream_callback = Proc.new do |resp|
98
+ safely(env){ aroundware.accept_response(:downstream_resp, true, resp) }
99
+ new_resp = safely(env){ aroundware.post_process }
100
+ async_callback.call(new_resp)
101
+ end
102
+
103
+ env['async.callback'] = downstream_callback
104
+ end
105
+
106
+ def final_response?(resp)
107
+ resp != Goliath::Connection::AsyncResponse
108
+ end
109
+
110
+ # Generate a aroundware to process the request, using request env & any args
111
+ # passed to this AroundwareFactory at creation
112
+ #
113
+ # @param env [Goliath::Env] The goliath environment
114
+ # @return [Goliath::Rack::SimpleAroundware] The aroundware to process this request
115
+ def new_aroundware(env)
116
+ @aroundware_klass.new(env, *@aroundware_args)
117
+ end
118
+
119
+ end
120
+ end
121
+ end
@@ -12,6 +12,13 @@ module Goliath
12
12
  include Goliath::Rack::Validator
13
13
  attr_reader :type, :key, :message
14
14
 
15
+ # extracted from activesupport 3.0.9
16
+ if defined?(Encoding) && "".respond_to?(:encode)
17
+ NON_WHITESPACE_REGEXP = %r![^[:space:]]!
18
+ else
19
+ NON_WHITESPACE_REGEXP = %r![^\s#{[0x3000].pack("U")}]!
20
+ end
21
+
15
22
  # Creates the Goliath::Rack::Validation::RequiredParam validator
16
23
  #
17
24
  # @param app The app object
@@ -34,7 +41,7 @@ module Goliath
34
41
 
35
42
  def key_valid?(params)
36
43
  if !params.has_key?(key) || params[key].nil? ||
37
- (params[key].is_a?(String) && params[key] =~ /^\s*$/)
44
+ (params[key].is_a?(String) && params[key] !~ NON_WHITESPACE_REGEXP)
38
45
  return false
39
46
  end
40
47
 
@@ -42,7 +49,7 @@ module Goliath
42
49
  unless params[key].compact.empty?
43
50
  params[key].each do |k|
44
51
  return true unless k.is_a?(String)
45
- return true unless k =~ /^\s*$/
52
+ return true unless k !~ NON_WHITESPACE_REGEXP
46
53
  end
47
54
  end
48
55
  return false
@@ -71,6 +71,8 @@ module Goliath
71
71
  @env[PATH_INFO] = parser.request_path
72
72
  @env[FRAGMENT] = parser.fragment
73
73
 
74
+ yield if block_given?
75
+
74
76
  begin
75
77
  @env[ASYNC_HEADERS].call(@env, h) if @env[ASYNC_HEADERS]
76
78
  rescue Exception => e
@@ -200,6 +202,11 @@ module Goliath
200
202
  headers['Content-Length'] = body.bytesize.to_s
201
203
  @env[:terminate_connection] = true
202
204
  post_process([status, headers, body])
205
+
206
+ # Mark the request as complete to force a flush on the response.
207
+ # Note: #on_body and #response hooks may still fire if the data
208
+ # is already in the parser buffer.
209
+ succeed
203
210
  end
204
211
 
205
212
  # Used to determine if the connection should be kept open
@@ -100,19 +100,31 @@ module Goliath
100
100
  opts.separator "Server options:"
101
101
 
102
102
  opts.on('-e', '--environment NAME', "Set the execution environment (prod, dev or test) (default: #{@options[:env]})") { |val| @options[:env] = val }
103
-
104
103
  opts.on('-a', '--address HOST', "Bind to HOST address (default: #{@options[:address]})") { |addr| @options[:address] = addr }
105
104
  opts.on('-p', '--port PORT', "Use PORT (default: #{@options[:port]})") { |port| @options[:port] = port.to_i }
105
+ opts.on('-S', '--socket FILE', "Bind to unix domain socket") { |v| @options[:address] = v; @options[:port] = nil }
106
+
107
+ opts.separator ""
108
+ opts.separator "Daemon options:"
106
109
 
107
110
  opts.on('-u', '--user USER', "Run as specified user") {|v| @options[:user] = v }
111
+ opts.on('-c', '--config FILE', "Config file (default: ./config/<server>.rb)") { |v| @options[:config] = v }
112
+ opts.on('-d', '--daemonize', "Run daemonized in the background (default: #{@options[:daemonize]})") { |v| @options[:daemonize] = v }
108
113
  opts.on('-l', '--log FILE', "Log to file (default: off)") { |file| @options[:log_file] = file }
109
114
  opts.on('-s', '--stdout', "Log to stdout (default: #{@options[:log_stdout]})") { |v| @options[:log_stdout] = v }
110
-
111
- opts.on('-c', '--config FILE', "Config file (default: ./config/<server>.rb)") { |v| @options[:config] = v }
112
115
  opts.on('-P', '--pid FILE', "Pid file (default: off)") { |file| @options[:pid_file] = file }
113
- opts.on('-d', '--daemonize', "Run daemonized in the background (default: #{@options[:daemonize]})") { |v| @options[:daemonize] = v }
114
- opts.on('-v', '--verbose', "Enable verbose logging (default: #{@options[:verbose]})") { |v| @options[:verbose] = v }
115
116
 
117
+ opts.separator ""
118
+ opts.separator "SSL options:"
119
+ opts.on('--ssl', 'Enables SSL (default: off)') {|v| @options[:ssl] = v }
120
+ opts.on('--ssl-key FILE', 'Path to private key') {|v| @options[:ssl_key] = v }
121
+ opts.on('--ssl-cert FILE', 'Path to certificate') {|v| @options[:ssl_cert] = v }
122
+ opts.on('--ssl-verify', 'Enables SSL certificate verification') {|v| @options[:ssl_verify] = v }
123
+
124
+ opts.separator ""
125
+ opts.separator "Common options:"
126
+
127
+ opts.on('-v', '--verbose', "Enable verbose logging (default: #{@options[:verbose]})") { |v| @options[:verbose] = v }
116
128
  opts.on('-h', '--help', 'Display help message') { show_options(opts) }
117
129
  end
118
130
  end