fiveruns-fiveruns-memcache-client 1.5.0.2

Sign up to get free protection for your applications and to get access to all the features.
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
+