right_support 0.9.9 → 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +9 -6
- data/lib/right_support/db/cassandra_model.rb +1 -1
- data/lib/right_support/net/balancing/health_check.rb +124 -0
- data/lib/right_support/net/balancing/round_robin.rb +43 -0
- data/lib/right_support/net/balancing.rb +33 -0
- data/lib/right_support/net/http_client.rb +98 -0
- data/lib/right_support/net/request_balancer.rb +98 -15
- data/lib/right_support.rb +0 -6
- data/right_support.gemspec +2 -2
- metadata +9 -12
- data/lib/right_support/cassandra_model.rb +0 -3
- data/lib/right_support/filter_logger.rb +0 -3
- data/lib/right_support/net/request_balancer/policy.rb +0 -14
- data/lib/right_support/net/request_balancer/round_robin.rb +0 -7
- data/lib/right_support/net/rest.rb +0 -90
- data/lib/right_support/system_logger.rb +0 -3
- data/lib/right_support/tag_logger.rb +0 -3
data/README.rdoc
CHANGED
@@ -65,17 +65,20 @@ any exceptions. (NB: a nil return value counts as success!!) If you specify that
|
|
65
65
|
certain class of exception is "fatal," then that exception will cause REST to re-
|
66
66
|
raise immediately
|
67
67
|
|
68
|
-
==
|
68
|
+
== HTTPClient
|
69
69
|
|
70
70
|
We provide a very thin wrapper around the rest-client gem that enables simple but
|
71
71
|
robust rest requests with a timeout, headers, etc.
|
72
72
|
|
73
|
-
The
|
73
|
+
The HTTPClient is interface-compatible with the RestClient
|
74
74
|
module, but allows an optional timeout to be specified as an extra parameter.
|
75
|
-
|
75
|
+
|
76
|
+
# Create a wrapper's object
|
77
|
+
@client = RightSupport::Net::HTTPClient.new
|
78
|
+
|
76
79
|
# Default timeout is 5 seconds
|
77
|
-
|
80
|
+
@client.get('http://localhost')
|
78
81
|
|
79
|
-
# Make sure the
|
82
|
+
# Make sure the HTTPClient request fails after 1 second so we can report an error
|
80
83
|
# and move on!
|
81
|
-
|
84
|
+
@client.get('http://localhost', {:headers => {'X-Hello'=>'hi!'}, :timeout => 1)}
|
@@ -32,7 +32,7 @@ module RightSupport::DB
|
|
32
32
|
return @@conn if @@conn
|
33
33
|
|
34
34
|
config = @@config[ENV["RACK_ENV"]]
|
35
|
-
@@conn = Cassandra.new(keyspace, config["server"],{:timeout => RightSupport::CassandraModel::DEFAULT_TIMEOUT})
|
35
|
+
@@conn = Cassandra.new(keyspace, config["server"],{:timeout => RightSupport::DB::CassandraModel::DEFAULT_TIMEOUT})
|
36
36
|
@@conn.disable_node_auto_discovery!
|
37
37
|
@@conn
|
38
38
|
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2011 RightScale Inc
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
|
23
|
+
require 'set'
|
24
|
+
|
25
|
+
module RightSupport::Net::Balancing
|
26
|
+
|
27
|
+
class EndpointsStack
|
28
|
+
|
29
|
+
# Modified by Ryan Williamson on 9/28/2011 to ensure default values exist
|
30
|
+
# for @yellow_states and @reset_time
|
31
|
+
DEFAULT_YELLOW_STATES = 4
|
32
|
+
DEFAULT_RESET_TIME = 300
|
33
|
+
|
34
|
+
def initialize(endpoints, yellow_states=nil, reset_time=nil)
|
35
|
+
@endpoints = Hash.new
|
36
|
+
@yellow_states = yellow_states || DEFAULT_YELLOW_STATES
|
37
|
+
@reset_time = reset_time || DEFAULT_RESET_TIME
|
38
|
+
endpoints.each { |ep| @endpoints[ep] = {:n_level => 0,:timestamp => 0 }}
|
39
|
+
end
|
40
|
+
|
41
|
+
def sweep
|
42
|
+
@endpoints.each { |k,v| decrease_state(k,0,Time.now) if Float(Time.now - v[:timestamp]) > @reset_time }
|
43
|
+
end
|
44
|
+
|
45
|
+
def sweep_and_return_yellow_and_green
|
46
|
+
sweep
|
47
|
+
@endpoints.select { |k,v| v[:n_level] < @yellow_states }
|
48
|
+
end
|
49
|
+
|
50
|
+
def decrease_state(endpoint,t0,t1)
|
51
|
+
unless @endpoints[endpoint][:n_level] == 0
|
52
|
+
@endpoints[endpoint][:n_level] -= 1
|
53
|
+
@endpoints[endpoint][:timestamp] = t1
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def increase_state(endpoint,t0,t1)
|
58
|
+
unless @endpoints[endpoint][:n_level] == @yellow_states
|
59
|
+
@endpoints[endpoint][:n_level] += 1
|
60
|
+
@endpoints[endpoint][:timestamp] = t1
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
# Implementation concepts: endpoints have three states, red, yellow and green. Yellow
|
67
|
+
# has several levels (@yellow_states) to determine the health of the endpoint. The
|
68
|
+
# balancer works by avoiding "red" endpoints and retrying them after awhile. Here is a
|
69
|
+
# brief description of the state transitions:
|
70
|
+
# * green: last request was successful.
|
71
|
+
# * on success: remain green
|
72
|
+
# * on failure: change state to yellow and set it's health to healthiest (1)
|
73
|
+
# * red: skip this server
|
74
|
+
# * after @reset_time passes change state to yellow and set it's health to
|
75
|
+
# sickest (@yellow_states)
|
76
|
+
# * yellow: last request was either successful or failed
|
77
|
+
# * on success: change state to green if it's health was healthiest (1), else
|
78
|
+
# retain yellow state and improve it's health
|
79
|
+
# * on failure: change state to red if it's health was sickest (@yellow_states), else
|
80
|
+
# retain yellow state and decrease it's health
|
81
|
+
|
82
|
+
class HealthCheck
|
83
|
+
|
84
|
+
def initialize(endpoints,options = {})
|
85
|
+
# Modified by Ryan Williamson on 9/27/2011
|
86
|
+
# Previously if you created an instance of HealthCheck without the required options
|
87
|
+
# they would get passed as nil and overwrite EndpointsStack's default options causing an ArgumentError
|
88
|
+
yellow_states = options[:yellow_states]
|
89
|
+
reset_time = options[:reset_time]
|
90
|
+
# End modification
|
91
|
+
@health_check = options.delete(:health_check)
|
92
|
+
|
93
|
+
@stack = EndpointsStack.new(endpoints,yellow_states,reset_time)
|
94
|
+
@counter = rand(0xffff)
|
95
|
+
end
|
96
|
+
|
97
|
+
def next
|
98
|
+
# Returns the array of hashes which consists of yellow and green endpoints with the
|
99
|
+
# following structure: [ [EP1, {:n_level => ..., :timestamp => ... }], [EP2, ... ] ]
|
100
|
+
endpoints = @stack.sweep_and_return_yellow_and_green
|
101
|
+
return nil if endpoints.empty?
|
102
|
+
|
103
|
+
# Selection of the next endpoint using RoundRobin
|
104
|
+
@counter += 1
|
105
|
+
i = @counter % endpoints.size
|
106
|
+
|
107
|
+
# Returns false or true, depending on whether EP is yellow or not
|
108
|
+
[ endpoints[i][0], endpoints[i][1][:n_level] != 0 ]
|
109
|
+
end
|
110
|
+
|
111
|
+
def good(endpoint, t0, t1)
|
112
|
+
@stack.decrease_state(endpoint,t0,t1)
|
113
|
+
end
|
114
|
+
|
115
|
+
def bad(endpoint, t0, t1)
|
116
|
+
@stack.increase_state(endpoint,t0,t1)
|
117
|
+
end
|
118
|
+
|
119
|
+
def health_check(endpoint)
|
120
|
+
@stack.increase_state(endpoint,t0,Time.now) unless @health_check.call(endpoint)
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2011 RightScale Inc
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
|
23
|
+
module RightSupport::Net::Balancing
|
24
|
+
class RoundRobin
|
25
|
+
def initialize(endpoints,options ={})
|
26
|
+
@endpoints = endpoints
|
27
|
+
@counter = rand(0xffff)
|
28
|
+
end
|
29
|
+
|
30
|
+
def next
|
31
|
+
@counter += 1
|
32
|
+
[ @endpoints[@counter % @endpoints.size], false ]
|
33
|
+
end
|
34
|
+
|
35
|
+
def good(endpoint, t0, t1)
|
36
|
+
#no-op; round robin does not care about failures
|
37
|
+
end
|
38
|
+
|
39
|
+
def bad(endpoint, t0, t1)
|
40
|
+
#no-op; round robin does not care about failures
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2011 RightScale Inc
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
|
23
|
+
#
|
24
|
+
# A namespace to hold load-balancing policies to be used with RequestBalancer
|
25
|
+
# and potentially other networking classes.
|
26
|
+
#
|
27
|
+
module RightSupport::Net::Balancing
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
Dir[File.expand_path('../balancing/*.rb', __FILE__)].each do |filename|
|
32
|
+
require filename
|
33
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module RightSupport::Net
|
2
|
+
if_require_succeeds('right_http_connection') do
|
3
|
+
#nothing, nothing at all! just need to make sure
|
4
|
+
#that RightHttpConnection gets loaded before
|
5
|
+
#rest-client, so the Net::HTTP monkey patches
|
6
|
+
#take effect.
|
7
|
+
end
|
8
|
+
|
9
|
+
if_require_succeeds('restclient') do
|
10
|
+
HAS_REST_CLIENT = true
|
11
|
+
end
|
12
|
+
|
13
|
+
# Raised to indicate that no suitable provider of REST/HTTP services was found. Since RightSupport's
|
14
|
+
# REST support is merely a wrapper around other libraries, it cannot work in isolation. See the REST
|
15
|
+
# module for more information about supported providers.
|
16
|
+
class NoProvider < Exception; end
|
17
|
+
|
18
|
+
#
|
19
|
+
# A wrapper for the rest-client gem that provides timeouts and other useful features while preserving
|
20
|
+
# the simplicity and ease of use of RestClient's simple, static (class-level) interface.
|
21
|
+
#
|
22
|
+
# Even though this code relies on RestClient, the right_support gem does not depend on the rest-client
|
23
|
+
# gem because not all users of right_support will want to make use of this interface. If one of HTTPClient
|
24
|
+
# instance's method is called and RestClient is not available, an exception will be raised.
|
25
|
+
#
|
26
|
+
#
|
27
|
+
# HTTPClient supports a subset of the module methods provided by RestClient and is interface-compatible
|
28
|
+
# with those methods it replaces; the only difference is that the HTTPClient version of each method accepts an
|
29
|
+
# additional, optional parameter which is a request timeout in seconds. The RestClient gem does not allow
|
30
|
+
# timeouts without instantiating a "heavyweight" HTTPClient object.
|
31
|
+
#
|
32
|
+
# # create an instance ot HTTPClient
|
33
|
+
# @client = HTTPClient.new()
|
34
|
+
#
|
35
|
+
# # GET
|
36
|
+
# xml = @client.get 'http://example.com/resource'
|
37
|
+
# # and, with timeout of 5 seconds...
|
38
|
+
# jpg = @client.get 'http://example.com/resource', {:accept => 'image/jpg', :timeout => 5}
|
39
|
+
#
|
40
|
+
# # authentication and SSL
|
41
|
+
# @client.get 'https://user:password@example.com/private/resource'
|
42
|
+
#
|
43
|
+
# # POST or PUT with a hash sends parameters as a urlencoded form body
|
44
|
+
# @client.post 'http://example.com/resource', {:param1 => 'one'}
|
45
|
+
#
|
46
|
+
# # nest hash parameters, add a timeout of 10 seconds (and specify "no extra headers")
|
47
|
+
# @client.post 'http://example.com/resource', {:payload => {:nested => {:param1 => 'one'}}, :timeout => 10}
|
48
|
+
#
|
49
|
+
# # POST and PUT with raw payloads
|
50
|
+
# @client.post 'http://example.com/resource', {:payload => 'the post body', :headers => {:content_type => 'text/plain'}}
|
51
|
+
# @client.post 'http://example.com/resource.xml', {:payload => xml_doc}
|
52
|
+
# @client.put 'http://example.com/resource.pdf', {:payload => File.read('my.pdf'), :headers => {:content_type => 'application/pdf'}}
|
53
|
+
#
|
54
|
+
# # DELETE
|
55
|
+
# @client.delete 'http://example.com/resource'
|
56
|
+
#
|
57
|
+
# # retrieve the response http code and headers
|
58
|
+
# res = @client.get 'http://example.com/some.jpg'
|
59
|
+
# res.code # => 200
|
60
|
+
# res.headers[:content_type] # => 'image/jpg'
|
61
|
+
class HTTPClient
|
62
|
+
|
63
|
+
DEFAULT_TIMEOUT = 5
|
64
|
+
DEFAULT_OPEN_TIMEOUT = 2
|
65
|
+
|
66
|
+
def initialize(options = {})
|
67
|
+
[:get, :post, :put, :delete].each do |method|
|
68
|
+
define_instance_method(method) {|*args| query(method, *args)}
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
protected
|
73
|
+
|
74
|
+
# Helps to add default methods to class
|
75
|
+
def define_instance_method(method, &block)
|
76
|
+
(class << self; self; end).module_eval do
|
77
|
+
define_method(method, &block)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def query(type, url, options={}, &block)
|
82
|
+
options[:timeout] ||= DEFAULT_TIMEOUT
|
83
|
+
options[:open_timeout] ||= DEFAULT_OPEN_TIMEOUT
|
84
|
+
options[:headers] ||= {}
|
85
|
+
options.merge!(:method => type, :url => url)
|
86
|
+
request(options, &block)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Wrapper around RestClient::Request.execute -- see class documentation for details.
|
90
|
+
def request(options, &block)
|
91
|
+
if HAS_REST_CLIENT
|
92
|
+
RestClient::Request.execute(options, &block)
|
93
|
+
else
|
94
|
+
raise NoProvider, "Cannot find a suitable HTTP client library"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end# HTTPClient
|
98
|
+
end
|
@@ -2,11 +2,15 @@ module RightSupport::Net
|
|
2
2
|
# Raised to indicate the (uncommon) error condition where a RequestBalancer rotated
|
3
3
|
# through EVERY URL in a list without getting a non-nil, non-timeout response.
|
4
4
|
class NoResult < Exception; end
|
5
|
-
|
5
|
+
|
6
6
|
# Utility class that allows network requests to be randomly distributed across
|
7
7
|
# a set of network endpoints. Generally used for REST requests by passing an
|
8
8
|
# Array of HTTP service endpoint URLs.
|
9
9
|
#
|
10
|
+
# Note that this class also serves as a namespace for endpoint selection policies,
|
11
|
+
# which are classes that actually choose the next endpoint based on some criterion
|
12
|
+
# (round-robin, health of endpoint, response time, etc).
|
13
|
+
#
|
10
14
|
# The balancer does not actually perform requests by itself, which makes this
|
11
15
|
# class usable for various network protocols, and potentially even for non-
|
12
16
|
# networking purposes. The block does all the work; the balancer merely selects
|
@@ -18,6 +22,10 @@ module RightSupport::Net
|
|
18
22
|
# MAY NOT BE SUFFICIENT for some uses of the request balancer! Please use the :fatal
|
19
23
|
# option if you need different behavior.
|
20
24
|
class RequestBalancer
|
25
|
+
DEFAULT_RETRY_PROC = lambda do |ep, n|
|
26
|
+
n < ep.size
|
27
|
+
end
|
28
|
+
|
21
29
|
DEFAULT_FATAL_EXCEPTIONS = [ScriptError, ArgumentError, IndexError, LocalJumpError, NameError]
|
22
30
|
|
23
31
|
DEFAULT_FATAL_PROC = lambda do |e|
|
@@ -35,11 +43,28 @@ module RightSupport::Net
|
|
35
43
|
end
|
36
44
|
end
|
37
45
|
|
46
|
+
DEFAULT_HEALTH_CHECK_PROC = Proc.new do |endpoint|
|
47
|
+
true
|
48
|
+
end
|
49
|
+
|
38
50
|
DEFAULT_OPTIONS = {
|
51
|
+
:policy => nil,
|
52
|
+
:retry => DEFAULT_RETRY_PROC,
|
39
53
|
:fatal => DEFAULT_FATAL_PROC,
|
40
|
-
:on_exception => nil
|
54
|
+
:on_exception => nil,
|
55
|
+
:health_check => DEFAULT_HEALTH_CHECK_PROC
|
41
56
|
}
|
42
57
|
|
58
|
+
@@logger = nil
|
59
|
+
|
60
|
+
def self.logger
|
61
|
+
@@logger
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.logger=(logger)
|
65
|
+
@@logger = logger
|
66
|
+
end
|
67
|
+
|
43
68
|
def self.request(endpoints, options={}, &block)
|
44
69
|
new(endpoints, options).request(&block)
|
45
70
|
end
|
@@ -52,8 +77,10 @@ module RightSupport::Net
|
|
52
77
|
# endpoints(Array):: a set of network endpoints (e.g. HTTP URLs) to be load-balanced
|
53
78
|
#
|
54
79
|
# === Options
|
55
|
-
#
|
56
|
-
#
|
80
|
+
# retry:: a Class, array of Class or decision Proc to determine whether to keep retrying; default is to try all endpoints
|
81
|
+
# fatal:: a Class, array of Class, or decision Proc to determine whether an exception is fatal and should not be retried
|
82
|
+
# on_exception(Proc):: notification hook that accepts three arguments: whether the exception is fatal, the exception itself, and the endpoint for which the exception happened
|
83
|
+
# health_check(Proc):: callback that allows balancer to check an endpoint health; should raise an exception if the endpoint is not healthy
|
57
84
|
#
|
58
85
|
def initialize(endpoints, options={})
|
59
86
|
@options = DEFAULT_OPTIONS.merge(options)
|
@@ -62,7 +89,18 @@ module RightSupport::Net
|
|
62
89
|
raise ArgumentError, "Must specify at least one endpoint"
|
63
90
|
end
|
64
91
|
|
65
|
-
|
92
|
+
@options[:policy] ||= RightSupport::Net::Balancing::RoundRobin
|
93
|
+
@policy = @options[:policy]
|
94
|
+
@policy = @policy.new(endpoints,options) if @policy.is_a?(Class)
|
95
|
+
unless test_policy_duck_type(@policy)
|
96
|
+
raise ArgumentError, ":policy must be a class/object that responds to :next, :good and :bad"
|
97
|
+
end
|
98
|
+
|
99
|
+
unless test_callable_arity(options[:retry], 2)
|
100
|
+
raise ArgumentError, ":retry callback must accept two parameters"
|
101
|
+
end
|
102
|
+
|
103
|
+
unless test_callable_arity(options[:fatal], 1)
|
66
104
|
raise ArgumentError, ":fatal callback must accept one parameter"
|
67
105
|
end
|
68
106
|
|
@@ -70,6 +108,10 @@ module RightSupport::Net
|
|
70
108
|
raise ArgumentError, ":on_exception callback must accept three parameters"
|
71
109
|
end
|
72
110
|
|
111
|
+
unless test_callable_arity(options[:health_check], 1, false)
|
112
|
+
raise ArgumentError, ":health_check callback must accept one parameters"
|
113
|
+
end
|
114
|
+
|
73
115
|
@endpoints = endpoints.shuffle
|
74
116
|
end
|
75
117
|
|
@@ -94,27 +136,59 @@ module RightSupport::Net
|
|
94
136
|
complete = false
|
95
137
|
n = 0
|
96
138
|
|
97
|
-
|
98
|
-
|
139
|
+
retry_opt = @options[:retry] || DEFAULT_RETRY_PROC
|
140
|
+
health_check = @options[:health_check]
|
141
|
+
|
142
|
+
loop do
|
143
|
+
if complete
|
144
|
+
break
|
145
|
+
else
|
146
|
+
max_n = retry_opt
|
147
|
+
max_n = max_n.call(@endpoints, n) if max_n.respond_to?(:call)
|
148
|
+
break if (max_n.is_a?(Integer) && n >= max_n) || !(max_n)
|
149
|
+
end
|
150
|
+
|
151
|
+
endpoint, need_health_check = @policy.next
|
152
|
+
|
153
|
+
raise NoResult, "No endpoints are available" unless endpoint
|
99
154
|
n += 1
|
155
|
+
t0 = Time.now
|
156
|
+
|
157
|
+
# HealthCheck goes here
|
158
|
+
if need_health_check
|
159
|
+
begin
|
160
|
+
@policy.health_check(endpoint)
|
161
|
+
rescue Exception => e
|
162
|
+
@policy.bad(endpoint, t0, Time.now)
|
163
|
+
log_error("RequestBalancer: health check failed to #{endpoint} because of #{e.class.name}: #{e.message}")
|
164
|
+
next
|
165
|
+
end
|
166
|
+
|
167
|
+
log_info("RequestBalancer: health check succeeded to #{endpoint}")
|
168
|
+
end
|
100
169
|
|
101
170
|
begin
|
102
171
|
result = yield(endpoint)
|
172
|
+
@policy.good(endpoint, t0, Time.now)
|
103
173
|
complete = true
|
104
174
|
break
|
105
175
|
rescue Exception => e
|
176
|
+
@policy.bad(endpoint, t0, Time.now)
|
106
177
|
if to_raise = handle_exception(endpoint, e)
|
107
178
|
raise(to_raise)
|
108
179
|
else
|
109
180
|
exceptions << e
|
110
181
|
end
|
111
182
|
end
|
183
|
+
|
112
184
|
end
|
113
185
|
|
114
186
|
return result if complete
|
115
187
|
|
116
188
|
exceptions = exceptions.map { |e| e.class.name }.uniq.join(', ')
|
117
|
-
|
189
|
+
msg = "No available endpoints from #{@endpoints.inspect}! Exceptions: #{exceptions}"
|
190
|
+
log_error("RequestBalancer: #{msg}")
|
191
|
+
raise NoResult, msg
|
118
192
|
end
|
119
193
|
|
120
194
|
protected
|
@@ -135,7 +209,8 @@ module RightSupport::Net
|
|
135
209
|
#whether the exception we're handling is an instance of any mentioned exception
|
136
210
|
#class
|
137
211
|
fatal = fatal.any?{ |c| e.is_a?(c) } if fatal.respond_to?(:any?)
|
138
|
-
|
212
|
+
msg = "RequestBalancer: rescued #{fatal ? 'fatal' : 'retryable'} #{e.class.name} during request to #{endpoint}: #{e.message}"
|
213
|
+
log_error msg
|
139
214
|
@options[:on_exception].call(fatal, e, endpoint) if @options[:on_exception]
|
140
215
|
|
141
216
|
if fatal
|
@@ -146,20 +221,28 @@ module RightSupport::Net
|
|
146
221
|
end
|
147
222
|
end
|
148
223
|
|
149
|
-
def
|
150
|
-
|
151
|
-
result = @endpoints[ @round_robin % @endpoints.size ]
|
152
|
-
@round_robin += 1
|
153
|
-
return result
|
224
|
+
def test_policy_duck_type(object)
|
225
|
+
[:next, :good, :bad].all? { |m| object.respond_to?(m) }
|
154
226
|
end
|
155
227
|
|
156
228
|
# Test that something is a callable (Proc, Lambda or similar) with the expected arity.
|
157
229
|
# Used mainly by the initializer to test for correct options.
|
158
|
-
def test_callable_arity(callable, arity, optional)
|
230
|
+
def test_callable_arity(callable, arity, optional=true)
|
159
231
|
return true if callable.nil?
|
160
232
|
return true if optional && !callable.respond_to?(:call)
|
161
233
|
return callable.respond_to?(:arity) && (callable.arity == arity)
|
162
234
|
end
|
235
|
+
|
236
|
+
# Log an info message with the class logger, if provided
|
237
|
+
def log_info(*args)
|
238
|
+
self.class.logger.__send__(:info, *args) if self.class.logger.respond_to?(:info)
|
239
|
+
end
|
240
|
+
|
241
|
+
# Log an error message with the class logger, if provided
|
242
|
+
def log_error(*args)
|
243
|
+
self.class.logger.__send__(:error, *args) if self.class.logger.respond_to?(:error)
|
244
|
+
end
|
245
|
+
|
163
246
|
end # RequestBalancer
|
164
247
|
|
165
248
|
end # RightScale
|
data/lib/right_support.rb
CHANGED
@@ -5,9 +5,3 @@ require 'right_support/log'
|
|
5
5
|
require 'right_support/net'
|
6
6
|
require 'right_support/rack'
|
7
7
|
require 'right_support/validation'
|
8
|
-
|
9
|
-
# Deprecated stubs
|
10
|
-
require 'right_support/cassandra_model'
|
11
|
-
require 'right_support/filter_logger'
|
12
|
-
require 'right_support/system_logger'
|
13
|
-
require 'right_support/tag_logger'
|
data/right_support.gemspec
CHANGED
@@ -7,8 +7,8 @@ spec = Gem::Specification.new do |s|
|
|
7
7
|
s.required_ruby_version = Gem::Requirement.new(">= 1.8.7")
|
8
8
|
|
9
9
|
s.name = 'right_support'
|
10
|
-
s.version = '0.
|
11
|
-
s.date = '2011-
|
10
|
+
s.version = '1.0.4'
|
11
|
+
s.date = '2011-10-06'
|
12
12
|
|
13
13
|
s.authors = ['Tony Spataro']
|
14
14
|
s.email = 'tony@rightscale.com'
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: right_support
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 31
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
|
+
- 1
|
7
8
|
- 0
|
8
|
-
-
|
9
|
-
|
10
|
-
version: 0.9.9
|
9
|
+
- 4
|
10
|
+
version: 1.0.4
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Tony Spataro
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-
|
18
|
+
date: 2011-10-06 00:00:00 -07:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -136,26 +136,23 @@ files:
|
|
136
136
|
- LICENSE
|
137
137
|
- README.rdoc
|
138
138
|
- lib/right_support.rb
|
139
|
-
- lib/right_support/cassandra_model.rb
|
140
139
|
- lib/right_support/db.rb
|
141
140
|
- lib/right_support/db/cassandra_model.rb
|
142
|
-
- lib/right_support/filter_logger.rb
|
143
141
|
- lib/right_support/log.rb
|
144
142
|
- lib/right_support/log/filter_logger.rb
|
145
143
|
- lib/right_support/log/system_logger.rb
|
146
144
|
- lib/right_support/log/tag_logger.rb
|
147
145
|
- lib/right_support/net.rb
|
148
146
|
- lib/right_support/net/address_helper.rb
|
147
|
+
- lib/right_support/net/balancing.rb
|
148
|
+
- lib/right_support/net/balancing/health_check.rb
|
149
|
+
- lib/right_support/net/balancing/round_robin.rb
|
150
|
+
- lib/right_support/net/http_client.rb
|
149
151
|
- lib/right_support/net/request_balancer.rb
|
150
|
-
- lib/right_support/net/request_balancer/policy.rb
|
151
|
-
- lib/right_support/net/request_balancer/round_robin.rb
|
152
|
-
- lib/right_support/net/rest.rb
|
153
152
|
- lib/right_support/rack.rb
|
154
153
|
- lib/right_support/rack/custom_logger.rb
|
155
154
|
- lib/right_support/ruby.rb
|
156
155
|
- lib/right_support/ruby/object_extensions.rb
|
157
|
-
- lib/right_support/system_logger.rb
|
158
|
-
- lib/right_support/tag_logger.rb
|
159
156
|
- lib/right_support/validation.rb
|
160
157
|
- lib/right_support/validation/openssl.rb
|
161
158
|
- lib/right_support/validation/ssh.rb
|
@@ -1,90 +0,0 @@
|
|
1
|
-
module RightSupport::Net
|
2
|
-
begin
|
3
|
-
require 'right_http_connection'
|
4
|
-
rescue LoadError
|
5
|
-
end
|
6
|
-
if_require_succeeds('restclient') do
|
7
|
-
HAS_REST_CLIENT = true
|
8
|
-
end
|
9
|
-
|
10
|
-
# Raised to indicate that no suitable provider of REST/HTTP services was found. Since RightSupport's
|
11
|
-
# REST support is merely a wrapper around other libraries, it cannot work in isolation. See the REST
|
12
|
-
# module for more information about supported providers.
|
13
|
-
class NoProvider < Exception; end
|
14
|
-
|
15
|
-
#
|
16
|
-
# A wrapper for the rest-client gem that provides timeouts and other useful features while preserving
|
17
|
-
# the simplicity and ease of use of RestClient's simple, static (module-level) interface.
|
18
|
-
#
|
19
|
-
# Even though this code relies on RestClient, the right_support gem does not depend on the rest-client
|
20
|
-
# gem because not all users of right_support will want to make use of this interface. If one of REST's
|
21
|
-
# method is called and RestClient is not available, an exception will be raised.
|
22
|
-
#
|
23
|
-
#
|
24
|
-
# This module supports a subset of the module methods provided by RestClient and is interface-compatible
|
25
|
-
# with those methods it replaces; the only difference is that the REST version of each method accepts an
|
26
|
-
# additional, optional parameter which is a request timeout in seconds. The RestClient gem does not allow
|
27
|
-
# timeouts without instantiating a "heavyweight" REST client object.
|
28
|
-
#
|
29
|
-
# # GET
|
30
|
-
# xml = REST.get 'http://example.com/resource'
|
31
|
-
# # and, with timeout of 5 seconds...
|
32
|
-
# jpg = REST.get 'http://example.com/resource', :accept => 'image/jpg', 5
|
33
|
-
#
|
34
|
-
# # authentication and SSL
|
35
|
-
# REST.get 'https://user:password@example.com/private/resource'
|
36
|
-
#
|
37
|
-
# # POST or PUT with a hash sends parameters as a urlencoded form body
|
38
|
-
# REST.post 'http://example.com/resource', :param1 => 'one'
|
39
|
-
#
|
40
|
-
# # nest hash parameters, add a timeout of 10 seconds (and specify "no extra headers")
|
41
|
-
# REST.post 'http://example.com/resource', :nested => { :param1 => 'one' }, {}, 10
|
42
|
-
#
|
43
|
-
# # POST and PUT with raw payloads
|
44
|
-
# REST.post 'http://example.com/resource', 'the post body', :content_type => 'text/plain'
|
45
|
-
# REST.post 'http://example.com/resource.xml', xml_doc
|
46
|
-
# REST.put 'http://example.com/resource.pdf', File.read('my.pdf'), :content_type => 'application/pdf'
|
47
|
-
#
|
48
|
-
# # DELETE
|
49
|
-
# REST.delete 'http://example.com/resource'
|
50
|
-
#
|
51
|
-
# # retrieve the response http code and headers
|
52
|
-
# res = REST.get 'http://example.com/some.jpg'
|
53
|
-
# res.code # => 200
|
54
|
-
# res.headers[:content_type] # => 'image/jpg'
|
55
|
-
module REST
|
56
|
-
DEFAULT_TIMEOUT = 5
|
57
|
-
|
58
|
-
# Wrapper around RestClient.get -- see class documentation for details.
|
59
|
-
def self.get(url, headers={}, timeout=DEFAULT_TIMEOUT, &block)
|
60
|
-
request(:method=>:get, :url=>url, :timeout=>timeout, :headers=>headers, &block)
|
61
|
-
end
|
62
|
-
|
63
|
-
# Wrapper around RestClient.get -- see class documentation for details.
|
64
|
-
def self.post(url, payload, headers={}, timeout=DEFAULT_TIMEOUT, &block)
|
65
|
-
request(:method=>:post, :url=>url, :payload=>payload,
|
66
|
-
:timeout=>timeout, :headers=>headers, &block)
|
67
|
-
end
|
68
|
-
|
69
|
-
# Wrapper around RestClient.get -- see class documentation for details.
|
70
|
-
def self.put(url, payload, headers={}, timeout=DEFAULT_TIMEOUT, &block)
|
71
|
-
request(:method=>:put, :url=>url, :payload=>payload,
|
72
|
-
:timeout=>timeout, :headers=>headers, &block)
|
73
|
-
end
|
74
|
-
|
75
|
-
# Wrapper around RestClient.get -- see class documentation for details.
|
76
|
-
def self.delete(url, headers={}, timeout=DEFAULT_TIMEOUT, &block)
|
77
|
-
request(:method=>:delete, :url=>url, :timeout=>timeout, :headers=>headers, &block)
|
78
|
-
end
|
79
|
-
|
80
|
-
# Wrapper around RestClient::Request.execute -- see class documentation for details.
|
81
|
-
def self.request(options, &block)
|
82
|
-
if HAS_REST_CLIENT
|
83
|
-
RestClient::Request.execute(options, &block)
|
84
|
-
else
|
85
|
-
raise NoProvider, "Cannot find a suitable HTTP client library"
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
end
|
90
|
-
end
|