dalli 2.6.4 → 2.7.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of dalli might be problematic. Click here for more details.

@@ -55,7 +55,7 @@ module Dalli
55
55
  # Chokepoint method for instrumentation
56
56
  def request(op, *args)
57
57
  verify_state
58
- raise Dalli::NetworkError, "#{hostname}:#{port} is down: #{@error} #{@msg}" unless alive?
58
+ raise Dalli::NetworkError, "#{hostname}:#{port} is down: #{@error} #{@msg}. If you are sure it is running, ensure memcached version is > 1.4." unless alive?
59
59
  begin
60
60
  send(op, *args)
61
61
  rescue Dalli::NetworkError
@@ -146,7 +146,7 @@ module Dalli
146
146
 
147
147
  while buf.bytesize - pos >= 24
148
148
  header = buf.slice(pos, 24)
149
- (key_length, _, body_length) = header.unpack(KV_HEADER)
149
+ (key_length, _, body_length, cas) = header.unpack(KV_HEADER)
150
150
 
151
151
  if key_length == 0
152
152
  # all done!
@@ -163,7 +163,7 @@ module Dalli
163
163
  pos = pos + 24 + body_length
164
164
 
165
165
  begin
166
- values[key] = deserialize(value, flags)
166
+ values[key] = [deserialize(value, flags), cas]
167
167
  rescue DalliError
168
168
  end
169
169
 
@@ -175,8 +175,8 @@ module Dalli
175
175
  @position = pos
176
176
 
177
177
  values
178
- rescue SystemCallError, Timeout::Error, EOFError
179
- failure!
178
+ rescue SystemCallError, Timeout::Error, EOFError => e
179
+ failure!(e)
180
180
  end
181
181
 
182
182
  # Abort an earlier #multi_response_start. Used to signal an external
@@ -188,7 +188,7 @@ module Dalli
188
188
  @multi_buffer = nil
189
189
  @position = nil
190
190
  @inprogress = false
191
- failure!
191
+ failure!(RuntimeError.new('External timeout'))
192
192
  rescue NetworkError
193
193
  true
194
194
  end
@@ -198,12 +198,13 @@ module Dalli
198
198
  private
199
199
 
200
200
  def verify_state
201
- failure! if @inprogress
202
- failure! if @pid && @pid != Process.pid
201
+ failure!(RuntimeError.new('Already writing to socket')) if @inprogress
202
+ failure!(RuntimeError.new('Cannot share client between multiple processes')) if @pid && @pid != Process.pid
203
203
  end
204
204
 
205
- def failure!
206
- Dalli.logger.info { "#{hostname}:#{port} failed (count: #{@fail_count})" }
205
+ def failure!(exception)
206
+ message = "#{hostname}:#{port} failed (count: #{@fail_count}) #{exception.class}: #{exception.message}"
207
+ Dalli.logger.info { message }
207
208
 
208
209
  @fail_count += 1
209
210
  if @fail_count >= options[:socket_max_failures]
@@ -271,7 +272,7 @@ module Dalli
271
272
  guard_max_value(key, value) do
272
273
  req = [REQUEST, OPCODES[multi? ? :setq : :set], key.bytesize, 8, 0, 0, value.bytesize + key.bytesize + 8, 0, cas, flags, ttl, key, value].pack(FORMAT[:set])
273
274
  write(req)
274
- generic_response unless multi?
275
+ cas_response unless multi?
275
276
  end
276
277
  end
277
278
 
@@ -281,22 +282,22 @@ module Dalli
281
282
  guard_max_value(key, value) do
282
283
  req = [REQUEST, OPCODES[multi? ? :addq : :add], key.bytesize, 8, 0, 0, value.bytesize + key.bytesize + 8, 0, 0, flags, ttl, key, value].pack(FORMAT[:add])
283
284
  write(req)
284
- generic_response unless multi?
285
+ cas_response unless multi?
285
286
  end
286
287
  end
287
288
 
288
- def replace(key, value, ttl, options)
289
+ def replace(key, value, ttl, cas, options)
289
290
  (value, flags) = serialize(key, value, options)
290
291
 
291
292
  guard_max_value(key, value) do
292
- req = [REQUEST, OPCODES[multi? ? :replaceq : :replace], key.bytesize, 8, 0, 0, value.bytesize + key.bytesize + 8, 0, 0, flags, ttl, key, value].pack(FORMAT[:replace])
293
+ req = [REQUEST, OPCODES[multi? ? :replaceq : :replace], key.bytesize, 8, 0, 0, value.bytesize + key.bytesize + 8, 0, cas, flags, ttl, key, value].pack(FORMAT[:replace])
293
294
  write(req)
