dalli 4.0.1 → 4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5494b3c5f7ce6c73cb061511ab96c7dafc0ed1f5d10a865310a7adc79c8587c9
4
- data.tar.gz: 1326cd38aad6ba7c7c0c65c2b0305b4c4c566e3a0050df1f8ccee6aa42ee2bc7
3
+ metadata.gz: e342d39d58d552607783486a9c9f0ffa23d9c479079c62cb760ec84a97d064da
4
+ data.tar.gz: 1ae923ede204d0a3e82426404cfb4882f4414a12cb351cf3d04b9ef8653051ce
5
5
  SHA512:
6
- metadata.gz: 5affa5731ead45a7c352628c4c3c471b1fc5c7499e9e63e58278e2c59c309e90fd550774a8808de78ff9daaa53913503810ded029d0879f3a6408f19a4bf67f2
7
- data.tar.gz: 244b909ac405e2f8757b3bfa3fdcea3fb17615f6d329527f63c3392a4921dd8298fa9d6eeaab368bd80112413c593d94f305062732fb28204072526b84eba3d4
6
+ metadata.gz: aeb1bec84092f4e07db884b415d6b25375dc995fff04cc58a1764d9ad0937986ba7ff5db5a665a831bd8ab488b3fa4868fce79834e93a26848bc37ac1c9f9855
7
+ data.tar.gz: 8c5a4e21f4b0a86279bf28edc729c0a1981964438342117f3bc180d15d5cdfc93e395913267db7af66b3907416051be7629e687708c163f4d4015d4e2a768508
data/CHANGELOG.md CHANGED
@@ -1,6 +1,31 @@
1
1
  Dalli Changelog
2
2
  =====================
3
3
 
4
+ 4.1.0
5
+ ==========
6
+
7
+ New Features:
8
+
9
+ - Add `set_multi` for efficient bulk set operations using pipelined requests
10
+ - Add `delete_multi` for efficient bulk delete operations using pipelined requests
11
+ - Add `fetch_with_lock` for thundering herd protection using meta protocol's vivify/recache flags (requires memcached 1.6+)
12
+ - Add thundering herd protection support to meta protocol (requires memcached 1.6+):
13
+ - `N` (vivify) flag for creating stubs on cache miss
14
+ - `R` (recache) flag for winning recache race when TTL is below threshold
15
+ - Response flags `W` (won recache), `X` (stale), `Z` (lost race)
16
+ - `delete_stale` method for marking items as stale instead of deleting
17
+ - Add `get_with_metadata` for advanced cache operations with metadata retrieval (requires memcached 1.6+):
18
+ - Returns hash with `:value`, `:cas`, `:won_recache`, `:stale`, `:lost_recache`
19
+ - Optional `:return_hit_status` returns `:hit_before` (true/false for previous access)
20
+ - Optional `:return_last_access` returns `:last_access` (seconds since last access)
21
+ - Optional `:skip_lru_bump` prevents LRU update on access
22
+ - Optional `:vivify_ttl` and `:recache_ttl` for thundering herd protection
23
+
24
+ Deprecations:
25
+
26
+ - Binary protocol is deprecated and will be removed in Dalli 5.0. Use `protocol: :meta` instead (requires memcached 1.6+)
27
+ - SASL authentication is deprecated and will be removed in Dalli 5.0. Consider using network-level security or memcached's TLS support
28
+
4
29
  4.0.1
5
30
  ==========
6
31
 
data/lib/dalli/client.rb CHANGED
@@ -56,6 +56,7 @@ module Dalli
56
56
  @options = normalize_options(options)
57
57
  @key_manager = ::Dalli::KeyManager.new(@options)
58
58
  @ring = nil
59
+ emit_deprecation_warnings
59
60
  end
60
61
 
61
62
  #
@@ -96,6 +97,51 @@ module Dalli
96
97
  yield value, cas
97
98
  end
98
99
 
