fiveruns-memcache-client 1.5.0.1

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,73 @@
1
+ = 1.5.0.1 (FiveRuns fork)
2
+
3
+ * Fix set not handling client disconnects.
4
+ http://dev.twitter.com/2008/02/solving-case-of-missing-updates.html
5
+
6
+ = 1.5.0
7
+
8
+ * Add MemCache#flush_all command. Patch #13019 and bug #10503. Patches
9
+ submitted by Sebastian Delmont and Rick Olson.
10
+ * Type-cast data returned by MemCache#stats. Patch #10505 submitted by
11
+ Sebastian Delmont.
12
+
13
+ = 1.4.0
14
+
15
+ * Fix bug #10371, #set does not check response for server errors.
16
+ Submitted by Ben VandenBos.
17
+ * Fix bug #12450, set TCP_NODELAY socket option. Patch by Chris
18
+ McGrath.
19
+ * Fix bug #10704, missing #add method. Patch by Jamie Macey.
20
+ * Fix bug #10371, handle socket EOF in cache_get. Submitted by Ben
21
+ VandenBos.
22
+
23
+ = 1.3.0
24
+
25
+ * Apply patch #6507, add stats command. Submitted by Tyler Kovacs.
26
+ * Apply patch #6509, parallel implementation of #get_multi. Submitted
27
+ by Tyler Kovacs.
28
+ * Validate keys. Disallow spaces in keys or keys that are too long.
29
+ * Perform more validation of server responses. MemCache now reports
30
+ errors if the socket was not in an expected state. (Please file
31
+ bugs if you find some.)
32
+ * Add #incr and #decr.
33
+ * Add raw argument to #set and #get to retrieve #incr and #decr
34
+ values.
35
+ * Also put on MemCacheError when using Cache::get with block.
36
+ * memcache.rb no longer sets $TESTING to a true value if it was
37
+ previously defined. Bug #8213 by Matijs van Zuijlen.
38
+
39
+ = 1.2.1
40
+
41
+ * Fix bug #7048, MemCache#servers= referenced changed local variable.
42
+ Submitted by Justin Dossey.
43
+ * Fix bug #7049, MemCache#initialize resets @buckets. Submitted by
44
+ Justin Dossey.
45
+ * Fix bug #6232, Make Cache::Get work with a block only when nil is
46
+ returned. Submitted by Jon Evans.
47
+ * Moved to the seattlerb project.
48
+
49
+ = 1.2.0
50
+
51
+ NOTE: This version will store keys in different places than previous
52
+ versions! Be prepared for some thrashing while memcached sorts itself
53
+ out!
54
+
55
+ * Fixed multithreaded operations, bug 5994 and 5989.
56
+ Thanks to Blaine Cook, Erik Hetzner, Elliot Smith, Dave Myron (and
57
+ possibly others I have forgotten).
58
+ * Made memcached interoperable with other memcached libraries, bug
59
+ 4509. Thanks to anonymous.
60
+ * Added get_multi to match Perl/etc APIs
61
+
62
+ = 1.1.0
63
+
64
+ * Added some tests
65
+ * Sped up non-multithreaded and multithreaded operation
66
+ * More Ruby-memcache compatibility
67
+ * More RDoc
68
+ * Switched to Hoe
69
+
70
+ = 1.0.0
71
+
72
+ Birthday!
73
+
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,813 @@
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
+ # Deceremets 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
+ server, cache_key = request_setup key
189
+
190
+ if @multithread then
191
+ threadsafe_cache_decr server, cache_key, amount
192
+ else
193
+ cache_decr server, cache_key, amount
194
+ end
195
+ rescue TypeError, SocketError, SystemCallError, IOError => err
196
+ handle_error server, err
197
+ end
198
+
199
+ ##
200
+ # Retrieves +key+ from memcache. If +raw+ is false, the value will be
201
+ # unmarshalled.
202
+
203
+ def get(key, raw = false)
204
+ server, cache_key = request_setup key
205
+
206
+ value = if @multithread then
207
+ threadsafe_cache_get server, cache_key
208
+ else
209
+ cache_get server, cache_key
210
+ end
211
+
212
+ return nil if value.nil?
213
+
214
+ value = Marshal.load value unless raw
215
+
216
+ return value
217
+ rescue TypeError, SocketError, SystemCallError, IOError => err
218
+ handle_error server, err
219
+ end
220
+
221
+ ##
222
+ # Retrieves multiple values from memcached in parallel, if possible.
223
+ #
224
+ # The memcached protocol supports the ability to retrieve multiple
225
+ # keys in a single request. Pass in an array of keys to this method
226
+ # and it will:
227
+ #
228
+ # 1. map the key to the appropriate memcached server
229
+ # 2. send a single request to each server that has one or more key values
230
+ #
231
+ # Returns a hash of values.
232
+ #
233
+ # cache["a"] = 1
234
+ # cache["b"] = 2
235
+ # cache.get_multi "a", "b" # => { "a" => 1, "b" => 2 }
236
+
237
+ def get_multi(*keys)
238
+ raise MemCacheError, 'No active servers' unless active?
239
+
240
+ keys.flatten!
241
+ key_count = keys.length
242
+ cache_keys = {}
243
+ server_keys = Hash.new { |h,k| h[k] = [] }
244
+
245
+ # map keys to servers
246
+ keys.each do |key|
247
+ server, cache_key = request_setup key
248
+ cache_keys[cache_key] = key
249
+ server_keys[server] << cache_key
250
+ end
251
+
252
+ results = {}
253
+
254
+ server_keys.each do |server, keys|
255
+ keys = keys.join ' '
256
+ values = if @multithread then
257
+ threadsafe_cache_get_multi server, keys
258
+ else
259
+ cache_get_multi server, keys
260
+ end
261
+ values.each do |key, value|
262
+ results[cache_keys[key]] = Marshal.load value
263
+ end
264
+ end
265
+
266
+ return results
267
+ rescue TypeError, SocketError, SystemCallError, IOError => err
268
+ handle_error server, err
269
+ end
270
+
271
+ ##
272
+ # Increments the value for +key+ by +amount+ and retruns the new value.
273
+ # +key+ must already exist. If +key+ is not an integer, it is assumed to be
274
+ # 0.
275
+
276
+ def incr(key, amount = 1)
277
+ server, cache_key = request_setup key
278
+
279
+ if @multithread then
280
+ threadsafe_cache_incr server, cache_key, amount
281
+ else
282
+ cache_incr server, cache_key, amount
283
+ end
284
+ rescue TypeError, SocketError, SystemCallError, IOError => err
285
+ handle_error server, err
286
+ end
287
+
288
+ ##
289
+ # Add +key+ to the cache with value +value+ that expires in +expiry+
290
+ # seconds. If +raw+ is true, +value+ will not be Marshalled.
291
+ #
292
+ # Warning: Readers should not call this method in the event of a cache miss;
293
+ # see MemCache#add.
294
+
295
+ def set(key, value, expiry = 0, raw = false)
296
+ raise MemCacheError, "Update of readonly cache" if @readonly
297
+ server, cache_key = request_setup key
298
+ socket = server.socket
299
+
300
+ value = Marshal.dump value unless raw
301
+ command = "set #{cache_key} 0 #{expiry} #{value.size}\r\n#{value}\r\n"
302
+
303
+ begin
304
+ @mutex.lock if @multithread
305
+ socket.write command
306
+ result = socket.gets
307
+ if result.nil?
308
+ server.close
309
+ raise MemCacheError, "lost connection to #{server.host}:#{server.port}"
310
+ end
311
+
312
+ if result =~ /^SERVER_ERROR (.*)/
313
+ server.close
314
+ raise MemCacheError, $1.strip
315
+ end
316
+ rescue SocketError, SystemCallError, IOError => err
317
+ server.close
318
+ raise MemCacheError, err.message
319
+ ensure
320
+ @mutex.unlock if @multithread
321
+ end
322
+ end
323
+
324
+ ##
325
+ # Add +key+ to the cache with value +value+ that expires in +expiry+
326
+ # seconds, but only if +key+ does not already exist in the cache.
327
+ # If +raw+ is true, +value+ will not be Marshalled.
328
+ #
329
+ # Readers should call this method in the event of a cache miss, not
330
+ # MemCache#set or MemCache#[]=.
331
+
332
+ def add(key, value, expiry = 0, raw = false)
333
+ raise MemCacheError, "Update of readonly cache" if @readonly
334
+ server, cache_key = request_setup key
335
+ socket = server.socket
336
+
337
+ value = Marshal.dump value unless raw
338
+ command = "add #{cache_key} 0 #{expiry} #{value.size}\r\n#{value}\r\n"
339
+
340
+ begin
341
+ @mutex.lock if @multithread
342
+ socket.write command
343
+ socket.gets
344
+ rescue SocketError, SystemCallError, IOError => err
345
+ server.close
346
+ raise MemCacheError, err.message
347
+ ensure
348
+ @mutex.unlock if @multithread
349
+ end
350
+ end
351
+
352
+ ##
353
+ # Removes +key+ from the cache in +expiry+ seconds.
354
+
355
+ def delete(key, expiry = 0)
356
+ @mutex.lock if @multithread
357
+
358
+ raise MemCacheError, "No active servers" unless active?
359
+ cache_key = make_cache_key key
360
+ server = get_server_for_key cache_key
361
+
362
+ sock = server.socket
363
+ raise MemCacheError, "No connection to server" if sock.nil?
364
+
365
+ begin
366
+ sock.write "delete #{cache_key} #{expiry}\r\n"
367
+ sock.gets
368
+ rescue SocketError, SystemCallError, IOError => err
369
+ server.close
370
+ raise MemCacheError, err.message
371
+ end
372
+ ensure
373
+ @mutex.unlock if @multithread
374
+ end
375
+
376
+ ##
377
+ # Flush the cache from all memcache servers.
378
+
379
+ def flush_all
380
+ raise MemCacheError, 'No active servers' unless active?
381
+ raise MemCacheError, "Update of readonly cache" if @readonly
382
+ begin
383
+ @mutex.lock if @multithread
384
+ @servers.each do |server|
385
+ begin
386
+ sock = server.socket
387
+ raise MemCacheError, "No connection to server" if sock.nil?
388
+ sock.write "flush_all\r\n"
389
+ result = sock.gets
390
+ raise MemCacheError, $2.strip if result =~ /^(SERVER_)?ERROR(.*)/
391
+ rescue SocketError, SystemCallError, IOError => err
392
+ server.close
393
+ raise MemCacheError, err.message
394
+ end
395
+ end
396
+ ensure
397
+ @mutex.unlock if @multithread
398
+ end
399
+ end
400
+
401
+ ##
402
+ # Reset the connection to all memcache servers. This should be called if
403
+ # there is a problem with a cache lookup that might have left the connection
404
+ # in a corrupted state.
405
+
406
+ def reset
407
+ @servers.each { |server| server.close }
408
+ end
409
+
410
+ ##
411
+ # Returns statistics for each memcached server. An explanation of the
412
+ # statistics can be found in the memcached docs:
413
+ #
414
+ # http://code.sixapart.com/svn/memcached/trunk/server/doc/protocol.txt
415
+ #
416
+ # Example:
417
+ #
418
+ # >> pp CACHE.stats
419
+ # {"localhost:11211"=>
420
+ # {"bytes"=>4718,
421
+ # "pid"=>20188,
422
+ # "connection_structures"=>4,
423
+ # "time"=>1162278121,
424
+ # "pointer_size"=>32,
425
+ # "limit_maxbytes"=>67108864,
426
+ # "cmd_get"=>14532,
427
+ # "version"=>"1.2.0",
428
+ # "bytes_written"=>432583,
429
+ # "cmd_set"=>32,
430
+ # "get_misses"=>0,
431
+ # "total_connections"=>19,
432
+ # "curr_connections"=>3,
433
+ # "curr_items"=>4,
434
+ # "uptime"=>1557,
435
+ # "get_hits"=>14532,
436
+ # "total_items"=>32,
437
+ # "rusage_system"=>0.313952,
438
+ # "rusage_user"=>0.119981,
439
+ # "bytes_read"=>190619}}
440
+ # => nil
441
+
442
+ def stats
443
+ raise MemCacheError, "No active servers" unless active?
444
+ server_stats = {}
445
+
446
+ @servers.each do |server|
447
+ sock = server.socket
448
+ raise MemCacheError, "No connection to server" if sock.nil?
449
+
450
+ value = nil
451
+ begin
452
+ sock.write "stats\r\n"
453
+ stats = {}
454
+ while line = sock.gets do
455
+ break if line == "END\r\n"
456
+ if line =~ /^STAT ([\w]+) ([\w\.\:]+)/ then
457
+ name, value = $1, $2
458
+ stats[name] = case name
459
+ when 'version'
460
+ value
461
+ when 'rusage_user', 'rusage_system' then
462
+ seconds, microseconds = value.split(/:/, 2)
463
+ microseconds ||= 0
464
+ Float(seconds) + (Float(microseconds) / 1_000_000)
465
+ else
466
+ if value =~ /^\d+$/ then
467
+ value.to_i
468
+ else
469
+ value
470
+ end
471
+ end
472
+ end
473
+ end
474
+ server_stats["#{server.host}:#{server.port}"] = stats
475
+ rescue SocketError, SystemCallError, IOError => err
476
+ server.close
477
+ raise MemCacheError, err.message
478
+ end
479
+ end
480
+
481
+ server_stats
482
+ end
483
+
484
+ ##
485
+ # Shortcut to get a value from the cache.
486
+
487
+ alias [] get
488
+
489
+ ##
490
+ # Shortcut to save a value in the cache. This method does not set an
491
+ # expiration on the entry. Use set to specify an explicit expiry.
492
+
493
+ def []=(key, value)
494
+ set key, value
495
+ end
496
+
497
+ protected unless $TESTING
498
+
499
+ ##
500
+ # Create a key for the cache, incorporating the namespace qualifier if
501
+ # requested.
502
+
503
+ def make_cache_key(key)
504
+ if namespace.nil? then
505
+ key
506
+ else
507
+ "#{@namespace}:#{key}"
508
+ end
509
+ end
510
+
511
+ ##
512
+ # Pick a server to handle the request based on a hash of the key.
513
+
514
+ def get_server_for_key(key)
515
+ raise ArgumentError, "illegal character in key #{key.inspect}" if
516
+ key =~ /\s/
517
+ raise ArgumentError, "key too long #{key.inspect}" if key.length > 250
518
+ raise MemCacheError, "No servers available" if @servers.empty?
519
+ return @servers.first if @servers.length == 1
520
+
521
+ hkey = hash_for key
522
+
523
+ 20.times do |try|
524
+ server = @buckets[hkey % @buckets.nitems]
525
+ return server if server.alive?
526
+ hkey += hash_for "#{try}#{key}"
527
+ end
528
+
529
+ raise MemCacheError, "No servers available"
530
+ end
531
+
532
+ ##
533
+ # Returns an interoperable hash value for +key+. (I think, docs are
534
+ # sketchy for down servers).
535
+
536
+ def hash_for(key)
537
+ (key.crc32_ITU_T >> 16) & 0x7fff
538
+ end
539
+
540
+ ##
541
+ # Performs a raw decr for +cache_key+ from +server+. Returns nil if not
542
+ # found.
543
+
544
+ def cache_decr(server, cache_key, amount)
545
+ socket = server.socket
546
+ socket.write "decr #{cache_key} #{amount}\r\n"
547
+ text = socket.gets
548
+ return nil if text == "NOT_FOUND\r\n"
549
+ return text.to_i
550
+ end
551
+
552
+ ##
553
+ # Fetches the raw data for +cache_key+ from +server+. Returns nil on cache
554
+ # miss.
555
+
556
+ def cache_get(server, cache_key)
557
+ socket = server.socket
558
+ socket.write "get #{cache_key}\r\n"
559
+ keyline = socket.gets # "VALUE <key> <flags> <bytes>\r\n"
560
+
561
+ if keyline.nil? then
562
+ server.close
563
+ raise MemCacheError, "lost connection to #{server.host}:#{server.port}"
564
+ end
565
+
566
+ return nil if keyline == "END\r\n"
567
+
568
+ unless keyline =~ /(\d+)\r/ then
569
+ server.close
570
+ raise MemCacheError, "unexpected response #{keyline.inspect}"
571
+ end
572
+ value = socket.read $1.to_i
573
+ socket.read 2 # "\r\n"
574
+ socket.gets # "END\r\n"
575
+ return value
576
+ end
577
+
578
+ ##
579
+ # Fetches +cache_keys+ from +server+ using a multi-get.
580
+
581
+ def cache_get_multi(server, cache_keys)
582
+ values = {}
583
+ socket = server.socket
584
+ socket.write "get #{cache_keys}\r\n"
585
+
586
+ while keyline = socket.gets do
587
+ return values if keyline == "END\r\n"
588
+
589
+ unless keyline =~ /^VALUE (.+) (.+) (.+)/ then
590
+ server.close
591
+ raise MemCacheError, "unexpected response #{keyline.inspect}"
592
+ end
593
+
594
+ key, data_length = $1, $3
595
+ values[$1] = socket.read data_length.to_i
596
+ socket.read(2) # "\r\n"
597
+ end
598
+
599
+ server.close
600
+ raise MemCacheError, "lost connection to #{server.host}:#{server.port}"
601
+ end
602
+
603
+ ##
604
+ # Performs a raw incr for +cache_key+ from +server+. Returns nil if not
605
+ # found.
606
+
607
+ def cache_incr(server, cache_key, amount)
608
+ socket = server.socket
609
+ socket.write "incr #{cache_key} #{amount}\r\n"
610
+ text = socket.gets
611
+ return nil if text == "NOT_FOUND\r\n"
612
+ return text.to_i
613
+ end
614
+
615
+ ##
616
+ # Handles +error+ from +server+.
617
+
618
+ def handle_error(server, error)
619
+ server.close if server
620
+ new_error = MemCacheError.new error.message
621
+ new_error.set_backtrace error.backtrace
622
+ raise new_error
623
+ end
624
+
625
+ ##
626
+ # Performs setup for making a request with +key+ from memcached. Returns
627
+ # the server to fetch the key from and the complete key to use.
628
+
629
+ def request_setup(key)
630
+ raise MemCacheError, 'No active servers' unless active?
631
+ cache_key = make_cache_key key
632
+ server = get_server_for_key cache_key
633
+ raise MemCacheError, 'No connection to server' if server.socket.nil?
634
+ return server, cache_key
635
+ end
636
+
637
+ def threadsafe_cache_decr(server, cache_key, amount) # :nodoc:
638
+ @mutex.lock
639
+ cache_decr server, cache_key, amount
640
+ ensure
641
+ @mutex.unlock
642
+ end
643
+
644
+ def threadsafe_cache_get(server, cache_key) # :nodoc:
645
+ @mutex.lock
646
+ cache_get server, cache_key
647
+ ensure
648
+ @mutex.unlock
649
+ end
650
+
651
+ def threadsafe_cache_get_multi(socket, cache_keys) # :nodoc:
652
+ @mutex.lock
653
+ cache_get_multi socket, cache_keys
654
+ ensure
655
+ @mutex.unlock
656
+ end
657
+
658
+ def threadsafe_cache_incr(server, cache_key, amount) # :nodoc:
659
+ @mutex.lock
660
+ cache_incr server, cache_key, amount
661
+ ensure
662
+ @mutex.unlock
663
+ end
664
+
665
+ ##
666
+ # This class represents a memcached server instance.
667
+
668
+ class Server
669
+
670
+ ##
671
+ # The amount of time to wait to establish a connection with a memcached
672
+ # server. If a connection cannot be established within this time limit,
673
+ # the server will be marked as down.
674
+
675
+ CONNECT_TIMEOUT = 0.25
676
+
677
+ ##
678
+ # The amount of time to wait before attempting to re-establish a
679
+ # connection with a server that is marked dead.
680
+
681
+ RETRY_DELAY = 30.0
682
+
683
+ ##
684
+ # The host the memcached server is running on.
685
+
686
+ attr_reader :host
687
+
688
+ ##
689
+ # The port the memcached server is listening on.
690
+
691
+ attr_reader :port
692
+
693
+ ##
694
+ # The weight given to the server.
695
+
696
+ attr_reader :weight
697
+
698
+ ##
699
+ # The time of next retry if the connection is dead.
700
+
701
+ attr_reader :retry
702
+
703
+ ##
704
+ # A text status string describing the state of the server.
705
+
706
+ attr_reader :status
707
+
708
+ ##
709
+ # Create a new MemCache::Server object for the memcached instance
710
+ # listening on the given host and port, weighted by the given weight.
711
+
712
+ def initialize(memcache, host, port = DEFAULT_PORT, weight = DEFAULT_WEIGHT)
713
+ raise ArgumentError, "No host specified" if host.nil? or host.empty?
714
+ raise ArgumentError, "No port specified" if port.nil? or port.to_i.zero?
715
+
716
+ @memcache = memcache
717
+ @host = host
718
+ @port = port.to_i
719
+ @weight = weight.to_i
720
+
721
+ @multithread = @memcache.multithread
722
+ @mutex = Mutex.new
723
+
724
+ @sock = nil
725
+ @retry = nil
726
+ @status = 'NOT CONNECTED'
727
+ end
728
+
729
+ ##
730
+ # Return a string representation of the server object.
731
+
732
+ def inspect
733
+ "<MemCache::Server: %s:%d [%d] (%s)>" % [@host, @port, @weight, @status]
734
+ end
735
+
736
+ ##
737
+ # Check whether the server connection is alive. This will cause the
738
+ # socket to attempt to connect if it isn't already connected and or if
739
+ # the server was previously marked as down and the retry time has
740
+ # been exceeded.
741
+
742
+ def alive?
743
+ !!socket
744
+ end
745
+
746
+ ##
747
+ # Try to connect to the memcached server targeted by this object.
748
+ # Returns the connected socket object on success or nil on failure.
749
+
750
+ def socket
751
+ @mutex.lock if @multithread
752
+ return @sock if @sock and not @sock.closed?
753
+
754
+ @sock = nil
755
+
756
+ # If the host was dead, don't retry for a while.
757
+ return if @retry and @retry > Time.now
758
+
759
+ # Attempt to connect if not already connected.
760
+ begin
761
+ @sock = timeout CONNECT_TIMEOUT do
762
+ TCPSocket.new @host, @port
763
+ end
764
+ if Socket.constants.include? 'TCP_NODELAY' then
765
+ @sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
766
+ end
767
+ @retry = nil
768
+ @status = 'CONNECTED'
769
+ rescue SocketError, SystemCallError, IOError, Timeout::Error => err
770
+ mark_dead err.message
771
+ end
772
+
773
+ return @sock
774
+ ensure
775
+ @mutex.unlock if @multithread
776
+ end
777
+
778
+ ##
779
+ # Close the connection to the memcached server targeted by this
780
+ # object. The server is not considered dead.
781
+
782
+ def close
783
+ @mutex.lock if @multithread
784
+ @sock.close if @sock && !@sock.closed?
785
+ @sock = nil
786
+ @retry = nil
787
+ @status = "NOT CONNECTED"
788
+ ensure
789
+ @mutex.unlock if @multithread
790
+ end
791
+
792
+ private
793
+
794
+ ##
795
+ # Mark the server as dead and close its socket.
796
+
797
+ def mark_dead(reason = "Unknown error")
798
+ @sock.close if @sock && !@sock.closed?
799
+ @sock = nil
800
+ @retry = Time.now + RETRY_DELAY
801
+
802
+ @status = sprintf "DEAD: %s, will retry at %s", reason, @retry
803
+ end
804
+
805
+ end
806
+
807
+ ##
808
+ # Base MemCache exception class.
809
+
810
+ class MemCacheError < RuntimeError; end
811
+
812
+ end
813
+