Skip to content
Browse files

Refactoring aroundware, Part VI (the last): documentation cleanup in …

…lib/; also, examples/auth_and_rate_limit now checks credentials beforehand (on non-GET/HEAD) or does so in parallel on idempotent requests
  • Loading branch information...
1 parent 20122fb commit 7295f1766d43f23411176bbb3fdd265e13e3d7c7 Philip (flip) Kromer committed Jul 30, 2011
View
89 examples/auth_and_rate_limit.rb
@@ -76,31 +76,38 @@
# Tracks and enforces account and rate limit policies.
#
-# This is like a bouncer who lets you order a drink while he checks your ID. We
-# proxy your request to a backend server and get your account/usage info; if
-# your ID is good there's no further wait on a response.
+# This is like a bouncer who lets townies order a drink while he checks their
+# ID, but who's a jerk to college kids.
#
-# This works through the magic of BarrierAroundware:
+# On GET or HEAD requests, it proxies the request and gets account/usage info
+# concurrently; authorizing the account doesn't delay the response.
+#
+# On a POST or other non-idempotent request, it checks the account/usage info
+# *before* allowing the request to fire. This takes longer, but is necessary and
+# tolerable.
+#
+# The magic of BarrierAroundware:
#
# 1) In pre_process (before the request):
# * validate an apikey was given; if not, raise (returning directly)
# * launch requests for the account and rate limit usage
#
-# 2) BarrierAroundwareFactory passes the request down the middleware chain
-#
-# 3) post_process resumes only when both proxied request & auth info are complete
+# 2) On a POST or other non-GET non-HEAD, we issue `perform`, which barriers
+# (allowing other requests to proceed) until the two pending requests
+# complete. It then checks the account exists and is valid, and that the rate
+# limit is OK
#
-# 4) The post_process method then
-# - Checks the account exists and is valid
-# - Checks the rate limit is OK
+# 3) If the auth check fails, we raise an error (later caught by a safely{}
+# block and turned into the right 4xx HTTP response.
#
-# 5) If it passes all those checks, the request goes through; otherwise we raise
-# an error that Goliath::Rack::Validator turns into a 4xx response
+# 4) If the auth check succeeds, or the request is a GET or HEAD, we return
+# Goliath::Connection::AsyncResponse, and BarrierAroundwareFactory passes the
+# request down the middleware chain
#
-# WARNING: Since this passes ALL requests through to the responder, it's only
-# suitable for idempotent requests (GET, typically). You may need to handle
-# POST/PUT/DELETE requests differently.
+# 5) post_process resumes only when both proxied request & auth info are complete
+# (it already has of course in the non-lazy scenario)
#
+# 6) If we were lazy, the post_process method now checks authorization
#
class AuthBarrier
include Goliath::Rack::BarrierAroundware
@@ -127,10 +134,13 @@ def pre_process
# the results of the afirst deferrable will be set right into account_info (and the request into successes)
enqueue_mongo_request(:account_info, { :_id => apikey })
enqueue_mongo_request(:usage_info, { :_id => usage_id })
+ maybe_fake_delay!
- # Fake out a delay in the database response if auth_db_delay is given
- if (auth_db_delay = env.params['auth_db_delay'].to_f) > 0
- enqueue_acceptor(:sleepy){|acc| EM.add_timer(auth_db_delay){ acc.succeed } }
+ # On non-GET non-HEAD requests, we have to check auth now.
+ unless lazy_authorization?
+ perform # yield execution until user_info has arrived
+ charge_usage
+ check_authorization!
end
env.trace('pre_process_end')
@@ -139,26 +149,22 @@ def pre_process
def post_process
env.trace('post_process_beg')
-
- # When post_process resumes, the db requests and the response are here!
# [:account_info, :usage_info, :status, :headers, :body].each{|attr| env.logger.info(("%23s\t%s" % [attr, self.send(attr).inspect[0..200]])) }
- self.account_info ||= {}
- self.usage_info ||= {}
-
inject_headers
- EM.next_tick do
- safely(env){ charge_usage }
+ # We have to check auth now, we skipped it before
+ if lazy_authorization?
+ charge_usage
+ check_authorization!
end
- safely(env, headers) do
- check_apikey!
- check_rate_limit!
+ env.trace('post_process_end')
+ [status, headers, body]
+ end
- env.trace('post_process_end')
- [status, headers, body]
- end
+ def lazy_authorization?
+ (env['REQUEST_METHOD'] == 'GET') || (env['REQUEST_METHOD'] == 'HEAD')
end
if defined?(EM::Mongo::Cursor)
@@ -175,35 +181,50 @@ def enqueue_mongo_request(handle, query)
end
end
+ # Fake out a delay in the database response if auth_db_delay is given
+ def maybe_fake_delay!
+ if (auth_db_delay = env.params['auth_db_delay'].to_f) > 0
+ enqueue_acceptor(:sleepy){|acc| EM.add_timer(auth_db_delay){ acc.succeed } }
+ end
+ end
+
def accept_response(handle, *args)
env.trace("received_#{handle}")
super(handle, *args)
end
# ===========================================================================
+ def check_authorization!
+ check_apikey!
+ check_rate_limit!
+ end
+
def validate_apikey!
if apikey.to_s.empty?
raise MissingApikeyError
end
end
def check_apikey!
- unless account_info['valid'] == true
+ unless account_info && (account_info['valid'] == true)
raise InvalidApikeyError
end
end
def check_rate_limit!
+ self.usage_info ||= {}
rate = usage_info['calls'].to_i + 1
limit = account_info['max_call_rate'].to_i
return true if rate <= limit
raise RateLimitExceededError, "Your request rate (#{rate}) is over your limit (#{limit})"
end
def charge_usage
- db.collection(:usage_info).update({ :_id => usage_id },
- { '$inc' => { :calls => 1 } }, :upsert => true)
+ EM.next_tick do
+ safely(env){ db.collection(:usage_info).update({ :_id => usage_id },
+ { '$inc' => { :calls => 1 } }, :upsert => true) }
+ end
end
def inject_headers
View
14 lib/goliath/deprecated/async_aroundware.rb
@@ -17,6 +17,20 @@ module Rack
# * ResponseReceiver used to masquerade as callback and middleware. Yuck.
# The downstream response is now set via #accept_response, not #call.
#
+ # * change
+ # use Goliath::Rack::AsyncAroundware, MyObsoleteReceiver
+ # to
+ # use Goliath::Rack::BarrierAroundwareFactory, MyHappyBarrier
+ # * `BarrierAroundware` provides the combined functionality of
+ # `MultiReceiver` and `ResponseReceiver`, which will go away. It's now a
+ # mixin (module) so you're not forced to inherit from it.
+ # * There is no more `responses` method: either use instance accessors or
+ # look in the `successes`/`failures` hashes for yourresults.
+ # * Both enqueued responses and the downstream response are sent to
+ # `accept_response`; there is no more `call` method.
+ # * `MongoReceiver` will go away, because there's no need for it. See
+ # `examples/auth_and_rate_limit.rb` for examples
+ #
class AsyncAroundware
include Goliath::Rack::Validator
View
2 lib/goliath/deprecated/response_receiver.rb
@@ -11,7 +11,7 @@ module Synchrony
module ResponseReceiver
# The request environment, set in the initializer
attr_reader :env
- # The response, set by the BarrierMiddleware's downstream
+ # The response, set by the ResponseReceiver's downstream
attr_accessor :status, :headers, :body
# Override this method in your middleware to perform any preprocessing
View
127 lib/goliath/rack/barrier_aroundware.rb
@@ -2,8 +2,129 @@ module Goliath
module Rack
#
- # The strategy here is similar to that of EM::Multi. Figuring out what goes
- # on there will help you understand this.
+ # This module gives you ergonomics similar to traditional Rack middleware:
+ #
+ # * Use instance variables! Each SimpleAroundware is unique to its request.
+ # * You have accessors for env and (once in post_process) status, headers,
+ # body -- no more shipping them around to every method.
+ #
+ # ...along with a new superpower: you can #enqueue requests in #pre_process,
+ # and the barrier will hold off on executing #post_process until both the
+ # downstream and your enqueued requests have completed.
+ #
+ # If in your traditional middleware you'd (with poor concurrency) do this:
+ #
+ # class MyRackMiddleware
+ # def call(env)
+ # user_info = get_user_from_db
+ # status, headers, body = @app.call(env)
+ # new_body = put_username_into_sidebar_text(body, user_info)
+ # [status, headers, new_body]
+ # end
+ # end
+ #
+ # You can now do this:
+ #
+ # class MyAwesomeAroundware
+ # include Goliath::Rack::BarrierAroundware
+ # attr_accessor :user_info
+ # def pre_process
+ # enqueue :user_info, async_get_user_from_db
+ # end
+ # # !concurrency!
+ # def post_process
+ # new_body = put_username_into_sidebar_text(body, user_info)
+ # [status, headers, new_body]
+ # end
+ # end
+ #
+ # Which you'd include in your endpoint like this:
+ #
+ # class AwesomeApi < Goliath::API
+ # use Goliath::Rack::BarrierAroundwareFactory, MyAwesomeAroundware
+ # end
+ #
+ # The user record was retrieved from the db while other processing happened;
+ # once the async request named :user_info returned, goliath noticed that you
+ # had a #user_info= setter and so it set the variable appropriately. (It's
+ # also put in the #successes (or #failures) hash).
+ #
+ # You can also enqueue a non-EM::Deferrable request. #enqueue_acceptor gives
+ # you a dummy deferrable; send the response to its succeed method:
+ #
+ # # a database lookup that takes a block
+ # enqueue_acceptor(:bob) do |acc|
+ # db.collection(:users).afind(:username => :bob) do |resp|
+ # acc.succeed(resp.first)
+ # end
+ # end
+ #
+ # You're free to invoke the barrier whenever you like. Consider a bouncer
+ # who is polite to townies (he lets them order from the bar while he checks
+ # their ID) but a jerk to college kids (who have to wait in line before they
+ # can order):
+ #
+ # class AuthAroundware
+ # include Goliath::Rack::BarrierAroundware
+ # attr_accessor :user_info
+ # def pre_process
+ # enqueue :user_info, async_get_user_from_db
+ # unless lazy_authorization?
+ # perform # yield execution until user_info has arrived
+ # check_authorization! # then check the info *before* continuing
+ # end
+ # end
+ # #
+ # def post_process
+ # check_authorization! if lazy_authorization?
+ # [status, headers, new_body]
+ # end
+ # def lazy_authorization?
+ # (env['REQUEST_METHOD'] == 'GET') || (env['REQUEST_METHOD'] == 'HEAD')
+ # end
+ # end
+ # class AwesomeApi < Goliath::API
+ # use Goliath::Rack::BarrierAroundwareFactory, AuthAroundware
+ # end
+ #
+ # The `perform` statement puts up a barrier until all pending requests (in
+ # this case, :user_info) complete. The downstream request isn't enqueued
+ # until pre_process completes, so in the non-`GET` branch the AuthAroundware
+ # is able to verify the user *before* allowing execution to proceed. If the
+ # request is a harmless `GET`, though, both the user_info and downstream
+ # requests can proceed concurrently, and we instead `check_authorization!`
+ # in the post_process block.
+ #
+ # @example
+ # class ShortenUrl
+ # attr_accessor :shortened_url
+ # include Goliath::Rack::BarrierAroundware
+ #
+ # def pre_process
+ # target_url = PostRank::URI.clean(env.params['url'])
+ # shortener_request = EM::HttpRequest.new('http://is.gd/create.php').aget(:query => { :format => 'simple', :url => target_url })
+ # enqueue :shortened_url, shortener_request
+ # Goliath::Connection::AsyncResponse
+ # end
+ #
+ # # by the time you get here, the AroundwareFactory will have populated
+ # # the [status, headers, body] and the shortener_request will have
+ # # populated the shortened_url attribute.
+ # def post_process
+ # if succeeded?(:shortened_url)
+ # headers['X-Shortened-URI'] = shortened_url
+ # end
+ # [status, headers, body]
+ # end
+ # end
+ #
+ # class AwesomeApiWithShortening < Goliath::API
+ # use Goliath::Rack::Params
+ # use Goliath::Rack::BarrierAroundwareFactory, ShortenUrl
+ # def response(env)
+ # # ... do something awesome
+ # end
+ # end
#
module BarrierAroundware
include EventMachine::Deferrable
@@ -17,7 +138,7 @@ module BarrierAroundware
attr_reader :failures
# @param env [Goliath::Env] The request environment
- # @return [Goliath::Rack::AsyncBarrier]
+ # @return [Goliath::Rack::BarrierAroundware]
def initialize(env)
@env = env
@pending_requests = Set.new
View
45 lib/goliath/rack/barrier_aroundware_factory.rb
@@ -2,7 +2,7 @@ module Goliath
module Rack
#
# Include this to enable middleware that can perform pre- and
- # post-processing, optionally having multiple responses pending.
+ # post-processing, orchestrating multiple concurrent requests.
#
# For internal reasons, you can't do the following as you would in Rack:
#
@@ -13,42 +13,19 @@ module Rack
# [status, headers, new_body]
# end
#
- # This class creates a "aroundware" helper to do that kind of "around"
+ # This class creates an "aroundware" helper to do that kind of
# processing. Goliath proceeds asynchronously, but will still "unwind" the
- # request by walking up the callback chain. Delegating out to the
- # aroundware also lets you carry state around -- the ban on instance
- # variables no longer applies, as each aroundware is unique per request.
+ # request by walking up the callback chain. Delegating out to the aroundware
+ # also lets you carry state around -- the ban on instance variables no
+ # longer applies, as each aroundware is unique per request.
#
- # @example
- # class ShortenUrl
- # attr_accessor :shortened_url
- # include Goliath::Rack::BarrierAroundware
+ # The strategy here is similar to that in EM::Multi. Figuring out what goes
+ # on there will help you understand this.
#
- # def pre_process
- # target_url = PostRank::URI.clean(env.params['url'])
- # shortener_request = EM::HttpRequest.new('http://is.gd/create.php').aget(:query => { :format => 'simple', :url => target_url })
- # enqueue :shortened_url, shortener_request
- # Goliath::Connection::AsyncResponse
- # end
- #
- # # by the time you get here, the AroundwareFactory will have populated
- # # the [status, headers, body] and the shortener_request will have
- # # populated the shortened_url attribute.
- # def post_process
- # if succeeded?(:shortened_url)
- # headers['X-Shortened-URI'] = shortened_url
- # end
- # [status, headers, body]
- # end
- # end
- #
- # class AwesomeApiWithShortening < Goliath::API
- # use Goliath::Rack::Params
- # use Goliath::Rack::BarrierAroundwareFactory, ShortenUrl
- # def response(env)
- # # ... do something awesome
- # end
- # end
+ # @see EventMachine::Multi
+ # @see Goliath::Rack::SimpleAroundware
+ # @see Goliath::Rack::SimpleAroundwareFactory
+ # @see Goliath::Rack::BarrierAroundware
#
class BarrierAroundwareFactory < Goliath::Rack::SimpleAroundwareFactory
include Goliath::Rack::Validator
View
67 lib/goliath/rack/simple_aroundware.rb
@@ -2,16 +2,79 @@ module Goliath
module Rack
#
+ # This module gives you ergonomics similar to traditional Rack middleware:
+ #
+ # * Use instance variables! Each SimpleAroundware is unique to its request.
+ # * You have accessors for env and (once in post_process) status, headers,
+ # body -- no more shipping them around to every method.
+ #
+ # If in your traditional rack middleware you'd do this:
+ #
+ # class MyRackMiddleware
+ # def call(env)
+ # get_ready_to_be_totally_awesome()
+ # status, headers, body = @app.call(env)
+ # new_body = make_totally_awesome(body)
+ # [status, headers, new_body]
+ # end
+ # end
+ #
+ # You'd now do this:
+ #
+ # class MyAwesomeAroundware
+ # include Goliath::Rack::SimpleAroundware
+ # def pre_process
+ # get_ready_to_be_totally_awesome()
+ # end
+ # def post_process
+ # new_body = make_totally_awesome(body)
+ # [status, headers, new_body]
+ # end
+ # end
+ #
+ # And you'd include it in your endpoint like this:
+ #
+ # class AwesomeApi < Goliath::API
+ # use Goliath::Rack::SimpleAroundwareFactory, MyAwesomeAroundware
+ # end
+ #
+ # @example
+ # # count incoming requests, outgoing responses, and
+ # # outgoing responses by status code
+ # class StatsdLogger
+ # include Goliath::Rack::SimpleAroundware
+ # def pre_process
+ # statsd_count("reqs.#{config['statsd_name']}.in")
+ # Goliath::Connection::AsyncResponse
+ # end
+ # def post_process
+ # statsd_count("reqs.#{config['statsd_name']}.out")
+ # statsd_count("reqs.#{config['statsd_name']}.#{status}")
+ # [status, headers, body]
+ # end
+ # def statsd_count(name, count=1, sampling_frac=nil)
+ # # ...
+ # end
+ # end
+ #
+ # class AwesomeApiWithLogging < Goliath::API
+ # use Goliath::Rack::Params
+ # use Goliath::Rack::SimpleAroundwareFactory, StatsdLogger
+ # def response(env)
+ # # ... do something awesome
+ # end
+ # end
+ #
module SimpleAroundware
include Goliath::Rack::Validator
# The request environment, set in the initializer
attr_reader :env
- # The response, set by the BarrierMiddleware's downstream
+ # The response, set by the SimpleAroundware's downstream
attr_accessor :status, :headers, :body
# @param env [Goliath::Env] The request environment
- # @return [Goliath::Rack::AsyncBarrier]
+ # @return [Goliath::Rack::SimpleAroundware]
def initialize(env)
@env = env
end
View
37 lib/goliath/rack/simple_aroundware_factory.rb
@@ -13,38 +13,15 @@ module Rack
# [status, headers, new_body]
# end
#
- # This class creates a "aroundware" helper to do that kind of "around"
+ # This class creates a "aroundware" helper to do that kind of
# processing. Goliath proceeds asynchronously, but will still "unwind" the
- # request by walking up the callback chain. Delegating out to the
- # aroundware also lets you carry state around -- the ban on instance
- # variables no longer applies, as each aroundware is unique per request.
+ # request by walking up the callback chain. Delegating out to the aroundware
+ # also lets you carry state around -- the ban on instance variables no
+ # longer applies, as each aroundware is unique per request.
#
- # @example
- # # count incoming requests, outgoing responses, and
- # # outgoing responses by status code
- # class StatsdLogger
- # include Goliath::Rack::SimpleAroundware
- # def pre_process
- # statsd_count("reqs.#{config['statsd_name']}.in")
- # Goliath::Connection::AsyncResponse
- # end
- # def post_process
- # statsd_count("reqs.#{config['statsd_name']}.out")
- # statsd_count("reqs.#{config['statsd_name']}.#{status}")
- # [status, headers, body]
- # end
- # def statsd_count(name, count=1, sampling_frac=nil)
- # # ...
- # end
- # end
- #
- # class AwesomeApiWith < Goliath::API
- # use Goliath::Rack::Params
- # use Goliath::Rack::AroundwareFactory, StatsdLogger
- # def response(env)
- # # ... do something awesome
- # end
- # end
+ # @see Goliath::Rack::AsyncMiddleware
+ # @see Goliath::Rack::SimpleAroundware
+ # @see Goliath::Rack::BarrierAroundware
#
class SimpleAroundwareFactory
include Goliath::Rack::Validator

0 comments on commit 7295f17

Please sign in to comment.
Something went wrong with that request. Please try again.