100
+ ##
101
+ # Get value with extended metadata using the meta protocol.
102
+ #
103
+ # IMPORTANT: This method requires memcached 1.6+ and the meta protocol (protocol: :meta).
104
+ # It will raise an error if used with the binary protocol.
105
+ #
106
+ # @param key [String] the cache key
107
+ # @param options [Hash] options controlling what metadata to return
108
+ # - :return_cas [Boolean] return the CAS value (default: true)
109
+ # - :return_hit_status [Boolean] return whether item was previously accessed
110
+ # - :return_last_access [Boolean] return seconds since last access
111
+ # - :skip_lru_bump [Boolean] don't bump LRU or update access stats
112
+ #
113
+ # @return [Hash] containing:
114
+ # - :value - the cached value (or nil on miss)
115
+ # - :cas - the CAS value
116
+ # - :hit_before - true/false if previously accessed (only if return_hit_status: true)
117
+ # - :last_access - seconds since last access (only if return_last_access: true)
118
+ #
119
+ # @example Get with hit status
120
+ # result = client.get_with_metadata('key', return_hit_status: true)
121
+ # # => { value: "data", cas: 123, hit_before: true }
122
+ #
123
+ # @example Get with all metadata without affecting LRU
124
+ # result = client.get_with_metadata('key',
125
+ # return_hit_status: true,
126
+ # return_last_access: true,
127
+ # skip_lru_bump: true
128
+ # )
129
+ # # => { value: "data", cas: 123, hit_before: true, last_access: 42 }
130
+ #
131
+ def get_with_metadata(key, options = {})
132
+ raise_unless_meta_protocol!
133
+
134
+ key = key.to_s
135
+ key = @key_manager.validate_key(key)
136
+
137
+ server = ring.server_for_key(key)
138
+ server.request(:meta_get, key, options)
139
+ rescue NetworkError => e
140
+ Dalli.logger.debug { e.inspect }
141
+ Dalli.logger.debug { 'retrying get_with_metadata with new server' }
142
+ retry
143
+ end
144
+
99
145
  ##
100
146
  # Fetch multiple keys efficiently.
101
147
  # If a block is given, yields key/value pairs one at a time.
@@ -149,6 +195,69 @@ module Dalli
149
195
  new_val
150
196
  end
151
197
 
198
+ ##
199
+ # Fetch the value with thundering herd protection using the meta protocol's
200
+ # N (vivify) and R (recache) flags.
201
+ #
202
+ # This method prevents multiple clients from simultaneously regenerating the same
203
+ # cache entry (the "thundering herd" problem). Only one client wins the right to
204
+ # regenerate; other clients receive the stale value (if available) or wait.
205
+ #
206
+ # IMPORTANT: This method requires memcached 1.6+ and the meta protocol (protocol: :meta).
207
+ # It will raise an error if used with the binary protocol.
208
+ #
209
+ # @param key [String] the cache key
210
+ # @param ttl [Integer] time-to-live for the cached value in seconds
211
+ # @param lock_ttl [Integer] how long the lock/stub lives (default: 30 seconds)
212
+ # This is the maximum time other clients will return stale data while
213
+ # waiting for regeneration. Should be longer than your expected regeneration time.
214
+ # @param recache_threshold [Integer, nil] if set, win the recache race when the
215
+ # item's remaining TTL is below this threshold. Useful for proactive recaching.
216
+ # @param req_options [Hash] options passed to set operations (e.g., raw: true)
217
+ #
218
+ # @yield Block to regenerate the value (only called if this client won the race)
219
+ # @return [Object] the cached value (may be stale if another client is regenerating)
220
+ #
221
+ # @example Basic usage
222
+ # client.fetch_with_lock('expensive_key', ttl: 300, lock_ttl: 30) do
223
+ # expensive_database_query
224
+ # end
225
+ #
226
+ # @example With proactive recaching (recache before expiry)
227
+ # client.fetch_with_lock('key', ttl: 300, lock_ttl: 30, recache_threshold: 60) do
228
+ # expensive_operation
229
+ # end
230
+ #
231
+ def fetch_with_lock(key, ttl: nil, lock_ttl: 30, recache_threshold: nil, req_options: nil)
232
+ raise ArgumentError, 'Block is required for fetch_with_lock' unless block_given?
233
+
234
+ raise_unless_meta_protocol!
235
+
236
+ key = key.to_s
237
+ key = @key_manager.validate_key(key)
238
+
239
+ server = ring.server_for_key(key)
240
+ result = server.request(:meta_get, key, {
241
+ vivify_ttl: lock_ttl,
242
+ recache_ttl: recache_threshold
243
+ })
244
+
245
+ if result[:won_recache]
246
+ # This client won the race - regenerate the value
247
+ new_val = yield
248
+ set(key, new_val, ttl_or_default(ttl), req_options)
249
+ new_val
250
+ else
251
+ # Another client is regenerating, or value exists and isn't stale
252
+ # Return the existing value
253
+ result[:value]
254
+ end
255
+ rescue NetworkError => e
256
+ Dalli.logger.debug { e.inspect }
257
+ Dalli.logger.debug { 'retrying fetch_with_lock with new server' }
258
+ retry
259
+ end
260
+
152
261
  ##
