telecaster 0.1.1 → 0.1.2
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 +8 -15
- data/example/servers.rb +9 -6
- data/lib/telecaster.rb +34 -12
- data/lib/telecaster/multi.rb +7 -5
- metadata +20 -25
- data/spec/backends.rb +0 -48
- data/spec/spec_helper.rb +0 -25
- data/spec/telecaster_spec.rb +0 -159
- data/spec/telecaster_steps.rb +0 -55
data/README.rdoc
CHANGED
@@ -16,29 +16,22 @@ the backend responses will be aggregated by the master.
|
|
16
16
|
|
17
17
|
== Usage
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
a logger:
|
19
|
+
Create a Telecaster with a list of backend hosts, a logger and a timeout in
|
20
|
+
seconds, then tell it to listen on a port:
|
22
21
|
|
23
22
|
require 'telecaster'
|
24
23
|
require 'logger'
|
25
24
|
|
26
25
|
telecaster = Telecaster.new(
|
27
26
|
:backends => %w[http://concerts-service http://accounts-service],
|
28
|
-
:logger => Logger.new($stdout)
|
27
|
+
:logger => Logger.new($stdout),
|
28
|
+
:timeout => 15
|
29
29
|
)
|
30
|
-
|
31
|
-
Then either boot it directly with Thin:
|
32
|
-
|
33
|
-
require 'rack'
|
34
|
-
Rack::Handler.get('thin').run(telecaster, :Port => 9000)
|
35
|
-
|
36
|
-
or use a +rackup+ script
|
37
|
-
|
38
|
-
# config.ru
|
39
|
-
run telecaster
|
40
30
|
|
41
|
-
|
31
|
+
telecaster.listen(9000)
|
32
|
+
|
33
|
+
This boots a Thin server with the request timeout set high enough to accomodate
|
34
|
+
your backend request timeout.
|
42
35
|
|
43
36
|
|
44
37
|
== Protocol
|
data/example/servers.rb
CHANGED
@@ -7,14 +7,17 @@ require 'thin'
|
|
7
7
|
Thin::Logging.silent = true
|
8
8
|
|
9
9
|
EM.run {
|
10
|
-
Rack::Handler.get('thin')
|
11
|
-
Rack::Handler.get('thin').run(Blank, :Port => 9002)
|
12
|
-
Rack::Handler.get('thin').run(Error, :Port => 9003)
|
10
|
+
thin = Rack::Handler.get('thin')
|
13
11
|
|
14
|
-
|
12
|
+
thin.run(Responder, :Port => 9001)
|
13
|
+
thin.run(Blank, :Port => 9002)
|
14
|
+
thin.run(Error, :Port => 9003)
|
15
|
+
thin.run(Slow, :Port => 9004)
|
16
|
+
|
17
|
+
hosts = (1..4).map { |n| "http://localhost:900#{n}" }
|
15
18
|
logger = Logger.new($stdout)
|
16
|
-
telecaster = Telecaster.new(:backends => hosts, :logger => logger)
|
19
|
+
telecaster = Telecaster.new(:backends => hosts, :logger => logger, :timeout => 1)
|
17
20
|
|
18
|
-
|
21
|
+
telecaster.listen(9000)
|
19
22
|
}
|
20
23
|
|
data/lib/telecaster.rb
CHANGED
@@ -1,35 +1,50 @@
|
|
1
1
|
require 'eventmachine'
|
2
|
-
require 'em-http
|
2
|
+
require 'em-http'
|
3
|
+
require 'em-http/version'
|
4
|
+
require 'thin'
|
3
5
|
require 'yajl'
|
4
6
|
|
5
7
|
class Telecaster
|
6
|
-
ASYNC_RESPONSE
|
7
|
-
|
8
|
+
ASYNC_RESPONSE = [-1, {}, []].freeze
|
9
|
+
DEFAULT_TIMEOUT = 10
|
10
|
+
EMHTTP_VERSION = EM::HttpRequest::VERSION.split('.')[0].to_i
|
11
|
+
TYPE_JSON = 'application/json'
|
8
12
|
|
9
13
|
ROOT = File.expand_path('..', __FILE__)
|
10
14
|
autoload :Multi, ROOT + '/telecaster/multi'
|
11
15
|
|
12
16
|
def initialize(options)
|
13
17
|
@backend_hosts = options[:backends]
|
14
|
-
@logger
|
18
|
+
@logger = options[:logger]
|
19
|
+
@timeout = options[:timeout] || DEFAULT_TIMEOUT
|
20
|
+
end
|
21
|
+
|
22
|
+
def listen(port)
|
23
|
+
app = self
|
24
|
+
server = Thin::Server.new('0.0.0.0', port) { run app }
|
25
|
+
server.timeout = 1.5 * @timeout
|
26
|
+
server.start
|
15
27
|
end
|
16
28
|
|
17
29
|
def call(env)
|
18
30
|
ensure_reactor_running
|
19
31
|
|
32
|
+
start = Time.now.to_f
|
20
33
|
method = env['REQUEST_METHOD'].downcase
|
21
34
|
multi = Multi.new
|
22
35
|
callback = env['async.callback']
|
23
|
-
|
36
|
+
args = create_args(env)
|
24
37
|
|
25
38
|
@backend_hosts.each do |host|
|
26
|
-
|
39
|
+
uri = File.join(host, env['REQUEST_URI'])
|
40
|
+
request = create_request(uri)
|
41
|
+
|
27
42
|
multi.add(host, request.__send__(method, args))
|
28
43
|
end
|
29
44
|
|
30
45
|
multi.callback do |response|
|
31
46
|
duration = ((Time.now.to_f - start) * 1000).round
|
32
|
-
summary = response.map { |r| '[' + [r['host'], r['status'], r['duration']] * ' ' + ']' }
|
47
|
+
summary = response.map { |r| '[' + [r['host'], r['status'] || '-', r['duration'] || '-'] * ' ' + ']' }
|
33
48
|
@logger.info "#{env['REQUEST_METHOD']} #{env['REQUEST_URI']} backends:#{response.size} duration:#{duration} #{summary * ' '}"
|
34
49
|
|
35
50
|
json = Yajl::Encoder.encode(response, :pretty => true, :indent => ' ')
|
@@ -44,10 +59,9 @@ class Telecaster
|
|
44
59
|
|
45
60
|
private
|
46
61
|
|
47
|
-
def
|
48
|
-
|
49
|
-
|
50
|
-
args = {:head => {}}
|
62
|
+
def create_args(env)
|
63
|
+
args = {:head => {}}
|
64
|
+
args[:timeout] = @timeout if EMHTTP_VERSION < 1
|
51
65
|
|
52
66
|
env.each do |header, value|
|
53
67
|
next unless header =~ /^HTTP_/ and header != 'HTTP_HOST'
|
@@ -71,7 +85,15 @@ private
|
|
71
85
|
args[:body] = input.read
|
72
86
|
end
|
73
87
|
|
74
|
-
|
88
|
+
args
|
89
|
+
end
|
90
|
+
|
91
|
+
def create_request(uri)
|
92
|
+
if EMHTTP_VERSION >= 1
|
93
|
+
EM::HttpRequest.new(uri, :inactivity_timeout => @timeout)
|
94
|
+
else
|
95
|
+
EM::HttpRequest.new(uri)
|
96
|
+
end
|
75
97
|
end
|
76
98
|
|
77
99
|
def ensure_reactor_running
|
data/lib/telecaster/multi.rb
CHANGED
@@ -25,19 +25,21 @@ class Telecaster
|
|
25
25
|
if content_type == TYPE_JSON
|
26
26
|
response['data'] = Yajl::Parser.parse(http.response)
|
27
27
|
end
|
28
|
-
|
29
|
-
succeed if @responses.size == @requests.size
|
28
|
+
add_response(host, response)
|
30
29
|
end
|
31
30
|
|
32
31
|
request.errback do |http|
|
33
|
-
|
34
|
-
@responses[host] = response
|
35
|
-
succeed if @responses.size == @requests.size
|
32
|
+
add_response(host, {'host' => host, 'status' => nil})
|
36
33
|
end
|
37
34
|
end
|
38
35
|
|
39
36
|
private
|
40
37
|
|
38
|
+
def add_response(host, response)
|
39
|
+
@responses[host] = response
|
40
|
+
succeed if @responses.size == @requests.size
|
41
|
+
end
|
42
|
+
|
41
43
|
def succeed
|
42
44
|
responses = []
|
43
45
|
@responses.each do |host, response|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: telecaster
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-09-
|
12
|
+
date: 2012-09-20 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: em-http-request
|
@@ -43,6 +43,22 @@ dependencies:
|
|
43
43
|
- - ! '>='
|
44
44
|
- !ruby/object:Gem::Version
|
45
45
|
version: 0.12.0
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: thin
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 1.2.0
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.2.0
|
46
62
|
- !ruby/object:Gem::Dependency
|
47
63
|
name: yajl-ruby
|
48
64
|
requirement: !ruby/object:Gem::Requirement
|
@@ -123,22 +139,6 @@ dependencies:
|
|
123
139
|
- - ! '>='
|
124
140
|
- !ruby/object:Gem::Version
|
125
141
|
version: '0'
|
126
|
-
- !ruby/object:Gem::Dependency
|
127
|
-
name: thin
|
128
|
-
requirement: !ruby/object:Gem::Requirement
|
129
|
-
none: false
|
130
|
-
requirements:
|
131
|
-
- - ! '>='
|
132
|
-
- !ruby/object:Gem::Version
|
133
|
-
version: 1.2.0
|
134
|
-
type: :development
|
135
|
-
prerelease: false
|
136
|
-
version_requirements: !ruby/object:Gem::Requirement
|
137
|
-
none: false
|
138
|
-
requirements:
|
139
|
-
- - ! '>='
|
140
|
-
- !ruby/object:Gem::Version
|
141
|
-
version: 1.2.0
|
142
142
|
description:
|
143
143
|
email: james@songkick.com
|
144
144
|
executables: []
|
@@ -148,12 +148,8 @@ extra_rdoc_files:
|
|
148
148
|
files:
|
149
149
|
- README.rdoc
|
150
150
|
- example/servers.rb
|
151
|
-
- lib/telecaster/multi.rb
|
152
151
|
- lib/telecaster.rb
|
153
|
-
-
|
154
|
-
- spec/spec_helper.rb
|
155
|
-
- spec/telecaster_spec.rb
|
156
|
-
- spec/telecaster_steps.rb
|
152
|
+
- lib/telecaster/multi.rb
|
157
153
|
homepage: http://github.com/songkick/telecaster
|
158
154
|
licenses: []
|
159
155
|
post_install_message:
|
@@ -179,6 +175,5 @@ rubyforge_project:
|
|
179
175
|
rubygems_version: 1.8.23
|
180
176
|
signing_key:
|
181
177
|
specification_version: 3
|
182
|
-
summary: HTTP proxy for
|
178
|
+
summary: Multiplexing HTTP proxy for decoupled inter-service notifications
|
183
179
|
test_files: []
|
184
|
-
has_rdoc:
|
data/spec/backends.rb
DELETED
@@ -1,48 +0,0 @@
|
|
1
|
-
require 'sinatra'
|
2
|
-
|
3
|
-
class Responder < Sinatra::Base
|
4
|
-
before do
|
5
|
-
headers 'Content-Type' => 'application/json'
|
6
|
-
end
|
7
|
-
|
8
|
-
get '/hello' do
|
9
|
-
Yajl::Encoder.encode('status' => 'ok')
|
10
|
-
end
|
11
|
-
|
12
|
-
get '/error' do
|
13
|
-
status 503
|
14
|
-
Yajl::Encoder.encode('error' => 'down')
|
15
|
-
end
|
16
|
-
|
17
|
-
get '/users/:username' do |username|
|
18
|
-
Yajl::Encoder.encode('username' => username)
|
19
|
-
end
|
20
|
-
|
21
|
-
get '/search' do
|
22
|
-
Yajl::Encoder.encode('query' => params[:q])
|
23
|
-
end
|
24
|
-
|
25
|
-
put '/blog' do
|
26
|
-
status 201
|
27
|
-
Yajl::Encoder.encode('title' => params[:title])
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
class Blank < Sinatra::Base
|
32
|
-
get '/hello' do
|
33
|
-
''
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
class Slow < Sinatra::Base
|
38
|
-
get '/hello' do
|
39
|
-
EM.add_timer 2 do
|
40
|
-
env['async.callback'].call [200, {}, []]
|
41
|
-
end
|
42
|
-
[-1, {}, []]
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
class Error < Sinatra::Base
|
47
|
-
end
|
48
|
-
|
data/spec/spec_helper.rb
DELETED
@@ -1,25 +0,0 @@
|
|
1
|
-
require 'rubygems'
|
2
|
-
require 'bundler/setup'
|
3
|
-
require File.expand_path('../../lib/telecaster', __FILE__)
|
4
|
-
|
5
|
-
require 'rack/proxy'
|
6
|
-
require 'rack/test'
|
7
|
-
require 'uri'
|
8
|
-
|
9
|
-
require File.expand_path('../../vendor/em-rspec/lib/em-rspec', __FILE__)
|
10
|
-
require File.expand_path('../telecaster_steps', __FILE__)
|
11
|
-
|
12
|
-
class Proxy < Rack::Proxy
|
13
|
-
def initialize(host)
|
14
|
-
super()
|
15
|
-
@uri = URI.parse(host)
|
16
|
-
end
|
17
|
-
|
18
|
-
def rewrite_env(env)
|
19
|
-
env['SERVER_NAME'] = @uri.host
|
20
|
-
env['SERVER_PORT'] = @uri.port.to_s
|
21
|
-
env['HTTP_HOST'] = [@uri.host, @uri.port].join(':')
|
22
|
-
env
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
data/spec/telecaster_spec.rb
DELETED
@@ -1,159 +0,0 @@
|
|
1
|
-
require "spec_helper"
|
2
|
-
require "backends"
|
3
|
-
require "logger"
|
4
|
-
|
5
|
-
describe Telecaster do
|
6
|
-
include TelecasterSteps
|
7
|
-
|
8
|
-
let(:logger) { Logger.new(StringIO.new) }
|
9
|
-
|
10
|
-
before do
|
11
|
-
Thread.new { EM.run }
|
12
|
-
Thread.pass until EM.reactor_running?
|
13
|
-
end
|
14
|
-
|
15
|
-
after { stop_all_servers }
|
16
|
-
|
17
|
-
describe "with a backend" do
|
18
|
-
let :app do
|
19
|
-
Proxy.new("http://localhost:9000")
|
20
|
-
end
|
21
|
-
|
22
|
-
before do
|
23
|
-
telecaster = Telecaster.new(:backends => ["http://localhost:9001"], :logger => logger)
|
24
|
-
boot telecaster, 9000
|
25
|
-
|
26
|
-
boot Responder, 9001
|
27
|
-
end
|
28
|
-
|
29
|
-
after { sync }
|
30
|
-
|
31
|
-
it "forwards a GET request" do
|
32
|
-
get "/hello", {}
|
33
|
-
check_status 200
|
34
|
-
check_json [{
|
35
|
-
"host" => "http://localhost:9001",
|
36
|
-
"status" => 200,
|
37
|
-
"duration" => an_instance_of(Fixnum),
|
38
|
-
"data" => {"status" => "ok"}
|
39
|
-
}]
|
40
|
-
end
|
41
|
-
|
42
|
-
it "forwards a GET request with path params" do
|
43
|
-
get "/users/jcoglan", {}
|
44
|
-
check_status 200
|
45
|
-
check_json [{
|
46
|
-
"host" => "http://localhost:9001",
|
47
|
-
"status" => 200,
|
48
|
-
"duration" => an_instance_of(Fixnum),
|
49
|
-
"data" => {"username" => "jcoglan"}
|
50
|
-
}]
|
51
|
-
end
|
52
|
-
|
53
|
-
it "forwards a GET request with query string params" do
|
54
|
-
get "/search?q=foo", {}
|
55
|
-
check_status 200
|
56
|
-
check_json [{
|
57
|
-
"host" => "http://localhost:9001",
|
58
|
-
"status" => 200,
|
59
|
-
"duration" => an_instance_of(Fixnum),
|
60
|
-
"data" => {"query" => "foo"}
|
61
|
-
}]
|
62
|
-
end
|
63
|
-
|
64
|
-
it "forwards a PUT request with entity body params" do
|
65
|
-
put "/blog", :title => "A new post"
|
66
|
-
check_status 200
|
67
|
-
check_json [{
|
68
|
-
"host" => "http://localhost:9001",
|
69
|
-
"status" => 201,
|
70
|
-
"duration" => an_instance_of(Fixnum),
|
71
|
-
"data" => {"title" => "A new post"}
|
72
|
-
}]
|
73
|
-
end
|
74
|
-
|
75
|
-
it "returns an error response" do
|
76
|
-
get "/error", {}
|
77
|
-
check_status 200
|
78
|
-
check_json [{
|
79
|
-
"host" => "http://localhost:9001",
|
80
|
-
"status" => 503,
|
81
|
-
"duration" => an_instance_of(Fixnum),
|
82
|
-
"data" => {"error" => "down"}
|
83
|
-
}]
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
describe "with a backend that's down" do
|
88
|
-
let :app do
|
89
|
-
Proxy.new("http://localhost:9000")
|
90
|
-
end
|
91
|
-
|
92
|
-
before do
|
93
|
-
telecaster = Telecaster.new(:backends => ["http://localhost:9001"], :logger => logger)
|
94
|
-
boot telecaster, 9000
|
95
|
-
end
|
96
|
-
|
97
|
-
after { sync }
|
98
|
-
|
99
|
-
it "returns a response with no status" do
|
100
|
-
get "/hello", {}
|
101
|
-
check_status 200
|
102
|
-
check_json [{ "host" => "http://localhost:9001", "status" => nil }]
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
describe "with multiple backends" do
|
107
|
-
let :app do
|
108
|
-
Proxy.new("http://localhost:9000")
|
109
|
-
end
|
110
|
-
|
111
|
-
before do
|
112
|
-
hosts = (1..3).map { |n| "http://localhost:900#{n}" }
|
113
|
-
telecaster = Telecaster.new(:backends => hosts, :logger => logger)
|
114
|
-
|
115
|
-
boot telecaster, 9000
|
116
|
-
|
117
|
-
boot Responder, 9001
|
118
|
-
boot Blank, 9002
|
119
|
-
boot Error, 9003
|
120
|
-
end
|
121
|
-
|
122
|
-
after { sync }
|
123
|
-
|
124
|
-
it "returns the responses from the backends" do
|
125
|
-
get "/hello", {}
|
126
|
-
check_status 200
|
127
|
-
check_json [
|
128
|
-
{ "host" => "http://localhost:9001", "status" => 200, "duration" => an_instance_of(Fixnum), "data" => {"status" => "ok"} },
|
129
|
-
{ "host" => "http://localhost:9002", "status" => 200, "duration" => an_instance_of(Fixnum) },
|
130
|
-
{ "host" => "http://localhost:9003", "status" => 404, "duration" => an_instance_of(Fixnum) }
|
131
|
-
]
|
132
|
-
end
|
133
|
-
|
134
|
-
describe "delegation" do
|
135
|
-
let :app do
|
136
|
-
Proxy.new("http://localhost:9005")
|
137
|
-
end
|
138
|
-
|
139
|
-
before do
|
140
|
-
telecaster = Telecaster.new(:backends => [0,4].map { |n| "http://localhost:900#{n}" }, :logger => logger)
|
141
|
-
boot telecaster, 9005
|
142
|
-
|
143
|
-
boot Slow, 9004
|
144
|
-
end
|
145
|
-
|
146
|
-
it "aggregates responses from delegate servers" do
|
147
|
-
get "/hello", {}
|
148
|
-
check_status 200
|
149
|
-
check_json [
|
150
|
-
{ "host" => "http://localhost:9001", "status" => 200, "duration" => an_instance_of(Fixnum), "data" => {"status" => "ok"} },
|
151
|
-
{ "host" => "http://localhost:9002", "status" => 200, "duration" => an_instance_of(Fixnum) },
|
152
|
-
{ "host" => "http://localhost:9003", "status" => 404, "duration" => an_instance_of(Fixnum) },
|
153
|
-
{ "host" => "http://localhost:9004", "status" => 200, "duration" => an_instance_of(Fixnum) }
|
154
|
-
]
|
155
|
-
end
|
156
|
-
end
|
157
|
-
end
|
158
|
-
end
|
159
|
-
|
data/spec/telecaster_steps.rb
DELETED
@@ -1,55 +0,0 @@
|
|
1
|
-
TelecasterSteps = EM::RSpec.async_steps do
|
2
|
-
include Rack::Test::Methods
|
3
|
-
|
4
|
-
def boot(application, port, &callback)
|
5
|
-
@apps ||= {}
|
6
|
-
@apps[port] = application
|
7
|
-
application.extend(ThinRunner)
|
8
|
-
application.start(port)
|
9
|
-
EM.next_tick(&callback)
|
10
|
-
end
|
11
|
-
|
12
|
-
def stop_all_servers(&callback)
|
13
|
-
@apps.each_value { |app| app.stop }
|
14
|
-
EM.next_tick(&callback)
|
15
|
-
end
|
16
|
-
|
17
|
-
%w[get post put delete].each do |method|
|
18
|
-
class_eval %Q{
|
19
|
-
def #{method}(path, params, &callback)
|
20
|
-
EM.defer {
|
21
|
-
super(path, params)
|
22
|
-
callback.call
|
23
|
-
}
|
24
|
-
end
|
25
|
-
}
|
26
|
-
end
|
27
|
-
|
28
|
-
def check_status(status, &callback)
|
29
|
-
last_response.status.to_i.should == status
|
30
|
-
callback.call
|
31
|
-
end
|
32
|
-
|
33
|
-
def check_json(json, &callback)
|
34
|
-
last_response['Content-Type'].should == 'application/json'
|
35
|
-
Yajl::Parser.parse(last_response.body).should == json
|
36
|
-
callback.call
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
require 'thin'
|
41
|
-
Thin::Logging.silent = true
|
42
|
-
|
43
|
-
module ThinRunner
|
44
|
-
def start(port)
|
45
|
-
handler = Rack::Handler.get('thin')
|
46
|
-
handler.run(self, :Port => port) do |server|
|
47
|
-
@server = server
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
def stop
|
52
|
-
@server.stop
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|