dalli 5.0.7 → 5.1.1

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.
@@ -36,36 +36,59 @@ module Dalli
36
36
  # - l<N>: Seconds since last access
37
37
  def meta_get(key:, value: true, return_cas: false, ttl: nil, quiet: false,
38
38
  vivify_ttl: nil, recache_ttl: nil,
39
- return_hit_status: false, return_last_access: false, skip_lru_bump: false,
40
- skip_flags: false)
39
+ return_hit_status: false, return_last_access: false, return_ttl_remaining: false,
40
+ skip_lru_bump: false, skip_flags: false, p_token: nil, l_token: nil)
41
41
  cmd = "mg #{encoded_key(key)}"
42
42
  # In raw mode (skip_flags: true), we don't request bitflags since they're not used.
43
43
  # This saves 2 bytes per request and skips parsing on response.
44
44
  cmd << (skip_flags ? ' v' : ' v f') if value
45
45
  cmd << ' c' if return_cas
46
46
  cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
47
+ cmd << routing_tokens(p_token: p_token, l_token: l_token)
47
48
  cmd << ' k q s' if quiet # Return the key in the response if quiet
48
49
  cmd << " N#{integer_flag(:vivify_ttl, vivify_ttl)}" if vivify_ttl # Thundering herd: vivify on miss
49
50
  # Thundering herd: win recache if TTL below threshold
50
51
  cmd << " R#{integer_flag(:recache_ttl, recache_ttl)}" if recache_ttl
51
52
  cmd << ' h' if return_hit_status # Return hit status (0 or 1)
52
53
  cmd << ' l' if return_last_access # Return seconds since last access
54
+ cmd << ' t' if return_ttl_remaining # Return seconds of TTL remaining (-1 = no TTL)
53
55
  cmd << ' u' if skip_lru_bump # Don't bump LRU or update access stats
54
56
  cmd << TERMINATOR
55
57
  end
56
58
 
57
- def multi_meta_get(keys, skip_flags: false)
59
+ # Fast path for the common single-key get with no optional flags.
60
+ # Produces the same bytes as meta_get(key: key, skip_flags: skip_flags)
61
+ # without the keyword-argument handling and incremental string building.
62
+ def plain_meta_get(key, skip_flags)
63
+ skip_flags ? "mg #{encoded_key(key)} v#{TERMINATOR}" : "mg #{encoded_key(key)} v f#{TERMINATOR}"
64
+ end
65
+
66
+ # Pass terminate: false to leave off the trailing noop, for callers
67
+ # (the pipelined get) that send it separately.
68
+ def multi_meta_get(keys, skip_flags: false, return_cas: false, terminate: true, p_token: nil, l_token: nil)
58
69
  # In raw mode: "mg <key> v k q s\r\n" (no f flag, key at index 2)
59
70
  # Normal mode: "mg <key> v f k q s\r\n" (key at index 3)
60
- post_get = skip_flags ? " v k q s\r\n" : " v f k q s\r\n"
71
+ # With return_cas a "c" flag follows, which shifts those indexes --
72
+ # callers of that variant locate tokens by flag rather than position.
73
+ # Routing tokens apply to every key in the batch, so the suffix is
74
+ # built once rather than per key.
75
+ post_get = if return_cas
76
+ skip_flags ? ' v c' : ' v f c'
77
+ else
78
+ skip_flags ? ' v' : ' v f'
79
+ end
80
+ post_get += routing_tokens(p_token: p_token, l_token: l_token)
81
+ post_get += " k q s#{TERMINATOR}"
82
+
61
83
  buffer = ''.b
62
84
  keys.each do |key|
63
85
  buffer << 'mg ' << encoded_key(key) << post_get
64
86
  end
65
- buffer << 'mn' << TERMINATOR
87
+ terminate ? buffer << 'mn' << TERMINATOR : buffer
66
88
  end
67
89
 
68
- def meta_set(key:, value:, bitflags: nil, cas: nil, ttl: nil, mode: :set, quiet: false)
90
+ def meta_set(key:, value:, bitflags: nil, cas: nil, ttl: nil, mode: :set, quiet: false,
91
+ p_token: nil, l_token: nil)
69
92
  base64 = KeyRegularizer.required?(key)
