dalli 4.3.2 → 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 +16 -0
- data/lib/dalli/pipelined_getter.rb +9 -14
- data/lib/dalli/protocol/base.rb +16 -5
- data/lib/dalli/protocol/binary/response_processor.rb +5 -15
- data/lib/dalli/protocol/meta/response_processor.rb +10 -22
- data/lib/dalli/protocol/response_buffer.rb +27 -12
- data/lib/dalli/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 57b78e30ee409a2d742fc47ee57ccdd482fe9c75f369366ae315b7ef8b649934
|
|
4
|
+
data.tar.gz: b8cad66f3cba53bbcb84b18f406eed60f22209c10dd4a312063a1e13189185ca
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 18b26e3c4aa30e5f8b4195d95307afca6c9240dd1154a36966d40d52047e638758850ec06e2a40fd1bd8e69fe150dee7409b1f4a565f0a80e397aaf115315463
|
|
7
|
+
data.tar.gz: b6db69ecbc1e6d34587d8ec29b861f6a71c863b36229d3c642e3d0e646ba6234e3bac04225d64ff174d12f35b728663e82e1d2b5f7e93dc9ac3e1ff33fd58db3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
Dalli Changelog
|
|
2
2
|
=====================
|
|
3
3
|
|
|
4
|
+
4.3.3
|
|
5
|
+
==========
|
|
6
|
+
|
|
7
|
+
Performance:
|
|
8
|
+
|
|
9
|
+
- Reduce object allocations in pipelined get response processing (#1072)
|
|
10
|
+
- Offset-based `ResponseBuffer`: track a read offset instead of slicing a new string after every parsed response; compact only when the consumed portion exceeds 4KB and more than half the buffer
|
|
11
|
+
- Inline response processor parsing: avoid intermediate array allocations from `split`-based header parsing in both binary and meta protocols
|
|
12
|
+
- Block-based `pipeline_next_responses`: yield `(key, value, cas)` directly when a block is given, avoiding per-call Hash allocation
|
|
13
|
+
- `PipelinedGetter`: replace Hash-based socket-to-server mapping with linear scan (faster for typical 1-5 server counts); use `Process.clock_gettime(CLOCK_MONOTONIC)` instead of `Time.now`
|
|
14
|
+
- Add cross-version benchmark script (`bin/compare_versions`) for reproducible performance comparisons across Dalli versions
|
|
15
|
+
|
|
16
|
+
Bug Fixes:
|
|
17
|
+
|
|
18
|
+
- Skip OTel integration tests when meta protocol is unavailable (#1072)
|
|
19
|
+
|
|
4
20
|
4.3.2
|
|
5
21
|
==========
|
|
6
22
|
|
|
@@ -29,7 +29,7 @@ module Dalli
|
|
|
29
29
|
# Stores partial results collected during interleaved send phase
|
|
30
30
|
@partial_results = {}
|
|
31
31
|
servers = setup_requests(keys)
|
|
32
|
-
start_time =
|
|
32
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
33
33
|
|
|
34
34
|
# First yield any partial results collected during interleaved send
|
|
35
35
|
yield_partial_results(&block)
|
|
@@ -149,7 +149,7 @@ module Dalli
|
|
|
149
149
|
end
|
|
150
150
|
|
|
151
151
|
def remaining_time(start, timeout)
|
|
152
|
-
elapsed =
|
|
152
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
|
153
153
|
return 0 if elapsed > timeout
|
|
154
154
|
|
|
155
155
|
timeout - elapsed
|
|
@@ -168,8 +168,8 @@ module Dalli
|
|
|
168
168
|
# Processes responses from a server. Returns true if there are no
|
|
169
169
|
# additional responses from this server.
|
|
170
170
|
def process_server(server)
|
|
171
|
-
server.pipeline_next_responses
|
|
172
|
-
yield @key_manager.key_without_namespace(key),
|
|
171
|
+
server.pipeline_next_responses do |key, value, cas|
|
|
172
|
+
yield @key_manager.key_without_namespace(key), [value, cas]
|
|
173
173
|
end
|
|
174
174
|
|
|
175
175
|
server.pipeline_complete?
|
|
@@ -178,18 +178,13 @@ module Dalli
|
|
|
178
178
|
def servers_with_response(servers, timeout)
|
|
179
179
|
return [] if servers.empty?
|
|
180
180
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
# My suspicion is that we may want to try and push this down into the
|
|
184
|
-
# individual servers, but I'm not sure. For now, we keep the
|
|
185
|
-
# mapping between the alerted object (the socket) and the
|
|
186
|
-
# corrresponding server here.
|
|
187
|
-
server_map = servers.each_with_object({}) { |s, h| h[s.sock] = s }
|
|
188
|
-
|
|
189
|
-
readable, = IO.select(server_map.keys, nil, nil, timeout)
|
|
181
|
+
sockets = servers.map(&:sock)
|
|
182
|
+
readable, = IO.select(sockets, nil, nil, timeout)
|
|
190
183
|
return [] if readable.nil?
|
|
191
184
|
|
|
192
|
-
|
|
185
|
+
# For typical server counts (1-5), linear scan is faster than
|
|
186
|
+
# building and looking up a hash map
|
|
187
|
+
readable.filter_map { |sock| servers.find { |s| s.sock == sock } }
|
|
193
188
|
end
|
|
194
189
|
|
|
195
190
|
def groups_for_keys(*keys)
|
data/lib/dalli/protocol/base.rb
CHANGED
|
@@ -91,10 +91,13 @@ module Dalli
|
|
|
91
91
|
# repeatedly whenever this server's socket is readable until
|
|
92
92
|
# #pipeline_complete?.
|
|
93
93
|
#
|
|
94
|
-
#
|
|
95
|
-
|
|
94
|
+
# When a block is given, yields (key, value, cas) for each response,
|
|
95
|
+
# avoiding intermediate Hash allocation. Returns nil.
|
|
96
|
+
# Without a block, returns a Hash of { key => [value, cas] }.
|
|
97
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
98
|
+
def pipeline_next_responses(&block)
|
|
96
99
|
reconnect_on_pipeline_complete!
|
|
97
|
-
values =
|
|
100
|
+
values = nil
|
|
98
101
|
|
|
99
102
|
response_buffer.read
|
|
100
103
|
|
|
@@ -108,16 +111,24 @@ module Dalli
|
|
|
108
111
|
|
|
109
112
|
# If the status is ok and the key is not nil, then this is a
|
|
110
113
|
# getkq response with a value that we want to set in the response hash
|
|
111
|
-
|
|
114
|
+
unless key.nil?
|
|
115
|
+
if block
|
|
116
|
+
yield key, value, cas
|
|
117
|
+
else
|
|
118
|
+
values ||= {}
|
|
119
|
+
values[key] = [value, cas]
|
|
120
|
+
end
|
|
121
|
+
end
|
|
112
122
|
|
|
113
123
|
# Get the next response from the buffer
|
|
114
124
|
status, cas, key, value = response_buffer.process_single_getk_response
|
|
115
125
|
end
|
|
116
126
|
|
|
117
|
-
values
|
|
127
|
+
values || {}
|
|
118
128
|
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
|
|
119
129
|
@connection_manager.error_on_request!(e)
|
|
120
130
|
end
|
|
131
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
121
132
|
|
|
122
133
|
# Abort current pipelined get. Generally used to signal an external
|
|
123
134
|
# timeout during pipelined get. The underlying socket is
|
|
@@ -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
|
|
@@ -147,14 +147,6 @@ module Dalli
|
|
|
147
147
|
true
|
|
148
148
|
end
|
|
149
149
|
|
|
150
|
-
def tokens_from_header_buffer(buf)
|
|
151
|
-
header = header_from_buffer(buf)
|
|
152
|
-
tokens = header.split
|
|
153
|
-
header_len = header.bytesize + TERMINATOR.length
|
|
154
|
-
body_len = body_len_from_tokens(tokens)
|
|
155
|
-
[tokens, header_len, body_len]
|
|
156
|
-
end
|
|
157
|
-
|
|
158
150
|
def full_response_from_buffer(tokens, body, resp_size)
|
|
159
151
|
value = @value_marshaller.retrieve(body, bitflags_from_tokens(tokens))
|
|
160
152
|
[resp_size, tokens.first == VA, cas_from_tokens(tokens), key_from_tokens(tokens), value]
|
|
@@ -170,11 +162,15 @@ module Dalli
|
|
|
170
162
|
# The remaining three values in the array are the ResponseHeader,
|
|
171
163
|
# key, and value.
|
|
172
164
|
##
|
|
173
|
-
def getk_response_from_buffer(buf)
|
|
174
|
-
#
|
|
175
|
-
|
|
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
|
|
176
169
|
|
|
177
|
-
|
|
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)
|
|
178
174
|
|
|
179
175
|
# We have a complete response that has no body.
|
|
180
176
|
# This is either the response to the terminating
|
|
@@ -185,22 +181,14 @@ module Dalli
|
|
|
185
181
|
resp_size = header_len + body_len + TERMINATOR.length
|
|
186
182
|
# The header is in the buffer, but the body is not. As we don't have
|
|
187
183
|
# a complete response, don't advance the buffer
|
|
188
|
-
return [0, nil, nil, nil, nil] unless buf.bytesize >= resp_size
|
|
184
|
+
return [0, nil, nil, nil, nil] unless buf.bytesize >= offset + resp_size
|
|
189
185
|
|
|
190
186
|
# The full response is in our buffer, so parse it and return
|
|
191
187
|
# the values
|
|
192
|
-
body = buf.
|
|
188
|
+
body = buf.byteslice(offset + header_len, body_len)
|
|
193
189
|
full_response_from_buffer(tokens, body, resp_size)
|
|
194
190
|
end
|
|
195
191
|
|
|
196
|
-
def contains_header?(buf)
|
|
197
|
-
buf.include?(TERMINATOR)
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
def header_from_buffer(buf)
|
|
201
|
-
buf.split(TERMINATOR, 2).first
|
|
202
|
-
end
|
|
203
|
-
|
|
204
192
|
def error_on_unexpected!(expected_codes)
|
|
205
193
|
tokens = next_line_to_tokens
|
|
206
194
|
|
|
@@ -7,38 +7,39 @@ module Dalli
|
|
|
7
7
|
module Protocol
|
|
8
8
|
##
|
|
9
9
|
# Manages the buffer for responses from memcached.
|
|
10
|
+
# Uses an offset-based approach to avoid string allocations
|
|
11
|
+
# when advancing through parsed responses.
|
|
10
12
|
##
|
|
11
13
|
class ResponseBuffer
|
|
14
|
+
# Compact the buffer when the consumed portion exceeds this
|
|
15
|
+
# threshold and represents more than half the buffer
|
|
16
|
+
COMPACT_THRESHOLD = 4096
|
|
17
|
+
|
|
12
18
|
def initialize(io_source, response_processor)
|
|
13
19
|
@io_source = io_source
|
|
14
20
|
@response_processor = response_processor
|
|
15
21
|
@buffer = nil
|
|
22
|
+
@offset = 0
|
|
16
23
|
end
|
|
17
24
|
|
|
18
25
|
def read
|
|
19
26
|
@buffer << @io_source.read_nonblock
|
|
20
27
|
end
|
|
21
28
|
|
|
22
|
-
# Attempts to process a single response from the buffer
|
|
23
|
-
#
|
|
29
|
+
# Attempts to process a single response from the buffer,
|
|
30
|
+
# advancing the offset past the consumed bytes.
|
|
24
31
|
def process_single_getk_response
|
|
25
|
-
bytes, status, cas, key, value = @response_processor.getk_response_from_buffer(@buffer)
|
|
26
|
-
|
|
32
|
+
bytes, status, cas, key, value = @response_processor.getk_response_from_buffer(@buffer, @offset)
|
|
33
|
+
@offset += bytes
|
|
34
|
+
compact_if_needed
|
|
27
35
|
[status, cas, key, value]
|
|
28
36
|
end
|
|
29
37
|
|
|
30
|
-
# Advances the internal response buffer by bytes_to_advance
|
|
31
|
-
# bytes. The
|
|
32
|
-
def advance(bytes_to_advance)
|
|
33
|
-
return unless bytes_to_advance.positive?
|
|
34
|
-
|
|
35
|
-
@buffer = @buffer.byteslice(bytes_to_advance..-1)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
38
|
# Resets the internal buffer to an empty state,
|
|
39
39
|
# so that we're ready to read pipelined responses
|
|
40
40
|
def reset
|
|
41
41
|
@buffer = ''.b
|
|
42
|
+
@offset = 0
|
|
42
43
|
end
|
|
43
44
|
|
|
44
45
|
# Ensures the buffer is initialized for reading without discarding
|
|
@@ -48,16 +49,30 @@ module Dalli
|
|
|
48
49
|
return if in_progress?
|
|
49
50
|
|
|
50
51
|
@buffer = ''.b
|
|
52
|
+
@offset = 0
|
|
51
53
|
end
|
|
52
54
|
|
|
53
55
|
# Clear the internal response buffer
|
|
54
56
|
def clear
|
|
55
57
|
@buffer = nil
|
|
58
|
+
@offset = 0
|
|
56
59
|
end
|
|
57
60
|
|
|
58
61
|
def in_progress?
|
|
59
62
|
!@buffer.nil?
|
|
60
63
|
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# Only compact when we've consumed a significant portion of the buffer.
|
|
68
|
+
# This avoids per-response string allocation while preventing unbounded
|
|
69
|
+
# memory growth for large pipelines.
|
|
70
|
+
def compact_if_needed
|
|
71
|
+
return unless @offset > COMPACT_THRESHOLD && @offset > @buffer.bytesize / 2
|
|
72
|
+
|
|
73
|
+
@buffer = @buffer.byteslice(@offset..)
|
|
74
|
+
@offset = 0
|
|
75
|
+
end
|
|
61
76
|
end
|
|
62
77
|
end
|
|
63
78
|
end
|
data/lib/dalli/version.rb
CHANGED
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.3.
|
|
4
|
+
version: 4.3.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter M. Goldstein
|
|
@@ -77,7 +77,7 @@ licenses:
|
|
|
77
77
|
- MIT
|
|
78
78
|
metadata:
|
|
79
79
|
bug_tracker_uri: https://github.com/petergoldstein/dalli/issues
|
|
80
|
-
changelog_uri: https://github.com/petergoldstein/dalli/blob/
|
|
80
|
+
changelog_uri: https://github.com/petergoldstein/dalli/blob/v4.3/CHANGELOG.md
|
|
81
81
|
rubygems_mfa_required: 'true'
|
|
82
82
|
rdoc_options: []
|
|
83
83
|
require_paths:
|