brendanlim-memcache-client 1.5.0.6

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,109 @@
1
+ = Unreleased
2
+
3
+ * Implement a consistent hashing algorithm, as described in libketama.
4
+ This dramatically reduces the cost of adding or removing servers dynamically
5
+ as keys are much more likely to map to the same server.
6
+
7
+ Take a scenario where we add a fourth server. With a dumb modulo algorithm, about
8
+ 25% of the keys will map to the same server. In other words, 75% of your memcached
9
+ content suddenly becomes invalid. With a consistent algorithm, 75% of the keys
10
+ will map to the same server as before - only 25% will be invalidated.
11
+
12
+ = 1.5.0.5
13
+
14
+ * Remove native C CRC32_ITU_T extension in favor of Zlib's crc32 method.
15
+ memcache-client is now pure Ruby again and will work with JRuby and Rubinius.
16
+
17
+ = 1.5.0.4
18
+
19
+ * Get test suite working again (packagethief)
20
+ * Ruby 1.9 compatiblity fixes (packagethief, mperham)
21
+ * Consistently return server responses and check for errors (packagethief)
22
+ * Properly calculate CRC in Ruby 1.9 strings (mperham)
23
+ * Drop rspec in favor of test/unit, for 1.9 compat (mperham)
24
+
25
+ = 1.5.0.3 (FiveRuns fork)
26
+
27
+ * Integrated ITU-T CRC32 operation in native C extension for speed. Thanks to Justin Balthrop!
28
+
29
+ = 1.5.0.2 (FiveRuns fork)
30
+
31
+ * Add support for seamless failover between servers. If one server connection dies,
32
+ the client will retry the operation on another server before giving up.
33
+
34
+ * Merge Will Bryant's socket retry patch.
35
+ http://willbryant.net/software/2007/12/21/ruby-memcache-client-reconnect-and-retry
36
+
37
+ = 1.5.0.1 (FiveRuns fork)
38
+
39
+ * Fix set not handling client disconnects.
40
+ http://dev.twitter.com/2008/02/solving-case-of-missing-updates.html
41
+
42
+ = 1.5.0
43
+
44
+ * Add MemCache#flush_all command. Patch #13019 and bug #10503. Patches
45
+ submitted by Sebastian Delmont and Rick Olson.
46
+ * Type-cast data returned by MemCache#stats. Patch #10505 submitted by
47
+ Sebastian Delmont.
48
+
49
+ = 1.4.0
50
+
51
+ * Fix bug #10371, #set does not check response for server errors.
52
+ Submitted by Ben VandenBos.
53
+ * Fix bug #12450, set TCP_NODELAY socket option. Patch by Chris
54
+ McGrath.
55
+ * Fix bug #10704, missing #add method. Patch by Jamie Macey.
56
+ * Fix bug #10371, handle socket EOF in cache_get. Submitted by Ben
57
+ VandenBos.
58
+
59
+ = 1.3.0
60
+
61
+ * Apply patch #6507, add stats command. Submitted by Tyler Kovacs.
62
+ * Apply patch #6509, parallel implementation of #get_multi. Submitted
63
+ by Tyler Kovacs.
64
+ * Validate keys. Disallow spaces in keys or keys that are too long.
65
+ * Perform more validation of server responses. MemCache now reports
66
+ errors if the socket was not in an expected state. (Please file
67
+ bugs if you find some.)
68
+ * Add #incr and #decr.
69
+ * Add raw argument to #set and #get to retrieve #incr and #decr
70
+ values.
71
+ * Also put on MemCacheError when using Cache::get with block.
72
+ * memcache.rb no longer sets $TESTING to a true value if it was
73
+ previously defined. Bug #8213 by Matijs van Zuijlen.
74
+
75
+ = 1.2.1
76
+
77
+ * Fix bug #7048, MemCache#servers= referenced changed local variable.
78
+ Submitted by Justin Dossey.
79
+ * Fix bug #7049, MemCache#initialize resets @buckets. Submitted by
80
+ Justin Dossey.
81
+ * Fix bug #6232, Make Cache::Get work with a block only when nil is
82
+ returned. Submitted by Jon Evans.
83
+ * Moved to the seattlerb project.
84
+
85
+ = 1.2.0
86
+
87
+ NOTE: This version will store keys in different places than previous
88
+ versions! Be prepared for some thrashing while memcached sorts itself
89
+ out!
90
+
91
+ * Fixed multithreaded operations, bug 5994 and 5989.
92
+ Thanks to Blaine Cook, Erik Hetzner, Elliot Smith, Dave Myron (and
93
+ possibly others I have forgotten).
94
+ * Made memcached interoperable with other memcached libraries, bug
95
+ 4509. Thanks to anonymous.
96
+ * Added get_multi to match Perl/etc APIs
97
+
98
+ = 1.1.0
99
+
100
+ * Added some tests
101
+ * Sped up non-multithreaded and multithreaded operation
102
+ * More Ruby-memcache compatibility
103
+ * More RDoc
104
+ * Switched to Hoe
105
+
106
+ = 1.0.0
107
+
108
+ Birthday!
109
+
data/LICENSE.txt ADDED
@@ -0,0 +1,28 @@
1
+ All original code copyright 2005, 2006, 2007 Bob Cottrell, Eric Hodel,
2
+ The Robot Co-op. All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions
6
+ are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright
9
+ notice, this list of conditions and the following disclaimer.
10
+ 2. Redistributions in binary form must reproduce the above copyright
11
+ notice, this list of conditions and the following disclaimer in the
12
+ documentation and/or other materials provided with the distribution.
13
+ 3. Neither the names of the authors nor the names of their contributors
14
+ may be used to endorse or promote products derived from this software
15
+ without specific prior written permission.
16
+
17
+ THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS
18
+ OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20
+ ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE
21
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
22
+ OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
23
+ OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
24
+ BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
25
+ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
26
+ OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
27
+ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
+
data/README.txt ADDED
@@ -0,0 +1,5 @@
1
+ This repository contains the 1.5.0.x source for the FiveRuns memcache-client.
2
+
3
+ The latest memcache-client 1.6.0 source code can now be found at:
4
+
5
+ http://github.com/mperham/memcache-client
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ # vim: syntax=Ruby
2
+ require 'rubygems'
3
+ require 'rake/rdoctask'
4
+ require 'rake/testtask'
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
+ Rake::RDocTask.new do |rd|
15
+ rd.main = "README.rdoc"
16
+ rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
17
+ rd.rdoc_dir = 'doc'
18
+ end
19
+
20
+ Rake::TestTask.new
21
+
22
+ task :default => :test
data/lib/memcache.rb ADDED
@@ -0,0 +1,841 @@
1
+ $TESTING = defined?($TESTING) && $TESTING
2
+
3
+ require 'socket'
4
+ require 'thread'
5
+ require 'timeout'
6
+ require 'rubygems'
7
+ require 'zlib'
8
+ require 'digest/sha1'
9
+ require 'digest/md5'
10
+ ##
11
+ # A Ruby client library for memcached.
12
+ #
13
+ # This is intended to provide access to basic memcached functionality. It
14
+ # does not attempt to be complete implementation of the entire API, but it is
15
+ # approaching a complete implementation.
16
+
17
+ class MemCache
18
+
19
+ ##
20
+ # The version of MemCache you are using.
21
+
22
+ VERSION = '1.5.0.7'
23
+
24
+ ##
25
+ # Default options for the cache object.
26
+
27
+ DEFAULT_OPTIONS = {
28
+ :namespace => nil,
29
+ :readonly => false,
30
+ :multithread => false,
31
+ }
32
+
33
+ ##
34
+ # Default memcached port.
35
+
36
+ DEFAULT_PORT = 11211
37
+
38
+ ##
39
+ # Default memcached server weight.
40
+
41
+ DEFAULT_WEIGHT = 1
42
+
43
+ ##
44
+ # The amount of time to wait for a response from a memcached server. If a
45
+ # response is not completed within this time, the connection to the server
46
+ # will be closed and an error will be raised.
47
+
48
+ attr_accessor :request_timeout
49
+
50
+ ##
51
+ # The namespace for this instance
52
+
53
+ attr_reader :namespace
54
+
55
+ ##
56
+ # The multithread setting for this instance
57
+
58
+ attr_reader :multithread
59
+
60
+ ##
61
+ # The servers this client talks to. Play at your own peril.
62
+
63
+ attr_reader :servers
64
+
65
+ ##
66
+ # Accepts a list of +servers+ and a list of +opts+. +servers+ may be
67
+ # omitted. See +servers=+ for acceptable server list arguments.
68
+ #
69
+ # Valid options for +opts+ are:
70
+ #
71
+ # [:namespace] Prepends this value to all keys added or retrieved.
72
+ # [:readonly] Raises an exception on cache writes when true.
73
+ # [:multithread] Wraps cache access in a Mutex for thread safety.
74
+ #
75
+ # Other options are ignored.
76
+
77
+ def initialize(*args)
78
+ servers = []
79
+ opts = {}
80
+
81
+ case args.length
82
+ when 0 then # NOP
83
+ when 1 then
84
+ arg = args.shift
85
+ case arg
86
+ when Hash then opts = arg
87
+ when Array then servers = arg
88
+ when String then servers = [arg]
89
+ else raise ArgumentError, 'first argument must be Array, Hash or String'
90
+ end
91
+ when 2 then
92
+ servers, opts = args
93
+ else
94
+ raise ArgumentError, "wrong number of arguments (#{args.length} for 2)"
95
+ end
96
+
97
+ opts = DEFAULT_OPTIONS.merge opts
98
+ @namespace = opts[:namespace]
99
+ @readonly = opts[:readonly]
100
+ @multithread = opts[:multithread]
101
+ @mutex = Mutex.new if @multithread
102
+ @buckets = []
103
+ self.servers = servers
104
+ end
105
+
106
+ ##
107
+ # Returns a string representation of the cache object.
108
+
109
+ def inspect
110
+ "<MemCache: %d servers, %d buckets, ns: %p, ro: %p>" %
111
+ [@servers.length, @buckets.length, @namespace, @readonly]
112
+ end
113
+
114
+ ##
115
+ # Returns whether there is at least one active server for the object.
116
+
117
+ def active?
118
+ not @servers.empty?
119
+ end
120
+
121
+ ##
122
+ # Returns whether or not the cache object was created read only.
123
+
124
+ def readonly?
125
+ @readonly
126
+ end
127
+
128
+ ##
129
+ # Set the servers that the requests will be distributed between. Entries
130
+ # can be either strings of the form "hostname:port" or
131
+ # "hostname:port:weight" or MemCache::Server objects.
132
+ #
133
+ def servers=(servers)
134
+ # Create the server objects.
135
+ @servers = Array(servers).collect do |server|
136
+ case server
137
+ when String
138
+ host, port, weight = server.split ':', 3
139
+ port ||= DEFAULT_PORT
140
+ weight ||= DEFAULT_WEIGHT
141
+ Server.new self, host, port, weight
142
+ when Server
143
+ if server.multithread != @multithread then
144
+ raise ArgumentError, "can't mix threaded and non-threaded servers"
145
+ end
146
+ server
147
+ else
148
+ raise TypeError, "cannot convert #{server.class} into MemCache::Server"
149
+ end
150
+ end
151
+
152
+ # There's no point in doing this if there's only one server
153
+ @continuum = create_continuum_for(@servers) if @servers.size > 1
154
+
155
+ @servers
156
+ end
157
+
158
+ ##
159
+ # Decrements the value for +key+ by +amount+ and returns the new value.
160
+ # +key+ must already exist. If +key+ is not an integer, it is assumed to be
161
+ # 0. +key+ can not be decremented below 0.
162
+
163
+ def decr(key, amount = 1)
164
+ raise MemCacheError, "Update of readonly cache" if @readonly
165
+ with_server(key) do |server, cache_key|
166
+ cache_decr server, cache_key, amount
167
+ end
168
+ rescue TypeError => err
169
+ handle_error nil, err
170
+ end
171
+
172
+ ##
173
+ # Retrieves +key+ from memcache. If +raw+ is false, the value will be
174
+ # unmarshalled.
175
+
176
+ def get(key, raw = false)
177
+ with_server(key) do |server, cache_key|
178
+ value = cache_get server, cache_key
179
+ return nil if value.nil?
180
+ value = Marshal.load value unless raw
181
+ return value
182
+ end
183
+ rescue TypeError => err
184
+ handle_error nil, err
185
+ end
186
+
187
+ ##
188
+ # Retrieves multiple values from memcached in parallel, if possible.
189
+ #
190
+ # The memcached protocol supports the ability to retrieve multiple
191
+ # keys in a single request. Pass in an array of keys to this method
192
+ # and it will:
193
+ #
194
+ # 1. map the key to the appropriate memcached server
195
+ # 2. send a single request to each server that has one or more key values
196
+ #
197
+ # Returns a hash of values.
198
+ #
199
+ # cache["a"] = 1
200
+ # cache["b"] = 2
201
+ # cache.get_multi "a", "b" # => { "a" => 1, "b" => 2 }
202
+
203
+ def get_multi(*keys)
204
+ raise MemCacheError, 'No active servers' unless active?
205
+
206
+ keys.flatten!
207
+ key_count = keys.length
208
+ cache_keys = {}
209
+ server_keys = Hash.new { |h,k| h[k] = [] }
210
+
211
+ # map keys to servers
212
+ keys.each do |key|
213
+ server, cache_key = request_setup key
214
+ cache_keys[cache_key] = key
215
+ server_keys[server] << cache_key
216
+ end
217
+
218
+ results = {}
219
+
220
+ server_keys.each do |server, keys_for_server|
221
+ keys_for_server = keys_for_server.join ' '
222
+ values = cache_get_multi server, keys_for_server
223
+ values.each do |key, value|
224
+ results[cache_keys[key]] = Marshal.load value
225
+ end
226
+ end
227
+
228
+ return results
229
+ rescue TypeError, IndexError => err
230
+ handle_error nil, err
231
+ end
232
+
233
+ ##
234
+ # Increments the value for +key+ by +amount+ and returns the new value.
235
+ # +key+ must already exist. If +key+ is not an integer, it is assumed to be
236
+ # 0.
237
+
238
+ def incr(key, amount = 1)
239
+ raise MemCacheError, "Update of readonly cache" if @readonly
240
+ with_server(key) do |server, cache_key|
241
+ cache_incr server, cache_key, amount
242
+ end
243
+ rescue TypeError => err
244
+ handle_error nil, err
245
+ end
246
+
247
+ ##
248
+ # Add +key+ to the cache with value +value+ that expires in +expiry+
249
+ # seconds. If +raw+ is true, +value+ will not be Marshalled.
250
+ #
251
+ # Warning: Readers should not call this method in the event of a cache miss;
252
+ # see MemCache#add.
253
+
254
+ def set(key, value, expiry = 0, raw = false)
255
+ raise MemCacheError, "Update of readonly cache" if @readonly
256
+ with_server(key) do |server, cache_key|
257
+
258
+ value = Marshal.dump value unless raw
259
+ command = "set #{cache_key} 0 #{expiry} #{value.to_s.size}\r\n#{value}\r\n"
260
+
261
+ with_socket_management(server) do |socket|
262
+ socket.write command
263
+ result = socket.gets
264
+ raise_on_error_response! result
265
+
266
+ if result.nil?
267
+ server.close
268
+ raise MemCacheError, "lost connection to #{server.host}:#{server.port}"
269
+ end
270
+
271
+ result
272
+ end
273
+ end
274
+ end
275
+
276
+ ##
277
+ # Add +key+ to the cache with value +value+ that expires in +expiry+
278
+ # seconds, but only if +key+ does not already exist in the cache.
279
+ # If +raw+ is true, +value+ will not be Marshalled.
280
+ #
281
+ # Readers should call this method in the event of a cache miss, not
282
+ # MemCache#set or MemCache#[]=.
283
+
284
+ def add(key, value, expiry = 0, raw = false)
285
+ raise MemCacheError, "Update of readonly cache" if @readonly
286
+ with_server(key) do |server, cache_key|
287
+ value = Marshal.dump value unless raw
288
+ command = "add #{cache_key} 0 #{expiry} #{value.size}\r\n#{value}\r\n"
289
+
290
+ with_socket_management(server) do |socket|
291
+ socket.write command
292
+ result = socket.gets
293
+ raise_on_error_response! result
294
+ result
295
+ end
296
+ end
297
+ end
298
+
299
+ ##
300
+ # Removes +key+ from the cache in +expiry+ seconds.
301
+
302
+ def delete(key, expiry = 0)
303
+ raise MemCacheError, "Update of readonly cache" if @readonly
304
+ with_server(key) do |server, cache_key|
305
+ with_socket_management(server) do |socket|
306
+ socket.write "delete #{cache_key} #{expiry}\r\n"
307
+ result = socket.gets
308
+ raise_on_error_response! result
309
+ result
310
+ end
311
+ end
312
+ end
313
+
314
+ ##
315
+ # Flush the cache from all memcache servers.
316
+
317
+ def flush_all
318
+ raise MemCacheError, 'No active servers' unless active?
319
+ raise MemCacheError, "Update of readonly cache" if @readonly
320
+
321
+ begin
322
+ @mutex.lock if @multithread
323
+ @servers.each do |server|
324
+ with_socket_management(server) do |socket|
325
+ socket.write "flush_all\r\n"
326
+ result = socket.gets
327
+ raise_on_error_response! result
328
+ result
329
+ end
330
+ end
331
+ rescue IndexError => err
332
+ handle_error nil, err
333
+ ensure
334
+ @mutex.unlock if @multithread
335
+ end
336
+ end
337
+
338
+ ##
339
+ # Reset the connection to all memcache servers. This should be called if
340
+ # there is a problem with a cache lookup that might have left the connection
341
+ # in a corrupted state.
342
+
343
+ def reset
344
+ @servers.each { |server| server.close }
345
+ end
346
+
347
+ ##
348
+ # Returns statistics for each memcached server. An explanation of the
349
+ # statistics can be found in the memcached docs:
350
+ #
351
+ # http://code.sixapart.com/svn/memcached/trunk/server/doc/protocol.txt
352
+ #
353
+ # Example:
354
+ #
355
+ # >> pp CACHE.stats
356
+ # {"localhost:11211"=>
357
+ # {"bytes"=>4718,
358
+ # "pid"=>20188,
359
+ # "connection_structures"=>4,
360
+ # "time"=>1162278121,
361
+ # "pointer_size"=>32,
362
+ # "limit_maxbytes"=>67108864,
363
+ # "cmd_get"=>14532,
364
+ # "version"=>"1.2.0",
365
+ # "bytes_written"=>432583,
366
+ # "cmd_set"=>32,
367
+ # "get_misses"=>0,
368
+ # "total_connections"=>19,
369
+ # "curr_connections"=>3,
370
+ # "curr_items"=>4,
371
+ # "uptime"=>1557,
372
+ # "get_hits"=>14532,
373
+ # "total_items"=>32,
374
+ # "rusage_system"=>0.313952,
375
+ # "rusage_user"=>0.119981,
376
+ # "bytes_read"=>190619}}
377
+ # => nil
378
+
379
+ def stats
380
+ raise MemCacheError, "No active servers" unless active?
381
+ server_stats = {}
382
+
383
+ @servers.each do |server|
384
+ next unless server.alive?
385
+
386
+ with_socket_management(server) do |socket|
387
+ value = nil
388
+ socket.write "stats\r\n"
389
+ stats = {}
390
+ while line = socket.gets do
391
+ raise_on_error_response! line
392
+ break if line == "END\r\n"
393
+ if line =~ /\ASTAT ([\w]+) ([\w\.\:]+)/ then
394
+ name, value = $1, $2
395
+ stats[name] = case name
396
+ when 'version'
397
+ value
398
+ when 'rusage_user', 'rusage_system' then
399
+ seconds, microseconds = value.split(/:/, 2)
400
+ microseconds ||= 0
401
+ Float(seconds) + (Float(microseconds) / 1_000_000)
402
+ else
403
+ if value =~ /\A\d+\Z/ then
404
+ value.to_i
405
+ else
406
+ value
407
+ end
408
+ end
409
+ end
410
+ end
411
+ server_stats["#{server.host}:#{server.port}"] = stats
412
+ end
413
+ end
414
+
415
+ raise MemCacheError, "No active servers" if server_stats.empty?
416
+ server_stats
417
+ end
418
+
419
+ ##
420
+ # Shortcut to get a value from the cache.
421
+
422
+ alias [] get
423
+
424
+ ##
425
+ # Shortcut to save a value in the cache. This method does not set an
426
+ # expiration on the entry. Use set to specify an explicit expiry.
427
+
428
+ def []=(key, value)
429
+ set key, value
430
+ end
431
+
432
+ protected unless $TESTING
433
+
434
+ ##
435
+ # Create a key for the cache, incorporating the namespace qualifier if
436
+ # requested.
437
+
438
+ def make_cache_key(key)
439
+ if namespace.nil? then
440
+ key
441
+ else
442
+ "#{@namespace}:#{key}"
443
+ end
444
+ end
445
+
446
+ ##
447
+ # Returns an interoperable hash value for +key+. (I think, docs are
448
+ # sketchy for down servers).
449
+
450
+ def hash_for(key)
451
+ Zlib.crc32(key)
452
+ end
453
+
454
+ ##
455
+ # Pick a server to handle the request based on a hash of the key.
456
+
457
+ def get_server_for_key(key)
458
+ raise ArgumentError, "illegal character in key #{key.inspect}" if
459
+ key =~ /\s/
460
+ # raise ArgumentError, "key too long #{key.inspect}" if key.length > 250
461
+ raise MemCacheError, "No servers available" if @servers.empty?
462
+ return @servers.first if @servers.length == 1
463
+ key = Digest::MD5.hexdigest(key) if key.length > 250
464
+
465
+ hkey = hash_for(key)
466
+
467
+ 20.times do |try|
468
+ server = binary_search(@continuum, hkey) { |e| e.value }.server
469
+ return server if server.alive?
470
+ hkey = hash_for "#{try}#{key}"
471
+ end
472
+
473
+ raise MemCacheError, "No servers available"
474
+ end
475
+
476
+ ##
477
+ # Performs a raw decr for +cache_key+ from +server+. Returns nil if not
478
+ # found.
479
+
480
+ def cache_decr(server, cache_key, amount)
481
+ with_socket_management(server) do |socket|
482
+ socket.write "decr #{cache_key} #{amount}\r\n"
483
+ text = socket.gets
484
+ raise_on_error_response! text
485
+ return nil if text == "NOT_FOUND\r\n"
486
+ return text.to_i
487
+ end
488
+ end
489
+
490
+ ##
491
+ # Fetches the raw data for +cache_key+ from +server+. Returns nil on cache
492
+ # miss.
493
+
494
+ def cache_get(server, cache_key)
495
+ with_socket_management(server) do |socket|
496
+ socket.write "get #{cache_key}\r\n"
497
+ keyline = socket.gets # "VALUE <key> <flags> <bytes>\r\n"
498
+
499
+ if keyline.nil? then
500
+ server.close
501
+ raise MemCacheError, "lost connection to #{server.host}:#{server.port}"
502
+ end
503
+
504
+ raise_on_error_response! keyline
505
+ return nil if keyline == "END\r\n"
506
+
507
+ unless keyline =~ /(\d+)\r/ then
508
+ server.close
509
+ raise MemCacheError, "unexpected response #{keyline.inspect}"
510
+ end
511
+ value = socket.read $1.to_i
512
+ socket.read 2 # "\r\n"
513
+ socket.gets # "END\r\n"
514
+ return value
515
+ end
516
+ end
517
+
518
+ ##
519
+ # Fetches +cache_keys+ from +server+ using a multi-get.
520
+
521
+ def cache_get_multi(server, cache_keys)
522
+ with_socket_management(server) do |socket|
523
+ values = {}
524
+ socket.write "get #{cache_keys}\r\n"
525
+
526
+ while keyline = socket.gets do
527
+ return values if keyline == "END\r\n"
528
+ raise_on_error_response! keyline
529
+
530
+ unless keyline =~ /\AVALUE (.+) (.+) (.+)/ then
531
+ server.close
532
+ raise MemCacheError, "unexpected response #{keyline.inspect}"
533
+ end
534
+
535
+ key, data_length = $1, $3
536
+ values[$1] = socket.read data_length.to_i
537
+ socket.read(2) # "\r\n"
538
+ end
539
+
540
+ server.close
541
+ raise MemCacheError, "lost connection to #{server.host}:#{server.port}" # TODO: retry here too
542
+ end
543
+ end
544
+
545
+ ##
546
+ # Performs a raw incr for +cache_key+ from +server+. Returns nil if not
547
+ # found.
548
+
549
+ def cache_incr(server, cache_key, amount)
550
+ with_socket_management(server) do |socket|
551
+ socket.write "incr #{cache_key} #{amount}\r\n"
552
+ text = socket.gets
553
+ raise_on_error_response! text
554
+ return nil if text == "NOT_FOUND\r\n"
555
+ return text.to_i
556
+ end
557
+ end
558
+
559
+ ##
560
+ # Gets or creates a socket connected to the given server, and yields it
561
+ # to the block, wrapped in a mutex synchronization if @multithread is true.
562
+ #
563
+ # If a socket error (SocketError, SystemCallError, IOError) or protocol error
564
+ # (MemCacheError) is raised by the block, closes the socket, attempts to
565
+ # connect again, and retries the block (once). If an error is again raised,
566
+ # reraises it as MemCacheError.
567
+ #
568
+ # If unable to connect to the server (or if in the reconnect wait period),
569
+ # raises MemCacheError. Note that the socket connect code marks a server
570
+ # dead for a timeout period, so retrying does not apply to connection attempt
571
+ # failures (but does still apply to unexpectedly lost connections etc.).
572
+
573
+ def with_socket_management(server, &block)
574
+ @mutex.lock if @multithread
575
+ retried = false
576
+ begin
577
+ socket = server.socket
578
+
579
+ # Raise an IndexError to show this server is out of whack. If were inside
580
+ # a with_server block, we'll catch it and attempt to restart the operation.
581
+ raise IndexError, "No connection to server (#{server.status})" if socket.nil?
582
+
583
+ block.call(socket)
584
+ rescue MemCacheError, SocketError, SystemCallError, IOError => err
585
+ handle_error(server, err) if retried || socket.nil?
586
+ retried = true
587
+ retry
588
+ end
589
+ ensure
590
+ @mutex.unlock if @multithread
591
+ end
592
+
593
+ def with_server(key)
594
+ retried = false
595
+ begin
596
+ server, cache_key = request_setup(key)
597
+ yield server, cache_key
598
+ rescue IndexError => e
599
+ if !retried && @servers.size > 1
600
+ puts "Connection to server #{server.inspect} DIED! Retrying operation..."
601
+ retried = true
602
+ retry
603
+ end
604
+ handle_error(nil, e)
605
+ end
606
+ end
607
+
608
+ ##
609
+ # Handles +error+ from +server+.
610
+
611
+ def handle_error(server, error)
612
+ raise error if error.is_a?(MemCacheError)
613
+ server.close if server
614
+ new_error = MemCacheError.new error.message
615
+ new_error.set_backtrace error.backtrace
616
+ raise new_error
617
+ end
618
+
619
+ ##
620
+ # Performs setup for making a request with +key+ from memcached. Returns
621
+ # the server to fetch the key from and the complete key to use.
622
+
623
+ def request_setup(key)
624
+ raise MemCacheError, 'No active servers' unless active?
625
+ cache_key = make_cache_key key
626
+ server = get_server_for_key cache_key
627
+ return server, cache_key
628
+ end
629
+
630
+ def raise_on_error_response!(response)
631
+ if response =~ /\A(?:CLIENT_|SERVER_)?ERROR(.*)/
632
+ raise MemCacheError, $1.strip
633
+ end
634
+ end
635
+
636
+ def create_continuum_for(servers)
637
+ total_weight = servers.inject(0) { |memo, srv| memo + srv.weight }
638
+ continuum = []
639
+
640
+ servers.each do |server|
641
+ entry_count_for(server, servers.size, total_weight).times do |idx|
642
+ hash = Digest::SHA1.hexdigest("#{server.host}:#{server.port}:#{idx}")
643
+ value = Integer("0x#{hash[0..7]}")
644
+ continuum << ContinuumEntry.new(value, server)
645
+ end
646
+ end
647
+
648
+ continuum.sort { |a, b| a.value <=> b.value }
649
+ end
650
+
651
+ def entry_count_for(server, total_servers, total_weight)
652
+ ((total_servers * ContinuumEntry::POINTS_PER_SERVER * server.weight) / Float(total_weight)).floor
653
+ end
654
+
655
+ class ContinuumEntry
656
+ POINTS_PER_SERVER = 160 # this is the default in libmemcached
657
+
658
+ attr_reader :value
659
+ attr_reader :server
660
+
661
+ def initialize(val, srv)
662
+ @value = val
663
+ @server = srv
664
+ end
665
+
666
+ def inspect
667
+ "<#{value}, #{server.host}:#{server.port}>"
668
+ end
669
+ end
670
+
671
+ ##
672
+ # This class represents a memcached server instance.
673
+
674
+ class Server
675
+
676
+ ##
677
+ # The amount of time to wait to establish a connection with a memcached
678
+ # server. If a connection cannot be established within this time limit,
679
+ # the server will be marked as down.
680
+
681
+ CONNECT_TIMEOUT = 0.25
682
+
683
+ ##
684
+ # The amount of time to wait before attempting to re-establish a
685
+ # connection with a server that is marked dead.
686
+
687
+ RETRY_DELAY = 30.0
688
+
689
+ ##
690
+ # The host the memcached server is running on.
691
+
692
+ attr_reader :host
693
+
694
+ ##
695
+ # The port the memcached server is listening on.
696
+
697
+ attr_reader :port
698
+
699
+ ##
700
+ # The weight given to the server.
701
+
702
+ attr_reader :weight
703
+
704
+ ##
705
+ # The time of next retry if the connection is dead.
706
+
707
+ attr_reader :retry
708
+
709
+ ##
710
+ # A text status string describing the state of the server.
711
+
712
+ attr_reader :status
713
+
714
+ attr_reader :multithread
715
+
716
+ ##
717
+ # Create a new MemCache::Server object for the memcached instance
718
+ # listening on the given host and port, weighted by the given weight.
719
+
720
+ def initialize(memcache, host, port = DEFAULT_PORT, weight = DEFAULT_WEIGHT)
721
+ raise ArgumentError, "No host specified" if host.nil? or host.empty?
722
+ raise ArgumentError, "No port specified" if port.nil? or port.to_i.zero?
723
+
724
+ @host = host
725
+ @port = port.to_i
726
+ @weight = weight.to_i
727
+
728
+ @multithread = memcache.multithread
729
+ @mutex = Mutex.new
730
+
731
+ @sock = nil
732
+ @retry = nil
733
+ @status = 'NOT CONNECTED'
734
+ end
735
+
736
+ ##
737
+ # Return a string representation of the server object.
738
+
739
+ def inspect
740
+ "<MemCache::Server: %s:%d [%d] (%s)>" % [@host, @port, @weight, @status]
741
+ end
742
+
743
+ ##
744
+ # Check whether the server connection is alive. This will cause the
745
+ # socket to attempt to connect if it isn't already connected and or if
746
+ # the server was previously marked as down and the retry time has
747
+ # been exceeded.
748
+
749
+ def alive?
750
+ !!socket
751
+ end
752
+
753
+ ##
754
+ # Try to connect to the memcached server targeted by this object.
755
+ # Returns the connected socket object on success or nil on failure.
756
+
757
+ def socket
758
+ @mutex.lock if @multithread
759
+ return @sock if @sock and not @sock.closed?
760
+
761
+ @sock = nil
762
+
763
+ # If the host was dead, don't retry for a while.
764
+ return if @retry and @retry > Time.now
765
+
766
+ # Attempt to connect if not already connected.
767
+ begin
768
+ @sock = timeout CONNECT_TIMEOUT do
769
+ TCPSocket.new @host, @port
770
+ end
771
+ if Socket.constants.include? 'TCP_NODELAY' then
772
+ @sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
773
+ end
774
+ @retry = nil
775
+ @status = 'CONNECTED'
776
+ rescue SocketError, SystemCallError, IOError, Timeout::Error => err
777
+ mark_dead err.message
778
+ end
779
+
780
+ return @sock
781
+ ensure
782
+ @mutex.unlock if @multithread
783
+ end
784
+
785
+ ##
786
+ # Close the connection to the memcached server targeted by this
787
+ # object. The server is not considered dead.
788
+
789
+ def close
790
+ @mutex.lock if @multithread
791
+ @sock.close if @sock && !@sock.closed?
792
+ @sock = nil
793
+ @retry = nil
794
+ @status = "NOT CONNECTED"
795
+ ensure
796
+ @mutex.unlock if @multithread
797
+ end
798
+
799
+ private
800
+
801
+ ##
802
+ # Mark the server as dead and close its socket.
803
+
804
+ def mark_dead(reason = "Unknown error")
805
+ @sock.close if @sock && !@sock.closed?
806
+ @sock = nil
807
+ @retry = Time.now + RETRY_DELAY
808
+
809
+ @status = sprintf "DEAD: %s, will retry at %s", reason, @retry
810
+ end
811
+
812
+ end
813
+
814
+ ##
815
+ # Base MemCache exception class.
816
+
817
+ class MemCacheError < RuntimeError; end
818
+
819
+
820
+ # Find the closest element in Array less than or equal to value.
821
+ def binary_search(ary, value, &block)
822
+ upper = ary.size - 1
823
+ lower = 0
824
+ idx = 0
825
+
826
+ result = while(lower <= upper) do
827
+ idx = (lower + upper) / 2
828
+ comp = block.call(ary[idx]) <=> value
829
+
830
+ if comp == 0
831
+ break idx
832
+ elsif comp > 0
833
+ upper = idx - 1
834
+ else
835
+ lower = idx + 1
836
+ end
837
+ end
838
+ result ? ary[result] : ary[upper]
839
+ end
840
+ end
841
+