153
262
  # compare and swap values using optimistic locking.
154
263
  # Fetch the existing value for key.
@@ -206,6 +315,24 @@ module Dalli
206
315
  set_cas(key, value, 0, ttl, req_options)
207
316
  end
208
317
 
318
+ ##
319
+ # Set multiple keys and values efficiently using pipelining.
320
+ # This method is more efficient than calling set() in a loop because
321
+ # it batches requests by server and uses quiet mode.
322
+ #
323
+ # @param hash [Hash] key-value pairs to set
324
+ # @param ttl [Integer] time-to-live in seconds (optional, uses default if not provided)
325
+ # @param req_options [Hash] options passed to each set operation
326
+ # @return [void]
327
+ #
328
+ # Example:
329
+ # client.set_multi({ 'key1' => 'value1', 'key2' => 'value2' }, 300)
330
+ def set_multi(hash, ttl = nil, req_options = nil)
331
+ return if hash.empty?
332
+
333
+ pipelined_setter.process(hash, ttl_or_default(ttl), req_options)
334
+ end
335
+
209
336
  ##
210
337
  # Set the key-value pair, verifying existing CAS.
211
338
  # Returns the resulting CAS value if succeeded, and falsy otherwise.
@@ -245,6 +372,22 @@ module Dalli
245
372
  delete_cas(key, 0)
246
373
  end
247
374
 
375
+ ##
376
+ # Delete multiple keys efficiently using pipelining.
377
+ # This method is more efficient than calling delete() in a loop because
378
+ # it batches requests by server and uses quiet mode.
379
+ #
380
+ # @param keys [Array<String>] keys to delete
381
+ # @return [void]
382
+ #
383
+ # Example:
384
+ # client.delete_multi(['key1', 'key2', 'key3'])
385
+ def delete_multi(keys)
386
+ return if keys.empty?
387
+
388
+ pipelined_deleter.process(keys)
389
+ end
390
+
248
391
  ##
249
392
  # Append value to the value already stored on the server for 'key'.
250
393
  # Appending only works for values stored with :raw => true.
@@ -445,5 +588,23 @@ module Dalli
445
588
  def pipelined_getter
446
589
  PipelinedGetter.new(ring, @key_manager)
447
590
  end
591
+
592
+ def pipelined_setter
593
+ PipelinedSetter.new(ring, @key_manager)
594
+ end
595
+
596
+ def pipelined_deleter
597
+ PipelinedDeleter.new(ring, @key_manager)
598
+ end
599
+
600
+ def raise_unless_meta_protocol!
601
+ return if protocol_implementation == Dalli::Protocol::Meta
602
+
603
+ raise Dalli::DalliError,
604
+ 'This operation requires the meta protocol (memcached 1.6+). ' \
605
+ 'Use protocol: :meta when creating the client.'
606
+ end
607
+
608
+ include ProtocolDeprecations
448
609
  end
449
610
  end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dalli
