dalli 4.0.1 → 4.2.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.
@@ -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
@@ -23,10 +23,17 @@ module Dalli
23
23
  def initialize(attribs, client_options = {})
24
24
  hostname, port, socket_type, @weight, user_creds = ServerConfigParser.parse(attribs)
25
25
  @options = client_options.merge(user_creds)
26
- @value_marshaller = client_options[:raw] ? StringMarshaller.new(@options) : ValueMarshaller.new(@options)
26
+ @raw_mode = client_options[:raw]
27
+ @value_marshaller = @raw_mode ? StringMarshaller.new(@options) : ValueMarshaller.new(@options)
27
28
  @connection_manager = ConnectionManager.new(hostname, port, socket_type, @options)
28
29
  end
29
30
 
31
+ # Returns true if client is in raw mode (no serialization/compression).
32
+ # In raw mode, we can skip requesting bitflags from the server.
33
+ def raw_mode?
34
+ @raw_mode
35
+ end
36
+
30
37
  # Chokepoint method for error handling and ensuring liveness
31
38
  def request(opkey, *args)
32
39
  verify_state(opkey)
@@ -22,6 +22,7 @@ module Dalli
22
22
  def get(key, options = nil)
23
23
  req = RequestFormatter.standard_request(opkey: :get, key: key)
24
24
  write(req)
25
+ @connection_manager.flush
25
26
  response_processor.get(cache_nils: cache_nils?(options))
26
27
  end
27
28
 
@@ -33,12 +34,14 @@ module Dalli
33
34
  ttl = TtlSanitizer.sanitize(ttl)
34
35
  req = RequestFormatter.standard_request(opkey: :gat, key: key, ttl: ttl)
35
36
  write(req)
37
+ @connection_manager.flush
36
38
  response_processor.get(cache_nils: cache_nils?(options))
37
39
  end
38
40
 
39
41
  def touch(key, ttl)
40
42
  ttl = TtlSanitizer.sanitize(ttl)
41
43
  write(RequestFormatter.standard_request(opkey: :touch, key: key, ttl: ttl))
44
+ @connection_manager.flush
42
45
  response_processor.generic_response
43
46
  end
44
47
 
@@ -47,6 +50,7 @@ module Dalli
47
50
  def cas(key)
48
51
  req = RequestFormatter.standard_request(opkey: :get, key: key)
49
52
  write(req)
53
+ @connection_manager.flush
50
54
  response_processor.data_cas_response
51
55
  end
52
56
 
@@ -56,6 +60,12 @@ module Dalli
56
60
  storage_req(opkey, key, value, ttl, cas, options)
57
61
  end
58
62
 
63
+ # Pipelined set - writes a quiet set request without reading response.
64
+ # Used by PipelinedSetter for bulk operations.
65
+ def pipelined_set(key, value, ttl, options)
66
+ storage_req(:setq, key, value, ttl, 0, options)
67
+ end
68
+
59
69
  def add(key, value, ttl, options)
60
70
  opkey = quiet? ? :addq : :add
61
71
  storage_req(opkey, key, value, ttl, 0, options)
@@ -75,6 +85,7 @@ module Dalli
75
85
  value: value, bitflags: bitflags,
76
86
  ttl: ttl, cas: cas)
77
87
  write(req)
88
+ @connection_manager.flush unless quiet?
78
89
  response_processor.storage_response unless quiet?
79
90
  end
80
91
  # rubocop:enable Metrics/ParameterLists
@@ -91,6 +102,7 @@ module Dalli
91
102
 
92
103
  def write_append_prepend(opkey, key, value)
93
104
  write(RequestFormatter.standard_request(opkey: opkey, key: key, value: value))
105
+ @connection_manager.flush unless quiet?
94
106
  response_processor.no_body_response unless quiet?
95
107
  end
96
108
 
@@ -99,9 +111,17 @@ module Dalli
99
111
  opkey = quiet? ? :deleteq : :delete
100
112
  req = RequestFormatter.standard_request(opkey: opkey, key: key, cas: cas)
101
113
  write(req)
114
+ @connection_manager.flush unless quiet?
102
115
  response_processor.delete unless quiet?
103
116
  end
104
117
 
