dalli 1.1.4 → 2.7.11

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.

@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ require 'zlib'
3
+ require 'stringio'
4
+
5
+ module Dalli
6
+ class Compressor
7
+ def self.compress(data)
8
+ Zlib::Deflate.deflate(data)
9
+ end
10
+
11
+ def self.decompress(data)
12
+ Zlib::Inflate.inflate(data)
13
+ end
14
+ end
15
+
16
+ class GzipCompressor
17
+ def self.compress(data)
18
+ io = StringIO.new(String.new(""), "w")
19
+ gz = Zlib::GzipWriter.new(io)
20
+ gz.write(data)
21
+ gz.close
22
+ io.string
23
+ end
24
+
25
+ def self.decompress(data)
26
+ io = StringIO.new(data, "rb")
27
+ Zlib::GzipReader.new(io).read
28
+ end
29
+ end
30
+ end
data/lib/dalli/options.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'thread'
2
3
  require 'monitor'
3
4
 
@@ -31,6 +32,24 @@ module Dalli
31
32
  end
32
33
  end
33
34
 
35
+ def multi_response_start
36
+ @lock.synchronize do
37
+ super
38
+ end
39
+ end
40
+
41
+ def multi_response_nonblock
42
+ @lock.synchronize do
43
+ super
44
+ end
45
+ end
46
+
47
+ def multi_response_abort
48
+ @lock.synchronize do
49
+ super
50
+ end
51
+ end
52
+
34
53
  def lock!
35
54
  @lock.mon_enter
36
55
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+ module Dalli
3
+ class Railtie < ::Rails::Railtie
4
+ config.before_configuration do
5
+ config.cache_store = :dalli_store
6
+ end
7
+ end
8
+ end
data/lib/dalli/ring.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'digest/sha1'
2
3
  require 'zlib'
3
4
 
@@ -15,12 +16,12 @@ module Dalli
15
16
  continuum = []
16
17
  servers.each do |server|
17
18
  entry_count_for(server, servers.size, total_weight).times do |idx|
18
- hash = Digest::SHA1.hexdigest("#{server.hostname}:#{server.port}:#{idx}")
19
+ hash = Digest::SHA1.hexdigest("#{server.name}:#{idx}")
19
20
  value = Integer("0x#{hash[0..7]}")
20
21
  continuum << Dalli::Ring::Entry.new(value, server)
21
22
  end
22
23
  end
23
- @continuum = continuum.sort { |a, b| a.value <=> b.value }
24
+ @continuum = continuum.sort_by(&:value)
24
25
  end
25
26
 
26
27
  threadsafe! unless options[:threadsafe] == false
@@ -31,7 +32,7 @@ module Dalli
31
32
  if @continuum
32
33
  hkey = hash_for(key)
33
34
  20.times do |try|
34
- entryidx = self.class.binary_search(@continuum, hkey)
35
+ entryidx = binary_search(@continuum, hkey)
35
36
  server = @continuum[entryidx].server
36
37
  return server if server.alive?
37
38
  break unless @failover
@@ -46,11 +47,11 @@ module Dalli
46
47
  end
47
48
 
48
49
  def lock
49
- @servers.each { |s| s.lock! }
50
+ @servers.each(&:lock!)
50
51
  begin
51
52
  return yield
52
53
  ensure
53
- @servers.each { |s| s.unlock! }
54
+ @servers.each(&:unlock!)
54
55
  end
55
56
  end
56
57
 
@@ -70,25 +71,61 @@ module Dalli
70
71
  ((total_servers * POINTS_PER_SERVER * server.weight) / Float(total_weight)).floor
71
72
  end
72
73
 
73
- # Find the closest index in the Ring with value <= the given value
74
- def self.binary_search(ary, value)
75
- upper = ary.size - 1
76
- lower = 0
77
- idx = 0
78
-
79
- while (lower <= upper) do
80
- idx = (lower + upper) / 2
81
- comp = ary[idx].value <=> value
82
-
83
- if comp == 0
84
- return idx
85
- elsif comp > 0
86
- upper = idx - 1
87
- else
88
- lower = idx + 1
74
+ # Native extension to perform the binary search within the continuum
75
+ # space. Fallback to a pure Ruby version if the compilation doesn't work.
76
+ # optional for performance and only necessary if you are using multiple
77
+ # memcached servers.
78
+ begin
79
+ require 'inline'
80
+ inline do |builder|
81
+ builder.c <<-EOM
82
+ int binary_search(VALUE ary, unsigned int r) {
83
+ long upper = RARRAY_LEN(ary) - 1;
84
+ long lower = 0;
85
+ long idx = 0;
86
+ ID value = rb_intern("value");
87
+ VALUE continuumValue;
88
+ unsigned int l;
89
+
90
+ while (lower <= upper) {
91
+ idx = (lower + upper) / 2;
92
+
93
+ continuumValue = rb_funcall(RARRAY_PTR(ary)[idx], value, 0);
94
+ l = NUM2UINT(continuumValue);
95
+ if (l == r) {
96
+ return idx;
97
+ }
98
+ else if (l > r) {
99
+ upper = idx - 1;
100
+ }
101
+ else {
102
+ lower = idx + 1;
103
+ }
104
+ }
105
+ return upper;
106
+ }
107
+ EOM
108
+ end
109
+ rescue LoadError
110
+ # Find the closest index in the Ring with value <= the given value
111
+ def binary_search(ary, value)
112
+ upper = ary.size - 1
113
+ lower = 0
114
+
115
+ while (lower <= upper) do
116
+ idx = (lower + upper) / 2
117
+ comp = ary[idx].value <=> value
118
+
119
+ if comp == 0
120
+ return idx
121
+ elsif comp > 0
122
+ upper = idx - 1
123
+ else
124
+ lower = idx + 1
125
+ end
89
126
  end
