memcache-client 1.6.5 → 1.7.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.
@@ -1,3 +1,29 @@
1
+ = 1.7.0 (2009-03-08)
2
+
3
+ * Go through the memcached protocol document and implement any commands not already implemented:
4
+ - cas
5
+ - append
6
+ - prepend
7
+ - replace
8
+
9
+ Append and prepend only work with raw data since it makes no sense to concatenate two Marshalled
10
+ values together. The cas functionality should be considered a prototype. Since I don't have an
11
+ application which uses +cas+, I'm not sure what semantic sugar the API should provide. Should it
12
+ retry if the value was changed? Should it massage the returned string into true/false? Feedback
13
+ would be appreciated.
14
+
15
+ * Add fetch method which provides a method very similar to ActiveSupport::Cache::Store#fetch,
16
+ basically a wrapper around get and add. (djanowski)
17
+
18
+ * Implement the flush_all delay parameter, to allow a large memcached farm to be flushed gradually.
19
+
20
+ * Implement the noreply flag, which tells memcached not to reply in operations which don't
21
+ need a reply, i.e. set/add/delete/flush_all.
22
+
23
+ * The only known functionality not implemented anymore is the <flags> parameter to the storage
24
+ commands. This would require modification of the API method signatures. If someone can come
25
+ up with a clean way to implement it, I would be happy to consider including it.
26
+
1
27
  = 1.6.5 (2009-02-27)
2
28
 
3
29
  * Change memcache-client to multithreaded by default. The mutex does not add significant
data/lib/memcache.rb CHANGED
@@ -33,7 +33,7 @@ class MemCache
33
33
  ##
34
34
  # The version of MemCache you are using.
35
35
 
36
- VERSION = '1.6.5'
36
+ VERSION = '1.7.0'
37
37
 
38
38
  ##
39
39
  # Default options for the cache object.
@@ -45,6 +45,7 @@ class MemCache
45
45
  :failover => true,
46
46
  :timeout => 0.5,
47
47
  :logger => nil,
48
+ :no_reply => false,
48
49
  }
49
50
 
50
51
  ##
@@ -89,6 +90,12 @@ class MemCache
89
90
 
90
91
  attr_reader :logger
91
92
 
93
+ ##
94
+ # Don't send or look for a reply from the memcached server for write operations.
95
+ # Please note this feature only works in memcached 1.2.5 and later. Earlier
96
+ # versions will reply with "ERROR".
97
+ attr_reader :no_reply
98
+
92
99
  ##
93
100
  # Accepts a list of +servers+ and a list of +opts+. +servers+ may be
94
101
  # omitted. See +servers=+ for acceptable server list arguments.
@@ -104,6 +111,9 @@ class MemCache
104
111
  # set to nil to disable timeouts (this is a major performance penalty in Ruby 1.8,
105
112
  # "gem install SystemTimer' to remove most of the penalty).
106
113
  # [:logger] Logger to use for info/debug output, defaults to nil
114
+ # [:no_reply] Don't bother looking for a reply for write operations (i.e. they
115
+ # become 'fire and forget'), memcached 1.2.5 and later only, speeds up
116
+ # set/add/delete/incr/decr significantly.
107
117
  #
108
118
  # Other options are ignored.
109
119
 
@@ -134,6 +144,7 @@ class MemCache
134
144
  @timeout = opts[:timeout]
135
145
  @failover = opts[:failover]
136
146
  @logger = opts[:logger]
147
+ @no_reply = opts[:no_reply]
137
148
  @mutex = Mutex.new if @multithread
138
149
 
139
150
  logger.info { "memcache-client #{VERSION} #{Array(servers).inspect}" } if logger
@@ -212,8 +223,8 @@ class MemCache
212
223
 
213
224
  def get(key, raw = false)
214
225
  with_server(key) do |server, cache_key|
226
+ logger.debug { "get #{key} from #{server.inspect}: #{value ? value.to_s.size : 'nil'}" } if logger
215
227
  value = cache_get server, cache_key
