fiveruns-memcache-client 1.5.0.1

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