70
93
  key = KeyRegularizer.encode(key) if base64
71
94
  cmd = "ms #{key} #{value.bytesize}"
@@ -78,11 +101,16 @@ module Dalli
78
101
  cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
79
102
  cmd << " M#{mode_to_token(mode)}"
80
103
  cmd << ' q' if quiet
104
+ cmd << routing_tokens(p_token: p_token, l_token: l_token)
81
105
  cmd << TERMINATOR
82
106
  end
83
107
 
84
- def multi_meta_set(entries, ttl: nil)
108
+ def multi_meta_set(entries, ttl: nil, p_token: nil, l_token: nil)
109
+ # Routing tokens apply to every entry in the batch, so the suffix is
110
+ # built once rather than per entry.
111
+ token_suffix = routing_tokens(p_token: p_token, l_token: l_token)
85
112
  ttl = integer_flag(:ttl, ttl) if ttl
113
+
86
114
  buffer = ''.b
87
115
  entries.each do |key, pair|
88
116
  value, bitflags = pair
@@ -90,12 +118,12 @@ module Dalli
90
118
  base64 = KeyRegularizer.required?(key)
91
119
  key = KeyRegularizer.encode(key) if base64
92
120
 
93
- # Inline format: "ms <key> <size> c [b] F<flags> T<ttl> MS q\r\n"
121
+ # Inline format: "ms <key> <size> c [b] F<flags> T<ttl> MS q [P/L]\r\n"
94
122
  buffer << "ms #{key} #{value.bytesize} c"
95
123
  buffer << ' b' if base64
96
124
  buffer << " F#{bitflags}" if bitflags
97
125
  buffer << " T#{ttl}" if ttl
98
- buffer << ' MS q' << TERMINATOR << value << TERMINATOR
126
+ buffer << ' MS q' << token_suffix << TERMINATOR << value << TERMINATOR
99
127
  end
100
128
  buffer << META_NOOP
101
129
  end
@@ -103,24 +131,55 @@ module Dalli
103
131
  # Thundering herd protection flag:
104
132
  # - stale (I flag): Instead of deleting the item, mark it as stale. Other clients
105
133
  # using N/R flags will see the X flag and know the item is being regenerated.
106
- def meta_delete(key:, cas: nil, ttl: nil, quiet: false, stale: false)
134
+ # Tombstone flags:
135
+ # - stale (I flag): mark the item stale instead of removing it. Readers
136
+ # using N/R flags, or get_with_metadata, see the X flag and know the
137
+ # item is being regenerated.
138
+ # - ttl (T flag): how long the stale marker lives. memcached only honors
139
+ # T on a delete when it is paired with I, so this raises rather than
140
+ # emitting a request the server would apply differently than intended.
141
+ # - drop_value (x flag): remove the item's value but leave the item, so a
142
+ # tombstone can be left without retaining the old payload.
143
+ def meta_delete(key:, cas: nil, ttl: nil, quiet: false, stale: false, drop_value: false,
144
+ p_token: nil, l_token: nil)
145
+ # Message uses this method's own parameter names (ttl/stale), not the
146
+ # client-facing tombstone_ttl/invalidate names Dalli::Client validates
147
+ # against -- this guard is also reachable by internal callers (tests,
148
+ # direct RequestFormatter use) that never go through the client.
149
+ raise ArgumentError, 'ttl requires stale: true' if ttl && !stale
150
+
107
151
  cmd = "md #{encoded_key(key)}"
108
152
  cmd << cas_string(cas)
109
- cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
110
153
  cmd << ' I' if stale # Mark stale instead of deleting
154
+ cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
155
+ cmd << ' x' if drop_value # Drop the value but keep the item
111
156
  cmd << ' q' if quiet
157
+ cmd << routing_tokens(p_token: p_token, l_token: l_token)
112
158
  cmd << TERMINATOR
113
159
  end
114
160
 
