dalli 5.0.5 → 5.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +172 -0
- data/README.md +6 -1
- data/lib/dalli/client.rb +274 -48
- data/lib/dalli/options.rb +1 -1
- data/lib/dalli/pipelined_deleter.rb +41 -17
- data/lib/dalli/pipelined_getter.rb +57 -8
- data/lib/dalli/pipelined_setter.rb +15 -3
- data/lib/dalli/protocol/base.rb +62 -5
- data/lib/dalli/protocol/connection_manager.rb +34 -10
- data/lib/dalli/protocol/key_regularizer.rb +25 -9
- data/lib/dalli/protocol/meta.rb +100 -90
- data/lib/dalli/protocol/request_formatter.rb +166 -27
- data/lib/dalli/protocol/response_buffer.rb +21 -22
- data/lib/dalli/protocol/response_processor.rb +64 -17
- data/lib/dalli/socket.rb +47 -32
- data/lib/dalli/version.rb +2 -2
- metadata +2 -3
- data/lib/dalli/pid_cache.rb +0 -40
|
@@ -5,16 +5,16 @@ require 'timeout'
|
|
|
5
5
|
|
|
6
6
|
module Dalli
|
|
7
7
|
module Protocol
|
|
8
|
+
# Compact the buffer when the consumed portion exceeds this
|
|
9
|
+
# threshold and represents more than half the buffer
|
|
10
|
+
COMPACT_THRESHOLD = 4096
|
|
11
|
+
|
|
8
12
|
##
|
|
9
13
|
# Manages the buffer for responses from memcached.
|
|
10
14
|
# Uses an offset-based approach to avoid string allocations
|
|
11
15
|
# when advancing through parsed responses.
|
|
12
16
|
##
|
|
13
17
|
class ResponseBuffer
|
|
14
|
-
# Compact the buffer when the consumed portion exceeds this
|
|
15
|
-
# threshold and represents more than half the buffer
|
|
16
|
-
COMPACT_THRESHOLD = 4096
|
|
17
|
-
|
|
18
18
|
def initialize(io_source, response_processor)
|
|
19
19
|
@io_source = io_source
|
|
20
20
|
@response_processor = response_processor
|
|
@@ -23,22 +23,32 @@ module Dalli
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def read
|
|
26
|
-
@buffer
|
|
26
|
+
remaining_bytes = @buffer.bytesize - @offset
|
|
27
|
+
if remaining_bytes.zero?
|
|
28
|
+
@offset = 0
|
|
29
|
+
@buffer = @io_source.read_available(@buffer)
|
|
30
|
+
else
|
|
31
|
+
if @offset > COMPACT_THRESHOLD && @offset > (@buffer.bytesize / 2)
|
|
32
|
+
@buffer.bytesplice(0, @offset, '')
|
|
33
|
+
@offset = 0
|
|
34
|
+
end
|
|
35
|
+
@buffer << @io_source.read_available
|
|
36
|
+
end
|
|
27
37
|
end
|
|
28
38
|
|
|
29
39
|
# Attempts to process a single response from the buffer,
|
|
30
40
|
# advancing the offset past the consumed bytes.
|
|
31
41
|
def process_single_getk_response
|
|
32
|
-
|
|
33
|
-
@offset +=
|
|
34
|
-
|
|
35
|
-
[status, cas, key, value]
|
|
42
|
+
response = @response_processor.getk_response_from_buffer(@buffer, @offset)
|
|
43
|
+
@offset += response.pop
|
|
44
|
+
response
|
|
36
45
|
end
|
|
37
46
|
|
|
38
47
|
# Resets the internal buffer to an empty state,
|
|
39
48
|
# so that we're ready to read pipelined responses
|
|
40
49
|
def reset
|
|
41
|
-
@buffer
|
|
50
|
+
@buffer&.clear
|
|
51
|
+
@buffer ||= ''.b
|
|
42
52
|
@offset = 0
|
|
43
53
|
end
|
|
44
54
|
|
|
@@ -54,6 +64,7 @@ module Dalli
|
|
|
54
64
|
|
|
55
65
|
# Clear the internal response buffer
|
|
56
66
|
def clear
|
|
67
|
+
@buffer&.clear
|
|
57
68
|
@buffer = nil
|
|
58
69
|
@offset = 0
|
|
59
70
|
end
|
|
@@ -61,18 +72,6 @@ module Dalli
|
|
|
61
72
|
def in_progress?
|
|
62
73
|
!@buffer.nil?
|
|
63
74
|
end
|
|
64
|
-
|
|
65
|
-
private
|
|
66
|
-
|
|
67
|
-
# Only compact when we've consumed a significant portion of the buffer.
|
|
68
|
-
# This avoids per-response string allocation while preventing unbounded
|
|
69
|
-
# memory growth for large pipelines.
|
|
70
|
-
def compact_if_needed
|
|
71
|
-
return unless @offset > COMPACT_THRESHOLD && @offset > @buffer.bytesize / 2
|
|
72
|
-
|
|
73
|
-
@buffer = @buffer.byteslice(@offset..)
|
|
74
|
-
@offset = 0
|
|
75
|
-
end
|
|
76
75
|
end
|
|
77
76
|
end
|
|
78
77
|
end
|
|
@@ -72,11 +72,13 @@ module Dalli
|
|
|
72
72
|
#
|
|
73
73
|
# Used by meta_get for comprehensive metadata retrieval.
|
|
74
74
|
# Supports thundering herd protection (N/R flags) and metadata flags (h/l/u).
|
|
75
|
-
def meta_get_with_metadata(cache_nils: false, return_hit_status: false, return_last_access: false
|
|
75
|
+
def meta_get_with_metadata(cache_nils: false, return_hit_status: false, return_last_access: false,
|
|
76
|
+
return_ttl_remaining: false)
|
|
76
77
|
tokens = error_on_unexpected!(T_VA_EN_HD)
|
|
77
78
|
result = build_metadata_result(tokens)
|
|
78
79
|
result[:hit_before] = hit_status_from_tokens(tokens) if return_hit_status
|
|
79
80
|
result[:last_access] = last_access_from_tokens(tokens) if return_last_access
|
|
81
|
+
result[:ttl_remaining] = ttl_remaining_from_tokens(tokens) if return_ttl_remaining
|
|
80
82
|
result[:value] = parse_value_from_tokens(tokens, cache_nils)
|
|
81
83
|
result
|
|
82
84
|
end
|
|
@@ -85,7 +87,12 @@ module Dalli
|
|
|
85
87
|
{
|
|
86
88
|
value: nil, cas: cas_from_tokens(tokens),
|
|
87
89
|
won_recache: tokens.include?('W'), stale: tokens.include?('X'),
|
|
88
|
-
lost_recache: tokens.include?('Z')
|
|
90
|
+
lost_recache: tokens.include?('Z'),
|
|
91
|
+
# Explicit miss marker: EN means the key does not exist. A tombstoned
|
|
92
|
+
# item is NOT a miss -- it answers VA/HD with the X flag set -- and a
|
|
93
|
+
# stored nil under cache_nils is not one either, so neither can be
|
|
94
|
+
# inferred from value or cas alone.
|
|
95
|
+
miss: tokens.first == EN
|
|
89
96
|
}
|
|
90
97
|
end
|
|
91
98
|
|
|
@@ -157,9 +164,26 @@ module Dalli
|
|
|
157
164
|
true
|
|
158
165
|
end
|
|
159
166
|
|
|
167
|
+
# Consumes the responses to a batch of quiet (pipelined) delete
|
|
168
|
+
# requests, which are terminated by a noop (MN). In quiet mode
|
|
169
|
+
# memcached suppresses the success response for each deleted key, so
|
|
170
|
+
# every line received before the terminator corresponds to a key that
|
|
171
|
+
# was NOT deleted -- a miss (NF) or an error. Returns that count so
|
|
172
|
+
# callers can derive the number of successful deletes as
|
|
173
|
+
# (keys_sent - non_deletions).
|
|
174
|
+
def pipelined_delete_non_deletions
|
|
175
|
+
non_deletions = 0
|
|
176
|
+
tokens = next_line_to_tokens
|
|
177
|
+
until tokens.first == MN
|
|
178
|
+
non_deletions += 1
|
|
179
|
+
tokens = next_line_to_tokens
|
|
180
|
+
end
|
|
181
|
+
non_deletions
|
|
182
|
+
end
|
|
183
|
+
|
|
160
184
|
def full_response_from_buffer(tokens, body, resp_size)
|
|
161
185
|
value = @value_marshaller.retrieve(body, bitflags_from_tokens(tokens))
|
|
162
|
-
[
|
|
186
|
+
[tokens.first == VA, cas_from_tokens(tokens), key_from_tokens(tokens), value, resp_size]
|
|
163
187
|
end
|
|
164
188
|
|
|
165
189
|
##
|
|
@@ -174,24 +198,26 @@ module Dalli
|
|
|
174
198
|
##
|
|
175
199
|
def getk_response_from_buffer(buf, offset = 0)
|
|
176
200
|
# Find the header terminator starting from offset
|
|
177
|
-
term_idx = buf.
|
|
178
|
-
return [0
|
|
201
|
+
term_idx = buf.byteindex(TERMINATOR, offset)
|
|
202
|
+
return [0] unless term_idx
|
|
179
203
|
|
|
180
204
|
header = buf.byteslice(offset, term_idx - offset)
|
|
181
205
|
tokens = header.split
|
|
182
206
|
header_len = header.bytesize + TERMINATOR.length
|
|
207
|
+
|
|
208
|
+
# The body len is removed from the tokens array
|
|
183
209
|
body_len = body_len_from_tokens(tokens)
|
|
184
210
|
|
|
185
211
|
# We have a complete response that has no body.
|
|
186
212
|
# This is either the response to the terminating
|
|
187
213
|
# noop or, if the status is not MN, an intermediate
|
|
188
214
|
# error response that needs to be discarded.
|
|
189
|
-
return [
|
|
215
|
+
return [true, header_len] if body_len.zero?
|
|
190
216
|
|
|
191
217
|
resp_size = header_len + body_len + TERMINATOR.length
|
|
192
218
|
# The header is in the buffer, but the body is not. As we don't have
|
|
193
219
|
# a complete response, don't advance the buffer
|
|
194
|
-
return [0
|
|
220
|
+
return [0] unless buf.bytesize >= offset + resp_size
|
|
195
221
|
|
|
196
222
|
# The full response is in our buffer, so parse it and return
|
|
197
223
|
# the values
|
|
@@ -210,17 +236,28 @@ module Dalli
|
|
|
210
236
|
end
|
|
211
237
|
|
|
212
238
|
def bitflags_from_tokens(tokens)
|
|
213
|
-
value_from_tokens(tokens, 'f')
|
|
239
|
+
value_from_tokens(tokens, 'f').to_i
|
|
214
240
|
end
|
|
215
241
|
|
|
216
242
|
def cas_from_tokens(tokens)
|
|
217
|
-
value_from_tokens(tokens, 'c')
|
|
243
|
+
value_from_tokens(tokens, 'c').to_i
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Detects the X presence flag, set when an item has been marked stale by a
|
|
247
|
+
# prior `md key I`. Uses strict equality (Array#any? with a String pattern
|
|
248
|
+
# compares with ==) so a future value-bearing flag beginning with X cannot
|
|
249
|
+
# be mistaken for it.
|
|
250
|
+
def stale_from_tokens(tokens)
|
|
251
|
+
tokens.any?('X')
|
|
218
252
|
end
|
|
219
253
|
|
|
220
254
|
def key_from_tokens(tokens)
|
|
221
255
|
encoded_key = value_from_tokens(tokens, 'k')
|
|
222
|
-
|
|
223
|
-
|
|
256
|
+
if tokens.delete('b')
|
|
257
|
+
KeyRegularizer.decode(encoded_key)
|
|
258
|
+
else
|
|
259
|
+
encoded_key
|
|
260
|
+
end
|
|
224
261
|
end
|
|
225
262
|
|
|
226
263
|
# Returns true if item was previously hit, false if first access, nil if not requested
|
|
@@ -235,18 +272,28 @@ module Dalli
|
|
|
235
272
|
# Returns seconds since last access, or nil if not requested
|
|
236
273
|
# The l flag returns l<seconds>
|
|
237
274
|
def last_access_from_tokens(tokens)
|
|
238
|
-
value_from_tokens(tokens, 'l')
|
|
275
|
+
value_from_tokens(tokens, 'l').to_i
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Returns seconds of TTL remaining; -1 when the item has no expiry.
|
|
279
|
+
# The t flag returns t<seconds>.
|
|
280
|
+
def ttl_remaining_from_tokens(tokens)
|
|
281
|
+
value_from_tokens(tokens, 't').to_i
|
|
239
282
|
end
|
|
240
283
|
|
|
241
284
|
def body_len_from_tokens(tokens)
|
|
242
|
-
value_from_tokens(tokens, 's')
|
|
285
|
+
value_from_tokens(tokens, 's').to_i
|
|
243
286
|
end
|
|
244
287
|
|
|
245
288
|
def value_from_tokens(tokens, flag)
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
289
|
+
# NB: as an optimization, we're mutating the matching token in place
|
|
290
|
+
# so there is a baked assumption that we're only accessing each token at most once.
|
|
291
|
+
index = tokens.find_index { |t| t.start_with?(flag) }
|
|
292
|
+
if index
|
|
293
|
+
tokens.delete_at(index).delete_prefix!(flag)
|
|
294
|
+
else
|
|
295
|
+
0
|
|
296
|
+
end
|
|
250
297
|
end
|
|
251
298
|
|
|
252
299
|
def read_line
|
data/lib/dalli/socket.rb
CHANGED
|
@@ -12,48 +12,63 @@ module Dalli
|
|
|
12
12
|
# Common methods for all socket implementations.
|
|
13
13
|
##
|
|
14
14
|
module InstanceMethods
|
|
15
|
-
def
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
def read_available(reusable_buffer = nil)
|
|
16
|
+
if reusable_buffer
|
|
17
|
+
value = read_nonblock(8196, reusable_buffer, exception: false)
|
|
18
|
+
case value
|
|
19
|
+
when :wait_writable, :wait_readable
|
|
20
|
+
return reusable_buffer.clear
|
|
21
|
+
when nil
|
|
22
|
+
raise Errno::ECONNRESET, "Connection reset: #{logged_options.inspect}"
|
|
23
|
+
end
|
|
24
|
+
else
|
|
25
|
+
value = ''.b
|
|
21
26
|
end
|
|
22
|
-
value
|
|
23
|
-
end
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
value = +''
|
|
28
|
+
buffer = ''.b
|
|
27
29
|
loop do
|
|
28
|
-
result = read_nonblock(8196, exception: false)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
result = read_nonblock(8196, buffer, exception: false)
|
|
31
|
+
case result
|
|
32
|
+
when :wait_writable, :wait_readable
|
|
33
|
+
buffer.clear
|
|
34
|
+
return value
|
|
35
|
+
when nil
|
|
36
|
+
raise Errno::ECONNRESET, "Connection reset: #{logged_options.inspect}"
|
|
37
|
+
else
|
|
38
|
+
value << result
|
|
39
|
+
end
|
|
33
40
|
end
|
|
34
|
-
value
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
WAIT_RCS = %i[wait_writable wait_readable].freeze
|
|
38
|
-
|
|
39
|
-
def append_to_buffer?(result)
|
|
40
|
-
raise Timeout::Error, "IO timeout: #{logged_options.inspect}" if nonblock_timed_out?(result)
|
|
41
|
-
raise Errno::ECONNRESET, "Connection reset: #{logged_options.inspect}" unless result
|
|
42
|
-
|
|
43
|
-
!WAIT_RCS.include?(result)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def nonblock_timed_out?(result)
|
|
47
|
-
return true if result == :wait_readable && !wait_readable(options[:socket_timeout])
|
|
48
|
-
|
|
49
|
-
# TODO: Do we actually need this? Looks to be only used in read_nonblock
|
|
50
|
-
result == :wait_writable && !wait_writable(options[:socket_timeout])
|
|
51
41
|
end
|
|
52
42
|
|
|
53
43
|
FILTERED_OUT_OPTIONS = %i[username password].freeze
|
|
54
44
|
def logged_options
|
|
55
45
|
options.except(*FILTERED_OUT_OPTIONS)
|
|
56
46
|
end
|
|
47
|
+
|
|
48
|
+
# JRuby doesn't support IO#timeout=, so use custom readfull implementation
|
|
49
|
+
# CRuby 3.3+ has IO#timeout= which makes IO#read work with timeouts
|
|
50
|
+
if RUBY_ENGINE == 'jruby'
|
|
51
|
+
def readfull(count)
|
|
52
|
+
value = String.new(capacity: count + 1)
|
|
53
|
+
|
|
54
|
+
until value.bytesize == count
|
|
55
|
+
result = read_nonblock(count - value.bytesize, exception: false)
|
|
56
|
+
case result
|
|
57
|
+
when :wait_readable
|
|
58
|
+
wait_readable(options[:socket_timeout]) or raise Timeout::Error, "IO timeout: #{logged_options.inspect}"
|
|
59
|
+
when :wait_writable
|
|
60
|
+
wait_writable(options[:socket_timeout]) or raise Timeout::Error, "IO timeout: #{logged_options.inspect}"
|
|
61
|
+
when nil
|
|
62
|
+
raise Errno::ECONNRESET, "Connection reset: #{logged_options.inspect}"
|
|
63
|
+
else
|
|
64
|
+
value << result
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
value
|
|
69
|
+
end
|
|
70
|
+
# rubocop:enable Metrics/AbcSize
|
|
71
|
+
end
|
|
57
72
|
end
|
|
58
73
|
|
|
59
74
|
##
|
data/lib/dalli/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dalli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.0
|
|
4
|
+
version: 5.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter M. Goldstein
|
|
@@ -44,7 +44,6 @@ files:
|
|
|
44
44
|
- lib/dalli/instrumentation.rb
|
|
45
45
|
- lib/dalli/key_manager.rb
|
|
46
46
|
- lib/dalli/options.rb
|
|
47
|
-
- lib/dalli/pid_cache.rb
|
|
48
47
|
- lib/dalli/pipelined_deleter.rb
|
|
49
48
|
- lib/dalli/pipelined_getter.rb
|
|
50
49
|
- lib/dalli/pipelined_setter.rb
|
|
@@ -88,7 +87,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
88
87
|
- !ruby/object:Gem::Version
|
|
89
88
|
version: '0'
|
|
90
89
|
requirements: []
|
|
91
|
-
rubygems_version: 4.0.
|
|
90
|
+
rubygems_version: 4.0.18
|
|
92
91
|
specification_version: 4
|
|
93
92
|
summary: High performance memcached client for Ruby
|
|
94
93
|
test_files: []
|
data/lib/dalli/pid_cache.rb
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Dalli
|
|
4
|
-
##
|
|
5
|
-
# Dalli::PIDCache is a wrapper class for PID checking to avoid system calls when checking the PID.
|
|
6
|
-
##
|
|
7
|
-
module PIDCache
|
|
8
|
-
if !Process.respond_to?(:fork) # JRuby or TruffleRuby
|
|
9
|
-
@pid = Process.pid
|
|
10
|
-
singleton_class.attr_reader(:pid)
|
|
11
|
-
elsif Process.respond_to?(:_fork) # Ruby 3.1+
|
|
12
|
-
class << self
|
|
13
|
-
attr_reader :pid
|
|
14
|
-
|
|
15
|
-
def update!
|
|
16
|
-
@pid = Process.pid # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
update!
|
|
20
|
-
|
|
21
|
-
##
|
|
22
|
-
# Dalli::PIDCache::CoreExt hooks into Process to be able to reset the PID cache after fork
|
|
23
|
-
##
|
|
24
|
-
module CoreExt
|
|
25
|
-
def _fork
|
|
26
|
-
child_pid = super
|
|
27
|
-
PIDCache.update! if child_pid.zero?
|
|
28
|
-
child_pid
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
Process.singleton_class.prepend(CoreExt)
|
|
32
|
-
else # Ruby 3.0 or older
|
|
33
|
-
class << self
|
|
34
|
-
def pid
|
|
35
|
-
Process.pid
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|