dalli 3.2.8 → 4.3.3
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 +169 -1
- data/Gemfile +15 -2
- data/README.md +92 -0
- data/lib/dalli/client.rb +246 -11
- data/lib/dalli/instrumentation.rb +141 -0
- data/lib/dalli/key_manager.rb +23 -8
- data/lib/dalli/pipelined_deleter.rb +82 -0
- data/lib/dalli/pipelined_getter.rb +46 -20
- data/lib/dalli/pipelined_setter.rb +87 -0
- data/lib/dalli/protocol/base.rb +82 -10
- data/lib/dalli/protocol/binary/response_processor.rb +5 -15
- data/lib/dalli/protocol/binary.rb +27 -0
- data/lib/dalli/protocol/connection_manager.rb +16 -11
- data/lib/dalli/protocol/meta/key_regularizer.rb +1 -1
- data/lib/dalli/protocol/meta/request_formatter.rb +42 -10
- data/lib/dalli/protocol/meta/response_processor.rb +72 -26
- data/lib/dalli/protocol/meta.rb +96 -5
- data/lib/dalli/protocol/response_buffer.rb +36 -12
- data/lib/dalli/protocol/server_config_parser.rb +1 -1
- data/lib/dalli/protocol/string_marshaller.rb +65 -0
- data/lib/dalli/protocol/ttl_sanitizer.rb +1 -1
- data/lib/dalli/protocol/value_compressor.rb +2 -11
- data/lib/dalli/protocol/value_marshaller.rb +1 -1
- data/lib/dalli/protocol/value_serializer.rb +59 -40
- data/lib/dalli/protocol.rb +10 -0
- data/lib/dalli/protocol_deprecations.rb +45 -0
- data/lib/dalli/socket.rb +70 -14
- data/lib/dalli/version.rb +1 -1
- data/lib/dalli.rb +11 -2
- data/lib/rack/session/dalli.rb +43 -8
- metadata +25 -10
- data/lib/dalli/server.rb +0 -6
|
@@ -189,16 +189,6 @@ module Dalli
|
|
|
189
189
|
[resp_header.status, content]
|
|
190
190
|
end
|
|
191
191
|
|
|
192
|
-
def contains_header?(buf)
|
|
193
|
-
return false unless buf
|
|
194
|
-
|
|
195
|
-
buf.bytesize >= ResponseHeader::SIZE
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
def response_header_from_buffer(buf)
|
|
199
|
-
ResponseHeader.new(buf)
|
|
200
|
-
end
|
|
201
|
-
|
|
202
192
|
##
|
|
203
193
|
# This method returns an array of values used in a pipelined
|
|
204
194
|
# getk process. The first value is the number of bytes by
|
|
@@ -209,11 +199,11 @@ module Dalli
|
|
|
209
199
|
# The remaining three values in the array are the ResponseHeader,
|
|
210
200
|
# key, and value.
|
|
211
201
|
##
|
|
212
|
-
def getk_response_from_buffer(buf)
|
|
202
|
+
def getk_response_from_buffer(buf, offset = 0)
|
|
213
203
|
# There's no header in the buffer, so don't advance
|
|
214
|
-
return [0, nil, nil, nil, nil] unless
|
|
204
|
+
return [0, nil, nil, nil, nil] unless buf && buf.bytesize >= offset + ResponseHeader::SIZE
|
|
215
205
|
|
|
216
|
-
resp_header =
|
|
206
|
+
resp_header = ResponseHeader.new(buf.byteslice(offset, ResponseHeader::SIZE))
|
|
217
207
|
body_len = resp_header.body_len
|
|
218
208
|
|
|
219
209
|
# We have a complete response that has no body.
|
|
@@ -225,11 +215,11 @@ module Dalli
|
|
|
225
215
|
resp_size = ResponseHeader::SIZE + body_len
|
|
226
216
|
# The header is in the buffer, but the body is not. As we don't have
|
|
227
217
|
# a complete response, don't advance the buffer
|
|
228
|
-
return [0, nil, nil, nil, nil] unless buf.bytesize >= resp_size
|
|
218
|
+
return [0, nil, nil, nil, nil] unless buf.bytesize >= offset + resp_size
|
|
229
219
|
|
|
230
220
|
# The full response is in our buffer, so parse it and return
|
|
231
221
|
# the values
|
|
232
|
-
body = buf.byteslice(ResponseHeader::SIZE, body_len)
|
|
222
|
+
body = buf.byteslice(offset + ResponseHeader::SIZE, body_len)
|
|
233
223
|
key, value = unpack_response_body(resp_header, body, true)
|
|
234
224
|
[resp_size, resp_header.ok?, resp_header.cas, key, value]
|
|
235
225
|
end
|
|
@@ -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
|
|
@@ -119,12 +139,14 @@ module Dalli
|
|
|
119
139
|
# if the key doesn't already exist, rather than
|
|
120
140
|
# setting the initial value
|
|
121
141
|
NOT_FOUND_EXPIRY = 0xFFFFFFFF
|
|
142
|
+
private_constant :NOT_FOUND_EXPIRY
|
|
122
143
|
|
|
123
144
|
def decr_incr(opkey, key, count, ttl, initial)
|
|
124
145
|
expiry = initial ? TtlSanitizer.sanitize(ttl) : NOT_FOUND_EXPIRY
|
|
125
146
|
initial ||= 0
|
|
126
147
|
write(RequestFormatter.decr_incr_request(opkey: opkey, key: key,
|
|
127
148
|
count: count, initial: initial, expiry: expiry))
|
|
149
|
+
@connection_manager.flush unless quiet?
|
|
128
150
|
response_processor.decr_incr unless quiet?
|
|
129
151
|
end
|
|
130
152
|
|
|
@@ -132,6 +154,7 @@ module Dalli
|
|
|
132
154
|
def flush(ttl = 0)
|
|
133
155
|
opkey = quiet? ? :flushq : :flush
|
|
134
156
|
write(RequestFormatter.standard_request(opkey: opkey, ttl: ttl))
|
|
157
|
+
@connection_manager.flush unless quiet?
|
|
135
158
|
response_processor.no_body_response unless quiet?
|
|
136
159
|
end
|
|
137
160
|
|
|
@@ -145,22 +168,26 @@ module Dalli
|
|
|
145
168
|
def stats(info = '')
|
|
146
169
|
req = RequestFormatter.standard_request(opkey: :stat, key: info)
|
|
147
170
|
write(req)
|
|
171
|
+
@connection_manager.flush
|
|
148
172
|
response_processor.stats
|
|
149
173
|
end
|
|
150
174
|
|
|
151
175
|
def reset_stats
|
|
152
176
|
write(RequestFormatter.standard_request(opkey: :stat, key: 'reset'))
|
|
177
|
+
@connection_manager.flush
|
|
153
178
|
response_processor.reset
|
|
154
179
|
end
|
|
155
180
|
|
|
156
181
|
def version
|
|
157
182
|
write(RequestFormatter.standard_request(opkey: :version))
|
|
183
|
+
@connection_manager.flush
|
|
158
184
|
response_processor.version
|
|
159
185
|
end
|
|
160
186
|
|
|
161
187
|
def write_noop
|
|
162
188
|
req = RequestFormatter.standard_request(opkey: :noop)
|
|
163
189
|
write(req)
|
|
190
|
+
@connection_manager.flush
|
|
164
191
|
end
|
|
165
192
|
|
|
166
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
|
|
@@ -100,13 +101,13 @@ module Dalli
|
|
|
100
101
|
|
|
101
102
|
def confirm_ready!
|
|
102
103
|
close if request_in_progress?
|
|
103
|
-
|
|
104
|
+
reconnect_on_fork if fork_detected?
|
|
104
105
|
end
|
|
105
106
|
|
|
106
107
|
def confirm_in_progress!
|
|
107
108
|
raise '[Dalli] No request in progress. This may be a bug in Dalli.' unless request_in_progress?
|
|
108
109
|
|
|
109
|
-
|
|
110
|
+
reconnect_on_fork if fork_detected?
|
|
110
111
|
end
|
|
111
112
|
|
|
112
113
|
def close
|
|
@@ -150,19 +151,25 @@ module Dalli
|
|
|
150
151
|
data = @sock.gets("\r\n")
|
|
151
152
|
error_on_request!('EOF in read_line') if data.nil?
|
|
152
153
|
data
|
|
153
|
-
rescue SystemCallError, *TIMEOUT_ERRORS, EOFError => e
|
|
154
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
|
|
154
155
|
error_on_request!(e)
|
|
155
156
|
end
|
|
156
157
|
|
|
157
158
|
def read(count)
|
|
158
159
|
@sock.readfull(count)
|
|
159
|
-
rescue SystemCallError, *TIMEOUT_ERRORS, EOFError => e
|
|
160
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
|
|
160
161
|
error_on_request!(e)
|
|
161
162
|
end
|
|
162
163
|
|
|
163
164
|
def write(bytes)
|
|
164
165
|
@sock.write(bytes)
|
|
165
|
-
rescue SystemCallError, *TIMEOUT_ERRORS => e
|
|
166
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS => e
|
|
167
|
+
error_on_request!(e)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def flush
|
|
171
|
+
@sock.flush
|
|
172
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS => e
|
|
166
173
|
error_on_request!(e)
|
|
167
174
|
end
|
|
168
175
|
|
|
@@ -192,7 +199,7 @@ module Dalli
|
|
|
192
199
|
def reconnect!(message)
|
|
193
200
|
close
|
|
194
201
|
sleep(options[:socket_failure_delay]) if options[:socket_failure_delay]
|
|
195
|
-
raise Dalli::
|
|
202
|
+
raise Dalli::RetryableNetworkError, message
|
|
196
203
|
end
|
|
197
204
|
|
|
198
205
|
def reset_down_info
|
|
@@ -212,20 +219,18 @@ module Dalli
|
|
|
212
219
|
end
|
|
213
220
|
|
|
214
221
|
def log_warn_message(err_or_string)
|
|
215
|
-
detail = err_or_string.is_a?(String) ? err_or_string : "#{err_or_string.class}: #{err_or_string.message}"
|
|
216
222
|
Dalli.logger.warn do
|
|
217
223
|
detail = err_or_string.is_a?(String) ? err_or_string : "#{err_or_string.class}: #{err_or_string.message}"
|
|
218
224
|
"#{name} failed (count: #{@fail_count}) #{detail}"
|
|
219
225
|
end
|
|
220
226
|
end
|
|
221
227
|
|
|
222
|
-
def
|
|
228
|
+
def reconnect_on_fork
|
|
223
229
|
message = 'Fork detected, re-connecting child process...'
|
|
224
230
|
Dalli.logger.info { message }
|
|
225
|
-
# Close socket on a fork
|
|
226
|
-
# on next request.
|
|
231
|
+
# Close socket on a fork and reconnect immediately
|
|
227
232
|
close
|
|
228
|
-
|
|
233
|
+
establish_connection
|
|
229
234
|
end
|
|
230
235
|
|
|
231
236
|
def fork_detected?
|
|
@@ -10,7 +10,7 @@ module Dalli
|
|
|
10
10
|
# memcached supports the use of base64 hashes for keys containing
|
|
11
11
|
# whitespace or non-ASCII characters, provided the 'b' flag is included in the request.
|
|
12
12
|
class KeyRegularizer
|
|
13
|
-
WHITESPACE = /\s
|
|
13
|
+
WHITESPACE = /\s/
|
|
14
14
|
|
|
15
15
|
def self.encode(key)
|
|
16
16
|
return [key, false] if key.ascii_only? && !WHITESPACE.match(key)
|
|
@@ -13,16 +13,45 @@ module Dalli
|
|
|
13
13
|
# and introducing an intermediate object seems like overkill.
|
|
14
14
|
#
|
|
15
15
|
# rubocop:disable Metrics/CyclomaticComplexity
|
|
16
|
-
# rubocop:disable Metrics/MethodLength
|
|
17
16
|
# rubocop:disable Metrics/ParameterLists
|
|
18
17
|
# rubocop:disable Metrics/PerceivedComplexity
|
|
19
|
-
|
|
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)
|
|
20
42
|
cmd = "mg #{key}"
|
|
21
|
-
|
|
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
|
|
22
46
|
cmd << ' c' if return_cas
|
|
23
47
|
cmd << ' b' if base64
|
|
24
48
|
cmd << " T#{ttl}" if ttl
|
|
25
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
|
|
26
55
|
cmd + TERMINATOR
|
|
27
56
|
end
|
|
28
57
|
|
|
@@ -36,15 +65,17 @@ module Dalli
|
|
|
36
65
|
cmd << " M#{mode_to_token(mode)}"
|
|
37
66
|
cmd << ' q' if quiet
|
|
38
67
|
cmd << TERMINATOR
|
|
39
|
-
cmd << value
|
|
40
|
-
cmd + TERMINATOR
|
|
41
68
|
end
|
|
42
69
|
|
|
43
|
-
|
|
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)
|
|
44
74
|
cmd = "md #{key}"
|
|
45
75
|
cmd << ' b' if base64
|
|
46
76
|
cmd << cas_string(cas)
|
|
47
77
|
cmd << " T#{ttl}" if ttl
|
|
78
|
+
cmd << ' I' if stale # Mark stale instead of deleting
|
|
48
79
|
cmd << ' q' if quiet
|
|
49
80
|
cmd + TERMINATOR
|
|
50
81
|
end
|
|
@@ -62,7 +93,6 @@ module Dalli
|
|
|
62
93
|
cmd + TERMINATOR
|
|
63
94
|
end
|
|
64
95
|
# rubocop:enable Metrics/CyclomaticComplexity
|
|
65
|
-
# rubocop:enable Metrics/MethodLength
|
|
66
96
|
# rubocop:enable Metrics/ParameterLists
|
|
67
97
|
# rubocop:enable Metrics/PerceivedComplexity
|
|
68
98
|
|
|
@@ -81,13 +111,16 @@ module Dalli
|
|
|
81
111
|
cmd + TERMINATOR
|
|
82
112
|
end
|
|
83
113
|
|
|
114
|
+
ALLOWED_STATS_ARGS = [nil, '', 'items', 'slabs', 'settings', 'reset'].freeze
|
|
115
|
+
|
|
84
116
|
def self.stats(arg = nil)
|
|
117
|
+
raise ArgumentError, "Invalid stats argument: #{arg.inspect}" unless ALLOWED_STATS_ARGS.include?(arg)
|
|
118
|
+
|
|
85
119
|
cmd = +'stats'
|
|
86
|
-
cmd << " #{arg}" if arg
|
|
120
|
+
cmd << " #{arg}" if arg && !arg.empty?
|
|
87
121
|
cmd + TERMINATOR
|
|
88
122
|
end
|
|
89
123
|
|
|
90
|
-
# rubocop:disable Metrics/MethodLength
|
|
91
124
|
def self.mode_to_token(mode)
|
|
92
125
|
case mode
|
|
93
126
|
when :add
|
|
@@ -102,7 +135,6 @@ module Dalli
|
|
|
102
135
|
'S'
|
|
103
136
|
end
|
|
104
137
|
end
|
|
105
|
-
# rubocop:enable Metrics/MethodLength
|
|
106
138
|
|
|
107
139
|
def self.cas_string(cas)
|
|
108
140
|
cas = parse_to_64_bit_int(cas, nil)
|
|
@@ -21,6 +21,7 @@ module Dalli
|
|
|
21
21
|
STAT = 'STAT'
|
|
22
22
|
VA = 'VA'
|
|
23
23
|
VERSION = 'VERSION'
|
|
24
|
+
SERVER_ERROR = 'SERVER_ERROR'
|
|
24
25
|
|
|
25
26
|
def initialize(io_source, value_marshaller)
|
|
26
27
|
@io_source = io_source
|
|
@@ -32,7 +33,7 @@ module Dalli
|
|
|
32
33
|
return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
|
|
33
34
|
return true unless tokens.first == VA
|
|
34
35
|
|
|
35
|
-
@value_marshaller.retrieve(
|
|
36
|
+
@value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens))
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
def meta_get_with_value_and_cas
|
|
@@ -42,7 +43,7 @@ module Dalli
|
|
|
42
43
|
cas = cas_from_tokens(tokens)
|
|
43
44
|
return [nil, cas] unless tokens.first == VA
|
|
44
45
|
|
|
45
|
-
[@value_marshaller.retrieve(
|
|
46
|
+
[@value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens)), cas]
|
|
46
47
|
end
|
|
47
48
|
|
|
48
49
|
def meta_get_without_value
|
|
@@ -50,6 +51,41 @@ module Dalli
|
|
|
50
51
|
tokens.first == EN ? nil : true
|
|
51
52
|
end
|
|
52
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
|
+
|
|
53
89
|
def meta_set_with_cas
|
|
54
90
|
tokens = error_on_unexpected!([HD, NS, NF, EX])
|
|
55
91
|
return false unless tokens.first == HD
|
|
@@ -111,14 +147,6 @@ module Dalli
|
|
|
111
147
|
true
|
|
112
148
|
end
|
|
113
149
|
|
|
114
|
-
def tokens_from_header_buffer(buf)
|
|
115
|
-
header = header_from_buffer(buf)
|
|
116
|
-
tokens = header.split
|
|
117
|
-
header_len = header.bytesize + TERMINATOR.length
|
|
118
|
-
body_len = body_len_from_tokens(tokens)
|
|
119
|
-
[tokens, header_len, body_len]
|
|
120
|
-
end
|
|
121
|
-
|
|
122
150
|
def full_response_from_buffer(tokens, body, resp_size)
|
|
123
151
|
value = @value_marshaller.retrieve(body, bitflags_from_tokens(tokens))
|
|
124
152
|
[resp_size, tokens.first == VA, cas_from_tokens(tokens), key_from_tokens(tokens), value]
|
|
@@ -134,11 +162,15 @@ module Dalli
|
|
|
134
162
|
# The remaining three values in the array are the ResponseHeader,
|
|
135
163
|
# key, and value.
|
|
136
164
|
##
|
|
137
|
-
def getk_response_from_buffer(buf)
|
|
138
|
-
#
|
|
139
|
-
|
|
165
|
+
def getk_response_from_buffer(buf, offset = 0)
|
|
166
|
+
# Find the header terminator starting from offset
|
|
167
|
+
term_idx = buf.index(TERMINATOR, offset)
|
|
168
|
+
return [0, nil, nil, nil, nil] unless term_idx
|
|
140
169
|
|
|
141
|
-
|
|
170
|
+
header = buf.byteslice(offset, term_idx - offset)
|
|
171
|
+
tokens = header.split
|
|
172
|
+
header_len = header.bytesize + TERMINATOR.length
|
|
173
|
+
body_len = body_len_from_tokens(tokens)
|
|
142
174
|
|
|
143
175
|
# We have a complete response that has no body.
|
|
144
176
|
# This is either the response to the terminating
|
|
@@ -149,27 +181,22 @@ module Dalli
|
|
|
149
181
|
resp_size = header_len + body_len + TERMINATOR.length
|
|
150
182
|
# The header is in the buffer, but the body is not. As we don't have
|
|
151
183
|
# a complete response, don't advance the buffer
|
|
152
|
-
return [0, nil, nil, nil, nil] unless buf.bytesize >= resp_size
|
|
184
|
+
return [0, nil, nil, nil, nil] unless buf.bytesize >= offset + resp_size
|
|
153
185
|
|
|
154
186
|
# The full response is in our buffer, so parse it and return
|
|
155
187
|
# the values
|
|
156
|
-
body = buf.
|
|
188
|
+
body = buf.byteslice(offset + header_len, body_len)
|
|
157
189
|
full_response_from_buffer(tokens, body, resp_size)
|
|
158
190
|
end
|
|
159
191
|
|
|
160
|
-
def contains_header?(buf)
|
|
161
|
-
buf.include?(TERMINATOR)
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def header_from_buffer(buf)
|
|
165
|
-
buf.split(TERMINATOR, 2).first
|
|
166
|
-
end
|
|
167
|
-
|
|
168
192
|
def error_on_unexpected!(expected_codes)
|
|
169
193
|
tokens = next_line_to_tokens
|
|
170
|
-
raise Dalli::DalliError, "Response error: #{tokens.first}" unless expected_codes.include?(tokens.first)
|
|
171
194
|
|
|
172
|
-
tokens
|
|
195
|
+
return tokens if expected_codes.include?(tokens.first)
|
|
196
|
+
|
|
197
|
+
raise Dalli::ServerError, tokens.join(' ').to_s if tokens.first == SERVER_ERROR
|
|
198
|
+
|
|
199
|
+
raise Dalli::DalliError, "Response error: #{tokens.first}"
|
|
173
200
|
end
|
|
174
201
|
|
|
175
202
|
def bitflags_from_tokens(tokens)
|
|
@@ -186,6 +213,21 @@ module Dalli
|
|
|
186
213
|
KeyRegularizer.decode(encoded_key, base64_encoded)
|
|
187
214
|
end
|
|
188
215
|
|
|
216
|
+
# Returns true if item was previously hit, false if first access, nil if not requested
|
|
217
|
+
# The h flag returns h0 (first access) or h1 (previously accessed)
|
|
218
|
+
def hit_status_from_tokens(tokens)
|
|
219
|
+
hit_token = tokens.find { |t| t.start_with?('h') && t.length == 2 }
|
|
220
|
+
return nil unless hit_token
|
|
221
|
+
|
|
222
|
+
hit_token[1] == '1'
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Returns seconds since last access, or nil if not requested
|
|
226
|
+
# The l flag returns l<seconds>
|
|
227
|
+
def last_access_from_tokens(tokens)
|
|
228
|
+
value_from_tokens(tokens, 'l')&.to_i
|
|
229
|
+
end
|
|
230
|
+
|
|
189
231
|
def body_len_from_tokens(tokens)
|
|
190
232
|
value_from_tokens(tokens, 's')&.to_i
|
|
191
233
|
end
|
|
@@ -205,6 +247,10 @@ module Dalli
|
|
|
205
247
|
line = read_line
|
|
206
248
|
line&.split || []
|
|
207
249
|
end
|
|
250
|
+
|
|
251
|
+
def read_data(data_size)
|
|
252
|
+
@io_source.read(data_size + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
|
|
253
|
+
end
|
|
208
254
|
end
|
|
209
255
|
end
|
|
210
256
|
end
|