dalli 2.7.2 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of dalli might be problematic. Click here for more details.

data/lib/dalli/client.rb CHANGED
@@ -1,20 +1,24 @@
1
- require 'digest/md5'
2
- require 'set'
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/md5"
4
+ require "set"
3
5
 
4
6
  # encoding: ascii
5
7
  module Dalli
6
8
  class Client
7
-
8
9
  ##
9
10
  # Dalli::Client is the main class which developers will use to interact with
10
11
  # the memcached server. Usage:
11
12
  #
12
- # Dalli::Client.new(['localhost:11211:10', 'cache-2.example.com:11211:5', '192.168.0.1:22122:5'],
13
+ # Dalli::Client.new(['localhost:11211:10', 'cache-2.example.com:11211:5', '192.168.0.1:22122:5', '/var/run/memcached/socket'],
13
14
  # :threadsafe => true, :failover => true, :expires_in => 300)
14
15
  #
15
16
  # servers is an Array of "host:port:weight" where weight allows you to distribute cache unevenly.
16
17
  # Both weight and port are optional. If you pass in nil, Dalli will use the <tt>MEMCACHE_SERVERS</tt>
17
- # environment variable or default to 'localhost:11211' if it is not present.
18
+ # environment variable or default to 'localhost:11211' if it is not present. Dalli also supports
19
+ # the ability to connect to Memcached on localhost through a UNIX socket. To use this functionality,
20
+ # use a full pathname (beginning with a slash character '/') in place of the "host:port" pair in
21
+ # the server configuration.
18
22
  #
19
23
  # Options:
20
24
  # - :namespace - prepend each key with this value to provide simple namespacing.
@@ -24,9 +28,13 @@ module Dalli
24
28
  # - :compress - defaults to false, if true Dalli will compress values larger than 1024 bytes before sending them to memcached.
25
29
  # - :serializer - defaults to Marshal
26
30
  # - :compressor - defaults to zlib
31
+ # - :cache_nils - defaults to false, if true Dalli will not treat cached nil values as 'not found' for #fetch operations.
32
+ # - :digest_class - defaults to Digest::MD5, allows you to pass in an object that responds to the hexdigest method, useful for injecting a FIPS compliant hash object.
33
+ # - :protocol_implementation - defaults to Dalli::Protocol::Binary which uses the binary protocol. Allows you to pass an alternative implementation using another protocol.
27
34
  #
28
- def initialize(servers=nil, options={})
29
- @servers = normalize_servers(servers || ENV["MEMCACHE_SERVERS"] || '127.0.0.1:11211')
35
+ def initialize(servers = nil, options = {})
36
+ validate_servers_arg(servers)
37
+ @servers = normalize_servers(servers || ENV["MEMCACHE_SERVERS"] || "127.0.0.1:11211")
30
38
  @options = normalize_options(options)
31
39
  @ring = nil
32
40
  end
@@ -49,8 +57,9 @@ module Dalli
49
57
 
50
58
  ##
51
59
  # Get the value associated with the key.
52
- def get(key, options=nil)
53
- perform(:get, key)
60
+ # If a value is not found, then +nil+ is returned.
61
+ def get(key, options = nil)
62
+ perform(:get, key, options)
54
63
  end
55
64
 
56
65
  ##
@@ -58,21 +67,38 @@ module Dalli
58
67
  # If a block is given, yields key/value pairs one at a time.
59
68
  # Otherwise returns a hash of { 'key' => 'value', 'key2' => 'value1' }
60
69
  def get_multi(*keys)
70
+ keys.flatten!
71
+ keys.compact!
72
+
73
+ return {} if keys.empty?
61
74
  if block_given?
62
- get_multi_yielder(keys) {|k, data| yield k, data.first}
75
+ get_multi_yielder(keys) { |k, data| yield k, data.first }
63
76
  else
64
- Hash.new.tap do |hash|
65
- get_multi_yielder(keys) {|k, data| hash[k] = data.first}
77
+ {}.tap do |hash|
78
+ get_multi_yielder(keys) { |k, data| hash[k] = data.first }
66
79
  end
67
80
  end
68
81
  end
69
82
 