118
+ # Pipelined delete - writes a quiet delete request without reading response.
119
+ # Used by PipelinedDeleter for bulk operations.
120
+ def pipelined_delete(key)
121
+ req = RequestFormatter.standard_request(opkey: :deleteq, key: key, cas: 0)
122
+ write(req)
123
+ end
124
+
105
125
  # Arithmetic Commands
106
126
  def decr(key, count, ttl, initial)
107
127
  opkey = quiet? ? :decrq : :decr
@@ -126,6 +146,7 @@ module Dalli
126
146
  initial ||= 0
127
147
  write(RequestFormatter.decr_incr_request(opkey: opkey, key: key,
128
148
  count: count, initial: initial, expiry: expiry))
149
+ @connection_manager.flush unless quiet?
129
150
  response_processor.decr_incr unless quiet?
130
151
  end
131
152
 
@@ -133,6 +154,7 @@ module Dalli
133
154
  def flush(ttl = 0)
134
155
  opkey = quiet? ? :flushq : :flush
135
156
  write(RequestFormatter.standard_request(opkey: opkey, ttl: ttl))
157
+ @connection_manager.flush unless quiet?
136
158
  response_processor.no_body_response unless quiet?
137
159
  end
138
160
 
@@ -146,22 +168,26 @@ module Dalli
146
168
  def stats(info = '')
147
169
  req = RequestFormatter.standard_request(opkey: :stat, key: info)
148
170
  write(req)
171
+ @connection_manager.flush
149
172
  response_processor.stats
150
173
  end
151
174
 
152
175
  def reset_stats
153
176
  write(RequestFormatter.standard_request(opkey: :stat, key: 'reset'))
177
+ @connection_manager.flush
154
178
  response_processor.reset
155
179
  end
156
180
 
157
181
  def version
158
182
  write(RequestFormatter.standard_request(opkey: :version))
183
+ @connection_manager.flush
159
184
  response_processor.version
160
185
  end
161
186
 
162
187
  def write_noop
163
188
  req = RequestFormatter.standard_request(opkey: :noop)
164
189
  write(req)
190
+ @connection_manager.flush
165
191
  end
166
192
 
167
193
  require_relative 'binary/request_formatter'
@@ -53,6 +53,7 @@ module Dalli
53
53
  Dalli.logger.debug { "Dalli::Server#connect #{name}" }
54
54
 
55
55
  @sock = memcached_socket
56
+ @sock.sync = false # Enable buffered I/O for better performance
56
57
  @pid = PIDCache.pid
57
58
  @request_in_progress = false
58
59
  rescue SystemCallError, *TIMEOUT_ERRORS, EOFError, SocketError => e
@@ -166,6 +167,12 @@ module Dalli
166
167
  error_on_request!(e)
167
168
  end
168
169
 
170
+ def flush
171
+ @sock.flush
172
+ rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS => e
173
+ error_on_request!(e)
174
+ end
175
+
169
176
  # Non-blocking read. Here to support the operation
170
177
  # of the get_multi operation
171
178
  def read_nonblock
@@ -15,13 +15,43 @@ 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,
41
+ skip_flags: false)
19
42
  cmd = "mg #{key}"
20
- cmd << ' v f' if value
43
+ # In raw mode (skip_flags: true), we don't request bitflags since they're not used.
44
+ # This saves 2 bytes per request and skips parsing on response.
45
+ cmd << (skip_flags ? ' v' : ' v f') if value
21
46
  cmd << ' c' if return_cas
22
47
  cmd << ' b' if base64
23
48
  cmd << " T#{ttl}" if ttl
24
49
  cmd << ' k q s' if quiet # Return the key in the response if quiet
50
+ cmd << " N#{vivify_ttl}" if vivify_ttl # Thundering herd: vivify on miss
51
+ cmd << " R#{recache_ttl}" if recache_ttl # Thundering herd: win recache if TTL below threshold
52
+ cmd << ' h' if return_hit_status # Return hit status (0 or 1)
53
+ cmd << ' l' if return_last_access # Return seconds since last access
54
+ cmd << ' u' if skip_lru_bump # Don't bump LRU or update access stats
25
55
  cmd + TERMINATOR
26
56
  end
27
57
 
@@ -37,11 +67,15 @@ module Dalli
37
67
  cmd << TERMINATOR
38
68
  end
39
69
 
