right_support 1.1.2 → 1.2.0

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.
@@ -29,8 +29,10 @@ module RightSupport
29
29
  end
30
30
  end
31
31
 
32
- Dir[File.expand_path('../net/*.rb', __FILE__)].each do |filename|
33
- require filename
34
- end
32
+ require 'right_support/net/address_helper'
33
+ require 'right_support/net/http_client'
34
+ require 'right_support/net/string_encoder'
35
+ require 'right_support/net/balancing'
36
+ require 'right_support/net/request_balancer'
35
37
 
36
38
  RightSupport::Net.extend(RightSupport::Net::AddressHelper)
@@ -31,45 +31,55 @@ module RightSupport::Net::Balancing
31
31
  DEFAULT_YELLOW_STATES = 4
32
32
  DEFAULT_RESET_TIME = 300
33
33
 
34
- def initialize(endpoints, yellow_states=nil, reset_time=nil)
34
+ def initialize(endpoints, yellow_states=nil, reset_time=nil, on_health_change=nil)
35
35
  @endpoints = Hash.new
36
36
  @yellow_states = yellow_states || DEFAULT_YELLOW_STATES
37
37
  @reset_time = reset_time || DEFAULT_RESET_TIME
38
- endpoints.each { |ep| @endpoints[ep] = {:n_level => 0,:timestamp => 0 }}
38
+ @on_health_change = on_health_change
39
+ @min_n_level = 0
40
+ endpoints.each { |ep| @endpoints[ep] = {:n_level => @min_n_level, :timestamp => 0} }
39
41
  end
40
42
 
41
43
  def sweep
42
- @endpoints.each { |k,v| decrease_state(k,0,Time.now) if Float(Time.now - v[:timestamp]) > @reset_time }
44
+ @endpoints.each { |k,v| decrease_state(k, 0, Time.now) if Float(Time.now - v[:timestamp]) > @reset_time }
43
45
  end
44
46
 
45
47
  def sweep_and_return_yellow_and_green
46
48
  sweep
47
49
  @endpoints.select { |k,v| v[:n_level] < @yellow_states }
48
50
  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
51
+
52
+ def decrease_state(endpoint, t0, t1)
53
+ update_state(endpoint, -1, t1) unless @endpoints[endpoint][:n_level] == 0
55
54
  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
55
+
56
+ def increase_state(endpoint, t0, t1)
57
+ update_state(endpoint, 1, t1) unless @endpoints[endpoint][:n_level] == @yellow_states
58
+ end
59
+
60
+ def update_state(endpoint, change, t1)
61
+ @endpoints[endpoint][:timestamp] = t1
62
+ n_level = @endpoints[endpoint][:n_level] += change
63
+ if @on_health_change &&
64
+ (n_level < @min_n_level ||
65
+ (n_level > @min_n_level && n_level == @endpoints.map { |(k, v)| v[:n_level] }.min))
66
+ @min_n_level = n_level
67
+ @on_health_change.call(state_color(n_level))
61
68
  end
62
69
  end
63
70
 
71
+ def state_color(n_level)
72
+ color = 'green' if n_level == 0
73
+ color = 'red' if n_level == @yellow_states
74
+ color = "yellow-#{n_level}" if n_level > 0 && n_level < @yellow_states
75
+ color
76
+ end
77
+
64
78
  # Returns a hash of endpoints and their colored health status
65
79
  # Useful for logging and debugging
66
80
  def get_stats
67
81
  stats = {}
68
- @endpoints.each do |k, v|
69
- stats[k] = 'green' if v[:n_level] == 0
70
- stats[k] = 'red' if v[:n_level] == @yellow_states
71
- stats[k] = "yellow-#{v[:n_level]}" if v[:n_level] > 0 && v[:n_level] < @yellow_states
72
- end
82
+ @endpoints.each { |k, v| stats[k] = state_color(v[:n_level]) }
73
83
  stats
