right_support 0.8.0 → 0.9.3
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 +28 -13
- data/lib/right_support/net/request_balancer.rb +122 -15
- data/lib/right_support/net/rest.rb +51 -4
- data/lib/right_support/system_logger.rb +5 -3
- data/right_support.gemspec +2 -2
- metadata +5 -5
data/README.rdoc
CHANGED
@@ -1,13 +1,16 @@
|
|
1
|
-
RightSupport is a library of reusable, unit-tested Ruby code that RightScale has found broadly useful.
|
1
|
+
RightSupport is a library of reusable, unit- and functional-tested Ruby code that RightScale has found broadly useful.
|
2
2
|
|
3
3
|
== What Does It Do?
|
4
4
|
|
5
5
|
=== Logging
|
6
6
|
|
7
7
|
SystemLogger is a rewrite of the seattle.rb SyslogLogger class that features the following improvements:
|
8
|
-
|
9
|
-
|
10
|
-
|
8
|
+
* Contains several bugfixes vs. SyslogLogger 1.4.0
|
9
|
+
* Inherits from standard Ruby Logger class for guaranteed compatibility
|
10
|
+
* Can be configured with options about how to handle newlines, ANSI escape codes, etc
|
11
|
+
|
12
|
+
It is very similar to SystemLogger, with a few differences (e.g. it is a child class of Logger instead of
|
13
|
+
merely being duck-type compatible). You use it as follows.
|
11
14
|
|
12
15
|
@logger = SystemLogger.new('my_cool_app', :split=>true, :color=>false)
|
13
16
|
@logger.info "Hello world\nThis will appear on separate lines\nand without \e[33;0mbeautiful colors"
|
@@ -28,9 +31,19 @@ your web app's models before saving, check for preconditions in your controllers
|
|
28
31
|
so forth.
|
29
32
|
|
30
33
|
You can use it as a mixin by including the appropriate child module of
|
31
|
-
RightSupport::Validation
|
32
|
-
|
33
|
-
|
34
|
+
RightSupport::Validation.
|
35
|
+
|
36
|
+
class AwesomenessGenerator < ActiveRecord::Base
|
37
|
+
include RightSupport::Validation::OpenSSL
|
38
|
+
|
39
|
+
before_save do |record|
|
40
|
+
errors[:foo] = 'Hey, that's not a key!' unless pem_public_key?(record.foo)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
But you really don't want to do that, do you? Instead, you want to call the module
|
45
|
+
methods of RightSupport::Validation, which contains all of the same mixin methods,
|
46
|
+
but does not pollute the dispatch table of your application classes.
|
34
47
|
|
35
48
|
the_key = STDIN.read
|
36
49
|
raise ArgumentError unless RightSupport::Validation.ssh_public_key?(the_key)
|
@@ -48,19 +61,21 @@ which lets you perform easy client-side load balancing:
|
|
48
61
|
end
|
49
62
|
|
50
63
|
The balancer will keep trying requests until one of them succeeds without throwing
|
51
|
-
any exceptions. (NB: a nil return value counts as success!!)
|
64
|
+
any exceptions. (NB: a nil return value counts as success!!) If you specify that a
|
65
|
+
certain class of exception is "fatal," then that exception will cause REST to re-
|
66
|
+
raise immediately
|
52
67
|
|
53
68
|
== HTTP REST Client
|
54
69
|
|
55
|
-
|
56
|
-
|
70
|
+
We provide a very thin wrapper around the rest-client gem that enables simple but
|
71
|
+
robust rest requests with a timeout, headers, etc.
|
57
72
|
|
58
|
-
|
59
|
-
|
73
|
+
The RightSupport::Net::REST module is interface-compatible with the RestClient
|
74
|
+
module, but allows an optional timeout to be specified as an extra parameter.
|
60
75
|
|
61
76
|
# Default timeout is 5 seconds
|
62
77
|
RightSupport::Net::REST.get('http://localhost')
|
63
78
|
|
64
79
|
# Make sure the REST request fails after 1 second so we can report an error
|
65
80
|
# and move on!
|
66
|
-
RightSupport::Net::REST.get('http://localhost', {'X-Hello'=>'hi!'}, 1)
|
81
|
+
RightSupport::Net::REST.get('http://localhost', {'X-Hello'=>'hi!'}, 1)
|
@@ -1,5 +1,7 @@
|
|
1
1
|
module RightSupport::Net
|
2
|
-
|
2
|
+
# Raised to indicate the (uncommon) error condition where a RequestBalancer rotated
|
3
|
+
# through EVERY URL in a list without getting a non-nil, non-timeout response.
|
4
|
+
class NoResult < Exception; end
|
3
5
|
|
4
6
|
# Utility class that allows network requests to be randomly distributed across
|
5
7
|
# a set of network endpoints. Generally used for REST requests by passing an
|
@@ -9,38 +11,143 @@ module RightSupport::Net
|
|
9
11
|
# class usable for various network protocols, and potentially even for non-
|
10
12
|
# networking purposes. The block does all the work; the balancer merely selects
|
11
13
|
# a random request endpoint to pass to the block.
|
14
|
+
#
|
15
|
+
# PLEASE NOTE that the request balancer has a rather dumb notion of what is considered
|
16
|
+
# a "fatal" error for purposes of being able to retry; by default, it will consider
|
17
|
+
# any StandardError or any RestClient::Exception whose code is between 400-499. This
|
18
|
+
# MAY NOT BE SUFFICIENT for some uses of the request balancer! Please use the :fatal
|
19
|
+
# option if you need different behavior.
|
12
20
|
class RequestBalancer
|
21
|
+
DEFAULT_FATAL_EXCEPTIONS = [ScriptError, ArgumentError, IndexError, LocalJumpError, NameError]
|
22
|
+
|
23
|
+
DEFAULT_FATAL_PROC = lambda do |e|
|
24
|
+
if DEFAULT_FATAL_EXCEPTIONS.any? { |c| e.is_a?(c) }
|
25
|
+
#Some Ruby builtin exceptions indicate program errors
|
26
|
+
true
|
27
|
+
elsif e.respond_to?(:http_code) && (e.http_code != nil)
|
28
|
+
#RestClient's exceptions all respond to http_code, allowing us
|
29
|
+
#to decide based on the HTTP response code.
|
30
|
+
#Any HTTP 4xx code EXCEPT 408 (Request Timeout) counts as fatal.
|
31
|
+
(e.http_code >= 400 && e.http_code < 500) && (e.http_code != 408)
|
32
|
+
else
|
33
|
+
#Anything else counts as non-fatal
|
34
|
+
false
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
DEFAULT_OPTIONS = {
|
39
|
+
:fatal => DEFAULT_FATAL_PROC,
|
40
|
+
:on_exception => nil
|
41
|
+
}
|
42
|
+
|
13
43
|
def self.request(endpoints, options={}, &block)
|
14
44
|
new(endpoints, options).request(&block)
|
15
45
|
end
|
16
46
|
|
47
|
+
# Constructor. Accepts a sequence of request endpoints which it shuffles randomly at
|
48
|
+
# creation time; however, the ordering of the endpoints does not change thereafter
|
49
|
+
# and the sequence is tried from the beginning for every request.
|
50
|
+
#
|
51
|
+
# === Parameters
|
52
|
+
# endpoints(Array):: a set of network endpoints (e.g. HTTP URLs) to be load-balanced
|
53
|
+
#
|
54
|
+
# === Options
|
55
|
+
# fatal(Class):: a class, list of classes or decision Proc to determine whether an exception is fatal and should not be retried
|
56
|
+
# on_exception(Proc|Lambda):: notification hook that accepts three arguments: whether the exception is fatal, the exception itself, and the endpoint for which the exception happened
|
57
|
+
#
|
17
58
|
def initialize(endpoints, options={})
|
18
|
-
|
59
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
60
|
+
|
61
|
+
unless endpoints && !endpoints.empty?
|
62
|
+
raise ArgumentError, "Must specify at least one endpoint"
|
63
|
+
end
|
64
|
+
|
65
|
+
unless test_callable_arity(options[:fatal], 1, true)
|
66
|
+
raise ArgumentError, ":fatal callback must accept one parameter"
|
67
|
+
end
|
68
|
+
|
69
|
+
unless test_callable_arity(options[:on_exception], 3, false)
|
70
|
+
raise ArgumentError, ":on_exception callback must accept three parameters"
|
71
|
+
end
|
72
|
+
|
19
73
|
@endpoints = endpoints.shuffle
|
20
|
-
@options = options.dup
|
21
74
|
end
|
22
75
|
|
76
|
+
# Perform a request.
|
77
|
+
#
|
78
|
+
# === Block
|
79
|
+
# This method requires a block, to which it yields in order to perform the actual network
|
80
|
+
# request. If the block raises an exception or provides nil, the balancer proceeds to try
|
81
|
+
# the next URL in the list.
|
82
|
+
#
|
83
|
+
# === Raise
|
84
|
+
# ArgumentError:: if a block isn't supplied
|
85
|
+
# NoResult:: if *every* URL in the list times out or returns nil
|
86
|
+
#
|
87
|
+
# === Return
|
88
|
+
# Return the first non-nil value provided by the block.
|
23
89
|
def request
|
24
90
|
raise ArgumentError, "Must call this method with a block" unless block_given?
|
25
91
|
|
26
|
-
|
27
|
-
result
|
92
|
+
exceptions = []
|
93
|
+
result = nil
|
94
|
+
success = false
|
28
95
|
|
29
|
-
@endpoints.each do |
|
96
|
+
@endpoints.each do |endpoint|
|
30
97
|
begin
|
31
|
-
result
|
32
|
-
|
98
|
+
result = yield(endpoint)
|
99
|
+
success = true
|
100
|
+
break
|
33
101
|
rescue Exception => e
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
102
|
+
if to_raise = handle_exception(endpoint, e)
|
103
|
+
raise(to_raise)
|
104
|
+
else
|
105
|
+
exceptions << e
|
106
|
+
end
|
38
107
|
end
|
39
108
|
end
|
40
109
|
|
41
|
-
return result if
|
42
|
-
|
43
|
-
|
110
|
+
return result if success
|
111
|
+
|
112
|
+
exceptions = exceptions.map { |e| e.class.name }.uniq.join(', ')
|
113
|
+
raise NoResult, "All URLs in the rotation failed! Exceptions: #{exceptions}"
|
114
|
+
end
|
115
|
+
|
116
|
+
protected
|
117
|
+
|
118
|
+
# Decide what to do with an exception. The decision is influenced by the :fatal
|
119
|
+
# option passed to the constructor.
|
120
|
+
def handle_exception(endpoint, e)
|
121
|
+
fatal = @options[:fatal] || DEFAULT_FATAL_PROC
|
122
|
+
|
123
|
+
#The option may be a proc or lambda; call it to get input
|
124
|
+
fatal = fatal.call(e) if fatal.respond_to?(:call)
|
125
|
+
|
126
|
+
#The options may be single exception classes, in which case we want to expand
|
127
|
+
#it out into a list
|
128
|
+
fatal = [fatal] if fatal.is_a?(Class)
|
129
|
+
|
130
|
+
#The option may be a list of exception classes, in which case we want to evaluate
|
131
|
+
#whether the exception we're handling is an instance of any mentioned exception
|
132
|
+
#class
|
133
|
+
fatal = fatal.any?{ |c| e.is_a?(c) } if fatal.respond_to?(:any?)
|
134
|
+
|
135
|
+
@options[:on_exception].call(fatal, e, endpoint) if @options[:on_exception]
|
136
|
+
|
137
|
+
if fatal
|
138
|
+
#Final decision: did we identify it as fatal?
|
139
|
+
return e
|
140
|
+
else
|
141
|
+
return nil
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Test that something is a callable (Proc, Lambda or similar) with the expected arity.
|
146
|
+
# Used mainly by the initializer to test for correct options.
|
147
|
+
def test_callable_arity(callable, arity, optional)
|
148
|
+
return true if callable.nil?
|
149
|
+
return true if optional && !callable.respond_to?(:call)
|
150
|
+
return callable.respond_to?(:arity) && (callable.arity == arity)
|
44
151
|
end
|
45
152
|
end # RequestBalancer
|
46
153
|
|
@@ -1,36 +1,83 @@
|
|
1
1
|
module RightSupport::Net
|
2
|
+
begin
|
3
|
+
require 'right_http_connection'
|
4
|
+
rescue LoadError
|
5
|
+
end
|
2
6
|
if_require_succeeds('restclient') do
|
3
7
|
HAS_REST_CLIENT = true
|
4
8
|
end
|
5
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.
|
6
13
|
class NoProvider < Exception; end
|
7
14
|
|
8
15
|
#
|
9
|
-
# A wrapper for the rest-client gem that provides timeouts and other
|
10
|
-
#
|
11
|
-
#
|
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'
|
12
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'
|
13
55
|
module REST
|
14
56
|
DEFAULT_TIMEOUT = 5
|
15
|
-
|
57
|
+
|
58
|
+
# Wrapper around RestClient.get -- see class documentation for details.
|
16
59
|
def self.get(url, headers={}, timeout=DEFAULT_TIMEOUT, &block)
|
17
60
|
request(:method=>:get, :url=>url, :timeout=>timeout, :headers=>headers, &block)
|
18
61
|
end
|
19
62
|
|
63
|
+
# Wrapper around RestClient.get -- see class documentation for details.
|
20
64
|
def self.post(url, payload, headers={}, timeout=DEFAULT_TIMEOUT, &block)
|
21
65
|
request(:method=>:post, :url=>url, :payload=>payload,
|
22
66
|
:timeout=>timeout, :headers=>headers, &block)
|
23
67
|
end
|
24
68
|
|
69
|
+
# Wrapper around RestClient.get -- see class documentation for details.
|
25
70
|
def self.put(url, payload, headers={}, timeout=DEFAULT_TIMEOUT, &block)
|
26
71
|
request(:method=>:put, :url=>url, :payload=>payload,
|
27
72
|
:timeout=>timeout, :headers=>headers, &block)
|
28
73
|
end
|
29
74
|
|
75
|
+
# Wrapper around RestClient.get -- see class documentation for details.
|
30
76
|
def self.delete(url, headers={}, timeout=DEFAULT_TIMEOUT, &block)
|
31
77
|
request(:method=>:delete, :url=>url, :timeout=>timeout, :headers=>headers, &block)
|
32
78
|
end
|
33
79
|
|
80
|
+
# Wrapper around RestClient::Request.execute -- see class documentation for details.
|
34
81
|
def self.request(options, &block)
|
35
82
|
if HAS_REST_CLIENT
|
36
83
|
RestClient::Request.execute(options, &block)
|
@@ -54,9 +54,9 @@ module RightSupport
|
|
54
54
|
# options(Hash):: (optional) configuration options to use, see below
|
55
55
|
#
|
56
56
|
# === Options
|
57
|
-
#
|
58
|
-
#
|
59
|
-
#
|
57
|
+
# facility:: the syslog facility to use for messages, 'local0' by default
|
58
|
+
# split(true|false):: if true, splits multi-line messages into separate syslog entries
|
59
|
+
# color(true|false):: if true, passes ANSI escape sequences through to syslog
|
60
60
|
#
|
61
61
|
def initialize(program_name='ruby', options={})
|
62
62
|
@options = DEFAULT_OPTIONS.merge(options)
|
@@ -65,6 +65,8 @@ module RightSupport
|
|
65
65
|
facility = options[:facility] || 'local0'
|
66
66
|
fac_map = {'user'=>8}
|
67
67
|
(0..7).each { |i| fac_map['local'+i.to_s] = 128+8*i }
|
68
|
+
|
69
|
+
@@syslog.close if @@syslog && @@syslog.opened?
|
68
70
|
@@syslog ||= Syslog.open(program_name, nil, fac_map[facility.to_s])
|
69
71
|
end
|
70
72
|
|
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 = '0.9.3'
|
11
|
+
s.date = '2011-05-13'
|
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: 61
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 0.
|
8
|
+
- 9
|
9
|
+
- 3
|
10
|
+
version: 0.9.3
|
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-05-13 00:00:00 -07:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|