right_support 0.8.0 → 0.9.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|