mperham-memcache-client 1.6.0

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/lib/memcache.rb ADDED
@@ -0,0 +1,886 @@
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
+
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.6.0'
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
+ self.servers = servers
103
+ end
104
+
105
+ ##
106
+ # Returns a string representation of the cache object.
107
+
108
+ def inspect
109
+ "<MemCache: %d servers, ns: %p, ro: %p>" %
110
+ [@servers.length, @namespace, @readonly]
111
+ end
112
+
113
+ ##
114
+ # Returns whether there is at least one active server for the object.
115
+
116
+ def active?
117
+ not @servers.empty?
118
+ end
119
+
120
+ ##
121
+ # Returns whether or not the cache object was created read only.
122
+
123
+ def readonly?
124
+ @readonly
125
+ end
126
+
127
+ ##
128
+ # Set the servers that the requests will be distributed between. Entries
129
+ # can be either strings of the form "hostname:port" or
130
+ # "hostname:port:weight" or MemCache::Server objects.
131
+ #
132
+ def servers=(servers)
133
+ # Create the server objects.
134
+ @servers = Array(servers).collect do |server|
135
+ case server
136
+ when String
137
+ host, port, weight = server.split ':', 3
138
+ port ||= DEFAULT_PORT
139
+ weight ||= DEFAULT_WEIGHT
140
+ Server.new self, host, port, weight
141
+ when Server
142
+ if server.multithread != @multithread then
143
+ raise ArgumentError, "can't mix threaded and non-threaded servers"
144
+ end
145
+ server
146
+ else
147
+ raise TypeError, "cannot convert #{server.class} into MemCache::Server"
148
+ end
149
+ end
150
+
151
+ # There's no point in doing this if there's only one server
152
+ @continuum = create_continuum_for(@servers) if @servers.size > 1
153
+
154
+ @servers
155
+ end
156
+
157
+ ##
158
+ # Decrements the value for +key+ by +amount+ and returns the new value.
159
+ # +key+ must already exist. If +key+ is not an integer, it is assumed to be
160
+ # 0. +key+ can not be decremented below 0.
161
+
162
+ def decr(key, amount = 1)
163
+ raise MemCacheError, "Update of readonly cache" if @readonly
164
+ with_server(key) do |server, cache_key|
165
+ cache_decr server, cache_key, amount
166
+ end
167
+ rescue TypeError => err
168
+ handle_error nil, err
169
+ end
170
+
171
+ ##
172
+ # Retrieves +key+ from memcache. If +raw+ is false, the value will be
173
+ # unmarshalled.
174
+
175
+ def get(key, raw = false)
176
+ with_server(key) do |server, cache_key|
177
+ value = cache_get server, cache_key
178
+ return nil if value.nil?
179
+ value = Marshal.load value unless raw
180
+ return value
181
+ end
182
+ rescue TypeError => err
183
+ handle_error nil, err
184
+ end
185
+
186
+ ##
187
+ # Retrieves multiple values from memcached in parallel, if possible.
188
+ #
189
+ # The memcached protocol supports the ability to retrieve multiple
190
+ # keys in a single request. Pass in an array of keys to this method
191
+ # and it will:
192
+ #
193
+ # 1. map the key to the appropriate memcached server
194
+ # 2. send a single request to each server that has one or more key values
195
+ #
196
+ # Returns a hash of values.
197
+ #
198
+ # cache["a"] = 1
199
+ # cache["b"] = 2
200
+ # cache.get_multi "a", "b" # => { "a" => 1, "b" => 2 }
201
+
202
+ def get_multi(*keys)
203
+ raise MemCacheError, 'No active servers' unless active?
204
+
205
+ keys.flatten!
206
+ key_count = keys.length
207
+ cache_keys = {}
208
+ server_keys = Hash.new { |h,k| h[k] = [] }
209
+
210
+ # map keys to servers
211
+ keys.each do |key|
212
+ server, cache_key = request_setup key
213
+ cache_keys[cache_key] = key
214
+ server_keys[server] << cache_key
215
+ end
216
+
217
+ results = {}
218
+
219
+ server_keys.each do |server, keys_for_server|
220
+ keys_for_server = keys_for_server.join ' '
221
+ values = cache_get_multi server, keys_for_server
222
+ values.each do |key, value|
223
+ results[cache_keys[key]] = Marshal.load value
224
+ end
225
+ end
226
+
227
+ return results
228
+ rescue TypeError, IndexError => err
229
+ handle_error nil, err
230
+ end
231
+
232
+ ##
233
+ # Increments the value for +key+ by +amount+ and returns the new value.
234
+ # +key+ must already exist. If +key+ is not an integer, it is assumed to be
235
+ # 0.
236
+
237
+ def incr(key, amount = 1)
238
+ raise MemCacheError, "Update of readonly cache" if @readonly
239
+ with_server(key) do |server, cache_key|
240
+ cache_incr server, cache_key, amount
241
+ end
242
+ rescue TypeError => err
243
+ handle_error nil, err
244
+ end
245
+
246
+ ##
247
+ # Add +key+ to the cache with value +value+ that expires in +expiry+
248
+ # seconds. If +raw+ is true, +value+ will not be Marshalled.
249
+ #
250
+ # Warning: Readers should not call this method in the event of a cache miss;
251
+ # see MemCache#add.
252
+
253
+ def set(key, value, expiry = 0, raw = false)
254
+ raise MemCacheError, "Update of readonly cache" if @readonly
255
+ with_server(key) do |server, cache_key|
256
+
257
+ value = Marshal.dump value unless raw
258
+ command = "set #{cache_key} 0 #{expiry} #{value.to_s.size}\r\n#{value}\r\n"
259
+
260
+ with_socket_management(server) do |socket|
261
+ socket.write command
262
+ result = socket.gets
263
+ raise_on_error_response! result
264
+
265
+ if result.nil?
266
+ server.close
267
+ raise MemCacheError, "lost connection to #{server.host}:#{server.port}"
268
+ end
269
+
270
+ result
271
+ end
272
+ end
273
+ end
274
+
275
+ ##
276
+ # Add +key+ to the cache with value +value+ that expires in +expiry+
277
+ # seconds, but only if +key+ does not already exist in the cache.
278
+ # If +raw+ is true, +value+ will not be Marshalled.
279
+ #
280
+ # Readers should call this method in the event of a cache miss, not
281
+ # MemCache#set or MemCache#[]=.
282
+
283
+ def add(key, value, expiry = 0, raw = false)
284
+ raise MemCacheError, "Update of readonly cache" if @readonly
285
+ with_server(key) do |server, cache_key|
286
+ value = Marshal.dump value unless raw
287
+ command = "add #{cache_key} 0 #{expiry} #{value.size}\r\n#{value}\r\n"
288
+
289
+ with_socket_management(server) do |socket|
290
+ socket.write command
291
+ result = socket.gets
292
+ raise_on_error_response! result
293
+ result
294
+ end
295
+ end
296
+ end
297
+
298
+ ##
299
+ # Removes +key+ from the cache in +expiry+ seconds.
300
+
301
+ def delete(key, expiry = 0)
302
+ raise MemCacheError, "Update of readonly cache" if @readonly
303
+ with_server(key) do |server, cache_key|
304
+ with_socket_management(server) do |socket|
305
+ socket.write "delete #{cache_key} #{expiry}\r\n"
306
+ result = socket.gets
307
+ raise_on_error_response! result
308
+ result
309
+ end
310
+ end
311
+ end
312
+
313
+ ##
314
+ # Flush the cache from all memcache servers.
315
+
316
+ def flush_all
317
+ raise MemCacheError, 'No active servers' unless active?
318
+ raise MemCacheError, "Update of readonly cache" if @readonly
319
+
320
+ begin
321
+ @mutex.lock if @multithread
322
+ @servers.each do |server|
323
+ with_socket_management(server) do |socket|
324
+ socket.write "flush_all\r\n"
325
+ result = socket.gets
326
+ raise_on_error_response! result
327
+ result
328
+ end
329
+ end
330
+ rescue IndexError => err
331
+ handle_error nil, err
332
+ ensure
333
+ @mutex.unlock if @multithread
334
+ end
335
+ end
336
+
337
+ ##
338
+ # Reset the connection to all memcache servers. This should be called if
339
+ # there is a problem with a cache lookup that might have left the connection
340
+ # in a corrupted state.
341
+
342
+ def reset
343
+ @servers.each { |server| server.close }
344
+ end
345
+
346
+ ##
347
+ # Returns statistics for each memcached server. An explanation of the
348
+ # statistics can be found in the memcached docs:
349
+ #
350
+ # http://code.sixapart.com/svn/memcached/trunk/server/doc/protocol.txt
351
+ #
352
+ # Example:
353
+ #
354
+ # >> pp CACHE.stats
355
+ # {"localhost:11211"=>
356
+ # {"bytes"=>4718,
357
+ # "pid"=>20188,
358
+ # "connection_structures"=>4,
359
+ # "time"=>1162278121,
360
+ # "pointer_size"=>32,
361
+ # "limit_maxbytes"=>67108864,
362
+ # "cmd_get"=>14532,
363
+ # "version"=>"1.2.0",
364
+ # "bytes_written"=>432583,
365
+ # "cmd_set"=>32,
366
+ # "get_misses"=>0,
367
+ # "total_connections"=>19,
368
+ # "curr_connections"=>3,
369
+ # "curr_items"=>4,
370
+ # "uptime"=>1557,
371
+ # "get_hits"=>14532,
372
+ # "total_items"=>32,
373
+ # "rusage_system"=>0.313952,
374
+ # "rusage_user"=>0.119981,
375
+ # "bytes_read"=>190619}}
376
+ # => nil
377
+
378
+ def stats
379
+ raise MemCacheError, "No active servers" unless active?
380
+ server_stats = {}
381
+
382
+ @servers.each do |server|
383
+ next unless server.alive?
384
+
385
+ with_socket_management(server) do |socket|
386
+ value = nil
387
+ socket.write "stats\r\n"
388
+ stats = {}
389
+ while line = socket.gets do
390
+ raise_on_error_response! line
391
+ break if line == "END\r\n"
392
+ if line =~ /\ASTAT ([\w]+) ([\w\.\:]+)/ then
393
+ name, value = $1, $2
394
+ stats[name] = case name
395
+ when 'version'
396
+ value
397
+ when 'rusage_user', 'rusage_system' then
398
+ seconds, microseconds = value.split(/:/, 2)
399
+ microseconds ||= 0
400
+ Float(seconds) + (Float(microseconds) / 1_000_000)
401
+ else
402
+ if value =~ /\A\d+\Z/ then
403
+ value.to_i
404
+ else
405
+ value
406
+ end
407
+ end
408
+ end
409
+ end
410
+ server_stats["#{server.host}:#{server.port}"] = stats
411
+ end
412
+ end
413
+
414
+ raise MemCacheError, "No active servers" if server_stats.empty?
415
+ server_stats
416
+ end
417
+
418
+ ##
419
+ # Shortcut to get a value from the cache.
420
+
421
+ alias [] get
422
+
423
+ ##
424
+ # Shortcut to save a value in the cache. This method does not set an
425
+ # expiration on the entry. Use set to specify an explicit expiry.
426
+
427
+ def []=(key, value)
428
+ set key, value
429
+ end
430
+
431
+ protected unless $TESTING
432
+
433
+ ##
434
+ # Create a key for the cache, incorporating the namespace qualifier if
435
+ # requested.
436
+
437
+ def make_cache_key(key)
438
+ if namespace.nil? then
439
+ key
440
+ else
441
+ "#{@namespace}:#{key}"
442
+ end
443
+ end
444
+
445
+ ##
446
+ # Returns an interoperable hash value for +key+. (I think, docs are
447
+ # sketchy for down servers).
448
+
449
+ def hash_for(key)
450
+ Zlib.crc32(key)
451
+ end
452
+
453
+ ##
454
+ # Pick a server to handle the request based on a hash of the key.
455
+
456
+ def get_server_for_key(key, options = {})
457
+ raise ArgumentError, "illegal character in key #{key.inspect}" if
458
+ key =~ /\s/
459
+ raise ArgumentError, "key too long #{key.inspect}" if key.length > 250
460
+ raise MemCacheError, "No servers available" if @servers.empty?
461
+ return @servers.first if @servers.length == 1
462
+
463
+ hkey = hash_for(key)
464
+
465
+ 20.times do |try|
466
+ server = binary_search(@continuum, hkey) { |e| e.value }.server
467
+ return server if server.alive?
468
+ hkey = hash_for "#{try}#{key}"
469
+ end
470
+
471
+ raise MemCacheError, "No servers available"
472
+ end
473
+
474
+ ##
475
+ # Performs a raw decr for +cache_key+ from +server+. Returns nil if not
476
+ # found.
477
+
478
+ def cache_decr(server, cache_key, amount)
479
+ with_socket_management(server) do |socket|
480
+ socket.write "decr #{cache_key} #{amount}\r\n"
481
+ text = socket.gets
482
+ raise_on_error_response! text
483
+ return nil if text == "NOT_FOUND\r\n"
484
+ return text.to_i
485
+ end
486
+ end
487
+
488
+ ##
489
+ # Fetches the raw data for +cache_key+ from +server+. Returns nil on cache
490
+ # miss.
491
+
492
+ def cache_get(server, cache_key)
493
+ with_socket_management(server) do |socket|
494
+ socket.write "get #{cache_key}\r\n"
495
+ keyline = socket.gets # "VALUE <key> <flags> <bytes>\r\n"
496
+
497
+ if keyline.nil? then
498
+ server.close
499
+ raise MemCacheError, "lost connection to #{server.host}:#{server.port}"
500
+ end
501
+
502
+ raise_on_error_response! keyline
503
+ return nil if keyline == "END\r\n"
504
+
505
+ unless keyline =~ /(\d+)\r/ then
506
+ server.close
507
+ raise MemCacheError, "unexpected response #{keyline.inspect}"
508
+ end
509
+ value = socket.read $1.to_i
510
+ socket.read 2 # "\r\n"
511
+ socket.gets # "END\r\n"
512
+ return value
513
+ end
514
+ end
515
+
516
+ ##
517
+ # Fetches +cache_keys+ from +server+ using a multi-get.
518
+
519
+ def cache_get_multi(server, cache_keys)
520
+ with_socket_management(server) do |socket|
521
+ values = {}
522
+ socket.write "get #{cache_keys}\r\n"
523
+
524
+ while keyline = socket.gets do
525
+ return values if keyline == "END\r\n"
526
+ raise_on_error_response! keyline
527
+
528
+ unless keyline =~ /\AVALUE (.+) (.+) (.+)/ then
529
+ server.close
530
+ raise MemCacheError, "unexpected response #{keyline.inspect}"
531
+ end
532
+
533
+ key, data_length = $1, $3
534
+ values[$1] = socket.read data_length.to_i
535
+ socket.read(2) # "\r\n"
536
+ end
537
+
538
+ server.close
539
+ raise MemCacheError, "lost connection to #{server.host}:#{server.port}" # TODO: retry here too
540
+ end
541
+ end
542
+
543
+ ##
544
+ # Performs a raw incr for +cache_key+ from +server+. Returns nil if not
545
+ # found.
546
+
547
+ def cache_incr(server, cache_key, amount)
548
+ with_socket_management(server) do |socket|
549
+ socket.write "incr #{cache_key} #{amount}\r\n"
550
+ text = socket.gets
551
+ raise_on_error_response! text
552
+ return nil if text == "NOT_FOUND\r\n"
553
+ return text.to_i
554
+ end
555
+ end
556
+
557
+ ##
558
+ # Gets or creates a socket connected to the given server, and yields it
559
+ # to the block, wrapped in a mutex synchronization if @multithread is true.
560
+ #
561
+ # If a socket error (SocketError, SystemCallError, IOError) or protocol error
562
+ # (MemCacheError) is raised by the block, closes the socket, attempts to
563
+ # connect again, and retries the block (once). If an error is again raised,
564
+ # reraises it as MemCacheError.
565
+ #
566
+ # If unable to connect to the server (or if in the reconnect wait period),
567
+ # raises MemCacheError. Note that the socket connect code marks a server
568
+ # dead for a timeout period, so retrying does not apply to connection attempt
569
+ # failures (but does still apply to unexpectedly lost connections etc.).
570
+
571
+ def with_socket_management(server, &block)
572
+ @mutex.lock if @multithread
573
+ retried = false
574
+
575
+ begin
576
+ socket = server.socket
577
+
578
+ # Raise an IndexError to show this server is out of whack. If were inside
579
+ # a with_server block, we'll catch it and attempt to restart the operation.
580
+
581
+ raise IndexError, "No connection to server (#{server.status})" if socket.nil?
582
+
583
+ block.call(socket)
584
+
585
+ rescue SocketError => err
586
+ server.mark_dead(err.message)
587
+ handle_error(server, err)
588
+
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
+ def raise_on_error_response!(response)
636
+ if response =~ /\A(?:CLIENT_|SERVER_)?ERROR(.*)/
637
+ raise MemCacheError, $1.strip
638
+ end
639
+ end
640
+
641
+ def create_continuum_for(servers)
642
+ total_weight = servers.inject(0) { |memo, srv| memo + srv.weight }
643
+ continuum = []
644
+
645
+ servers.each do |server|
646
+ entry_count_for(server, servers.size, total_weight).times do |idx|
647
+ hash = Digest::SHA1.hexdigest("#{server.host}:#{server.port}:#{idx}")
648
+ value = Integer("0x#{hash[0..7]}")
649
+ continuum << ContinuumEntry.new(value, server)
650
+ end
651
+ end
652
+
653
+ continuum.sort { |a, b| a.value <=> b.value }
654
+ end
655
+
656
+ def entry_count_for(server, total_servers, total_weight)
657
+ ((total_servers * ContinuumEntry::POINTS_PER_SERVER * server.weight) / Float(total_weight)).floor
658
+ end
659
+
660
+ class ContinuumEntry
661
+ POINTS_PER_SERVER = 160 # this is the default in libmemcached
662
+
663
+ attr_reader :value
664
+ attr_reader :server
665
+
666
+ def initialize(val, srv)
667
+ @value = val
668
+ @server = srv
669
+ end
670
+
671
+ def inspect
672
+ "<#{value}, #{server.host}:#{server.port}>"
673
+ end
674
+ end
675
+
676
+ ##
677
+ # This class represents a memcached server instance.
678
+
679
+ class Server
680
+
681
+ ##
682
+ # The amount of time to wait to establish a connection with a memcached
683
+ # server. If a connection cannot be established within this time limit,
684
+ # the server will be marked as down.
685
+
686
+ CONNECT_TIMEOUT = 0.25
687
+
688
+ ##
689
+ # The amount of time to wait for a response from a memcached server.
690
+ # If a response isn't received within this time limit,
691
+ # the server will be marked as down.
692
+
693
+ SOCKET_TIMEOUT = 0.25
694
+
695
+ ##
696
+ # The amount of time to wait before attempting to re-establish a
697
+ # connection with a server that is marked dead.
698
+
699
+ RETRY_DELAY = 30.0
700
+
701
+ ##
702
+ # The host the memcached server is running on.
703
+
704
+ attr_reader :host
705
+
706
+ ##
707
+ # The port the memcached server is listening on.
708
+
709
+ attr_reader :port
710
+
711
+ ##
712
+ # The weight given to the server.
713
+
714
+ attr_reader :weight
715
+
716
+ ##
717
+ # The time of next retry if the connection is dead.
718
+
719
+ attr_reader :retry
720
+
721
+ ##
722
+ # A text status string describing the state of the server.
723
+
724
+ attr_reader :status
725
+
726
+ attr_reader :multithread
727
+
728
+ ##
729
+ # Create a new MemCache::Server object for the memcached instance
730
+ # listening on the given host and port, weighted by the given weight.
731
+
732
+ def initialize(memcache, host, port = DEFAULT_PORT, weight = DEFAULT_WEIGHT)
733
+ raise ArgumentError, "No host specified" if host.nil? or host.empty?
734
+ raise ArgumentError, "No port specified" if port.nil? or port.to_i.zero?
735
+
736
+ @host = host
737
+ @port = port.to_i
738
+ @weight = weight.to_i
739
+
740
+ @multithread = memcache.multithread
741
+ @mutex = Mutex.new
742
+
743
+ @sock = nil
744
+ @retry = nil
745
+ @status = 'NOT CONNECTED'
746
+ end
747
+
748
+ ##
749
+ # Return a string representation of the server object.
750
+
751
+ def inspect
752
+ "<MemCache::Server: %s:%d [%d] (%s)>" % [@host, @port, @weight, @status]
753
+ end
754
+
755
+ ##
756
+ # Check whether the server connection is alive. This will cause the
757
+ # socket to attempt to connect if it isn't already connected and or if
758
+ # the server was previously marked as down and the retry time has
759
+ # been exceeded.
760
+
761
+ def alive?
762
+ !!socket
763
+ end
764
+
765
+ ##
766
+ # Try to connect to the memcached server targeted by this object.
767
+ # Returns the connected socket object on success or nil on failure.
768
+
769
+ def socket
770
+ @mutex.lock if @multithread
771
+ return @sock if @sock and not @sock.closed?
772
+
773
+ @sock = nil
774
+
775
+ # If the host was dead, don't retry for a while.
776
+ return if @retry and @retry > Time.now
777
+
778
+ # Attempt to connect if not already connected.
779
+ begin
780
+
781
+ @sock = TCPTimeoutSocket.new @host, @port
782
+
783
+ if Socket.constants.include? 'TCP_NODELAY' then
784
+ @sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
785
+ end
786
+ @retry = nil
787
+ @status = 'CONNECTED'
788
+ rescue SocketError, SystemCallError, IOError, Timeout::Error => err
789
+ mark_dead err.message
790
+ end
791
+
792
+ return @sock
793
+ ensure
794
+ @mutex.unlock if @multithread
795
+ end
796
+
797
+ ##
798
+ # Close the connection to the memcached server targeted by this
799
+ # object. The server is not considered dead.
800
+
801
+ def close
802
+ @mutex.lock if @multithread
803
+ @sock.close if @sock && !@sock.closed?
804
+ @sock = nil
805
+ @retry = nil
806
+ @status = "NOT CONNECTED"
807
+ ensure
808
+ @mutex.unlock if @multithread
809
+ end
810
+
811
+ ##
812
+ # Mark the server as dead and close its socket.
813
+
814
+ def mark_dead(reason = "Unknown error")
815
+ @sock.close if @sock && !@sock.closed?
816
+ @sock = nil
817
+ @retry = Time.now + RETRY_DELAY
818
+
819
+ @status = sprintf "%s:%s DEAD: %s, will retry at %s", @host, @port, reason, @retry
820
+ end
821
+
822
+ end
823
+
824
+ ##
825
+ # Base MemCache exception class.
826
+
827
+ class MemCacheError < RuntimeError; end
828
+
829
+
830
+ # Find the closest element in Array less than or equal to value.
831
+ def binary_search(ary, value, &block)
832
+ upper = ary.size - 1
833
+ lower = 0
834
+ idx = 0
835
+
836
+ result = while(lower <= upper) do
837
+ idx = (lower + upper) / 2
838
+ comp = block.call(ary[idx]) <=> value
839
+
840
+ if comp == 0
841
+ break idx
842
+ elsif comp > 0
843
+ upper = idx - 1
844
+ else
845
+ lower = idx + 1
846
+ end
847
+ end
848
+ result ? ary[result] : ary[upper]
849
+ end
850
+ end
851
+
852
+ # TCPSocket facade class which implements timeouts.
853
+ class TCPTimeoutSocket
854
+ def initialize(*args)
855
+ Timeout::timeout(MemCache::Server::CONNECT_TIMEOUT, SocketError) do
856
+ @sock = TCPSocket.new(*args)
857
+ @len = MemCache::Server::SOCKET_TIMEOUT
858
+ end
859
+ end
860
+
861
+ def write(*args)
862
+ Timeout::timeout(@len, SocketError) do
863
+ @sock.write(*args)
864
+ end
865
+ end
866
+
867
+ def gets(*args)
868
+ Timeout::timeout(@len, SocketError) do
869
+ @sock.gets(*args)
870
+ end
871
+ end
872
+
873
+ def read(*args)
874
+ Timeout::timeout(@len, SocketError) do
875
+ @sock.read(*args)
876
+ end
877
+ end
878
+
879
+ def _socket
880
+ @sock
881
+ end
882
+
883
+ def method_missing(meth, *args)
884
+ @sock.__send__(meth, *args)
885
+ end
886
+ end