294
- generic_response unless multi?
295
+ cas_response unless multi?
295
296
  end
296
297
  end
297
298
 
298
- def delete(key)
299
- req = [REQUEST, OPCODES[multi? ? :deleteq : :delete], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:delete])
299
+ def delete(key, cas)
300
+ req = [REQUEST, OPCODES[multi? ? :deleteq : :delete], key.bytesize, 0, 0, 0, key.bytesize, 0, cas, key].pack(FORMAT[:delete])
300
301
  write(req)
301
302
  generic_response unless multi?
302
303
  end
@@ -368,7 +369,7 @@ module Dalli
368
369
  def cas(key)
369
370
  req = [REQUEST, OPCODES[:get], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:get])
370
371
  write(req)
371
- cas_response
372
+ data_cas_response
372
373
  end
373
374
 
374
375
  def version
@@ -432,7 +433,7 @@ module Dalli
432
433
  raise UnmarshalError, "Unable to uncompress value: #{$!.message}"
433
434
  end
434
435
 
435
- def cas_response
436
+ def data_cas_response
436
437
  header = read(24)
437
438
  raise Dalli::NetworkError, 'No response' if !header
438
439
  (extras, _, status, count, _, cas) = header.unpack(CAS_HEADER)
@@ -451,7 +452,7 @@ module Dalli
451
452
 
452
453
  CAS_HEADER = '@4CCnNNQ'
453
454
  NORMAL_HEADER = '@4CCnN'
454
- KV_HEADER = '@2n@6nN'
455
+ KV_HEADER = '@2n@6nN@16Q'
455
456
 
456
457
  def guard_max_value(key, value)
457
458
  if value.bytesize <= @options[:value_max_bytes]
@@ -482,12 +483,28 @@ module Dalli
482
483
  end
483
484
  end
484
485
 
486
+ def cas_response
487
+ header = read(24)
488
+ raise Dalli::NetworkError, 'No response' if !header
489
+ (_, _, status, count, _, cas) = header.unpack(CAS_HEADER)
490
+ read(count) if count > 0 # this is potential data that we don't care about
491
+ if status == 1
492
+ nil
493
+ elsif status == 2 || status == 5
494
+ false # Not stored, normal status for add operation
495
+ elsif status != 0
496
+ raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}"
497
+ else
498
+ cas
499
+ end
500
+ end
501
+
485
502
  def keyvalue_response
486
503
  hash = {}
487
504
  loop do
488
505
  header = read(24)
489
506
  raise Dalli::NetworkError, 'No response' if !header
490
- (key_length, _, body_length) = header.unpack(KV_HEADER)
507
+ (key_length, _, body_length, _) = header.unpack(KV_HEADER)
491
508
  return hash if key_length == 0
492
509
  key = read(key_length)
493
510
  value = read(body_length - key_length) if body_length - key_length > 0
@@ -500,7 +517,7 @@ module Dalli
500
517
  loop do
501
518
  header = read(24)
502
519
  raise Dalli::NetworkError, 'No response' if !header
503
- (key_length, _, body_length) = header.unpack(KV_HEADER)
520
+ (key_length, _, body_length, _) = header.unpack(KV_HEADER)
504
521
  return hash if key_length == 0
505
522
  flags = read(4).unpack('N')[0]
506
523
  key = read(key_length)
@@ -515,8 +532,8 @@ module Dalli
515
532
  result = @sock.write(bytes)
516
533
  @inprogress = false
517
534
  result
518
- rescue SystemCallError, Timeout::Error
519
- failure!
535
+ rescue SystemCallError, Timeout::Error => e
536
+ failure!(e)
520
537
  end
521
538
  end
522
539
 
@@ -526,8 +543,8 @@ module Dalli
526
543
  data = @sock.readfull(count)
527
544
  @inprogress = false
528
545
  data
529
- rescue SystemCallError, Timeout::Error, EOFError
530
- failure!
546
+ rescue SystemCallError, Timeout::Error, EOFError => e
547
+ failure!(e)
531
548
  end
532
549
  end
533
550
 
@@ -542,9 +559,9 @@ module Dalli
542
559
  up!
543
560
  rescue Dalli::DalliError # SASL auth failure
544
561
  raise
545
- rescue SystemCallError, Timeout::Error, EOFError, SocketError
562
+ rescue SystemCallError, Timeout::Error, EOFError, SocketError => e
546
563
  # SocketError = DNS resolution failure
