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.
- data/README.rdoc +38 -14
- data/lib/right_support.rb +1 -1
- data/lib/right_support/crypto/signed_hash.rb +11 -2
- data/lib/right_support/db.rb +1 -3
- data/lib/right_support/db/cassandra_model.rb +299 -18
- data/lib/right_support/log.rb +4 -0
- data/lib/right_support/log/exception_logger.rb +86 -0
- data/lib/right_support/log/filter_logger.rb +75 -0
- data/lib/right_support/log/mixin.rb +104 -0
- data/lib/right_support/log/multiplexer.rb +93 -0
- data/lib/right_support/log/null_logger.rb +76 -0
- data/lib/right_support/net.rb +5 -3
- data/lib/right_support/net/balancing/health_check.rb +40 -23
- data/lib/right_support/net/request_balancer.rb +19 -32
- data/lib/right_support/rack.rb +2 -3
- data/lib/right_support/rack/custom_logger.rb +29 -8
- data/lib/right_support/rack/request_logger.rb +113 -0
- data/lib/right_support/ruby.rb +4 -3
- data/lib/right_support/ruby/easy_singleton.rb +24 -0
- data/lib/right_support/ruby/object_extensions.rb +18 -2
- data/lib/right_support/ruby/string_extensions.rb +120 -0
- data/lib/right_support/stats.rb +34 -0
- data/lib/right_support/stats/activity.rb +206 -0
- data/lib/right_support/stats/exceptions.rb +96 -0
- data/lib/right_support/stats/helpers.rb +438 -0
- data/lib/right_support/validation.rb +2 -4
- data/right_support.gemspec +3 -5
- metadata +18 -20
data/lib/right_support/net.rb
CHANGED
@@ -29,8 +29,10 @@ module RightSupport
|
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
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
|
-
|
60
|
-
|
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
|
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,
|
96
|
-
#
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/right_support/rack.rb
CHANGED
@@ -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
|
-
#
|
36
|
-
#
|
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,
|
39
|
-
|
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
|
-
|
42
|
-
|
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
|
-
|
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
|
data/lib/right_support/ruby.rb
CHANGED
@@ -29,6 +29,7 @@ module RightSupport
|
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|