Module: Goliath::Rack::BarrierAroundware
- Includes:
- EventMachine::Deferrable, SimpleAroundware
- Defined in:
- lib/goliath/rack/barrier_aroundware.rb
Overview
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 = (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 = (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
perform # yield execution until user_info has arrived
# then check the info *before* continuing
end
end
#
def post_process
if
[status, headers, new_body]
end
def
(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.
Instance Attribute Summary collapse
-
#failures ⇒ Object
readonly
Pool with handles of failed requests.
-
#pending_requests ⇒ Object
readonly
Pool with handles of pending requests.
-
#successes ⇒ Object
readonly
Pool with handles of sucessful requests.
Attributes included from SimpleAroundware
#body, #env, #headers, #status
Instance Method Summary collapse
-
#accept_response(handle, resp_succ, resp, req = nil, fiber = nil) ⇒ Object
On receipt of an async result, * remove the tracking handle from pending_requests * and file the response in either successes or failures as appropriate * call the setter for that handle if any (on receipt of :shortened_url, calls self.shortened_url = resp) * check progress – succeeds (transferring controll) if nothing is pending.
-
#add_to_pending(handle) ⇒ Object
Register a pending request.
-
#enqueue(handle, deferred_req) ⇒ Object
Add a deferred request to the pending pool, and set a callback to #accept_response when the request completes.
-
#enqueue_acceptor(handle) {|acceptor| ... } ⇒ Object
Do you have a method that uses a block, not a deferrable? This method gives you a deferrable ‘acceptor’ and enqueues it – simply call #succeed (or #fail) on the acceptor from within the block, passing it your desired response.
- #finished? ⇒ Boolean
- #initialize(env) ⇒ Goliath::Rack::BarrierAroundware
-
#perform ⇒ Object
Perform will yield (allowing other processes to continue) until all pending responses complete.
Methods included from SimpleAroundware
#downstream_resp=, #post_process, #pre_process
Methods included from Validator
Instance Attribute Details
#failures ⇒ Object (readonly)
Pool with handles of failed requests
138 139 140 |
# File 'lib/goliath/rack/barrier_aroundware.rb', line 138 def failures @failures end |
#pending_requests ⇒ Object (readonly)
Pool with handles of pending requests
134 135 136 |
# File 'lib/goliath/rack/barrier_aroundware.rb', line 134 def pending_requests @pending_requests end |
#successes ⇒ Object (readonly)
Pool with handles of sucessful requests
136 137 138 |
# File 'lib/goliath/rack/barrier_aroundware.rb', line 136 def successes @successes end |
Instance Method Details
#accept_response(handle, resp_succ, resp, req = nil, fiber = nil) ⇒ Object
On receipt of an async result,
-
remove the tracking handle from pending_requests
-
and file the response in either successes or failures as appropriate
-
call the setter for that handle if any (on receipt of :shortened_url, calls self.shortened_url = resp)
-
check progress – succeeds (transferring controll) if nothing is pending.
155 156 157 158 159 160 161 162 |
# File 'lib/goliath/rack/barrier_aroundware.rb', line 155 def accept_response(handle, resp_succ, resp, req=nil, fiber=nil) raise "received response for a non-pending request!" if not pending_requests.include?(handle) pending_requests.delete(handle) resp_succ ? (successes[handle] = [req, resp]) : (failures[handle] = [req, resp]) self.send("#{handle}=", resp) if self.respond_to?("#{handle}=") check_progress(fiber) resp end |
#add_to_pending(handle) ⇒ Object
Register a pending request. If you call this from outside #enqueue, you must construct callbacks that eventually invoke accept_response
200 201 202 203 |
# File 'lib/goliath/rack/barrier_aroundware.rb', line 200 def add_to_pending(handle) set_deferred_status(nil) # we're not done yet, even if we were @pending_requests << handle end |
#enqueue(handle, deferred_req) ⇒ Object
Add a deferred request to the pending pool, and set a callback to #accept_response when the request completes
166 167 168 169 170 171 |
# File 'lib/goliath/rack/barrier_aroundware.rb', line 166 def enqueue(handle, deferred_req) fiber = Fiber.current add_to_pending(handle) deferred_req.callback{|resp| safely(env){ accept_response(handle, true, resp, deferred_req, fiber) } } deferred_req.errback{|resp| safely(env){ accept_response(handle, false, resp, deferred_req, fiber) } } end |
#enqueue_acceptor(handle) {|acceptor| ... } ⇒ Object
Do you have a method that uses a block, not a deferrable? This method gives you a deferrable ‘acceptor’ and enqueues it – simply call #succeed (or #fail) on the acceptor from within the block, passing it your desired response.
192 193 194 195 196 |
# File 'lib/goliath/rack/barrier_aroundware.rb', line 192 def enqueue_acceptor(handle) acceptor = EM::DefaultDeferrable.new yield(acceptor) enqueue handle, acceptor end |
#finished? ⇒ Boolean
205 206 207 |
# File 'lib/goliath/rack/barrier_aroundware.rb', line 205 def finished? pending_requests.empty? end |
#initialize(env) ⇒ Goliath::Rack::BarrierAroundware
142 143 144 145 146 147 |
# File 'lib/goliath/rack/barrier_aroundware.rb', line 142 def initialize(env) @env = env @pending_requests = Set.new @successes = {} @failures = {} end |
#perform ⇒ Object
Perform will yield (allowing other processes to continue) until all pending responses complete. You’re free to enqueue responses, call perform,
212 213 214 |
# File 'lib/goliath/rack/barrier_aroundware.rb', line 212 def perform Fiber.yield unless finished? end |