telecaster 0.1.1
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 +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:
|