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
@@ -1,7 +1,16 @@
1
- require 'goliath/synchrony/response_receiver'
1
+ require 'goliath/deprecated/response_receiver'
2
+ require 'em-synchrony/em-mongo'
2
3
 
3
4
  module Goliath
4
5
  module Synchrony
6
+ #
7
+ # Note: This class is deprecated. Please instead use BarrierAroundware
8
+ # (orchestrates multiple concurrent requests) or SimpleAroundware (like
9
+ # AsyncMiddleware, but with a simpler interface).
10
+ #
11
+ # There are more notes on the lib/goliath/deprecated/async_aroundware docs.
12
+ #
13
+ # ___________________________________________________________________________
5
14
  #
6
15
  # Currently, you must provide in the env a method 'mongo' that returns a mongo
7
16
  # collection or collection proxy (probably by setting it up in the config).
@@ -31,18 +40,29 @@ module Goliath
31
40
  # ... requests aren't deferrables so they're tracked in @pending_queries
32
41
  end
33
42
 
34
- def find(collection, selector={}, opts={}, &block)
35
- @pending_queries += 1
36
- db.collection(collection).find(selector, opts) do |result|
37
- yield result
38
- @pending_queries -= 1
39
- self.succeed if finished?
43
+ if defined?(EM::Mongo::Cursor)
44
+ def find(collection, selector={}, opts={}, &block)
45
+ @pending_queries += 1
46
+ db.collection(collection).afind(selector, opts).to_a.callback do |result|
47
+ yield result
48
+ @pending_queries -= 1
49
+ self.succeed if finished?
50
+ end
51
+ end
52
+ else
53
+ def find(collection, selector={}, opts={}, &block)
54
+ @pending_queries += 1
55
+ db.collection(collection).afind(selector, opts) do |result|
56
+ yield result
57
+ @pending_queries -= 1
58
+ self.succeed if finished?
59
+ end
40
60
  end
41
61
  end
42
62
 
43
63
  def first(collection, selector={}, opts={}, &block)
44
64
  opts[:limit] = 1
45
- find(collection, selector, opts) do |result|
65
+ self.find(collection, selector, opts) do |result|
46
66
  yield result.first
47
67
  end
48
68
  end
@@ -0,0 +1,97 @@
1
+ module Goliath
2
+ module Synchrony
3
+
4
+ #
5
+ # Note: This class is deprecated. Please instead use BarrierAroundware
6
+ # (orchestrates multiple concurrent requests) or SimpleAroundware (like
7
+ # AsyncMiddleware, but with a simpler interface).
8
+ #
9
+ # There are more notes on the lib/goliath/deprecated/async_aroundware docs.
10
+ #
11
+ module ResponseReceiver
12
+ # The request environment, set in the initializer
13
+ attr_reader :env
14
+ # The response, set by the ResponseReceiver's downstream
15
+ attr_accessor :status, :headers, :body
16
+
17
+ # Override this method in your middleware to perform any preprocessing
18
+ # (launching a deferred request, perhaps).
19
+ #
20
+ # @return [Array] array contains [status, headers, body]
21
+ def pre_process
22
+ Goliath::Connection::AsyncResponse
23
+ end
24
+
25
+ # Override this method in your middleware to perform any postprocessing.
26
+ # This will only be invoked when all deferred requests (including the
27
+ # response) have completed.
28
+ #
29
+ # @return [Array] array contains [status, headers, body]
30
+ def post_process
31
+ [status, headers, body]
32
+ end
33
+
34
+ # Virtual setter for the downstream middleware/endpoint response
35
+ def downstream_resp=(status_headers_body)
36
+ @status, @headers, @body = status_headers_body
37
+ end
38
+
39
+ # Invoked by the async_callback chain. Stores the [status, headers, body]
40
+ # for post_process'ing
41
+ def call resp
42
+ return resp if resp.first == Goliath::Connection::AsyncResponse.first
43
+ self.downstream_resp = resp
44
+ check_progress(nil)
45
+ end
46
+
47
+ # Have we received a response?
48
+ def response_received?
49
+ !! @status
50
+ end
51
+
52
+ protected
53
+
54
+ def check_progress(fiber)
55
+ if finished?
56
+ succeed
57
+ # continue processing
58
+ fiber.resume(self) if fiber && fiber.alive? && fiber != Fiber.current
59
+ end
60
+ end
61
+ end
62
+
63
+ #
64
+ # Note: This class is deprecated. Please instead use BarrierAroundware
65
+ # (orchestrates multiple concurrent requests) or SimpleAroundware (like
66
+ # AsyncMiddleware, but with a simpler interface).
67
+ #
68
+ # There are more notes on the lib/goliath/deprecated/async_aroundware docs.
69
+ #
70
+ class MultiReceiver < EM::Synchrony::Multi
71
+ include ResponseReceiver
72
+
73
+ # Create a new MultiReceiver
74
+ # @param env [Goliath::Env] the current environment
75
+ def initialize env
76
+ @env = env
77
+ super()
78
+ end
79
+
80
+ alias_method :enqueue, :add
81
+
82
+ def successes
83
+ responses[:callback]
84
+ end
85
+
86
+ def failures
87
+ responses[:errback]
88
+ end
89
+
90
+ # Finished if we received a response and the multi request is finished
91
+ def finished?
92
+ super && response_received?
93
+ end
94
+ end
95
+
96
+ end
97
+ end
data/lib/goliath/env.rb CHANGED
@@ -6,6 +6,7 @@ module Goliath
6
6
  # Goliath::Env also provides access to the logger, configuration information