70
- def fetch(key, ttl=nil, options=nil)
71
- ttl ||= @options[:expires_in].to_i
83
+ CACHE_NILS = {cache_nils: true}.freeze
84
+
85
+ # Fetch the value associated with the key.
86
+ # If a value is found, then it is returned.
87
+ #
88
+ # If a value is not found and no block is given, then nil is returned.
89
+ #
90
+ # If a value is not found (or if the found value is nil and :cache_nils is false)
91
+ # and a block is given, the block will be invoked and its return value
92
+ # written to the cache and returned.
93
+ def fetch(key, ttl = nil, options = nil)
94
+ options = options.nil? ? CACHE_NILS : options.merge(CACHE_NILS) if @options[:cache_nils]
72
95
  val = get(key, options)
73
- if val.nil? && block_given?
96
+ not_found = @options[:cache_nils] ?
97
+ val == Dalli::Protocol::NOT_FOUND :
98
+ val.nil?
99
+ if not_found && block_given?
74
100
  val = yield
75
- add(key, val, ttl, options)
101
+ add(key, val, ttl_or_default(ttl), options)
76
102
  end
77
103
  val
78
104
  end
@@ -88,35 +114,37 @@ module Dalli
88
114
  # - nil if the key did not exist.
89
115
  # - false if the value was changed by someone else.
90
116
  # - true if the value was successfully updated.
91
- def cas(key, ttl=nil, options=nil, &block)
92
- ttl ||= @options[:expires_in].to_i
93
- (value, cas) = perform(:cas, key)
94
- value = (!value || value == 'Not found') ? nil : value
95
- if value
96
- newvalue = block.call(value)
97
- perform(:set, key, newvalue, ttl, cas, options)
98
- end
117
+ def cas(key, ttl = nil, options = nil, &block)
118
+ cas_core(key, false, ttl, options, &block)
99
119
  end
100
120
 
101
- def set(key, value, ttl=nil, options=nil)
102
- ttl ||= @options[:expires_in].to_i
103
- perform(:set, key, value, ttl, 0, options)
121
+ ##
122
+ # like #cas, but will yield to the block whether or not the value
123
+ # already exists.
124
+ #
125
+ # Returns:
126
+ # - false if the value was changed by someone else.
127
+ # - true if the value was successfully updated.
128
+ def cas!(key, ttl = nil, options = nil, &block)
129
+ cas_core(key, true, ttl, options, &block)
130
+ end
131
+
132
+ def set(key, value, ttl = nil, options = nil)
133
+ perform(:set, key, value, ttl_or_default(ttl), 0, options)
104
134
  end
105
135
 
106
136
  ##
107
137
  # Conditionally add a key/value pair, if the key does not already exist
108
138
  # on the server. Returns truthy if the operation succeeded.
109
- def add(key, value, ttl=nil, options=nil)
110
- ttl ||= @options[:expires_in].to_i
111
- perform(:add, key, value, ttl, options)
139
+ def add(key, value, ttl = nil, options = nil)
140
+ perform(:add, key, value, ttl_or_default(ttl), options)
112
141
  end
113
142
 
114
143
  ##
115
144
  # Conditionally add a key/value pair, only if the key already exists
116
145
  # on the server. Returns truthy if the operation succeeded.
117
- def replace(key, value, ttl=nil, options=nil)
118
- ttl ||= @options[:expires_in].to_i
119
- perform(:replace, key, value, ttl, 0, options)
146
+ def replace(key, value, ttl = nil, options = nil)
147
+ perform(:replace, key, value, ttl_or_default(ttl), 0, options)
120
148
  end
121
149
 
122
150
  def delete(key)
@@ -137,7 +165,7 @@ module Dalli
137
165
  perform(:prepend, key, value.to_s)
138
166
  end
139
167
 
140
- def flush(delay=0)
168
+ def flush(delay = 0)
141
169
  time = -delay
142
170
  ring.servers.map { |s| s.request(:flush, time += delay) }
143
171
  end
@@ -155,10 +183,9 @@ module Dalli
155
183
  # Note that the ttl will only apply if the counter does not already
156
184
  # exist. To increase an existing counter and update its TTL, use
157
185
  # #cas.
158
- def incr(key, amt=1, ttl=nil, default=nil)
186
+ def incr(key, amt = 1, ttl = nil, default = nil)
159
187
  raise ArgumentError, "Positive values only: #{amt}" if amt < 0
