right_support 0.9.9 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +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
|