547
- failure!
564
+ failure!(e)
548
565
  end
549
566
  end
550
567
 
@@ -1,3 +1,3 @@
1
1
  module Dalli
2
- VERSION = '2.6.4'
2
+ VERSION = '2.7.0'
3
3
  end
@@ -21,7 +21,7 @@ describe 'performance' do
21
21
  @counter = 'counter'
22
22
  end
23
23
 
24
- should 'run benchmarks' do
24
+ it 'runs benchmarks' do
25
25
  memcached do
26
26
 
27
27
  Benchmark.bm(37) do |x|
@@ -7,6 +7,8 @@ require 'minitest/autorun'
7
7
  require 'mocha/setup'
8
8
  require 'memcached_mock'
9
9
 
10
+ ENV['MEMCACHED_SASL_PWDB'] = "#{File.dirname(__FILE__)}/sasldb"
11
+
10
12
  WANT_RAILS_VERSION = ENV['RAILS_VERSION'] || '>= 3.0.0'
11
13
  gem 'rails', WANT_RAILS_VERSION
12
14
  require 'rails'
@@ -26,6 +28,19 @@ class MiniTest::Spec
26
28
  assert_match(regexp, ex.message, "#{ex.class.name}: #{ex.message}\n#{ex.backtrace.join("\n\t")}")
27
29
  end
28
30
 
31
+ def op_cas_succeeds(rsp)
32
+ rsp.is_a?(Integer) && rsp > 0
33
+ end
34
+
35
+ def op_replace_succeeds(rsp)
36
+ rsp.is_a?(Integer) && rsp > 0
37
+ end
38
+
39
+ # add and set must have the same return value because of DalliStore#write_entry
40
+ def op_addset_succeeds(rsp)
41
+ rsp.is_a?(Integer) && rsp > 0
42
+ end
43
+
29
44
  def with_activesupport
30
45
  require 'active_support/all'
31
46
  require 'active_support/cache/dalli_store'
@@ -66,6 +66,17 @@ module MemcachedMock
66
66
  end
67
67
 
68
68
  def memcached(port=19122, args='', options={})
69
+ memcached_server(port, args)
70
+ yield Dalli::Client.new(["localhost:#{port}", "127.0.0.1:#{port}"], options)
71
+ end
72
+
73
+ def memcached_cas(port=19122, args='', options={})
74
+ memcached_server(port, args)
75
+ require 'dalli/cas/client'
76
+ yield Dalli::Client.new(["localhost:#{port}", "127.0.0.1:#{port}"], options)
77
+ end
78
+
79
+ def memcached_server(port=19122, args='')
69
80
  Memcached.path ||= find_memcached
70
81
 
71
82
  cmd = "#{Memcached.path}memcached #{args} -p #{port}"
@@ -83,7 +94,6 @@ module MemcachedMock
83
94
  sleep 0.1
84
95
  pid
85
96
  end
86
- yield Dalli::Client.new(["localhost:#{port}", "127.0.0.1:#{port}"], options)
87
97
  end
88
98
 
89
99
  def supports_fork?
@@ -0,0 +1 @@
1
+ testuser:testtest:::::::
@@ -1,5 +1,6 @@
1
1
  # encoding: utf-8
2
2
  require 'helper'
3
+ require 'connection_pool'
3
4
 
4
5
  class MockUser
5
6
  def cache_key
@@ -18,7 +19,7 @@ describe 'ActiveSupport' do
18
19
  it 'allow mute and silence' do
19
20
  @dalli = ActiveSupport::Cache.lookup_store(:dalli_store, 'localhost:19122')
20
21
  @dalli.mute do
21
- assert_equal true, @dalli.write('foo', 'bar', nil)
22
+ assert op_addset_succeeds(@dalli.write('foo', 'bar', nil))
22
23
  assert_equal 'bar', @dalli.read('foo', nil)
23
24
  end
24
25
  refute @dalli.silence?
@@ -28,7 +29,7 @@ describe 'ActiveSupport' do
28
29
 
29
30
  it 'handle nil options' do
30
31
  @dalli = ActiveSupport::Cache.lookup_store(:dalli_store, 'localhost:19122')
31
- assert_equal true, @dalli.write('foo', 'bar', nil)
32
+ assert op_addset_succeeds(@dalli.write('foo', 'bar', nil))
32
33
  assert_equal 'bar', @dalli.read('foo', nil)
33
34
  assert_equal 18, @dalli.fetch('lkjsadlfk', nil) { 18 }