160
- ttl ||= @options[:expires_in].to_i
161
- perform(:incr, key, amt.to_i, ttl, default)
188
+ perform(:incr, key, amt.to_i, ttl_or_default(ttl), default)
162
189
  end
163
190
 
164
191
  ##
@@ -175,31 +202,37 @@ module Dalli
175
202
  # Note that the ttl will only apply if the counter does not already
176
203
  # exist. To decrease an existing counter and update its TTL, use
177
204
  # #cas.
178
- def decr(key, amt=1, ttl=nil, default=nil)
205
+ def decr(key, amt = 1, ttl = nil, default = nil)
179
206
  raise ArgumentError, "Positive values only: #{amt}" if amt < 0
180
- ttl ||= @options[:expires_in].to_i
181
- perform(:decr, key, amt.to_i, ttl, default)
207
+ perform(:decr, key, amt.to_i, ttl_or_default(ttl), default)
182
208
  end
183
209
 
184
210
  ##
185
211
  # Touch updates expiration time for a given key.
186
212
  #
187
213
  # Returns true if key exists, otherwise nil.
188
- def touch(key, ttl=nil)
189
- ttl ||= @options[:expires_in].to_i
190
- resp = perform(:touch, key, ttl)
214
+ def touch(key, ttl = nil)
215
+ resp = perform(:touch, key, ttl_or_default(ttl))
191
216
  resp.nil? ? nil : true
192
217
  end
193
218
 
219
+ ##
220
+ # Gat (get and touch) fetch an item and simultaneously update its expiration time.
221
+ #
222
+ # If a value is not found, then +nil+ is returned.
223
+ def gat(key, ttl = nil)
224
+ perform(:gat, key, ttl_or_default(ttl))
225
+ end
226
+
194
227
  ##
195
228
  # Collect the stats for each server.
196
- # You can optionally pass a type including :items or :slabs to get specific stats
229
+ # You can optionally pass a type including :items, :slabs or :settings to get specific stats
197
230
  # Returns a hash like { 'hostname:port' => { 'stat1' => 'value1', ... }, 'hostname2:port' => { ... } }
198
- def stats(type=nil)
199
- type = nil if ![nil, :items,:slabs].include? type
231
+ def stats(type = nil)
232
+ type = nil unless [nil, :items, :slabs, :settings].include? type
200
233
  values = {}
201
234
  ring.servers.each do |server|
202
- values["#{server.hostname}:#{server.port}"] = server.alive? ? server.request(:stats,type.to_s) : nil
235
+ values[server.name.to_s] = server.alive? ? server.request(:stats, type.to_s) : nil
203
236
  end
204
237
  values
205
238
  end
@@ -223,11 +256,63 @@ module Dalli
223
256
  def version
224
257
  values = {}
225
258
  ring.servers.each do |server|
226
- values["#{server.hostname}:#{server.port}"] = server.alive? ? server.request(:version) : nil
259
+ values[server.name.to_s] = server.alive? ? server.request(:version) : nil
227
260
  end
228
261
  values
229
262
  end
230
263
 
264
+ ##
265
+ # Get the value and CAS ID associated with the key. If a block is provided,
266
+ # value and CAS will be passed to the block.
267
+ def get_cas(key)
268
+ (value, cas) = perform(:cas, key)
269
+ value = !value || value == "Not found" ? nil : value
270
+ if block_given?
271
+ yield value, cas
272
+ else
273
+ [value, cas]
274
+ end
275
+ end
276
+
277
+ ##
278
+ # Fetch multiple keys efficiently, including available metadata such as CAS.
279
+ # If a block is given, yields key/data pairs one a time. Data is an array:
280
+ # [value, cas_id]
281
+ # If no block is given, returns a hash of
282
+ # { 'key' => [value, cas_id] }
283
+ def get_multi_cas(*keys)
284
+ if block_given?
285
+ get_multi_yielder(keys) { |*args| yield(*args) }
286
+ else
287
+ {}.tap do |hash|
288
+ get_multi_yielder(keys) { |k, data| hash[k] = data }
289
+ end
290
+ end
291
+ end
292
+
293
+ ##
294
+ # Set the key-value pair, verifying existing CAS.
295
+ # Returns the resulting CAS value if succeeded, and falsy otherwise.
296
+ def set_cas(key, value, cas, ttl = nil, options = nil)
297
+ ttl ||= @options[:expires_in].to_i
298
+ perform(:set, key, value, ttl, cas, options)
299
+ end
300
+
301
+ ##
302
+ # Conditionally add a key/value pair, verifying existing CAS, only if the
303
+ # key already exists on the server. Returns the new CAS value if the
304
+ # operation succeeded, or falsy otherwise.
305
+ def replace_cas(key, value, cas, ttl = nil, options = nil)
306
+ ttl ||= @options[:expires_in].to_i
307
+ perform(:replace, key, value, ttl, cas, options)
308
+ end
309
+
310
+ # Delete a key/value pair, verifying existing CAS.
311
+ # Returns true if succeeded, and falsy otherwise.
312
+ def delete_cas(key, cas = 0)
313
+ perform(:delete, key, cas)
314
+ end
315
+
231
316
  ##
