right_support 1.1.2 → 1.2.0

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