telecaster 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +111 -0
- data/example/servers.rb +20 -0
- data/lib/telecaster.rb +83 -0
- data/lib/telecaster/multi.rb +54 -0
- data/spec/backends.rb +48 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/telecaster_spec.rb +159 -0
- data/spec/telecaster_steps.rb +55 -0
- metadata +184 -0
data/README.rdoc
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
= Telecaster
|
2
|
+
|
3
|
+
A multiplexing HTTP proxy based on EventMachine. Forwards every incoming request
|
4
|
+
onto a set of backend servers, collects the responses and sends a summary back
|
5
|
+
to the client.
|
6
|
+
|
7
|
+
This is designed to allow independent services to notify each other about
|
8
|
+
changes without tightly coupling them. Each service just calls a Telecaster
|
9
|
+
server, which knows where to forward things to. The publisher gets a summary of
|
10
|
+
which services were called and how they responded, which makes this notification
|
11
|
+
style easier to debug than async message-passing systems.
|
12
|
+
|
13
|
+
Telecaster supports delegation, so requests can be distributed to slaves and all
|
14
|
+
the backend responses will be aggregated by the master.
|
15
|
+
|
16
|
+
|
17
|
+
== Usage
|
18
|
+
|
19
|
+
We recommend an EventMachine-based server like Thin, to best take advantage of
|
20
|
+
EventMachine's concurrency. Create a Telecaster with a list of backend hosts and
|
21
|
+
a logger:
|
22
|
+
|
23
|
+
require 'telecaster'
|
24
|
+
require 'logger'
|
25
|
+
|
26
|
+
telecaster = Telecaster.new(
|
27
|
+
:backends => %w[http://concerts-service http://accounts-service],
|
28
|
+
:logger => Logger.new($stdout)
|
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
|
+
|
41
|
+
$ thin start -R config.ru -p 9000
|
42
|
+
|
43
|
+
|
44
|
+
== Protocol
|
45
|
+
|
46
|
+
On every request, Telecaster returns a 200 response containing a JSON summary of
|
47
|
+
the backend responses, for example:
|
48
|
+
|
49
|
+
$ curl localhost:9000/hello
|
50
|
+
[
|
51
|
+
{
|
52
|
+
"host": "http://localhost:9001",
|
53
|
+
"status": 200,
|
54
|
+
"duration": 16,
|
55
|
+
"data": {
|
56
|
+
"status": "ok"
|
57
|
+
}
|
58
|
+
},
|
59
|
+
{
|
60
|
+
"host": "http://localhost:9002",
|
61
|
+
"status": 200,
|
62
|
+
"duration": 16
|
63
|
+
},
|
64
|
+
{
|
65
|
+
"host": "http://localhost:9003",
|
66
|
+
"status": 404,
|
67
|
+
"duration": 15
|
68
|
+
}
|
69
|
+
]
|
70
|
+
|
71
|
+
This JSON list contains a record for each backend that was called. Each record
|
72
|
+
contains the following fields:
|
73
|
+
|
74
|
+
* +host+ - the hostname of the backend server
|
75
|
+
* +status+ - a numetic HTTP status code if a response was received, else +nil+
|
76
|
+
* +duration+ - the request time in milliseconds
|
77
|
+
* +data+ - any JSON data returned by the backend
|
78
|
+
|
79
|
+
Every backend may return a JSON object using <tt>Content-Type:
|
80
|
+
application/json</tt>. If it does so, the data is included in the Telecaster
|
81
|
+
summary.
|
82
|
+
|
83
|
+
If the backend returns a JSON array, this is interpretted as Telecaster summary
|
84
|
+
data and merged in - this is how Telecaster aggregates responses from delegate
|
85
|
+
servers. So if you want to return custom data, the root of the response must be
|
86
|
+
a single JSON object.
|
87
|
+
|
88
|
+
|
89
|
+
== License
|
90
|
+
|
91
|
+
(The MIT License)
|
92
|
+
|
93
|
+
Copyright (c) 2012 Songkick.com
|
94
|
+
|
95
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
96
|
+
this software and associated documentation files (the 'Software'), to deal in
|
97
|
+
the Software without restriction, including without limitation the rights to use,
|
98
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
99
|
+
Software, and to permit persons to whom the Software is furnished to do so,
|
100
|
+
subject to the following conditions:
|
101
|
+
|
102
|
+
The above copyright notice and this permission notice shall be included in all
|
103
|
+
copies or substantial portions of the Software.
|
104
|
+
|
105
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
106
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
107
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
108
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
109
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
110
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
111
|
+
|
data/example/servers.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require File.expand_path('../../lib/telecaster', __FILE__)
|
3
|
+
require File.expand_path('../../spec/backends', __FILE__)
|
4
|
+
|
5
|
+
require 'logger'
|
6
|
+
require 'thin'
|
7
|
+
Thin::Logging.silent = true
|
8
|
+
|
9
|
+
EM.run {
|
10
|
+
Rack::Handler.get('thin').run(Responder, :Port => 9001)
|
11
|
+
Rack::Handler.get('thin').run(Blank, :Port => 9002)
|
12
|
+
Rack::Handler.get('thin').run(Error, :Port => 9003)
|
13
|
+
|
14
|
+
hosts = (1..3).map { |n| "http://localhost:900#{n}" }
|
15
|
+
logger = Logger.new($stdout)
|
16
|
+
telecaster = Telecaster.new(:backends => hosts, :logger => logger)
|
17
|
+
|
18
|
+
Rack::Handler.get('thin').run(telecaster, :Port => 9000)
|
19
|
+
}
|
20
|
+
|
data/lib/telecaster.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'em-http-request'
|
3
|
+
require 'yajl'
|
4
|
+
|
5
|
+
class Telecaster
|
6
|
+
ASYNC_RESPONSE = [-1, {}, []].freeze
|
7
|
+
TYPE_JSON = 'application/json'
|
8
|
+
|
9
|
+
ROOT = File.expand_path('..', __FILE__)
|
10
|
+
autoload :Multi, ROOT + '/telecaster/multi'
|
11
|
+
|
12
|
+
def initialize(options)
|
13
|
+
@backend_hosts = options[:backends]
|
14
|
+
@logger = options[:logger]
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(env)
|
18
|
+
ensure_reactor_running
|
19
|
+
|
20
|
+
method = env['REQUEST_METHOD'].downcase
|
21
|
+
multi = Multi.new
|
22
|
+
callback = env['async.callback']
|
23
|
+
start = Time.now.to_f
|
24
|
+
|
25
|
+
@backend_hosts.each do |host|
|
26
|
+
request, args = *create_request(env, host)
|
27
|
+
multi.add(host, request.__send__(method, args))
|
28
|
+
end
|
29
|
+
|
30
|
+
multi.callback do |response|
|
31
|
+
duration = ((Time.now.to_f - start) * 1000).round
|
32
|
+
summary = response.map { |r| '[' + [r['host'], r['status'], r['duration']] * ' ' + ']' }
|
33
|
+
@logger.info "#{env['REQUEST_METHOD']} #{env['REQUEST_URI']} backends:#{response.size} duration:#{duration} #{summary * ' '}"
|
34
|
+
|
35
|
+
json = Yajl::Encoder.encode(response, :pretty => true, :indent => ' ')
|
36
|
+
callback.call [200, {'Content-Type' => TYPE_JSON}, [json]]
|
37
|
+
end
|
38
|
+
|
39
|
+
ASYNC_RESPONSE
|
40
|
+
|
41
|
+
rescue => e
|
42
|
+
[500, {'Content-Type' => 'text/plain'}, [e.message]]
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def create_request(env, host)
|
48
|
+
uri = File.join(host, env['REQUEST_URI'])
|
49
|
+
request = EM::HttpRequest.new(uri)
|
50
|
+
args = {:head => {}}
|
51
|
+
|
52
|
+
env.each do |header, value|
|
53
|
+
next unless header =~ /^HTTP_/ and header != 'HTTP_HOST'
|
54
|
+
|
55
|
+
name = header.gsub(/^HTTP_/, '').gsub('_', '-').
|
56
|
+
downcase.
|
57
|
+
gsub(/(^|-)([a-z])/) { $1 + $2.upcase }
|
58
|
+
|
59
|
+
args[:head][name] = value
|
60
|
+
end
|
61
|
+
|
62
|
+
if length = env['CONTENT_LENGTH']
|
63
|
+
args[:head]['Content-Length'] = length
|
64
|
+
end
|
65
|
+
|
66
|
+
if type = env['CONTENT_TYPE']
|
67
|
+
args[:head]['Content-Type'] = type
|
68
|
+
end
|
69
|
+
|
70
|
+
if input = env['rack.input']
|
71
|
+
args[:body] = input.read
|
72
|
+
end
|
73
|
+
|
74
|
+
[request, args]
|
75
|
+
end
|
76
|
+
|
77
|
+
def ensure_reactor_running
|
78
|
+
return if EM.reactor_running?
|
79
|
+
Thread.new { EM.run }
|
80
|
+
Thread.pass until EM.reactor_running?
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# This is Telecaster::Multi, not a multitelecaster.
|
2
|
+
# http://farm4.static.flickr.com/3068/2369563217_bd2c623c53.jpg
|
3
|
+
|
4
|
+
class Telecaster
|
5
|
+
class Multi
|
6
|
+
include EM::Deferrable
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@requests = {}
|
10
|
+
@responses = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def add(host, request)
|
14
|
+
start_time = Time.now.to_f
|
15
|
+
@requests[host] = request
|
16
|
+
|
17
|
+
request.callback do |http|
|
18
|
+
response = {
|
19
|
+
'host' => host,
|
20
|
+
'status' => http.response_header.status,
|
21
|
+
'duration' => ((Time.now.to_f - start_time) * 1000).round
|
22
|
+
}
|
23
|
+
content_type = (http.response_header['CONTENT_TYPE'] || '').split(/\s*;\s*/).first
|
24
|
+
|
25
|
+
if content_type == TYPE_JSON
|
26
|
+
response['data'] = Yajl::Parser.parse(http.response)
|
27
|
+
end
|
28
|
+
@responses[host] = response
|
29
|
+
succeed if @responses.size == @requests.size
|
30
|
+
end
|
31
|
+
|
32
|
+
request.errback do |http|
|
33
|
+
response = {'host' => host, 'status' => nil}
|
34
|
+
@responses[host] = response
|
35
|
+
succeed if @responses.size == @requests.size
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def succeed
|
42
|
+
responses = []
|
43
|
+
@responses.each do |host, response|
|
44
|
+
if Array === response['data']
|
45
|
+
responses.concat(response['data'])
|
46
|
+
else
|
47
|
+
responses << response
|
48
|
+
end
|
49
|
+
end
|
50
|
+
super responses.sort_by { |r| r['host'] }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
data/spec/backends.rb
ADDED
@@ -0,0 +1,48 @@
|
|
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
ADDED
@@ -0,0 +1,25 @@
|
|
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
|
+
|
@@ -0,0 +1,159 @@
|
|
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
|
+
|
@@ -0,0 +1,55 @@
|
|
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
|
+
|
metadata
ADDED
@@ -0,0 +1,184 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: telecaster
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- James Coglan
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-09-19 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: em-http-request
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.3.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 0.3.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: eventmachine
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 0.12.0
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 0.12.0
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: yajl-ruby
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 1.0.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.0.0
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rspec
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 2.8.0
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 2.8.0
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: rack-proxy
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: rack-test
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: sinatra
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
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
|
+
description:
|
143
|
+
email: james@songkick.com
|
144
|
+
executables: []
|
145
|
+
extensions: []
|
146
|
+
extra_rdoc_files:
|
147
|
+
- README.rdoc
|
148
|
+
files:
|
149
|
+
- README.rdoc
|
150
|
+
- example/servers.rb
|
151
|
+
- lib/telecaster/multi.rb
|
152
|
+
- lib/telecaster.rb
|
153
|
+
- spec/backends.rb
|
154
|
+
- spec/spec_helper.rb
|
155
|
+
- spec/telecaster_spec.rb
|
156
|
+
- spec/telecaster_steps.rb
|
157
|
+
homepage: http://github.com/songkick/telecaster
|
158
|
+
licenses: []
|
159
|
+
post_install_message:
|
160
|
+
rdoc_options:
|
161
|
+
- --main
|
162
|
+
- README.rdoc
|
163
|
+
require_paths:
|
164
|
+
- lib
|
165
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
166
|
+
none: false
|
167
|
+
requirements:
|
168
|
+
- - ! '>='
|
169
|
+
- !ruby/object:Gem::Version
|
170
|
+
version: '0'
|
171
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
172
|
+
none: false
|
173
|
+
requirements:
|
174
|
+
- - ! '>='
|
175
|
+
- !ruby/object:Gem::Version
|
176
|
+
version: '0'
|
177
|
+
requirements: []
|
178
|
+
rubyforge_project:
|
179
|
+
rubygems_version: 1.8.23
|
180
|
+
signing_key:
|
181
|
+
specification_version: 3
|
182
|
+
summary: HTTP proxy for forwarding to multiple backends and collecting results
|
183
|
+
test_files: []
|
184
|
+
has_rdoc:
|