127
+ upper
90
128
  end
91
- return upper
92
129
  end
93
130
 
94
131
  class Entry
data/lib/dalli/server.rb CHANGED
@@ -1,6 +1,6 @@
1
+ # frozen_string_literal: true
1
2
  require 'socket'
2
3
  require 'timeout'
3
- require 'zlib'
4
4
 
5
5
  module Dalli
6
6
  class Server
@@ -8,10 +8,14 @@ module Dalli
8
8
  attr_accessor :port
9
9
  attr_accessor :weight
10
10
  attr_accessor :options
11
+ attr_reader :sock
12
+ attr_reader :socket_type # possible values: :unix, :tcp
11
13
 
14
+ DEFAULT_PORT = 11211
15
+ DEFAULT_WEIGHT = 1
12
16
  DEFAULTS = {
13
17
  # seconds between trying to contact a remote server
14
- :down_retry_delay => 1,
18
+ :down_retry_delay => 60,
15
19
  # connect/read/write timeout for socket operations
16
20
  :socket_timeout => 0.5,
17
21
  # times a socket operation may fail before considering the server dead
@@ -20,42 +24,59 @@ module Dalli
20
24
  :socket_failure_delay => 0.01,
21
25
  # max size of value in bytes (default is 1 MB, can be overriden with "memcached -I <size>")
22
26
  :value_max_bytes => 1024 * 1024,
27
+ # surpassing value_max_bytes either warns (false) or throws (true)
28
+ :error_when_over_max_size => false,
29
+ :compressor => Compressor,
30
+ # min byte size to attempt compression
31
+ :compression_min_size => 1024,
32
+ # max byte size for compression
33
+ :compression_max_size => false,
34
+ :serializer => Marshal,
23
35
  :username => nil,
24
36
  :password => nil,
25
- :async => false,
37
+ :keepalive => true,
38
+ # max byte size for SO_SNDBUF
39
+ :sndbuf => nil,
40
+ # max byte size for SO_RCVBUF
41
+ :rcvbuf => nil
26
42
  }
27
43
 
28
44
  def initialize(attribs, options = {})
29
- (@hostname, @port, @weight) = attribs.split(':')
30
- @port ||= 11211
31
- @port = Integer(@port)
32
- @weight ||= 1
33
- @weight = Integer(@weight)
45
+ @hostname, @port, @weight, @socket_type = parse_hostname(attribs)
34
46
  @fail_count = 0
35
47
  @down_at = nil
36
48
  @last_down_at = nil
37
49
  @options = DEFAULTS.merge(options)
38
50
  @sock = nil
39
51
  @msg = nil
52
+ @error = nil
53
+ @pid = nil
54
+ @inprogress = nil
55
+ end
56
+
57
+ def name
58
+ if socket_type == :unix
59
+ hostname
60
+ else
61
+ "#{hostname}:#{port}"
62
+ end
40
63
  end
41
64
 
42
65
  # Chokepoint method for instrumentation
43
66
  def request(op, *args)
44
- raise Dalli::NetworkError, "#{hostname}:#{port} is down: #{@error} #{@msg}" unless alive?
67
+ verify_state
68
+ raise Dalli::NetworkError, "#{name} is down: #{@error} #{@msg}. If you are sure it is running, ensure memcached version is > 1.4." unless alive?
45
69
  begin
46
70
  send(op, *args)
47
- rescue Dalli::NetworkError
48
- raise
49
71
  rescue Dalli::MarshalError => ex
50
72
  Dalli.logger.error "Marshalling error for key '#{args.first}': #{ex.message}"
51
73
  Dalli.logger.error "You are trying to cache a Ruby object which cannot be serialized to memcached."
52
74
  Dalli.logger.error ex.backtrace.join("\n\t")
53
75
  false
54
- rescue Dalli::DalliError
76
+ rescue Dalli::DalliError, Dalli::NetworkError, Dalli::ValueOverMaxSize, Timeout::Error
55
77
  raise
56
78
  rescue => ex
