fiveruns-fiveruns-memcache-client 1.5.0.2

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