40
- def self.meta_delete(key:, cas: nil, ttl: nil, base64: false, quiet: false)
70
+ # Thundering herd protection flag:
71
+ # - stale (I flag): Instead of deleting the item, mark it as stale. Other clients
72
+ # using N/R flags will see the X flag and know the item is being regenerated.
73
+ def self.meta_delete(key:, cas: nil, ttl: nil, base64: false, quiet: false, stale: false)
41
74
  cmd = "md #{key}"
42
75
  cmd << ' b' if base64
43
76
  cmd << cas_string(cas)
44
77
  cmd << " T#{ttl}" if ttl
78
+ cmd << ' I' if stale # Mark stale instead of deleting
45
79
  cmd << ' q' if quiet
46
80
  cmd + TERMINATOR
47
81
  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
@@ -25,21 +25,28 @@ module Dalli
25
25
  # Retrieval Commands
26
26
  def get(key, options = nil)
27
27
  encoded_key, base64 = KeyRegularizer.encode(key)
28
- req = RequestFormatter.meta_get(key: encoded_key, base64: base64)
28
+ # Skip bitflags in raw mode - saves 2 bytes per request and skips parsing
29
+ skip_flags = raw_mode? || (options && options[:raw])
30
+ req = RequestFormatter.meta_get(key: encoded_key, base64: base64, skip_flags: skip_flags)
29
31
  write(req)
32
+ @connection_manager.flush
30
33
  response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
31
34
  end
32
35
 
33
36
  def quiet_get_request(key)
34
37
  encoded_key, base64 = KeyRegularizer.encode(key)
35
- RequestFormatter.meta_get(key: encoded_key, return_cas: true, base64: base64, quiet: true)
38
+ # Skip bitflags in raw mode - saves 2 bytes per request and skips parsing
39
+ RequestFormatter.meta_get(key: encoded_key, return_cas: true, base64: base64, quiet: true,
40
+ skip_flags: raw_mode?)
36
41
  end
37
42
 
38
43
  def gat(key, ttl, options = nil)
39
44
  ttl = TtlSanitizer.sanitize(ttl)
40
45
  encoded_key, base64 = KeyRegularizer.encode(key)
41
- req = RequestFormatter.meta_get(key: encoded_key, ttl: ttl, base64: base64)
46
+ skip_flags = raw_mode? || (options && options[:raw])
47
+ req = RequestFormatter.meta_get(key: encoded_key, ttl: ttl, base64: base64, skip_flags: skip_flags)
42
48
  write(req)
49
+ @connection_manager.flush
43
50
  response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
44
51
  end
45
52
 
@@ -48,6 +55,7 @@ module Dalli
48
55
  encoded_key, base64 = KeyRegularizer.encode(key)
49
56
  req = RequestFormatter.meta_get(key: encoded_key, ttl: ttl, value: false, base64: base64)
50
57
  write(req)
58
+ @connection_manager.flush
51
59
  response_processor.meta_get_without_value
52
60
  end
53
61
 
@@ -57,15 +65,77 @@ module Dalli
57
65
  encoded_key, base64 = KeyRegularizer.encode(key)
58
66
  req = RequestFormatter.meta_get(key: encoded_key, value: true, return_cas: true, base64: base64)
59
67
  write(req)
68
+ @connection_manager.flush
60
69
  response_processor.meta_get_with_value_and_cas
61
70
  end
62
71
 