4
+ ##
5
+ # Contains logic for the pipelined delete operations implemented by the client.
6
+ # Efficiently deletes multiple keys by grouping requests by server
7
+ # and using quiet mode to minimize round trips.
8
+ ##
9
+ class PipelinedDeleter
10
+ def initialize(ring, key_manager)
11
+ @ring = ring
12
+ @key_manager = key_manager
13
+ end
14
+
15
+ ##
16
+ # Deletes multiple keys from memcached.
17
+ #
18
+ # @param keys [Array<String>] keys to delete
19
+ # @return [void]
20
+ ##
21
+ def process(keys)
22
+ return if keys.empty?
23
+
24
+ @ring.lock do
25
+ servers = setup_requests(keys)
26
+ finish_requests(servers)
27
+ end
28
+ rescue NetworkError => e
29
+ Dalli.logger.debug { e.inspect }
30
+ Dalli.logger.debug { 'retrying pipelined deletes because of network error' }
31
+ retry
32
+ end
33
+
34
+ private
35
+
36
+ def setup_requests(keys)
37
+ groups = groups_for_keys(keys)
38
+ make_delete_requests(groups)
39
+ groups.keys
40
+ end
41
+
42
+ ##
43
+ # Loop through the server-grouped sets of keys, writing
44
+ # the corresponding quiet delete requests to the appropriate servers
45
+ ##
46
+ def make_delete_requests(groups)
47
+ groups.each do |server, keys_for_server|
48
+ keys_for_server.each do |key|
49
+ server.request(:pipelined_delete, key)
50
+ rescue DalliError, NetworkError => e
51
+ Dalli.logger.debug { e.inspect }
52
+ Dalli.logger.debug { "unable to delete key #{key} for server #{server.name}" }
53
+ end
54
+ end
55
+ end
56
+
57
+ ##
58
+ # Sends noop to each server to flush responses and ensure all deletes complete.
59
+ ##
60
+ def finish_requests(servers)
61
+ servers.each do |server|
62
+ server.request(:noop)
63
+ rescue DalliError, NetworkError => e
64
+ Dalli.logger.debug { e.inspect }
65
+ Dalli.logger.debug { "unable to complete pipelined delete on server #{server.name}" }
66
+ end
67
+ end
68
+
69
+ def groups_for_keys(keys)
70
+ validated_keys = keys.map { |k| @key_manager.validate_key(k.to_s) }
71
+ groups = @ring.keys_grouped_by_server(validated_keys)
72
+
73
+ if (unfound_keys = groups.delete(nil))
74
+ Dalli.logger.debug do
75
+ "unable to delete #{unfound_keys.length} keys because no matching server was found"
76
+ end
77
+ end
78
+
79
+ groups
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dalli
4
+ ##
5
+ # Contains logic for the pipelined set operations implemented by the client.
6
+ # Efficiently writes multiple key-value pairs by grouping requests by server
7
+ # and using quiet mode to minimize round trips.
8
+ ##
9
+ class PipelinedSetter
10
+ def initialize(ring, key_manager)
11
+ @ring = ring
12
+ @key_manager = key_manager
13
+ end
14
+
15
+ ##
16
+ # Writes multiple key-value pairs to memcached.
17
+ # Raises an error if any server is unavailable.
18
+ #
19
+ # @param hash [Hash] key-value pairs to set
20
+ # @param ttl [Integer] time-to-live in seconds
21
+ # @param req_options [Hash] options passed to each set operation
22
+ # @return [void]
23
+ ##
24
+ def process(hash, ttl, req_options)
25
+ return if hash.empty?
26
+
27
+ @ring.lock do
28
+ servers = setup_requests(hash, ttl, req_options)
29
+ finish_requests(servers)
30
+ end
31
+ rescue NetworkError => e
32
+ Dalli.logger.debug { e.inspect }
33
+ Dalli.logger.debug { 'retrying pipelined sets because of network error' }
34
+ retry
35
+ end
36
+
37
+ private
38
+
39
+ def setup_requests(hash, ttl, req_options)
40
+ groups = groups_for_keys(hash.keys)
41
+ make_set_requests(groups, hash, ttl, req_options)
42
+ groups.keys
43
+ end
44
+
45
+ ##
46
+ # Loop through the server-grouped sets of keys, writing
47
+ # the corresponding quiet set requests to the appropriate servers
48
+ ##
49
+ def make_set_requests(groups, hash, ttl, req_options)
50
+ groups.each do |server, keys_for_server|
51
+ keys_for_server.each do |key|
52
+ original_key = @key_manager.key_without_namespace(key)
53
+ value = hash[original_key]
54
+ server.request(:pipelined_set, key, value, ttl, req_options)
55
+ rescue DalliError, NetworkError => e
56
+ Dalli.logger.debug { e.inspect }
57
+ Dalli.logger.debug { "unable to set key #{key} for server #{server.name}" }
58
+ end
59
+ end
60
+ end
61
+
62
+ ##
63
+ # Sends noop to each server to flush responses and ensure all writes complete.
64
+ ##
65
+ def finish_requests(servers)
66
+ servers.each do |server|
67
+ server.request(:noop)
68
+ rescue DalliError, NetworkError => e
69
+ Dalli.logger.debug { e.inspect }
70
+ Dalli.logger.debug { "unable to complete pipelined set on server #{server.name}" }
71
+ end
72
+ end
73
+
74
+ def groups_for_keys(keys)
75
+ validated_keys = keys.map { |k| @key_manager.validate_key(k.to_s) }
76
+ groups = @ring.keys_grouped_by_server(validated_keys)
77
+
78
+ if (unfound_keys = groups.delete(nil))
79
+ Dalli.logger.debug do
80
+ "unable to set #{unfound_keys.length} keys because no matching server was found"
81
+ end
82
+ end
83
+
84
+ groups
85
+ end
86
+ end
87
+ end
@@ -56,6 +56,12 @@ module Dalli
56
56
  storage_req(opkey, key, value, ttl, cas, options)
57
57
  end
58
58
 
59
+ # Pipelined set - writes a quiet set request without reading response.
60
+ # Used by PipelinedSetter for bulk operations.
61
+ def pipelined_set(key, value, ttl, options)
62
+ storage_req(:setq, key, value, ttl, 0, options)
63
+ end
64
+
59
65
  def add(key, value, ttl, options)
60
66
  opkey = quiet? ? :addq : :add
61
67
  storage_req(opkey, key, value, ttl, 0, options)
@@ -102,6 +108,13 @@ module Dalli
102
108
  response_processor.delete unless quiet?
103
109
  end
104
110
 
111
+ # Pipelined delete - writes a quiet delete request without reading response.
112
+ # Used by PipelinedDeleter for bulk operations.
113
+ def pipelined_delete(key)
114
+ req = RequestFormatter.standard_request(opkey: :deleteq, key: key, cas: 0)
115
+ write(req)
116
+ end
117
+
105
118
  # Arithmetic Commands
106
119
  def decr(key, count, ttl, initial)
107
120
  opkey = quiet? ? :decrq : :decr
@@ -15,13 +15,40 @@ module Dalli
15
15
  # rubocop:disable Metrics/CyclomaticComplexity
16
16
  # rubocop:disable Metrics/ParameterLists
17
17
  # rubocop:disable Metrics/PerceivedComplexity
18
- def self.meta_get(key:, value: true, return_cas: false, ttl: nil, base64: false, quiet: false)
18
+ #
19
+ # Meta get flags:
20
+ #
21
+ # Thundering herd protection:
22
+ # - vivify_ttl (N flag): On miss, create a stub item and return W flag. The TTL
23
+ # specifies how long the stub lives. Other clients see X (stale) and Z (lost race).
24
+ # - recache_ttl (R flag): If item's remaining TTL is below this threshold, return W
25
+ # flag to indicate this client should recache. Other clients get Z (lost race).
26
+ #
27
+ # Metadata flags:
28
+ # - return_hit_status (h flag): Return whether item has been hit before (0 or 1)
29
+ # - return_last_access (l flag): Return seconds since item was last accessed
30
+ # - skip_lru_bump (u flag): Don't bump item in LRU, don't update hit status or last access
31
+ #
32
+ # Response flags (parsed by response processor):
33
+ # - W: Client won the right to recache this item
34
+ # - X: Item is stale (another client is regenerating)
35
+ # - Z: Client lost the recache race (another client is already regenerating)
36
+ # - h0/h1: Hit status (0 = first access, 1 = previously accessed)
37
+ # - l<N>: Seconds since last access
38
+ def self.meta_get(key:, value: true, return_cas: false, ttl: nil, base64: false, quiet: false,
39
+ vivify_ttl: nil, recache_ttl: nil,
40
+ return_hit_status: false, return_last_access: false, skip_lru_bump: false)
19
41
  cmd = "mg #{key}"
20
42
  cmd << ' v f' if value
21
43
  cmd << ' c' if return_cas
22
44
  cmd << ' b' if base64
23
45
  cmd << " T#{ttl}" if ttl
24
46
  cmd << ' k q s' if quiet # Return the key in the response if quiet
47
+ cmd << " N#{vivify_ttl}" if vivify_ttl # Thundering herd: vivify on miss
48
+ cmd << " R#{recache_ttl}" if recache_ttl # Thundering herd: win recache if TTL below threshold
49
+ cmd << ' h' if return_hit_status # Return hit status (0 or 1)
50
+ cmd << ' l' if return_last_access # Return seconds since last access
51
+ cmd << ' u' if skip_lru_bump # Don't bump LRU or update access stats
25
52
  cmd + TERMINATOR
26
53
  end
27
54
 
@@ -37,11 +64,15 @@ module Dalli
37
64
  cmd << TERMINATOR
38
65
  end
39
66
 
40
- def self.meta_delete(key:, cas: nil, ttl: nil, base64: false, quiet: false)
67
+ # Thundering herd protection flag:
68
+ # - stale (I flag): Instead of deleting the item, mark it as stale. Other clients
69
+ # using N/R flags will see the X flag and know the item is being regenerated.
70
+ def self.meta_delete(key:, cas: nil, ttl: nil, base64: false, quiet: false, stale: false)
41
71
  cmd = "md #{key}"
42
72
  cmd << ' b' if base64
43
73
  cmd << cas_string(cas)
44
74
  cmd << " T#{ttl}" if ttl
75
+ cmd << ' I' if stale # Mark stale instead of deleting
45
76
  cmd << ' q' if quiet
46
77
  cmd + TERMINATOR
47
78
  end
@@ -51,6 +51,41 @@ module Dalli
51
51
  tokens.first == EN ? nil : true
52
52
  end
53
53
 
54
+ # Returns a hash with all requested metadata:
55
+ # - :value - the cached value (or nil if miss)
56
+ # - :cas - the CAS value (if return_cas was requested)
57
+ # - :won_recache - true if client won the right to recache (W flag)
58
+ # - :stale - true if the item is stale (X flag)
59
+ # - :lost_recache - true if another client is already recaching (Z flag)
60
+ # - :hit_before - true/false if item was previously accessed (h flag, if requested)
61
+ # - :last_access - seconds since last access (l flag, if requested)
62
+ #
63
+ # Used by meta_get for comprehensive metadata retrieval.
64
+ # Supports thundering herd protection (N/R flags) and metadata flags (h/l/u).
65
+ def meta_get_with_metadata(cache_nils: false, return_hit_status: false, return_last_access: false)
66
+ tokens = error_on_unexpected!([VA, EN, HD])
67
+ result = build_metadata_result(tokens)
68
+ result[:hit_before] = hit_status_from_tokens(tokens) if return_hit_status
69
+ result[:last_access] = last_access_from_tokens(tokens) if return_last_access
70
+ result[:value] = parse_value_from_tokens(tokens, cache_nils)
71
+ result
72
+ end
73
+
74
+ def build_metadata_result(tokens)
75
+ {
76
+ value: nil, cas: cas_from_tokens(tokens),
77
+ won_recache: tokens.include?('W'), stale: tokens.include?('X'),
78
+ lost_recache: tokens.include?('Z')
79
+ }
80
+ end
81
+
82
+ def parse_value_from_tokens(tokens, cache_nils)
83
+ return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
84
+ return unless tokens.first == VA
85
+
86
+ @value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens))
87
+ end
88
+
54
89
  def meta_set_with_cas
55
90
  tokens = error_on_unexpected!([HD, NS, NF, EX])
56
91
  return false unless tokens.first == HD
@@ -190,6 +225,21 @@ module Dalli
190
225
  KeyRegularizer.decode(encoded_key, base64_encoded)
191
226
  end
192
227
 
228
+ # Returns true if item was previously hit, false if first access, nil if not requested
229
+ # The h flag returns h0 (first access) or h1 (previously accessed)
230
+ def hit_status_from_tokens(tokens)
231
+ hit_token = tokens.find { |t| t.start_with?('h') && t.length == 2 }
232
+ return nil unless hit_token
233
+
234
+ hit_token[1] == '1'
235
+ end
236
+
237
+ # Returns seconds since last access, or nil if not requested
238
+ # The l flag returns l<seconds>
239
+ def last_access_from_tokens(tokens)
240
+ value_from_tokens(tokens, 'l')&.to_i
241
+ end
242
+
193
243
  def body_len_from_tokens(tokens)
194
244
  value_from_tokens(tokens, 's')&.to_i
195
245
  end
@@ -60,12 +60,71 @@ module Dalli
60
60
  response_processor.meta_get_with_value_and_cas
61
61
  end
62
62
 
63
+ # Comprehensive meta get with support for all metadata flags.
64
+ # @note Requires memcached 1.6+ (meta protocol feature)
65
+ #
66
+ # This is the full-featured get method that supports:
67
+ # - Thundering herd protection (vivify_ttl, recache_ttl)
68
+ # - Item metadata (hit_status, last_access)
69
+ # - LRU control (skip_lru_bump)
70
+ #
71
+ # @param key [String] the key to retrieve
72
+ # @param options [Hash] options controlling what metadata to return
73
+ # - :vivify_ttl [Integer] creates a stub on miss with this TTL (N flag)
74
+ # - :recache_ttl [Integer] wins recache race if remaining TTL is below this (R flag)
75
+ # - :return_hit_status [Boolean] return whether item was previously accessed (h flag)
76
+ # - :return_last_access [Boolean] return seconds since last access (l flag)
77
+ # - :skip_lru_bump [Boolean] don't bump LRU or update access stats (u flag)
78
+ # - :cache_nils [Boolean] whether to cache nil values
79
+ # @return [Hash] containing:
80
+ # - :value - the cached value (or nil on miss)
81
+ # - :cas - the CAS value
82
+ # - :won_recache - true if client won recache race (W flag)
83
+ # - :stale - true if item is stale (X flag)
84
+ # - :lost_recache - true if another client is recaching (Z flag)
85
+ # - :hit_before - true/false if previously accessed (only if return_hit_status: true)
86
+ # - :last_access - seconds since last access (only if return_last_access: true)
87
+ def meta_get(key, options = {})
88
+ encoded_key, base64 = KeyRegularizer.encode(key)
89
+ req = RequestFormatter.meta_get(
90
+ key: encoded_key, value: true, return_cas: true, base64: base64,
91
+ vivify_ttl: options[:vivify_ttl], recache_ttl: options[:recache_ttl],
92
+ return_hit_status: options[:return_hit_status],
93
+ return_last_access: options[:return_last_access], skip_lru_bump: options[:skip_lru_bump]
94
+ )
95
+ write(req)
96
+ response_processor.meta_get_with_metadata(
97
+ cache_nils: cache_nils?(options), return_hit_status: options[:return_hit_status],
98
+ return_last_access: options[:return_last_access]
99
+ )
100
+ end
101
+
102
+ # Delete with stale invalidation instead of actual deletion.
103
+ # Used with thundering herd protection to mark items as stale rather than removing them.
104
+ # @note Requires memcached 1.6+ (meta protocol feature)
105
+ #
106
+ # @param key [String] the key to invalidate
107
+ # @param cas [Integer] optional CAS value for compare-and-swap
108
+ # @return [Boolean] true if successful
109
+ def delete_stale(key, cas = nil)
110
+ encoded_key, base64 = KeyRegularizer.encode(key)
111
+ req = RequestFormatter.meta_delete(key: encoded_key, cas: cas, base64: base64, stale: true)
112
+ write(req)
113
+ response_processor.meta_delete
114
+ end
115
+
63
116
  # Storage Commands
64
117
  def set(key, value, ttl, cas, options)
65
118
  write_storage_req(:set, key, value, ttl, cas, options)
66
119
  response_processor.meta_set_with_cas unless quiet?
67
120
  end
68
121
 
122
+ # Pipelined set - writes a quiet set request without reading response.
123
+ # Used by PipelinedSetter for bulk operations.
124
+ def pipelined_set(key, value, ttl, options)
125
+ write_storage_req(:set, key, value, ttl, nil, options, quiet: true)
126
+ end
127
+
69
128
  def add(key, value, ttl, options)
70
129
  write_storage_req(:add, key, value, ttl, nil, options)
71
130
  response_processor.meta_set_with_cas unless quiet?
@@ -77,13 +136,13 @@ module Dalli
77
136
  end
78
137
 
79
138
  # rubocop:disable Metrics/ParameterLists
80
- def write_storage_req(mode, key, raw_value, ttl = nil, cas = nil, options = {})
139
+ def write_storage_req(mode, key, raw_value, ttl = nil, cas = nil, options = {}, quiet: quiet?)
81
140
  (value, bitflags) = @value_marshaller.store(key, raw_value, options)
82
141
  ttl = TtlSanitizer.sanitize(ttl) if ttl
83
142
  encoded_key, base64 = KeyRegularizer.encode(key)
84
143
  req = RequestFormatter.meta_set(key: encoded_key, value: value,
85
144
  bitflags: bitflags, cas: cas,
86
- ttl: ttl, mode: mode, quiet: quiet?, base64: base64)
145
+ ttl: ttl, mode: mode, quiet: quiet, base64: base64)
87
146
  write(req)
88
147
  write(value)
89
148
  write(TERMINATOR)
@@ -121,6 +180,14 @@ module Dalli
121
180
  response_processor.meta_delete unless quiet?
122
181
  end
123
182
 
183
+ # Pipelined delete - writes a quiet delete request without reading response.
184
+ # Used by PipelinedDeleter for bulk operations.
185
+ def pipelined_delete(key)
186
+ encoded_key, base64 = KeyRegularizer.encode(key)
187
+ req = RequestFormatter.meta_delete(key: encoded_key, base64: base64, quiet: true)
188
+ write(req)
189
+ end
190
+
124
191
  # Arithmetic Commands
125
192
  def decr(key, count, ttl, initial)
126
193
  decr_incr false, key, count, ttl, initial
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dalli
4
+ ##
5
+ # Handles deprecation warnings for protocol and authentication features
6
+ # that will be removed in Dalli 5.0.
7
+ ##
8
+ module ProtocolDeprecations
9
+ BINARY_PROTOCOL_DEPRECATION_MESSAGE = <<~MSG.chomp
10
+ [DEPRECATION] The binary protocol is deprecated and will be removed in Dalli 5.0. \
11
+ Please use `protocol: :meta` instead. The meta protocol requires memcached 1.6+. \
12
+ See https://github.com/petergoldstein/dalli for migration details.
13
+ MSG
14
+
15
+ SASL_AUTH_DEPRECATION_MESSAGE = <<~MSG.chomp
16
+ [DEPRECATION] SASL authentication is deprecated and will be removed in Dalli 5.0. \
17
+ SASL is only supported by the binary protocol, which is being removed. \
18
+ Consider using network-level security (firewall rules, VPN) or memcached's TLS support instead.
19
+ MSG
20
+
21
+ private
22
+
23
+ def emit_deprecation_warnings
24
+ emit_binary_protocol_deprecation_warning
25
+ emit_sasl_auth_deprecation_warning
26
+ end
27
+
28
+ def emit_binary_protocol_deprecation_warning
29
+ protocol = @options[:protocol]
30
+ # Binary is used when protocol is nil, :binary, or 'binary'
31
+ return if protocol.to_s == 'meta'
32
+
33
+ warn BINARY_PROTOCOL_DEPRECATION_MESSAGE
34
+ Dalli.logger.warn(BINARY_PROTOCOL_DEPRECATION_MESSAGE)
35
+ end
36
+
37
+ def emit_sasl_auth_deprecation_warning
38
+ username = @options[:username] || ENV.fetch('MEMCACHE_USERNAME', nil)
39
+ return unless username
40
+
41
+ warn SASL_AUTH_DEPRECATION_MESSAGE
42
+ Dalli.logger.warn(SASL_AUTH_DEPRECATION_MESSAGE)
43
+ end
44
+ end
45
+ 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 = '4.0.1'
4
+ VERSION = '4.1.0'
5
5
 
6
6
  MIN_SUPPORTED_MEMCACHED_VERSION = '1.4'
7
7
  end
data/lib/dalli.rb CHANGED
@@ -58,9 +58,12 @@ end
58
58
  require_relative 'dalli/version'
59
59
 
60
60
  require_relative 'dalli/compressor'
61
+ require_relative 'dalli/protocol_deprecations'
61
62
  require_relative 'dalli/client'
62
63
  require_relative 'dalli/key_manager'
63
64
  require_relative 'dalli/pipelined_getter'
65
+ require_relative 'dalli/pipelined_setter'
66
+ require_relative 'dalli/pipelined_deleter'
64
67
  require_relative 'dalli/ring'
65
68
  require_relative 'dalli/protocol'
66
69
  require_relative 'dalli/protocol/base'
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: 4.0.1
4
+ version: 4.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter M. Goldstein
@@ -43,7 +43,9 @@ files:
43
43
  - lib/dalli/key_manager.rb
44
44
  - lib/dalli/options.rb
45
45
  - lib/dalli/pid_cache.rb
46
+ - lib/dalli/pipelined_deleter.rb
46
47
  - lib/dalli/pipelined_getter.rb
48
+ - lib/dalli/pipelined_setter.rb
47
49
  - lib/dalli/protocol.rb
48
50
  - lib/dalli/protocol/base.rb
49
51
  - lib/dalli/protocol/binary.rb
@@ -63,6 +65,7 @@ files:
63
65
  - lib/dalli/protocol/value_compressor.rb
64
66
  - lib/dalli/protocol/value_marshaller.rb
65
67
  - lib/dalli/protocol/value_serializer.rb
68
+ - lib/dalli/protocol_deprecations.rb
66
69
  - lib/dalli/ring.rb
67
70
  - lib/dalli/servers_arg_normalizer.rb
68
71
  - lib/dalli/socket.rb