216
- logger.debug { "GET #{key} from #{server.inspect}: #{value ? value.to_s.size : 'nil'}" } if logger
217
228
  return nil if value.nil?
218
229
  value = Marshal.load value unless raw
219
230
  return value
@@ -222,6 +233,25 @@ class MemCache
222
233
  handle_error nil, err
223
234
  end
224
235
 
236
+ ##
237
+ # Performs a +get+ with the given +key+. If
238
+ # the value does not exist and a block was given,
239
+ # the block will be called and the result saved via +add+.
240
+ #
241
+ # If you do not provide a block, using this
242
+ # method is the same as using +get+.
243
+ #
244
+ def fetch(key, expiry = 0, raw = false)
245
+ value = get(key, raw)
246
+
247
+ if value.nil? && block_given?
248
+ value = yield
249
+ add(key, value, expiry, raw)
250
+ end
251
+
252
+ value
253
+ end
254
+
225
255
  ##
226
256
  # Retrieves multiple values from memcached in parallel, if possible.
227
257
  #
@@ -303,15 +333,62 @@ class MemCache
303
333
  with_server(key) do |server, cache_key|
304
334
 
305
335
  value = Marshal.dump value unless raw
306
- logger.debug { "SET #{key} to #{server.inspect}: #{value ? value.to_s.size : 'nil'}" } if logger
307
-
308
336
  data = value.to_s
337
+ logger.debug { "set #{key} to #{server.inspect}: #{data.size}" } if logger
338
+
309
339
  raise MemCacheError, "Value too large, memcached can only store 1MB of data per key" if data.size > ONE_MB
310
340
 
311
- command = "set #{cache_key} 0 #{expiry} #{data.size}\r\n#{data}\r\n"
341
+ command = "set #{cache_key} 0 #{expiry} #{data.size}#{noreply}\r\n#{data}\r\n"
312
342
 
313
343
  with_socket_management(server) do |socket|
314
344
  socket.write command
345
+ break nil if @no_reply
346
+ result = socket.gets
347
+ raise_on_error_response! result
348
+
349
+ if result.nil?
350
+ server.close
351
+ raise MemCacheError, "lost connection to #{server.host}:#{server.port}"
352
+ end
353
+
354
+ result
355
+ end
356
+ end
357
+ end
358
+
359
+ ##
360
+ # "cas" is a check and set operation which means "store this data but
361
+ # only if no one else has updated since I last fetched it." This can
362
+ # be used as a form of optimistic locking.
363
+ #
364
+ # Works in block form like so:
365
+ # cache.cas('some-key') do |value|
366
+ # value + 1
367
+ # end
368
+ #
369
+ # Returns:
370
+ # +nil+ if the value was not found on the memcached server.
371
+ # +STORED+ if the value was updated successfully
372
+ # +EXISTS+ if the value was updated by someone else since last fetch
373
+
374
+ def cas(key, expiry=0, raw=false)
375
+ raise MemCacheError, "Update of readonly cache" if @readonly
376
+ raise MemCacheError, "A block is required" unless block_given?
377
+
378
+ (value, token) = gets(key, raw)
379
+ return nil unless value
380
+ updated = yield value
381
+
382
+ with_server(key) do |server, cache_key|
383
+ logger.debug { "cas #{key} to #{server.inspect}: #{data.size}" } if logger
384
+
385
+ value = Marshal.dump updated unless raw
386
+ data = value.to_s
387
+ command = "cas #{cache_key} 0 #{expiry} #{value.size} #{token}#{noreply}\r\n#{value}\r\n"
388
+
389
+ with_socket_management(server) do |socket|
390
+ socket.write command
391
+ break nil if @no_reply
315
392
  result = socket.gets
316
393
  raise_on_error_response! result
317
394
 
@@ -331,17 +408,79 @@ class MemCache
331
408
  # If +raw+ is true, +value+ will not be Marshalled.
332
409
  #
333
410
  # Readers should call this method in the event of a cache miss, not
334
- # MemCache#set or MemCache#[]=.
411
+ # MemCache#set.
335
412
 
