AdoccaMemcache 0.1.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/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:
|