74
84
  end
75
85
 
@@ -90,16 +100,23 @@ module RightSupport::Net::Balancing
90
100
  # retain yellow state and improve it's health
91
101
  # * on failure: change state to red if it's health was sickest (@yellow_states), else
92
102
  # retain yellow state and decrease it's health
103
+ # A callback option is provided to receive notification of changes in the overall
104
+ # health of the endpoints. The overall health starts out green. When the last
105
+ # endpoint transitions from green to yellow, a callback is made to report the overall
106
+ # health as yellow (or level of yellow). When the last endpoint transitions from yellow
107
+ # to red, a callback is made to report the transition to red. Similarly transitions are
108
+ # reported on the way back down, e.g., yellow is reported as soon as the first endpoint
109
+ # transitions from red to yellow, and so on.
93
110
 
94
111
  class HealthCheck
95
112
 
96
- def initialize(endpoints,options = {})
113
+ def initialize(endpoints, options = {})
97
114
  yellow_states = options[:yellow_states]
98
115
  reset_time = options[:reset_time]
99
116
 
100
117
  @health_check = options.delete(:health_check)
101
118
 
102
- @stack = EndpointsStack.new(endpoints,yellow_states,reset_time)
119
+ @stack = EndpointsStack.new(endpoints, yellow_states, reset_time, options[:on_health_change])
103
120
  @counter = rand(0xffff) % endpoints.size
104
121
  @last_size = endpoints.size
105
122
  end
@@ -128,11 +145,11 @@ module RightSupport::Net::Balancing
128
145
  end
129
146
 
130
147
  def good(endpoint, t0, t1)
131
- @stack.decrease_state(endpoint,t0,t1)
148
+ @stack.decrease_state(endpoint, t0, t1)
132
149
  end
133
150
 
134
151
  def bad(endpoint, t0, t1)
135
- @stack.increase_state(endpoint,t0,t1)
152
+ @stack.increase_state(endpoint, t0, t1)
136
153
  end
137
154
 
138
155
  def health_check(endpoint)
@@ -22,6 +22,8 @@ module RightSupport::Net
22
22
  # default list of fatal exceptions and default logic for deciding whether a
23
23
  # given exception is fatal! There are some subtleties.
24
24
  class RequestBalancer
25
+ include RightSupport::Log::Mixin
26
+
25
27
  DEFAULT_RETRY_PROC = lambda do |ep, n|
26
28
  n < ep.size
27
29
  end
@@ -68,16 +70,6 @@ module RightSupport::Net
68
70
  :health_check => DEFAULT_HEALTH_CHECK_PROC
69
71
  }
70
72
 
71
- @@logger = nil
72
-
73
- def self.logger
74
- @@logger
75
- end
76
-
77
- def self.logger=(logger)
78
- @@logger = logger
79
- end
80
-
81
73
  def self.request(endpoints, options={}, &block)
82
74
  new(endpoints, options).request(&block)
83
75
  end
@@ -92,8 +84,12 @@ module RightSupport::Net
92
84
  # === Options
93
85
  # retry:: a Class, array of Class or decision Proc to determine whether to keep retrying; default is to try all endpoints
94
86
  # fatal:: a Class, array of Class, or decision Proc to determine whether an exception is fatal and should not be retried
95
- # 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
96
- # health_check(Proc):: callback that allows balancer to check an endpoint health; should raise an exception if the endpoint is not healthy
87
+ # on_exception(Proc):: notification hook that accepts three arguments: whether the exception is fatal, the exception itself,
88
+ # and the endpoint for which the exception happened
89
+ # health_check(Proc):: callback that allows balancer to check an endpoint health; should raise an exception if the endpoint
90
+ # is not healthy
91
+ # on_health_change(Proc):: callback that is made when the overall health of the endpoints transition to a different level;
92
+ # its single argument contains the new minimum health level
97
93
  #
