myobie-memcache-client 1.5.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,85 @@
1
+ = 1.5.0.3 (FiveRuns fork)
2
+
3
+ * Integrated ITU-T CRC32 operation in native C extension for speed. Thanks to Justin Balthrop!
4
+
5
+ = 1.5.0.2 (FiveRuns fork)
6
+
7
+ * Add support for seamless failover between servers. If one server connection dies,
8
+ the client will retry the operation on another server before giving up.
9
+
10
+ * Merge Will Bryant's socket retry patch.
11
+ http://willbryant.net/software/2007/12/21/ruby-memcache-client-reconnect-and-retry
12
+
13
+ = 1.5.0.1 (FiveRuns fork)
14
+
15
+ * Fix set not handling client disconnects.
16
+ http://dev.twitter.com/2008/02/solving-case-of-missing-updates.html
17
+
18
+ = 1.5.0
19
+
20
+ * Add MemCache#flush_all command. Patch #13019 and bug #10503. Patches
21
+ submitted by Sebastian Delmont and Rick Olson.
22
+ * Type-cast data returned by MemCache#stats. Patch #10505 submitted by
23
+ Sebastian Delmont.
24
+
25
+ = 1.4.0
26
+
27
+ * Fix bug #10371, #set does not check response for server errors.
28
+ Submitted by Ben VandenBos.
29
+ * Fix bug #12450, set TCP_NODELAY socket option. Patch by Chris
30
+ McGrath.
31
+ * Fix bug #10704, missing #add method. Patch by Jamie Macey.
32
+ * Fix bug #10371, handle socket EOF in cache_get. Submitted by Ben
33
+ VandenBos.
34
+
35
+ = 1.3.0
36
+
37
+ * Apply patch #6507, add stats command. Submitted by Tyler Kovacs.
38
+ * Apply patch #6509, parallel implementation of #get_multi. Submitted
39
+ by Tyler Kovacs.
40
+ * Validate keys. Disallow spaces in keys or keys that are too long.
41
+ * Perform more validation of server responses. MemCache now reports
42
+ errors if the socket was not in an expected state. (Please file
43
+ bugs if you find some.)
44
+ * Add #incr and #decr.
45
+ * Add raw argument to #set and #get to retrieve #incr and #decr
46
+ values.
47
+ * Also put on MemCacheError when using Cache::get with block.
48
+ * memcache.rb no longer sets $TESTING to a true value if it was
49
+ previously defined. Bug #8213 by Matijs van Zuijlen.
50
+
51
+ = 1.2.1
52
+
53
+ * Fix bug #7048, MemCache#servers= referenced changed local variable.
54
+ Submitted by Justin Dossey.
55
+ * Fix bug #7049, MemCache#initialize resets @buckets. Submitted by
56
+ Justin Dossey.
57
+ * Fix bug #6232, Make Cache::Get work with a block only when nil is
58
+ returned. Submitted by Jon Evans.
59
+ * Moved to the seattlerb project.
60
+
61
+ = 1.2.0
62
+
63
+ NOTE: This version will store keys in different places than previous
64
+ versions! Be prepared for some thrashing while memcached sorts itself
65
+ out!
66
+
67
+ * Fixed multithreaded operations, bug 5994 and 5989.
68
+ Thanks to Blaine Cook, Erik Hetzner, Elliot Smith, Dave Myron (and
69
+ possibly others I have forgotten).
70
+ * Made memcached interoperable with other memcached libraries, bug
71
+ 4509. Thanks to anonymous.
72
+ * Added get_multi to match Perl/etc APIs
73
+
74
+ = 1.1.0
75
+
76
+ * Added some tests
77
+ * Sped up non-multithreaded and multithreaded operation
78
+ * More Ruby-memcache compatibility
79
+ * More RDoc
80
+ * Switched to Hoe
81
+
82
+ = 1.0.0
83
+
84
+ Birthday!
85
+
data/README.txt ADDED
@@ -0,0 +1,54 @@
1
+ = memcache-client
2
+
3
+ This is the FiveRuns fork of seattle.rb's memcache-client 1.5.0. We've fixed several bugs
4
+ which are in that version.
5
+
6
+ Rubyforge Project:
7
+
8
+ http://rubyforge.org/projects/seattlerb
9
+
10
+ Documentation:
11
+
12
+ http://seattlerb.org/memcache-client
13
+
14
+ == Installing memcache-client
15
+
16
+ Just install the gem:
17
+
18
+ $ sudo gem install fiveruns-memcache-client --source http://gems.github.com
19
+
20
+ == Using memcache-client
21
+
22
+ With one server:
23
+
24
+ CACHE = MemCache.new 'localhost:11211', :namespace => 'my_namespace'
25
+
26
+ Or with multiple servers:
27
+
28
+ CACHE = MemCache.new %w[one.example.com:11211 two.example.com:11211],
29
+ :namespace => 'my_namespace'
30
+
31
+ See MemCache.new for details.
32
+
33
+ === Using memcache-client with Rails
34
+
35
+ Rails will automatically load the memcache-client gem, but you may
36
+ need to uninstall Ruby-memcache, I don't know which one will get
37
+ picked by default.
38
+
39
+ Add your environment-specific caches to config/environment/*. If you run both
40
+ development and production on the same memcached server sets, be sure
41
+ to use different namespaces. Be careful when running tests using
42
+ memcache, you may get strange results. It will be less of a headache
43
+ to simply use a readonly memcache when testing.
44
+
45
+ memcache-client also comes with a wrapper called Cache in memcache_util.rb for
46
+ use with Rails. To use it be sure to assign your memcache connection to
47
+ CACHE. Cache returns nil on all memcache errors so you don't have to rescue
48
+ the errors yourself. It has #get, #put and #delete module functions.
49
+
50
+ === Improving Performance ===
51
+
52
+ Performing the CRC-32 ITU-T step to determine which server to use for a given key
53
+ is VERY slow in Ruby. RubyGems should compile a native library for performing this
54
+ operation when the gem is installed.
data/Rakefile ADDED
@@ -0,0 +1,24 @@
1
+ # vim: syntax=Ruby
2
+ require 'rubygems'
3
+ require 'rake/rdoctask'
4
+ require 'spec/rake/spectask'
5
+
6
+ task :gem do
7
+ sh "gem build memcache-client.gemspec"
8
+ end
9
+
10
+ task :install => [:gem] do
11
+ sh "sudo gem install memcache-client-*.gem"
12
+ end
13
+
14
+ Spec::Rake::SpecTask.new do |t|
15
+ t.ruby_opts = ['-rtest/unit']
16
+ t.spec_files = FileList['test/test_*.rb']
17
+ t.fail_on_error = true
18
+ end
19
+
20
+ Rake::RDocTask.new do |rd|
21
+ rd.main = "README.rdoc"
22
+ rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
23
+ rd.rdoc_dir = 'doc'
24
+ end
data/ext/crc32/crc32.c ADDED
@@ -0,0 +1,28 @@
1
+ #include "ruby.h"
2
+ #include "stdio.h"
3
+
4
+ static VALUE t_itu_t(VALUE self, VALUE string) {
5
+ VALUE str = StringValue(string);
6
+ int n = RSTRING(str)->len;
7
+ char* p = RSTRING(str)->ptr;
8
+ unsigned long r = 0xFFFFFFFF;
9
+ int i, j;
10
+
11
+ for (i = 0; i < n; i++) {
12
+ r = r ^ p[i];
13
+ for (j = 0; j < 8; j++) {
14
+ if ( (r & 1) != 0 ) {
15
+ r = (r >> 1) ^ 0xEDB88320;
16
+ } else {
17
+ r = r >> 1;
18
+ }
19
+ }
20
+ }
21
+ return INT2FIX(r ^ 0xFFFFFFFF);
22
+ }
23
+
24
+ VALUE cCRC32;
25
+ void Init_crc32() {
26
+ cCRC32 = rb_define_module("CRC32");
27
+ rb_define_module_function(cCRC32, "itu_t", t_itu_t, 1);
28
+ }
@@ -0,0 +1,5 @@
1
+ require 'mkmf'
2
+
3
+ dir_config("crc32")
4
+
5
+ create_makefile("crc32")
data/lib/memcache.rb ADDED
@@ -0,0 +1,791 @@
1
+ $TESTING = defined?($TESTING) && $TESTING
2
+
3
+ require 'socket'
4
+ require 'thread'
5
+ require 'timeout'
6
+ require 'rubygems'
7
+
8
+ class String
9
+
10
+ ##
11
+ # Uses the ITU-T polynomial in the CRC32 algorithm.
12
+ begin
13
+ require 'crc32'
14
+ def crc32_ITU_T
15
+ CRC32.itu_t(self)
16
+ end
17
+ rescue LoadError => e
18
+ puts "Loading with slow CRC32 ITU-T implementation: #{e.message}"
19
+
20
+ def crc32_ITU_T
21
+ n = length
22
+ r = 0xFFFFFFFF
23
+
24
+ n.times do |i|
25
+ r ^= self[i]
26
+ 8.times do
27
+ if (r & 1) != 0 then
28
+ r = (r>>1) ^ 0xEDB88320
29
+ else
30
+ r >>= 1
31
+ end
32
+ end
33
+ end
34
+
35
+ r ^ 0xFFFFFFFF
36
+ end
37
+ end
38
+
39
+ end
40
+
41
+ ##
42
+ # A Ruby client library for memcached.
43
+ #
44
+ # This is intended to provide access to basic memcached functionality. It
45
+ # does not attempt to be complete implementation of the entire API, but it is
46
+ # approaching a complete implementation.
47
+
48
+ class MemCache
49
+
50
+ ##
51
+ # The version of MemCache you are using.
52
+
53
+ VERSION = '1.5.0.1'
54
+
55
+ ##
56
+ # Default options for the cache object.
57
+
58
+ DEFAULT_OPTIONS = {
59
+ :namespace => nil,
60
+ :readonly => false,
61
+ :multithread => false,
62
+ }
63
+
64
+ ##
65
+ # Default memcached port.
66
+
67
+ DEFAULT_PORT = 11211
68
+
69
+ ##
70
+ # Default memcached server weight.
71
+
72
+ DEFAULT_WEIGHT = 1
73
+
74
+ ##
75
+ # The amount of time to wait for a response from a memcached server. If a
76
+ # response is not completed within this time, the connection to the server
77
+ # will be closed and an error will be raised.
78
+
79
+ attr_accessor :request_timeout
80
+
81
+ ##
82
+ # The namespace for this instance
83
+
84
+ attr_reader :namespace
85
+
86
+ ##
87
+ # The multithread setting for this instance
88
+
89
+ attr_reader :multithread
90
+
91
+ ##
92
+ # The servers this client talks to. Play at your own peril.
93
+
94
+ attr_reader :servers
95
+
96
+ ##
97
+ # Accepts a list of +servers+ and a list of +opts+. +servers+ may be
98
+ # omitted. See +servers=+ for acceptable server list arguments.
99
+ #
100
+ # Valid options for +opts+ are:
101
+ #
102
+ # [:namespace] Prepends this value to all keys added or retrieved.
103
+ # [:readonly] Raises an exeception on cache writes when true.
104
+ # [:multithread] Wraps cache access in a Mutex for thread safety.
105
+ #
106
+ # Other options are ignored.
107
+
108
+ def initialize(*args)
109
+ servers = []
110
+ opts = {}
111
+
112
+ case args.length
113
+ when 0 then # NOP
114
+ when 1 then
115
+ arg = args.shift
116
+ case arg
117
+ when Hash then opts = arg
118
+ when Array then servers = arg
119
+ when String then servers = [arg]
120
+ else raise ArgumentError, 'first argument must be Array, Hash or String'
121
+ end
122
+ when 2 then
123
+ servers, opts = args
124
+ else
125
+ raise ArgumentError, "wrong number of arguments (#{args.length} for 2)"
126
+ end
127
+
128
+ opts = DEFAULT_OPTIONS.merge opts
129
+ @namespace = opts[:namespace]
130
+ @readonly = opts[:readonly]
131
+ @multithread = opts[:multithread]
132
+ @mutex = Mutex.new if @multithread
133
+ @buckets = []
134
+ self.servers = servers
135
+ end
136
+
137
+ ##
138
+ # Returns a string representation of the cache object.
139
+
140
+ def inspect
141
+ "<MemCache: %d servers, %d buckets, ns: %p, ro: %p>" %
142
+ [@servers.length, @buckets.length, @namespace, @readonly]
143
+ end
144
+
145
+ ##
146
+ # Returns whether there is at least one active server for the object.
147
+
148
+ def active?
149
+ not @servers.empty?
150
+ end
151
+
152
+ ##
153
+ # Returns whether or not the cache object was created read only.
154
+
155
+ def readonly?
156
+ @readonly
157
+ end
158
+
159
+ ##
160
+ # Set the servers that the requests will be distributed between. Entries
161
+ # can be either strings of the form "hostname:port" or
162
+ # "hostname:port:weight" or MemCache::Server objects.
163
+
164
+ def servers=(servers)
165
+ # Create the server objects.
166
+ @servers = servers.collect do |server|
167
+ case server
168
+ when String
169
+ host, port, weight = server.split ':', 3
170
+ port ||= DEFAULT_PORT
171
+ weight ||= DEFAULT_WEIGHT
172
+ Server.new self, host, port, weight
173
+ when Server
174
+ if server.memcache.multithread != @multithread then
175
+ raise ArgumentError, "can't mix threaded and non-threaded servers"
176
+ end
177
+ server
178
+ else
179
+ raise TypeError, "cannot convert #{server.class} into MemCache::Server"
180
+ end
181
+ end
182
+
183
+ # Create an array of server buckets for weight selection of servers.
184
+ @buckets = []
185
+ @servers.each do |server|
186
+ server.weight.times { @buckets.push(server) }
187
+ end
188
+ end
189
+
190
+ ##
191
+ # Decrements the value for +key+ by +amount+ and returns the new value.
192
+ # +key+ must already exist. If +key+ is not an integer, it is assumed to be
193
+ # 0. +key+ can not be decremented below 0.
194
+
195
+ def decr(key, amount = 1)
196
+ raise MemCacheError, "Update of readonly cache" if @readonly
197
+ with_server(key) do |server, cache_key|
198
+ cache_decr server, cache_key, amount
199
+ end
200
+ rescue TypeError => err
201
+ handle_error server, err
202
+ end
203
+
204
+ ##
205
+ # Retrieves +key+ from memcache. If +raw+ is false, the value will be
206
+ # unmarshalled.
207
+
208
+ def get(key, raw = false)
209
+ with_server(key) do |server, cache_key|
210
+ value = cache_get server, cache_key
211
+ return nil if value.nil?
212
+ value = Marshal.load value unless raw
213
+ return value
214
+ end
215
+ rescue TypeError => err
216
+ handle_error server, err
217
+ end
218
+
219
+ ##
220
+ # Retrieves multiple values from memcached in parallel, if possible.
221
+ #
222
+ # The memcached protocol supports the ability to retrieve multiple
223
+ # keys in a single request. Pass in an array of keys to this method
224
+ # and it will:
225
+ #
226
+ # 1. map the key to the appropriate memcached server
227
+ # 2. send a single request to each server that has one or more key values
228
+ #
229
+ # Returns a hash of values.
230
+ #
231
+ # cache["a"] = 1
232
+ # cache["b"] = 2
233
+ # cache.get_multi "a", "b" # => { "a" => 1, "b" => 2 }
234
+
235
+ def get_multi(*keys)
236
+ raise MemCacheError, 'No active servers' unless active?
237
+
238
+ keys.flatten!
239
+ key_count = keys.length
240
+ cache_keys = {}
241
+ server_keys = Hash.new { |h,k| h[k] = [] }
242
+
243
+ # map keys to servers
244
+ keys.each do |key|
245
+ server, cache_key = request_setup key
246
+ cache_keys[cache_key] = key
247
+ server_keys[server] << cache_key
248
+ end
249
+
250
+ results = {}
251
+
252
+ server_keys.each do |server, keys|
253
+ keys = keys.join ' '
254
+ values = cache_get_multi server, keys
255
+ values.each do |key, value|
256
+ results[cache_keys[key]] = Marshal.load value
257
+ end
258
+ end
259
+
260
+ return results
261
+ rescue TypeError => err
262
+ handle_error server, err
263
+ end
264
+
265
+ ##
266
+ # Increments the value for +key+ by +amount+ and returns the new value.
267
+ # +key+ must already exist. If +key+ is not an integer, it is assumed to be
268
+ # 0.
269
+
270
+ def incr(key, amount = 1)
271
+ raise MemCacheError, "Update of readonly cache" if @readonly
272
+ with_server(key) do |server, cache_key|
273
+ cache_incr server, cache_key, amount
274
+ end
275
+ rescue TypeError => err
276
+ handle_error server, err
277
+ end
278
+
279
+ ##
280
+ # Add +key+ to the cache with value +value+ that expires in +expiry+
281
+ # seconds. If +raw+ is true, +value+ will not be Marshalled.
282
+ #
283
+ # Warning: Readers should not call this method in the event of a cache miss;
284
+ # see MemCache#add.
285
+
286
+ def set(key, value, expiry = 0, raw = false)
287
+ raise MemCacheError, "Update of readonly cache" if @readonly
288
+ with_server(key) do |server, cache_key|
289
+
290
+ value = Marshal.dump value unless raw
291
+ command = "set #{cache_key} 0 #{expiry} #{value.to_s.size}\r\n#{value}\r\n"
292
+
293
+ with_socket_management(server) do |socket|
294
+ socket.write command
295
+ result = socket.gets
296
+ if result.nil?
297
+ server.close
298
+ raise MemCacheError, "lost connection to #{server.host}:#{server.port}"
299
+ end
300
+
301
+ if result =~ /^SERVER_ERROR (.*)/
302
+ server.close
303
+ raise MemCacheError, $1.strip
304
+ end
305
+ end
306
+ end
307
+ end
308
+
309
+ ##
310
+ # Add +key+ to the cache with value +value+ that expires in +expiry+
311
+ # seconds, but only if +key+ does not already exist in the cache.
312
+ # If +raw+ is true, +value+ will not be Marshalled.
313
+ #
314
+ # Readers should call this method in the event of a cache miss, not
315
+ # MemCache#set or MemCache#[]=.
316
+
317
+ def add(key, value, expiry = 0, raw = false)
318
+ raise MemCacheError, "Update of readonly cache" if @readonly
319
+ with_server(key) do |server, cache_key|
320
+ value = Marshal.dump value unless raw
321
+ command = "add #{cache_key} 0 #{expiry} #{value.size}\r\n#{value}\r\n"
322
+
323
+ with_socket_management(server) do |socket|
324
+ socket.write command
325
+ socket.gets
326
+ end
327
+ end
328
+ end
329
+
330
+ ##
331
+ # Removes +key+ from the cache in +expiry+ seconds.
332
+
333
+ def delete(key, expiry = 0)
334
+ raise MemCacheError, "Update of readonly cache" if @readonly
335
+ server, cache_key = request_setup key
336
+
337
+ with_socket_management(server) do |socket|
338
+ socket.write "delete #{cache_key} #{expiry}\r\n"
339
+ socket.gets
340
+ end
341
+ end
342
+
343
+ ##
344
+ # Flush the cache from all memcache servers.
345
+
346
+ def flush_all
347
+ raise MemCacheError, 'No active servers' unless active?
348
+ raise MemCacheError, "Update of readonly cache" if @readonly
349
+ begin
350
+ @mutex.lock if @multithread
351
+ @servers.each do |server|
352
+ with_socket_management(server) do |socket|
353
+ socket.write "flush_all\r\n"
354
+ result = socket.gets
355
+ raise MemCacheError, $2.strip if result =~ /^(SERVER_)?ERROR(.*)/
356
+ end
357
+ end
358
+ ensure
359
+ @mutex.unlock if @multithread
360
+ end
361
+ end
362
+
363
+ ##
364
+ # Reset the connection to all memcache servers. This should be called if
365
+ # there is a problem with a cache lookup that might have left the connection
366
+ # in a corrupted state.
367
+
368
+ def reset
369
+ @servers.each { |server| server.close }
370
+ end
371
+
372
+ ##
373
+ # Returns statistics for each memcached server. An explanation of the
374
+ # statistics can be found in the memcached docs:
375
+ #
376
+ # http://code.sixapart.com/svn/memcached/trunk/server/doc/protocol.txt
377
+ #
378
+ # Example:
379
+ #
380
+ # >> pp CACHE.stats
381
+ # {"localhost:11211"=>
382
+ # {"bytes"=>4718,
383
+ # "pid"=>20188,
384
+ # "connection_structures"=>4,
385
+ # "time"=>1162278121,
386
+ # "pointer_size"=>32,
387
+ # "limit_maxbytes"=>67108864,
388
+ # "cmd_get"=>14532,
389
+ # "version"=>"1.2.0",
390
+ # "bytes_written"=>432583,
391
+ # "cmd_set"=>32,
392
+ # "get_misses"=>0,
393
+ # "total_connections"=>19,
394
+ # "curr_connections"=>3,
395
+ # "curr_items"=>4,
396
+ # "uptime"=>1557,
397
+ # "get_hits"=>14532,
398
+ # "total_items"=>32,
399
+ # "rusage_system"=>0.313952,
400
+ # "rusage_user"=>0.119981,
401
+ # "bytes_read"=>190619}}
402
+ # => nil
403
+
404
+ def stats
405
+ raise MemCacheError, "No active servers" unless active?
406
+ server_stats = {}
407
+
408
+ @servers.each do |server|
409
+ next unless server.alive?
410
+ with_socket_management(server) do |socket|
411
+ value = nil # TODO: why is this line here?
412
+ socket.write "stats\r\n"
413
+ stats = {}
414
+ while line = socket.gets do
415
+ break if line == "END\r\n"
416
+ if line =~ /^STAT ([\w]+) ([\w\.\:]+)/ then
417
+ name, value = $1, $2
418
+ stats[name] = case name
419
+ when 'version'
420
+ value
421
+ when 'rusage_user', 'rusage_system' then
422
+ seconds, microseconds = value.split(/:/, 2)
423
+ microseconds ||= 0
424
+ Float(seconds) + (Float(microseconds) / 1_000_000)
425
+ else
426
+ if value =~ /^\d+$/ then
427
+ value.to_i
428
+ else
429
+ value
430
+ end
431
+ end
432
+ end
433
+ end
434
+ server_stats["#{server.host}:#{server.port}"] = stats
435
+ end
436
+ end
437
+
438
+ server_stats
439
+ end
440
+
441
+ ##
442
+ # Shortcut to get a value from the cache.
443
+
444
+ alias [] get
445
+
446
+ ##
447
+ # Shortcut to save a value in the cache. This method does not set an
448
+ # expiration on the entry. Use set to specify an explicit expiry.
449
+
450
+ def []=(key, value)
451
+ set key, value
452
+ end
453
+
454
+ protected unless $TESTING
455
+
456
+ ##
457
+ # Create a key for the cache, incorporating the namespace qualifier if
458
+ # requested.
459
+
460
+ def make_cache_key(key)
461
+ if namespace.nil? then
462
+ key
463
+ else
464
+ "#{@namespace}:#{key}"
465
+ end
466
+ end
467
+
468
+ ##
469
+ # Pick a server to handle the request based on a hash of the key.
470
+
471
+ def get_server_for_key(key)
472
+ raise ArgumentError, "illegal character in key #{key.inspect}" if
473
+ key =~ /\s/
474
+ raise ArgumentError, "key too long #{key.inspect}" if key.length > 250
475
+ raise MemCacheError, "No servers available" if @servers.empty?
476
+ return @servers.first if @servers.length == 1
477
+
478
+ hkey = hash_for key
479
+
480
+ 20.times do |try|
481
+ server = @buckets[hkey % @buckets.nitems]
482
+ return server if server.alive?
483
+ hkey += hash_for "#{try}#{key}"
484
+ end
485
+
486
+ raise MemCacheError, "No servers available"
487
+ end
488
+
489
+ ##
490
+ # Returns an interoperable hash value for +key+. (I think, docs are
491
+ # sketchy for down servers).
492
+
493
+ def hash_for(key)
494
+ (key.crc32_ITU_T >> 16) & 0x7fff
495
+ end
496
+
497
+ ##
498
+ # Performs a raw decr for +cache_key+ from +server+. Returns nil if not
499
+ # found.
500
+
501
+ def cache_decr(server, cache_key, amount)
502
+ with_socket_management(server) do |socket|
503
+ socket.write "decr #{cache_key} #{amount}\r\n"
504
+ text = socket.gets
505
+ return nil if text == "NOT_FOUND\r\n"
506
+ return text.to_i
507
+ end
508
+ end
509
+
510
+ ##
511
+ # Fetches the raw data for +cache_key+ from +server+. Returns nil on cache
512
+ # miss.
513
+
514
+ def cache_get(server, cache_key)
515
+ with_socket_management(server) do |socket|
516
+ socket.write "get #{cache_key}\r\n"
517
+ keyline = socket.gets # "VALUE <key> <flags> <bytes>\r\n"
518
+
519
+ if keyline.nil? then
520
+ server.close
521
+ raise MemCacheError, "lost connection to #{server.host}:#{server.port}" # TODO: retry here too
522
+ end
523
+
524
+ return nil if keyline == "END\r\n"
525
+
526
+ unless keyline =~ /(\d+)\r/ then
527
+ server.close
528
+ raise MemCacheError, "unexpected response #{keyline.inspect}"
529
+ end
530
+ value = socket.read $1.to_i
531
+ socket.read 2 # "\r\n"
532
+ socket.gets # "END\r\n"
533
+ return value
534
+ end
535
+ end
536
+
537
+ ##
538
+ # Fetches +cache_keys+ from +server+ using a multi-get.
539
+
540
+ def cache_get_multi(server, cache_keys)
541
+ with_socket_management(server) do |socket|
542
+ values = {}
543
+ socket.write "get #{cache_keys}\r\n"
544
+
545
+ while keyline = socket.gets do
546
+ return values if keyline == "END\r\n"
547
+
548
+ unless keyline =~ /^VALUE (.+) (.+) (.+)/ then
549
+ server.close
550
+ raise MemCacheError, "unexpected response #{keyline.inspect}"
551
+ end
552
+
553
+ key, data_length = $1, $3
554
+ values[$1] = socket.read data_length.to_i
555
+ socket.read(2) # "\r\n"
556
+ end
557
+
558
+ server.close
559
+ raise MemCacheError, "lost connection to #{server.host}:#{server.port}" # TODO: retry here too
560
+ end
561
+ end
562
+
563
+ ##
564
+ # Performs a raw incr for +cache_key+ from +server+. Returns nil if not
565
+ # found.
566
+
567
+ def cache_incr(server, cache_key, amount)
568
+ with_socket_management(server) do |socket|
569
+ socket.write "incr #{cache_key} #{amount}\r\n"
570
+ text = socket.gets
571
+ return nil if text == "NOT_FOUND\r\n"
572
+ return text.to_i
573
+ end
574
+ end
575
+
576
+ ##
577
+ # Gets or creates a socket connected to the given server, and yields it
578
+ # to the block. If a socket error (SocketError, SystemCallError, IOError)
579
+ # or protocol error (MemCacheError) is raised by the block, closes the
580
+ # socket, attempts to connect again, and retries the block (once). If
581
+ # an error is again raised, reraises it as MemCacheError.
582
+ # If unable to connect to the server (or if in the reconnect wait period),
583
+ # raises MemCacheError - note that the socket connect code marks a server
584
+ # dead for a timeout period, so retrying does not apply to connection attempt
585
+ # failures (but does still apply to unexpectedly lost connections etc.).
586
+ # Wraps the whole lot in mutex synchronization if @multithread is true.
587
+
588
+ def with_socket_management(server, &block)
589
+ @mutex.lock if @multithread
590
+ retried = false
591
+ begin
592
+ socket = server.socket
593
+ # Raise an IndexError to show this server is out of whack.
594
+ # We'll catch it in higher-level code and attempt to restart the operation.
595
+ raise IndexError, "No connection to server (#{server.status})" if socket.nil?
596
+ block.call(socket)
597
+ rescue MemCacheError, SocketError, SystemCallError, IOError => err
598
+ handle_error(server, err) if retried || socket.nil?
599
+ retried = true
600
+ retry
601
+ end
602
+ ensure
603
+ @mutex.unlock if @multithread
604
+ end
605
+
606
+ def with_server(key)
607
+ retried = false
608
+ begin
609
+ server, cache_key = request_setup(key)
610
+ yield server, cache_key
611
+ rescue IndexError => e
612
+ if !retried && @servers.size > 1
613
+ puts "Connection to server #{server.inspect} DIED! Retrying operation..."
614
+ retried = true
615
+ retry
616
+ end
617
+ handle_error(nil, e)
618
+ end
619
+ end
620
+
621
+ ##
622
+ # Handles +error+ from +server+.
623
+
624
+ def handle_error(server, error)
625
+ raise error if error.is_a?(MemCacheError)
626
+ server.close if server
627
+ new_error = MemCacheError.new error.message
628
+ new_error.set_backtrace error.backtrace
629
+ raise new_error
630
+ end
631
+
632
+ ##
633
+ # Performs setup for making a request with +key+ from memcached. Returns
634
+ # the server to fetch the key from and the complete key to use.
635
+
636
+ def request_setup(key)
637
+ raise MemCacheError, 'No active servers' unless active?
638
+ cache_key = make_cache_key key
639
+ server = get_server_for_key cache_key
640
+ return server, cache_key
641
+ end
642
+
643
+ ##
644
+ # This class represents a memcached server instance.
645
+
646
+ class Server
647
+
648
+ ##
649
+ # The amount of time to wait to establish a connection with a memcached
650
+ # server. If a connection cannot be established within this time limit,
651
+ # the server will be marked as down.
652
+
653
+ CONNECT_TIMEOUT = 0.25
654
+
655
+ ##
656
+ # The amount of time to wait before attempting to re-establish a
657
+ # connection with a server that is marked dead.
658
+
659
+ RETRY_DELAY = 30.0
660
+
661
+ ##
662
+ # The host the memcached server is running on.
663
+
664
+ attr_reader :host
665
+
666
+ ##
667
+ # The port the memcached server is listening on.
668
+
669
+ attr_reader :port
670
+
671
+ ##
672
+ # The weight given to the server.
673
+
674
+ attr_reader :weight
675
+
676
+ ##
677
+ # The time of next retry if the connection is dead.
678
+
679
+ attr_reader :retry
680
+
681
+ ##
682
+ # A text status string describing the state of the server.
683
+
684
+ attr_reader :status
685
+
686
+ ##
687
+ # Create a new MemCache::Server object for the memcached instance
688
+ # listening on the given host and port, weighted by the given weight.
689
+
690
+ def initialize(memcache, host, port = DEFAULT_PORT, weight = DEFAULT_WEIGHT)
691
+ raise ArgumentError, "No host specified" if host.nil? or host.empty?
692
+ raise ArgumentError, "No port specified" if port.nil? or port.to_i.zero?
693
+
694
+ @memcache = memcache
695
+ @host = host
696
+ @port = port.to_i
697
+ @weight = weight.to_i
698
+
699
+ @multithread = @memcache.multithread
700
+ @mutex = Mutex.new
701
+
702
+ @sock = nil
703
+ @retry = nil
704
+ @status = 'NOT CONNECTED'
705
+ end
706
+
707
+ ##
708
+ # Return a string representation of the server object.
709
+
710
+ def inspect
711
+ "<MemCache::Server: %s:%d [%d] (%s)>" % [@host, @port, @weight, @status]
712
+ end
713
+
714
+ ##
715
+ # Check whether the server connection is alive. This will cause the
716
+ # socket to attempt to connect if it isn't already connected and or if
717
+ # the server was previously marked as down and the retry time has
718
+ # been exceeded.
719
+
720
+ def alive?
721
+ !!socket
722
+ end
723
+
724
+ ##
725
+ # Try to connect to the memcached server targeted by this object.
726
+ # Returns the connected socket object on success or nil on failure.
727
+
728
+ def socket
729
+ @mutex.lock if @multithread
730
+ return @sock if @sock and not @sock.closed?
731
+
732
+ @sock = nil
733
+
734
+ # If the host was dead, don't retry for a while.
735
+ return if @retry and @retry > Time.now
736
+
737
+ # Attempt to connect if not already connected.
738
+ begin
739
+ @sock = timeout CONNECT_TIMEOUT do
740
+ TCPSocket.new @host, @port
741
+ end
742
+ if Socket.constants.include? 'TCP_NODELAY' then
743
+ @sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
744
+ end
745
+ @retry = nil
746
+ @status = 'CONNECTED'
747
+ rescue SocketError, SystemCallError, IOError, Timeout::Error => err
748
+ mark_dead err.message
749
+ end
750
+
751
+ return @sock
752
+ ensure
753
+ @mutex.unlock if @multithread
754
+ end
755
+
756
+ ##
757
+ # Close the connection to the memcached server targeted by this
758
+ # object. The server is not considered dead.
759
+
760
+ def close
761
+ @mutex.lock if @multithread
762
+ @sock.close if @sock && !@sock.closed?
763
+ @sock = nil
764
+ @retry = nil
765
+ @status = "NOT CONNECTED"
766
+ ensure
767
+ @mutex.unlock if @multithread
768
+ end
769
+
770
+ private
771
+
772
+ ##
773
+ # Mark the server as dead and close its socket.
774
+
775
+ def mark_dead(reason = "Unknown error")
776
+ @sock.close if @sock && !@sock.closed?
777
+ @sock = nil
778
+ @retry = Time.now + RETRY_DELAY
779
+
780
+ @status = sprintf "DEAD: %s, will retry at %s", reason, @retry
781
+ end
782
+
783
+ end
784
+
785
+ ##
786
+ # Base MemCache exception class.
787
+
788
+ class MemCacheError < RuntimeError; end
789
+
790
+ end
791
+