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 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