72
+ # Comprehensive meta get with support for all metadata flags.
73
+ # @note Requires memcached 1.6+ (meta protocol feature)
74
+ #
75
+ # This is the full-featured get method that supports:
76
+ # - Thundering herd protection (vivify_ttl, recache_ttl)
77
+ # - Item metadata (hit_status, last_access)
78
+ # - LRU control (skip_lru_bump)
79
+ #
80
+ # @param key [String] the key to retrieve
81
+ # @param options [Hash] options controlling what metadata to return
82
+ # - :vivify_ttl [Integer] creates a stub on miss with this TTL (N flag)
83
+ # - :recache_ttl [Integer] wins recache race if remaining TTL is below this (R flag)
84
+ # - :return_hit_status [Boolean] return whether item was previously accessed (h flag)
85
+ # - :return_last_access [Boolean] return seconds since last access (l flag)
86
+ # - :skip_lru_bump [Boolean] don't bump LRU or update access stats (u flag)
87
+ # - :cache_nils [Boolean] whether to cache nil values
88
+ # @return [Hash] containing:
89
+ # - :value - the cached value (or nil on miss)
90
+ # - :cas - the CAS value
91
+ # - :won_recache - true if client won recache race (W flag)
92
+ # - :stale - true if item is stale (X flag)
93
+ # - :lost_recache - true if another client is recaching (Z flag)
94
+ # - :hit_before - true/false if previously accessed (only if return_hit_status: true)
95
+ # - :last_access - seconds since last access (only if return_last_access: true)
96
+ def meta_get(key, options = {})
97
+ encoded_key, base64 = KeyRegularizer.encode(key)
98
+ req = RequestFormatter.meta_get(
99
+ key: encoded_key, value: true, return_cas: true, base64: base64,
100
+ vivify_ttl: options[:vivify_ttl], recache_ttl: options[:recache_ttl],
101
+ return_hit_status: options[:return_hit_status],
102
+ return_last_access: options[:return_last_access], skip_lru_bump: options[:skip_lru_bump]
103
+ )
104
+ write(req)
105
+ @connection_manager.flush
106
+ response_processor.meta_get_with_metadata(
107
+ cache_nils: cache_nils?(options), return_hit_status: options[:return_hit_status],
108
+ return_last_access: options[:return_last_access]
109
+ )
110
+ end
111
+
112
+ # Delete with stale invalidation instead of actual deletion.
113
+ # Used with thundering herd protection to mark items as stale rather than removing them.
114
+ # @note Requires memcached 1.6+ (meta protocol feature)
115
+ #
116
+ # @param key [String] the key to invalidate
117
+ # @param cas [Integer] optional CAS value for compare-and-swap
118
+ # @return [Boolean] true if successful
119
+ def delete_stale(key, cas = nil)
120
+ encoded_key, base64 = KeyRegularizer.encode(key)
121
+ req = RequestFormatter.meta_delete(key: encoded_key, cas: cas, base64: base64, stale: true)
122
+ write(req)
123
+ @connection_manager.flush
124
+ response_processor.meta_delete
125
+ end
126
+
63
127
  # Storage Commands
64
128
  def set(key, value, ttl, cas, options)
65
129
  write_storage_req(:set, key, value, ttl, cas, options)
66
130
  response_processor.meta_set_with_cas unless quiet?
67
131
  end
68
132
 
133
+ # Pipelined set - writes a quiet set request without reading response.
134
+ # Used by PipelinedSetter for bulk operations.
135
+ def pipelined_set(key, value, ttl, options)
136
+ write_storage_req(:set, key, value, ttl, nil, options, quiet: true)
137
+ end
138
+
69
139
  def add(key, value, ttl, options)
70
140
  write_storage_req(:add, key, value, ttl, nil, options)
71
141
  response_processor.meta_set_with_cas unless quiet?
@@ -77,16 +147,17 @@ module Dalli
77
147
  end
78
148
 
79
149
  # rubocop:disable Metrics/ParameterLists
80
- def write_storage_req(mode, key, raw_value, ttl = nil, cas = nil, options = {})
150
+ def write_storage_req(mode, key, raw_value, ttl = nil, cas = nil, options = {}, quiet: quiet?)
81
151
  (value, bitflags) = @value_marshaller.store(key, raw_value, options)
82
152
  ttl = TtlSanitizer.sanitize(ttl) if ttl
83
153
  encoded_key, base64 = KeyRegularizer.encode(key)
84
154
  req = RequestFormatter.meta_set(key: encoded_key, value: value,
85
155
  bitflags: bitflags, cas: cas,
86
- ttl: ttl, mode: mode, quiet: quiet?, base64: base64)
156
+ ttl: ttl, mode: mode, quiet: quiet, base64: base64)
87
157
  write(req)
88
158
  write(value)
89
159
  write(TERMINATOR)
160
+ @connection_manager.flush unless quiet
90
161
  end
91
162
  # rubocop:enable Metrics/ParameterLists
92
163
 
@@ -109,6 +180,7 @@ module Dalli
109
180
  write(req)
110
181
  write(value)
111
182
  write(TERMINATOR)
183
+ @connection_manager.flush unless quiet?
112
184
  end
113
185
  # rubocop:enable Metrics/ParameterLists
114
186
 
@@ -118,9 +190,18 @@ module Dalli
118
190
  req = RequestFormatter.meta_delete(key: encoded_key, cas: cas,
119
191
  base64: base64, quiet: quiet?)
120
192
  write(req)
