dalli 5.0.4 → 5.0.6
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 +137 -0
- data/lib/dalli/client.rb +23 -7
- data/lib/dalli/options.rb +1 -1
- data/lib/dalli/pipelined_deleter.rb +17 -10
- data/lib/dalli/protocol/base.rb +15 -2
- data/lib/dalli/protocol/connection_manager.rb +53 -17
- data/lib/dalli/protocol/key_regularizer.rb +8 -9
- data/lib/dalli/protocol/meta.rb +46 -87
- data/lib/dalli/protocol/request_formatter.rb +80 -32
- data/lib/dalli/protocol/response_buffer.rb +21 -22
- data/lib/dalli/protocol/response_processor.rb +63 -27
- data/lib/dalli/socket.rb +47 -32
- data/lib/dalli/version.rb +1 -1
- metadata +2 -3
- data/lib/dalli/pid_cache.rb +0 -40
|
@@ -23,13 +23,23 @@ module Dalli
|
|
|
23
23
|
VERSION = 'VERSION'
|
|
24
24
|
SERVER_ERROR = 'SERVER_ERROR'
|
|
25
25
|
|
|
26
|
+
T_OK = [OK].freeze
|
|
27
|
+
T_RESET = [RESET].freeze
|
|
28
|
+
T_EN_HD = [EN, HD].freeze
|
|
29
|
+
T_VERSION = [VERSION].freeze
|
|
30
|
+
T_VA_EN_HD = [VA, EN, HD].freeze
|
|
31
|
+
T_HD_NF_EX = [HD, NF, EX].freeze
|
|
32
|
+
T_HD_NS_NF_EX = [HD, NS, NF, EX].freeze
|
|
33
|
+
T_VA_NF_NS_EX = [VA, NF, NS, EX].freeze
|
|
34
|
+
T_END_TOKEN_STAT = [END_TOKEN, STAT].freeze
|
|
35
|
+
|
|
26
36
|
def initialize(io_source, value_marshaller)
|
|
27
37
|
@io_source = io_source
|
|
28
38
|
@value_marshaller = value_marshaller
|
|
29
39
|
end
|
|
30
40
|
|
|
31
41
|
def meta_get_with_value(cache_nils: false)
|
|
32
|
-
tokens = error_on_unexpected!(
|
|
42
|
+
tokens = error_on_unexpected!(T_VA_EN_HD)
|
|
33
43
|
return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
|
|
34
44
|
return true unless tokens.first == VA
|
|
35
45
|
|
|
@@ -37,7 +47,7 @@ module Dalli
|
|
|
37
47
|
end
|
|
38
48
|
|
|
39
49
|
def meta_get_with_value_and_cas
|
|
40
|
-
tokens = error_on_unexpected!(
|
|
50
|
+
tokens = error_on_unexpected!(T_VA_EN_HD)
|
|
41
51
|
return [nil, 0] if tokens.first == EN
|
|
42
52
|
|
|
43
53
|
cas = cas_from_tokens(tokens)
|
|
@@ -47,7 +57,7 @@ module Dalli
|
|
|
47
57
|
end
|
|
48
58
|
|
|
49
59
|
def meta_get_without_value
|
|
50
|
-
tokens = error_on_unexpected!(
|
|
60
|
+
tokens = error_on_unexpected!(T_EN_HD)
|
|
51
61
|
tokens.first == EN ? nil : true
|
|
52
62
|
end
|
|
53
63
|
|
|
@@ -63,7 +73,7 @@ module Dalli
|
|
|
63
73
|
# Used by meta_get for comprehensive metadata retrieval.
|
|
64
74
|
# Supports thundering herd protection (N/R flags) and metadata flags (h/l/u).
|
|
65
75
|
def meta_get_with_metadata(cache_nils: false, return_hit_status: false, return_last_access: false)
|
|
66
|
-
tokens = error_on_unexpected!(
|
|
76
|
+
tokens = error_on_unexpected!(T_VA_EN_HD)
|
|
67
77
|
result = build_metadata_result(tokens)
|
|
68
78
|
result[:hit_before] = hit_status_from_tokens(tokens) if return_hit_status
|
|
69
79
|
result[:last_access] = last_access_from_tokens(tokens) if return_last_access
|
|
@@ -87,26 +97,26 @@ module Dalli
|
|
|
87
97
|
end
|
|
88
98
|
|
|
89
99
|
def meta_set_with_cas
|
|
90
|
-
tokens = error_on_unexpected!(
|
|
100
|
+
tokens = error_on_unexpected!(T_HD_NS_NF_EX)
|
|
91
101
|
return false unless tokens.first == HD
|
|
92
102
|
|
|
93
103
|
cas_from_tokens(tokens)
|
|
94
104
|
end
|
|
95
105
|
|
|
96
106
|
def meta_set_append_prepend
|
|
97
|
-
tokens = error_on_unexpected!(
|
|
107
|
+
tokens = error_on_unexpected!(T_HD_NS_NF_EX)
|
|
98
108
|
return false unless tokens.first == HD
|
|
99
109
|
|
|
100
110
|
true
|
|
101
111
|
end
|
|
102
112
|
|
|
103
113
|
def meta_delete
|
|
104
|
-
tokens = error_on_unexpected!(
|
|
114
|
+
tokens = error_on_unexpected!(T_HD_NF_EX)
|
|
105
115
|
tokens.first == HD
|
|
106
116
|
end
|
|
107
117
|
|
|
108
118
|
def decr_incr
|
|
109
|
-
tokens = error_on_unexpected!(
|
|
119
|
+
tokens = error_on_unexpected!(T_VA_NF_NS_EX)
|
|
110
120
|
return false if [NS, EX].include?(tokens.first)
|
|
111
121
|
return nil if tokens.first == NF
|
|
112
122
|
|
|
@@ -114,7 +124,7 @@ module Dalli
|
|
|
114
124
|
end
|
|
115
125
|
|
|
116
126
|
def stats
|
|
117
|
-
tokens = error_on_unexpected!(
|
|
127
|
+
tokens = error_on_unexpected!(T_END_TOKEN_STAT)
|
|
118
128
|
values = {}
|
|
119
129
|
while tokens.first != END_TOKEN
|
|
120
130
|
values[tokens[1]] = tokens[2]
|
|
@@ -124,19 +134,19 @@ module Dalli
|
|
|
124
134
|
end
|
|
125
135
|
|
|
126
136
|
def flush
|
|
127
|
-
error_on_unexpected!(
|
|
137
|
+
error_on_unexpected!(T_OK)
|
|
128
138
|
|
|
129
139
|
true
|
|
130
140
|
end
|
|
131
141
|
|
|
132
142
|
def reset
|
|
133
|
-
error_on_unexpected!(
|
|
143
|
+
error_on_unexpected!(T_RESET)
|
|
134
144
|
|
|
135
145
|
true
|
|
136
146
|
end
|
|
137
147
|
|
|
138
148
|
def version
|
|
139
|
-
tokens = error_on_unexpected!(
|
|
149
|
+
tokens = error_on_unexpected!(T_VERSION)
|
|
140
150
|
tokens.last
|
|
141
151
|
end
|
|
142
152
|
|
|
@@ -147,9 +157,26 @@ module Dalli
|
|
|
147
157
|
true
|
|
148
158
|
end
|
|
149
159
|
|
|
160
|
+
# Consumes the responses to a batch of quiet (pipelined) delete
|
|
161
|
+
# requests, which are terminated by a noop (MN). In quiet mode
|
|
162
|
+
# memcached suppresses the success response for each deleted key, so
|
|
163
|
+
# every line received before the terminator corresponds to a key that
|
|
164
|
+
# was NOT deleted -- a miss (NF) or an error. Returns that count so
|
|
165
|
+
# callers can derive the number of successful deletes as
|
|
166
|
+
# (keys_sent - non_deletions).
|
|
167
|
+
def pipelined_delete_non_deletions
|
|
168
|
+
non_deletions = 0
|
|
169
|
+
tokens = next_line_to_tokens
|
|
170
|
+
until tokens.first == MN
|
|
171
|
+
non_deletions += 1
|
|
172
|
+
tokens = next_line_to_tokens
|
|
173
|
+
end
|
|
174
|
+
non_deletions
|
|
175
|
+
end
|
|
176
|
+
|
|
150
177
|
def full_response_from_buffer(tokens, body, resp_size)
|
|
151
178
|
value = @value_marshaller.retrieve(body, bitflags_from_tokens(tokens))
|
|
152
|
-
[
|
|
179
|
+
[tokens.first == VA, cas_from_tokens(tokens), key_from_tokens(tokens), value, resp_size]
|
|
153
180
|
end
|
|
154
181
|
|
|
155
182
|
##
|
|
@@ -164,24 +191,26 @@ module Dalli
|
|
|
164
191
|
##
|
|
165
192
|
def getk_response_from_buffer(buf, offset = 0)
|
|
166
193
|
# Find the header terminator starting from offset
|
|
167
|
-
term_idx = buf.
|
|
168
|
-
return [0
|
|
194
|
+
term_idx = buf.byteindex(TERMINATOR, offset)
|
|
195
|
+
return [0] unless term_idx
|
|
169
196
|
|
|
170
197
|
header = buf.byteslice(offset, term_idx - offset)
|
|
171
198
|
tokens = header.split
|
|
172
199
|
header_len = header.bytesize + TERMINATOR.length
|
|
200
|
+
|
|
201
|
+
# The body len is removed from the tokens array
|
|
173
202
|
body_len = body_len_from_tokens(tokens)
|
|
174
203
|
|
|
175
204
|
# We have a complete response that has no body.
|
|
176
205
|
# This is either the response to the terminating
|
|
177
206
|
# noop or, if the status is not MN, an intermediate
|
|
178
207
|
# error response that needs to be discarded.
|
|
179
|
-
return [
|
|
208
|
+
return [true, header_len] if body_len.zero?
|
|
180
209
|
|
|
181
210
|
resp_size = header_len + body_len + TERMINATOR.length
|
|
182
211
|
# The header is in the buffer, but the body is not. As we don't have
|
|
183
212
|
# a complete response, don't advance the buffer
|
|
184
|
-
return [0
|
|
213
|
+
return [0] unless buf.bytesize >= offset + resp_size
|
|
185
214
|
|
|
186
215
|
# The full response is in our buffer, so parse it and return
|
|
187
216
|
# the values
|
|
@@ -200,17 +229,20 @@ module Dalli
|
|
|
200
229
|
end
|
|
201
230
|
|
|
202
231
|
def bitflags_from_tokens(tokens)
|
|
203
|
-
value_from_tokens(tokens, 'f')
|
|
232
|
+
value_from_tokens(tokens, 'f').to_i
|
|
204
233
|
end
|
|
205
234
|
|
|
206
235
|
def cas_from_tokens(tokens)
|
|
207
|
-
value_from_tokens(tokens, 'c')
|
|
236
|
+
value_from_tokens(tokens, 'c').to_i
|
|
208
237
|
end
|
|
209
238
|
|
|
210
239
|
def key_from_tokens(tokens)
|
|
211
240
|
encoded_key = value_from_tokens(tokens, 'k')
|
|
212
|
-
|
|
213
|
-
|
|
241
|
+
if tokens.delete('b')
|
|
242
|
+
KeyRegularizer.decode(encoded_key)
|
|
243
|
+
else
|
|
244
|
+
encoded_key
|
|
245
|
+
end
|
|
214
246
|
end
|
|
215
247
|
|
|
216
248
|
# Returns true if item was previously hit, false if first access, nil if not requested
|
|
@@ -225,18 +257,22 @@ module Dalli
|
|
|
225
257
|
# Returns seconds since last access, or nil if not requested
|
|
226
258
|
# The l flag returns l<seconds>
|
|
227
259
|
def last_access_from_tokens(tokens)
|
|
228
|
-
value_from_tokens(tokens, 'l')
|
|
260
|
+
value_from_tokens(tokens, 'l').to_i
|
|
229
261
|
end
|
|
230
262
|
|
|
231
263
|
def body_len_from_tokens(tokens)
|
|
232
|
-
value_from_tokens(tokens, 's')
|
|
264
|
+
value_from_tokens(tokens, 's').to_i
|
|
233
265
|
end
|
|
234
266
|
|
|
235
267
|
def value_from_tokens(tokens, flag)
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
268
|
+
# NB: as an optimization, we're mutating the matching token in place
|
|
269
|
+
# so there is a baked assumption that we're only accessing each token at most once.
|
|
270
|
+
index = tokens.find_index { |t| t.start_with?(flag) }
|
|
271
|
+
if index
|
|
272
|
+
tokens.delete_at(index).delete_prefix!(flag)
|
|
273
|
+
else
|
|
274
|
+
0
|
|
275
|
+
end
|
|
240
276
|
end
|
|
241
277
|
|
|
242
278
|
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.0.6
|
|
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.16
|
|
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
|