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.
@@ -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
-