193
+ @connection_manager.flush unless quiet?
121
194
  response_processor.meta_delete unless quiet?
122
195
  end
123
196
 
197
+ # Pipelined delete - writes a quiet delete request without reading response.
198
+ # Used by PipelinedDeleter for bulk operations.
199
+ def pipelined_delete(key)
200
+ encoded_key, base64 = KeyRegularizer.encode(key)
201
+ req = RequestFormatter.meta_delete(key: encoded_key, base64: base64, quiet: true)
202
+ write(req)
203
+ end
204
+
124
205
  # Arithmetic Commands
125
206
  def decr(key, count, ttl, initial)
126
207
  decr_incr false, key, count, ttl, initial
@@ -135,12 +216,14 @@ module Dalli
135
216
  encoded_key, base64 = KeyRegularizer.encode(key)
136
217
  write(RequestFormatter.meta_arithmetic(key: encoded_key, delta: delta, initial: initial, incr: incr, ttl: ttl,
137
218
  quiet: quiet?, base64: base64))
219
+ @connection_manager.flush unless quiet?
138
220
  response_processor.decr_incr unless quiet?
139
221
  end
140
222
 
141
223
  # Other Commands
142
224
  def flush(delay = 0)
143
225
  write(RequestFormatter.flush(delay: delay))
226
+ @connection_manager.flush unless quiet?
144
227
  response_processor.flush unless quiet?
145
228
  end
146
229
 
@@ -153,21 +236,25 @@ module Dalli
153
236
 
154
237
  def stats(info = nil)
155
238
  write(RequestFormatter.stats(info))
239
+ @connection_manager.flush
156
240
  response_processor.stats
157
241
  end
158
242
 
159
243
  def reset_stats
160
244
  write(RequestFormatter.stats('reset'))
245
+ @connection_manager.flush
161
246
  response_processor.reset
162
247
  end
163
248
 
164
249
  def version
165
250
  write(RequestFormatter.version)
251
+ @connection_manager.flush
166
252
  response_processor.version
167
253
  end
168
254
 
169
255
  def write_noop
170
256
  write(RequestFormatter.meta_noop)
257
+ @connection_manager.flush
171
258
  end
172
259
 
173
260
  def authenticate_connection
@@ -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.2.0'
5
5
 
6
6
  MIN_SUPPORTED_MEMCACHED_VERSION = '1.4'
7
7
  end
data/lib/dalli.rb CHANGED
@@ -56,11 +56,15 @@ module Dalli
56
56
  end
57
57
 
58
58
  require_relative 'dalli/version'
59
+ require_relative 'dalli/instrumentation'
59
60
 
60
61
  require_relative 'dalli/compressor'
62
+ require_relative 'dalli/protocol_deprecations'
61
63
  require_relative 'dalli/client'
62
64
  require_relative 'dalli/key_manager'
63
65
  require_relative 'dalli/pipelined_getter'
66
+ require_relative 'dalli/pipelined_setter'
67
+ require_relative 'dalli/pipelined_deleter'
64
68
  require_relative 'dalli/ring'
65
69
  require_relative 'dalli/protocol'
66
70
  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.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter M. Goldstein
@@ -40,10 +40,13 @@ files:
40
40
  - lib/dalli/cas/client.rb
41
41
  - lib/dalli/client.rb
42
42
  - lib/dalli/compressor.rb
43
+ - lib/dalli/instrumentation.rb
43
44
  - lib/dalli/key_manager.rb
44
45
  - lib/dalli/options.rb
45
46
  - lib/dalli/pid_cache.rb
47
+ - lib/dalli/pipelined_deleter.rb
46
48
  - lib/dalli/pipelined_getter.rb
49
+ - lib/dalli/pipelined_setter.rb
47
50
  - lib/dalli/protocol.rb
48
51
  - lib/dalli/protocol/base.rb
49
52
  - lib/dalli/protocol/binary.rb
@@ -63,6 +66,7 @@ files:
63
66
  - lib/dalli/protocol/value_compressor.rb
64
67
  - lib/dalli/protocol/value_marshaller.rb
65
68
  - lib/dalli/protocol/value_serializer.rb
69
+ - lib/dalli/protocol_deprecations.rb
66
70
  - lib/dalli/ring.rb
67
71
  - lib/dalli/servers_arg_normalizer.rb
68
72
  - lib/dalli/socket.rb