jsierles-memcache-client 1.5.0.5

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