57
- Dalli.logger.error "Unexpected exception in Dalli: #{ex.class.name}: #{ex.message}"
58
- Dalli.logger.error "This is a bug in Dalli, please enter an issue in Github if it does not already exist."
79
+ Dalli.logger.error "Unexpected exception during Dalli request: #{ex.class.name}: #{ex.message}"
59
80
  Dalli.logger.error ex.backtrace.join("\n\t")
60
81
  down!
61
82
  end
@@ -66,7 +87,7 @@ module Dalli
66
87
 
67
88
  if @last_down_at && @last_down_at + options[:down_retry_delay] >= Time.now
68
89
  time = @last_down_at + options[:down_retry_delay] - Time.now
69
- Dalli.logger.debug { "down_retry_delay not reached for #{hostname}:#{port} (%.3f seconds left)" % time }
90
+ Dalli.logger.debug { "down_retry_delay not reached for #{name} (%.3f seconds left)" % time }
70
91
  return false
71
92
  end
72
93
 
@@ -80,6 +101,8 @@ module Dalli
80
101
  return unless @sock
81
102
  @sock.close rescue nil
82
103
  @sock = nil
104
+ @pid = nil
105
+ @inprogress = false
83
106
  end
84
107
 
85
108
  def lock!
@@ -88,24 +111,123 @@ module Dalli
88
111
  def unlock!
89
112
  end
90
113
 
114
+ def serializer
115
+ @options[:serializer]
116
+ end
117
+
118
+ def compressor
119
+ @options[:compressor]
120
+ end
121
+
122
+ # Start reading key/value pairs from this connection. This is usually called
123
+ # after a series of GETKQ commands. A NOOP is sent, and the server begins
124
+ # flushing responses for kv pairs that were found.
125
+ #
126
+ # Returns nothing.
127
+ def multi_response_start
128
+ verify_state
129
+ write_noop
130
+ @multi_buffer = String.new('')
131
+ @position = 0
132
+ @inprogress = true
133
+ end
134
+
135
+ # Did the last call to #multi_response_start complete successfully?
136
+ def multi_response_completed?
137
+ @multi_buffer.nil?
138
+ end
139
+
140
+ # Attempt to receive and parse as many key/value pairs as possible
141
+ # from this server. After #multi_response_start, this should be invoked
142
+ # repeatedly whenever this server's socket is readable until
143
+ # #multi_response_completed?.
144
+ #
145
+ # Returns a Hash of kv pairs received.
146
+ def multi_response_nonblock
147
+ raise 'multi_response has completed' if @multi_buffer.nil?
148
+
149
+ @multi_buffer << @sock.read_available
150
+ buf = @multi_buffer
151
+ pos = @position
152
+ values = {}
153
+
154
+ while buf.bytesize - pos >= 24
155
+ header = buf.slice(pos, 24)
156
+ (key_length, _, body_length, cas) = header.unpack(KV_HEADER)
157
+
158
+ if key_length == 0
159
+ # all done!
160
+ @multi_buffer = nil
161
+ @position = nil
162
+ @inprogress = false
163
+ break
164
+
165
+ elsif buf.bytesize - pos >= 24 + body_length
166
+ flags = buf.slice(pos + 24, 4).unpack('N')[0]
167
+ key = buf.slice(pos + 24 + 4, key_length)
168
+ value = buf.slice(pos + 24 + 4 + key_length, body_length - key_length - 4) if body_length - key_length - 4 > 0
169
+
170
+ pos = pos + 24 + body_length
171
+
172
+ begin
173
+ values[key] = [deserialize(value, flags), cas]
174
+ rescue DalliError
175
+ end
176
+
177
+ else
178
+ # not enough data yet, wait for more
179
+ break
180
+ end
181
+ end
182
+ @position = pos
183
+
184
+ values
185
+ rescue SystemCallError, Timeout::Error, EOFError => e
186
+ failure!(e)
187
+ end
188
+
189
+ # Abort an earlier #multi_response_start. Used to signal an external
190
+ # timeout. The underlying socket is disconnected, and the exception is
191
+ # swallowed.
192
+ #
193
+ # Returns nothing.
194
+ def multi_response_abort
195
+ @multi_buffer = nil
196
+ @position = nil
197
+ @inprogress = false
198
+ failure!(RuntimeError.new('External timeout'))
199
+ rescue NetworkError
200
+ true
201
+ end
202
+
91
203
  # NOTE: Additional public methods should be overridden in Dalli::Threadsafe
92
204
 
93
205
  private
94
206
 
95
- def is_unix_socket?(string)
96
- !!(/^\/(.+)$/ =~ string)
207
+ def verify_state
208
+ failure!(RuntimeError.new('Already writing to socket')) if @inprogress
209
+ if @pid && @pid != Process.pid
210
+ message = 'Fork detected, re-connecting child process...'
211
+ Dalli.logger.info { message }
212
+ reconnect! message
213
+ end
97
214
  end
98
215
 
99
- def failure!
100
- Dalli.logger.info { "#{hostname}:#{port} failed (count: #{@fail_count})" }
216
+ def reconnect!(message)
217
+ close
218
+ sleep(options[:socket_failure_delay]) if options[:socket_failure_delay]
219
+ raise Dalli::NetworkError, message
220
+ end
221
+
222
+ def failure!(exception)
223
+ message = "#{name} failed (count: #{@fail_count}) #{exception.class}: #{exception.message}"
224
+ Dalli.logger.warn { message }
101
225
 
102
226
  @fail_count += 1
103
227
  if @fail_count >= options[:socket_max_failures]
104
228
  down!
105
229
  else
106
- close
107
- sleep(options[:socket_failure_delay]) if options[:socket_failure_delay]
108
- raise Dalli::NetworkError, "Socket operation failed, retrying..."
230
+ reconnect! 'Socket operation failed, retrying...'
109
231
  end
110
232
  end
111
233
 
@@ -116,21 +238,21 @@ module Dalli
116
238
 
117
239
  if @down_at
118
240
  time = Time.now - @down_at
119
- Dalli.logger.debug { "#{hostname}:#{port} is still down (for %.3f seconds now)" % time }
241
+ Dalli.logger.debug { "#{name} is still down (for %.3f seconds now)" % time }
120
242
  else
121
243
  @down_at = @last_down_at
122
- Dalli.logger.warn { "#{hostname}:#{port} is down" }
244
+ Dalli.logger.warn { "#{name} is down" }
123
245
  end
124
246
 
125
247
  @error = $! && $!.class.name
126
248
  @msg = @msg || ($! && $!.message && !$!.message.empty? && $!.message)
127
- raise Dalli::NetworkError, "#{hostname}:#{port} is down: #{@error} #{@msg}"
249
+ raise Dalli::NetworkError, "#{name} is down: #{@error} #{@msg}"
128
250
  end
129
251
 
130
252
  def up!
131
253
  if @down_at
132
254
  time = Time.now - @down_at
133
- Dalli.logger.warn { "#{hostname}:#{port} is back (downtime was %.3f seconds)" % time }
255
+ Dalli.logger.warn { "#{name} is back (downtime was %.3f seconds)" % time }
134
256
  end
135
257
 
136
258
  @fail_count = 0
@@ -144,42 +266,56 @@ module Dalli
144
266
  Thread.current[:dalli_multi]
145
267
  end
146
268
 
147
- def get(key)
269
+ def get(key, options=nil)
148
270
  req = [REQUEST, OPCODES[:get], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:get])
149
271
  write(req)
150
- generic_response(true)
272
+ generic_response(true, !!(options && options.is_a?(Hash) && options[:cache_nils]))
151
273
  end
152
274
 
153
- def getkq(key)
154
- req = [REQUEST, OPCODES[:getkq], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:getkq])
275
+ def send_multiget(keys)
276
+ req = String.new("")
277
+ keys.each do |key|
278
+ req << [REQUEST, OPCODES[:getkq], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:getkq])
279
+ end
280
+ # Could send noop here instead of in multi_response_start
155
281
  write(req)
156
282
  end
157
283
 
158
284
  def set(key, value, ttl, cas, options)
159
285
  (value, flags) = serialize(key, value, options)
286
+ ttl = sanitize_ttl(ttl)
160
287
 
161
- 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])
162
- write(req)
163
- generic_response unless multi?
288
+ guard_max_value(key, value) do
289
+ 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])
290
+ write(req)
291
+ cas_response unless multi?
292
+ end
164
293
  end
165
294
 
166
295
  def add(key, value, ttl, options)
167
296
  (value, flags) = serialize(key, value, options)
297
+ ttl = sanitize_ttl(ttl)
168
298
 
169
- 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])
170
- write(req)
171
- generic_response unless multi?
299
+ guard_max_value(key, value) do
300
+ 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])
301
+ write(req)
302
+ cas_response unless multi?
303
+ end
172
304
  end
173
305
 
174
- def replace(key, value, ttl, options)
306
+ def replace(key, value, ttl, cas, options)
175
307
  (value, flags) = serialize(key, value, options)
176
- 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])
177
- write(req)
178
- generic_response unless multi?
308
+ ttl = sanitize_ttl(ttl)
309
+
310
+ guard_max_value(key, value) do
311
+ 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])
312
+ write(req)
313
+ cas_response unless multi?
314
+ end
179
315
  end
180
316
 
181
- def delete(key)
182
- req = [REQUEST, OPCODES[multi? ? :deleteq : :delete], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:delete])
317
+ def delete(key, cas)
318
+ req = [REQUEST, OPCODES[multi? ? :deleteq : :delete], key.bytesize, 0, 0, 0, key.bytesize, 0, cas, key].pack(FORMAT[:delete])
183
319
  write(req)
184
320
  generic_response unless multi?
185
321
  end
@@ -190,46 +326,52 @@ module Dalli
190
326
  generic_response
191
327
  end
192
328
 
193
- def decr(key, count, ttl, default)
194
- expiry = default ? ttl : 0xFFFFFFFF
329
+ def decr_incr(opcode, key, count, ttl, default)
330
+ expiry = default ? sanitize_ttl(ttl) : 0xFFFFFFFF
195
331
  default ||= 0
196
332
  (h, l) = split(count)
197
333
  (dh, dl) = split(default)
198
- req = [REQUEST, OPCODES[:decr], key.bytesize, 20, 0, 0, key.bytesize + 20, 0, 0, h, l, dh, dl, expiry, key].pack(FORMAT[:decr])
334
+ req = [REQUEST, OPCODES[opcode], key.bytesize, 20, 0, 0, key.bytesize + 20, 0, 0, h, l, dh, dl, expiry, key].pack(FORMAT[opcode])
199
335
  write(req)
200
336
  body = generic_response
201
- body ? longlong(*body.unpack('NN')) : body
337
+ body ? body.unpack('Q>').first : body
338
+ end
339
+
340
+ def decr(key, count, ttl, default)
341
+ decr_incr :decr, key, count, ttl, default
202
342
  end
203
343
 
204
344
  def incr(key, count, ttl, default)
205
- expiry = default ? ttl : 0xFFFFFFFF
206
- default ||= 0
207
- (h, l) = split(count)
208
- (dh, dl) = split(default)
209
- req = [REQUEST, OPCODES[:incr], key.bytesize, 20, 0, 0, key.bytesize + 20, 0, 0, h, l, dh, dl, expiry, key].pack(FORMAT[:incr])
345
+ decr_incr :incr, key, count, ttl, default
346
+ end
347
+
348
+ def write_append_prepend(opcode, key, value)
349
+ write_generic [REQUEST, OPCODES[opcode], key.bytesize, 0, 0, 0, value.bytesize + key.bytesize, 0, 0, key, value].pack(FORMAT[opcode])
350
+ end
351
+
352
+ def write_generic(bytes)
353
+ write(bytes)
354
+ generic_response
355
+ end
356
+
357
+ def write_noop
358
+ req = [REQUEST, OPCODES[:noop], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
210
359
  write(req)
211
- body = generic_response
212
- body ? longlong(*body.unpack('NN')) : body
213
360
  end
214
361
 
215
362
  # Noop is a keepalive operation but also used to demarcate the end of a set of pipelined commands.
216
363
  # We need to read all the responses at once.
217
364
  def noop
218
- req = [REQUEST, OPCODES[:noop], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
219
- write(req)
365
+ write_noop
220
366
  multi_response
221
367
  end
222
368
 
223
369
  def append(key, value)
224
- req = [REQUEST, OPCODES[:append], key.bytesize, 0, 0, 0, value.bytesize + key.bytesize, 0, 0, key, value].pack(FORMAT[:append])
225
- write(req)
226
- generic_response
370
+ write_append_prepend :append, key, value
227
371
  end
228
372
 
229
373
  def prepend(key, value)
230
- req = [REQUEST, OPCODES[:prepend], key.bytesize, 0, 0, 0, value.bytesize + key.bytesize, 0, 0, key, value].pack(FORMAT[:prepend])
231
- write(req)
232
- generic_response
374
+ write_append_prepend :prepend, key, value
233
375
  end
234
376
 
235
377
  def stats(info='')
@@ -238,24 +380,29 @@ module Dalli
238
380
  keyvalue_response
239
381
  end
240
382
 
383
+ def reset_stats
384
+ write_generic [REQUEST, OPCODES[:stat], 'reset'.bytesize, 0, 0, 0, 'reset'.bytesize, 0, 0, 'reset'].pack(FORMAT[:stat])
385
+ end
386
+
241
387
  def cas(key)
242
388
  req = [REQUEST, OPCODES[:get], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:get])
243
389
  write(req)
244
- cas_response
390
+ data_cas_response
245
391
  end
246
392
 
247
393
  def version
248
- req = [REQUEST, OPCODES[:version], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
249
- write(req)
250
- generic_response
394
+ write_generic [REQUEST, OPCODES[:version], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
251
395
  end
252
396
 
253
- COMPRESSION_MIN_SIZE = 1024
397
+ def touch(key, ttl)
398
+ ttl = sanitize_ttl(ttl)
399
+ write_generic [REQUEST, OPCODES[:touch], key.bytesize, 4, 0, 0, key.bytesize + 4, 0, 0, ttl, key].pack(FORMAT[:touch])
400
+ end
254
401
 
255
402
  # http://www.hjp.at/zettel/m/memcached_flags.rxml
256
403
  # Looks like most clients use bit 0 to indicate native language serialization
257
404
  # and bit 1 to indicate gzip compression.
258
- FLAG_MARSHALLED = 0x1
405
+ FLAG_SERIALIZED = 0x1
259
406
  FLAG_COMPRESSED = 0x2
260
407
 
261
408
  def serialize(key, value, options=nil)
@@ -263,7 +410,9 @@ module Dalli
263
410
  value = unless options && options[:raw]
264
411
  marshalled = true
265
412
  begin
266
- Marshal.dump(value)
413
+ self.serializer.dump(value)
414
+ rescue Timeout::Error => e
415
+ raise e
267
416
  rescue => ex
268
417
  # Marshalling can throw several different types of generic Ruby exceptions.
269
418
  # Convert to a specific exception so we can special case it higher up the stack.
@@ -275,31 +424,38 @@ module Dalli
275
424
  value.to_s
276
425
  end
277
426
  compressed = false
278
- if @options[:compression] && value.bytesize >= COMPRESSION_MIN_SIZE
279
- value = Zlib::Deflate.deflate(value)
427
+ set_compress_option = true if options && options[:compress]
428
+ if (@options[:compress] || set_compress_option) && value.bytesize >= @options[:compression_min_size] &&
429
+ (!@options[:compression_max_size] || value.bytesize <= @options[:compression_max_size])
430
+ value = self.compressor.compress(value)
280
431
  compressed = true
281
432
  end
282
- raise Dalli::DalliError, "Value too large, memcached can only store #{@options[:value_max_bytes]} bytes per key [key: #{key}, size: #{value.bytesize}]" if value.bytesize > @options[:value_max_bytes]
433
+
283
434
  flags = 0
284
435
  flags |= FLAG_COMPRESSED if compressed
285
- flags |= FLAG_MARSHALLED if marshalled
436
+ flags |= FLAG_SERIALIZED if marshalled
286
437
  [value, flags]
287
438
  end
288
439
 
289
440
  def deserialize(value, flags)
290
- value = Zlib::Inflate.inflate(value) if (flags & FLAG_COMPRESSED) != 0
291
- value = Marshal.load(value) if (flags & FLAG_MARSHALLED) != 0
441
+ value = self.compressor.decompress(value) if (flags & FLAG_COMPRESSED) != 0
442
+ value = self.serializer.load(value) if (flags & FLAG_SERIALIZED) != 0
292
443
  value
293
- rescue TypeError, ArgumentError
294
- raise DalliError, "Unable to unmarshal value: #{$!.message}"
444
+ rescue TypeError
445
+ raise if $!.message !~ /needs to have method `_load'|exception class\/object expected|instance of IO needed|incompatible marshal file format/
446
+ raise UnmarshalError, "Unable to unmarshal value: #{$!.message}"
447
+ rescue ArgumentError
448
+ raise if $!.message !~ /undefined class|marshal data too short/
449
+ raise UnmarshalError, "Unable to unmarshal value: #{$!.message}"
450
+ rescue NameError
451
+ raise if $!.message !~ /uninitialized constant/
452
+ raise UnmarshalError, "Unable to unmarshal value: #{$!.message}"
295
453
  rescue Zlib::Error
296
- raise DalliError, "Unable to uncompress value: #{$!.message}"
454
+ raise UnmarshalError, "Unable to uncompress value: #{$!.message}"
297
455
  end
298
456
 
299
- def cas_response
300
- header = read(24)
301
- raise Dalli::NetworkError, 'No response' if !header
302
- (extras, type, status, count, _, cas) = header.unpack(CAS_HEADER)
457
+ def data_cas_response
458
+ (extras, _, status, count, _, cas) = read_header.unpack(CAS_HEADER)
303
459
  data = read(count) if count > 0
304
460
  if status == 1
305
461
  nil
@@ -315,15 +471,41 @@ module Dalli
315
471
 
316
472
  CAS_HEADER = '@4CCnNNQ'
317
473
  NORMAL_HEADER = '@4CCnN'
318
- KV_HEADER = '@2n@6nN'
474
+ KV_HEADER = '@2n@6nN@16Q'
319
475
 
320
- def generic_response(unpack=false)
321
- header = read(24)
322
- raise Dalli::NetworkError, 'No response' if !header
323
- (extras, type, status, count) = header.unpack(NORMAL_HEADER)
476
+ def guard_max_value(key, value)
477
+ if value.bytesize <= @options[:value_max_bytes]
478
+ yield
479
+ else
480
+ message = "Value for #{key} over max size: #{@options[:value_max_bytes]} <= #{value.bytesize}"
481
+ raise Dalli::ValueOverMaxSize, message if @options[:error_when_over_max_size]
482
+
483
+ Dalli.logger.error "#{message} - this value may be truncated by memcached"
484
+ false
485
+ end
486
+ end
487
+
488
+ # https://github.com/memcached/memcached/blob/master/doc/protocol.txt#L79
489
+ # > An expiration time, in seconds. Can be up to 30 days. After 30 days, is treated as a unix timestamp of an exact date.
490
+ MAX_ACCEPTABLE_EXPIRATION_INTERVAL = 30*24*60*60 # 30 days
491
+ def sanitize_ttl(ttl)
492
+ ttl_as_i = ttl.to_i
493
+ return ttl_as_i if ttl_as_i <= MAX_ACCEPTABLE_EXPIRATION_INTERVAL
494
+ now = Time.now.to_i
495
+ return ttl_as_i if ttl_as_i > now # already a timestamp
496
+ Dalli.logger.debug "Expiration interval (#{ttl_as_i}) too long for Memcached, converting to an expiration timestamp"
497
+ now + ttl_as_i
498
+ end
499
+
500
+ # Implements the NullObject pattern to store an application-defined value for 'Key not found' responses.
501
+ class NilObject; end
502
+ NOT_FOUND = NilObject.new
503
+
504
+ def generic_response(unpack=false, cache_nils=false)
505
+ (extras, _, status, count) = read_header.unpack(NORMAL_HEADER)
324
506
  data = read(count) if count > 0
325
507
  if status == 1
326
- nil
508
+ cache_nils ? NOT_FOUND : nil
327
509
  elsif status == 2 || status == 5
328
510
  false # Not stored, normal status for add operation
329
511
  elsif status != 0
@@ -337,12 +519,24 @@ module Dalli
337
519
  end
338
520
  end
339
521
 
522
+ def cas_response
523
+ (_, _, status, count, _, cas) = read_header.unpack(CAS_HEADER)
524
+ read(count) if count > 0 # this is potential data that we don't care about
525
+ if status == 1
526
+ nil
527
+ elsif status == 2 || status == 5
528
+ false # Not stored, normal status for add operation
529
+ elsif status != 0
530
+ raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}"
531
+ else
532
+ cas
533
+ end
534
+ end
535
+
340
536
  def keyvalue_response
341
537
  hash = {}
342
- loop do
343
- header = read(24)
344
- raise Dalli::NetworkError, 'No response' if !header
345
- (key_length, status, body_length) = header.unpack(KV_HEADER)
538
+ while true
539
+ (key_length, _, body_length, _) = read_header.unpack(KV_HEADER)
346
540
  return hash if key_length == 0
347
541
  key = read(key_length)
348
542
  value = read(body_length - key_length) if body_length - key_length > 0
@@ -352,10 +546,8 @@ module Dalli
352
546
 
353
547
  def multi_response
354
548
  hash = {}
355
- loop do
356
- header = read(24)
357
- raise Dalli::NetworkError, 'No response' if !header
358
- (key_length, status, body_length) = header.unpack(KV_HEADER)
549
+ while true
550
+ (key_length, _, body_length, _) = read_header.unpack(KV_HEADER)
359
551
  return hash if key_length == 0
360
552
  flags = read(4).unpack('N')[0]
361
553
  key = read(key_length)
@@ -366,43 +558,48 @@ module Dalli
366
558
 
367
559
  def write(bytes)
368
560
  begin
369
- @sock.write(bytes)
370
- rescue SystemCallError, Timeout::Error
371
- failure!
372
- retry
561
+ @inprogress = true
562
+ result = @sock.write(bytes)
563
+ @inprogress = false
564
+ result
565
+ rescue SystemCallError, Timeout::Error => e
566
+ failure!(e)
373
567
  end
374
568
  end
375
569
 
376
570
  def read(count)
377
571
  begin
378
- @sock.readfull(count)
379
- rescue SystemCallError, Timeout::Error, EOFError
380
- failure!
381
- retry
572
+ @inprogress = true
573
+ data = @sock.readfull(count)
574
+ @inprogress = false
575
+ data
576
+ rescue SystemCallError, Timeout::Error, EOFError => e
577
+ failure!(e)
382
578
  end
383
579
  end
384
580
 
581
+ def read_header
582
+ read(24) || raise(Dalli::NetworkError, 'No response')
583
+ end
584
+
385
585
  def connect
386
- Dalli.logger.debug { "Dalli::Server#connect #{hostname}:#{port}" }
586
+ Dalli.logger.debug { "Dalli::Server#connect #{name}" }
387
587
 
388
588
  begin
389
- if @hostname =~ /^\//
390
- @sock = USocket.new(hostname)
391
- elsif options[:async]
392
- raise Dalli::DalliError, "EM support not enabled, as em-synchrony is not installed." if not defined?(AsyncSocket)
393
- @sock = AsyncSocket.open(hostname, port, :timeout => options[:socket_timeout])
589
+ @pid = Process.pid
590
+ if socket_type == :unix
591
+ @sock = KSocket::UNIX.open(hostname, self, options)
394
592
  else
395
- @sock = KSocket.open(hostname, port, :timeout => options[:socket_timeout])
593
+ @sock = KSocket::TCP.open(hostname, port, self, options)
396
594
  end
397
- @version = version # trigger actual connect
398
595
  sasl_authentication if need_auth?
596
+ @version = version # trigger actual connect
399
597
  up!
400
598
  rescue Dalli::DalliError # SASL auth failure
401
599
  raise
402
- rescue SystemCallError, Timeout::Error, EOFError, SocketError
600
+ rescue SystemCallError, Timeout::Error, EOFError, SocketError => e
403
601
  # SocketError = DNS resolution failure
404
- failure!
405
- retry
602
+ failure!(e)
406
603
  end
407
604
  end
408
605
 
@@ -410,13 +607,11 @@ module Dalli
410
607
  [n >> 32, 0xFFFFFFFF & n]
411
608
  end
412
609
 
413
- def longlong(a, b)
414
- (a << 32) | b
415
- end
416
-
417
610
  REQUEST = 0x80
418
611
  RESPONSE = 0x81
419
612
 
613
+ # Response codes taken from:
614
+ # https://github.com/memcached/memcached/wiki/BinaryProtocolRevamped#response-status
420
615
  RESPONSE_CODES = {
421
616
  0 => 'No error',
422
617
  1 => 'Key not found',
@@ -425,9 +620,16 @@ module Dalli
425
620
  4 => 'Invalid arguments',
426
621
  5 => 'Item not stored',
427
622
  6 => 'Incr/decr on a non-numeric value',
623
+ 7 => 'The vbucket belongs to another server',
624
+ 8 => 'Authentication error',
625
+ 9 => 'Authentication continue',
428
626
  0x20 => 'Authentication required',
429
627
  0x81 => 'Unknown command',
430
628
  0x82 => 'Out of memory',
629
+ 0x83 => 'Not supported',
630
+ 0x84 => 'Internal error',
631
+ 0x85 => 'Busy',
632
+ 0x86 => 'Temporary failure'
431
633
  }
432
634
 
433
635
  OPCODES = {
@@ -454,6 +656,7 @@ module Dalli
454
656
  :auth_negotiation => 0x20,
455
657
  :auth_request => 0x21,
456
658
  :auth_continue => 0x22,
659
+ :touch => 0x1C,
457
660
  }
458
661
 
459
662
  HEADER = "CCnCCnNNQ"
@@ -474,6 +677,7 @@ module Dalli
474
677
  :prepend => 'a*a*',
475
678
  :auth_request => 'a*a*',
476
679
  :auth_continue => 'a*a*',
680
+ :touch => 'Na*',
477
681
  }
478
682
  FORMAT = OP_FORMAT.inject({}) { |memo, (k, v)| memo[k] = HEADER + v; memo }
479
683
 
@@ -500,11 +704,10 @@ module Dalli
500
704
  # negotiate
501
705
  req = [REQUEST, OPCODES[:auth_negotiation], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
502
706
  write(req)
503
- header = read(24)
504
- raise Dalli::NetworkError, 'No response' if !header
505
- (extras, type, status, count) = header.unpack(NORMAL_HEADER)
707
+
708
+ (extras, _type, status, count) = read_header.unpack(NORMAL_HEADER)
506
709
  raise Dalli::NetworkError, "Unexpected message format: #{extras} #{count}" unless extras == 0 && count > 0
507
- content = read(count)
710
+ content = read(count).gsub(/\u0000/, ' ')
508
711
  return (Dalli.logger.debug("Authentication not required/supported by server")) if status == 0x81
509
712
  mechanisms = content.split(' ')
510
713
  raise NotImplementedError, "Dalli only supports the PLAIN authentication mechanism" if !mechanisms.include?('PLAIN')
@@ -515,9 +718,7 @@ module Dalli
515
718
  req = [REQUEST, OPCODES[:auth_request], mechanism.bytesize, 0, 0, 0, mechanism.bytesize + msg.bytesize, 0, 0, mechanism, msg].pack(FORMAT[:auth_request])
516
719
  write(req)
517
720
 
518
- header = read(24)
519
- raise Dalli::NetworkError, 'No response' if !header
520
- (extras, type, status, count) = header.unpack(NORMAL_HEADER)
721
+ (extras, _type, status, count) = read_header.unpack(NORMAL_HEADER)
521
722
  raise Dalli::NetworkError, "Unexpected message format: #{extras} #{count}" unless extras == 0 && count > 0
522
723
  content = read(count)
523
724
  return Dalli.logger.info("Dalli/SASL: #{content}") if status == 0
@@ -527,5 +728,25 @@ module Dalli
527
728
  # (step, msg) = sasl.receive('challenge', content)
528
729
  # raise Dalli::NetworkError, "Authentication failed" if sasl.failed? || step != 'response'
529
730
  end
731
+
732
+ def parse_hostname(str)
733
+ res = str.match(/\A(\[([\h:]+)\]|[^:]+)(?::(\d+))?(?::(\d+))?\z/)
734
+ raise Dalli::DalliError, "Could not parse hostname #{str}" if res.nil? || res[1] == '[]'
735
+ hostnam = res[2] || res[1]
736
+ if hostnam =~ /\A\//
737
+ socket_type = :unix
738
+ # in case of unix socket, allow only setting of weight, not port
739
+ raise Dalli::DalliError, "Could not parse hostname #{str}" if res[4]
740
+ weigh = res[3]
741
+ else
742
+ socket_type = :tcp
743
+ por = res[3] || DEFAULT_PORT
744
+ por = Integer(por)
745
+ weigh = res[4]
746
+ end
747
+ weigh ||= DEFAULT_WEIGHT
748
+ weigh = Integer(weigh)
749
+ return hostnam, por, weigh, socket_type
750
+ end
530
751
  end
531
752
  end