98
94
  def initialize(endpoints, options={})
99
95
  @options = DEFAULT_OPTIONS.merge(options)
@@ -104,7 +100,7 @@ module RightSupport::Net
104
100
 
105
101
  @options[:policy] ||= RightSupport::Net::Balancing::RoundRobin
106
102
  @policy = @options[:policy]
107
- @policy = @policy.new(endpoints,options) if @policy.is_a?(Class)
103
+ @policy = @policy.new(endpoints, options) if @policy.is_a?(Class)
108
104
  unless test_policy_duck_type(@policy)
109
105
  raise ArgumentError, ":policy must be a class/object that responds to :next, :good and :bad"
110
106
  end
@@ -122,7 +118,11 @@ module RightSupport::Net
122
118
  end
123
119
 
124
120
  unless test_callable_arity(options[:health_check], 1, false)
125
- raise ArgumentError, ":health_check callback must accept one parameters"
121
+ raise ArgumentError, ":health_check callback must accept one parameter"
122
+ end
123
+
124
+ unless test_callable_arity(options[:on_health_change], 1, false)
125
+ raise ArgumentError, ":on_health_change callback must accept one parameter"
126
126
  end
127
127
 
128
128
  @endpoints = endpoints
@@ -169,15 +169,15 @@ module RightSupport::Net
169
169
  if need_health_check
170
170
  begin
171
171
  unless @policy.health_check(endpoint)
172
- log_error("RequestBalancer: health check failed to #{endpoint} because of non-true return value")
172
+ logger.error "RequestBalancer: health check failed to #{endpoint} because of non-true return value"
173
173
  next
174
174
  end
175
175
  rescue Exception => e
176
- log_error("RequestBalancer: health check failed to #{endpoint} because of #{e.class.name}: #{e.message}")
176
+ logger.error "RequestBalancer: health check failed to #{endpoint} because of #{e.class.name}: #{e.message}"
177
177
  next
178
178
  end
179
179
 
180
- log_info("RequestBalancer: health check succeeded to #{endpoint}")
180
+ logger.info "RequestBalancer: health check succeeded to #{endpoint}"
181
181
  end
182
182
 
183
183
  begin
@@ -200,7 +200,7 @@ module RightSupport::Net
200
200
 
201
201
  exceptions = exceptions.map { |e| e.class.name }.uniq.join(', ')
202
202
  msg = "No available endpoints from #{@endpoints.inspect}! Exceptions: #{exceptions}"
203
- log_error("RequestBalancer: #{msg}")
203
+ logger.error "RequestBalancer: #{msg}"
204
204
  raise NoResult, msg
205
205
  end
206
206
 
@@ -247,7 +247,7 @@ module RightSupport::Net
247
247
  fatal = fatal.any?{ |c| e.is_a?(c) } if fatal.respond_to?(:any?)
248
248
  duration = sprintf('%.4f', Time.now - t0)
249
249
  msg = "RequestBalancer: rescued #{fatal ? 'fatal' : 'retryable'} #{e.class.name} during request to #{endpoint}: #{e.message} after #{duration} seconds"
250
- log_error msg
250
+ logger.error msg
251
251
  @options[:on_exception].call(fatal, e, endpoint) if @options[:on_exception]
252
252
 
253
253
  if fatal
@@ -269,19 +269,6 @@ module RightSupport::Net
269
269
  return true if optional && !callable.respond_to?(:call)
270
270
  return callable.respond_to?(:arity) && (callable.arity == arity)
271
271
  end
272
-
273
- # Log an info message with the class logger, if provided. Can't duck type because some loggers
274
- # use fallback methods to perform their logging and don't respond_to?() :info or :error
275
- def log_info(*args)
276
- self.class.logger.__send__(:info, *args) unless self.class.logger.nil?
277
- end
278
-
279
- # Log an error message with the class logger, if provided. Can't duck type because some loggers
280
- # use fallback methods to perform their logging and don't respond_to?() :info or :error
281
- def log_error(*args)
282
- self.class.logger.__send__(:error, *args) unless self.class.logger.nil?
283
- end
284
-
285
272
  end # RequestBalancer
