torquebox-cache 2.0.0.beta1-java
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/lib/active_support/cache/torque_box_store.rb +124 -0
- data/lib/cache.rb +420 -0
- data/lib/cache_listener.rb +46 -0
- data/lib/datamapper/dm-infinispan-adapter.rb +128 -0
- data/lib/datamapper/model.rb +185 -0
- data/lib/datamapper/search.rb +145 -0
- data/lib/dm-infinispan-adapter.rb +1 -0
- data/lib/gem_hook.rb +22 -0
- data/lib/torquebox-cache.jar +0 -0
- data/lib/torquebox-cache.rb +13 -0
- data/licenses/lgpl-2.1.txt +502 -0
- data/spec/cache_listener_spec.rb +93 -0
- data/spec/cache_spec.rb +361 -0
- data/spec/dm-infinispan-adapter_spec.rb +375 -0
- data/spec/spec.opts +5 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/torque_box_store_spec.rb +220 -0
- metadata +161 -0
@@ -0,0 +1,124 @@
|
|
1
|
+
# Copyright 2008-2011 Red Hat, Inc, and individual contributors.
|
2
|
+
#
|
3
|
+
# This is free software; you can redistribute it and/or modify it
|
4
|
+
# under the terms of the GNU Lesser General Public License as
|
5
|
+
# published by the Free Software Foundation; either version 2.1 of
|
6
|
+
# the License, or (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This software is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
11
|
+
# Lesser General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU Lesser General Public
|
14
|
+
# License along with this software; if not, write to the Free
|
15
|
+
# Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
|
16
|
+
# 02110-1301 USA, or see the FSF site: http://www.fsf.org.
|
17
|
+
|
18
|
+
require 'active_support/cache'
|
19
|
+
require 'torquebox/kernel'
|
20
|
+
require 'cache'
|
21
|
+
|
22
|
+
module ActiveSupport
|
23
|
+
module Cache
|
24
|
+
class TorqueBoxStore < Store
|
25
|
+
|
26
|
+
SECONDS = java.util.concurrent.TimeUnit::SECONDS
|
27
|
+
|
28
|
+
def initialize(options = {})
|
29
|
+
super(options)
|
30
|
+
cache
|
31
|
+
end
|
32
|
+
|
33
|
+
def name
|
34
|
+
options[:name] || TORQUEBOX_APP_NAME
|
35
|
+
end
|
36
|
+
|
37
|
+
def clustering_mode
|
38
|
+
cache.clustering_mode
|
39
|
+
end
|
40
|
+
|
41
|
+
# Clear the entire cache. Be careful with this method since it could
|
42
|
+
# affect other processes if shared cache is being used.
|
43
|
+
def clear(options = nil)
|
44
|
+
cache.clear
|
45
|
+
end
|
46
|
+
|
47
|
+
# Delete all entries with keys matching the pattern.
|
48
|
+
def delete_matched( matcher, options = nil )
|
49
|
+
options = merged_options(options)
|
50
|
+
pattern = key_matcher( matcher, options )
|
51
|
+
keys.each { |key| delete( key, options ) if key =~ pattern }
|
52
|
+
end
|
53
|
+
|
54
|
+
# Increment an integer value in the cache; return new value
|
55
|
+
def increment(name, amount = 1, options = nil)
|
56
|
+
options = merged_options( options )
|
57
|
+
|
58
|
+
# Get the current entry
|
59
|
+
key = namespaced_key( name, options )
|
60
|
+
current = cache.get(key)
|
61
|
+
value = decode(current).value.to_i
|
62
|
+
|
63
|
+
new_entry = Entry.new( value+amount, options )
|
64
|
+
if cache.replace( key, current, encode(new_entry) )
|
65
|
+
return new_entry.value
|
66
|
+
else
|
67
|
+
raise "Concurrent modification, old value was #{value}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Decrement an integer value in the cache; return new value
|
72
|
+
def decrement(name, amount = 1, options = nil)
|
73
|
+
increment( name, -amount, options )
|
74
|
+
end
|
75
|
+
|
76
|
+
# Cleanup the cache by removing expired entries.
|
77
|
+
def cleanup(options = nil)
|
78
|
+
options = merged_options(options)
|
79
|
+
keys.each do |key|
|
80
|
+
entry = read_entry(key, options)
|
81
|
+
delete_entry(key, options) if entry && entry.expired?
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
protected
|
86
|
+
|
87
|
+
def encode value
|
88
|
+
Marshal.dump(value).to_java_bytes
|
89
|
+
end
|
90
|
+
|
91
|
+
def decode value
|
92
|
+
value && Marshal.load(String.from_java_bytes(value))
|
93
|
+
end
|
94
|
+
|
95
|
+
# Return the keys in the cache; potentially very expensive depending on configuration
|
96
|
+
def keys
|
97
|
+
cache.keys
|
98
|
+
end
|
99
|
+
|
100
|
+
# Read an entry from the cache implementation. Subclasses must implement this method.
|
101
|
+
def read_entry(key, options)
|
102
|
+
decode( cache.get( key ) )
|
103
|
+
end
|
104
|
+
|
105
|
+
# Write an entry to the cache implementation. Subclasses must implement this method.
|
106
|
+
def write_entry(key, entry, options = {})
|
107
|
+
options[:unless_exist] ? cache.put_if_absent( key, encode(entry) ) : cache.put( key, encode(entry) )
|
108
|
+
end
|
109
|
+
|
110
|
+
# Delete an entry from the cache implementation. Subclasses must implement this method.
|
111
|
+
def delete_entry(key, options) # :nodoc:
|
112
|
+
cache.remove( key ) && true
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def cache
|
119
|
+
@cache ||= TorqueBox::Infinispan::Cache.new(options)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
data/lib/cache.rb
ADDED
@@ -0,0 +1,420 @@
|
|
1
|
+
# Copyright 2008-2011 Red Hat, Inc, and individual contributors.
|
2
|
+
#
|
3
|
+
# This is free software; you can redistribute it and/or modify it
|
4
|
+
# under the terms of the GNU Lesser General Public License as
|
5
|
+
# published by the Free Software Foundation; either version 2.1 of
|
6
|
+
# the License, or (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This software is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
11
|
+
# Lesser General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU Lesser General Public
|
14
|
+
# License along with this software; if not, write to the Free
|
15
|
+
# Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
|
16
|
+
# 02110-1301 USA, or see the FSF site: http://www.fsf.org.
|
17
|
+
|
18
|
+
require 'torquebox/kernel'
|
19
|
+
require 'torquebox/injectors'
|
20
|
+
require 'torquebox/transactions'
|
21
|
+
|
22
|
+
module TorqueBox
|
23
|
+
module Infinispan
|
24
|
+
|
25
|
+
class ContainerTransactionManagerLookup
|
26
|
+
include TorqueBox::Injectors
|
27
|
+
begin
|
28
|
+
include org.infinispan.transaction.lookup.TransactionManagerLookup
|
29
|
+
rescue NameError
|
30
|
+
# Not running inside TorqueBox
|
31
|
+
end
|
32
|
+
|
33
|
+
def getTransactionManager
|
34
|
+
inject('transaction-manager')
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class NoOpCodec
|
39
|
+
def self.encode(object)
|
40
|
+
object
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.decode(object)
|
44
|
+
object
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class Sequence
|
49
|
+
include java.io.Serializable
|
50
|
+
|
51
|
+
class Codec
|
52
|
+
def self.encode(sequence)
|
53
|
+
sequence.value.to_s
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.decode(sequence_bytes)
|
57
|
+
sequence_bytes && Sequence.new( sequence_bytes.to_s.to_i )
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def initialize(amount = 1)
|
62
|
+
@data = amount
|
63
|
+
end
|
64
|
+
|
65
|
+
def value
|
66
|
+
@data ? @data.to_i : @data
|
67
|
+
end
|
68
|
+
|
69
|
+
def next(amount = 1)
|
70
|
+
Sequence.new( @data.to_i + amount )
|
71
|
+
end
|
72
|
+
|
73
|
+
def ==(other)
|
74
|
+
self.value == other.value
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_s
|
78
|
+
"Sequence: #{self.value}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
class Cache
|
83
|
+
include TorqueBox::Injectors
|
84
|
+
|
85
|
+
SECONDS = java.util.concurrent.TimeUnit::SECONDS
|
86
|
+
begin
|
87
|
+
java_import org.infinispan.config.Configuration::CacheMode
|
88
|
+
java_import org.infinispan.transaction::TransactionMode
|
89
|
+
java_import org.infinispan.transaction::LockingMode
|
90
|
+
INFINISPAN_AVAILABLE = true
|
91
|
+
rescue NameError
|
92
|
+
INFINISPAN_AVAILABLE = false
|
93
|
+
# Not running inside TorqueBox
|
94
|
+
end
|
95
|
+
|
96
|
+
def initialize(opts = {})
|
97
|
+
@options = opts
|
98
|
+
options[:transaction_mode] = :transactional unless options.has_key?( :transaction_mode )
|
99
|
+
options[:locking_mode] ||= :optimistic if (transactional? && !options.has_key?( :locking_mode ))
|
100
|
+
cache
|
101
|
+
end
|
102
|
+
|
103
|
+
def name
|
104
|
+
options[:name] || TORQUEBOX_APP_NAME
|
105
|
+
end
|
106
|
+
|
107
|
+
def search_manager
|
108
|
+
@search_manager ||= org.infinispan.query.Search.getSearchManager(@cache)
|
109
|
+
end
|
110
|
+
|
111
|
+
def clustering_mode
|
112
|
+
replicated = [:r, :repl, :replicated, :replication].include? options[:mode]
|
113
|
+
distributed = [:d, :dist, :distributed, :distribution].include? options[:mode]
|
114
|
+
sync = !!options[:sync]
|
115
|
+
case
|
116
|
+
when replicated
|
117
|
+
sync ? CacheMode::REPL_SYNC : CacheMode::REPL_ASYNC
|
118
|
+
when distributed
|
119
|
+
sync ? CacheMode::DIST_SYNC : CacheMode::DIST_ASYNC
|
120
|
+
else
|
121
|
+
sync ? CacheMode::INVALIDATION_SYNC : CacheMode::INVALIDATION_ASYNC
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def locking_mode
|
126
|
+
case options[:locking_mode]
|
127
|
+
when :optimistic then LockingMode::OPTIMISTIC
|
128
|
+
when :pessimistic then LockingMode::PESSIMISTIC
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def transaction_mode
|
133
|
+
options[:transaction_mode] == :transactional ? TransactionMode::TRANSACTIONAL : TransactionMode::NON_TRANSACTIONAL
|
134
|
+
end
|
135
|
+
|
136
|
+
def transactional?
|
137
|
+
transaction_mode == TransactionMode::TRANSACTIONAL
|
138
|
+
end
|
139
|
+
|
140
|
+
# Clear the entire cache. Be careful with this method since it could
|
141
|
+
# affect other processes if shared cache is being used.
|
142
|
+
def clear
|
143
|
+
cache.clearAsync
|
144
|
+
end
|
145
|
+
|
146
|
+
# Return the keys in the cache; potentially very expensive depending on configuration
|
147
|
+
def keys
|
148
|
+
cache.key_set
|
149
|
+
end
|
150
|
+
|
151
|
+
def all
|
152
|
+
cache.key_set.collect{|k| get(k)}
|
153
|
+
end
|
154
|
+
|
155
|
+
def contains_key?( key )
|
156
|
+
cache.contains_key( key )
|
157
|
+
end
|
158
|
+
|
159
|
+
# Get an entry from the cache
|
160
|
+
def get(key)
|
161
|
+
cache.get(key)
|
162
|
+
end
|
163
|
+
|
164
|
+
# Write an entry to the cache
|
165
|
+
def put(key, value, expires = 0)
|
166
|
+
__put(key, value, expires, :put_async)
|
167
|
+
end
|
168
|
+
|
169
|
+
def put_if_absent(key, value, expires = 0)
|
170
|
+
__put(key, value, expires, :put_if_absent_async)
|
171
|
+
end
|
172
|
+
|
173
|
+
def evict( key )
|
174
|
+
cache.evict( key )
|
175
|
+
end
|
176
|
+
|
177
|
+
def replace(key, original_value, new_value, codec=NoOpCodec)
|
178
|
+
# First, grab the raw value from the cache, which is a byte[]
|
179
|
+
|
180
|
+
current = get( key )
|
181
|
+
decoded = codec.decode( current )
|
182
|
+
|
183
|
+
# great! we've got a byte[] now. Let's apply == to it, like Jim says will work always
|
184
|
+
|
185
|
+
if ( decoded == original_value )
|
186
|
+
# how does this work?
|
187
|
+
cache.replace( key, current, codec.encode( new_value ) )
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Delete an entry from the cache
|
192
|
+
def remove(key)
|
193
|
+
cache.removeAsync( key ) && true
|
194
|
+
end
|
195
|
+
|
196
|
+
def increment( sequence_name, amount = 1 )
|
197
|
+
current_entry = Sequence::Codec.decode( get( sequence_name ) )
|
198
|
+
|
199
|
+
# If we can't find the sequence in the cache, create a new one and return
|
200
|
+
put( sequence_name, Sequence::Codec.encode( Sequence.new( amount ) ) ) and return amount if current_entry.nil?
|
201
|
+
|
202
|
+
# Increment the sequence, stash it, and return
|
203
|
+
next_entry = current_entry.next( amount )
|
204
|
+
|
205
|
+
# Since replace() doesn't encode, let's encode everything to a byte[] for it, no?
|
206
|
+
if replace( sequence_name, current_entry, next_entry, Sequence::Codec )
|
207
|
+
return next_entry.value
|
208
|
+
else
|
209
|
+
raise "Concurrent modification, old value was #{current_entry.value} new value #{next_entry.value}"
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# Decrement an integer value in the cache; return new value
|
214
|
+
def decrement(name, amount = 1)
|
215
|
+
increment( name, -amount )
|
216
|
+
end
|
217
|
+
|
218
|
+
def transaction(&block)
|
219
|
+
if !transactional?
|
220
|
+
yield self
|
221
|
+
elsif inject('transaction-manager').nil?
|
222
|
+
tm = cache.getAdvancedCache.getTransactionManager
|
223
|
+
begin
|
224
|
+
tm.begin if tm
|
225
|
+
yield self
|
226
|
+
tm.commit if tm
|
227
|
+
rescue Exception => e
|
228
|
+
if tm.nil?
|
229
|
+
log( "Transaction is nil", "ERROR" )
|
230
|
+
log( e.message, 'ERROR' )
|
231
|
+
log( e.backtrace, 'ERROR' )
|
232
|
+
elsif tm.status == javax.transaction.Status.STATUS_NO_TRANSACTION
|
233
|
+
log( "No transaction was started", "ERROR" )
|
234
|
+
log( e.message, 'ERROR' )
|
235
|
+
log( e.backtrace, 'ERROR' )
|
236
|
+
else
|
237
|
+
tm.rollback
|
238
|
+
log( "Rolling back.", 'ERROR' )
|
239
|
+
log( e.message, 'ERROR' )
|
240
|
+
log( e.backtrace, 'ERROR' )
|
241
|
+
end
|
242
|
+
raise e
|
243
|
+
end
|
244
|
+
else
|
245
|
+
TorqueBox.transaction do
|
246
|
+
yield self
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def add_listener( listener )
|
252
|
+
cache.add_listener( listener )
|
253
|
+
end
|
254
|
+
|
255
|
+
def stop
|
256
|
+
cache.stop
|
257
|
+
end
|
258
|
+
|
259
|
+
def self.shutdown
|
260
|
+
Cache.local_managers.each { |m| m.stop }
|
261
|
+
end
|
262
|
+
|
263
|
+
@@local_managers = []
|
264
|
+
def self.local_managers
|
265
|
+
@@local_managers
|
266
|
+
end
|
267
|
+
|
268
|
+
def self.log( message, status = 'INFO' )
|
269
|
+
$stdout.puts( "#{status}: #{message}" )
|
270
|
+
end
|
271
|
+
|
272
|
+
def log( message, status = 'INFO' )
|
273
|
+
TorqueBox::Infinispan::Cache.log( message, status )
|
274
|
+
end
|
275
|
+
|
276
|
+
private
|
277
|
+
|
278
|
+
def options
|
279
|
+
@options ||= {}
|
280
|
+
end
|
281
|
+
|
282
|
+
def cache
|
283
|
+
if INFINISPAN_AVAILABLE
|
284
|
+
@cache ||= clustered || local || nothing
|
285
|
+
else
|
286
|
+
@cache ||= nothing
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
def manager
|
291
|
+
begin
|
292
|
+
@manager ||= TorqueBox::ServiceRegistry[org.jboss.msc.service.ServiceName::JBOSS.append( "infinispan", "torquebox" )]
|
293
|
+
rescue Exception => e
|
294
|
+
log( "Caught exception while looking up Infinispan service.", 'ERROR' )
|
295
|
+
log( e.message, 'ERROR' )
|
296
|
+
end
|
297
|
+
@manager
|
298
|
+
end
|
299
|
+
|
300
|
+
def reconfigure(mode=clustering_mode)
|
301
|
+
cache = manager.get_cache(name)
|
302
|
+
base_config = cache.configuration
|
303
|
+
unless base_config.cache_mode == mode
|
304
|
+
log( "Reconfiguring Infinispan cache #{name} from #{config.cache_mode} to #{mode}" )
|
305
|
+
cache.stop
|
306
|
+
base_config.cache_mode = mode
|
307
|
+
config = base_config.fluent
|
308
|
+
config.transaction.transactionMode( transaction_mode )
|
309
|
+
config.transaction.recovery.transactionManagerLookup( transaction_manager_lookup )
|
310
|
+
manager.define_configuration(name, config.build)
|
311
|
+
cache.start
|
312
|
+
end
|
313
|
+
return cache
|
314
|
+
end
|
315
|
+
|
316
|
+
def configure(mode=clustering_mode)
|
317
|
+
log( "Configuring Infinispan cache #{name} as #{mode}" )
|
318
|
+
base_config = manager.default_configuration.clone
|
319
|
+
base_config.class_loader = java.lang::Thread.current_thread.context_class_loader
|
320
|
+
base_config.cache_mode = mode
|
321
|
+
|
322
|
+
config = base_config.fluent
|
323
|
+
config.transaction.transactionMode( transaction_mode )
|
324
|
+
config.transaction.recovery.transactionManagerLookup( transaction_manager_lookup )
|
325
|
+
manager.define_configuration(name, config.build )
|
326
|
+
manager.get_cache(name)
|
327
|
+
end
|
328
|
+
|
329
|
+
def transaction_manager_lookup
|
330
|
+
@tm ||= if inject('transaction-manager')
|
331
|
+
ContainerTransactionManagerLookup.new
|
332
|
+
else
|
333
|
+
org.infinispan.transaction.lookup.GenericTransactionManagerLookup.new
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
def clustered
|
338
|
+
(manager.running?(name) ? reconfigure : configure) if manager
|
339
|
+
rescue
|
340
|
+
log( "Clustered: Can't get clustered cache; falling back to local: #{$!}", 'ERROR' )
|
341
|
+
end
|
342
|
+
|
343
|
+
def local
|
344
|
+
log( "Configuring Infinispan local cache #{name}" )
|
345
|
+
bare_config = org.infinispan.config.Configuration.new
|
346
|
+
bare_config.class_loader = java.lang::Thread.current_thread.context_class_loader
|
347
|
+
|
348
|
+
config = bare_config.fluent
|
349
|
+
config.transaction.transactionMode( transaction_mode )
|
350
|
+
|
351
|
+
if transactional?
|
352
|
+
config.transaction.recovery.transactionManagerLookup( transaction_manager_lookup )
|
353
|
+
config.transaction.lockingMode( locking_mode )
|
354
|
+
end
|
355
|
+
|
356
|
+
if options[:persist]
|
357
|
+
log( "Configuring #{name} local cache for file-based persistence" )
|
358
|
+
store = org.infinispan.loaders.file.FileCacheStoreConfig.new
|
359
|
+
store.purge_on_startup( false )
|
360
|
+
store.location(options[:persist]) if File.exist?( options[:persist].to_s )
|
361
|
+
config.loaders.add_cache_loader( store )
|
362
|
+
end
|
363
|
+
|
364
|
+
if options[:index]
|
365
|
+
log( "Configuring #{name} local cache for local-only indexing" )
|
366
|
+
config.indexing.index_local_only(true)
|
367
|
+
end
|
368
|
+
|
369
|
+
if ((local_manager = Cache.find_local_manager(name)) == nil)
|
370
|
+
local_manager = org.infinispan.manager.DefaultCacheManager.new
|
371
|
+
local_manager.define_configuration( name, config.build )
|
372
|
+
Cache.local_managers << local_manager
|
373
|
+
end
|
374
|
+
|
375
|
+
local_manager.get_cache( self.name )
|
376
|
+
rescue Exception => e
|
377
|
+
log( "Unable to obtain local cache: #{$!}", 'ERROR' )
|
378
|
+
log( e.backtrace, 'ERROR' )
|
379
|
+
end
|
380
|
+
|
381
|
+
def nothing
|
382
|
+
result = Object.new
|
383
|
+
def result.method_missing(*args); end
|
384
|
+
log( "Nothing: Can't get or create an Infinispan cache. No caching will occur", 'ERROR' )
|
385
|
+
result
|
386
|
+
end
|
387
|
+
|
388
|
+
def __put(key, value, expires, operation)
|
389
|
+
args = [ operation, key, value ]
|
390
|
+
if expires > 0
|
391
|
+
# Set the Infinispan expire a few minutes into the future to support
|
392
|
+
# :race_condition_ttl on read
|
393
|
+
#expires_in = expires + 300 # 300 seconds == 5 minutes
|
394
|
+
expires_in = expires
|
395
|
+
args << expires_in << SECONDS
|
396
|
+
args << expires << SECONDS
|
397
|
+
end
|
398
|
+
#$stderr.puts "cache=#{cache.inspect}"
|
399
|
+
#$stderr.puts "*args=#{args.inspect}"
|
400
|
+
cache.send( *args ) && true
|
401
|
+
end
|
402
|
+
|
403
|
+
# Finds the CacheManager for a given cache and returns it - or nil if not found
|
404
|
+
def self.find_local_manager( cache_name )
|
405
|
+
local_managers.each do |m|
|
406
|
+
if m.cache_exists( cache_name )
|
407
|
+
log( ":-:-:-: local_manager already exists for #{cache_name}" )
|
408
|
+
return m
|
409
|
+
end
|
410
|
+
end
|
411
|
+
return nil
|
412
|
+
end
|
413
|
+
|
414
|
+
end
|
415
|
+
|
416
|
+
at_exit { Cache.shutdown }
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
|