dalli 5.0.7 → 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.
@@ -36,28 +36,40 @@ 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
- cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
46
+ cmd << " T#{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
- cmd << " N#{integer_flag(:vivify_ttl, vivify_ttl)}" if vivify_ttl # Thundering herd: vivify on miss
49
- # Thundering herd: win recache if TTL below threshold
50
- cmd << " R#{integer_flag(:recache_ttl, recache_ttl)}" if recache_ttl
49
+ cmd << " N#{vivify_ttl}" if vivify_ttl # Thundering herd: vivify on miss
50
+ cmd << " R#{recache_ttl}" if recache_ttl # Thundering herd: win recache if TTL below threshold
51
51
  cmd << ' h' if return_hit_status # Return hit status (0 or 1)
52
52
  cmd << ' l' if return_last_access # Return seconds since last access
53
+ cmd << ' t' if return_ttl_remaining # Return seconds of TTL remaining (-1 = no TTL)
53
54
  cmd << ' u' if skip_lru_bump # Don't bump LRU or update access stats
54
55
  cmd << TERMINATOR
55
56
  end
56
57
 
57
- def multi_meta_get(keys, skip_flags: false)
58
+ def multi_meta_get(keys, skip_flags: false, return_cas: false, p_token: nil, l_token: nil)
58
59
  # In raw mode: "mg <key> v k q s\r\n" (no f flag, key at index 2)
59
60
  # 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"
61
+ # With return_cas a "c" flag follows, which shifts those indexes --
62
+ # callers of that variant locate tokens by flag rather than position.
63
+ # Routing tokens apply to every key in the batch, so the suffix is
64
+ # built once rather than per key.
65
+ post_get = if return_cas
66
+ skip_flags ? ' v c' : ' v f c'
67
+ else
68
+ skip_flags ? ' v' : ' v f'
69
+ end
70
+ post_get += routing_tokens(p_token: p_token, l_token: l_token)
71
+ post_get += " k q s#{TERMINATOR}"
72
+
61
73
  buffer = ''.b
62
74
  keys.each do |key|
63
75
  buffer << 'mg ' << encoded_key(key) << post_get
@@ -65,7 +77,8 @@ module Dalli
65
77
  buffer << 'mn' << TERMINATOR
66
78
  end
67
79
 
68
- def meta_set(key:, value:, bitflags: nil, cas: nil, ttl: nil, mode: :set, quiet: false)
80
+ def meta_set(key:, value:, bitflags: nil, cas: nil, ttl: nil, mode: :set, quiet: false,
81
+ p_token: nil, l_token: nil)
69
82
  base64 = KeyRegularizer.required?(key)
70
83
  key = KeyRegularizer.encode(key) if base64
71
84
  cmd = "ms #{key} #{value.bytesize}"
@@ -75,14 +88,18 @@ module Dalli
75
88
  cmd << ' b' if base64
76
89
  cmd << " F#{bitflags}" if bitflags
77
90
  cmd << cas_string(cas)
78
- cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
91
+ cmd << " T#{ttl}" if ttl
79
92
  cmd << " M#{mode_to_token(mode)}"
80
93
  cmd << ' q' if quiet
94
+ cmd << routing_tokens(p_token: p_token, l_token: l_token)
81
95
  cmd << TERMINATOR
82
96
  end
83
97
 
84
- def multi_meta_set(entries, ttl: nil)
85
- ttl = integer_flag(:ttl, ttl) if ttl
98
+ def multi_meta_set(entries, ttl: nil, p_token: nil, l_token: nil)
99
+ # Routing tokens apply to every entry in the batch, so the suffix is
100
+ # built once rather than per entry.
101
+ token_suffix = routing_tokens(p_token: p_token, l_token: l_token)
102
+
86
103
  buffer = ''.b
87
104
  entries.each do |key, pair|
88
105
  value, bitflags = pair
@@ -90,12 +107,12 @@ module Dalli
90
107
  base64 = KeyRegularizer.required?(key)
91
108
  key = KeyRegularizer.encode(key) if base64
92
109
 
93
- # Inline format: "ms <key> <size> c [b] F<flags> T<ttl> MS q\r\n"
110
+ # Inline format: "ms <key> <size> c [b] F<flags> T<ttl> MS q [P/L]\r\n"
94
111
  buffer << "ms #{key} #{value.bytesize} c"
95
112
  buffer << ' b' if base64
96
113
  buffer << " F#{bitflags}" if bitflags
97
114
  buffer << " T#{ttl}" if ttl
98
- buffer << ' MS q' << TERMINATOR << value << TERMINATOR
115
+ buffer << ' MS q' << token_suffix << TERMINATOR << value << TERMINATOR
99
116
  end
100
117
  buffer << META_NOOP
101
118
  end
@@ -103,34 +120,96 @@ module Dalli
103
120
  # Thundering herd protection flag:
104
121
  # - stale (I flag): Instead of deleting the item, mark it as stale. Other clients
105
122
  # 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)
123
+ # Tombstone flags:
124
+ # - stale (I flag): mark the item stale instead of removing it. Readers
125
+ # using N/R flags, or get_with_metadata, see the X flag and know the
126
+ # item is being regenerated.
127
+ # - ttl (T flag): how long the stale marker lives. memcached only honors
128
+ # T on a delete when it is paired with I, so this raises rather than
129
+ # emitting a request the server would apply differently than intended.
130
+ # - drop_value (x flag): remove the item's value but leave the item, so a
131
+ # tombstone can be left without retaining the old payload.
132
+ def meta_delete(key:, cas: nil, ttl: nil, quiet: false, stale: false, drop_value: false,
133
+ p_token: nil, l_token: nil)
134
+ # Message uses this method's own parameter names (ttl/stale), not the
135
+ # client-facing tombstone_ttl/invalidate names Dalli::Client validates
136
+ # against -- this guard is also reachable by internal callers (tests,
137
+ # direct RequestFormatter use) that never go through the client.
138
+ raise ArgumentError, 'ttl requires stale: true' if ttl && !stale
139
+
107
140
  cmd = "md #{encoded_key(key)}"
108
141
  cmd << cas_string(cas)
109
- cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
110
142
  cmd << ' I' if stale # Mark stale instead of deleting
143
+ cmd << " T#{Integer(ttl)}" if ttl
144
+ cmd << ' x' if drop_value # Drop the value but keep the item
111
145
  cmd << ' q' if quiet
146
+ cmd << routing_tokens(p_token: p_token, l_token: l_token)
112
147
  cmd << TERMINATOR
113
148
  end
114
149
 
115
- def multi_meta_delete(keys)
150
+ # Tombstone and routing-token flags apply to every key in the batch;
151
+ # see meta_delete.
152
+ def multi_meta_delete(keys, stale: false, ttl: nil, drop_value: false, p_token: nil, l_token: nil)
153
+ raise ArgumentError, 'tombstone_ttl requires invalidate: true' if ttl && !stale
154
+
155
+ suffix = +''
156
+ suffix << ' I' if stale
157
+ suffix << " T#{Integer(ttl)}" if ttl
158
+ suffix << ' x' if drop_value
159
+ suffix << ' q'
160
+ suffix << routing_tokens(p_token: p_token, l_token: l_token)
161
+ suffix << TERMINATOR
162
+
116
163
  buffer = ''.b
117
164
  keys.each do |key|
118
- buffer << 'md ' << encoded_key(key) << ' q' << TERMINATOR
165
+ buffer << 'md ' << encoded_key(key) << suffix
119
166
  end
120
167
  buffer << META_NOOP
121
168
  end
122
169
 
123
- def meta_arithmetic(key:, delta:, initial:, incr: true, cas: nil, ttl: nil, quiet: false)
170
+ def meta_arithmetic(key:, delta:, initial:, incr: true, cas: nil, ttl: nil, quiet: false,
171
+ p_token: nil, l_token: nil)
124
172
  cmd = "ma #{encoded_key(key)} v"
125
- cmd << " D#{integer_flag(:delta, delta)}" if delta
126
- cmd << " J#{integer_flag(:initial, initial)}" if initial
173
+ cmd << " D#{delta}" if delta
174
+ cmd << " J#{initial}" if initial
127
175
  # Always set a TTL if an initial value is specified
128
- cmd << " N#{ttl ? integer_flag(:ttl, ttl) : 0}" if ttl || initial
176
+ cmd << " N#{ttl || 0}" if ttl || initial
129
177
  cmd << cas_string(cas)
130
178
  cmd << ' q' if quiet
131
179
  cmd << " M#{incr ? 'I' : 'D'}"
180
+ cmd << routing_tokens(p_token: p_token, l_token: l_token)
132
181
  cmd << TERMINATOR
133
182
  end
183
+
184
+ # Builds the wire-format suffix for opaque routing tokens (P and L).
185
+ # memcached itself ignores these; they exist as hints for a proxy or
186
+ # router sitting between the client and memcached. See protocol.txt:
187
+ # "All commands accept tokens 'P' and 'L' which are completely ignored.
188
+ # The arguments to 'P' and 'L' can be used as hints or path
189
+ # specifications to a proxy or router inbetween a client and a
190
+ # memcached daemon."
191
+ #
192
+ # Empty / nil tokens are treated as no-ops. CRLF and null bytes are
193
+ # rejected with ArgumentError to prevent the token from being used as a
194
+ # wire-protocol injection vector (e.g. "foo\r\nflush_all\r\n" would
195
+ # otherwise be parsed as a second command by memcached or any
196
+ # intermediate proxy/LB).
197
+ def routing_tokens(p_token: nil, l_token: nil)
198
+ # Only an empty *String* is a no-op. Checking respond_to?(:empty?)
199
+ # instead would also swallow p_token: [] / {} before the type check
200
+ # below ever runs, silently dropping caller mistakes that should
201
+ # raise "must be a String".
202
+ p_token = nil if p_token.is_a?(String) && p_token.empty?
203
+ l_token = nil if l_token.is_a?(String) && l_token.empty?
204
+ validate_routing_token!('p_token', p_token)
205
+ validate_routing_token!('l_token', l_token)
206
+ return '' unless p_token || l_token
207
+
208
+ s = +''
209
+ s << " P#{p_token}" if p_token
210
+ s << " L#{l_token}" if l_token
211
+ s
212
+ end
134
213
  # rubocop:enable Metrics/CyclomaticComplexity
135
214
  # rubocop:enable Metrics/ParameterLists
136
215
  # rubocop:enable Metrics/PerceivedComplexity
@@ -171,16 +250,15 @@ module Dalli
171
250
 
172
251
  private
173
252
 
174
- # Numeric flag values are written straight into the command line, so a
175
- # 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.
180
- def integer_flag(name, value)
181
- value.is_a?(String) ? Integer(value, 10) : Integer(value)
182
- rescue ArgumentError, TypeError, FloatDomainError
183
- raise ArgumentError, "#{name} must be an Integer, got #{value.inspect}"
253
+ # Disallowed bytes: CR, LF, NUL. Any of these embedded in a routing
254
+ # token would let the caller inject a second wire-protocol command.
255
+ ROUTING_TOKEN_FORBIDDEN = /[\r\n\0]/
256
+ private_constant :ROUTING_TOKEN_FORBIDDEN
257
+
258
+ def validate_routing_token!(name, value)
259
+ return if value.nil?
260
+ raise ArgumentError, "#{name} must be a String, got #{value.class}" unless value.is_a?(String)
261
+ raise ArgumentError, "#{name} must not contain CRLF or null bytes" if value.match?(ROUTING_TOKEN_FORBIDDEN)
184
262
  end
185
263
 
186
264
  def mode_to_token(mode)
@@ -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
 
@@ -236,6 +243,14 @@ module Dalli
236
243
  value_from_tokens(tokens, 'c').to_i
237
244
  end
238
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')
252
+ end
253
+
239
254
  def key_from_tokens(tokens)
240
255
  encoded_key = value_from_tokens(tokens, 'k')
241
256
  if tokens.delete('b')
@@ -260,6 +275,12 @@ module Dalli
260
275
  value_from_tokens(tokens, 'l').to_i
261
276
  end
262
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
282
+ end
283
+
263
284
  def body_len_from_tokens(tokens)
264
285
  value_from_tokens(tokens, 's').to_i
265
286
  end
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.0'
5
5
 
6
- MIN_SUPPORTED_MEMCACHED_VERSION = '1.6'
6
+ MIN_SUPPORTED_MEMCACHED_VERSION = '1.6.27'
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.7
4
+ version: 5.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter M. Goldstein
@@ -87,7 +87,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
87
87
  - !ruby/object:Gem::Version
88
88
  version: '0'
89
89
  requirements: []
90
- rubygems_version: 4.0.21
90
+ rubygems_version: 4.0.18
91
91
  specification_version: 4
92
92
  summary: High performance memcached client for Ruby
93
93
  test_files: []