telecaster 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -16,29 +16,22 @@ the backend responses will be aggregated by the master.
16
16
 
17
17
  == Usage
18
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:
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
- $ thin start -R config.ru -p 9000
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
@@ -7,14 +7,17 @@ require 'thin'
7
7
  Thin::Logging.silent = true
8
8
 
9
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)
10
+ thin = Rack::Handler.get('thin')
13
11
 
14
- hosts = (1..3).map { |n| "http://localhost:900#{n}" }
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
- Rack::Handler.get('thin').run(telecaster, :Port => 9000)
21
+ telecaster.listen(9000)
19
22
  }
20
23
 
@@ -1,35 +1,50 @@
1
1
  require 'eventmachine'
2
- require 'em-http-request'
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 = [-1, {}, []].freeze
7
- TYPE_JSON = 'application/json'
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 = options[: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
- start = Time.now.to_f
36
+ args = create_args(env)
24
37
 
25
38
  @backend_hosts.each do |host|
26
- request, args = *create_request(env, host)
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 create_request(env, host)
48
- uri = File.join(host, env['REQUEST_URI'])
49
- request = EM::HttpRequest.new(uri)
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
- [request, args]
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
@@ -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
- @responses[host] = response
29
- succeed if @responses.size == @requests.size
28
+ add_response(host, response)
30
29
  end
31
30
 
32
31
  request.errback do |http|
33
- response = {'host' => host, 'status' => nil}
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.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-19 00:00:00.000000000 Z
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
- - spec/backends.rb
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 forwarding to multiple backends and collecting results
178
+ summary: Multiplexing HTTP proxy for decoupled inter-service notifications
183
179
  test_files: []
184
- has_rdoc:
@@ -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
-
@@ -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
-
@@ -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
-
@@ -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
-