336
413
  def add(key, value, expiry = 0, raw = false)
337
414
  raise MemCacheError, "Update of readonly cache" if @readonly
338
415
  with_server(key) do |server, cache_key|
339
416
  value = Marshal.dump value unless raw
340
- logger.debug { "ADD #{key} to #{server}: #{value ? value.to_s.size : 'nil'}" } if logger
341
- command = "add #{cache_key} 0 #{expiry} #{value.to_s.size}\r\n#{value}\r\n"
417
+ logger.debug { "add #{key} to #{server}: #{value ? value.to_s.size : 'nil'}" } if logger
418
+ command = "add #{cache_key} 0 #{expiry} #{value.to_s.size}#{noreply}\r\n#{value}\r\n"
342
419
 
343
420
  with_socket_management(server) do |socket|
344
421
  socket.write command
422
+ break nil if @no_reply
423
+ result = socket.gets
424
+ raise_on_error_response! result
425
+ result
426
+ end
427
+ end
428
+ end
429
+
430
+ ##
431
+ # Add +key+ to the cache with value +value+ that expires in +expiry+
432
+ # seconds, but only if +key+ already exists in the cache.
433
+ # If +raw+ is true, +value+ will not be Marshalled.
434
+ def replace(key, value, expiry = 0, raw = false)
435
+ raise MemCacheError, "Update of readonly cache" if @readonly
436
+ with_server(key) do |server, cache_key|
437
+ value = Marshal.dump value unless raw
438
+ logger.debug { "replace #{key} to #{server}: #{value ? value.to_s.size : 'nil'}" } if logger
439
+ command = "replace #{cache_key} 0 #{expiry} #{value.to_s.size}#{noreply}\r\n#{value}\r\n"
440
+
441
+ with_socket_management(server) do |socket|
442
+ socket.write command
443
+ break nil if @no_reply
444
+ result = socket.gets
445
+ raise_on_error_response! result
446
+ result
447
+ end
448
+ end
449
+ end
450
+
451
+ ##
452
+ # Append - 'add this data to an existing key after existing data'
453
+ # Please note the value is always passed to memcached as raw since it
454
+ # doesn't make a lot of sense to concatenate marshalled data together.
455
+ def append(key, value)
456
+ raise MemCacheError, "Update of readonly cache" if @readonly
457
+ with_server(key) do |server, cache_key|
458
+ logger.debug { "append #{key} to #{server}: #{value ? value.to_s.size : 'nil'}" } if logger
459
+ command = "append #{cache_key} 0 0 #{value.to_s.size}#{noreply}\r\n#{value}\r\n"
460
+
461
+ with_socket_management(server) do |socket|
462
+ socket.write command
463
+ break nil if @no_reply
464
+ result = socket.gets
465
+ raise_on_error_response! result
466
+ result
467
+ end
468
+ end
469
+ end
470
+
471
+ ##
472
+ # Prepend - 'add this data to an existing key before existing data'
473
+ # Please note the value is always passed to memcached as raw since it
474
+ # doesn't make a lot of sense to concatenate marshalled data together.
475
+ def prepend(key, value)
476
+ raise MemCacheError, "Update of readonly cache" if @readonly
477
+ with_server(key) do |server, cache_key|
478
+ logger.debug { "prepend #{key} to #{server}: #{value ? value.to_s.size : 'nil'}" } if logger
479
+ command = "prepend #{cache_key} 0 0 #{value.to_s.size}#{noreply}\r\n#{value}\r\n"
480
+
481
+ with_socket_management(server) do |socket|
482
+ socket.write command
483
+ break nil if @no_reply
345
484
  result = socket.gets
346
485
  raise_on_error_response! result
347
486
  result
@@ -356,7 +495,9 @@ class MemCache
356
495
  raise MemCacheError, "Update of readonly cache" if @readonly
357
496
  with_server(key) do |server, cache_key|
358
497
  with_socket_management(server) do |socket|
