AdoccaMemcache 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +5 -0
- data/Manifest.txt +12 -0
- data/README.txt +40 -0
- data/Rakefile +15 -0
- data/bin/adocca_memcache +0 -0
- data/lib/adocca_memcache.rb +16 -0
- data/lib/am_action_view.rb +6 -0
- data/lib/am_caching.rb +28 -0
- data/lib/am_memcache.rb +552 -0
- data/lib/am_memcache_memoize.rb +110 -0
- data/lib/am_memcache_store.rb +13 -0
- data/test/test_adocca_memcache.rb +0 -0
- metadata +64 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
data/README.txt
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
AdoccaMemcache
|
2
|
+
by Adocca AB
|
3
|
+
http://rubyforge.org/projects/adocca-plugins
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
A client library to simplify usage of memcached within Ruby on Rails projects.
|
8
|
+
|
9
|
+
== FEATURES/PROBLEMS:
|
10
|
+
|
11
|
+
* FIX (list of features or problems)
|
12
|
+
|
13
|
+
== SYNOPSYS:
|
14
|
+
|
15
|
+
FIX (code sample of usage)
|
16
|
+
|
17
|
+
== REQUIREMENTS:
|
18
|
+
|
19
|
+
* FIX (list of requirements)
|
20
|
+
|
21
|
+
== INSTALL:
|
22
|
+
|
23
|
+
* FIX (sudo gem install, anything else)
|
24
|
+
|
25
|
+
== LICENSE:
|
26
|
+
|
27
|
+
This program is free software; you can redistribute it and/or
|
28
|
+
modify it under the terms of the GNU General Public License
|
29
|
+
as published by the Free Software Foundation; either version 2
|
30
|
+
of the License, or (at your option) any later version.
|
31
|
+
|
32
|
+
This program is distributed in the hope that it will be useful,
|
33
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
34
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
35
|
+
GNU General Public License for more details.
|
36
|
+
|
37
|
+
You should have received a copy of the GNU General Public License
|
38
|
+
along with this program; if not, write to the Free Software
|
39
|
+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
40
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hoe'
|
5
|
+
require './lib/adocca_memcache.rb'
|
6
|
+
|
7
|
+
Hoe.new('AdoccaMemcache', '0.1.0') do |p|
|
8
|
+
p.rubyforge_name = 'adocca-plugins'
|
9
|
+
p.summary = 'A client library to simplify using memcached with Ruby on Rails projects.'
|
10
|
+
p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n")
|
11
|
+
p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1]
|
12
|
+
p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
|
13
|
+
end
|
14
|
+
|
15
|
+
# vim: syntax=Ruby
|
data/bin/adocca_memcache
ADDED
File without changes
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# Include hook code here
|
2
|
+
|
3
|
+
$: << File.dirname(__FILE__)
|
4
|
+
|
5
|
+
require_gem 'rails'
|
6
|
+
require 'am_action_view.rb'
|
7
|
+
require 'am_memcache.rb'
|
8
|
+
require 'am_memcache_store.rb'
|
9
|
+
require 'am_caching.rb'
|
10
|
+
require 'am_memcache_memoize.rb'
|
11
|
+
|
12
|
+
ActiveRecord::Base.send(:include, MemcacheMemoize)
|
13
|
+
ActiveRecord::Base.send(:extend, MemcacheMemoize)
|
14
|
+
ActionView::Base.send(:include, MemcacheMemoize)
|
15
|
+
ActionController::Base.send(:include, MemcacheMemoize)
|
16
|
+
ActionController::Base.send(:extend, MemcacheMemoize)
|
data/lib/am_caching.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
module ActionView
|
2
|
+
module Helpers
|
3
|
+
module CacheHelper
|
4
|
+
def cache_value_erb(basename, expiry=0, args={}, &block)
|
5
|
+
@controller.cache_value_erb(basename, expiry, args, &block)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
module ActionController
|
11
|
+
module Caching
|
12
|
+
module Fragments
|
13
|
+
def cache_value_erb(basename, expiry=0, args={}, &block)
|
14
|
+
buffer = eval("_erbout", block.binding)
|
15
|
+
|
16
|
+
if cache = cache_read(basename,args)
|
17
|
+
buffer.concat(cache)
|
18
|
+
else
|
19
|
+
pos = buffer.length
|
20
|
+
block.call
|
21
|
+
cache_value(basename, expiry, args) do
|
22
|
+
buffer[pos..-1]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/am_memcache.rb
ADDED
@@ -0,0 +1,552 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'thread'
|
3
|
+
require 'digest/sha1'
|
4
|
+
require 'timeout'
|
5
|
+
|
6
|
+
module Adocca
|
7
|
+
|
8
|
+
#
|
9
|
+
# Extend this module to be able to magically synchronize on any object, just like in Java(tm)!
|
10
|
+
#
|
11
|
+
module Synchronized
|
12
|
+
def synchronize(&block)
|
13
|
+
raise "Adocca::Synchronized::CACHE is not defined!" unless defined?(Adocca::Synchronized::CACHE)
|
14
|
+
Adocca::Synchronized::CACHE.synchronize(self.inspect) do
|
15
|
+
yield
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class NullMutex
|
21
|
+
def synchronize(&block)
|
22
|
+
block.call
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# A Ruby client library for memcached. Ripped from the gem.
|
28
|
+
#
|
29
|
+
class MemCache
|
30
|
+
|
31
|
+
# a semi-unique id for this ruby instance
|
32
|
+
HOST_HASH = Array.new(32).collect do |e| (65 + rand(25)).chr end.join
|
33
|
+
|
34
|
+
# Default options for the cache object.
|
35
|
+
DEFAULT_OPTIONS = {
|
36
|
+
:namespace => nil,
|
37
|
+
:readonly => false
|
38
|
+
}
|
39
|
+
|
40
|
+
# Default memcached port.
|
41
|
+
DEFAULT_PORT = 11211
|
42
|
+
|
43
|
+
# Default memcached server weight.
|
44
|
+
DEFAULT_WEIGHT = 1
|
45
|
+
|
46
|
+
# The amount of time to wait for a response from a memcached server. If a
|
47
|
+
# response is not completed within this time, the connection to the server
|
48
|
+
# will be closed and an error will be raised.
|
49
|
+
attr_accessor :request_timeout
|
50
|
+
|
51
|
+
# Valid options are:
|
52
|
+
#
|
53
|
+
# :namespace
|
54
|
+
# If specified, all keys will have the given value prepended
|
55
|
+
# before accessing the cache. Defaults to nil.
|
56
|
+
#
|
57
|
+
# :readonly
|
58
|
+
# If this is set, any attempt to write to the cache will generate
|
59
|
+
# an exception. Defaults to false.
|
60
|
+
#
|
61
|
+
def initialize(opts = {})
|
62
|
+
opts = DEFAULT_OPTIONS.merge(opts)
|
63
|
+
@namespace = opts[:namespace]
|
64
|
+
@readonly = opts[:readonly]
|
65
|
+
if ActionController::Base.allow_concurrency
|
66
|
+
@mutex = Mutex.new
|
67
|
+
else
|
68
|
+
@mutex = NullMutex.new
|
69
|
+
end
|
70
|
+
@servers = []
|
71
|
+
@buckets = []
|
72
|
+
end
|
73
|
+
|
74
|
+
# Return a string representation of the cache object.
|
75
|
+
def inspect
|
76
|
+
sprintf("<MemCache: %s servers, %s buckets, ns: %p, ro: %p>",
|
77
|
+
@servers.nitems, @buckets.nitems, @namespace, @readonly)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Returns whether there is at least one active server for the object.
|
81
|
+
def active?
|
82
|
+
not @servers.empty?
|
83
|
+
end
|
84
|
+
|
85
|
+
# Returns whether the cache was created read only.
|
86
|
+
def readonly?
|
87
|
+
@readonly
|
88
|
+
end
|
89
|
+
|
90
|
+
# Set the servers that the requests will be distributed between. Entries
|
91
|
+
# can be either strings of the form "hostname:port" or
|
92
|
+
# "hostname:port:weight" or MemCache::Server objects.
|
93
|
+
def servers=(servers)
|
94
|
+
# Create the server objects.
|
95
|
+
@servers = servers.collect do |server|
|
96
|
+
case server
|
97
|
+
when String
|
98
|
+
host, port, weight = server.split(/:/, 3)
|
99
|
+
port ||= DEFAULT_PORT
|
100
|
+
weight ||= DEFAULT_WEIGHT
|
101
|
+
Server::new(host, port, weight)
|
102
|
+
when Server
|
103
|
+
server
|
104
|
+
else
|
105
|
+
raise TypeError, "Cannot convert %s to MemCache::Server" %
|
106
|
+
svr.class.name
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Create an array of server buckets for weight selection of servers.
|
111
|
+
@buckets = []
|
112
|
+
@servers.each do |server|
|
113
|
+
server.weight.times { @buckets.push(server) }
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# get an entry from the cache
|
118
|
+
def get(key)
|
119
|
+
cache_key = make_cache_key(key)
|
120
|
+
rval = nil
|
121
|
+
@mutex.synchronize do
|
122
|
+
begin
|
123
|
+
response, sock, server = sock_send_command(cache_key, "get #{cache_key}")
|
124
|
+
return :MemCache_no_such_entry if response =~ /^END/
|
125
|
+
|
126
|
+
v, cache_key, flags, bytes = response.split(/ /)
|
127
|
+
rval = sock.read(bytes.to_i)
|
128
|
+
|
129
|
+
sock.gets
|
130
|
+
sock.gets
|
131
|
+
rescue SystemCallError, IOError => err
|
132
|
+
server.close
|
133
|
+
raise MemCacheError, err.message
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
if rval.strip.match(/^\d+$/)
|
138
|
+
rval.strip.to_i
|
139
|
+
else
|
140
|
+
# Return the unmarshaled value.
|
141
|
+
begin
|
142
|
+
Marshal.load(rval)
|
143
|
+
rescue ArgumentError, TypeError => err
|
144
|
+
raise MemCacheError, err.message
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
#
|
150
|
+
# Invalidate a namespace in the cache.
|
151
|
+
#
|
152
|
+
def invalidate_namespace(ns)
|
153
|
+
inc(make_namespace_key(ns).last)
|
154
|
+
end
|
155
|
+
|
156
|
+
#
|
157
|
+
# Increment an entry in the cache
|
158
|
+
#
|
159
|
+
def inc(key, amount = 1)
|
160
|
+
cache_key = make_cache_key(key)
|
161
|
+
rval = nil
|
162
|
+
@mutex.synchronize do
|
163
|
+
rval = send_command(cache_key, "incr #{cache_key} #{amount}")
|
164
|
+
end
|
165
|
+
rval = rval.strip.to_i if rval =~ /^\d+/
|
166
|
+
rval
|
167
|
+
end
|
168
|
+
|
169
|
+
#
|
170
|
+
# Increment an entry in the cache
|
171
|
+
#
|
172
|
+
def dec(key, amount = 1)
|
173
|
+
cache_key = make_cache_key(key)
|
174
|
+
rval = nil
|
175
|
+
@mutex.synchronize do
|
176
|
+
rval = send_command(cache_key, "decr #{cache_key} #{amount}")
|
177
|
+
end
|
178
|
+
rval = rval.strip.to_i if rval =~ /^\d+/
|
179
|
+
rval
|
180
|
+
end
|
181
|
+
|
182
|
+
# the response when the key is present
|
183
|
+
ADD_NOTSTORED = "NOT_STORED\r\n"
|
184
|
+
# the response when the key is NOT present
|
185
|
+
ADD_STORED = "STORED\r\n"
|
186
|
+
|
187
|
+
# Add an entry to the cache
|
188
|
+
# will return true if the entry didnt already exist
|
189
|
+
def add(key, value, expiry = 0)
|
190
|
+
cache_key = make_cache_key(key)
|
191
|
+
marshaled_value = nil
|
192
|
+
if Integer === value
|
193
|
+
marshaled_value = value.to_s
|
194
|
+
else
|
195
|
+
marshaled_value = Marshal.dump(value)
|
196
|
+
end
|
197
|
+
@mutex.synchronize do
|
198
|
+
send_command(cache_key, "add #{cache_key} 0 #{expiry} #{marshaled_value.size}\r\n" + marshaled_value) == ADD_STORED
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# Add an entry to the cache.
|
203
|
+
def set(key, value, expiry = 0)
|
204
|
+
cache_key = make_cache_key(key)
|
205
|
+
marshaled_value = nil
|
206
|
+
if Integer === value
|
207
|
+
marshaled_value = value.to_s
|
208
|
+
else
|
209
|
+
marshaled_value = Marshal.dump(value)
|
210
|
+
end
|
211
|
+
@mutex.synchronize do
|
212
|
+
send_command(cache_key, "set #{cache_key} 0 #{expiry} #{marshaled_value.size}\r\n" + marshaled_value)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# Remove an entry from the cache.
|
217
|
+
# return true if it is deleted
|
218
|
+
def delete(key, expiry = 0)
|
219
|
+
cache_key = make_cache_key(key)
|
220
|
+
@mutex.synchronize do
|
221
|
+
send_command(cache_key, "delete #{cache_key} #{expiry}")
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
# Reset the connection to all memcache servers. This should be called if
|
226
|
+
# there is a problem with a cache lookup that might have left the
|
227
|
+
# connection in a corrupted state.
|
228
|
+
def reset
|
229
|
+
@mutex.synchronize do
|
230
|
+
@servers.each { |server| server.close }
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# Method to check if a value is set, and NOT unmarshal, only return bool
|
235
|
+
def check(key)
|
236
|
+
cache_key = make_cache_key(key)
|
237
|
+
@mutex.synchronize do
|
238
|
+
|
239
|
+
begin
|
240
|
+
response, sock, server = sock_send_command(cache_key, "get #{cache_key}")
|
241
|
+
|
242
|
+
return false if response =~ /^END/
|
243
|
+
|
244
|
+
v, key, flags, bytes = response.split(/ /)
|
245
|
+
value = sock.read(bytes.to_i)
|
246
|
+
sock.gets
|
247
|
+
sock.gets
|
248
|
+
rescue SystemCallError, IOError => err
|
249
|
+
server.close
|
250
|
+
raise MemCacheError, err.message
|
251
|
+
end
|
252
|
+
|
253
|
+
true
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
# Flush cache
|
258
|
+
def flush
|
259
|
+
@mutex.synchronize do
|
260
|
+
raise MemCacheError, "No active servers" unless self.active?
|
261
|
+
@servers.each do |server|
|
262
|
+
|
263
|
+
sock = server.socket
|
264
|
+
if sock.nil?
|
265
|
+
raise MemCacheError, "No connection to server"
|
266
|
+
end
|
267
|
+
|
268
|
+
begin
|
269
|
+
sock.write "flush_all\r\n"
|
270
|
+
sock.gets
|
271
|
+
rescue SystemCallError, IOError => err
|
272
|
+
server.close
|
273
|
+
raise MemCacheError, err.message
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
# lock expiry time
|
280
|
+
TRANS_EXPTIME = 10
|
281
|
+
TRANS_MAX_TRIES = 10
|
282
|
+
|
283
|
+
# memcache-driven transaction mechanism
|
284
|
+
def synchronize(key)
|
285
|
+
cache_key = make_cache_key(key)
|
286
|
+
tries = 0
|
287
|
+
we_locked_now = false
|
288
|
+
we_locked_earlier = false
|
289
|
+
ok_value = "#{HOST_HASH}:#{Thread.current.object_id}"
|
290
|
+
while (!(we_locked_now = add(cache_key, ok_value, TRANS_EXPTIME)) &&
|
291
|
+
!(we_locked_earlier = (get(cache_key) == ok_value)) &&
|
292
|
+
tries < TRANS_MAX_TRIES)
|
293
|
+
sleep 1
|
294
|
+
tries += 1
|
295
|
+
end
|
296
|
+
if we_locked_now || we_locked_earlier
|
297
|
+
yield
|
298
|
+
self.delete(cache_key) if we_locked_now
|
299
|
+
else
|
300
|
+
raise MemCacheError, "Couldn't obtain lock"
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
# Shortcut to get a value from the cache.
|
305
|
+
def [](key)
|
306
|
+
self.get(key)
|
307
|
+
end
|
308
|
+
|
309
|
+
# Shortcut to save a value in the cache. This method does not set an
|
310
|
+
# expiration on the entry. Use set to specify an explicit expiry.
|
311
|
+
def []=(key, value)
|
312
|
+
self.set(key, value)
|
313
|
+
end
|
314
|
+
|
315
|
+
###########################################################################
|
316
|
+
# S E R V E R C L A S S
|
317
|
+
###########################################################################
|
318
|
+
|
319
|
+
# This class represents a memcached server instance.
|
320
|
+
class Server
|
321
|
+
# The amount of time to wait to establish a connection with a
|
322
|
+
# memcached server. If a connection cannot be established within
|
323
|
+
# this time limit, the server will be marked as down.
|
324
|
+
CONNECT_TIMEOUT = 0.25
|
325
|
+
|
326
|
+
# The amount of time to wait before attempting to re-establish a
|
327
|
+
# connection with a server that is marked dead.
|
328
|
+
RETRY_DELAY = 30.0
|
329
|
+
|
330
|
+
# The host the memcached server is running on.
|
331
|
+
attr_reader :host
|
332
|
+
|
333
|
+
# The port the memcached server is listening on.
|
334
|
+
attr_reader :port
|
335
|
+
|
336
|
+
# The weight given to the server.
|
337
|
+
attr_reader :weight
|
338
|
+
|
339
|
+
# The time of next retry if the connection is dead.
|
340
|
+
attr_reader :retry
|
341
|
+
|
342
|
+
# A text status string describing the state of the server.
|
343
|
+
attr_reader :status
|
344
|
+
|
345
|
+
# Create a new MemCache::Server object for the memcached instance
|
346
|
+
# listening on the given host and port, weighted by the given weight.
|
347
|
+
def initialize(host, port = DEFAULT_PORT, weight = DEFAULT_WEIGHT)
|
348
|
+
if host.nil? || host.empty?
|
349
|
+
raise ArgumentError, "No host specified"
|
350
|
+
elsif port.nil? || port.to_i.zero?
|
351
|
+
raise ArgumentError, "No port specified"
|
352
|
+
end
|
353
|
+
|
354
|
+
@host = host
|
355
|
+
@port = port.to_i
|
356
|
+
@weight = weight.to_i
|
357
|
+
|
358
|
+
@sock = nil
|
359
|
+
@retry = nil
|
360
|
+
@status = "NOT CONNECTED"
|
361
|
+
end
|
362
|
+
|
363
|
+
# Return a string representation of the server object.
|
364
|
+
def inspect
|
365
|
+
sprintf("<MemCache::Server: %s:%d [%d] (%s)>",
|
366
|
+
@host, @port, @weight, @status)
|
367
|
+
end
|
368
|
+
|
369
|
+
# Check whether the server connection is alive. This will cause the
|
370
|
+
# socket to attempt to connect if it isn't already connected and or if
|
371
|
+
# the server was previously marked as down and the retry time has
|
372
|
+
# been exceeded.
|
373
|
+
def alive?
|
374
|
+
!self.socket.nil?
|
375
|
+
end
|
376
|
+
|
377
|
+
# Try to connect to the memcached server targeted by this object.
|
378
|
+
# Returns the connected socket object on success or nil on failure.
|
379
|
+
def socket
|
380
|
+
# Attempt to connect if not already connected.
|
381
|
+
unless @sock || (!@sock.nil? && @sock.closed?)
|
382
|
+
# If the host was dead, don't retry for a while.
|
383
|
+
if @retry && (@retry > Time::now)
|
384
|
+
@sock = nil
|
385
|
+
else
|
386
|
+
begin
|
387
|
+
@sock = timeout(CONNECT_TIMEOUT) {
|
388
|
+
TCPSocket::new(@host, @port)
|
389
|
+
}
|
390
|
+
@retry = nil
|
391
|
+
@status = "CONNECTED"
|
392
|
+
rescue SystemCallError, IOError, Timeout::Error => err
|
393
|
+
self.mark_dead(err.message)
|
394
|
+
end
|
395
|
+
end
|
396
|
+
end
|
397
|
+
@sock
|
398
|
+
end
|
399
|
+
|
400
|
+
# Close the connection to the memcached server targeted by this
|
401
|
+
# object. The server is not considered dead.
|
402
|
+
def close
|
403
|
+
@sock.close if @sock &&!@sock.closed?
|
404
|
+
@sock = nil
|
405
|
+
@retry = nil
|
406
|
+
@status = "NOT CONNECTED"
|
407
|
+
end
|
408
|
+
|
409
|
+
# Mark the server as dead and close its socket.
|
410
|
+
def mark_dead(reason = "Unknown error")
|
411
|
+
@sock.close if @sock && !@sock.closed?
|
412
|
+
@sock = nil
|
413
|
+
@retry = Time::now + RETRY_DELAY
|
414
|
+
|
415
|
+
@status = sprintf("DEAD: %s, will retry at %s", reason, @retry)
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
|
420
|
+
###########################################################################
|
421
|
+
# E X C E P T I O N C L A S S E S
|
422
|
+
###########################################################################
|
423
|
+
|
424
|
+
# Base MemCache exception class.
|
425
|
+
class MemCacheError < ::Exception
|
426
|
+
end
|
427
|
+
|
428
|
+
# MemCache internal error class. Instances of this class mean that there
|
429
|
+
# is some internal error either in the memcache client library or the
|
430
|
+
# memcached server it is talking to.
|
431
|
+
class InternalError < MemCacheError
|
432
|
+
end
|
433
|
+
|
434
|
+
# MemCache client error class. Instances of this class mean that a
|
435
|
+
# "CLIENT_ERROR" response was seen in the dialog with a memcached server.
|
436
|
+
class ClientError < InternalError
|
437
|
+
end
|
438
|
+
|
439
|
+
# MemCache server error class. Instances of this class mean that a
|
440
|
+
# "SERVER_ERROR" response was seen in the dialog with a memcached server.
|
441
|
+
class ServerError < InternalError
|
442
|
+
attr_reader :server
|
443
|
+
|
444
|
+
def initalize(server)
|
445
|
+
@server = server
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
# private
|
450
|
+
|
451
|
+
#
|
452
|
+
# Will send a command to the proper server for this key
|
453
|
+
# and return the response
|
454
|
+
#
|
455
|
+
def send_command(key, command)
|
456
|
+
sock_send_command(key, command)[0]
|
457
|
+
end
|
458
|
+
|
459
|
+
#
|
460
|
+
# Will send a command to the proper server for this key
|
461
|
+
# and return the response, socket and server
|
462
|
+
#
|
463
|
+
def sock_send_command(key, command)
|
464
|
+
raise MemCacheError, "No active servers" unless self.active?
|
465
|
+
raise MemCacheError, "Update of readonly cache" if @readonly
|
466
|
+
server = get_server_for_key(key)
|
467
|
+
|
468
|
+
sock = server.socket
|
469
|
+
if sock.nil?
|
470
|
+
raise MemCacheError, "No connection to server"
|
471
|
+
end
|
472
|
+
|
473
|
+
response = nil
|
474
|
+
begin
|
475
|
+
sock.write "#{command}\r\n"
|
476
|
+
[sock.gets, sock, server]
|
477
|
+
rescue SystemCallError, IOError => err
|
478
|
+
server.close
|
479
|
+
raise MemCacheError, err.message
|
480
|
+
end
|
481
|
+
end
|
482
|
+
|
483
|
+
#
|
484
|
+
# Create a key for this namespace and
|
485
|
+
# return it along with the string you want
|
486
|
+
# to delete or incr to invalidate this specific
|
487
|
+
# namespace
|
488
|
+
#
|
489
|
+
def make_namespace_key(ns, appendage = nil)
|
490
|
+
# ensure that this is an Array
|
491
|
+
ns = ns.to_a
|
492
|
+
# get the first part of the Array
|
493
|
+
next_part = ns.shift
|
494
|
+
# create a unique namespace name of it
|
495
|
+
next_part = [appendage, next_part].join(":") if appendage
|
496
|
+
# create a unique memcache key of the namespace name
|
497
|
+
next_key = "adocca:memcache:namespace:#{next_part}"
|
498
|
+
# make sure it exists in memcache
|
499
|
+
add(next_key, rand(1 << 31))
|
500
|
+
# get its value in memcache
|
501
|
+
salt = get(next_key)
|
502
|
+
if ns.empty?
|
503
|
+
# if this is the last part of the key return it + its secret salt along with itself (for invalidation purposes)
|
504
|
+
["#{next_part}:#{salt}", next_key]
|
505
|
+
else
|
506
|
+
# otherwise, fetch the rest along with the
|
507
|
+
last_part = make_namespace_key(ns, next_part)
|
508
|
+
["#{next_part}:#{salt}:#{last_part.first}", last_part.last]
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
512
|
+
# Create a key for the cache, incorporating the namespace qualifier if
|
513
|
+
# requested.
|
514
|
+
def make_cache_key(o)
|
515
|
+
if Array === o
|
516
|
+
o = o.clone
|
517
|
+
key = o.pop
|
518
|
+
|
519
|
+
namespace_key = ""
|
520
|
+
namespace_key << ":#{make_namespace_key(o).first}" unless o.empty?
|
521
|
+
|
522
|
+
key = "#{@namespace}#{namespace_key}:#{key}"
|
523
|
+
Digest::SHA1.new(key)
|
524
|
+
else
|
525
|
+
key = "#{@namespace}:#{o}"
|
526
|
+
Digest::SHA1.new(key)
|
527
|
+
end
|
528
|
+
end
|
529
|
+
|
530
|
+
# Pick a server to handle the request based on a hash of the key.
|
531
|
+
def get_server_for_key(key)
|
532
|
+
# Easy enough if there is only one server.
|
533
|
+
return @servers.first if @servers.length == 1
|
534
|
+
|
535
|
+
# Hash the value of the key to select the bucket.
|
536
|
+
hkey = Integer("0x#{key}")
|
537
|
+
|
538
|
+
# Fetch a server for the given key, retrying if that server is
|
539
|
+
# offline.
|
540
|
+
server = nil
|
541
|
+
20.times do |tries|
|
542
|
+
server = @buckets[(hkey + tries) % @buckets.nitems]
|
543
|
+
break if server.alive?
|
544
|
+
end
|
545
|
+
|
546
|
+
raise MemCacheError, "No servers available" unless server
|
547
|
+
server
|
548
|
+
end
|
549
|
+
|
550
|
+
|
551
|
+
end
|
552
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module MemcacheMemoize
|
2
|
+
|
3
|
+
# The following is used to cache, using memcache, the results of executing a code block.
|
4
|
+
# The scope of the caching is determined by the basename and the hash args. For instance:
|
5
|
+
#
|
6
|
+
# cache_value(:unread_messages, 0, :user => @user.id) do
|
7
|
+
# Messages.find(:all, :conditions => ......).size
|
8
|
+
# end
|
9
|
+
#
|
10
|
+
# will store the result of the block under the key "unread_messages:user:123" in memcache,
|
11
|
+
# for ever (expiry=0 seconds, meaning no expiration time). The number of hash pairs is
|
12
|
+
# unlimited. Duplicates will be removed.
|
13
|
+
#
|
14
|
+
def cache_value(basename, expiry=0, args={}, &cached_code)
|
15
|
+
return cached_code.call if $DISABLE_CACHE
|
16
|
+
key = compute_memcache_key(basename, args)
|
17
|
+
# Check if the value is cached
|
18
|
+
result = CACHE.get key
|
19
|
+
if result == :MemCache_no_such_entry
|
20
|
+
# No, we need to call the block and cache the result
|
21
|
+
result = cached_code.call
|
22
|
+
CACHE.set key, result, expiry
|
23
|
+
RAILS_DEFAULT_LOGGER.debug "MemCache: Computed and set value for #{key.inspect} (expiry: #{expiry} seconds)"
|
24
|
+
else
|
25
|
+
RAILS_DEFAULT_LOGGER.debug "MemCache: Got #{key.inspect}"
|
26
|
+
end
|
27
|
+
# Return the result
|
28
|
+
result
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Only read a value from memcache
|
33
|
+
#
|
34
|
+
|
35
|
+
def cache_read(basename, args={})
|
36
|
+
return nil if $DISABLE_CACHE
|
37
|
+
key = compute_memcache_key(basename, args)
|
38
|
+
result = CACHE.get key
|
39
|
+
RAILS_DEFAULT_LOGGER.debug "MemCache: Read #{key.inspect}"
|
40
|
+
if result == :MemCache_no_such_entry
|
41
|
+
nil
|
42
|
+
else
|
43
|
+
result
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
#
|
48
|
+
# Returns the obj, unless the obj is_a?(ActiveRecord::Base), in which case it will return
|
49
|
+
# the object without associations.
|
50
|
+
#
|
51
|
+
def without_associations(obj)
|
52
|
+
if obj.is_a?(ActiveRecord::Base)
|
53
|
+
obj.remove_associations
|
54
|
+
else
|
55
|
+
obj
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# This method is used to expire cached data. Basename and args are as per cache_value(),
|
61
|
+
# but expiry controls in how many seconds the expiration is to take place. The default
|
62
|
+
# value of 0 means 'immediately'.
|
63
|
+
#
|
64
|
+
def expire_cached_value(basename, expiry=0, args={})
|
65
|
+
key = compute_memcache_key(basename, args)
|
66
|
+
RAILS_DEFAULT_LOGGER.debug "MemCache: Invalidating #{key.inspect}"
|
67
|
+
CACHE.delete key, expiry
|
68
|
+
end
|
69
|
+
|
70
|
+
#
|
71
|
+
# Helper method to invalidate namespaces
|
72
|
+
#
|
73
|
+
def expire_cached_namespace(namespace_string)
|
74
|
+
RAILS_DEFAULT_LOGGER.debug "MemCache: Invalidating namespace: #{namespace_string.to_s}"
|
75
|
+
CACHE.invalidate_namespace(namespace_string)
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
# Return true if there is such a value in the memcache, otherwise false
|
80
|
+
#
|
81
|
+
def check_cached_value(basename, args={})
|
82
|
+
key = compute_memcache_key(basename, args)
|
83
|
+
rval = CACHE.check(key)
|
84
|
+
RAILS_DEFAULT_LOGGER.debug "MemCache: Checked for #{key.inspect} - returned: " + rval.to_s
|
85
|
+
return rval
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
#
|
91
|
+
# Compute the memcache key string from a basename and a hash
|
92
|
+
#
|
93
|
+
def compute_memcache_key(base, args)
|
94
|
+
key_base = Array(base)
|
95
|
+
# First sort the args so that their order doesn't matter. Also eliminate duplicates.
|
96
|
+
args = (args.sort_by { |key, val| key.to_s }).uniq
|
97
|
+
|
98
|
+
# Construct and return the memcache key string
|
99
|
+
key_base[-1] = key_base.last.to_s << ":" << args.inject("") { |acc, assoc| acc + ':' + assoc[0].to_s + ':' + assoc[1].to_s } unless args.empty?
|
100
|
+
|
101
|
+
key_base
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
ActiveRecord::Base.send(:include, MemcacheMemoize)
|
107
|
+
ActiveRecord::Base.send(:extend, MemcacheMemoize)
|
108
|
+
ActionView::Base.send(:include, MemcacheMemoize)
|
109
|
+
ActionController::Base.send(:include, MemcacheMemoize)
|
110
|
+
ActionController::Base.send(:extend, MemcacheMemoize)
|
File without changes
|
metadata
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.8.11
|
3
|
+
specification_version: 1
|
4
|
+
name: AdoccaMemcache
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.1.0
|
7
|
+
date: 2006-11-13 00:00:00 +01:00
|
8
|
+
summary: A client library to simplify using memcached with Ruby on Rails projects.
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: ryand-ruby@zenspider.com
|
12
|
+
homepage: " by Adocca AB"
|
13
|
+
rubyforge_project: adocca-plugins
|
14
|
+
description: "== FEATURES/PROBLEMS: * FIX (list of features or problems) == SYNOPSYS: FIX (code sample of usage) == REQUIREMENTS:"
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
authors:
|
29
|
+
- Ryan Davis
|
30
|
+
files:
|
31
|
+
- History.txt
|
32
|
+
- Manifest.txt
|
33
|
+
- README.txt
|
34
|
+
- Rakefile
|
35
|
+
- bin/adocca_memcache
|
36
|
+
- lib/adocca_memcache.rb
|
37
|
+
- lib/am_memcache.rb
|
38
|
+
- lib/am_action_view.rb
|
39
|
+
- lib/am_caching.rb
|
40
|
+
- lib/am_memcache_store.rb
|
41
|
+
- lib/am_memcache_memoize.rb
|
42
|
+
- test/test_adocca_memcache.rb
|
43
|
+
test_files:
|
44
|
+
- test/test_adocca_memcache.rb
|
45
|
+
rdoc_options: []
|
46
|
+
|
47
|
+
extra_rdoc_files: []
|
48
|
+
|
49
|
+
executables:
|
50
|
+
- adocca_memcache
|
51
|
+
extensions: []
|
52
|
+
|
53
|
+
requirements: []
|
54
|
+
|
55
|
+
dependencies:
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: hoe
|
58
|
+
version_requirement:
|
59
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: 1.1.4
|
64
|
+
version:
|