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 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
- * 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
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, but you really don't want to do that, do you? Instead, you
32
- want to call the module methods of RightSupport::Validation, which contains all of
33
- the same mixin methods.
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
- We provide a very thin wrapper around the rest-client gem that enables simple but
56
- robust rest requests with a timeout, headers, etc.
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
- The RightSupport::Net::REST module is interface-compatible with the RestClient
59
- module, but allows an optional timeout to be specified as an extra parameter.
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
- class NoResponse < Exception; end
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
- raise ArgumentError, "Must specify at least one endpoint" unless endpoints && !endpoints.empty?
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
- exception = nil
27
- result = nil
92
+ exceptions = []
93
+ result = nil
94
+ success = false
28
95
 
29
- @endpoints.each do |host|
96
+ @endpoints.each do |endpoint|
30
97
  begin
31
- result = yield(host)
32
- break unless result.nil?
98
+ result = yield(endpoint)
99
+ success = true
100
+ break
33
101
  rescue Exception => e
34
- fatal = @options[:fatal]
35
- safe = @options[:safe]
36
- raise e if (fatal && e.kind_of?(fatal)) && !(safe && e.kind_of?(safe))
37
- exception = e
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 result
42
- raise exception if exception
43
- raise NoResponse, "Tried all URLs with neither result nor exception!"
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
- # useful features while preserving the simplicity and ease of use of
11
- # RestClient's simple, static (module-level) interface.
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
- # :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
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
 
@@ -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.8.0'
11
- s.date = '2011-04-09'
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: 63
4
+ hash: 61
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
- - 8
9
- - 0
10
- version: 0.8.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-04-09 00:00:00 -07:00
18
+ date: 2011-05-13 00:00:00 -07:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency