async_sinatra 0.1.5 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -7,11 +7,17 @@
7
7
  == DESCRIPTION:
8
8
 
9
9
  A Sinatra plugin to provide convenience whilst performing asynchronous
10
- responses inside of the Sinatra framework running under the Thin webserver.
10
+ responses inside of the Sinatra framework running under async webservers.
11
11
 
12
12
  To properly utilise this package, some knowledge of EventMachine and/or
13
13
  asynchronous patterns is recommended.
14
14
 
15
+ Currently, supporting servers include:
16
+
17
+ * Thin
18
+ * Rainbows
19
+ * Zbatery
20
+
15
21
  == SYNOPSIS:
16
22
 
17
23
  A quick example:
@@ -36,7 +42,7 @@ See Sinatra::Async for more details.
36
42
  == REQUIREMENTS:
37
43
 
38
44
  * Sinatra (>= 0.9)
39
- * Thin (>= 1.2)
45
+ * An async webserver
40
46
 
41
47
  == INSTALL:
42
48
 
data/Rakefile CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env rake
2
2
  require 'rake/clean'
3
3
 
4
- task :default => :spec
4
+ task :default => :test
5
5
 
6
6
  def spec(file = Dir['*.gemspec'].first)
7
7
  @spec ||=
@@ -19,27 +19,25 @@ end
19
19
 
20
20
  def manifest; @manifest ||= `git ls-files`.split("\n").reject{|s|s=~/\.gemspec$|\.gitignore$/}; end
21
21
 
22
- require 'rake/gempackagetask'
23
- def gem_task; @gem_task ||= Rake::GemPackageTask.new(spec); end
22
+ require 'rubygems/package_task'
23
+ def gem_task; @gem_task ||= Gem::PackageTask.new(spec); end
24
24
  gem_task.define
25
25
  Rake::Task[:clobber].enhance [:clobber_package]
26
26
 
27
27
  require 'rake/testtask'
28
- Rake::TestTask.new(:spec) do |t|
28
+ Rake::TestTask.new do |t|
29
29
  t.test_files = spec.test_files
30
- t.ruby_opts = ['-rubygems'] if defined? Gem
30
+ t.ruby_opts = ['-rubygems']
31
31
  t.warning = true
32
32
  end unless spec.test_files.empty?
33
33
 
34
- require 'rake/rdoctask'
35
- df = begin; require 'rdoc/generator/darkfish'; true; rescue LoadError; end
36
- rdtask = Rake::RDocTask.new do |rd|
34
+ require 'rdoc/task'
35
+ rdtask = RDoc::Task.new do |rd|
37
36
  rd.title = spec.name
38
37
  rd.main = spec.extra_rdoc_files.first
39
38
  lib_rexp = spec.require_paths.map { |p| Regexp.escape p }.join('|')