115
- def multi_meta_delete(keys)
161
+ # Tombstone and routing-token flags apply to every key in the batch;
162
+ # see meta_delete.
163
+ def multi_meta_delete(keys, stale: false, ttl: nil, drop_value: false, p_token: nil, l_token: nil)
164
+ raise ArgumentError, 'tombstone_ttl requires invalidate: true' if ttl && !stale
165
+
166
+ suffix = +''
167
+ suffix << ' I' if stale
168
+ suffix << " T#{integer_flag(:ttl, ttl)}" if ttl
169
+ suffix << ' x' if drop_value
170
+ suffix << ' q'
171
+ suffix << routing_tokens(p_token: p_token, l_token: l_token)
172
+ suffix << TERMINATOR
173
+
116
174
  buffer = ''.b
117
175
  keys.each do |key|
118
- buffer << 'md ' << encoded_key(key) << ' q' << TERMINATOR
176
+ buffer << 'md ' << encoded_key(key) << suffix
119
177
  end
120
178
  buffer << META_NOOP
121
179
  end
122
180
 
123
- def meta_arithmetic(key:, delta:, initial:, incr: true, cas: nil, ttl: nil, quiet: false)
181
+ def meta_arithmetic(key:, delta:, initial:, incr: true, cas: nil, ttl: nil, quiet: false,
182
+ p_token: nil, l_token: nil)
124
183
  cmd = "ma #{encoded_key(key)} v"
125
184
  cmd << " D#{integer_flag(:delta, delta)}" if delta
126
185
  cmd << " J#{integer_flag(:initial, initial)}" if initial
@@ -129,8 +188,39 @@ module Dalli
129
188
  cmd << cas_string(cas)
130
189
  cmd << ' q' if quiet
131
190
  cmd << " M#{incr ? 'I' : 'D'}"
191
+ cmd << routing_tokens(p_token: p_token, l_token: l_token)
132
192
  cmd << TERMINATOR
133
193
  end
194
+
195
+ # Builds the wire-format suffix for opaque routing tokens (P and L).
196
+ # memcached itself ignores these; they exist as hints for a proxy or
197
+ # router sitting between the client and memcached. See protocol.txt:
198
+ # "All commands accept tokens 'P' and 'L' which are completely ignored.
199
+ # The arguments to 'P' and 'L' can be used as hints or path
200
+ # specifications to a proxy or router inbetween a client and a
201
+ # memcached daemon."
202
+ #
203
+ # Empty / nil tokens are treated as no-ops. CRLF and null bytes are
204
+ # rejected with ArgumentError to prevent the token from being used as a
205
+ # wire-protocol injection vector (e.g. "foo\r\nflush_all\r\n" would
206
+ # otherwise be parsed as a second command by memcached or any
207
+ # intermediate proxy/LB).
208
+ def routing_tokens(p_token: nil, l_token: nil)
209
+ # Only an empty *String* is a no-op. Checking respond_to?(:empty?)
210
+ # instead would also swallow p_token: [] / {} before the type check
211
+ # below ever runs, silently dropping caller mistakes that should
212
+ # raise "must be a String".
213
+ p_token = nil if p_token.is_a?(String) && p_token.empty?
214
+ l_token = nil if l_token.is_a?(String) && l_token.empty?
215
+ validate_routing_token!('p_token', p_token)
216
+ validate_routing_token!('l_token', l_token)
217
+ return '' unless p_token || l_token
218
+
219
+ s = +''
220
+ s << " P#{p_token}" if p_token
221
+ s << " L#{l_token}" if l_token
222
+ s
223
+ end
134
224
  # rubocop:enable Metrics/CyclomaticComplexity
135
225
  # rubocop:enable Metrics/ParameterLists
136
226
  # rubocop:enable Metrics/PerceivedComplexity
@@ -171,12 +261,22 @@ module Dalli
171
261
 
172
262
  private
173
263
 
