async_sinatra 0.1.5 → 0.2.0

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.
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