359
- socket.write "delete #{cache_key} #{expiry}\r\n"
498
+ logger.debug { "delete #{cache_key} on #{server}" } if logger
499
+ socket.write "delete #{cache_key} #{expiry}#{noreply}\r\n"
500
+ break nil if @no_reply
360
501
  result = socket.gets
361
502
  raise_on_error_response! result
362
503
  result
@@ -366,19 +507,29 @@ class MemCache
366
507
 
367
508
  ##
368
509
  # Flush the cache from all memcache servers.
369
-
370
- def flush_all
510
+ # A non-zero value for +delay+ will ensure that the flush
511
+ # is propogated slowly through your memcached server farm.
512
+ # The Nth server will be flushed N*delay seconds from now,
513
+ # asynchronously so this method returns quickly.
514
+ # This prevents a huge database spike due to a total
515
+ # flush all at once.
516
+
517
+ def flush_all(delay=0)
371
518
  raise MemCacheError, 'No active servers' unless active?
372
519
  raise MemCacheError, "Update of readonly cache" if @readonly
373
520
 
374
521
  begin
522
+ delay_time = 0
375
523
  @servers.each do |server|
376
524
  with_socket_management(server) do |socket|
377
- socket.write "flush_all\r\n"
525
+ logger.debug { "flush_all #{delay_time} on #{server}" } if logger
526
+ socket.write "flush_all #{delay_time}#{noreply}\r\n"
527
+ break nil if @no_reply
378
528
  result = socket.gets
379
529
  raise_on_error_response! result
380
530
  result
381
531
  end
532
+ delay_time += delay
382
533
  end
383
534
  rescue IndexError => err
384
535
  handle_error nil, err
@@ -530,7 +681,8 @@ class MemCache
530
681
 
531
682
  def cache_decr(server, cache_key, amount)
532
683
  with_socket_management(server) do |socket|
533
- socket.write "decr #{cache_key} #{amount}\r\n"
684
+ socket.write "decr #{cache_key} #{amount}#{noreply}\r\n"
685
+ break nil if @no_reply
534
686
  text = socket.gets
535
687
  raise_on_error_response! text
536
688
  return nil if text == "NOT_FOUND\r\n"
@@ -566,6 +718,38 @@ class MemCache
566
718
  end
567
719
  end
568
720
 
721
+ def gets(key, raw = false)
722
+ with_server(key) do |server, cache_key|
723
+ logger.debug { "gets #{key} from #{server.inspect}: #{value ? value.to_s.size : 'nil'}" } if logger
724
+ result = with_socket_management(server) do |socket|
725
+ socket.write "gets #{cache_key}\r\n"
726
+ keyline = socket.gets # "VALUE <key> <flags> <bytes> <cas token>\r\n"
727
+
728
+ if keyline.nil? then
729
+ server.close
730
+ raise MemCacheError, "lost connection to #{server.host}:#{server.port}"
731
+ end
732
+
733
+ raise_on_error_response! keyline
734
+ return nil if keyline == "END\r\n"
735
+
736
+ unless keyline =~ /(\d+) (\w+)\r/ then
737
+ server.close
738
+ raise MemCacheError, "unexpected response #{keyline.inspect}"
739
+ end
740
+ value = socket.read $1.to_i
741
+ socket.read 2 # "\r\n"
742
+ socket.gets # "END\r\n"
743
+ [value, $2]
744
+ end
745
+ result[0] = Marshal.load result[0] unless raw
746
+ result
747
+ end
748
+ rescue TypeError => err
749
+ handle_error nil, err
750
+ end
751
+
752
+
569
753
  ##
570
754
  # Fetches +cache_keys+ from +server+ using a multi-get.
571
755
 
@@ -599,7 +783,8 @@ class MemCache
599
783
 
600
784
  def cache_incr(server, cache_key, amount)
601
785
  with_socket_management(server) do |socket|
602
- socket.write "incr #{cache_key} #{amount}\r\n"
786
+ socket.write "incr #{cache_key} #{amount}#{noreply}\r\n"
787
+ break nil if @no_reply
603
788
  text = socket.gets
