dalli 5.0.4 → 5.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b211a810c4d54d2d4d92203b390e8b29260f725e6dae699ca04703ac42cef8e9
4
- data.tar.gz: 4d765c4491412ad4b9c8ab6038232436cae5824db2abaffc20119f2dfb7fa1ee
3
+ metadata.gz: 5c110d569b03993cefe140fdc4b54fb5720ef407e15c0fe100e4a49f90b5ce36
4
+ data.tar.gz: 8a3967c3d9d82dd3a28172fdb06e7f235a8ebb6598fd742e04770b458d24d041
5
5
  SHA512:
6
- metadata.gz: a09168f456e5ce4691d0e24b128997c3f87bee7e046592a600490bc8e9526d1ce681d156fc9997c7045e623017ee0df1273d46c16656820b0f4933b8acc42cbb
7
- data.tar.gz: 14e72c85d60582e03b19f04b4fd822a2e789fc6f780ae3f55944271dc25f0eeb7d6de0acade39bedf4729710731d273f8501f2078dc690fa3093926041b8d178
6
+ metadata.gz: 486715371411ddcf9118f87296abaaf7a30597a340bba41f26d26a750268d85751fef62be52f332662d427fa79e0ed3ea9bf0e503747203f5039194875a878de
7
+ data.tar.gz: d1bdf88817ba281d7b88525b1e0579c6efefdd68cb1b437dbbc0cf4fa2f386a6d9612e579cbfc409b1c1458a05ee5b4c7af841599e1930433912a909550b84f1
data/CHANGELOG.md CHANGED
@@ -4,6 +4,28 @@ Dalli Changelog
4
4
  Unreleased
5
5
  ==========
6
6
 
