super action middleware
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.