34
35
  assert_equal 18, @dalli.fetch('lkjsadlfk', nil) { 18 }
@@ -147,6 +148,26 @@ describe 'ActiveSupport' do
147
148
  end
148
149
  end
149
150
 
151
+ it 'supports fetch_multi' do
152
+ with_activesupport do
153
+ memcached do
154
+ connect
155
+
156
+ x = rand_key.to_s
157
+ y = rand_key
158
+ hash = { x => 'ABC', y => 'DEF' }
159
+
160
+ @dalli.write(y, '123')
161
+
162
+ results = @dalli.fetch_multi(x, y) { |key| hash[key] }
163
+
164
+ assert_equal({ x => 'ABC', y => '123' }, results)
165
+ assert_equal('ABC', @dalli.read(x))
166
+ assert_equal('123', @dalli.read(y))
167
+ end
168
+ end
169
+ end
170
+
150
171
  it 'support read, write and delete' do
151
172
  with_activesupport do
152
173
  memcached do
@@ -154,7 +175,7 @@ describe 'ActiveSupport' do
154
175
  y = rand_key
155
176
  assert_nil @dalli.read(y)
156
177
  dres = @dalli.write(y, 123)
157
- assert_equal true, dres
178
+ assert op_addset_succeeds(dres)
158
179
 
159
180
  dres = @dalli.read(y)
160
181
  assert_equal 123, dres
@@ -164,7 +185,7 @@ describe 'ActiveSupport' do
164
185
 
165
186
  user = MockUser.new
166
187
  dres = @dalli.write(user.cache_key, "foo")
167
- assert_equal true, dres
188
+ assert op_addset_succeeds(dres)
168
189
 
169
190
  dres = @dalli.read(user)
170
191
  assert_equal "foo", dres
@@ -236,7 +257,7 @@ describe 'ActiveSupport' do
236
257
  with_activesupport do
237
258
  memcached do
238
259
  connect
239
- assert_equal true, @dalli.write('counter', 0, :raw => true)
260
+ assert op_addset_succeeds(@dalli.write('counter', 0, :raw => true))
240
261
  assert_equal 1, @dalli.increment('counter')
241
262
  assert_equal 2, @dalli.increment('counter')
242
263
  assert_equal 1, @dalli.decrement('counter')
@@ -261,7 +282,7 @@ describe 'ActiveSupport' do
261
282
  assert_equal nil, @dalli.read('counterZ2')
262
283
 
263
284
  user = MockUser.new
264
- assert_equal true, @dalli.write(user, 0, :raw => true)
285
+ assert op_addset_succeeds(@dalli.write(user, 0, :raw => true))
265
286
  assert_equal 1, @dalli.increment(user)
266
287
  assert_equal 2, @dalli.increment(user)
267
288
  assert_equal 1, @dalli.decrement(user)
@@ -338,7 +359,7 @@ describe 'ActiveSupport' do
338
359
  connect
339
360
  key = "fooƒ"
340
361
  value = 'bafƒ'
341
- assert_equal true, @dalli.write(key, value)
362
+ assert op_addset_succeeds(@dalli.write(key, value))
342
363
  assert_equal value, @dalli.read(key)
343
364
  end
344
365
  end
@@ -354,13 +375,30 @@ describe 'ActiveSupport' do
354
375
  end
355
376
  end
356
377
 
378
+ it 'supports connection pooling' do
379
+ with_activesupport do
380
+ memcached do
381
+ @dalli = ActiveSupport::Cache::DalliStore.new('localhost:19122', :expires_in => 1, :namespace => 'foo', :compress => true, :pool_size => 3)
382
+ assert_equal nil, @dalli.read('foo')
383
+ assert @dalli.write('foo', 1)
384
+ assert_equal 1, @dalli.fetch('foo') { raise 'boom' }
385
+ assert_equal true, @dalli.dalli.is_a?(ConnectionPool)
386
+ assert_equal 1, @dalli.increment('bar')
387
+ assert_equal 0, @dalli.decrement('bar')
388
+ assert_equal true, @dalli.delete('bar')
389
+ assert_equal [true], @dalli.clear
390
+ assert_equal 1, @dalli.stats.size
391
+ end
392
+ end
393
+ end
394
+
357
395
  it 'allow keys to be frozen' do
358
396
  with_activesupport do
359
397
  memcached do
360
398
  connect
361
399
  key = "foo"
362
400
  key.freeze
363
- assert_equal true, @dalli.write(key, "value")
401
+ assert op_addset_succeeds(@dalli.write(key, "value"))
364
402
  end
