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.
@@ -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!([VA, EN, HD])
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!([VA, EN, HD])
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!([EN, HD])
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!([VA, EN, HD])
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!([HD, NS, NF, EX])
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!([HD, NS, NF, EX])
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!([HD, NF, EX])
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!([VA, NF, NS, EX])
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!([END_TOKEN, STAT])
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!([OK])
137
+ error_on_unexpected!(T_OK)
128
138
 
129
139
  true
130
140
  end
131
141
 
132
142
  def reset
133
- error_on_unexpected!([RESET])
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!([VERSION])
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
- [resp_size, tokens.first == VA, cas_from_tokens(tokens), key_from_tokens(tokens), value]
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.index(TERMINATOR, offset)
168
- return [0, nil, nil, nil, nil] unless term_idx
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 [header_len, true, nil, nil, nil] if body_len.zero?
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, nil, nil, nil, nil] unless buf.bytesize >= offset + resp_size
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')&.to_i
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')&.to_i
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
- base64_encoded = tokens.any?('b')
213
- KeyRegularizer.decode(encoded_key, base64_encoded)
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')&.to_i
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')&.to_i
264
+ value_from_tokens(tokens, 's').to_i
233
265
  end
234
266
 
235
267
  def value_from_tokens(tokens, flag)
236
- bitflags_token = tokens.find { |t| t.start_with?(flag) }
237
- return 0 unless bitflags_token
238
-
239
- bitflags_token[1..]
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 readfull(count)
16
- value = String.new(capacity: count + 1)
17
- loop do
18
- result = read_nonblock(count - value.bytesize, exception: false)
19
- value << result if append_to_buffer?(result)
20
- break if value.bytesize == count
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
- def read_available
26
- value = +''
28
+ buffer = ''.b
27
29
  loop do
28
- result = read_nonblock(8196, exception: false)
29
- break if WAIT_RCS.include?(result)
30
- raise Errno::ECONNRESET, "Connection reset: #{logged_options.inspect}" unless result
31
-
32
- value << result
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
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dalli
4
- VERSION = '5.0.4'
4
+ VERSION = '5.0.6'
5
5
 
6
6
  MIN_SUPPORTED_MEMCACHED_VERSION = '1.6'
7
7
  end
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
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.10
90
+ rubygems_version: 4.0.16
92
91
  specification_version: 4
93
92
  summary: High performance memcached client for Ruby
94
93
  test_files: []
@@ -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