7
+ 5.0.5
8
+ ==========
9
+
10
+ Performance:
11
+
12
+ - Batch multi-key commands into a single write to reduce packet overhead (#1107)
13
+ - With `TCP_NODELAY` set on sockets, each `write` call emits a separate packet; the meta protocol was calling `write` up to 3 times per key in multi-key operations (`get_multi`, `set_multi`, `delete_multi`), significantly increasing network traffic compared to the old binary protocol
14
+ - Multi-key request paths now buffer all per-key commands into a single binary string and flush once; single-key paths combine the write and flush into one `flushed_write` call
15
+ - Thanks to Jean Boussier for this contribution
16
+
17
+ - Avoid repeated `RUBY_ENGINE` checks on every socket read (#1103)
18
+ - Moved the JRuby branch from a runtime `if` inside `ConnectionManager#read` to a class-level conditional method definition, so the check happens once at load time rather than on every read call
19
+ - Thanks to Jean Boussier for this contribution
20
+
21
+ - Eliminate per-call array allocations in `ResponseProcessor` (#1104)
22
+ - Token sets passed to `error_on_unexpected!` (e.g. `[VA, EN, HD]`) were allocated as new arrays on every invocation; replaced with frozen constants defined once at class load time
23
+ - Thanks to Jean Boussier for this contribution
24
+
25
+ - Avoid string copies when building request commands in `RequestFormatter` (#1106)
26
+ - Changed `cmd + TERMINATOR` to `cmd << TERMINATOR`; since `cmd` is always a mutable string, the in-place append avoids copying the entire command string just to append two bytes
27
+ - Thanks to Jean Boussier for this contribution
28
+
7
29
  5.0.4
8
30
  ==========
9
31
 
@@ -18,7 +18,7 @@ module Dalli
18
18
 
19
19
  def_delegators :@value_marshaller, :serializer, :compressor, :compression_min_size, :compress_by_default?
20
20
  def_delegators :@connection_manager, :name, :sock, :hostname, :port, :close, :connected?, :socket_timeout,
21
- :socket_type, :up!, :down!, :write, :reconnect_down_server?, :raise_down_error
21
+ :socket_type, :up!, :down!, :write, :reconnect_down_server?, :raise_down_error, :flushed_write
22
22
 
23
23
  def initialize(attribs, client_options = {})
24
24
  hostname, port, socket_type, @weight, user_creds = ServerConfigParser.parse(attribs)
@@ -155,16 +155,20 @@ module Dalli
155
155
  error_on_request!(e)
156
156
  end
157
157
 
158
- def read(count)
159
- # JRuby doesn't support IO#timeout=, so use custom readfull implementation
160
- # CRuby 3.3+ has IO#timeout= which makes IO#read work with timeouts
161
- if RUBY_ENGINE == 'jruby'
158
+ # JRuby doesn't support IO#timeout=, so use custom readfull implementation
159
+ # CRuby 3.3+ has IO#timeout= which makes IO#read work with timeouts
160
+ if RUBY_ENGINE == 'jruby'
161
+ def read(count)
162
162
  @sock.readfull(count)
163
- else
163
+ rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
164
+ error_on_request!(e)
165
+ end
166
+ else
167
+ def read(count)
164
168
  @sock.read(count)
169
+ rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
170
+ error_on_request!(e)
165
171
  end
166
- rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
167
- error_on_request!(e)
168
172
  end
169
173
 
170
174
  def write(bytes)
@@ -179,6 +183,14 @@ module Dalli
179
183
  error_on_request!(e)
180
184
  end
181
185
 
186
+ def flushed_write(bytes)
187
+ written = @sock.write(bytes)
188
+ @sock.flush
189
+ written
190
+ rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, IOError => e
191
+ error_on_request!(e)
192
+ end
193
+
182
194
  # Non-blocking read. Here to support the operation
183
195
  # of the get_multi operation
184
196
  def read_nonblock
@@ -28,8 +28,7 @@ module Dalli
28
28
  # Skip bitflags in raw mode - saves 2 bytes per request and skips parsing
29
29
  skip_flags = raw_mode? || (options && options[:raw])
30
30
  req = RequestFormatter.meta_get(key: encoded_key, base64: base64, skip_flags: skip_flags)
31
- write(req)
32
- @connection_manager.flush
31
+ flushed_write(req)
33
32
  response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
34
33
  end
35
34
 
@@ -45,8 +44,7 @@ module Dalli
45
44
  encoded_key, base64 = KeyRegularizer.encode(key)
46
45
  skip_flags = raw_mode? || (options && options[:raw])
47
46
  req = RequestFormatter.meta_get(key: encoded_key, ttl: ttl, base64: base64, skip_flags: skip_flags)
48
- write(req)
49
- @connection_manager.flush
47
+ flushed_write(req)
50
48
  response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
51
49
  end
52
50
 
@@ -54,8 +52,7 @@ module Dalli
54
52
  ttl = TtlSanitizer.sanitize(ttl)
55
53
  encoded_key, base64 = KeyRegularizer.encode(key)
56
54
  req = RequestFormatter.meta_get(key: encoded_key, ttl: ttl, value: false, base64: base64)
57
- write(req)
58
- @connection_manager.flush
55
+ flushed_write(req)
59
56
  response_processor.meta_get_without_value
60
57
  end
61
58
 
@@ -64,8 +61,7 @@ module Dalli
64
61
  def cas(key)
65
62
  encoded_key, base64 = KeyRegularizer.encode(key)
66
63
  req = RequestFormatter.meta_get(key: encoded_key, value: true, return_cas: true, base64: base64)
67
- write(req)
68
- @connection_manager.flush
64
+ flushed_write(req)
69
65
  response_processor.meta_get_with_value_and_cas
70
66
  end
71
67
 
@@ -101,8 +97,7 @@ module Dalli
101
97
  return_hit_status: options[:return_hit_status],
102
98
  return_last_access: options[:return_last_access], skip_lru_bump: options[:skip_lru_bump]
103
99
  )
104
- write(req)
105
- @connection_manager.flush
100
+ flushed_write(req)
106
101
  response_processor.meta_get_with_metadata(
107
102
  cache_nils: cache_nils?(options), return_hit_status: options[:return_hit_status],
108
103
  return_last_access: options[:return_last_access]
@@ -119,8 +114,7 @@ module Dalli
119
114
  def delete_stale(key, cas = nil)
120
115
  encoded_key, base64 = KeyRegularizer.encode(key)
121
116
  req = RequestFormatter.meta_delete(key: encoded_key, cas: cas, base64: base64, stale: true)
122
- write(req)
123
- @connection_manager.flush
117
+ flushed_write(req)
124
118
  response_processor.meta_delete
125
119
  end
126
120
 
@@ -154,9 +148,7 @@ module Dalli
154
148
  req = RequestFormatter.meta_set(key: encoded_key, value: value,
155
149
  bitflags: bitflags, cas: cas,
156
150
  ttl: ttl, mode: mode, quiet: quiet, base64: base64)
157
- write(req)
158
- write(value)
159
- write(TERMINATOR)
151
+ write("#{req}#{value}#{TERMINATOR}")
160
152
  @connection_manager.flush unless quiet
161
153
  end
162
154
  # rubocop:enable Metrics/ParameterLists
@@ -177,9 +169,7 @@ module Dalli
177
169
  encoded_key, base64 = KeyRegularizer.encode(key)
178
170
  req = RequestFormatter.meta_set(key: encoded_key, value: value, base64: base64,
179
171
  cas: cas, ttl: ttl, mode: mode, quiet: quiet?)
180
- write(req)
181
- write(value)
182
- write(TERMINATOR)
172
+ write("#{req}#{value}#{TERMINATOR}")
183
173
  @connection_manager.flush unless quiet?
184
174
  end
185
175
  # rubocop:enable Metrics/ParameterLists
@@ -235,26 +225,22 @@ module Dalli
235
225
  end
236
226
 
237
227
  def stats(info = nil)
238
- write(RequestFormatter.stats(info))
239
- @connection_manager.flush
228
+ flushed_write(RequestFormatter.stats(info))
240
229
  response_processor.stats
241
230
  end
242
231
 
243
232
  def reset_stats
244
- write(RequestFormatter.stats('reset'))
245
- @connection_manager.flush
233
+ flushed_write(RequestFormatter.stats('reset'))
246
234
  response_processor.reset
247
235
  end
248
236
 
249
237
  def version
250
- write(RequestFormatter.version)
251
- @connection_manager.flush
238
+ flushed_write(RequestFormatter.version)
252
239
  response_processor.version
253
240
  end
254
241
 
255
242
  def write_noop
256
- write(RequestFormatter.meta_noop)
257
- @connection_manager.flush
243
+ flushed_write(RequestFormatter.meta_noop)
258
244
  end
259
245
 
260
246
  # Single-server fast path for get_multi. Inlines request formatting and
@@ -266,12 +252,18 @@ module Dalli
266
252
  # In raw mode: "mg <key> v k q s\r\n" (no f flag, key at index 2)
267
253
  # Normal mode: "mg <key> v f k q s\r\n" (key at index 3)
268
254
  post_get = is_raw ? " v k q s\r\n" : " v f k q s\r\n"
255
+ buffer = ''.b
269
256
  keys.each do |key|
270
257
  encoded_key, base64 = KeyRegularizer.encode(key)
271
- write(base64 ? "mg #{encoded_key} b#{post_get}" : "mg #{encoded_key}#{post_get}")
258
+ if base64
259
+ buffer << 'mg ' << encoded_key << ' b' << post_get
260
+ else
261
+ buffer << 'mg ' << encoded_key << post_get
262
+ end
272
263
  end
273
- write("mn\r\n")
274
- @connection_manager.flush
264
+ buffer << 'mn' << TERMINATOR
265
+ flushed_write(buffer)
266
+ buffer.clear
275
267
 
276
268
  read_multi_get_responses(is_raw)
277
269
  end
@@ -300,36 +292,46 @@ module Dalli
300
292
  [key, @value_marshaller.retrieve(value, bitflags)]
301
293
  end
302
294
 
295
+ # rubocop:disable Metrics/AbcSize
296
+
303
297
  # Single-server fast path for set_multi. Inlines request formatting to
304
298
  # minimize per-key overhead. Avoids PipelinedSetter server grouping.
305
299
  def write_multi_req(pairs, ttl, req_options)
306
300
  ttl = TtlSanitizer.sanitize(ttl) if ttl
301
+ buffer = ''.b
307
302
  pairs.each do |key, raw_value|
308
303
  (value, bitflags) = @value_marshaller.store(key, raw_value, req_options)
309
304
  encoded_key, base64 = KeyRegularizer.encode(key)
310
305
  # Inline format: "ms <key> <size> c [b] F<flags> T<ttl> MS q\r\n"
311
- cmd = "ms #{encoded_key} #{value.bytesize} c"
312
- cmd << ' b' if base64
313
- cmd << " F#{bitflags}" if bitflags
314
- cmd << " T#{ttl}" if ttl
315
- cmd << " MS q\r\n"
316
- write(cmd)
317
- write(value)
318
- write(TERMINATOR)
306
+ buffer << "ms #{encoded_key} #{value.bytesize} c"
307
+ buffer << ' b' if base64
308
+ buffer << " F#{bitflags}" if bitflags
309
+ buffer << " T#{ttl}" if ttl
310
+ buffer << ' MS q' << TERMINATOR << value << TERMINATOR
319
311
  end
320
- write_noop
312
+ buffer << RequestFormatter.meta_noop
313
+ flushed_write(buffer)
314
+ buffer.clear
321
315
  response_processor.consume_all_responses_until_mn
322
316
  end
317
+ # rubocop:enable Metrics/AbcSize
323
318
 
324
319
  # Single-server fast path for delete_multi. Writes all quiet delete requests
325
320
  # terminated by a noop, then consumes all responses.
326
321
  def delete_multi_req(keys)
322
+ buffer = ''.b
327
323
  keys.each do |key|
328
324
  encoded_key, base64 = KeyRegularizer.encode(key)
329
325
  # Inline format: "md <key> [b] q\r\n"
330
- write(base64 ? "md #{encoded_key} b q\r\n" : "md #{encoded_key} q\r\n")
326
+ if base64
327
+ buffer << 'md ' << encoded_key << ' b q' << TERMINATOR
328
+ else
329
+ buffer << 'md ' << encoded_key << ' q' << TERMINATOR
330
+ end
331
331
  end
332
- write_noop
332
+ buffer << RequestFormatter.meta_noop
333
+ flushed_write(buffer)
334
+ buffer.clear
333
335
  response_processor.consume_all_responses_until_mn
334
336
  end
335
337
 
@@ -52,7 +52,7 @@ module Dalli
52
52
  cmd << ' h' if return_hit_status # Return hit status (0 or 1)
53
53
  cmd << ' l' if return_last_access # Return seconds since last access
54
54
  cmd << ' u' if skip_lru_bump # Don't bump LRU or update access stats
55
- cmd + TERMINATOR
55
+ cmd << TERMINATOR
56
56
  end
57
57
 
58
58
  def self.meta_set(key:, value:, bitflags: nil, cas: nil, ttl: nil, mode: :set, base64: false, quiet: false)
@@ -77,7 +77,7 @@ module Dalli
77
77
  cmd << " T#{ttl}" if ttl
78
78
  cmd << ' I' if stale # Mark stale instead of deleting
79
79
  cmd << ' q' if quiet
80
- cmd + TERMINATOR
80
+ cmd << TERMINATOR
81
81
  end
82
82
 
83
83
  def self.meta_arithmetic(key:, delta:, initial:, incr: true, cas: nil, ttl: nil, base64: false, quiet: false)
@@ -90,14 +90,15 @@ module Dalli
90
90
  cmd << cas_string(cas)
91
91
  cmd << ' q' if quiet
92
92
  cmd << " M#{incr ? 'I' : 'D'}"
93
- cmd + TERMINATOR
93
+ cmd << TERMINATOR
94
94
  end
95
95
  # rubocop:enable Metrics/CyclomaticComplexity
96
96
  # rubocop:enable Metrics/ParameterLists
97
97
  # rubocop:enable Metrics/PerceivedComplexity
98
98
 
99
+ META_NOOP = "mn#{TERMINATOR}".freeze
99
100
  def self.meta_noop
100
- "mn#{TERMINATOR}"
101
+ META_NOOP
101
102
  end
102
103
 
103
104
  def self.version
@@ -108,7 +109,7 @@ module Dalli
108
109
  cmd = +'flush_all'
109
110
  cmd << " #{parse_to_64_bit_int(delay, 0)}" if delay
110
111
  cmd << ' noreply' if quiet
111
- cmd + TERMINATOR
112
+ cmd << TERMINATOR
112
113
  end
113
114
 
114
115
  ALLOWED_STATS_ARGS = [nil, '', 'items', 'slabs', 'settings', 'reset'].freeze
@@ -118,7 +119,7 @@ module Dalli
118
119
 
119
120
  cmd = +'stats'
120
121
  cmd << " #{arg}" if arg && !arg.empty?
121
- cmd + TERMINATOR
122
+ cmd << TERMINATOR
122
123
  end
123
124
 
124
125
  def self.mode_to_token(mode)
@@ -23,13 +23,23 @@ module Dalli
23
23
  VERSION = 'VERSION'
24
24
  SERVER_ERROR = 'SERVER_ERROR'
25
25
 
26
+ T_OK = [OK].freeze
27
+ T_RESET = [RESET].freeze
28
+ T_EN_HD = [EN, HD].freeze
29
+ T_VERSION = [VERSION].freeze
30
+ T_VA_EN_HD = [VA, EN, HD].freeze
31
+ T_HD_NF_EX = [HD, NF, EX].freeze
32
+ T_HD_NS_NF_EX = [HD, NS, NF, EX].freeze
33
+ T_VA_NF_NS_EX = [VA, NF, NS, EX].freeze
34
+ T_END_TOKEN_STAT = [END_TOKEN, STAT].freeze
35
+
26
36
  def initialize(io_source, value_marshaller)
27
37
  @io_source = io_source
28
38
  @value_marshaller = value_marshaller
29
39
  end
30
40
 
31
41
  def meta_get_with_value(cache_nils: false)
32
- tokens = error_on_unexpected!([VA, EN, HD])
42
+ tokens = error_on_unexpected!(T_VA_EN_HD)
33
43
  return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
34
44
  return true unless tokens.first == VA
35
45
 
@@ -37,7 +47,7 @@ module Dalli
37
47
  end
38
48
 
39
49
  def meta_get_with_value_and_cas
40
- tokens = error_on_unexpected!([VA, EN, HD])
50
+ tokens = error_on_unexpected!(T_VA_EN_HD)
41
51
  return [nil, 0] if tokens.first == EN
42
52
 
43
53
  cas = cas_from_tokens(tokens)
@@ -47,7 +57,7 @@ module Dalli
47
57
  end
48
58
 
49
59
  def meta_get_without_value
50
- tokens = error_on_unexpected!([EN, HD])
60
+ tokens = error_on_unexpected!(T_EN_HD)
51
61
  tokens.first == EN ? nil : true
52
62
  end
53
63
 
@@ -63,7 +73,7 @@ module Dalli
63
73
  # Used by meta_get for comprehensive metadata retrieval.
64
74
  # Supports thundering herd protection (N/R flags) and metadata flags (h/l/u).
65
75
  def meta_get_with_metadata(cache_nils: false, return_hit_status: false, return_last_access: false)
66
- tokens = error_on_unexpected!([VA, EN, HD])
76
+ tokens = error_on_unexpected!(T_VA_EN_HD)
67
77
  result = build_metadata_result(tokens)
68
78
  result[:hit_before] = hit_status_from_tokens(tokens) if return_hit_status
69
79
  result[:last_access] = last_access_from_tokens(tokens) if return_last_access
@@ -87,26 +97,26 @@ module Dalli
87
97
  end
88
98
 
89
99
  def meta_set_with_cas
90
- tokens = error_on_unexpected!([HD, NS, NF, EX])
100
+ tokens = error_on_unexpected!(T_HD_NS_NF_EX)
91
101
  return false unless tokens.first == HD
92
102
 
93
103
  cas_from_tokens(tokens)
94
104
  end
95
105
 
96
106
  def meta_set_append_prepend
97
- tokens = error_on_unexpected!([HD, NS, NF, EX])
107
+ tokens = error_on_unexpected!(T_HD_NS_NF_EX)
98
108
  return false unless tokens.first == HD
99
109
 
100
110
  true
101
111
  end
102
112
 
103
113
  def meta_delete
104
- tokens = error_on_unexpected!([HD, NF, EX])
114
+ tokens = error_on_unexpected!(T_HD_NF_EX)
105
115
  tokens.first == HD
106
116
  end
107
117
 
108
118
  def decr_incr
109
- tokens = error_on_unexpected!([VA, NF, NS, EX])
119
+ tokens = error_on_unexpected!(T_VA_NF_NS_EX)
110
120
  return false if [NS, EX].include?(tokens.first)
111
121
  return nil if tokens.first == NF
112
122
 
@@ -114,7 +124,7 @@ module Dalli
114
124
  end
115
125
 
116
126
  def stats
117
- tokens = error_on_unexpected!([END_TOKEN, STAT])
127
+ tokens = error_on_unexpected!(T_END_TOKEN_STAT)
118
128
  values = {}
119
129
  while tokens.first != END_TOKEN
120
130
  values[tokens[1]] = tokens[2]
@@ -124,19 +134,19 @@ module Dalli
124
134
  end
125
135
 
126
136
  def flush
127
- error_on_unexpected!([OK])
137
+ error_on_unexpected!(T_OK)
128
138
 
129
139
  true
130
140
  end
131
141
 
132
142
  def reset
133
- error_on_unexpected!([RESET])
143
+ error_on_unexpected!(T_RESET)
134
144
 
135
145
  true
136
146
  end
137
147
 
138
148
  def version
139
- tokens = error_on_unexpected!([VERSION])
149
+ tokens = error_on_unexpected!(T_VERSION)
140
150
  tokens.last
141
151
  end
142
152
 
data/lib/dalli/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dalli
4
- VERSION = '5.0.4'
4
+ VERSION = '5.0.5'
5
5
 
6
6
  MIN_SUPPORTED_MEMCACHED_VERSION = '1.6'
7
7
  end
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: 5.0.4
4
+ version: 5.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter M. Goldstein
@@ -88,7 +88,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
88
88
  - !ruby/object:Gem::Version
89
89
  version: '0'
90
90
  requirements: []
91
- rubygems_version: 4.0.10
91
+ rubygems_version: 4.0.12
92
92
  specification_version: 4
93
93
  summary: High performance memcached client for Ruby
94
94
  test_files: []