goliath 0.9.2 → 0.9.4
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of goliath might be problematic. Click here for more details.
- data/Gemfile +1 -1
- data/{HISTORY → HISTORY.md} +26 -12
- data/README.md +17 -10
- data/examples/api_proxy.rb +28 -0
- data/examples/async_aroundware_demo.rb +14 -10
- data/examples/auth_and_rate_limit.rb +160 -38
- data/examples/config/auth_and_rate_limit.rb +8 -5
- data/examples/config/content_stream.rb +5 -9
- data/examples/early_abort.rb +37 -0
- data/examples/env_use_statements.rb +3 -0
- data/examples/favicon.rb +40 -0
- data/examples/http_log.rb +2 -1
- data/examples/public/favicon.ico +0 -0
- data/examples/rack_routes.rb +19 -0
- data/examples/rasterize/rasterize.rb +2 -1
- data/examples/rasterize/rasterize_and_shorten.rb +10 -5
- data/goliath.gemspec +7 -9
- data/lib/goliath/api.rb +16 -4
- data/lib/goliath/connection.rb +8 -7
- data/lib/goliath/deprecated/async_aroundware.rb +133 -0
- data/lib/goliath/{synchrony → deprecated}/mongo_receiver.rb +28 -8
- data/lib/goliath/deprecated/response_receiver.rb +97 -0
- data/lib/goliath/env.rb +5 -0
- data/lib/goliath/rack.rb +6 -1
- data/lib/goliath/rack/async_middleware.rb +34 -12
- data/lib/goliath/rack/barrier_aroundware.rb +228 -0
- data/lib/goliath/rack/barrier_aroundware_factory.rb +60 -0
- data/lib/goliath/rack/builder.rb +22 -6
- data/lib/goliath/rack/heartbeat.rb +8 -5
- data/lib/goliath/rack/simple_aroundware.rb +114 -0
- data/lib/goliath/rack/simple_aroundware_factory.rb +121 -0
- data/lib/goliath/rack/validation/required_param.rb +9 -2
- data/lib/goliath/request.rb +7 -0
- data/lib/goliath/runner.rb +17 -5
- data/lib/goliath/server.rb +11 -3
- data/lib/goliath/test_helper.rb +14 -14
- data/lib/goliath/version.rb +1 -1
- data/spec/integration/early_abort_spec.rb +50 -0
- data/spec/integration/keepalive_spec.rb +2 -2
- data/spec/integration/pipelining_spec.rb +2 -2
- data/spec/integration/rack_routes_spec.rb +25 -0
- data/spec/integration/template_spec.rb +2 -0
- data/spec/unit/rack/heartbeat_spec.rb +11 -1
- data/spec/unit/rack/validation/required_param_spec.rb +10 -0
- data/spec/unit/runner_spec.rb +13 -0
- data/spec/unit/server_spec.rb +4 -0
- metadata +218 -265
- data/lib/goliath/rack/async_aroundware.rb +0 -56
- data/lib/goliath/synchrony/response_receiver.rb +0 -64
@@ -11,20 +11,23 @@ environment(:development) do
|
|
11
11
|
timebin = ((Time.now.to_i / 3600).floor * 3600)
|
12
12
|
|
13
13
|
# This user's calls should all go through
|
14
|
-
config['api_auth_db'].collection(
|
14
|
+
config['api_auth_db'].collection(:account_info).save({
|
15
15
|
:_id => 'i_am_awesome', 'valid' => true, 'max_call_rate' => 1_000_000 })
|
16
16
|
|
17
17
|
# this user's account is disabled
|
18
|
-
config['api_auth_db'].collection(
|
18
|
+
config['api_auth_db'].collection(:account_info).save({
|
19
19
|
:_id => 'i_am_lame', 'valid' => false, 'max_call_rate' => 1_000 })
|
20
20
|
|
21
21
|
# this user has not been seen, but will very quickly hit their limit
|
22
|
-
config['api_auth_db'].collection(
|
22
|
+
config['api_auth_db'].collection(:account_info).save({
|
23
23
|
:_id => 'i_am_limited', 'valid' => true, 'max_call_rate' => 10 })
|
24
|
+
config['api_auth_db'].collection(:usage_info).save({
|
25
|
+
:_id => "i_am_limited-#{timebin}", 'calls' => 0 })
|
24
26
|
|
25
27
|
# fakes a user with a bunch of calls already made this hour -- two more = no yuo
|
26
|
-
config['api_auth_db'].collection(
|
28
|
+
config['api_auth_db'].collection(:account_info).save({
|
27
29
|
:_id => 'i_am_busy', 'valid' => true, 'max_call_rate' => 1_000 })
|
28
|
-
config['api_auth_db'].collection(
|
30
|
+
config['api_auth_db'].collection(:usage_info).save({
|
29
31
|
:_id => "i_am_busy-#{timebin}", 'calls' => 999 })
|
32
|
+
|
30
33
|
end
|
@@ -10,21 +10,17 @@ amqp_config = {
|
|
10
10
|
}
|
11
11
|
|
12
12
|
conn = AMQP.connect(amqp_config)
|
13
|
-
|
14
13
|
xchange = AMQP::Channel.new(conn).fanout('stream')
|
14
|
+
|
15
15
|
q = AMQP::Channel.new(conn).queue('stream/StreamAPI')
|
16
16
|
q.bind(xchange)
|
17
17
|
|
18
|
-
|
19
|
-
|
20
|
-
if data.nil?
|
21
|
-
EM.add_timer(1) { q.pop }
|
22
|
-
else
|
23
|
-
config['channel'].push(data)
|
24
|
-
q.pop
|
25
|
-
end
|
18
|
+
def handle_message(metadata, payload)
|
19
|
+
config['channel'].push(payload)
|
26
20
|
end
|
27
21
|
|
22
|
+
q.subscribe(&method(:handle_message))
|
23
|
+
|
28
24
|
# push data into the stream. (Just so we have stuff going in)
|
29
25
|
count = 0
|
30
26
|
EM.add_periodic_timer(2) do
|
@@ -0,0 +1,37 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$:<< '../lib' << 'lib'
|
3
|
+
require 'goliath'
|
4
|
+
|
5
|
+
class EarlyAbort < Goliath::API
|
6
|
+
include Goliath::Validation
|
7
|
+
MAX_SIZE = 10
|
8
|
+
TEST_FILE = "/tmp/goliath-test-error.log"
|
9
|
+
|
10
|
+
def on_headers(env, headers)
|
11
|
+
env.logger.info 'received headers: ' + headers.inspect
|
12
|
+
env['async-headers'] = headers
|
13
|
+
|
14
|
+
if env['HTTP_X_CRASH'] && env['HTTP_X_CRASH'] == 'true'
|
15
|
+
raise Goliath::Validation::NotImplementedError.new("Can't handle requests with X-Crash: true.")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def on_body(env, data)
|
20
|
+
env.logger.info 'received data: ' + data
|
21
|
+
(env['async-body'] ||= '') << data
|
22
|
+
size = env['async-body'].size
|
23
|
+
|
24
|
+
if size >= MAX_SIZE
|
25
|
+
raise Goliath::Validation::BadRequestError.new("Payload size can't exceed #{MAX_SIZE} bytes. Received #{size.inspect} bytes.")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def on_close(env)
|
30
|
+
env.logger.info 'closing connection'
|
31
|
+
end
|
32
|
+
|
33
|
+
def response(env)
|
34
|
+
File.open(TEST_FILE, "w+") { |f| f << "response that should not be here"}
|
35
|
+
[200, {}, "OK"]
|
36
|
+
end
|
37
|
+
end
|
@@ -4,6 +4,9 @@ $:<< '../lib' << 'lib'
|
|
4
4
|
require 'goliath'
|
5
5
|
require 'yajl'
|
6
6
|
|
7
|
+
# API must be started with -e [production, development, ...]
|
8
|
+
# or set your ENV['RACK_ENV'] to specify the environemtn
|
9
|
+
|
7
10
|
class EnvUseStatements < Goliath::API
|
8
11
|
if Goliath.dev?
|
9
12
|
use Goliath::Rack::Render, 'json'
|
data/examples/favicon.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
#
|
5
|
+
# Reads a favicon.ico statically at load time, renders it on any request for
|
6
|
+
# '/favicon.ico', and sends every other request on downstream.
|
7
|
+
#
|
8
|
+
# If you will be serving even one more file than this one, you should instead
|
9
|
+
# use Rack::Static:
|
10
|
+
#
|
11
|
+
# use(Rack::Static, # render static files from ./public
|
12
|
+
# :root => Goliath::Application.app_path("public"),
|
13
|
+
# :urls => ["/favicon.ico", '/stylesheets', '/javascripts', '/images'])
|
14
|
+
#
|
15
|
+
class Favicon
|
16
|
+
def initialize(app, filename)
|
17
|
+
@@favicon = File.read(filename)
|
18
|
+
@@last_mod = File.mtime(filename).utc.rfc822
|
19
|
+
@@expires = Time.at(Time.now + 604800).utc.rfc822 # 1 week from now
|
20
|
+
@app = app
|
21
|
+
end
|
22
|
+
|
23
|
+
def call(env, *args)
|
24
|
+
if env['REQUEST_PATH'] == '/favicon.ico'
|
25
|
+
return [200, {"Last-Modified"=> @@last_mod.to_s, "Expires" => @@expires, "Content-Type"=>"image/vnd.microsoft.icon"}, @@favicon]
|
26
|
+
else
|
27
|
+
return @app.call(env)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
if File.expand_path($0) == File.expand_path(__FILE__)
|
33
|
+
$:<< '../lib' << 'lib'
|
34
|
+
require 'goliath'
|
35
|
+
puts "starting hello world!"
|
36
|
+
class HelloWorld < Goliath::API
|
37
|
+
HelloWorld.use(Favicon, File.expand_path(File.dirname(__FILE__)+"/public/favicon.ico"))
|
38
|
+
end
|
39
|
+
require(File.dirname(__FILE__)+'/hello_world.rb')
|
40
|
+
end
|
data/examples/http_log.rb
CHANGED
@@ -56,6 +56,7 @@ class HttpLog < Goliath::API
|
|
56
56
|
# Write the request information into mongo
|
57
57
|
def record(process_time, resp, client_headers, response_headers)
|
58
58
|
e = env
|
59
|
+
e.trace('http_log_record')
|
59
60
|
EM.next_tick do
|
60
61
|
doc = {
|
61
62
|
request: {
|
@@ -81,4 +82,4 @@ class HttpLog < Goliath::API
|
|
81
82
|
e.mongo.insert(doc)
|
82
83
|
end
|
83
84
|
end
|
84
|
-
end
|
85
|
+
end
|
Binary file
|
data/examples/rack_routes.rb
CHANGED
@@ -26,6 +26,17 @@ class PostHelloWorld < Goliath::API
|
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
|
+
class HeaderCollector < Goliath::API
|
30
|
+
def on_headers(env, header)
|
31
|
+
@headers ||= {}
|
32
|
+
@headers.merge!(header)
|
33
|
+
end
|
34
|
+
|
35
|
+
def response(env)
|
36
|
+
[200, {}, "headers: #{@headers.inspect}"]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
29
40
|
class HelloNumber < Goliath::API
|
30
41
|
use Goliath::Rack::Params
|
31
42
|
def response(env)
|
@@ -76,6 +87,14 @@ class RackRoutes < Goliath::API
|
|
76
87
|
run HelloWorld.new
|
77
88
|
end
|
78
89
|
|
90
|
+
head "/hello_world" do
|
91
|
+
run HelloWorld.new
|
92
|
+
end
|
93
|
+
|
94
|
+
map "/headers", HeaderCollector do
|
95
|
+
use Goliath::Rack::Validation::RequestMethod, %w(GET)
|
96
|
+
end
|
97
|
+
|
79
98
|
map "/bonjour" do
|
80
99
|
run Bonjour.new
|
81
100
|
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
$: << File.dirname(__FILE__)+'/../../lib'
|
3
3
|
require 'goliath'
|
4
4
|
require 'postrank-uri'
|
5
|
+
require File.dirname(__FILE__)+'/../favicon'
|
5
6
|
|
6
7
|
# Install phantomjs: http://code.google.com/p/phantomjs/wiki/QuickStart
|
7
8
|
# $> ruby rasterize.rb -sv
|
@@ -9,7 +10,7 @@ require 'postrank-uri'
|
|
9
10
|
|
10
11
|
class Rasterize < Goliath::API
|
11
12
|
use Goliath::Rack::Params
|
12
|
-
|
13
|
+
use Favicon, File.expand_path(File.dirname(__FILE__)+"/../public/favicon.ico")
|
13
14
|
use Goliath::Rack::Validation::RequestMethod, %w(GET)
|
14
15
|
use Goliath::Rack::Validation::RequiredParam, {:key => 'url'}
|
15
16
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
$: << File.dirname(__FILE__)+'/../../lib'
|
3
3
|
require File.dirname(__FILE__)+'/rasterize'
|
4
|
+
require File.dirname(__FILE__)+'/../favicon'
|
4
5
|
|
5
6
|
require 'goliath'
|
6
7
|
require 'em-synchrony/em-http'
|
@@ -11,18 +12,21 @@ require 'postrank-uri'
|
|
11
12
|
# generate a shortened link, stuffing it in the header. Both requests happen
|
12
13
|
# simultaneously.
|
13
14
|
#
|
14
|
-
class ShortenURL
|
15
|
+
class ShortenURL
|
16
|
+
include Goliath::Rack::BarrierAroundware
|
15
17
|
SHORTENER_URL_BASE = 'http://is.gd/create.php'
|
18
|
+
attr_accessor :shortened_url
|
16
19
|
|
17
20
|
def pre_process
|
18
21
|
target_url = PostRank::URI.clean(env.params['url'])
|
19
22
|
shortener_request = EM::HttpRequest.new(SHORTENER_URL_BASE).aget(:query => { :format => 'simple', :url => target_url })
|
20
|
-
enqueue :
|
23
|
+
enqueue :shortened_url, shortener_request
|
24
|
+
return Goliath::Connection::AsyncResponse
|
21
25
|
end
|
22
26
|
|
23
27
|
def post_process
|
24
|
-
if
|
25
|
-
headers['X-Shortened-URI'] =
|
28
|
+
if shortened_url
|
29
|
+
headers['X-Shortened-URI'] = shortened_url.response
|
26
30
|
end
|
27
31
|
[status, headers, body]
|
28
32
|
end
|
@@ -30,8 +34,9 @@ end
|
|
30
34
|
|
31
35
|
class RasterizeAndShorten < Rasterize
|
32
36
|
use Goliath::Rack::Params
|
37
|
+
use Favicon, File.expand_path(File.dirname(__FILE__)+"/../public/favicon.ico")
|
33
38
|
use Goliath::Rack::Validation::RequestMethod, %w(GET)
|
34
39
|
use Goliath::Rack::Validation::RequiredParam, {:key => 'url'}
|
35
40
|
#
|
36
|
-
use Goliath::Rack::
|
41
|
+
use Goliath::Rack::BarrierAroundwareFactory, ShortenURL
|
37
42
|
end
|
data/goliath.gemspec
CHANGED
@@ -8,14 +8,12 @@ Gem::Specification.new do |s|
|
|
8
8
|
s.platform = Gem::Platform::RUBY
|
9
9
|
s.authors = ['dan sinclair', 'Ilya Grigorik']
|
10
10
|
s.email = ['dj2@everburning.com', 'ilya@igvita.com']
|
11
|
-
s.homepage = 'http://
|
12
|
-
s.summary = '
|
11
|
+
s.homepage = 'http://goliath.io/'
|
12
|
+
s.summary = 'Async framework for writing API servers'
|
13
13
|
s.description = s.summary
|
14
14
|
|
15
|
-
s.required_ruby_version = '>=1.9.2'
|
16
|
-
|
17
15
|
s.add_dependency 'eventmachine', '>= 1.0.0.beta.3'
|
18
|
-
s.add_dependency 'em-synchrony', '>= 0.
|
16
|
+
s.add_dependency 'em-synchrony', '>= 1.0.0'
|
19
17
|
s.add_dependency 'http_parser.rb'
|
20
18
|
s.add_dependency 'log4r'
|
21
19
|
|
@@ -24,13 +22,13 @@ Gem::Specification.new do |s|
|
|
24
22
|
s.add_dependency 'rack-respond_to'
|
25
23
|
s.add_dependency 'async-rack'
|
26
24
|
s.add_dependency 'multi_json'
|
27
|
-
s.add_dependency 'http_router', '~> 0.
|
25
|
+
s.add_dependency 'http_router', '~> 0.9.0'
|
28
26
|
|
29
|
-
s.add_development_dependency 'rake', '0.8.7'
|
27
|
+
s.add_development_dependency 'rake', '>=0.8.7'
|
30
28
|
s.add_development_dependency 'rspec', '>2.0'
|
31
29
|
s.add_development_dependency 'nokogiri'
|
32
|
-
s.add_development_dependency 'em-http-request', '>=
|
33
|
-
s.add_development_dependency 'em-mongo'
|
30
|
+
s.add_development_dependency 'em-http-request', '>=1.0.0'
|
31
|
+
s.add_development_dependency 'em-mongo', '~> 0.4.0'
|
34
32
|
s.add_development_dependency 'yajl-ruby'
|
35
33
|
s.add_development_dependency 'rack-rewrite'
|
36
34
|
s.add_development_dependency 'multipart_body'
|
data/lib/goliath/api.rb
CHANGED
@@ -125,9 +125,6 @@ module Goliath
|
|
125
125
|
def map(name, *args, &block)
|
126
126
|
opts = args.last.is_a?(Hash) ? args.pop : {}
|
127
127
|
klass = args.first
|
128
|
-
if klass && block_given?
|
129
|
-
raise "Can't provide class and block to map"
|
130
|
-
end
|
131
128
|
maps.push([name, klass, opts, block])
|
132
129
|
end
|
133
130
|
|
@@ -137,7 +134,7 @@ module Goliath
|
|
137
134
|
opts = args.last.is_a?(Hash) ? args.pop : {}
|
138
135
|
klass = args.first
|
139
136
|
opts[:conditions] ||= {}
|
140
|
-
opts[:conditions][:request_method] = [#{http_method
|
137
|
+
opts[:conditions][:request_method] = [#{http_method.to_s.upcase.inspect}]
|
141
138
|
map(name, klass, opts, &block)
|
142
139
|
end
|
143
140
|
EOT
|
@@ -281,5 +278,20 @@ module Goliath
|
|
281
278
|
def chunked_streaming_response(status_code = 200, headers = {})
|
282
279
|
streaming_response(status_code, headers.merge(Goliath::Response::CHUNKED_STREAM_HEADERS))
|
283
280
|
end
|
281
|
+
|
282
|
+
# Helper method to initialize the approriate API handler
|
283
|
+
#
|
284
|
+
# Called by the parser once headers are available to detect
|
285
|
+
# which API class should be handling the incoming request
|
286
|
+
def set_event_handler!(env)
|
287
|
+
if self.class.maps?
|
288
|
+
response = self.class.router.recognize(env)
|
289
|
+
if response = self.class.router.recognize(env) and response.respond_to?(:path) and response.path.route.api_class
|
290
|
+
env.event_handler = response.path.route.api_class.new
|
291
|
+
end
|
292
|
+
end
|
293
|
+
env.event_handler ||= self
|
294
|
+
end
|
295
|
+
|
284
296
|
end
|
285
297
|
end
|
data/lib/goliath/connection.rb
CHANGED
@@ -15,7 +15,6 @@ module Goliath
|
|
15
15
|
attr_reader :parser
|
16
16
|
|
17
17
|
AsyncResponse = [-1, {}, []]
|
18
|
-
|
19
18
|
def post_init
|
20
19
|
@current = nil
|
21
20
|
@requests = []
|
@@ -24,7 +23,7 @@ module Goliath
|
|
24
23
|
@parser = Http::Parser.new
|
25
24
|
@parser.on_headers_complete = proc do |h|
|
26
25
|
|
27
|
-
env = Goliath::Env.new
|
26
|
+
env = Thread.current[GOLIATH_ENV] = Goliath::Env.new
|
28
27
|
env[SERVER_PORT] = port.to_s
|
29
28
|
env[RACK_LOGGER] = logger
|
30
29
|
env[OPTIONS] = options
|
@@ -32,12 +31,14 @@ module Goliath
|
|
32
31
|
env[CONFIG] = config
|
33
32
|
env[REMOTE_ADDR] = remote_address
|
34
33
|
|
35
|
-
env[ASYNC_HEADERS] = @api.method(:on_headers) if @api.respond_to? :on_headers
|
36
|
-
env[ASYNC_BODY] = @api.method(:on_body) if @api.respond_to? :on_body
|
37
|
-
env[ASYNC_CLOSE] = @api.method(:on_close) if @api.respond_to? :on_close
|
38
|
-
|
39
34
|
r = Goliath::Request.new(@app, self, env)
|
40
|
-
r.parse_header(h, @parser)
|
35
|
+
r.parse_header(h, @parser) do
|
36
|
+
@api.set_event_handler!(env) if @api
|
37
|
+
|
38
|
+
env[ASYNC_HEADERS] = env.event_handler.method(:on_headers) if env.event_handler.respond_to? :on_headers
|
39
|
+
env[ASYNC_BODY] = env.event_handler.method(:on_body) if env.event_handler.respond_to? :on_body
|
40
|
+
env[ASYNC_CLOSE] = env.event_handler.method(:on_close) if env.event_handler.respond_to? :on_close
|
41
|
+
end
|
41
42
|
|
42
43
|
@requests.push(r)
|
43
44
|
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
module Goliath
|
2
|
+
module Rack
|
3
|
+
#
|
4
|
+
# Note: This class is deprecated. Instead, use BarrierAroundwareFactory
|
5
|
+
# (orchestrates multiple concurrent requests) or SimpleAroundwareFactory
|
6
|
+
# (like AsyncMiddleware, but with a simpler interface).
|
7
|
+
#
|
8
|
+
# The differences:
|
9
|
+
# * ResponseReceiver/MultiReceiver was a stupid name. The thing that has
|
10
|
+
# pre_ and post_process is the Aroundware, the thing that manufactures
|
11
|
+
# it is an AroundwareFactory.
|
12
|
+
# * An aroundware's pre_process may return a direct response, which is
|
13
|
+
# immediately sent back upstream (no further downstream processing
|
14
|
+
# happens). In the typical case, you will want to add
|
15
|
+
# return Goliath::Connection::AsyncResponse
|
16
|
+
# to your pre_process method.
|
17
|
+
# * ResponseReceiver used to masquerade as callback and middleware. Yuck.
|
18
|
+
# The downstream response is now set via #accept_response, not #call.
|
19
|
+
#
|
20
|
+
# * change
|
21
|
+
# use Goliath::Rack::AsyncAroundware, MyObsoleteReceiver
|
22
|
+
# to
|
23
|
+
# use Goliath::Rack::BarrierAroundwareFactory, MyHappyBarrier
|
24
|
+
# * `BarrierAroundware` provides the combined functionality of
|
25
|
+
# `MultiReceiver` and `ResponseReceiver`, which will go away. It's now a
|
26
|
+
# mixin (module) so you're not forced to inherit from it.
|
27
|
+
# * There is no more `responses` method: either use instance accessors or
|
28
|
+
# look in the `successes`/`failures` hashes for yourresults.
|
29
|
+
# * Both enqueued responses and the downstream response are sent to
|
30
|
+
# `accept_response`; there is no more `call` method.
|
31
|
+
# * `MongoReceiver` will go away, because there's no need for it. See
|
32
|
+
# `examples/auth_and_rate_limit.rb` for examples
|
33
|
+
#
|
34
|
+
class AsyncAroundware
|
35
|
+
include Goliath::Rack::Validator
|
36
|
+
|
37
|
+
#
|
38
|
+
# Called by the framework to create the middleware.
|
39
|
+
#
|
40
|
+
# Any extra args passed to the use statement are sent to each
|
41
|
+
# aroundware_klass as it is created.
|
42
|
+
#
|
43
|
+
# @example
|
44
|
+
# class Awesomizer2011 < Goliath::Rack::MultiReceiver
|
45
|
+
# def initialize(env, aq)
|
46
|
+
# @awesomeness_quotient = aq
|
47
|
+
# super(env)
|
48
|
+
# end
|
49
|
+
# # ... define pre_process and post_process ...
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# class AwesomeApiWithShortening < Goliath::API
|
53
|
+
# use Goliath::Rack::AsyncAroundware, Awesomizer2011, 3
|
54
|
+
# # ... stuff ...
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# @param app [#call] the downstream app
|
58
|
+
# @param aroundware_klass a class that quacks like a
|
59
|
+
# Goliath::Rack::ResponseReceiver and an EM::Deferrable
|
60
|
+
# @param *args [Array] extra args to pass to the aroundware
|
61
|
+
# @return [Goliath::Rack::AsyncAroundware]
|
62
|
+
def initialize app, aroundware_klass, *args
|
63
|
+
@app = app
|
64
|
+
@aroundware_klass = aroundware_klass
|
65
|
+
@aroundware_args = args
|
66
|
+
end
|
67
|
+
|
68
|
+
# Coordinates aroundware to process a request.
|
69
|
+
#
|
70
|
+
# We hook the aroundware in the middle of the async_callback chain:
|
71
|
+
# * send the downstream response to the aroundware, whether received directly
|
72
|
+
# from @app.call or via async callback
|
73
|
+
# * have the upstream callback chain be invoked when the aroundware completes
|
74
|
+
#
|
75
|
+
# @param env [Goliath::Env] The goliath environment
|
76
|
+
# @return [Array] The [status_code, headers, body] tuple
|
77
|
+
def call(env)
|
78
|
+
aroundware = new_aroundware(env)
|
79
|
+
|
80
|
+
aroundware_resp = aroundware.pre_process
|
81
|
+
|
82
|
+
hook_into_callback_chain(env, aroundware)
|
83
|
+
|
84
|
+
downstream_resp = @app.call(env)
|
85
|
+
|
86
|
+
# if downstream resp is final, pass it to the aroundware; it will invoke
|
87
|
+
# the callback chain at its leisure. Our response is *always* async.
|
88
|
+
if final_response?(downstream_resp)
|
89
|
+
aroundware.call(downstream_resp)
|
90
|
+
end
|
91
|
+
return Goliath::Connection::AsyncResponse
|
92
|
+
end
|
93
|
+
|
94
|
+
# Put aroundware in the middle of the async_callback chain:
|
95
|
+
# * save the old callback chain;
|
96
|
+
# * have the downstream callback send results to the aroundware (possibly
|
97
|
+
# completing it)
|
98
|
+
# * set the old callback chain to fire when the aroundware completes
|
99
|
+
def hook_into_callback_chain(env, aroundware)
|
100
|
+
async_callback = env['async.callback']
|
101
|
+
|
102
|
+
# The response from the downstream app is accepted by the aroundware...
|
103
|
+
downstream_callback = Proc.new do |resp|
|
104
|
+
safely(env){ aroundware.call(resp) }
|
105
|
+
end
|
106
|
+
|
107
|
+
# .. but the upstream chain is only invoked when the aroundware completes
|
108
|
+
invoke_upstream_chain = Proc.new do
|
109
|
+
new_resp = safely(env){ aroundware.post_process }
|
110
|
+
async_callback.call(new_resp)
|
111
|
+
end
|
112
|
+
|
113
|
+
env['async.callback'] = downstream_callback
|
114
|
+
aroundware.callback(&invoke_upstream_chain)
|
115
|
+
aroundware.errback(&invoke_upstream_chain)
|
116
|
+
end
|
117
|
+
|
118
|
+
def final_response?(resp)
|
119
|
+
resp != Goliath::Connection::AsyncResponse
|
120
|
+
end
|
121
|
+
|
122
|
+
# Generate a aroundware to process the request, using request env & any args
|
123
|
+
# passed to this AsyncAroundware at creation
|
124
|
+
#
|
125
|
+
# @param env [Goliath::Env] The goliath environment
|
126
|
+
# @return [Goliath::Rack::ResponseReceiver] The response_receiver to process this request
|
127
|
+
def new_aroundware(env)
|
128
|
+
@aroundware_klass.new(env, *@aroundware_args)
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|