286
273
 
287
274
  end # RightScale
@@ -29,6 +29,5 @@ module RightSupport
29
29
  end
30
30
  end
31
31
 
32
- Dir[File.expand_path('../rack/*.rb', __FILE__)].each do |filename|
33
- require filename
34
- end
32
+ require 'right_support/rack/custom_logger'
33
+ require 'right_support/rack/request_logger'
@@ -28,19 +28,30 @@ module RightSupport::Rack
28
28
  # to a file-based Logger and doesn't allow you to control anything other than the
29
29
  # filename.
30
30
  class CustomLogger
31
- # Initialize an instance of the middleware.
31
+ # Initialize an instance of the middleware. For backward compatibility, the order of the
32
+ # logger and level parameters can be switched.
32
33
  #
33
34
  # === Parameters
34
35
  # app(Object):: the inner application or middleware layer; must respond to #call
35
- # level(Integer):: one of the Logger constants: DEBUG, INFO, WARN, ERROR, FATAL
36
- # logger(Logger):: (optional) the Logger object to use, if other than default
36
+ # logger(Logger):: (optional) the Logger object to use, defaults to a STDERR logger
37
+ # level(Integer):: (optional) a Logger level-constant (INFO, ERROR) to set the logger to
37
38
  #
38
- def initialize(app, level = ::Logger::INFO, logger = nil)
39
- @app, @level = app, level
39
+ def initialize(app, arg1=nil, arg2=nil)
40
+ if arg1.is_a?(Integer)
41
+ level = arg1
42
+ elsif arg1.is_a?(Logger)
43
+ logger = arg1
44
+ end
40
45
 
41
- logger ||= ::Logger.new(env['rack.errors'])
42
- logger.level = @level
46
+ if arg2.is_a?(Integer)
47
+ level = arg2
48
+ elsif arg2.is_a?(Logger)
49
+ logger = arg2
50
+ end
51
+
52
+ @app = app
43
53
  @logger = logger
54
+ @level = level
44
55
  end
45
56
 
46
57
  # Add a logger to the Rack environment and call the next middleware.
@@ -51,8 +62,18 @@ module RightSupport::Rack
51
62
  # === Return
52
63
  # always returns whatever value is returned by the next layer of middleware
53
64
  def call(env)
65
+ #emulate the behavior of Rack::CommonLogger middleware, which instantiates a
66
+ #default logger if one has not been provided in the initializer
67
+ @logger = ::Logger.new(env['rack.errors'] || STDERR) unless @logger
68
+
69
+ if @level
70
+ old_level = @logger.level
71
+ @logger.level = @level
72
+ end
54
73
  env['rack.logger'] = @logger
55
- return @app.call(env)
74
+ status, header, body = @app.call(env)
75
+ @logger.level = old_level if @level
76
+ return [status, header, body]
56
77
  end
57
78
  end
58
79
  end