365
403
  end
366
404
  end
@@ -371,7 +409,7 @@ describe 'ActiveSupport' do
371
409
  connect
372
410
  map = { "one" => "one", "two" => "two" }
373
411
  map.each_pair do |k, v|
374
- assert_equal true, @dalli.write(k, v)
412
+ assert op_addset_succeeds(@dalli.write(k, v))
375
413
  end
376
414
  assert_equal map, @dalli.read_multi(*(map.keys))
377
415
  end
@@ -0,0 +1,107 @@
1
+ require 'helper'
2
+ require 'memcached_mock'
3
+
4
+ describe 'Dalli::Cas::Client' do
5
+ describe 'using a live server' do
6
+ it 'supports get with CAS' do
7
+ memcached_cas do |dc|
8
+ dc.flush
9
+
10
+ expected = { 'blah' => 'blerg!' }
11
+ get_block_called = false
12
+ stored_value = stored_cas = nil
13
+ # Validate call-with-block
14
+ dc.get_cas('gets_key') do |v, cas|
15
+ get_block_called = true
16
+ stored_value = v
17
+ stored_cas = cas
18
+ end
19
+ assert get_block_called
20
+ assert_nil stored_value
21
+
22
+ dc.set('gets_key', expected)
23
+
24
+ # Validate call-with-return-value
25
+ stored_value, stored_cas = dc.get_cas('gets_key')
26
+ assert_equal stored_value, expected
27
+ assert(stored_cas != 0)
28
+ end
29
+ end
30
+
31
+ it 'supports multi-get with CAS' do
32
+ memcached_cas do |dc|
33
+ dc.close
34
+ dc.flush
35
+
36
+ expected_hash = {'a' => 'foo', 'b' => 123}
37
+ expected_hash.each_pair do |k, v|
38
+ dc.set(k, v)
39
+ end
40
+
41
+ # Invocation without block
42
+ resp = dc.get_multi_cas(%w(a b c d e f))
43
+ resp.each_pair do |k, data|
44
+ value, cas = [data.first, data.second]
45
+ assert_equal expected_hash[k], value
46
+ assert(cas && cas != 0)
47
+ end
48
+
49
+ # Invocation with block
50
+ dc.get_multi_cas(%w(a b c d e f)) do |k, data|
51
+ value, cas = [data.first, data.second]
52
+ assert_equal expected_hash[k], value
53
+ assert(cas && cas != 0)
54
+ end
55
+ end
56
+ end
57
+
58
+ it 'supports replace-with-CAS operation' do
59
+ memcached_cas do |dc|
60
+ dc.flush
61
+ cas = dc.set('key', 'value')
62
+
63
+ # Accepts CAS, replaces, and returns new CAS
64
+ cas = dc.replace_cas('key', 'value2', cas)
65
+ assert cas.is_a?(Integer)
66
+
67
+ assert_equal 'value2', dc.get('key')
68
+ end
69
+ end
70
+
71
+ it 'supports delete with CAS' do
72
+ memcached_cas do |dc|
73
+ cas = dc.set('some_key', 'some_value')
74
+ dc.delete_cas('some_key', cas)
75
+ assert_nil dc.get('some_key')
76
+ end
77
+ end
78
+
79
+ it 'handles CAS round-trip operations' do
80
+ memcached_cas do |dc|
81
+ dc.flush
82
+
83
+ expected = {'blah' => 'blerg!'}
84
+ dc.set('some_key', expected)
85
+
86
+ value, cas = dc.get_cas('some_key')
87
+ assert_equal value, expected
88
+ assert(!cas.nil? && cas != 0)
89
+
90
+ # Set operation, first with wrong then with correct CAS
91
+ expected = {'blah' => 'set succeeded'}
92
+ assert(dc.set_cas('some_key', expected, cas+1) == false)
93
+ assert op_addset_succeeds(cas = dc.set_cas('some_key', expected, cas))
94
+
95
+ # Replace operation, first with wrong then with correct CAS
96
+ expected = {'blah' => 'replace succeeded'}
97
+ assert(dc.replace_cas('some_key', expected, cas+1) == false)
98
+ assert op_addset_succeeds(cas = dc.replace_cas('some_key', expected, cas))
99
+
100
+ # Delete operation, first with wrong then with correct CAS
101
+ assert(dc.delete_cas('some_key', cas+1) == false)
102
+ assert dc.delete_cas('some_key', cas)
103
+ end
104
+ end
105
+
106
+ end
107
+ end