async-rack 0.4.0.b

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,128 @@
1
+ # AsyncRack
2
+
3
+ So, have you been amazed by thin's `async.callback`? If not, [go](http://macournoyer.com/blog/2009/06/04/pusher-and-async-with-thin/) [check](http://github.com/raggi/async_sinatra) [it](http://github.com/raggi/thin/blob/async_for_rack/example/async_app.ru) [out](http://m.onkey.org/2010/1/7/introducing-cramp). Come back here when you start missing your middleware.
4
+
5
+ So what is the issue with Rack and `async.callback`? Currently there are two ways of triggering a async responds. The first is to `throw :async`, the latter to return a status code of -1 (even though thin and ebb do disagree on that). Opposed to what others say, I would recommend using `throw`, as it simply skips middleware not able to handle `:async`. Also, it works on all servers supporting `async.callback` – thin, ebb, rainbows! and zbatery – about the same and copes better with middleware that is unable to handle an async respond.
6
+
7
+ That's the issue with async.callback: Most middleware is not aware of it. Let's say you got an app somewhat like that:
8
+
9
+ class Farnsworth
10
+ def when_there_is_good_news
11
+ Thread.new do # Well, actually, you want to hook into your event loop instead, I guess.
12
+ wait_for_good_news
13
+ yield
14
+ end
15
+ end
16
+
17
+ def call(env)
18
+ when_there_is_good_news do
19
+ env["async.callback"].call [200, {'Content-Type' => 'text/plain'}, ['Good news, everyone!']]
20
+ end
21
+ throw :async
22
+ end
23
+ end
24
+
25
+ Ok, now, since this app could end up on Reddit, you better prepare yourself for some heavy traffic. Say, you want to use the `Rack::Deflate` middleware, so you set it up in your config.ru and add the link to reddit yourself. The next day you get a call from your server admin. Why don't you at least compress your http response? Well what happened? The problem is, that by sending your response via `env["async.callback"].call` you talk directly to your web server (i.e. thin), bypassing all potential middleware.
26
+
27
+ Well, how do you avoid that? Simple: By just using middleware that plays well with `async.callback`. However, most middleware does not play well with it. In fact, most middleware that ships with rack does not play well with it. That's what I wrote this little library for. If you load `async-rack` it modifies all middleware that ships with rack, so it will work just fine with you throwing around your :async.
28
+
29
+ How does that work? Simple, whenever necessary, `async-rack` will replace `async.callback` with an appropriate proc object, so it has the chance to do it's response modifications whenever you feel like answering the http request.
30
+
31
+ Note: This library only 'fixes' the middleware that ships with rack, not other rack middleware. However, you can use the included helper classes to easily make other libraries handle `async.callback`.
32
+
33
+ ## How to make a middleware async-proof?
34
+ There are three types of middleware:
35
+
36
+ ### Middleware doing stuff before handing on the request
37
+ Example: `Rack:::MethodOverride`
38
+
39
+ Such middleware already works fine with `async.callback`. Also, from our perspective, middleware either creating a own response and not calling your app at all, or calling your app without modifying neither request nor response falls into this category, too.
40
+
41
+ Such middleware can easily be identified by having `@app.call(env)` or something similar as last line or always prefixed with a `return` inside the `call` method.
42
+
43
+ ### Middleware doing stuff after handing on the request
44
+ Example: `Rack:::ETag`
45
+
46
+ Here it is a bit tricky. Essentially what you want is running `#call` again on an `async.callback` but replace `@app.call(env)` with the parameter passed to `async.callback`. Well, apparently this is the most common case inside rack, so I created a mixin for that:
47
+
48
+ # Ok, Rack::FancyStuff does currently not work with async responses
49
+ require 'rack/fancy_stuff'
50
+
51
+ class FixedFancyStuff < AsyncRack::AsyncCallback(:FancyStuff)
52
+ include AsyncRack::AsyncCallback::SimpleWrapper
53
+ end
54
+
55
+ See below to get an idea what actually happens here.
56
+
57
+ ### Middleware meddling with both your request and your response
58
+ Example: `Rack::Runtime`
59
+
60
+ # Let's assume there is some not so async middleware.
61
+ module Rack
62
+ class FancyStuff
63
+ def initialize(app)
64
+ @app = app
65
+ end
66
+
67
+ def call(env)
68
+ prepare_fancy_stuff env
69
+ result = @app.call env
70
+ perform_fancy_stuff result
71
+ end
72
+
73
+ def prepare_fancy_stuff(env)
74
+ # ...
75
+ end
76
+
77
+ def perform_fancy_stuff(result)
78
+ # ...
79
+ end
80
+ end
81
+ end
82
+
83
+ # What happens here is the following: We will subclass Rack::FancyStuff
84
+ # and then set Rack::FancyStuff = FixedFancyStuff. AsyncRack::AsyncCallback
85
+ # makes sure we don't screw that up.
86
+ class FixedFancyStuff < AsyncRack::AsyncCallback(:FancyStuff)
87
+ # this method will handle async.callback
88
+ def async_callback(result)
89
+ # pass it on to thin / ebb / other middleware
90
+ super perform_fancy_stuff(result)
91
+ end
92
+ end
93
+
94
+ Rack::FancyStuff == FixedFancyStuff # => true
95
+
96
+ ## Setup
97
+ In general: Place a `require 'async-rack'` before setting up any middleware or you will end up with the synchronous version!
98
+
99
+ Please keep in mind that it only "fixes" middleware that ships with rack. Read: It works very well with Sinatra. With Rails and Merb, not so much!
100
+
101
+ ### With Rack
102
+ In your `config.ru`:
103
+
104
+ require 'async-rack'
105
+ require 'your-app'
106
+
107
+ use Rack::SomeMiddleware
108
+ run YourApp
109
+
110
+ ### With Sinatra
111
+ In your application file:
112
+
113
+ require 'async-rack'
114
+ require 'sinatra'
115
+
116
+ get '/' do
117
+ # do some async stuff here
118
+ end
119
+
120
+ ### With Rails 2.x
121
+ In your `config/environment.rb`, add inside the `Rails::Initializer.run` block:
122
+
123
+ config.gem 'async-rack'
124
+
125
+ ### With Rails 3.x
126
+ In your `Gemfile`, add:
127
+
128
+ gem 'async-rack'
@@ -0,0 +1 @@
1
+ require "async_rack"
@@ -0,0 +1,46 @@
1
+ require "rack"
2
+ require "async_rack/async_callback"
3
+
4
+ module AsyncRack
5
+ module BaseMixin
6
+ ::Rack.extend self
7
+ ::Rack::Session.extend self
8
+ def autoload(class_name, path)
9
+ super unless autoload?(class_name) =~ /async_rack/
10
+ end
11
+ end
12
+
13
+ module ExtensionMixin
14
+ ::AsyncRack.extend self
15
+ def autoload(class_name, path)
16
+ super
17
+ if Rack.autoload? class_name then Rack.autoload(class_name, path)
18
+ elsif Rack.const_defined? class_name then require path
19
+ end
20
+ end
21
+ end
22
+
23
+ # Wrapped rack middleware
24
+ autoload :Chunked, "async_rack/chunked"
25
+ autoload :CommonLogger, "async_rack/commonlogger"
26
+ autoload :ConditionalGet, "async_rack/conditionalget"
27
+ autoload :ContentLength, "async_rack/content_length"
28
+ autoload :ContentType, "async_rack/content_type"
29
+ autoload :Deflater, "async_rack/deflater"
30
+ autoload :ETag, "async_rack/etag"
31
+ autoload :Head, "async_rack/head"
32
+ autoload :Lock, "async_rack/lock"
33
+ autoload :Logger, "async_rack/logger"
34
+ # autoload :Recursive, "async_rack/recursive"
35
+ autoload :Runtime, "async_rack/runtime"
36
+ autoload :Sendfile, "async_rack/sendfile"
37
+ autoload :ShowStatus, "async_rack/showstatus"
38
+
39
+ module Session
40
+ extend ExtensionMixin
41
+ autoload :Cookie, "async_rack/session/cookie"
42
+ autoload :Pool, "async_rack/session/pool"
43
+ autoload :Memcache, "async_rack/session/memcache"
44
+ end
45
+
46
+ end
@@ -0,0 +1,106 @@
1
+ module AsyncRack
2
+
3
+ ##
4
+ # @see AsyncRack::AsyncCallback
5
+ def self.AsyncCallback(name, namespace = Rack)
6
+ @wrapped ||= Hash.new { |h,k| h[k] = {} }
7
+ @wrapped[namespace][name.to_sym] ||= namespace.const_get(name).tap do |klass|
8
+ klass.extend AsyncCallback::InheritanceHook
9
+ klass.alias_subclass name, namespace
10
+ end
11
+ end
12
+
13
+ ##
14
+ # Helps wrapping already existent middleware in a transparent manner.
15
+ #
16
+ # @example
17
+ # module Rack
18
+ # class FancyMiddleware
19
+ # end
20
+ # end
21
+ #
22
+ # module AsyncRack
23
+ # class FancyMiddleware < AsyncCallback(:FancyMiddleware)
24
+ # end
25
+ # end
26
+ #
27
+ # Rack::FancyMiddleware # => AsyncRack::FancyMiddleware
28
+ # AsyncRack::FancyMiddleware.ancestors # => [AsyncRack::AsyncCallback::Mixin, Rack::FancyMiddleware, ...]
29
+ module AsyncCallback
30
+
31
+ ##
32
+ # Aliases a subclass on subclassing, but only once.
33
+ # If that name already is in use, it will be replaced.
34
+ #
35
+ # @example
36
+ # class Foo
37
+ # def self.bar
38
+ # 23
39
+ # end
40
+ # end
41
+ #
42
+ # Foo.extend AsyncRack::AsyncCallback::InheritanceHook
43
+ # Foo.alias_subclass :Baz
44
+ #
45
+ # class Bar < Foo
46
+ # def self.bar
47
+ # super + 19
48
+ # end
49
+ # end
50
+ #
51
+ # Baz.bar # => 42
52
+ module InheritanceHook
53
+
54
+ ##
55
+ # @param [Symbol] name Name it will be aliased to
56
+ # @param [Class, Module] namespace The module the constant will be defined in
57
+ def alias_subclass(name, namespace = Object)
58
+ @alias_subclass = [name, namespace]
59
+ end
60
+
61
+ ##
62
+ # @see InheritanceHook
63
+ def inherited(klass)
64
+ super
65
+ if @alias_subclass
66
+ name, namespace = @alias_subclass
67
+ @alias_subclass = nil
68
+ namespace.send :remove_const, name if namespace.const_defined? name
69
+ namespace.const_set name, klass
70
+ klass.send :include, AsyncRack::AsyncCallback::Mixin
71
+ end
72
+ end
73
+ end
74
+
75
+ module Mixin
76
+ def async_callback(result)
77
+ @async_callback.call result
78
+ end
79
+
80
+ def setup_async(env)
81
+ return false if @async_callback
82
+ @async_callback = env['async.callback']
83
+ env['async.callback'] = method :async_callback
84
+ @env = env
85
+ end
86
+
87
+ def call(env)
88
+ setup_async env
89
+ super
90
+ end
91
+ end
92
+
93
+ ##
94
+ # A simple wrapper is useful if the first thing a middleware does is something like
95
+ # @app.call and then modifies the response.
96
+ #
97
+ # In that case you just have to include SimpleWrapper in your async wrapper class.
98
+ module SimpleWrapper
99
+ include AsyncRack::AsyncCallback::Mixin
100
+ def async_callback(result)
101
+ @app = proc { result }
102
+ super call(@env)
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,7 @@
1
+ require "rack/chunked"
2
+
3
+ module AsyncRack
4
+ class Chunked < AsyncCallback(:Chunked)
5
+ include AsyncRack::AsyncCallback::SimpleWrapper
6
+ end
7
+ end
@@ -0,0 +1,17 @@
1
+ require "rack/commonlogger"
2
+
3
+ module AsyncRack
4
+ class CommonLogger < AsyncCallback(:CommonLogger)
5
+ def async_callback(result)
6
+ status, header, body = result
7
+ header = Rack::Utils::HeaderHash.new header
8
+ log env, status, header, @began_at
9
+ super [status, header, body]
10
+ end
11
+
12
+ def call(env)
13
+ @began_at = Time.now
14
+ super
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,7 @@
1
+ require "rack/conditionalget"
2
+
3
+ module AsyncRack
4
+ class ConditionalGet < AsyncCallback(:ConditionalGet)
5
+ include AsyncRack::AsyncCallback::SimpleWrapper
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require "rack/content_length"
2
+
3
+ module AsyncRack
4
+ class ContentLength < AsyncCallback(:ContentLength)
5
+ include AsyncRack::AsyncCallback::SimpleWrapper
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require "rack/content_type"
2
+
3
+ module AsyncRack
4
+ class ContentType < AsyncCallback(:ContentType)
5
+ include AsyncRack::AsyncCallback::SimpleWrapper
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require "rack/deflater"
2
+
3
+ module AsyncRack
4
+ class Deflater < AsyncCallback(:Deflater)
5
+ include AsyncRack::AsyncCallback::SimpleWrapper
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require "rack/etag"
2
+
3
+ module AsyncRack
4
+ class ETag < AsyncCallback(:ETag)
5
+ include AsyncRack::AsyncCallback::SimpleWrapper
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require "rack/head"
2
+
3
+ module AsyncRack
4
+ class Head < AsyncCallback(:Head)
5
+ include AsyncRack::AsyncCallback::SimpleWrapper
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ require "rack/lock"
2
+
3
+ module AsyncRack
4
+ class Lock < AsyncCallback(:Lock)
5
+ def async_callback(result)
6
+ raise RuntimeError, "does not support async.callback"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,21 @@
1
+ require "rack/logger"
2
+
3
+ module AsyncRack
4
+ class Logger < AsyncCallback(:Logger)
5
+ def async_callback(result)
6
+ @logger.close
7
+ super
8
+ end
9
+
10
+ def call(env)
11
+ @logger = ::Logger.new(env['rack.errors'])
12
+ @logger.level = @level
13
+ env['rack.logger'] = @logger
14
+ @app.call(env) # could throw :async
15
+ @logger.close
16
+ rescue Exception => error # does not get triggered by throwing :async (ensure does)
17
+ @logger.close
18
+ raise error
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ require "rack/runtime"
2
+
3
+ module AsyncRack
4
+ class Runtime < AsyncCallback(:Runtime)
5
+ def async_callback(result)
6
+ status, headers, body =result
7
+ request_time = Time.now - start_time
8
+ headers[@header_name] = "%0.6f" % request_time if !headers.has_key?(@header_name)
9
+ [status, headers, body]
10
+ end
11
+
12
+ def call(env)
13
+ @start_time = Time.now
14
+ super
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,7 @@
1
+ require "rack/sendfile"
2
+
3
+ module AsyncRack
4
+ class Sendfile < AsyncCallback(:Sendfile)
5
+ include AsyncRack::AsyncCallback::SimpleWrapper
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ require "rack/cookie"
2
+
3
+ module AsyncRack
4
+ module Session
5
+ class Cookie < AsyncCallback(:Cookie, Rack::Session)
6
+ def async_callback(result)
7
+ super commit_session(@env, *result)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ require "rack/memcache"
2
+
3
+ module AsyncRack
4
+ module Session
5
+ class Memcache < AsyncCallback(:Memcache, Rack::Session)
6
+ def async_callback(result)
7
+ super commit_session(@env, *result)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ require "rack/pool"
2
+
3
+ module AsyncRack
4
+ module Session
5
+ class Pool < AsyncCallback(:Pool, Rack::Session)
6
+ def async_callback(result)
7
+ super commit_session(@env, *result)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ require "rack/showstatus"
2
+
3
+ module AsyncRack
4
+ class ShowStatus < AsyncCallback(:ShowStatus)
5
+ include AsyncRack::AsyncCallback::SimpleWrapper
6
+ end
7
+ end
@@ -0,0 +1,55 @@
1
+ require File.expand_path("../../spec_helper", __FILE__)
2
+
3
+ describe AsyncRack::AsyncCallback do
4
+ before do
5
+ @namespace = Module.new
6
+ @namespace.const_set :Foo, Class.new
7
+ end
8
+
9
+ it "wraps rack middleware by replacing it" do
10
+ non_async_middleware = @namespace::Foo
11
+ @namespace::Foo.should == non_async_middleware
12
+ async_middleware = Class.new AsyncRack::AsyncCallback(:Foo, @namespace)
13
+ @namespace::Foo.should_not == non_async_middleware
14
+ @namespace::Foo.should == async_middleware
15
+ AsyncRack::AsyncCallback(:Foo, @namespace).should == non_async_middleware
16
+ end
17
+
18
+ describe :InheritanceHook do
19
+ it "alows aliasing subclasses automatically" do
20
+ @namespace::Foo.extend AsyncRack::AsyncCallback::InheritanceHook
21
+ @namespace::Foo.alias_subclass :Bar, @namespace
22
+ subclass = Class.new(@namespace::Foo)
23
+ @namespace::Bar.should == subclass
24
+ end
25
+ end
26
+
27
+ describe :SimpleWrapper do
28
+ it "runs #call again on async callback, replacing app" do
29
+ klass = Class.new do
30
+ include AsyncRack::AsyncCallback::SimpleWrapper
31
+ attr_accessor :app, :env
32
+ def call(env)
33
+ setup_async env
34
+ @app.call(env) + 5
35
+ end
36
+ end
37
+ middleware = klass.new
38
+ middleware.app = proc { throw :async }
39
+ catch(:async) do
40
+ middleware.call "async.callback" => proc { |x| x + 10 }
41
+ raise "should not get here"
42
+ end
43
+ middleware.env["async.callback"].call(0).should == 15
44
+ end
45
+ end
46
+
47
+ describe :Mixin do
48
+ it "wrapps async.callback" do
49
+ @middleware = proc { |env| env['async.callback'].call [200, {'Content-Type' => 'text/plain'}, ['OK']] }
50
+ @middleware.extend AsyncRack::AsyncCallback::Mixin
51
+ @middleware.should_receive(:async_callback)
52
+ @middleware.call "async.callback" => proc { }
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
2
+ require "async-rack"
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: async-rack
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: true
5
+ segments:
6
+ - 0
7
+ - 4
8
+ - 0
9
+ - b
10
+ version: 0.4.0.b
11
+ platform: ruby
12
+ authors:
13
+ - Konstantin Haase
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-03-09 00:00:00 +01:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rack
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 1
30
+ - 1
31
+ - 0
32
+ version: 1.1.0
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: rspec
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ segments:
43
+ - 1
44
+ - 3
45
+ - 0
46
+ version: 1.3.0
47
+ type: :development
48
+ version_requirements: *id002
49
+ description: Makes middleware that ships with Rack bullet-proof for async responses.
50
+ email: konstantin.mailinglists@googlemail.com
51
+ executables: []
52
+
53
+ extensions: []
54
+
55
+ extra_rdoc_files: []
56
+
57
+ files:
58
+ - lib/async-rack.rb
59
+ - lib/async_rack/async_callback.rb
60
+ - lib/async_rack/chunked.rb
61
+ - lib/async_rack/commonlogger.rb
62
+ - lib/async_rack/conditionalget.rb
63
+ - lib/async_rack/content_length.rb
64
+ - lib/async_rack/content_type.rb
65
+ - lib/async_rack/deflater.rb
66
+ - lib/async_rack/etag.rb
67
+ - lib/async_rack/head.rb
68
+ - lib/async_rack/lock.rb
69
+ - lib/async_rack/logger.rb
70
+ - lib/async_rack/runtime.rb
71
+ - lib/async_rack/sendfile.rb
72
+ - lib/async_rack/session/cookie.rb
73
+ - lib/async_rack/session/memcache.rb
74
+ - lib/async_rack/session/pool.rb
75
+ - lib/async_rack/showstatus.rb
76
+ - lib/async_rack.rb
77
+ - spec/async_rack/async_callback_spec.rb
78
+ - spec/spec_helper.rb
79
+ - README.md
80
+ has_rdoc: yard
81
+ homepage: http://github.com/rkh/async-rack
82
+ licenses: []
83
+
84
+ post_install_message:
85
+ rdoc_options: []
86
+
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ segments:
94
+ - 0
95
+ version: "0"
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">"
99
+ - !ruby/object:Gem::Version
100
+ segments:
101
+ - 1
102
+ - 3
103
+ - 1
104
+ version: 1.3.1
105
+ requirements: []
106
+
107
+ rubyforge_project:
108
+ rubygems_version: 1.3.6
109
+ signing_key:
110
+ specification_version: 3
111
+ summary: Makes middleware that ships with Rack bullet-proof for async responses.
112
+ test_files: []
113
+