264
+ # Disallowed bytes: CR, LF, NUL. Any of these embedded in a routing
265
+ # token would let the caller inject a second wire-protocol command.
266
+ ROUTING_TOKEN_FORBIDDEN = /[\r\n\0]/
267
+ private_constant :ROUTING_TOKEN_FORBIDDEN
268
+
269
+ def validate_routing_token!(name, value)
270
+ return if value.nil?
271
+ raise ArgumentError, "#{name} must be a String, got #{value.class}" unless value.is_a?(String)
272
+ raise ArgumentError, "#{name} must not contain CRLF or null bytes" if value.match?(ROUTING_TOKEN_FORBIDDEN)
273
+ end
274
+
174
275
  # Numeric flag values are written straight into the command line, so a
175
276
  # value that isn't an integer -- e.g. a String carrying CRLF -- would let
176
- # the caller inject further memcached commands (GHSA-6wmv-xq9m-fmp7).
177
- # Converting to Integer means only digits ever reach the wire. Strings
178
- # are parsed as base 10 so "010" means 10, as memcached would read it,
179
- # rather than octal 8.
277
+ # the caller inject further memcached commands. Converting to Integer
278
+ # means only digits ever reach the wire. Strings are parsed as base 10 so
279
+ # "010" means 10, as memcached would read it, rather than octal 8.
180
280
  def integer_flag(name, value)
181
281
  value.is_a?(String) ? Integer(value, 10) : Integer(value)
182
282
  rescue ArgumentError, TypeError, FloatDomainError
@@ -23,6 +23,14 @@ module Dalli
23
23
  VERSION = 'VERSION'
24
24
  SERVER_ERROR = 'SERVER_ERROR'
25
25
 
26
+ VA_PREFIX = 'VA '
27
+ FLAGS_TOKEN_PREFIX = ' f'
28
+ BYTE_B = 'b'.ord
29
+ BYTE_C = 'c'.ord
30
+ BYTE_F = 'f'.ord
31
+ BYTE_K = 'k'.ord
32
+ BYTE_S = 's'.ord
33
+
26
34
  T_OK = [OK].freeze
27
35
  T_RESET = [RESET].freeze
28
36
  T_EN_HD = [EN, HD].freeze
@@ -39,11 +47,24 @@ module Dalli
39
47
  end
40
48
 
41
49
  def meta_get_with_value(cache_nils: false)
42
- tokens = error_on_unexpected!(T_VA_EN_HD)
43
- return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
44
- return true unless tokens.first == VA
50
+ line = read_line
51
+ # A hit ("VA <size> f<flags>") is parsed in place rather than split
52
+ # into tokens, which saves several allocations on the hottest path.
53
+ if line&.start_with?(VA_PREFIX)
54
+ return @value_marshaller.retrieve(read_data(size_from_va_line(line)), bitflags_from_va_line(line))
55
+ end
45
56
 
46
- @value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens))
57
+ tokens = line&.split || []
58
+ case tokens.first
59
+ when EN
60
+ cache_nils ? ::Dalli::NOT_FOUND : nil
61
+ when VA # only reached for an unusually formatted hit line
62
+ @value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens))
63
+ when HD
64
+ true
65
+ else
66
+ raise_unexpected!(tokens)
67
+ end
47
68
  end
48
69
 
49
70
  def meta_get_with_value_and_cas
@@ -72,11 +93,13 @@ module Dalli
72
93
  #
73
94
  # Used by meta_get for comprehensive metadata retrieval.
74
95
  # 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)
96
+ def meta_get_with_metadata(cache_nils: false, return_hit_status: false, return_last_access: false,
97
+ return_ttl_remaining: false)
76
98
  tokens = error_on_unexpected!(T_VA_EN_HD)
77
99
  result = build_metadata_result(tokens)
78
100
  result[:hit_before] = hit_status_from_tokens(tokens) if return_hit_status
79
101
  result[:last_access] = last_access_from_tokens(tokens) if return_last_access
102
+ result[:ttl_remaining] = ttl_remaining_from_tokens(tokens) if return_ttl_remaining
80
103
  result[:value] = parse_value_from_tokens(tokens, cache_nils)
81
104
  result
82
105
  end