@@ -0,0 +1,113 @@
1
+ #
2
+ # Copyright (c) 2012 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 'logger'
24
+
25
+ module RightSupport::Rack
26
+ # A Rack middleware that logs information about every HTTP request received and
27
+ # every exception raised while processing a request.
28
+ #
29
+ # The middleware can be configured to use its own logger, but defaults to using
30
+ # env['rack.logger'] for logging if it is present. If 'rack.logger' is not set,
31
+ # this middleware will set it before calling the next middleware. Therefore,
32
+ # RequestLogger can be used standalone to fulfill all logging needs, or combined
33
+ # with Rack::Logger or another middleware that provides logging services.
34
+ class RequestLogger
35
+ # Initialize an instance of the middleware. For backward compatibility, the order of the
36
+ # logger and level parameters can be switched.
37
+ #
38
+ # === Parameters
39
+ # app(Object):: the inner application or middleware layer; must respond to #call
40
+ # logger(Logger):: (optional) the Logger object to use, defaults to a STDERR logger
41
+ # level(Integer):: (optional) a Logger level-constant (INFO, ERROR) to set the logger to
42
+ #
43
+ def initialize(app, options={})
44
+ @app = app
45
+ @logger = options[:logger]
46
+ end
47
+
48
+ # Add a logger to the Rack environment and call the next middleware.
49
+ #
50
+ # === Parameters
51
+ # env(Hash):: the Rack environment
52
+ #
53
+ # === Return
54
+ # always returns whatever value is returned by the next layer of middleware
55
+ def call(env)
56
+ if @logger
57
+ logger = @logger
58
+ elsif env['rack.logger']
59
+ logger = env['rack.logger']
60
+ end
61
+
62
+ env['rack.logger'] ||= logger
63
+
64
+ began_at = Time.now
65
+ status, header, body = @app.call(env)
66
+ log_request(logger, env, status, began_at)
67
+ log_exception(logger, env['sinatra.error']) if env['sinatra.error']
68
+
69
+ return [status, header, body]
70
+ rescue Exception => e
71
+ log_exception(logger, e)
72
+ raise e
73
+ end
74
+
75
+ private
76
+
77
+ # NON Logger functions below
78
+ def log_request(logger, env, status, began_at)
79
+ duration = Time.now - began_at
80
+
81
+ # Assuming remote addresses are IPv4, make them all align to the same width
82
+ remote_addr = env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-"
83
+ remote_addr = remote_addr.ljust(15)
84
+
85
+ # Log the fact that a query string was present, but do not log its contents
86
+ # because it may have sensitive data.
87
+ if (query = env["QUERY_STRING"]) && !query.empty?
88
+ query_info = '?...'
89
+ else
90
+ query_info = ''
91
+ end
92
+
93
+ params = [
94
+ remote_addr,
95
+ env["REQUEST_METHOD"],
96
+ env["PATH_INFO"],
97
+ query_info,
98
+ env["HTTP_VERSION"],
99
+ status,
100
+ duration
101
+ ]
102
+
103
+ logger.info %Q{%s "%s %s%s %s" %d %0.3f} % params
104
+ end
105
+
106
+ def log_exception(logger, e)
107
+ msg = ["#{e.class} - #{e.message}", *e.backtrace].join("\n")
108
+ logger.error(msg)
109
+ rescue
110
+ #no-op, something is seriously messed up by this point...
111
+ end
112
+ end
113
+ end
@@ -29,6 +29,7 @@ module RightSupport
29
29
  end
30
30
  end
31
31
 
32
- Dir[File.expand_path('../ruby/*.rb', __FILE__)].each do |filename|
33
- require filename
34
- end
32
+ # object_extensions must be first
33
+ require 'right_support/ruby/object_extensions'
34
+ require 'right_support/ruby/string_extensions'
35
+ require 'right_support/ruby/easy_singleton'
@@ -0,0 +1,24 @@
1
+ require 'singleton'
2
+
3
+ module RightSupport::Ruby
4
+ module EasySingleton
5
+ module ClassMethods
6
+ def method_missing(meth, *args)
7
+ if self.instance && self.instance.respond_to?(meth)
8
+ self.instance.__send__(meth, *args)
9
+ else
10
+ super
11
+ end
12
+ end
13
+
14
+ def respond_to?(meth)
15
+ super(meth) || self.instance.respond_to?(meth)
16
+ end
17
+ end
18
+
19
+ def self.included(base)
20
+ base.__send__(:include, ::Singleton) unless base.ancestors.include?(::Singleton)
21
+ base.__send__(:extend, ClassMethods)
22
+ end
23
+ end
24
+ end