7
7
  # and anything else set into the config data during initialization.
8
8
  class Env < Hash
9
+ attr_accessor :event_handler
9
10
  include Constants
10
11
 
11
12
  # Create a new Goliath::Env object
@@ -121,6 +122,10 @@ module Goliath
121
122
  super
122
123
  end
123
124
 
125
+ def defer_stack
126
+ @defer_stack ||= []
127
+ end
128
+
124
129
  # The Goliath::Env will provide any of it's keys as a method. It will also provide
125
130
  # any of the keys in the config object as methods. The methods will return
126
131
  # the value of the key. If the key doesn't exist in either hash this will
data/lib/goliath/rack.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  module Goliath
2
2
  module Rack
3
- autoload :AsyncAroundware, 'goliath/rack/async_aroundware'
4
3
  autoload :AsyncMiddleware, 'goliath/rack/async_middleware'
4
+ autoload :BarrierAroundware, 'goliath/rack/barrier_aroundware'
5
+ autoload :BarrierAroundwareFactory, 'goliath/rack/barrier_aroundware_factory'
5
6
  autoload :Builder, 'goliath/rack/builder'
6
7
  autoload :DefaultMimeType, 'goliath/rack/default_mime_type'
7
8
  autoload :DefaultResponseFormat, 'goliath/rack/default_response_format'
@@ -10,9 +11,13 @@ module Goliath
10
11
  autoload :JSONP, 'goliath/rack/jsonp'
11
12
  autoload :Params, 'goliath/rack/params'
12
13
  autoload :Render, 'goliath/rack/render'
14
+ autoload :SimpleAroundware, 'goliath/rack/simple_aroundware'
15
+ autoload :SimpleAroundwareFactory, 'goliath/rack/simple_aroundware_factory'
13
16
  autoload :Templates, 'goliath/rack/templates'
14
17
  autoload :Tracer, 'goliath/rack/tracer'
15
18
  autoload :Validator, 'goliath/rack/validator'
16
19
  autoload :Validation, 'goliath/rack/validation'
20
+ #
21
+ autoload :AsyncAroundware, 'goliath/deprecated/async_aroundware'
17
22
  end
18
23
  end
@@ -21,13 +21,13 @@ module Goliath
21
21
  # include Goliath::Rack::AsyncMiddleware
22
22
  #
23
23
  # def call(env)
24
- # awesomness_quotient = 3
24
+ # awesomeness_quotient = 3
25
25
  # # the extra args sent to super are passed along to post_process