@@ -85,7 +108,12 @@ module Dalli
85
108
  {
86
109
  value: nil, cas: cas_from_tokens(tokens),
87
110
  won_recache: tokens.include?('W'), stale: tokens.include?('X'),
88
- lost_recache: tokens.include?('Z')
111
+ lost_recache: tokens.include?('Z'),
112
+ # Explicit miss marker: EN means the key does not exist. A tombstoned
113
+ # item is NOT a miss -- it answers VA/HD with the X flag set -- and a
114
+ # stored nil under cache_nils is not one either, so neither can be
115
+ # inferred from value or cas alone.
116
+ miss: tokens.first == EN
89
117
  }
90
118
  end
91
119
 
@@ -174,11 +202,6 @@ module Dalli
174
202
  non_deletions
175
203
  end
176
204
 
177
- def full_response_from_buffer(tokens, body, resp_size)
178
- value = @value_marshaller.retrieve(body, bitflags_from_tokens(tokens))
179
- [tokens.first == VA, cas_from_tokens(tokens), key_from_tokens(tokens), value, resp_size]
180
- end
181
-
182
205
  ##
183
206
  # This method returns an array of values used in a pipelined
184
207
  # getk process. The first value is the number of bytes by
@@ -198,8 +221,20 @@ module Dalli
198
221
  tokens = header.split
199
222
  header_len = header.bytesize + TERMINATOR.length
200
223
 
201
- # The body len is removed from the tokens array
202
- body_len = body_len_from_tokens(tokens)
224
+ # Read the s, f, c and k flags and the b marker in one pass. As with
225
+ # value_from_tokens, the first token for each flag wins.
226
+ size = bitflags = cas = key = nil
227
+ base64 = false
228
+ tokens.each do |token|
229
+ case token.getbyte(0)
230
+ when BYTE_S then size ||= token
231
+ when BYTE_F then bitflags ||= token
232
+ when BYTE_C then cas ||= token
233
+ when BYTE_K then key ||= token
234
+ when BYTE_B then base64 ||= token == 'b'
235
+ end
236
+ end
237
+ body_len = flag_int(size, 's')
203
238
 
204
239
  # We have a complete response that has no body.
205
240
  # This is either the response to the terminating
@@ -215,7 +250,16 @@ module Dalli
215
250
  # The full response is in our buffer, so parse it and return
216
251
  # the values
217
252
  body = buf.byteslice(offset + header_len, body_len)
218
- full_response_from_buffer(tokens, body, resp_size)
253
+ value = @value_marshaller.retrieve(body, flag_int(bitflags, 'f'))
254
+ key = key ? key.delete_prefix!('k') : 0
255
+ key = KeyRegularizer.decode(key) if base64
256
+ [tokens.first == VA, flag_int(cas, 'c'), key, value, resp_size]
257
+ end
258
+
259
+ # Integer value of a flag token such as "f123", or 0 when absent.
260
+ # Strips the prefix in place, like value_from_tokens.
261
+ def flag_int(token, flag)
262
+ token ? token.delete_prefix!(flag).to_i : 0
219
263
  end
220
264
 
221
265
  def error_on_unexpected!(expected_codes)
@@ -223,6 +267,10 @@ module Dalli
223
267
 
224
268
  return tokens if expected_codes.include?(tokens.first)
225
269
 
270
+ raise_unexpected!(tokens)
271
+ end
272
+
273
+ def raise_unexpected!(tokens)
226
274
  raise Dalli::ServerError, tokens.join(' ').to_s if tokens.first == SERVER_ERROR
227
275
 
228
276
  raise Dalli::DalliError, "Response error: #{tokens.first}"
@@ -232,10 +280,29 @@ module Dalli
232
280
  value_from_tokens(tokens, 'f').to_i
233
281
  end
234
282
 
283
+ # String#to_i stops at the first non-digit, so these read one token's
284
+ # integer straight out of the header line without splitting it.
285
+ def size_from_va_line(line)
286
+ line.byteslice(VA_PREFIX.bytesize, line.bytesize).to_i
287
+ end
288
+
289
+ def bitflags_from_va_line(line)
290
+ idx = line.index(FLAGS_TOKEN_PREFIX, VA_PREFIX.bytesize)
291
+ idx ? line.byteslice(idx + FLAGS_TOKEN_PREFIX.bytesize, line.bytesize).to_i : 0
292
+ end
293
+
235
294
  def cas_from_tokens(tokens)
