Archive

Archive for June 30th, 2010

super action middleware

June 30th, 2010 Comments off

As a proof of concept I created an super action that allows to process more than one action at once.
Probably most of you will ask a question ‘why’ if there is an posibility to use nested resources. Yes you can, but what if there are more different actions that do not have common root?

Code bellow is only proof of concept, it was tested only using integration tests, if you ever try it more please let me know.

First Step is to create middleware scaffold ‘lib/super_action.rb’:

class SuperAction
  include Rack::Utils

  def initialize(app)
    @app = app
  end

  def call(env)
    @app.call(env)
  end
end

Next we have to connect it, we can not do it as advised in environment file, we also do not want to change the whole stack, the way to go is to hook into application loading process soewhere after it initilaizes all automatic middlewares. Add the following code (module) into ‘config/environment.rb’:

...
require File.join(File.dirname(__FILE__), 'boot')

module Rails
  class Initializer
    alias :old_load_application_classes :load_application_classes
    def load_application_classes
      configuration.middleware.delete ActiveRecord::QueryCache
      configuration.middleware.delete ActiveRecord::ConnectionAdapters::ConnectionManagement
      configuration.middleware.insert_before ActionController::ParamsParser, 'SuperAction'
      configuration.middleware.insert_before 'SuperAction', ActiveRecord::ConnectionAdapters::ConnectionManagement
      configuration.middleware.insert_before 'SuperAction', ActiveRecord::QueryCache
      old_load_application_classes
    end
  end
end

Rails::Initializer.run do |config|
...

We can not do the middleware changes directly in initializer using config.middleware because then it raises exception NameError on ‘ActiveRecord::QueryCache’ – it is loaded in the initialization stack after environment. So this is the current stack looks now for me:

~/projects/super_action> rake middleware
use Rack::Lock
use ActionController::Failsafe
use ActionController::Session::CookieStore, #
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
use SuperAction
use ActionController::ParamsParser
use Rack::MethodOverride
use Rack::Head
use ActionController::StringCoercion
run ActionController::Dispatcher.new

Important here was to be before ActionController::ParamsParser, because it is responsible for providing correct params to action. Putting ConnectionManagement and QueryCache before super action should be considered as an optimization.

When we have correct order of middlewares now the super action functionality, back to ‘lib/super_action.rb’:

class SuperAction
  include Rack::Utils

  def initialize(app)
    @app = app
  end

  def call(env)
    uri  = env['REQUEST_URI'] || env['PATH_INFO']
    wrapper = env['rack.input']
    body = wrapper.read
    wrapper.rewind

    unless body.blank?
      params, format = if uri == '/super_action.xml'
        [Hash.from_xml(body), :xml]
      end
    end

    first_response = nil

    if params
      response_array = params['requests'].map do |req|
        req_env = env.clone
        req_env['PATH_INFO']      = req['url']
        req_env['REQUEST_URI']    = req['url']
        req_env['REQUEST_METHOD'] = req['method']
        req_env['CONTENT_TYPE']   = req['content_type']
        req_env['rack.input']     = Rack::Lint::InputWrapper.new StringIO.new( req['body'] || '' )
        status, headers, req_response = @app.call(req_env)
        first_response ||= req_response
{
  :url => req['url'],
  :method => req['method'],
  :status => status,
  :body => req_response.body,
  :id => req['id']
}
      end

      response_body = case format
      when :xml then 
        xml = Builder::XmlMarkup.new
        xml.responses 'type' => 'array' do
          response_array.each do |resp|
            xml.response do
              xml.url resp[:url]
              xml.method resp[:method]
              xml.id resp[:id], 'type' => 'integer'
              xml.body do
                xml.cdata! resp[:body]
              end
            end
          end
        end
      end

      header = HeaderHash.new
      header['Content-Type'] = 'application/xml; charset=utf-8'
      header['Content-Length'] = response_body.size.to_s
      header['Cache-Control'] = 'private, max-age=0, must-revalidate'

      first_response.instance_variable_set(:@body, response_body)
      [ 200, header, response_body]
    else
      @app.call(env)
    end
  end
end

I know the code looks tricky, it realy is, but finally it allowed me to run the following test:

  test "update many books" do
    Book.delete_all

    start = Time.now

    xml = Builder::XmlMarkup.new
    xml.requests 'type' => 'array' do
      xml.request do
        xml.url '/books.xml'
        xml.method 'POST'
        xml.id 0
        xml.content_type 'application/xml'
        xml.body do
          xml.cdata! '<title>book2</title>'
        end
      end
      xml.request do
        xml.url '/books.xml'
        xml.method 'POST'
        xml.id 1
        xml.content_type 'application/xml'
        xml.body do
          xml.cdata! '<title>book3</title>'
        end
      end
      xml.request do
        xml.url '/books.xml'
        xml.method 'GET'
        xml.id 2
      end
    end

    assert_difference "Book.count", 2 do
      post '/super_action.xml', xml.target!
    end
    responses = Hash.from_xml( response.body )['responses']

    stop = Time.now
    puts "separate:#{stop-start}:"

y [ xml.target!, response.body ]

    assert_response :success
    assert_equal 3, responses.size
    responses.sort! { |a,b| a['id']  b['id'] }

    assert_equal 0, responses[0]['id']
    assert_equal 'book2', Hash.from_xml(responses[0]['body'])['book']['title']

    assert_equal 1, responses[1]['id']
    assert_equal 'book3', Hash.from_xml(responses[1]['body'])['book']['title']

    assert_equal 2, responses[2]['id']
    assert_equal 'book2', Hash.from_xml(responses[2]['body'])['books'][0]['title']
    assert_equal 'book3', Hash.from_xml(responses[2]['body'])['books'][1]['title']
  end

Speed up in integration tests is over 2x on three actions, this makes this something considerable … only if the code would be not so tricky.