232
317
  # Close our connection to each server.
233
318
  # If you perform another operation after this, the connections will be re-established.
@@ -239,41 +324,46 @@ module Dalli
239
324
  end
240
325
  alias_method :reset, :close
241
326
 
242
- # Stub method so a bare Dalli client can pretend to be a connection pool.
243
- def with
244
- yield self
327
+ private
328
+
329
+ def cas_core(key, always_set, ttl = nil, options = nil)
330
+ (value, cas) = perform(:cas, key)
331
+ value = !value || value == "Not found" ? nil : value
332
+ return if value.nil? && !always_set
333
+ newvalue = yield(value)
334
+ perform(:set, key, newvalue, ttl_or_default(ttl), cas, options)
245
335
  end
246
336
 
247
- private
337
+ def ttl_or_default(ttl)
338
+ (ttl || @options[:expires_in]).to_i
339
+ rescue NoMethodError
340
+ raise ArgumentError, "Cannot convert ttl (#{ttl}) to an integer"
341
+ end
248
342
 
249
343
  def groups_for_keys(*keys)
250
- groups = mapped_keys(keys).flatten.group_by do |key|
344
+ keys.flatten!
345
+ keys.map! { |a| validate_key(a.to_s) }
346
+
347
+ keys.group_by { |key|
251
348
  begin
252
349
  ring.server_for_key(key)
253
350
  rescue Dalli::RingError
254
351
  Dalli.logger.debug { "unable to get key #{key}" }
255
352
  nil
256
353
  end
257
- end
258
- return groups
259
- end
260
-
261
- def mapped_keys(keys)
262
- keys.flatten.map {|a| validate_key(a.to_s)}
354
+ }
263
355
  end
264
356
 
265
357
  def make_multi_get_requests(groups)
266
358
  groups.each do |server, keys_for_server|
267
- begin
268
- # TODO: do this with the perform chokepoint?
269
- # But given the fact that fetching the response doesn't take place
270
- # in that slot it's misleading anyway. Need to move all of this method
271
- # into perform to be meaningful
272
- server.request(:send_multiget, keys_for_server)
273
- rescue DalliError, NetworkError => e
274
- Dalli.logger.debug { e.inspect }
275
- Dalli.logger.debug { "unable to get keys for server #{server.hostname}:#{server.port}" }
276
- end
359
+ # TODO: do this with the perform chokepoint?
360
+ # But given the fact that fetching the response doesn't take place
361
+ # in that slot it's misleading anyway. Need to move all of this method
362
+ # into perform to be meaningful
363
+ server.request(:send_multiget, keys_for_server)
364
+ rescue DalliError, NetworkError => e
365
+ Dalli.logger.debug { e.inspect }
366
+ Dalli.logger.debug { "unable to get keys for server #{server.name}" }
277
367
  end
278
368
  end
279
369
 
@@ -292,42 +382,54 @@ module Dalli
292
382
  end
293
383
 
294
384
  ##
