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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +45 -0
- data/README.md +41 -0
- data/lib/dalli/client.rb +217 -6
- data/lib/dalli/instrumentation.rb +139 -0
- data/lib/dalli/pipelined_deleter.rb +82 -0
- data/lib/dalli/pipelined_getter.rb +5 -3
- data/lib/dalli/pipelined_setter.rb +87 -0
- data/lib/dalli/protocol/base.rb +8 -1
- data/lib/dalli/protocol/binary.rb +26 -0
- data/lib/dalli/protocol/connection_manager.rb +7 -0
- data/lib/dalli/protocol/meta/request_formatter.rb +37 -3
- data/lib/dalli/protocol/meta/response_processor.rb +50 -0
- data/lib/dalli/protocol/meta.rb +92 -5
- data/lib/dalli/protocol_deprecations.rb +45 -0
- data/lib/dalli/version.rb +1 -1
- data/lib/dalli.rb +4 -0
- metadata +5 -1
|
@@ -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
|
data/lib/dalli/protocol/base.rb
CHANGED
|
@@ -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
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/dalli/protocol/meta.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
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
|
|
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
|