26
- # super(env, awesomness_quotient)
26
+ # super(env, awesomeness_quotient)
27
27
  # end
28
28
  #
29
- # def post_process(env, status, headers, body, awesomness_quotient)
30
- # new_body = make_totally_awesome(body, awesomness_quotient)
29
+ # def post_process(env, status, headers, body, awesomeness_quotient)
30
+ # new_body = make_totally_awesome(body, awesomeness_quotient)
31
31
  # [status, headers, new_body]
32
32
  # end
33
33
  # end
@@ -41,6 +41,8 @@ module Goliath
41
41
  # next request. Everything that you need to store needs to be stored in
42
42
  # local variables.
43
43
  module AsyncMiddleware
44
+ include Goliath::Rack::Validator
45
+
44
46
  # Called by the framework to create the middleware.
45
47
  #
46
48
  # @param app [Proc] The application
@@ -65,19 +67,38 @@ module Goliath
65
67
  # @param env [Goliath::Env] The goliath environment
66
68
  # @return [Array] The [status_code, headers, body] tuple
67
69
  def call(env, *args)
68
- async_cb = env['async.callback']
69
70
 
70
- env['async.callback'] = Proc.new do |status, headers, body|
71
- async_cb.call(post_process(env, status, headers, body, *args))
72
- end
71
+ hook_into_callback_chain(env, *args)
73
72
 
74
- status, headers, body = @app.call(env)
73
+ downstream_resp = @app.call(env)
75
74
 
76
- if status == Goliath::Connection::AsyncResponse.first
77
- [status, headers, body]
78
- else
75
+ if final_response?(downstream_resp)
76
+ status, headers, body = downstream_resp
79
77
  post_process(env, status, headers, body, *args)
78
+ else
79
+ return Goliath::Connection::AsyncResponse
80
+ end
81
+ end
82
+
83
+ # Put a callback block in the middle of the async_callback chain:
84
+ # * save the old callback chain;
85
+ # * have the downstream callback send results to our proc...
86
+ # * which fires old callback chain when it completes
87
+ def hook_into_callback_chain(env, *args)
88
+ async_callback = env['async.callback']
89
+
90
+ # The response from the downstream app is sent to post_process
91
+ # and then directly up the callback chain
92
+ downstream_callback = Proc.new do |status, headers, body|
93
+ new_resp = safely(env){ post_process(env, status, headers, body, *args) }
94
+ async_callback.call(new_resp)
80
95
  end
96
+
97
+ env['async.callback'] = downstream_callback
98
+ end
99
+
100
+ def final_response?(resp)
101
+ resp != Goliath::Connection::AsyncResponse
81
102
  end
82
103
 
83
104
  # Override this method in your middleware to perform any
@@ -88,6 +109,7 @@ module Goliath
88
109
  def post_process(env, status, headers, body)
89
110
  [status, headers, body]
90
111
  end
112
+
91
113
  end
92
114
  end
93
115
  end
