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