604
789
  raise_on_error_response! text
605
790
  return nil if text == "NOT_FOUND\r\n"
@@ -679,6 +864,10 @@ class MemCache
679
864
  raise new_error
680
865
  end
681
866
 
867
+ def noreply
868
+ @no_reply ? ' noreply' : ''
869
+ end
870
+
682
871
  ##
683
872
  # Performs setup for making a request with +key+ from memcached. Returns
684
873
  # the server to fetch the key from and the complete key to use.
@@ -478,6 +478,49 @@ class TestMemCache < Test::Unit::TestCase
478
478
  assert_equal '0123456789', value
479
479
  end
480
480
 
481
+ def test_fetch_without_a_block
482
+ server = FakeServer.new
483
+ server.socket.data.write "END\r\n"
484
+ server.socket.data.rewind
485
+
486
+ @cache.servers = [server]
487
+
488
+ flexmock(@cache).should_receive(:get).with('key', false).and_return(nil)
489
+
490
+ value = @cache.fetch('key', 1)
491
+ assert_equal nil, value
492
+ end
493
+
494
+ def test_fetch_miss
495
+ server = FakeServer.new
496
+ server.socket.data.write "END\r\n"
497
+ server.socket.data.rewind
498
+
499
+ @cache.servers = [server]
500
+
501
+ flexmock(@cache).should_receive(:get).with('key', false).and_return(nil)
502
+ flexmock(@cache).should_receive(:add).with('key', 'value', 1, false)
503
+
504
+ value = @cache.fetch('key', 1) { 'value' }
505
+
506
+ assert_equal 'value', value
507
+ end
508
+
509
+ def test_fetch_hit
510
+ server = FakeServer.new
511
+ server.socket.data.write "END\r\n"
512
+ server.socket.data.rewind
513
+
514
+ @cache.servers = [server]
515
+
516
+ flexmock(@cache).should_receive(:get).with('key', false).and_return('value')
517
+ flexmock(@cache).should_receive(:add).never
518
+
519
+ value = @cache.fetch('key', 1) { raise 'Should not be called.' }
520
+
521
+ assert_equal 'value', value
522
+ end
523
+
481
524
  def test_get_bad_key
482
525
  util_setup_fake_server
483
526
  assert_raise ArgumentError do @cache.get 'k y' end
@@ -752,6 +795,49 @@ class TestMemCache < Test::Unit::TestCase
752
795
  assert_match /object too large for cache/, e.message
753
796
  end
754
797
 
798
+ def test_prepend
799
+ server = FakeServer.new
800
+ server.socket.data.write "STORED\r\n"
801
+ server.socket.data.rewind
802
+ @cache.servers = []
803
+ @cache.servers << server
804
+
805
+ @cache.prepend 'key', 'value'
806
+
807
+ dumped = Marshal.dump('value')
808
+
809
+ expected = "prepend my_namespace:key 0 0 5\r\nvalue\r\n"
810
+ assert_equal expected, server.socket.written.string
811
+ end
812
+
813
+ def test_append
814
+ server = FakeServer.new
815
+ server.socket.data.write "STORED\r\n"
816
+ server.socket.data.rewind
817
+ @cache.servers = []
818
+ @cache.servers << server
819
+
820
+ @cache.append 'key', 'value'
821
+
822
+ expected = "append my_namespace:key 0 0 5\r\nvalue\r\n"
823
+ assert_equal expected, server.socket.written.string
824
+ end
825
+
826
+ def test_replace
827
+ server = FakeServer.new
828
+ server.socket.data.write "STORED\r\n"
829
+ server.socket.data.rewind
830
+ @cache.servers = []
831
+ @cache.servers << server
832
+
833
+ @cache.replace 'key', 'value', 150
834
+
835
+ dumped = Marshal.dump('value')
836
+
837
+ expected = "replace my_namespace:key 0 150 #{dumped.length}\r\n#{dumped}\r\n"
838
+ assert_equal expected, server.socket.written.string
839
+ end
840
+
755
841
  def test_add