295
- # Normalizes the argument into an array of servers. If the argument is a string, it's expected to be of
296
- # the format "memcache1.example.com:11211[,memcache2.example.com:11211[,memcache3.example.com:11211[...]]]
385
+ # Ensures that the servers arg is either an array or a string.
386
+ def validate_servers_arg(servers)
387
+ return if servers.nil?
388
+ return if servers.is_a?(Array)
389
+ return if servers.is_a?(String)
390
+
391
+ raise ArgumentError, "An explicit servers argument must be a comma separated string or an array containing strings."
392
+ end
393
+
394
+ ##
395
+ # Normalizes the argument into an array of servers.
396
+ # If the argument is a string, or an array containing strings, it's expected that the URIs are comma separated e.g.
397
+ # "memcache1.example.com:11211,memcache2.example.com:11211,memcache3.example.com:11211"
297
398
  def normalize_servers(servers)
298
- if servers.is_a? String
299
- return servers.split(",")
300
- else
301
- return servers
399
+ Array(servers).flat_map do |server|
400
+ if server.is_a? String
401
+ server.split(",")
402
+ else
403
+ server
404
+ end
302
405
  end
303
406
  end
304
407
 
305
408
  def ring
306
409
  @ring ||= Dalli::Ring.new(
307
- @servers.map do |s|
308
- server_options = {}
309
- if s =~ %r{\Amemcached://}
410
+ @servers.map { |s|
411
+ server_options = {}
412
+ if s.start_with?("memcached://")
310
413
  uri = URI.parse(s)
311
414
  server_options[:username] = uri.user
312
415
  server_options[:password] = uri.password
313
416
  s = "#{uri.host}:#{uri.port}"
314
417
  end
315
- Dalli::Server.new(s, @options.merge(server_options))
316
- end, @options
418
+ @options.fetch(:protocol_implementation, Dalli::Protocol::Binary).new(s, @options.merge(server_options))
419
+ }, @options
317
420
  )
318
421
  end
319
422
 
320
423
  # Chokepoint method for instrumentation
321
- def perform(*all_args, &blk)
322
- return blk.call if blk
323
- op, key, *args = *all_args
424
+ def perform(*all_args)
425
+ return yield if block_given?
426
+ op, key, *args = all_args
324
427
 
325
428
  key = key.to_s
326
429
  key = validate_key(key)
327
430
  begin
328
431
  server = ring.server_for_key(key)
329
- ret = server.request(op, key, *args)
330
- ret
432
+ server.request(op, key, *args)
331
433
  rescue NetworkError => e
332
434
  Dalli.logger.debug { e.inspect }
333
435
  Dalli.logger.debug { "retrying request with new server" }
@@ -339,10 +441,11 @@ module Dalli
339
441
  raise ArgumentError, "key cannot be blank" if !key || key.length == 0
340
442
  key = key_with_namespace(key)
341
443
  if key.length > 250
342
- max_length_before_namespace = 212 - (namespace || '').size
343
- key = "#{key[0, max_length_before_namespace]}:md5:#{Digest::MD5.hexdigest(key)}"
444
+ digest_class = @options[:digest_class] || ::Digest::MD5
445
+ max_length_before_namespace = 212 - (namespace || "").size
446
+ key = "#{key[0, max_length_before_namespace]}:md5:#{digest_class.hexdigest(key)}"
344
447
  end
345
- return key
448
+ key
346
449
  end
347
450
 
348
451
  def key_with_namespace(key)
@@ -350,7 +453,7 @@ module Dalli
350
453
  end
351
454
 
352
455
  def key_without_namespace(key)
353
- (ns = namespace) ? key.sub(%r(\A#{ns}:), '') : key
456
+ (ns = namespace) ? key.sub(%r{\A#{Regexp.escape ns}:}, "") : key
354
457
  end
355
458
 
356
459
  def namespace
@@ -368,6 +471,9 @@ module Dalli
368
471
  rescue NoMethodError
369
472
  raise ArgumentError, "cannot convert :expires_in => #{opts[:expires_in].inspect} to an integer"
370
473
  end
474
+ if opts[:digest_class] && !opts[:digest_class].respond_to?(:hexdigest)
475
+ raise ArgumentError, "The digest_class object must respond to the hexdigest method"
476
+ end
371
477
  opts
372
478
  end
373
479
 
@@ -377,56 +483,52 @@ module Dalli
377
483
  perform do
378
484
  return {} if keys.empty?
379
485
  ring.lock do
380
- begin
381
- groups = groups_for_keys(keys)
382
- if unfound_keys = groups.delete(nil)
383
- Dalli.logger.debug { "unable to get keys for #{unfound_keys.length} keys because no matching server was found" }
384
- end
385
- make_multi_get_requests(groups)
386
-
387
- servers = groups.keys
388
- return if servers.empty?
389
- servers = perform_multi_response_start(servers)
390
-
391
- start = Time.now
392
- loop do
393
- # remove any dead servers
394
- servers.delete_if { |s| s.sock.nil? }
395
- break if servers.empty?
396
-
397
- # calculate remaining timeout
398
- elapsed = Time.now - start
399
- timeout = servers.first.options[:socket_timeout]
400
- if elapsed > timeout
401
- readable = nil
402
- else
403
- sockets = servers.map(&:sock)
404
- readable, _ = IO.select(sockets, nil, nil, timeout - elapsed)
486
+ groups = groups_for_keys(keys)
487
+ if (unfound_keys = groups.delete(nil))
488
+ Dalli.logger.debug { "unable to get keys for #{unfound_keys.length} keys because no matching server was found" }
489
+ end
490
+ make_multi_get_requests(groups)
491
+
492
+ servers = groups.keys
493
+ return if servers.empty?
494
+ servers = perform_multi_response_start(servers)
495
+
496
+ start = Time.now
497
+ loop do
498
+ # remove any dead servers
499
+ servers.delete_if { |s| s.sock.nil? }
500
+ break if servers.empty?
501
+
502
+ # calculate remaining timeout
503
+ elapsed = Time.now - start
504
+ timeout = servers.first.options[:socket_timeout]
505
+ time_left = elapsed > timeout ? 0 : timeout - elapsed
506
+
507
+ sockets = servers.map(&:sock)
508
+ readable, _ = IO.select(sockets, nil, nil, time_left)
509
+
510
+ if readable.nil?
511
+ # no response within timeout; abort pending connections
512
+ servers.each do |server|
513
+ Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" }
514
+ server.multi_response_abort
405
515
  end
516
+ break
406
517
 
407
- if readable.nil?
408
- # no response within timeout; abort pending connections
409
- servers.each do |server|
410
- Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" }
411
- server.multi_response_abort
412
- end
413
- break
414
-
415
- else
416
- readable.each do |sock|
417
- server = sock.server
518
+ else
519
+ readable.each do |sock|
520
+ server = sock.server
418
521
 
419
- begin
420
- server.multi_response_nonblock.each_pair do |key, value_list|
421
- yield key_without_namespace(key), value_list
422
- end
522
+ begin
523
+ server.multi_response_nonblock.each_pair do |key, value_list|
524
+ yield key_without_namespace(key), value_list
525
+ end
423
526
 
424
- if server.multi_response_completed?
425
- servers.delete(server)
426
- end
427
- rescue NetworkError
527
+ if server.multi_response_completed?
428
528
  servers.delete(server)
429
529
  end
530
+ rescue NetworkError
531
+ servers.delete(server)
430
532
  end
431
533
  end
432
534
  end
@@ -1,5 +1,7 @@
1
- require 'zlib'
2
- require 'stringio'
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+ require "stringio"
3
5
 
4
6
  module Dalli
5
7
  class Compressor
@@ -14,7 +16,7 @@ module Dalli
14
16
 
15
17
  class GzipCompressor
16
18
  def self.compress(data)
17
- io = StringIO.new("w")
19
+ io = StringIO.new(+"", "w")
18
20
  gz = Zlib::GzipWriter.new(io)
19
21
  gz.write(data)
20
22
  gz.close
data/lib/dalli/options.rb CHANGED
@@ -1,12 +1,12 @@
1
- require 'thread'
2
- require 'monitor'
1
+ # frozen_string_literal: true
3
2
 
4
- module Dalli
3
+ require "monitor"
5
4
 
5
+ module Dalli
6
6
  # Make Dalli threadsafe by using a lock around all
7
7
  # public server methods.
8
8
  #
9
- # Dalli::Server.extend(Dalli::Threadsafe)
9
+ # Dalli::Protocol::Binary.extend(Dalli::Threadsafe)
10
10
  #
11
11
  module Threadsafe
12
12
  def self.extended(obj)