@@ -0,0 +1,228 @@
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
+ # ...along with a new superpower: you can #enqueue requests in #pre_process,
12
+ # and the barrier will hold off on executing #post_process until both the
13
+ # downstream and your enqueued requests have completed.
14
+ #
15
+ # If in your traditional middleware you'd (with poor concurrency) do this:
16
+ #
17
+ # class MyRackMiddleware
18
+ # def call(env)
19
+ # user_info = get_user_from_db
20
+ # status, headers, body = @app.call(env)
21
+ # new_body = put_username_into_sidebar_text(body, user_info)
22
+ # [status, headers, new_body]
23
+ # end
24
+ # end
25
+ #
26
+ # You can now do this:
27
+ #
28
+ # class MyAwesomeAroundware
29
+ # include Goliath::Rack::BarrierAroundware
30
+ # attr_accessor :user_info
31
+ # def pre_process
32
+ # enqueue :user_info, async_get_user_from_db
33
+ # end
34
+ # # !concurrency!
35
+ # def post_process
36
+ # new_body = put_username_into_sidebar_text(body, user_info)
37
+ # [status, headers, new_body]
38
+ # end
39
+ # end
40
+ #
41
+ # Which you'd include in your endpoint like this:
42
+ #
43
+ # class AwesomeApi < Goliath::API
44
+ # use Goliath::Rack::BarrierAroundwareFactory, MyAwesomeAroundware
45
+ # end
46
+ #
47
+ # The user record was retrieved from the db while other processing happened;
48
+ # once the async request named :user_info returned, goliath noticed that you
49
+ # had a #user_info= setter and so it set the variable appropriately. (It's
50
+ # also put in the #successes (or #failures) hash).
51
+ #
52
+ # You can also enqueue a non-EM::Deferrable request. #enqueue_acceptor gives
53
+ # you a dummy deferrable; send the response to its succeed method:
54
+ #
55
+ # # a database lookup that takes a block
56
+ # enqueue_acceptor(:bob) do |acc|
57
+ # db.collection(:users).afind(:username => :bob) do |resp|
58
+ # acc.succeed(resp.first)
59
+ # end
60
+ # end
61
+ #
62
+ # You're free to invoke the barrier whenever you like. Consider a bouncer
63
+ # who is polite to townies (he lets them order from the bar while he checks
64
+ # their ID) but a jerk to college kids (who have to wait in line before they
65
+ # can order):
66
+ #
67
+ # class AuthAroundware
68
+ # include Goliath::Rack::BarrierAroundware
69
+ # attr_accessor :user_info
70
+ # def pre_process
71
+ # enqueue :user_info, async_get_user_from_db
72
+ # unless lazy_authorization?
73
+ # perform # yield execution until user_info has arrived
74
+ # check_authorization! # then check the info *before* continuing
75
+ # end
76
+ # end
77
+ # #
78
+ # def post_process
79
+ # check_authorization! if lazy_authorization?
80
+ # [status, headers, new_body]
81
+ # end
82
+ # def lazy_authorization?
83
+ # (env['REQUEST_METHOD'] == 'GET') || (env['REQUEST_METHOD'] == 'HEAD')
84
+ # end
85
+ # end
86
+ # class AwesomeApi < Goliath::API
87
+ # use Goliath::Rack::BarrierAroundwareFactory, AuthAroundware
88
+ # end
89
+ #
90
+ # The `perform` statement puts up a barrier until all pending requests (in
91
+ # this case, :user_info) complete. The downstream request isn't enqueued
92
+ # until pre_process completes, so in the non-`GET` branch the AuthAroundware
93
+ # is able to verify the user *before* allowing execution to proceed. If the
94
+ # request is a harmless `GET`, though, both the user_info and downstream
95
+ # requests can proceed concurrently, and we instead `check_authorization!`
96
+ # in the post_process block.
97
+ #
98
+ # @example
99
+ # class ShortenUrl
100
+ # attr_accessor :shortened_url
101
+ # include Goliath::Rack::BarrierAroundware
102
+ #
103
+ # def pre_process
104
+ # target_url = PostRank::URI.clean(env.params['url'])
105
+ # shortener_request = EM::HttpRequest.new('http://is.gd/create.php').aget(:query => { :format => 'simple', :url => target_url })
106
+ # enqueue :shortened_url, shortener_request
107
+ # Goliath::Connection::AsyncResponse
108
+ # end
109
+ #
110
+ # # by the time you get here, the AroundwareFactory will have populated
111
+ # # the [status, headers, body] and the shortener_request will have
112
+ # # populated the shortened_url attribute.
113
+ # def post_process
114
+ # if succeeded?(:shortened_url)
115
+ # headers['X-Shortened-URI'] = shortened_url
116
+ # end
117
+ # [status, headers, body]
118
+ # end
119
+ # end
120
+ #
121
+ # class AwesomeApiWithShortening < Goliath::API
122
+ # use Goliath::Rack::Params
123
+ # use Goliath::Rack::BarrierAroundwareFactory, ShortenUrl
124
+ # def response(env)
125
+ # # ... do something awesome
126
+ # end
127
+ # end
128
+ #
129
+ module BarrierAroundware
130
+ include EventMachine::Deferrable
131
+ include Goliath::Rack::SimpleAroundware
132
+
133
+ # Pool with handles of pending requests
134
+ attr_reader :pending_requests
135
+ # Pool with handles of sucessful requests
136
+ attr_reader :successes
137
+ # Pool with handles of failed requests
138
+ attr_reader :failures
139
+
140
+ # @param env [Goliath::Env] The request environment
141
+ # @return [Goliath::Rack::BarrierAroundware]
142
+ def initialize(env)
143
+ @env = env
144
+ @pending_requests = Set.new
145
+ @successes = {}
146
+ @failures = {}
147
+ end
148
+
149
+ # On receipt of an async result,
150
+ # * remove the tracking handle from pending_requests
151
+ # * and file the response in either successes or failures as appropriate
152
+ # * call the setter for that handle if any (on receipt of :shortened_url,
153
+ # calls self.shortened_url = resp)
154
+ # * check progress -- succeeds (transferring controll) if nothing is pending.
155
+ def accept_response(handle, resp_succ, resp, req=nil, fiber=nil)
156
+ raise "received response for a non-pending request!" if not pending_requests.include?(handle)
157
+ pending_requests.delete(handle)
158
+ resp_succ ? (successes[handle] = [req, resp]) : (failures[handle] = [req, resp])
159
+ self.send("#{handle}=", resp) if self.respond_to?("#{handle}=")
160
+ check_progress(fiber)
161
+ resp
162
+ end
163
+
164
+ # Add a deferred request to the pending pool, and set a callback to
165
+ # #accept_response when the request completes
166
+ def enqueue(handle, deferred_req)
167
+ fiber = Fiber.current
168
+ add_to_pending(handle)
169
+ deferred_req.callback{|resp| safely(env){ accept_response(handle, true, resp, deferred_req, fiber) } }
170
+ deferred_req.errback{|resp| safely(env){ accept_response(handle, false, resp, deferred_req, fiber) } }
171
+ end
172
+
173
+ # Do you have a method that uses a block, not a deferrable? This method
174
+ # gives you a deferrable 'acceptor' and enqueues it -- simply call
175
+ # #succeed (or #fail) on the acceptor from within the block, passing it
176
+ # your desired response.
177
+ #
178
+ # @example
179
+ # # sleep for 1.0 seconds and then complete
180
+ # enqueue_acceptor(:sleepy)do |acc|
181
+ # EM.add_timer(1.0){ acc.succeed }
182
+ # end
183
+ #
184
+ # @example
185
+ # # a database lookup that takes a block
186
+ # enqueue_acceptor(:bob) do |acc|
187
+ # db.collection(:users).afind(:username => :bob) do |resp|
188
+ # acc.succeed(resp.first)
189
+ # end
190
+ # end
191
+ #
192
+ def enqueue_acceptor(handle)
193
+ acceptor = EM::DefaultDeferrable.new
194
+ yield(acceptor)
195
+ enqueue handle, acceptor
196
+ end
197
+
198
+ # Register a pending request. If you call this from outside #enqueue, you
199
+ # must construct callbacks that eventually invoke accept_response
200
+ def add_to_pending(handle)
201
+ set_deferred_status(nil) # we're not done yet, even if we were
202
+ @pending_requests << handle
203
+ end
204
+
205
+ def finished?
206
+ pending_requests.empty?
207
+ end
208
+
209
+ # Perform will yield (allowing other processes to continue) until all
210
+ # pending responses complete. You're free to enqueue responses, call
211
+ # perform,
212
+ def perform
213
+ Fiber.yield unless finished?
214
+ end
215
+
216
+ protected
217
+
218
+ def check_progress(fiber)
219
+ if finished?
220
+ succeed
221
+ # continue processing
222
+ fiber.resume(self) if fiber && fiber.alive? && fiber != Fiber.current
223
+ end
224
+ end
225
+
226
+ end
227
+ end
228
+ end