Refactored aroundware #74

Merged
merged 8 commits into from Aug 9, 2011

Conversation

Projects
None yet
2 participants
Member

mrflip commented Jul 31, 2011

I refactored the aroundware to a form that I think makes a lot more sense. Pending approval, it's in the postrank-labs new_aroundware branch.

tl;dr:

  • If you weren't using aroundware, nothing significant should change.
  • If you weren't using aroundware because you tried to read the code and felt dizzy, give it another shot: there are multiple examples in the ./examples dir and below.

As part of this, I organized the control flow in AsyncMiddleware, the old AsyncAroundware, and the new Aroundware modules to be both clearer and easily comparable. I committed these in stages, so if you're looking to review my code (and please do) you should step through the commits in turn.

safely block post_process in AsyncMiddleware

  • The post_process method in an AsyncMiddleware now executes safely{} -- an error won't hang the response any more, and you can probably clean out some rescue blocks from your middlewares.

Minor (but possibly breaking) changes to AsyncAroundware & ResponseReceiver.

The naming of those is pretty stinky anyway, so I moved them to 'lib/goliath/deprecated', leaving all their functionality intact. Any program that used them before should still work unmodified.

To move to the new world:

  • make sure your pre_process returns Goliath::Connection::AsyncResponse (unless you want to return directly, which you now can).

  • 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 your results.

  • 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 -- instead of its soupy delegation you can now just enqueue handle, db.collection(handle).afirst({ :_id => apikey }).

SimpleAroundware and SimpleAroundwareFactory

SimpleAroundware gives you similar ergonomics to traditional Rack middleware, including instance variables and accessors for env, and (once in post_process) status, headers, body -- the latter set for you by goliath before post_process is called.

A SimpleAroundware is slightly heavier than an AsyncMiddleware, but if you're not counting single milliseconds and consider it cleaner, I think SimpleAroundware is the most readable option.

  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
  class AwesomeApi < Goliath::API
    use Goliath::Rack::SimpleAroundwareFactory, MyAwesomeAroundware
  end

BarrierAroundware and BarrierAroundwareFactory

BarrierAroundware is similar to the old AsyncAroundware, but has a cleaner interface and tidier internals.

  • BarrierAroundware and SimpleAroundware present the same interface, but BarrierAroundware allows you to enqueue requests and yield execution until the group of them completes. You use a BarrierAroundware like this:

    class HiBobAroundware
      include Goliath::Rack::BarrierAroundware
      attr_accessor :user_info
      def pre_process
        enqueue :user_info, async_get_user_from_db
      end
      # when post_process is called, status, headers, body and user_info have been set
      def post_process
        new_body = put_username_into_sidebar_text(body, user_info)
        [status, headers, new_body]
      end
    end
    class AwesomeApi < Goliath::API
      use Goliath::Rack::BarrierAroundwareFactory, HiBobAroundware
    end
  • If there is a setter matching the request's queue handle, BarrierAroundware will hand the response to it (so, enqueue(:shortened_url, req) will effectively call self.shortened_url = resp when it completes).

  • You can 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, only the user_info) completes. 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.

  • To enqueue a non-EM::Deferrable request, use #enqueue_acceptor. It gives you a dummy deferrable, and you send the response to its succeed method:

    enqueue_acceptor(:bob) do |acc|
      db.collection(:users).afind(:username => :bob) do |resp|
        acc.succeed(resp.first)
      end
    end
    

Philip (flip) Kromer added some commits Jul 30, 2011

Philip (flip) Kromer Refactoring async_aroundware, Part I. in ResponseReceiver, the defaul…
…t pre_process returns Goliath::Connection::AsyncResponse; env is an attr_reader not attr_accessor; in examples/async_aroundware_demo.rb added a missing include. Otherwise, this commit is largely cosmetic.
72435e0
Philip (flip) Kromer mongo aroundware must be 0.3.x version with current setup 8df93be
Philip (flip) Kromer Refactoring async_aroundware, Part II. Refactored to make the control…
… flow clearer. This is mostly cosmetic, and sets the stage for a new interface that will reduce the distinction between AsyncAroundware and normal middleware.
80fc1d1
Philip (flip) Kromer Refactoring aroundware, Part III: added new SimpleAroundware and Barr…
…ierAroundware, to replace the soon-deprecated (but still functional) AsyncAroundware and cronies.

* In AsyncMiddleware, moved callback hook into own method making it easier to follow; also, post_process executes in a safely{} block, avoiding a source of hung calls.
* Created two pairs of base modules for aroundware: SimpleAroundware and SimpleAroundwareFactory handle the case where you *may* want to share information across pre- and post-processing, but don't need to have a barrier to clear pending calls.
* BarrierAroundware and BarrierAroundwareFactory respect the same interface, but add equivalent functionality to EM::Multi.
  - Any deferrable you #enqueue goes into a pending_requests pool; once #pre_process returns, the downstream callback's response also goes in the pending_requests pool.
  - The BarrierAroundware's post_process method will not resume until all pending_requests (the aroundware's and the downstream response) have completed.
  - You're free to at any time also call #perform!, which concurrently waits for the pending pool to clear and then resume.
  - Completed requests are added to the succeses or failures hash as appropriate; and passed to the instance setter named for that handle if any (so, enqueue(:shortened_url, su_req) will eventually call self.shortened_url = su_req on completion).
In a following commit, I'll move AsyncAroundware, ResponseReceiver, MongoReciever to a deprecated/ directory and explain the differences
0f6ed2a
Philip (flip) Kromer Refactoring aroundware, Part IV: Deprecated old AsyncAroundware, Resp…
…onseReceiver, MultiReceiver, MongoReceiver. Examples still work with the old aroundwarez.
88e6748
Philip (flip) Kromer Added a favicon interceptor to the examples, and used it in the raste…
…rizer examples
6395370
Philip (flip) Kromer Refactoring aroundware, Part V: Moved all the aroundware examples ove…
…r to use the new aroundware, doing necessary cleanup along the way.

* BarrierAroundware now store [req, resp] in the successes / failures hashes
* mongo things now work with both old and future em-mongo gems, at the cost of a big conditional 'if' statement in the file
* Added enqueue_acceptor to let you enqueue activities that take a block without yielding a deferrable
20122fb
Philip (flip) Kromer 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
7295f17
Owner

igrigorik commented Jul 31, 2011

Took a quick pass over the changes, seems to make sense! Great work, I like the new API much better - it's still takes some time to wrap your head around it, but a major improvement.

Love the auth + rate limit example. Would love to see a presentation on how you guys are using it in production. :-)

@mrflip mrflip pushed a commit that referenced this pull request Aug 9, 2011

Philip (flip) Kromer Merge pull request #74 from postrank-labs/new_aroundware
Refactored aroundware
a91b4f7

@mrflip mrflip merged commit a91b4f7 into master Aug 9, 2011

Member

mrflip commented Aug 9, 2011

As no objections have sounded, I'm landing this -- please pull carefully!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment