dalli 5.0.6 → 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,27 +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
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
49
  cmd << " N#{vivify_ttl}" if vivify_ttl # Thundering herd: vivify on miss
49
50
  cmd << " R#{recache_ttl}" if recache_ttl # Thundering herd: win recache if TTL below threshold
50
51
  cmd << ' h' if return_hit_status # Return hit status (0 or 1)
51
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)
52
54
  cmd << ' u' if skip_lru_bump # Don't bump LRU or update access stats
53
55
  cmd << TERMINATOR
54
56
  end
55
57
 
56
- 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)
57
59
  # In raw mode: "mg <key> v k q s\r\n" (no f flag, key at index 2)
58
60
  # Normal mode: "mg <key> v f k q s\r\n" (key at index 3)
59
- 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
+
60
73
  buffer = ''.b
61
74
  keys.each do |key|
62
75
  buffer << 'mg ' << encoded_key(key) << post_get
@@ -64,7 +77,8 @@ module Dalli
64
77
  buffer << 'mn' << TERMINATOR
65
78
  end
66
79
 
67
- 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)
68
82
  base64 = KeyRegularizer.required?(key)
69
83
  key = KeyRegularizer.encode(key) if base64
70
84
  cmd = "ms #{key} #{value.bytesize}"
@@ -77,10 +91,15 @@ module Dalli
77
91
  cmd << " T#{ttl}" if ttl
78
92
  cmd << " M#{mode_to_token(mode)}"
79
93
  cmd << ' q' if quiet
94
+ cmd << routing_tokens(p_token: p_token, l_token: l_token)
80
95
  cmd << TERMINATOR
81
96
  end
82
97
 
83
- def multi_meta_set(entries, ttl: nil)
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
+
84
103
  buffer = ''.b
85
104
  entries.each do |key, pair|
86
105
  value, bitflags = pair
@@ -88,12 +107,12 @@ module Dalli
88
107
  base64 = KeyRegularizer.required?(key)
89
108
  key = KeyRegularizer.encode(key) if base64
90
109
 
91
- # 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"
92
111
  buffer << "ms #{key} #{value.bytesize} c"
93
112
  buffer << ' b' if base64
94
113
  buffer << " F#{bitflags}" if bitflags
95
114
  buffer << " T#{ttl}" if ttl
96
- buffer << ' MS q' << TERMINATOR << value << TERMINATOR
115
+ buffer << ' MS q' << token_suffix << TERMINATOR << value << TERMINATOR
97
116
  end
98
117
  buffer << META_NOOP
99
118
  end
@@ -101,24 +120,55 @@ module Dalli
101
120
  # Thundering herd protection flag:
102
121
  # - stale (I flag): Instead of deleting the item, mark it as stale. Other clients
103
122
  # using N/R flags will see the X flag and know the item is being regenerated.
104
- 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
+
105
140
  cmd = "md #{encoded_key(key)}"
106
141
  cmd << cas_string(cas)
107
- cmd << " T#{ttl}" if ttl
108
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
109
145
  cmd << ' q' if quiet
146
+ cmd << routing_tokens(p_token: p_token, l_token: l_token)
110
147
  cmd << TERMINATOR
111
148
  end
112
149
 
113
- 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
+
114
163
  buffer = ''.b
115
164
  keys.each do |key|
116
- buffer << 'md ' << encoded_key(key) << ' q' << TERMINATOR
165
+ buffer << 'md ' << encoded_key(key) << suffix
117
166
  end
118
167
  buffer << META_NOOP
119
168
  end
120
169
 
121
- 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)
122
172
  cmd = "ma #{encoded_key(key)} v"
123
173
  cmd << " D#{delta}" if delta
124
174
  cmd << " J#{initial}" if initial
@@ -127,8 +177,39 @@ module Dalli
127
177
  cmd << cas_string(cas)
128
178
  cmd << ' q' if quiet
129
179
  cmd << " M#{incr ? 'I' : 'D'}"
180
+ cmd << routing_tokens(p_token: p_token, l_token: l_token)
130
181
  cmd << TERMINATOR
131
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
132
213
  # rubocop:enable Metrics/CyclomaticComplexity
133
214
  # rubocop:enable Metrics/ParameterLists
134
215
  # rubocop:enable Metrics/PerceivedComplexity
@@ -169,6 +250,17 @@ module Dalli
169
250
 
170
251
  private
171
252
 
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)
262
+ end
263
+
172
264
  def mode_to_token(mode)
173
265
  case mode
174
266
  when :add
@@ -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.6'
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.6
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.16
90
+ rubygems_version: 4.0.18
91
91
  specification_version: 4
92
92
  summary: High performance memcached client for Ruby
93
93
  test_files: []