tom 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm --create use ruby-1.9.2-p290@leonard
data/Gemfile ADDED
@@ -0,0 +1,23 @@
1
+ source "http://rubygems.org"
2
+
3
+ group :default do
4
+ gem "goliath"
5
+ gem "em-synchrony"
6
+ gem "em-http-request"
7
+ gem "mp-deployment", "0.0.21"
8
+ gem "json"
9
+ gem "rake"
10
+ end
11
+
12
+ # Add dependencies to develop your gem here.
13
+ # Include everything needed to run rake, tests, features, etc.
14
+ group :development do
15
+ gem "bundler", "~> 1.0.0"
16
+ gem "jeweler", "~> 1.6.4"
17
+ gem "shoulda", ">= 0"
18
+ gem "rcov", ">= 0"
19
+ gem "ruby-debug19"
20
+ gem "rspec"
21
+ gem "webmock"
22
+ gem "yard"
23
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,128 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ addressable (2.2.6)
5
+ archive-tar-minitar (0.5.2)
6
+ async-rack (0.5.1)
7
+ rack (~> 1.1)
8
+ capistrano (2.5.21)
9
+ highline
10
+ net-scp (>= 1.0.0)
11
+ net-sftp (>= 2.0.0)
12
+ net-ssh (>= 2.0.14)
13
+ net-ssh-gateway (>= 1.0.0)
14
+ capistrano-ext (1.2.1)
15
+ capistrano (>= 1.0.0)
16
+ columnize (0.3.5)
17
+ crack (0.3.1)
18
+ diff-lcs (1.1.3)
19
+ em-http-request (1.0.0)
20
+ addressable (>= 2.2.3)
21
+ em-socksify
22
+ eventmachine (>= 1.0.0.beta.3)
23
+ http_parser.rb (>= 0.5.2)
24
+ em-socksify (0.1.0)
25
+ eventmachine
26
+ em-synchrony (1.0.0)
27
+ eventmachine (>= 1.0.0.beta.1)
28
+ eventmachine (1.0.0.beta.4)
29
+ facter (1.6.3)
30
+ git (1.2.5)
31
+ goliath (0.9.4)
32
+ async-rack
33
+ em-synchrony (>= 1.0.0)
34
+ eventmachine (>= 1.0.0.beta.3)
35
+ http_parser.rb
36
+ http_router (~> 0.9.0)
37
+ log4r
38
+ multi_json
39
+ rack (>= 1.2.2)
40
+ rack-contrib
41
+ rack-respond_to
42
+ hashie (1.0.0)
43
+ highline (1.6.8)
44
+ http_parser.rb (0.5.3)
45
+ http_router (0.9.7)
46
+ rack (>= 1.0.0)
47
+ url_mount (~> 0.2.1)
48
+ jeweler (1.6.4)
49
+ bundler (~> 1.0)
50
+ git (>= 1.2.5)
51
+ rake
52
+ json (1.6.2)
53
+ linecache19 (0.5.12)
54
+ ruby_core_source (>= 0.1.4)
55
+ log4r (1.1.9)
56
+ mp-deployment (0.0.21)
57
+ capistrano (~> 2.5.20)
58
+ capistrano-ext (~> 1.2.1)
59
+ facter (~> 1.6.0)
60
+ hashie (~> 1.0.0)
61
+ newrelic_rpm (~> 3.3.0)
62
+ rpm_contrib (~> 2.1.3)
63
+ rvm (~> 1.6.20)
64
+ multi_json (1.0.3)
65
+ net-scp (1.0.4)
66
+ net-ssh (>= 1.99.1)
67
+ net-sftp (2.0.5)
68
+ net-ssh (>= 2.0.9)
69
+ net-ssh (2.2.1)
70
+ net-ssh-gateway (1.1.0)
71
+ net-ssh (>= 1.99.1)
72
+ newrelic_rpm (3.3.0)
73
+ rack (1.3.5)
74
+ rack-accept-media-types (0.9)
75
+ rack-contrib (1.1.0)
76
+ rack (>= 0.9.1)
77
+ rack-respond_to (0.9.8)
78
+ rack-accept-media-types (>= 0.6)
79
+ rake (0.9.2.2)
80
+ rcov (0.9.11)
81
+ rpm_contrib (2.1.6)
82
+ newrelic_rpm (>= 3.1.1)
83
+ newrelic_rpm (>= 3.1.1)
84
+ rspec (2.7.0)
85
+ rspec-core (~> 2.7.0)
86
+ rspec-expectations (~> 2.7.0)
87
+ rspec-mocks (~> 2.7.0)
88
+ rspec-core (2.7.1)
89
+ rspec-expectations (2.7.0)
90
+ diff-lcs (~> 1.1.2)
91
+ rspec-mocks (2.7.0)
92
+ ruby-debug-base19 (0.11.25)
93
+ columnize (>= 0.3.1)
94
+ linecache19 (>= 0.5.11)
95
+ ruby_core_source (>= 0.1.4)
96
+ ruby-debug19 (0.11.6)
97
+ columnize (>= 0.3.1)
98
+ linecache19 (>= 0.5.11)
99
+ ruby-debug-base19 (>= 0.11.19)
100
+ ruby_core_source (0.1.5)
101
+ archive-tar-minitar (>= 0.5.2)
102
+ rvm (1.6.32)
103
+ shoulda (2.11.3)
104
+ url_mount (0.2.1)
105
+ rack
106
+ webmock (1.7.8)
107
+ addressable (~> 2.2, > 2.2.5)
108
+ crack (>= 0.1.7)
109
+ yard (0.7.3)
110
+
111
+ PLATFORMS
112
+ ruby
113
+
114
+ DEPENDENCIES
115
+ bundler (~> 1.0.0)
116
+ em-http-request
117
+ em-synchrony
118
+ goliath
119
+ jeweler (~> 1.6.4)
120
+ json
121
+ mp-deployment (= 0.0.21)
122
+ rake
123
+ rcov
124
+ rspec
125
+ ruby-debug19
126
+ shoulda
127
+ webmock
128
+ yard
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Jannis Hermanns
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,112 @@
1
+ ![Tom Smykowski](http://dl.dropbox.com/u/1953503/tom%20smykowsky.jpg)
2
+ # Tom Smykowski - I have people skills!
3
+
4
+ Tom Smykowski, Tom for short, is a gem that takes the specifications from the customers and brings them down to the engineers. He has people skills. He is good at dealing with people. If this does not make sense to you, please refer to [this introductory video](http://www.youtube.com/watch?v=mGS2tKQhdhY) and, of course, [the movie](http://www.imdb.com/video/screenplay/vi3215851801/).
5
+
6
+ To go into technical detail: Tom uses [Goliath](http://goliath.io) to dispatch HTTP requests to multiple other APIs (via `Adapter`s) in parallel. In a next step, a `Merger` merges the result and responds to the clients request.
7
+
8
+ All you have to do is define some `Adapter`s that get activated for certain routes and some `Merger`s for certain routes.
9
+
10
+ # TL;DR
11
+
12
+ The general flow goes like this:
13
+
14
+ TIME Request
15
+ | |
16
+ | Dispatcher
17
+ | .__________|___________.
18
+ | | | |
19
+ | Adapter1 Adapter3 AdapterN (in parallel)
20
+ | |________. | ._________|
21
+ | | | |
22
+ | Merger
23
+ | |
24
+ V Response
25
+
26
+ So per request there can be many `Adapter`s that talk to different APIs, and one `Merger` that combines the responses of all APIs to one response.
27
+
28
+ # How to use
29
+ ## Tom::Dispatcher
30
+
31
+ This class basically does what is pictured in the flow above:
32
+
33
+ 1. Take the request
34
+ 2. Find all `Adapter`s that registered for a matching route
35
+ 3. Dispatch the requests to them
36
+ 4. Collect results
37
+ 5. Merge results into one and respond
38
+
39
+ To add APIs or change the behavior of Tom, you don't have to touch this class, though. `Adapter` and `Merger` is what you're looking for.
40
+
41
+ ## Tom::Adapter
42
+
43
+ The `Adapter` class comes with the class methods:
44
+
45
+ - `host=` (set the api to talk to)
46
+ - `rewrite_request` (overwrite if you don't want to pass on URIs 1:1)
47
+ - `forward_request` (takes env, calls the `rewrite_request` method and returns the result)
48
+
49
+ To hook Tom up to an API, you would first create an `Adapter` that inherits from the `Tom::Adapter` class, register some routes (they become regular expressions) and finally implement the logic by overwriting the `handle` method:
50
+
51
+ class Sheldon < Tom::Adapter
52
+ register_route "/nodes/*"
53
+
54
+ def self.handle(env)
55
+ # Insert biz logic here
56
+ forward_request(env)
57
+ end
58
+ end
59
+
60
+ The handle method takes a rack env, does some stuff like removing things that are not needed in Sheldon and then forwards the request on to the API.
61
+
62
+ In your initializer you can configure `Adapter`s like this:
63
+
64
+ Sheldon.host = 'http://localhost:9292'
65
+
66
+ This causes the aforementioned `rewrite_request` method to use that host name. The `forward_request` method uses an instance variable called `@request` to know what to do. `rewrite_request`, for example, takes a rack env and initializes `@request` with `:host`, `:method` and `:uri`.
67
+
68
+ If the method is `POST` or `PUT`, then you should also set the appropriate `@request[:body]` if you want to use the automatic `forward_request` method.
69
+
70
+ ## Tom::Merger
71
+
72
+ `Mergers` work very similar to `Adapter`s. But all you have to do here is subclass it and implement the `merge` method. For example we could create a `Merger` that always ignores everything and just returns the response from Sheldon:
73
+
74
+ class OnlySheldon < Tom::Merger
75
+
76
+ register_route "^/nodes/[0-9]+$"
77
+
78
+ def self.merge(env, responses)
79
+ responses[Sheldon]
80
+ end
81
+ end
82
+
83
+ You can use the rack `env` to decide what to do, and you get a hash of `responses` that has the Adapter class as keys and their respective rack `responses``:
84
+
85
+ { MyAdapter => [[200,{},"first response" ],
86
+ [200,{},"second response"]],
87
+ MyOtherAdapter => […]
88
+ }
89
+
90
+ ## On registering routes
91
+
92
+ If you call
93
+
94
+ class Foo < Tom::Adapter
95
+ register_route "^/nodes/[0-9]+$"
96
+
97
+ end
98
+
99
+ the adapter will be registered for all methods (namely `head`, `get`, `put`, `post`, `delete`). If you just want to register for some methods, you can do that with
100
+
101
+ register_route "^/nodes/[0-9]_$", :get, :put
102
+
103
+ Same goes for mergers.
104
+
105
+ # Todo
106
+
107
+ - handle adapter errors/states in mergers
108
+ - register routes with concurrency on and off
109
+ - use Goliath::Rack::Params
110
+ - use Goliath::Rack::Heartbeat
111
+ - think about consensus protocols
112
+ - ...
data/Rakefile ADDED
@@ -0,0 +1,68 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+
6
+ begin
7
+ Bundler.setup(:default, :development)
8
+ rescue Bundler::BundlerError => e
9
+ $stderr.puts e.message
10
+ $stderr.puts "Run `bundle install` to install missing gems"
11
+ exit e.status_code
12
+ end
13
+
14
+ require 'rake'
15
+ require 'yard'
16
+ require 'rspec/core/rake_task'
17
+ require 'jeweler'
18
+
19
+
20
+ #
21
+ # Jeweler Stuff
22
+ #
23
+ Jeweler::Tasks.new do |gem|
24
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
25
+ gem.name = "tom"
26
+ gem.homepage = "http://github.com/moviepilot/tom"
27
+ gem.license = "MIT"
28
+ gem.summary = %Q{Parallel request dispatcher and merger for goliath.io}
29
+ gem.description = %Q{ Tom uses Goliath to dispatch HTTP requests to multiple other APIs (via Adapters) in parallel. In a next step, a Merger merges the result and responds to the clients request.}
30
+ gem.email = "jannis@gmail.com"
31
+ gem.authors = ["Jannis Hermanns"]
32
+ # dependencies defined in Gemfile
33
+ end
34
+ Jeweler::RubygemsDotOrgTasks.new
35
+
36
+
37
+ #
38
+ # Rcov
39
+ #
40
+ require 'rcov/rcovtask'
41
+ Rcov::RcovTask.new do |test|
42
+ test.libs << 'test'
43
+ test.pattern = 'test/**/test_*.rb'
44
+ test.verbose = true
45
+ test.rcov_opts << '--exclude "gems/*"'
46
+ end
47
+
48
+
49
+ #
50
+ # RSpec
51
+ #
52
+ task :default => [:spec]
53
+ task :test => [:spec]
54
+ desc "run spec tests"
55
+ RSpec::Core::RakeTask.new('spec') do |t|
56
+ t.pattern = 'spec/**/*_spec.rb'
57
+ end
58
+
59
+
60
+ #
61
+ # Yard
62
+ #
63
+ desc 'Generate documentation'
64
+ YARD::Rake::YardocTask.new do |t|
65
+ t.files = ['lib/**/*.rb', '-', 'LICENSE']
66
+ t.options = ['--main', 'README.md', '--no-private']
67
+ end
68
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/lib/adapter.rb ADDED
@@ -0,0 +1,96 @@
1
+ require 'em-synchrony/em-http'
2
+ require_relative 'http'
3
+
4
+ module Tom
5
+ class Adapter
6
+ class << self
7
+ attr_accessor :host
8
+ end
9
+
10
+ #
11
+ # Registers a route with the request dispatcher
12
+ # so that this classes subclass gets called when
13
+ # a request is made. One that matches the route.
14
+ #
15
+ # The route can be a string, but it becomes a
16
+ # regular expression in here, followed by methods.
17
+ #
18
+ def self.register_route(*args)
19
+ route = args[0]
20
+ methods = args[1..-1]
21
+ Dispatcher.register(route: /#{route}/, adapter: self, methods: methods)
22
+ end
23
+
24
+ def initialize
25
+ @request = {}
26
+ end
27
+
28
+ #
29
+ # Takes a request from rack and issues the same
30
+ # request again, just to a different host. This
31
+ # method is to be used by subclasses.
32
+ #
33
+ def forward_request(env)
34
+ rewrite_request(env)
35
+ options = http_request_options(env)
36
+ url = @request[:host] + @request[:uri]
37
+
38
+ result = Tom::Http.make_request(@request[:method], url, options)
39
+
40
+ headers = {"Downstream-Url" => url}.merge result.response_header
41
+ [result.response_header.status, headers, result.response]
42
+ end
43
+
44
+ #
45
+ # Takes a request and generates the options for calling
46
+ # HttpRequest.put (or whatever the the requested
47
+ # REQUEST_METHOD is).
48
+ #
49
+ # It's content depends on the request method, for PUTs
50
+ # and POSTs this will add the request body
51
+ #
52
+ def http_request_options(env)
53
+ opts = {}
54
+ if [:put, :post].include? @request[:method]
55
+ opts[:body] = @request[:body] || extract_request_body(env)
56
+ end
57
+ opts
58
+ end
59
+
60
+ #
61
+ # Extracts the given POT/PUST (hehe) body
62
+ #
63
+ def extract_request_body(env)
64
+ Rack::Request.new(env).POST.keys.first rescue "{}"
65
+ end
66
+
67
+ #
68
+ # Takes a request from rack and extracts the request
69
+ # method, uri and returns the host this adapter talks
70
+ # to. Can be overwritten if you want to change stuff
71
+ # before forwarding it.
72
+ #
73
+ def rewrite_request(env)
74
+ rewritten = rewrite_host(env)
75
+ @request = rewritten.merge(@request)
76
+ end
77
+
78
+ def handle(env)
79
+ raise "Subclass, implement #handle(env)!"
80
+ end
81
+
82
+ private
83
+
84
+ #
85
+ # Returns a hash that can be used as the @request variable,
86
+ # which is exactly like the given env except for a changed
87
+ # hostname.
88
+ #
89
+ def rewrite_host(env)
90
+ { host: self.class.host,
91
+ uri: env["REQUEST_URI"],
92
+ method: env["REQUEST_METHOD"].downcase.to_sym
93
+ }
94
+ end
95
+ end
96
+ end
data/lib/dispatcher.rb ADDED
@@ -0,0 +1,160 @@
1
+ require "em-synchrony/fiber_iterator"
2
+ require 'logger'
3
+
4
+ module Tom
5
+
6
+ LOG = ::Logger.new(STDOUT)
7
+ LOG.level = ::Logger::ERROR
8
+ LOG.datetime_format = "%H:%M:%S:"
9
+ Logger::Formatter.module_eval(
10
+ %q{ def call(severity, time, progname, msg)} +
11
+ %q{ "#{format_datetime(time)} #{msg2str(msg)}\n" end}
12
+ )
13
+
14
+ class Dispatcher
15
+
16
+ #
17
+ # Dispatches this request to all adapters that registered
18
+ # for the route and then calls the merger for this route
19
+ # to compose a response
20
+ #
21
+ def self.dispatch(env)
22
+ adapters = adapters_for_route(env)
23
+ return [404, {}, '{reason: "No adapters for this route"}'] if adapters.empty?
24
+
25
+ # Hit APIs. All at the same time. Oh, mygodd!
26
+ responses = {}
27
+ Tom::LOG.info "#{env['REQUEST_METHOD'].upcase} #{env['REQUEST_URI']}"
28
+ Tom::LOG.info "Dispatching to:"
29
+ EM::Synchrony::FiberIterator.new(adapters, adapters.count).map do |clazz|
30
+ Tom::LOG.info " -> #{clazz}"
31
+ (responses[clazz] ||= []) << clazz.new.handle(env)
32
+ end
33
+
34
+ merged = merge(env, responses)
35
+ Tom::LOG.info "-------------------------------------------------------n"
36
+ merged
37
+ end
38
+
39
+ #
40
+ # Takes a request (rack env) and a couple of responses
41
+ # generated by api adapters and composes a response for the
42
+ # client.
43
+ #
44
+ # The merger used depends on the route.
45
+ #
46
+ def self.merge(env, responses)
47
+ merger = merger_for_route(env)
48
+ Tom::LOG.info "Merging with:"
49
+ Tom::LOG.info " -> #{merger}"
50
+ merger.new.merge env, responses
51
+ end
52
+
53
+ #
54
+ # Registers a opts[:adapter] or opts[:merger] for the
55
+ # given opts[:route].
56
+ #
57
+ # This method should not be called directly, use register_route
58
+ # in Tom::Adapter or Tom::Merger instead.
59
+ #
60
+ def self.register(opts)
61
+ return register_adapter(opts) if opts[:adapter]
62
+ return register_merger(opts) if opts[:merger]
63
+ raise "You need to supply opts[:adapter] or opts[:merger]"
64
+ end
65
+
66
+ private
67
+
68
+ #
69
+ # Registers an adapter for a given route and request method
70
+ #
71
+ def self.register_adapter(opts)
72
+ validate_type(opts[:adapter], Adapter)
73
+ methods = get_methods(opts)
74
+ @adapters ||= default_methods_hash
75
+ methods.each do |method|
76
+ @adapters[method][opts[:route]] ||= []
77
+ @adapters[method][opts[:route]] << opts[:adapter]
78
+ end
79
+ end
80
+
81
+ #
82
+ # Registers merger for a given route and request method
83
+ #
84
+ def self.register_merger(opts)
85
+ validate_type(opts[:merger], Merger)
86
+ methods = get_methods(opts)
87
+ @mergers ||= default_methods_hash
88
+ methods.each do |method|
89
+ @mergers[method][opts[:route]] ||= []
90
+ @mergers[method][opts[:route]] << opts[:merger]
91
+ end
92
+ end
93
+
94
+ #
95
+ # Fetches the methods from the options hash, defaults
96
+ # to all methods.
97
+ #
98
+ def self.get_methods(opts)
99
+ return opts[:methods] unless opts[:methods].empty?
100
+ [:head, :get, :put, :post, :delete]
101
+ end
102
+
103
+ #
104
+ # Just some defaults to initialize thing
105
+ #
106
+ def self.default_methods_hash
107
+ { head: {},
108
+ get: {},
109
+ put: {},
110
+ post: {},
111
+ delete: {}
112
+ }
113
+ end
114
+
115
+ #
116
+ # Find the right adapter for a route
117
+ #
118
+ def self.adapters_for_route(env)
119
+ @adapters ||= default_methods_hash
120
+ route, method = route_and_method(env)
121
+ matches = []
122
+ @adapters[method].map do |reg_route, adapters|
123
+ next unless reg_route.match(route)
124
+ matches += adapters
125
+ end
126
+ matches.uniq
127
+ end
128
+
129
+ #
130
+ # Find the right merger for a route
131
+ #
132
+ def self.merger_for_route(env)
133
+ @mergers ||= default_methods_hash
134
+ route, method = route_and_method(env)
135
+ @mergers[method].each do |reg_route, mergers|
136
+ next unless reg_route.match(route)
137
+ return mergers.first
138
+ end
139
+ raise "Found no merger for route #{route}"
140
+ end
141
+
142
+ #
143
+ # Extract the route/request uri and the method from a
144
+ # rack env
145
+ #
146
+ def self.route_and_method(env)
147
+ [env["REQUEST_PATH"],
148
+ env["REQUEST_METHOD"].downcase.to_sym]
149
+ end
150
+
151
+ #
152
+ # Make sure one class is a subclass of another class
153
+ #
154
+ def self.validate_type(c, expected)
155
+ return if c < expected
156
+ raise "Invalid type. Expected #{expected} got #{c}"
157
+ end
158
+
159
+ end
160
+ end
data/lib/http.rb ADDED
@@ -0,0 +1,35 @@
1
+ module Tom
2
+ module Http
3
+
4
+ #
5
+ # Makes a http request of the given method to the given url.
6
+ # Passes the options on to EM::HttpRequest.put (or whatever
7
+ # method has to be called) and does some error handling and
8
+ # works around some EM:HttpRequest oddities (see handle_errors).
9
+ #
10
+ def self.make_request(method, url, options = {})
11
+ Tom::LOG.info " curl -X#{method.upcase} -d '#{options[:body]}' #{url}"
12
+
13
+ conn = EM::HttpRequest.new(url, connection_options)
14
+ result = conn.send(method, options)
15
+ handle_errors(method, url, result)
16
+
17
+ result
18
+ end
19
+
20
+ private
21
+
22
+ def self.connection_options
23
+ { connect_timeout: Tom.config[:timeouts][:connect_timeout],
24
+ inactivity_timeout: Tom.config[:timeouts][:inactivity_timeout]}
25
+ end
26
+
27
+ def self.handle_errors(method, url, result)
28
+ result.errback do
29
+ raise "Tom::Adapter.forward_request error #{method} #{url}"
30
+ end
31
+ return unless result.response_header.status == 0
32
+ raise "EM::HttpRequest returned response code 0 for #{url} - timeout?"
33
+ end
34
+ end
35
+ end
data/lib/init.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'bundler'
2
+ Bundler.require :default
3
+
4
+ module Tom; end
5
+
6
+ require_relative 'dispatcher'
7
+ require_relative 'adapter'
8
+ require_relative 'merger'
data/lib/merger.rb ADDED
@@ -0,0 +1,34 @@
1
+ module Tom
2
+ class Merger
3
+
4
+ #
5
+ # Registers a route with the request dispatcher
6
+ # so that this classes subclass gets called when
7
+ # a request is made. One that matches the route.
8
+ #
9
+ # The route can be a string, but it becomes a
10
+ # regular expression in here. When matching in
11
+ # order to find a merger for a request, the first
12
+ # one matching wins.
13
+ #
14
+ def self.register_route(*args)
15
+ route = args[0]
16
+ methods = args[1..-1]
17
+ Dispatcher.register(route: /#{route}/, merger: self, methods: methods)
18
+ end
19
+
20
+ #
21
+ # When the request dispatcher made all the requests,
22
+ # it will call the merge method of the subclass with
23
+ # the responses as a hash in the form
24
+ #
25
+ # {MyAdapter: rack_env, MyOtherAdapter: other_env}
26
+ #
27
+ # Has to return a rack response (for example, something
28
+ # like [200, {}, "body"])
29
+ #
30
+ def merge(env, responses)
31
+ raise "Subclass, implement #merge(env, responses)!"
32
+ end
33
+ end
34
+ end
data/lib/tom.rb ADDED
@@ -0,0 +1,62 @@
1
+ require_relative 'init'
2
+
3
+ # Init Goliath env unless it was done already
4
+ Goliath.env rescue Goliath.env = (ENV['RACK_ENV'] || 'development').to_sym
5
+
6
+ # In dev mode, we do some logging (defaults to Logger::ERROR in other
7
+ # envs)
8
+ if Goliath.env == :development
9
+ Tom::LOG.level = Logger::INFO
10
+ end
11
+ Tom::LOG.info "Started goliath in #{Goliath.env} environment (change with ruby your_app.rb -e development or by setting $RACK_ENV)"
12
+
13
+ module Tom
14
+
15
+ def self.config
16
+ @config || default_config
17
+ end
18
+
19
+ def self.config=(config)
20
+ @config = config
21
+ end
22
+
23
+ #
24
+ # WE HAZ ALL TEH GOLIATH REQUESTS AND FORWARDETH
25
+ # THEM TO DEH DISPATCHERETH.
26
+ #
27
+ # We have to see if this is the right way to do
28
+ # it when it comes to parallel stuff and so on...
29
+ #
30
+ class GoliathAPI < Goliath::API
31
+ use Goliath::Rack::JSONP
32
+ use Goliath::Rack::Params
33
+ use Goliath::Rack::Formatters::JSON
34
+ use Goliath::Rack::Render
35
+
36
+ def response(env)
37
+ begin
38
+ Tom::Dispatcher.dispatch(env)
39
+ rescue => e
40
+ handle_exception e, env
41
+ end
42
+ end
43
+
44
+ def handle_exception(e, env)
45
+ trace = e.backtrace.join "\n"
46
+ Tom::LOG.info e
47
+ Tom::LOG.info trace
48
+ [500, {}, {error: e,
49
+ stacktrace: trace,
50
+ url: env["REQUEST_URI"]
51
+ }.to_json]
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def self.default_config
58
+ { timeouts:
59
+ { connect_timeout: 5,
60
+ inactivity_timeout: 10 } }
61
+ end
62
+ end
@@ -0,0 +1,35 @@
1
+ require_relative '../spec_helper'
2
+
3
+ class ForwardingAdapter1 < Tom::Adapter
4
+ register_route "^/manatees/[0-9]+$"
5
+ def handle(env)
6
+ forward_request(env)
7
+ end
8
+ end
9
+ ForwardingAdapter1.host = 'http://webmocked-host-1.com'
10
+
11
+ describe Tom::Adapter do
12
+
13
+ let(:env){ {'REQUEST_URI' => '/manatees/15?foo',
14
+ 'REQUEST_METHOD' => 'GET'} }
15
+
16
+ it "forward_request uses rewrite_request to change the host" do
17
+ rewritten = {host: "http://webmocked-host-1.com", uri: "/o", method: "get" }
18
+ ForwardingAdapter1.any_instance.should_receive(:rewrite_host).and_return(rewritten)
19
+ with_api(Tom::GoliathAPI) do
20
+ request = get_request(:path => '/manatees/15?foo')
21
+ end
22
+ end
23
+
24
+ it "rewrite_request changes the host" do
25
+ rewritten = ForwardingAdapter1.new.rewrite_request(env)
26
+
27
+ rewritten[:host].should == "http://webmocked-host-1.com"
28
+ rewritten[:uri].should == "/manatees/15?foo"
29
+ rewritten[:method].should == :get
30
+ end
31
+
32
+ it "rubs the lotion on its skin" do
33
+ true
34
+ end
35
+ end
@@ -0,0 +1,48 @@
1
+ require_relative '../spec_helper'
2
+
3
+ class APIAdapter1 < Tom::Adapter
4
+ register_route "^/manatees/[0-9]+$"
5
+ def handle(env);end
6
+ end
7
+ APIAdapter1.host = 'http://api_host_1.com'
8
+
9
+ class APIAdapter2 < Tom::Adapter
10
+ register_route "^/manatees/[0-9]+$"
11
+ def handle(env);end
12
+ end
13
+ APIAdapter2.host = 'http://api_host_2.com'
14
+
15
+ class Merger < Tom::Merger
16
+ register_route ".*"
17
+ end
18
+
19
+ describe Tom do
20
+
21
+ before(:each) do
22
+
23
+ end
24
+
25
+ it "emits a 404 when there are no adapters for the route" do
26
+ with_api(Tom::GoliathAPI) do
27
+ request = get_request(:path => '/walruses/5')
28
+ request.response_header.status.should == 404
29
+ end
30
+ end
31
+
32
+ it "makes requests to all registered adapters" do
33
+ expected_rack_env = hash_including("REQUEST_URI" => "/manatees/15",
34
+ "REQUEST_METHOD" => "GET")
35
+
36
+ APIAdapter1.any_instance.should_receive(:handle).with(expected_rack_env).and_return true
37
+ APIAdapter2.any_instance.should_receive(:handle).with(expected_rack_env).and_return true
38
+
39
+ with_api(Tom::GoliathAPI) do
40
+ request = get_request(:path => '/manatees/15')
41
+ end
42
+ end
43
+
44
+ # Until we figure out how to properly mock EM:HttpRequest's with em-synchrony
45
+ # and Webmock, testing doesn't make too much sense here. So we'll discard it
46
+ # until then.
47
+ it "merges the result"
48
+ end
@@ -0,0 +1,40 @@
1
+ require 'bundler'
2
+
3
+ Bundler.setup
4
+ Bundler.require :default, :test
5
+
6
+ require 'goliath/test_helper'
7
+ require 'ruby-debug'
8
+ require 'webmock/rspec'
9
+
10
+ Goliath.env = :test
11
+ RSpec.configure do |c|
12
+ c.include Goliath::TestHelper, :example_group => {
13
+ :file_path => /spec/
14
+ }
15
+ end
16
+
17
+ RSpec.configure do |config|
18
+ # Until we figured out the Webmock/em-synchrony/em-http-request woes
19
+ config.before(:suite) do
20
+ WebMock.allow_net_connect!
21
+ end
22
+
23
+ config.before(:each) do
24
+ # I don't always make http requests. But when I do,
25
+ # they are successful.
26
+ stub_request(:any, /.*webmocked-host.*/)
27
+ end
28
+
29
+ config.after(:each) do
30
+ EM.stop rescue nil
31
+ end
32
+
33
+ end
34
+
35
+ require_relative '../lib/tom'
36
+
37
+ class Merger < Tom::Merger
38
+ register_route ".*"
39
+ def merge(a,b);[200, {}, ""]; end
40
+ end
data/tom.gemspec ADDED
@@ -0,0 +1,96 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "tom"
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Jannis Hermanns"]
12
+ s.date = "2011-12-02"
13
+ s.description = " Tom uses Goliath to dispatch HTTP requests to multiple other APIs (via Adapters) in parallel. In a next step, a Merger merges the result and responds to the clients request."
14
+ s.email = "jannis@gmail.com"
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.markdown"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".rvmrc",
22
+ "Gemfile",
23
+ "Gemfile.lock",
24
+ "LICENSE.txt",
25
+ "README.markdown",
26
+ "Rakefile",
27
+ "VERSION",
28
+ "lib/adapter.rb",
29
+ "lib/dispatcher.rb",
30
+ "lib/http.rb",
31
+ "lib/init.rb",
32
+ "lib/merger.rb",
33
+ "lib/tom.rb",
34
+ "spec/lib/adapter_spec.rb",
35
+ "spec/lib/dispatcher_spec.rb",
36
+ "spec/spec_helper.rb",
37
+ "tom.gemspec"
38
+ ]
39
+ s.homepage = "http://github.com/moviepilot/tom"
40
+ s.licenses = ["MIT"]
41
+ s.require_paths = ["lib"]
42
+ s.rubygems_version = "1.8.10"
43
+ s.summary = "Parallel request dispatcher and merger for goliath.io"
44
+
45
+ if s.respond_to? :specification_version then
46
+ s.specification_version = 3
47
+
48
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
49
+ s.add_runtime_dependency(%q<goliath>, [">= 0"])
50
+ s.add_runtime_dependency(%q<em-synchrony>, [">= 0"])
51
+ s.add_runtime_dependency(%q<em-http-request>, [">= 0"])
52
+ s.add_runtime_dependency(%q<mp-deployment>, ["= 0.0.21"])
53
+ s.add_runtime_dependency(%q<json>, [">= 0"])
54
+ s.add_runtime_dependency(%q<rake>, [">= 0"])
55
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
56
+ s.add_development_dependency(%q<jeweler>, ["~> 1.6.4"])
57
+ s.add_development_dependency(%q<shoulda>, [">= 0"])
58
+ s.add_development_dependency(%q<rcov>, [">= 0"])
59
+ s.add_development_dependency(%q<ruby-debug19>, [">= 0"])
60
+ s.add_development_dependency(%q<rspec>, [">= 0"])
61
+ s.add_development_dependency(%q<webmock>, [">= 0"])
62
+ s.add_development_dependency(%q<yard>, [">= 0"])
63
+ else
64
+ s.add_dependency(%q<goliath>, [">= 0"])
65
+ s.add_dependency(%q<em-synchrony>, [">= 0"])
66
+ s.add_dependency(%q<em-http-request>, [">= 0"])
67
+ s.add_dependency(%q<mp-deployment>, ["= 0.0.21"])
68
+ s.add_dependency(%q<json>, [">= 0"])
69
+ s.add_dependency(%q<rake>, [">= 0"])
70
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
71
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
72
+ s.add_dependency(%q<shoulda>, [">= 0"])
73
+ s.add_dependency(%q<rcov>, [">= 0"])
74
+ s.add_dependency(%q<ruby-debug19>, [">= 0"])
75
+ s.add_dependency(%q<rspec>, [">= 0"])
76
+ s.add_dependency(%q<webmock>, [">= 0"])
77
+ s.add_dependency(%q<yard>, [">= 0"])
78
+ end
79
+ else
80
+ s.add_dependency(%q<goliath>, [">= 0"])
81
+ s.add_dependency(%q<em-synchrony>, [">= 0"])
82
+ s.add_dependency(%q<em-http-request>, [">= 0"])
83
+ s.add_dependency(%q<mp-deployment>, ["= 0.0.21"])
84
+ s.add_dependency(%q<json>, [">= 0"])
85
+ s.add_dependency(%q<rake>, [">= 0"])
86
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
87
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
88
+ s.add_dependency(%q<shoulda>, [">= 0"])
89
+ s.add_dependency(%q<rcov>, [">= 0"])
90
+ s.add_dependency(%q<ruby-debug19>, [">= 0"])
91
+ s.add_dependency(%q<rspec>, [">= 0"])
92
+ s.add_dependency(%q<webmock>, [">= 0"])
93
+ s.add_dependency(%q<yard>, [">= 0"])
94
+ end
95
+ end
96
+
metadata ADDED
@@ -0,0 +1,224 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tom
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jannis Hermanns
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-12-02 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: goliath
16
+ requirement: &70256849153920 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70256849153920
25
+ - !ruby/object:Gem::Dependency
26
+ name: em-synchrony
27
+ requirement: &70256849153340 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70256849153340
36
+ - !ruby/object:Gem::Dependency
37
+ name: em-http-request
38
+ requirement: &70256849152840 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70256849152840
47
+ - !ruby/object:Gem::Dependency
48
+ name: mp-deployment
49
+ requirement: &70256849152280 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - =
53
+ - !ruby/object:Gem::Version
54
+ version: 0.0.21
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *70256849152280
58
+ - !ruby/object:Gem::Dependency
59
+ name: json
60
+ requirement: &70256849151680 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :runtime
67
+ prerelease: false
68
+ version_requirements: *70256849151680
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: &70256849151080 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: *70256849151080
80
+ - !ruby/object:Gem::Dependency
81
+ name: bundler
82
+ requirement: &70256849150560 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ~>
86
+ - !ruby/object:Gem::Version
87
+ version: 1.0.0
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: *70256849150560
91
+ - !ruby/object:Gem::Dependency
92
+ name: jeweler
93
+ requirement: &70256849150000 !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ~>
97
+ - !ruby/object:Gem::Version
98
+ version: 1.6.4
99
+ type: :development
100
+ prerelease: false
101
+ version_requirements: *70256849150000
102
+ - !ruby/object:Gem::Dependency
103
+ name: shoulda
104
+ requirement: &70256849149420 !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: *70256849149420
113
+ - !ruby/object:Gem::Dependency
114
+ name: rcov
115
+ requirement: &70256849148740 !ruby/object:Gem::Requirement
116
+ none: false
117
+ requirements:
118
+ - - ! '>='
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ type: :development
122
+ prerelease: false
123
+ version_requirements: *70256849148740
124
+ - !ruby/object:Gem::Dependency
125
+ name: ruby-debug19
126
+ requirement: &70256849148120 !ruby/object:Gem::Requirement
127
+ none: false
128
+ requirements:
129
+ - - ! '>='
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: *70256849148120
135
+ - !ruby/object:Gem::Dependency
136
+ name: rspec
137
+ requirement: &70256849147600 !ruby/object:Gem::Requirement
138
+ none: false
139
+ requirements:
140
+ - - ! '>='
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ type: :development
144
+ prerelease: false
145
+ version_requirements: *70256849147600
146
+ - !ruby/object:Gem::Dependency
147
+ name: webmock
148
+ requirement: &70256849147020 !ruby/object:Gem::Requirement
149
+ none: false
150
+ requirements:
151
+ - - ! '>='
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ type: :development
155
+ prerelease: false
156
+ version_requirements: *70256849147020
157
+ - !ruby/object:Gem::Dependency
158
+ name: yard
159
+ requirement: &70256849146400 !ruby/object:Gem::Requirement
160
+ none: false
161
+ requirements:
162
+ - - ! '>='
163
+ - !ruby/object:Gem::Version
164
+ version: '0'
165
+ type: :development
166
+ prerelease: false
167
+ version_requirements: *70256849146400
168
+ description: ! ' Tom uses Goliath to dispatch HTTP requests to multiple other APIs
169
+ (via Adapters) in parallel. In a next step, a Merger merges the result and responds
170
+ to the clients request.'
171
+ email: jannis@gmail.com
172
+ executables: []
173
+ extensions: []
174
+ extra_rdoc_files:
175
+ - LICENSE.txt
176
+ - README.markdown
177
+ files:
178
+ - .document
179
+ - .rvmrc
180
+ - Gemfile
181
+ - Gemfile.lock
182
+ - LICENSE.txt
183
+ - README.markdown
184
+ - Rakefile
185
+ - VERSION
186
+ - lib/adapter.rb
187
+ - lib/dispatcher.rb
188
+ - lib/http.rb
189
+ - lib/init.rb
190
+ - lib/merger.rb
191
+ - lib/tom.rb
192
+ - spec/lib/adapter_spec.rb
193
+ - spec/lib/dispatcher_spec.rb
194
+ - spec/spec_helper.rb
195
+ - tom.gemspec
196
+ homepage: http://github.com/moviepilot/tom
197
+ licenses:
198
+ - MIT
199
+ post_install_message:
200
+ rdoc_options: []
201
+ require_paths:
202
+ - lib
203
+ required_ruby_version: !ruby/object:Gem::Requirement
204
+ none: false
205
+ requirements:
206
+ - - ! '>='
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
209
+ segments:
210
+ - 0
211
+ hash: -4016356889838983520
212
+ required_rubygems_version: !ruby/object:Gem::Requirement
213
+ none: false
214
+ requirements:
215
+ - - ! '>='
216
+ - !ruby/object:Gem::Version
217
+ version: '0'
218
+ requirements: []
219
+ rubyforge_project:
220
+ rubygems_version: 1.8.10
221
+ signing_key:
222
+ specification_version: 3
223
+ summary: Parallel request dispatcher and merger for goliath.io
224
+ test_files: []