756
842
  server = FakeServer.new
757
843
  server.socket.data.write "STORED\r\n"
@@ -859,12 +945,24 @@ class TestMemCache < Test::Unit::TestCase
859
945
 
860
946
  @cache.flush_all
861
947
 
862
- expected = "flush_all\r\n"
948
+ expected = "flush_all 0\r\n"
863
949
  @cache.servers.each do |server|
864
950
  assert_equal expected, server.socket.written.string
865
951
  end
866
952
  end
867
953
 
954
+ def test_flush_all_with_delay
955
+ @cache.servers = []
956
+ 3.times { @cache.servers << FakeServer.new }
957
+
958
+ @cache.flush_all(10)
959
+
960
+ @cache.servers.each_with_index do |server, idx|
961
+ expected = "flush_all #{idx*10}\r\n"
962
+ assert_equal expected, server.socket.written.string
963
+ end
964
+ end
965
+
868
966
  def test_flush_all_failure
869
967
  socket = FakeSocket.new
870
968
 
@@ -881,7 +979,7 @@ class TestMemCache < Test::Unit::TestCase
881
979
  @cache.flush_all
882
980
  end
883
981
 
884
- assert_match /flush_all\r\n/, socket.written.string
982
+ assert_match /flush_all 0\r\n/, socket.written.string
885
983
  end
886
984
 
887
985
  def test_stats
@@ -982,6 +1080,12 @@ class TestMemCache < Test::Unit::TestCase
982
1080
  cache.flush_all
983
1081
  workers = []
984
1082
 
1083
+ cache.set('f', 'zzz')
1084
+ assert_equal "STORED\r\n", (cache.cas('f') do |value|
1085
+ value << 'z'
1086
+ end)
1087
+ assert_equal 'zzzz', cache.get('f')
1088
+
985
1089
  # Have a bunch of threads perform a bunch of operations at the same time.
986
1090
  # Verify the result of each operation to ensure the request and response
987
1091
  # are not intermingled between threads.
@@ -991,6 +1095,14 @@ class TestMemCache < Test::Unit::TestCase
991
1095
  cache.set('a', 9)
992
1096
  cache.set('b', 11)
993
1097
  cache.add('c', 10, 0, true)
1098
+ cache.set('d', 'a', 100, true)
1099
+ cache.set('e', 'x', 100, true)
1100
+ cache.set('f', 'zzz')
1101
+ assert_not_nil(cache.cas('f') do |value|
1102
+ value << 'z'
1103
+ end)
1104
+ cache.append('d', 'b')
1105
+ cache.prepend('e', 'y')
994
1106
  assert_equal "NOT_STORED\r\n", cache.add('a', 11)
995
1107
  assert_equal({ 'a' => 9, 'b' => 11 }, cache.get_multi(['a', 'b']))
996
1108
  inc = cache.incr('c', 10)
@@ -998,6 +1110,10 @@ class TestMemCache < Test::Unit::TestCase
998
1110
  assert inc > 14
999
1111
  assert cache.decr('c', 5) > 14
1000
1112
  assert_equal 11, cache.get('b')
1113
+ d = cache.get('d', true)
1114
+ assert_match /\Aab+\Z/, d
1115
+ e = cache.get('e', true)
1116
+ assert_match /\Ay+x\Z/, e
1001
1117
  end
1002
1118
  end
1003
1119
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: memcache-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.5
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Hodel
@@ -11,7 +11,7 @@ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
13
 
14
- date: 2009-02-27 00:00:00 -06:00
14
+ date: 2009-03-07 23:00:00 -06:00
15
15
  default_executable:
16
16
  dependencies: []
17
17
 
@@ -26,7 +26,7 @@ extra_rdoc_files: []
26
26
  files:
27
27
  - README.rdoc
28
28
  - LICENSE.txt
29
- - History.txt
29
+ - History.rdoc
30
30
  - Rakefile
31
31
  - lib/memcache.rb
32
32
  has_rdoc: false