async-rack 0.4.0.b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+