├── .gitignore ├── Gemfile ├── Rakefile ├── lib ├── rack_after_reply │ ├── version.rb │ ├── adapter │ │ ├── base.rb │ │ ├── thin.rb │ │ ├── mongrel.rb │ │ ├── unicorn.rb │ │ ├── passenger.rb │ │ └── webrick.rb │ ├── adapter.rb │ ├── request_handler.rb │ └── app_proxy.rb └── rack_after_reply.rb ├── CHANGELOG ├── rack_after_reply.gemspec ├── LICENSE └── README.markdown /.gitignore: -------------------------------------------------------------------------------- 1 | /Gemfile.lock -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | gemspec 2 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'ritual' 2 | -------------------------------------------------------------------------------- /lib/rack_after_reply/version.rb: -------------------------------------------------------------------------------- 1 | module RackAfterReply 2 | VERSION = [0, 0, 3] 3 | 4 | class << VERSION 5 | include Comparable 6 | 7 | def to_s 8 | join('.') 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | == 0.0.3 2012-05-17 2 | 3 | * Fix Passenger restarting workers unnecessarily. [Julien Letessier] 4 | 5 | == 0.0.2 2011-05-20 6 | 7 | * Handle RackAfterReply being loaded before web server library. 8 | 9 | == 0.0.1 2011-05-10 10 | 11 | * Hi. 12 | -------------------------------------------------------------------------------- /lib/rack_after_reply/adapter/base.rb: -------------------------------------------------------------------------------- 1 | module RackAfterReply 2 | module Adapter 3 | class Base 4 | def self.apply 5 | return if defined?(@applied) 6 | instance.apply 7 | @applied = true 8 | end 9 | 10 | def self.instance 11 | @instance ||= new 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/rack_after_reply/adapter.rb: -------------------------------------------------------------------------------- 1 | module RackAfterReply 2 | module Adapter 3 | autoload :Base, 'rack_after_reply/adapter/base' 4 | autoload :Mongrel, 'rack_after_reply/adapter/mongrel' 5 | autoload :Passenger, 'rack_after_reply/adapter/passenger' 6 | autoload :Thin, 'rack_after_reply/adapter/thin' 7 | autoload :Unicorn, 'rack_after_reply/adapter/unicorn' 8 | autoload :WEBrick, 'rack_after_reply/adapter/webrick' 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/rack_after_reply/adapter/thin.rb: -------------------------------------------------------------------------------- 1 | module RackAfterReply 2 | module Adapter 3 | class Thin < Base 4 | def apply 5 | ::Thin::Connection.module_eval do 6 | def pre_process_with_rack_after_reply 7 | callbacks = [] 8 | @request.env[RackAfterReply::CALLBACKS_KEY] = callbacks 9 | EM.next_tick { callbacks.each {|c| c.call} } 10 | pre_process_without_rack_after_reply 11 | end 12 | RackAfterReply.freedom_patch self, :pre_process 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/rack_after_reply/request_handler.rb: -------------------------------------------------------------------------------- 1 | module RackAfterReply 2 | module RequestHandler 3 | attr_accessor :rack_after_reply_callbacks 4 | 5 | def fire_rack_after_reply 6 | # Ensure we only fire the hook once. Passenger runs its request 7 | # handler when shutting down, causing an infinite loop if we 8 | # don't check for this. 9 | rack_after_reply_callbacks or 10 | return 11 | 12 | rack_after_reply_callbacks.each do |callback| 13 | callback.call 14 | end 15 | self.rack_after_reply_callbacks = nil 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/rack_after_reply/adapter/mongrel.rb: -------------------------------------------------------------------------------- 1 | module RackAfterReply 2 | module Adapter 3 | class Mongrel < Base 4 | def apply 5 | Rack::Handler::Mongrel.module_eval do 6 | include RackAfterReply::RequestHandler 7 | 8 | def initialize_with_rack_after_reply(app) 9 | app = AppProxy.new(self, app) 10 | initialize_without_rack_after_reply(app) 11 | end 12 | RackAfterReply.freedom_patch self, :initialize 13 | 14 | def process_with_rack_after_reply(request, response) 15 | process_without_rack_after_reply(request, response) 16 | ensure 17 | response.socket.close 18 | fire_rack_after_reply 19 | end 20 | RackAfterReply.freedom_patch self, :process 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/rack_after_reply/adapter/unicorn.rb: -------------------------------------------------------------------------------- 1 | module RackAfterReply 2 | module Adapter 3 | class Unicorn < Base 4 | def apply 5 | ::Unicorn::HttpServer.module_eval do 6 | include RackAfterReply::RequestHandler 7 | 8 | def process_client_with_rack_after_reply(client) 9 | # We can't install the AppProxy in #initialize, because 10 | # the HttpServer is already instantiated by the time we 11 | # typically run. Wrap it here exactly once. 12 | self.app = AppProxy.new(self, app) unless @rack_after_reply_wrapped 13 | @rack_after_reply_wrapped = true 14 | 15 | process_client_without_rack_after_reply(client) 16 | fire_rack_after_reply 17 | end 18 | RackAfterReply.freedom_patch(self, :process_client) 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/rack_after_reply/app_proxy.rb: -------------------------------------------------------------------------------- 1 | module RackAfterReply 2 | # 3 | # Wraps a Rack app to intercept the rack environment passed to #call 4 | # for access by the request handler after the socket is closed. 5 | # 6 | class AppProxy 7 | def initialize(request_handler, app) 8 | @request_handler = request_handler 9 | @app = app 10 | end 11 | 12 | def call(env) 13 | callbacks = [] 14 | env[RackAfterReply::CALLBACKS_KEY] = callbacks 15 | @request_handler.rack_after_reply_callbacks = callbacks 16 | @app.call(env) 17 | end 18 | 19 | def method_missing(name, *args, &block) 20 | class_eval <<-EOS 21 | def #{name}(*args, &block) 22 | @app.#{name}(*args, &block) 23 | end 24 | EOS 25 | send(name, *args, &block) 26 | end 27 | 28 | def respond_to?(name, include_private=false) 29 | super || @app.respond_to?(name, include_private) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/rack_after_reply/adapter/passenger.rb: -------------------------------------------------------------------------------- 1 | module RackAfterReply 2 | module Adapter 3 | class Passenger < Base 4 | def apply 5 | PhusionPassenger::Rack::RequestHandler.module_eval do 6 | include RackAfterReply::RequestHandler 7 | 8 | def initialize_with_rack_after_reply(owner_pipe, app, options = {}) 9 | app = AppProxy.new(self, app) 10 | initialize_without_rack_after_reply(owner_pipe, app, options) 11 | end 12 | RackAfterReply.freedom_patch self, :initialize 13 | 14 | def accept_and_process_next_request_with_rack_after_reply(socket_wrapper, channel, buffer) 15 | response = accept_and_process_next_request_without_rack_after_reply(socket_wrapper, channel, buffer) 16 | fire_rack_after_reply 17 | response 18 | end 19 | RackAfterReply.freedom_patch self, :accept_and_process_next_request 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /rack_after_reply.gemspec: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path('lib', File.dirname(__FILE__)) 2 | require 'rack_after_reply/version' 3 | 4 | Gem::Specification.new do |gem| 5 | gem.name = 'rack_after_reply' 6 | gem.date = Time.now.strftime('%Y-%m-%d') 7 | gem.version = RackAfterReply::VERSION.join('.') 8 | gem.platform = Gem::Platform::RUBY 9 | gem.authors = ["George Ogata"] 10 | gem.email = ["george.ogata@gmail.com"] 11 | gem.license = 'MIT' 12 | gem.homepage = "http://github.com/oggy/rack_after_reply" 13 | gem.summary = "Rack hook which fires after the socket to the client is closed." 14 | 15 | gem.required_rubygems_version = ">= 1.3.6" 16 | gem.files = Dir["{doc,lib,rails}/**/*"] + %w(LICENSE README.markdown Rakefile CHANGELOG) 17 | gem.test_files = Dir["spec/**/*"] 18 | gem.extra_rdoc_files = ["LICENSE", "README.markdown"] 19 | gem.require_path = 'lib' 20 | gem.specification_version = 3 21 | gem.rdoc_options = ["--charset=UTF-8"] 22 | end 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) George Ogata 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Rack After Reply 2 | 3 | A hook for Rack apps which fires after the response has been sent, and 4 | the socket to the client has been closed. 5 | 6 | This is the ideal time to perform delayable, non-backgroundable tasks, 7 | such as garbage collection, stats gathering, flushing logs, etc. 8 | without affecting response times at all. 9 | 10 | ## Usage 11 | 12 | Simply add your callbacks to `env['rack_after_reply.callbacks']`. 13 | 14 | use Rack::ContentLength 15 | use Rack::ContentType, 'text/plain' 16 | run lambda { |env| 17 | env['rack_after_reply.callbacks'] << lambda { ... } 18 | [200, {}, ['hi']] 19 | } 20 | 21 | ## Support 22 | 23 | Rack After Request works with these web servers: 24 | 25 | * [Mongrel](https://github.com/fauna/mongrel) 26 | * [Passenger](http://www.modrails.com) 27 | * [Thin](https://github.com/macournoyer/thin) 28 | * [Unicorn](http://unicorn.bogomips.org) 29 | * WEBrick (distributed with Ruby) 30 | 31 | To request support for other web servers, [open a ticket][issues] or 32 | submit a patch. 33 | 34 | [issues]: http://github.com/oggy/rack_after_reply/issues 35 | 36 | ## Contributing 37 | 38 | * [Bug reports](https://github.com/oggy/rack_after_reply/issues) 39 | * [Source](https://github.com/oggy/rack_after_reply) 40 | * Patches: Fork on Github, send pull request. 41 | * Ensure patch includes tests. 42 | * Leave the version alone, or bump it in a separate commit. 43 | 44 | ## Copyright 45 | 46 | Copyright (c) George Ogata. See LICENSE for details. 47 | -------------------------------------------------------------------------------- /lib/rack_after_reply/adapter/webrick.rb: -------------------------------------------------------------------------------- 1 | module RackAfterReply 2 | module Adapter 3 | class WEBrick < Base 4 | def apply 5 | # Rack::Handler::WEBrick#service returns before the socket is closed, 6 | # and if we close it ourselves, WEBrick will close it again causing a 7 | # bomb. We can access the socket through the response argument, though, 8 | # so we hook into its #close method. 9 | Rack::Handler::WEBrick.module_eval do 10 | include RackAfterReply::RequestHandler 11 | 12 | def initialize_with_rack_after_reply(server, app) 13 | app = AppProxy.new(self, app) 14 | initialize_without_rack_after_reply(server, app) 15 | end 16 | RackAfterReply.freedom_patch self, :initialize 17 | 18 | def service_with_rack_after_reply(request, response) 19 | response.extend ResponseExtension 20 | response.rack_after_reply_handler = self 21 | service_without_rack_after_reply(request, response) 22 | end 23 | RackAfterReply.freedom_patch self, :service 24 | end 25 | end 26 | 27 | module ResponseExtension 28 | def send_response(socket) 29 | socket.extend SocketExtension 30 | socket.rack_after_reply_handler = rack_after_reply_handler 31 | super 32 | end 33 | 34 | attr_accessor :rack_after_reply_handler 35 | end 36 | 37 | module SocketExtension 38 | def close 39 | super 40 | rack_after_reply_handler.fire_rack_after_reply 41 | end 42 | 43 | attr_accessor :rack_after_reply_handler 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/rack_after_reply.rb: -------------------------------------------------------------------------------- 1 | require 'rack' 2 | 3 | module RackAfterReply 4 | CALLBACKS_KEY = 'rack_after_reply.callbacks'.freeze 5 | 6 | autoload :AppProxy, 'rack_after_reply/app_proxy' 7 | autoload :Adapter, 'rack_after_reply/adapter' 8 | autoload :RequestHandler, 'rack_after_reply/request_handler' 9 | 10 | class << self 11 | # 12 | # Apply extensions for all loaded web servers. 13 | # 14 | def apply 15 | Adapter::Thin.apply if defined?(::Thin) 16 | Adapter::Mongrel.apply if defined?(::Mongrel) 17 | Adapter::Passenger.apply if defined?(::PhusionPassenger) 18 | Adapter::WEBrick.apply if defined?(::WEBrick) 19 | Adapter::Unicorn.apply if defined?(::Unicorn) 20 | end 21 | 22 | def freedom_patch(mod, method) # :nodoc: 23 | # Prevent infinite recursion if we've already done it. 24 | return if mod.method_defined?("#{method}_without_rack_after_reply") 25 | 26 | mod.module_eval do 27 | alias_method "#{method}_without_rack_after_reply", method 28 | alias_method method, "#{method}_with_rack_after_reply" 29 | end 30 | end 31 | 32 | def freedom_extend(object, method) # :nodoc: 33 | klass = (class << object; self; end) 34 | freedom_patch(klass, method) 35 | end 36 | end 37 | end 38 | 39 | RackAfterReply.apply 40 | 41 | # The web server library may not be loaded until we've instantiated the Rack 42 | # handler (e.g., Rails 3.0's console command when no server argument is given), 43 | # so call apply once we know that has happened too. 44 | Rack::Server.class_eval do 45 | def server_with_rack_after_reply 46 | result = server_without_rack_after_reply 47 | RackAfterReply.apply 48 | result 49 | end 50 | 51 | RackAfterReply.freedom_patch(self, :server) 52 | end 53 | --------------------------------------------------------------------------------