40
39
  rd.rdoc_files.include(*manifest.grep(/^(?:#{lib_rexp})/))
41
40
  rd.rdoc_files.include(*spec.extra_rdoc_files)
42
- rd.template = 'darkfish' if df
43
41
  end
44
42
 
45
43
  Rake::Task[:clobber].enhance [:clobber_rdoc]
@@ -70,7 +68,7 @@ task :gemspec => spec.filename
70
68
 
71
69
  task spec.filename do
72
70
  spec.files = manifest
73
- spec.test_files = manifest.grep(/(?:spec|test)\/*.rb/)
71
+ spec.test_files = manifest.grep(%r{test/test_.*\.rb})
74
72
  open(spec.filename, 'w') { |w| w.write spec.to_ruby }
75
73
  end
76
74
 
@@ -90,7 +88,8 @@ task :tag do
90
88
  end
91
89
  end
92
90
 
93
- desc "Release #{gem_task.gem_file} to rubyforge"
91
+ desc "Release #{gem_task.gem_spec.file_name}"
94
92
  task :release => [:tag, :gem, :publish] do |t|
95
- sh "rubyforge add_release #{spec.rubyforge_project} #{spec.name} #{spec.version} #{gem_task.package_dir}/#{gem_task.gem_file}"
93
+ sh "rubyforge add_release #{spec.rubyforge_project} #{spec.name} #{spec.version} #{gem_task.package_dir}/#{gem_task.gem_spec.file_name}"
94
+ sh "gem push #{gem_task.package_dir}/#{gem_task.gem_spec.file_name}"
96
95
  end
data/examples/basic.ru CHANGED
@@ -18,6 +18,15 @@ class AsyncTest < Sinatra::Base
18
18
  raise 'boom'
19
19
  end
20
20
 
21
+ aget '/araise' do
22
+ EM.add_timer(1) { body { raise "boom" } }
23
+ end
24
+
25
+ # This will blow up in thin currently
26
+ aget '/raise/die' do
27
+ EM.add_timer(1) { raise 'die' }
28
+ end
29
+
21
30
  end
22
31
 
23
32
  run AsyncTest.new
data/lib/sinatra/async.rb CHANGED
@@ -35,6 +35,7 @@ module Sinatra #:nodoc:
35
35
  #
36
36
  # end
37
37
  module Async
38
+
38
39
  # Similar to Sinatra::Base#get, but the block will be scheduled to run
39
40
  # during the next tick of the EventMachine reactor. In the meantime,
40
41
  # Thin will hold onto the client connection, awaiting a call to
@@ -58,43 +59,79 @@ module Sinatra #:nodoc:
58
59
 
59
60
  private
60
61
  def aroute(verb, path, opts = {}, &block) #:nodoc:
62
+ method = "A#{verb} #{path}".to_sym
63
+ define_method method, &block
64
+
61
65
  route(verb, path, opts) do |*bargs|
62
- method = "A#{verb} #{path}".to_sym
66
+ async_runner(method, *bargs)
67
+ async_response
68
+ end
69
+ end
63
70
 
64
- mc = class << self; self; end
65
- mc.send :define_method, method, &block
66
- mc.send :alias_method, :__async_callback, method
71
+ module Helpers
72
+ # Send the given body or block as the final response to the asynchronous
73
+ # request.
74
+ def body(*args)
75
+ if @async_running
76
+ block_given? ? async_handle_exception { super yield } : super
77
+ request.env['async.callback'][
78
+ [response.status, response.headers, response.body]
79
+ ]
80
+ else
81
+ super
82
+ end
83
+ end
67
84
 
68
- EM.next_tick {
69
- begin
70
- send(:__async_callback, *bargs)
71
- rescue ::Exception => boom
72
- if options.show_exceptions?
73
- # HACK: handle_exception! re-raises the exception if show_exceptions?,
74
- # so we ignore any errors and instead create a ShowExceptions page manually
75
- handle_exception!(boom) rescue nil
76
- s, h, b = Sinatra::ShowExceptions.new(proc{ raise boom }).call(request.env)
77
- response.status = s
78
- response.headers.replace(h)
79
- body(b)
80
- else
81
- body(handle_exception!(boom))
85
+ # By default async_schedule calls EventMachine#next_tick, if you're using
86
+ # threads or some other scheduling mechanism, it must take the block
87
+ # passed here.
88
+ def async_schedule(&b)
89
+ if options.environment == :test
90
+ options.set :async_schedules, [] unless options.respond_to? :async_schedules
91
+ options.async_schedules << b
92
+ else
93
+ EM.next_tick(&b)
94
+ end
95
+ end
96
+
97
+ # Defaults to throw async as that is most commonly used by servers.
98
+ def async_response
99
+ throw :async
100
+ end
101
+
102
+ def async_runner(method, *bargs)
103
+ async_schedule do
104
+ @async_running = true
105
+ async_handle_exception do
106
+ if h = catch(:halt) { __send__(method, *bargs); nil }
107
+ invoke { halt h }
108
+ invoke { error_block! response.status }
109
+ body(response.body)
82
110
  end
83
111
  end
84
- }
112
+ end
113
+ end
85
114
 
86
- throw :async
115
+ def async_handle_exception
116
+ yield
117
+ rescue ::Exception => boom
118
+ if options.show_exceptions?
119
+ printer = Sinatra::ShowExceptions.new(proc{ raise boom })
120
+ s, h, b = printer.call(request.env)
121
+ response.status = s
122
+ response.headers.replace(h)
123
+ response.body = b
124
+ else
125
+ body(handle_exception!(boom))
126
+ end
87
127
  end
88
- end
89
128
 
90
- module Helpers
91
- # Send the given body or block as the final response to the asynchronous
92
- # request.
93
- def body(*args, &blk)
94
- super
95
- request.env['async.callback'][
96
- [response.status, response.headers, response.body]
97
- ] if respond_to?(:__async_callback)
129
+ # Asynchronous halt must be used when the halt is occuring outside of
130
+ # the original call stack.
131
+ def ahalt(*args)
132
+ invoke { halt(*args) }
133
+ invoke { error_block! response.status }
134
+ body response.body
98
135
  end
99
136
  end
100
137
 
@@ -0,0 +1,67 @@
1
+ require 'sinatra/async'
2
+ require 'rack/test'
3
+
4
+ class Rack::MockResponse
5
+ def async?
6
+ self.status == -1
7
+ end
8
+ end
9
+
10
+ class Sinatra::Async::Test
11
+ class AsyncSession < Rack::MockSession
12
+ def request(uri, env)
13
+ env['async.callback'] = lambda { |r| s,h,b = *r; handle_last_response(uri, env, s,h,b) }
14
+ env['async.close'] = lambda { raise 'close connection' } # XXX deal with this
15
+ catch(:async) { super }
16
+ @last_response ||= Rack::MockResponse.new(-1, {}, [], env["rack.errors"].flush)
17
+ end
18
+
19
+ def handle_last_response(uri, env, status, headers, body)
20
+ @last_response = Rack::MockResponse.new(status, headers, body, env["rack.errors"].flush)
21
+ body.close if body.respond_to?(:close)
22
+
23
+ cookie_jar.merge(last_response.headers["Set-Cookie"], uri)
24
+
25
+ @after_request.each { |hook| hook.call }
26
+ @last_response
27
+ end
28
+ end
29
+
30
+ module Methods
31
+ include Rack::Test::Methods
32
+
33
+ %w(get put post delete head).each do |m|
34
+ eval <<-RUBY, binding, __FILE__, __LINE__ + 1
35
+ def a#{m}(*args)
36
+ #{m}(*args)
37
+ assert_async
38
+ async_continue
39
+ end
40
+ RUBY
41
+ end
42
+
43
+ def build_rack_mock_session # XXX move me
44
+ Sinatra::Async::Test::AsyncSession.new(app)
45
+ end
46
+
47
+ def assert_async
48
+ assert last_response.async?
49
+ end
50
+
51
+ def async_continue
52
+ while b = app.options.async_schedules.shift
53
+ b.call
54
+ end
55
+ end
56
+
57
+ def em_async_continue(timeout = 10)
58
+ timed = false
59
+ EM.run do
60
+ async_continue
61
+ EM.tick_loop { EM.stop unless last_response.async? }
62
+ EM.add_timer(timeout) { timed = true; EM.stop }
63
+ end
64
+ assert !timed, "asynchronous timeout after #{timeout} seconds"
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,161 @@
1
+ require 'eventmachine'
2
+ require "sinatra/async/test"
3
+
4
+ require 'spec/autorun'
5
+ require 'spec/interop/test'
6
+
7
+ require 'em-http'
8
+
9
+ Spec::Runner.configure { |c| }
10
+
11
+ def server(base=Sinatra::Base, &block)
12
+ s = Sinatra.new(base, &block).new
13
+ s.options.set :environment, :test
14
+ s
15
+ end
16
+
17
+ def app
18
+ @app
19
+ end
20
+
21
+ include Sinatra::Async::Test::Methods
22
+
23
+ describe "Asynchronous routes" do
24
+ it "should still work as usual" do
25
+ @app = server do
26
+ register Sinatra::Async
27
+ disable :raise_errors, :show_exceptions
28
+
29
+ aget '/' do
30
+ body "hello async"
31
+ end
32
+ end
33
+ aget '/'
34
+ last_response.status.should_equal 200
35
+ last_response.body.should == "hello async"
36
+ end
37
+
38
+ it "should correctly deal with raised exceptions" do
39
+ @app = server do
40
+ register Sinatra::Async
41
+ disable :raise_errors, :show_exceptions
42
+ aget '/' do
43
+ raise "boom"
44
+ body "hello async"
45
+ end
46
+ error Exception do
47
+ e = request.env['sinatra.error']
48
+ "problem: #{e.class.name} #{e.message}"
49
+ end
50
+ end
51
+ aget '/'
52
+ last_response.status.should == 500
53
+ last_response.body.should == "problem: RuntimeError boom"
54
+ end
55
+
56
+ it "should correctly deal with halts" do
57
+ @app = server do
58
+ register Sinatra::Async
59
+ disable :raise_errors, :show_exceptions
60
+ aget '/' do
61
+ halt 406, "Format not supported"
62
+ body "never called"
63
+ end
64
+ end
65
+
66
+ aget '/'
67
+ last_response.status.should == 406
68
+ last_response.body.should == "Format not supported"
69
+ end
70
+
71
+ it "should correctly deal with halts and pass it to the defined error blocks if any" do
72
+ @app = server do
73
+ register Sinatra::Async
74
+ disable :raise_errors, :show_exceptions
75
+ aget '/' do
76
+ halt 406, "Format not supported"
77
+ body "never called"
78
+ end
79
+ error 406 do
80
+ response['Content-Type'] = "text/plain"
81
+ "problem: #{response.body.to_s}"
82
+ end
83
+ end
84
+ aget '/'
85
+ last_response.status.should == 406
86
+ last_response.headers['Content-Type'].should == "text/plain"
87
+ last_response.body.should == "problem: Format not supported"
88
+ end
89
+
90
+ describe "using EM libraries inside route block" do
91
+ it "should still work as usual" do
92
+ @app = server do
93
+ register Sinatra::Async
94
+ disable :raise_errors, :show_exceptions
95
+ aget '/' do
96
+ url = "http://ruby.activeventure.com/programmingruby/book/tut_exceptions.html"
97
+ http = EM::HttpRequest.new(url).get
98
+ http.callback {
99
+ status http.response_header.status
100
+ body "ok"
101
+ }
102
+ http.errback {
103
+ body "nok"
104
+ }
105
+ end
106
+ end
107
+ EM.run { EM.add_timer(1) { EM.stop }; aget '/' }
108
+ last_response.status.should == 200
109
+ last_response.body.should == "ok"
110
+ end
111
+
112
+ it "should correctly deal with exceptions raised from within EM callbacks" do
113
+ @app = server do
114
+ register Sinatra::Async
115
+ disable :raise_errors, :show_exceptions
116
+ aget '/' do
117
+ url = "http://doesnotexist.local/whatever"
118
+ http = EM::HttpRequest.new(url).get
119
+ http.callback {
120
+ status http.response_header.status
121
+ body "ok"
122
+ }
123
+ http.errback {
124
+ raise "boom"
125
+ }
126
+ end
127
+ error Exception do
128
+ e = request.env['sinatra.error']
129
+ "#{e.class.name}: #{e.message}"
130
+ end
131
+ end
132
+ EM.run { EM.add_timer(1) { EM.stop }; aget '/' }
133
+ last_response.status.should == 500
134
+ last_response.body.should == "RuntimeError: boom"
135
+ end
136
+
137
+ it "should correctly deal with halts thrown from within EM callbacks" do
138
+ @app = server do
139
+ register Sinatra::Async
140
+ disable :raise_errors, :show_exceptions
141
+ aget '/' do
142
+ url = "http://doesnotexist.local/whatever"
143
+ http = EM::HttpRequest.new(url).get
144
+ http.callback {
145
+ status http.response_header.status
146
+ body "ok"
147
+ }
148
+ http.errback {
149
+ halt 503, "error: #{http.errors.inspect}"
150
+ }
151
+ end
152
+ error 503 do
153
+ "503: #{response.body.to_s}"
154
+ end
155
+ end
156
+ EM.run { EM.add_timer(1) { EM.stop }; aget '/' }
157
+ last_response.status.should == 503
158
+ last_response.body.should == "503: error: \"unable to resolve server address\""
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,116 @@
1
+ gem 'test-unit'
2
+ require "test/unit"
3
+
4
+ require 'eventmachine'
5
+
6
+ require "sinatra/async/test"
7
+
8
+ class TestSinatraAsync < Test::Unit::TestCase
9
+ include Sinatra::Async::Test::Methods
10
+
11
+ class TestApp < Sinatra::Base
12
+ set :environment, :test
13
+ register Sinatra::Async
14
+
15
+ error 401 do
16
+ '401'
17
+ end
18
+
19
+ aget '/hello' do
20
+ body { 'hello async' }
21
+ end
22
+
23
+ aget '/em' do
24
+ EM.add_timer(0.001) { body { 'em' }; EM.stop }
25
+ end
26
+
27
+ aget '/em_timeout' do
28
+ # never send a response
29
+ end
30
+
31
+ aget '/404' do
32
+ not_found
33
+ end
34
+
35
+ aget '/302' do
36
+ ahalt 302
37
+ end
38
+
39
+ aget '/em_halt' do
40
+ EM.next_tick { ahalt 404 }
41
+ end
42
+
43
+ aget '/s401' do
44
+ halt 401
45
+ end
46
+
47
+ aget '/a401' do
48
+ ahalt 401
49
+ end
50
+ end
51
+
52
+ def app
53
+ TestApp.new
54
+ end
55
+
56
+ def test_basic_async_get
57
+ get '/hello'
58
+ assert_async
59
+ async_continue
60
+ assert last_response.ok?
61
+ assert_equal 'hello async', last_response.body
62
+ end
63
+
64
+ def test_em_get
65
+ get '/em'
66
+ assert_async
67
+ em_async_continue
68
+ assert last_response.ok?
69
+ assert_equal 'em', last_response.body
70
+ end
71
+
72
+ def test_em_async_continue_timeout
73
+ get '/em_timeout'
74
+ assert_async
75
+ assert_raises(Test::Unit::AssertionFailedError) do
76
+ em_async_continue(0.001)
77
+ end
78
+ end
79
+
80
+ def test_404
81
+ get '/404'
82
+ assert_async
83
+ async_continue
84
+ assert_equal 404, last_response.status
85
+ end
86
+
87
+ def test_302
88
+ get '/302'
89
+ assert_async
90
+ async_continue
91
+ assert_equal 302, last_response.status
92
+ end
93
+
94
+ def test_em_halt
95
+ get '/em_halt'
96
+ assert_async
97
+ em_async_continue
98
+ assert_equal 404, last_response.status
99
+ end
100
+
101
+ def test_error_blocks_sync
102
+ get '/s401'
103
+ assert_async
104
+ async_continue
105
+ assert_equal 401, last_response.status
106
+ assert_equal '401', last_response.body
107
+ end
108
+
109
+ def test_error_blocks_async
110
+ get '/a401'
111
+ assert_async
112
+ async_continue
113
+ assert_equal 401, last_response.status
114
+ assert_equal '401', last_response.body
115
+ end
116
+ end
metadata CHANGED
@@ -1,7 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async_sinatra
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ hash: 23
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 2
9
+ - 0
10
+ version: 0.2.0
5
11
  platform: ruby
6
12
  authors:
7
13
  - James Tucker
@@ -9,50 +15,58 @@ autorequire:
9
15
  bindir: bin
10
16
  cert_chain: []
11
17
 
12
- date: 2009-03-24 00:00:00 +00:00
18
+ date: 2009-03-24 00:00:00 -03:00
13
19
  default_executable:
14
20
  dependencies:
15
21
  - !ruby/object:Gem::Dependency
16
22
  name: sinatra
17
- type: :runtime
18
- version_requirement:
19
- version_requirements: !ruby/object:Gem::Requirement
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
20
26
  requirements:
21
27
  - - ">="
22
28
  - !ruby/object:Gem::Version
29
+ hash: 57
30
+ segments:
31
+ - 0
32
+ - 9
33
+ - 1
23
34
  version: 0.9.1
24
- version:
25
- - !ruby/object:Gem::Dependency
26
- name: thin
27
35
  type: :runtime
28
- version_requirement:
29
- version_requirements: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: 1.2.0
34
- version:
36
+ version_requirements: *id001
35
37
  - !ruby/object:Gem::Dependency
36
38
  name: rdoc
37
- type: :development
38
- version_requirement:
39
- version_requirements: !ruby/object:Gem::Requirement
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
40
42
  requirements:
41
43
  - - ">="
42
44
  - !ruby/object:Gem::Version
45
+ hash: 29
46
+ segments:
47
+ - 2
48
+ - 4
49
+ - 1
43
50
  version: 2.4.1
44
- version:
51
+ type: :development
52
+ version_requirements: *id002
45
53
  - !ruby/object:Gem::Dependency
46
54
  name: rake
47
- type: :development
48
- version_requirement:
49
- version_requirements: !ruby/object:Gem::Requirement
55
+ prerelease: false
56
+ requirement: &id003 !ruby/object:Gem::Requirement
57
+ none: false
50
58
  requirements:
51
59
  - - ">="
52
60
  - !ruby/object:Gem::Version
61
+ hash: 57
62
+ segments:
63
+ - 0
64
+ - 8
65
+ - 3
53
66
  version: 0.8.3
54
- version:
55
- description: Asynchronous response API for Sinatra and Thin
67
+ type: :development
68
+ version_requirements: *id003
69
+ description: Asynchronous response API for Sinatra
56
70
  email: raggi@rubyforge.org
57
71
  executables: []
58
72
 
@@ -65,6 +79,9 @@ files:
65
79
  - Rakefile
66
80
  - examples/basic.ru
67
81
  - lib/sinatra/async.rb
82
+ - lib/sinatra/async/test.rb
83
+ - test/borked_test_crohr.rb
84
+ - test/test_async.rb
68
85
  has_rdoc: true
69
86
  homepage: http://libraggi.rubyforge.org/async_sinatra
70
87
  licenses: []
@@ -80,23 +97,29 @@ rdoc_options:
80
97
  require_paths:
81
98
  - lib
82
99
  required_ruby_version: !ruby/object:Gem::Requirement
100
+ none: false
83
101
  requirements:
84
102
  - - ">="
85
103
  - !ruby/object:Gem::Version
104
+ hash: 3
105
+ segments:
106
+ - 0
86
107
  version: "0"
87
- version:
88
108
  required_rubygems_version: !ruby/object:Gem::Requirement
109
+ none: false
89
110
  requirements:
90
111
  - - ">="
91
112
  - !ruby/object:Gem::Version
113
+ hash: 3
114
+ segments:
115
+ - 0
92
116
  version: "0"
93
- version:
94
117
  requirements: []
95
118
 
96
119
  rubyforge_project: libraggi
97
- rubygems_version: 1.3.5
120
+ rubygems_version: 1.3.7
98
121
  signing_key:
99
122
  specification_version: 2
100
- summary: Asynchronous response API for Sinatra and Thin
101
- test_files: []
102
-
123
+ summary: Asynchronous response API for Sinatra
124
+ test_files:
125
+ - test/test_async.rb