236
295
  value_from_tokens(tokens, 'c').to_i
237
296
  end
238
297
 
298
+ # Detects the X presence flag, set when an item has been marked stale by a
299
+ # prior `md key I`. Uses strict equality (Array#any? with a String pattern
300
+ # compares with ==) so a future value-bearing flag beginning with X cannot
301
+ # be mistaken for it.
302
+ def stale_from_tokens(tokens)
303
+ tokens.any?('X')
304
+ end
305
+
239
306
  def key_from_tokens(tokens)
240
307
  encoded_key = value_from_tokens(tokens, 'k')
241
308
  if tokens.delete('b')
@@ -260,8 +327,10 @@ module Dalli
260
327
  value_from_tokens(tokens, 'l').to_i
261
328
  end
262
329
 
263
- def body_len_from_tokens(tokens)
264
- value_from_tokens(tokens, 's').to_i
330
+ # Returns seconds of TTL remaining; -1 when the item has no expiry.
331
+ # The t flag returns t<seconds>.
332
+ def ttl_remaining_from_tokens(tokens)
333
+ value_from_tokens(tokens, 't').to_i
265
334
  end
266
335
 
267
336
  def value_from_tokens(tokens, flag)
data/lib/dalli/ring.rb CHANGED
@@ -21,41 +21,46 @@ module Dalli
21
21
  # in an equally weighted scenario.
22
22
  POINTS_PER_SERVER = 160 # this is the default in libmemcached
23
23
 
24
- attr_accessor :servers, :continuum
24
+ attr_accessor :servers
25
+ attr_reader :continuum
25
26
 
26
27
  def initialize(servers_arg, options)
27
28
  @servers = servers_arg.map do |s|
28
29
  Dalli::Protocol::Meta.new(s, options)
29
30
  end
30
- @continuum = nil
31
- @continuum = build_continuum(servers) if servers.size > 1
31
+ self.continuum = (build_continuum(servers) if servers.size > 1)
32
32
 
33
33
  threadsafe! unless options[:threadsafe] == false
34
34
  @failover = options[:failover] != false
35
35
  end
36
36
 
37
- def server_for_key(key)
37
+ def continuum=(entries)
38
+ @continuum = entries
39
+ # Plain integers, so the binary search in server_for_hash_key needn't
40
+ # call Entry#value at each step
41
+ @continuum_values = entries&.map(&:value)
42
+ end
43
+
44
+ # alive_cache (optional) remembers each server's alive? result, so a
45
+ # caller routing many keys checks each server once rather than per key.
46
+ def server_for_key(key, alive_cache = nil)
47
+ # server_from_continuum only returns a server that is alive
38
48
  server = if @continuum
39
- server_from_continuum(key)
40
- else
41
- @servers.first
49
+ server_from_continuum(key, alive_cache)
50
+ elsif (first = @servers.first) && server_alive?(first, alive_cache)
51
+ first
42
52
  end
43
-
44
- # Note that the call to alive? has the side effect of initializing
45
- # the socket
46
- return server if server&.alive?
53
+ return server if server
47
54
 
48
55
  raise Dalli::RingError, 'No server available'
49
56
  end
50
57
 
51
- def server_from_continuum(key)
58
+ def server_from_continuum(key, alive_cache = nil)
52
59
  hkey = hash_for(key)
53
60
  20.times do |try|
54
61
  server = server_for_hash_key(hkey)
55
62
 
56
- # Note that the call to alive? has the side effect of initializing
57
- # the socket
58
- return server if server.alive?
63
+ return server if server_alive?(server, alive_cache)
59
64
  break unless @failover
60
65
 
61
66
  hkey = hash_for("#{try}#{key}")
@@ -64,8 +69,9 @@ module Dalli
64
69
  end
65
70
 
66
71
  def keys_grouped_by_server(key_arr)
72
+ alive_cache = {}.compare_by_identity
67
73
  key_arr.group_by do |key|
68
- server_for_key(key)
74
+ server_for_key(key, alive_cache)
69
75
  rescue Dalli::RingError
70
76
  Dalli.logger.debug { "unable to get key #{key}" }
71
77
  nil
@@ -110,13 +116,21 @@ module Dalli
110
116
  Zlib.crc32(key)
111
117
  end
112
118
 
119
+ # Note that the call to alive? has the side effect of initializing
120
+ # the socket
121
+ def server_alive?(server, alive_cache)
122
+ return server.alive? unless alive_cache
123
+
124
+ alive_cache.fetch(server) { alive_cache[server] = server.alive? }
125
+ end
126
+
113
127
  def entry_count_for(server, total_servers, total_weight)
114
128
  ((total_servers * POINTS_PER_SERVER * server.weight) / Float(total_weight)).floor
115
129
  end
116
130
 
117
131
  def server_for_hash_key(hash_key)
118
132
  # Find the closest index in the Ring with value <= the given value
119
- entryidx = @continuum.bsearch_index { |entry| entry.value > hash_key }
133
+ entryidx = @continuum_values.bsearch_index { |value| value > hash_key }
120
134
  if entryidx.nil?
121
135
  entryidx = @continuum.size - 1
122
136
  else
data/lib/dalli/socket.rb CHANGED
@@ -124,7 +124,7 @@ module Dalli
124
124
  # Returns true for an unmodified TCPSocket on Ruby 3.0+, or for resolv-replace >= 0.2.0
125
125
  # which forwards keyword arguments through its patch.
126
126
  # Returns false when monkey-patched by gems like socksify or resolv-replace < 0.2.0.
127
- # rubocop:disable ThreadSafety/ClassInstanceVariable
127
+ # rubocop:disable-next ThreadSafety/ClassInstanceVariable
128
128
  def self.supports_connect_timeout?
129
129
  return @supports_connect_timeout if defined?(@supports_connect_timeout)
130
130
 
@@ -135,7 +135,6 @@ module Dalli
135
135
  end
136
136
  end
137
137
  end
138
- # rubocop:enable ThreadSafety/ClassInstanceVariable
139
138
 
140
139
  def self.create_socket_with_timeout(host, port, options)
141
140
  if supports_connect_timeout?
@@ -192,14 +191,13 @@ module Dalli
192
191
 
193
192
  # Detect and cache the correct pack format for struct timeval on this platform.
194
193
  # Different architectures have different sizes for time_t and suseconds_t.
195
- # rubocop:disable ThreadSafety/ClassInstanceVariable
194
+ # rubocop:disable-next ThreadSafety/ClassInstanceVariable
196
195
  def self.timeval_pack_format(sock)
197
196
  @timeval_pack_format ||= begin
198
197
  expected_size = sock.getsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVTIMEO).data.bytesize
199
198
  TIMEVAL_PACK_FORMATS.find { |fmt| TIMEVAL_TEST_VALUES.pack(fmt).bytesize == expected_size } || 'll'
200
199
  end
201
200
  end
202
- # rubocop:enable ThreadSafety/ClassInstanceVariable
203
201
 
204
202
  def self.pack_timeval(sock, seconds, microseconds)
205
203
  [seconds, microseconds].pack(timeval_pack_format(sock))
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.7'
4
+ VERSION = '5.1.1'
5
5
 
6
- MIN_SUPPORTED_MEMCACHED_VERSION = '1.6'
6
+ MIN_SUPPORTED_MEMCACHED_VERSION = '1.6.27'
7
7
  end
@@ -16,11 +16,10 @@ module Rack
16
16
  attr_reader :data
17
17
 
18
18
  # Don't freeze this until we fix the specs/implementation
19
- # rubocop:disable Style/MutableConstant
19
+ # rubocop:disable-next Style/MutableConstant
20
20
  DEFAULT_DALLI_OPTIONS = {
21
21
  namespace: 'rack:session'
22
22
  }
23
- # rubocop:enable Style/MutableConstant
24
23
 
25
24
  # Brings in a new Rack::Session::Dalli middleware with the given
26
25
  # `:memcache_server`. The server is either a hostname, or a
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.7
4
+ version: 5.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter M. Goldstein