memcache-client 1.6.5 → 1.7.0

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