dalli 5.0.1 → 5.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02d0fa949b7a065f86fb3ac7b511a3feacc50b700e82dcb385e0e79c56583a9d
4
- data.tar.gz: d72b9e4b014ae1ae0b0d5a1ebe09ea48cdf9fbf0a2cda000efc19f17d5d34368
3
+ metadata.gz: e67adaead94f1a41b7ef5554434e3d88742bdf6f80d6c6a7a8552ce325e4e212
4
+ data.tar.gz: 51924b636e902895274ef517305e501a79c3480a440f0be83110eb17a7c88c25
5
5
  SHA512:
6
- metadata.gz: bf26484aa345df2d43a78da570027b68a7fd0dd70d7a62275ebe95a5e29ed36c11a8b1385ff0c71818f55184245e085cd75962510f9f8559aad10b0d284f8d71
7
- data.tar.gz: 0b3913a33f61873d6e3da0d62ec643dbf49f679740d3469b7cb826083f02f3b32d37e2a20548634d09cecdc4389da7c5bdc43af530bb48956cb4084f91f10d9e
6
+ metadata.gz: b36ce658adf751963219ac3a32b14680a19fdc029be2dfc575429d17e5efd6ef2dbf377610b3f3d05dc5a13824c4bb5df8477b94973e5d210f887743f6e3886a
7
+ data.tar.gz: db34b04265de2c8911aae1ce20618e01375b08d769b81d965fca64e3ddd2c358b8a69ed885a821de46fe9d46d4a8f83c98f60f776a52fe8443c28ca631cfa9c6
data/CHANGELOG.md CHANGED
@@ -1,6 +1,40 @@
1
1
  Dalli Changelog
2
2
  =====================
3
3
 
4
+ Unreleased
5
+ ==========
6
+
7
+ 5.0.3
8
+ ==========
9
+
10
+ Performance:
11
+
12
+ - Eliminate double array allocation in `Client#perform` (#1093)
13
+ - Changed method signature from `perform(*all_args)` with destructuring to `perform(op, key, *args)`, letting Ruby decompose arguments directly without intermediate array allocations
14
+ - Reduces benchmark time by ~39% across all Dalli operations (get, set, delete, etc.)
15
+ - Thanks to Sam Obeid for this contribution
16
+
17
+ Features:
18
+
19
+ - Support `connect_timeout:` keyword argument with `resolv-replace` >= 0.2.0, which now correctly forwards keyword arguments through its `TCPSocket` patch (#1096)
20
+
21
+ - Add `Dalli::Instrumentation.disable!` to allow disabling OpenTelemetry instrumentation at runtime (#1088)
22
+ - Also exposes `Dalli::Instrumentation.tracer=` for setting a custom tracer
23
+
24
+ 5.0.2
25
+ ==========
26
+
27
+ Performance:
28
+
29
+ - Add single-server fast path for `get_multi`, `set_multi`, and `delete_multi` (#1077)
30
+ - When only one memcached server is configured, bypass the `Pipelined*` machinery (IO.select, response buffering, server grouping) and issue all quiet meta requests inline followed by a noop terminator
31
+ - `get_multi` shows ~1.5x improvement at 10 keys and ~1.75x at 100–500 keys compared to the `PipelinedGetter` path
32
+ - Thanks to Dan Mayer (Shopify) for this contribution
33
+
34
+ Development:
35
+
36
+ - Add `bin/benchmark_branch` script for benchmarking against the current branch
37
+
4
38
  5.0.1
5
39
  ==========
6
40
 
data/README.md CHANGED
@@ -90,6 +90,20 @@ Exceptions are automatically recorded on spans with error status. When an operat
90
90
  2. The span status is set to error with the exception message
91
91
  3. The exception is re-raised to the caller
92
92
 
93
+ ### Disabling Instrumentation
94
+
95
+ To disable instrumentation at runtime (e.g., in tests or specific environments):
96
+
97
+ ```ruby
98
+ Dalli::Instrumentation.disable!
99
+ ```
100
+
101
+ You can also assign a custom tracer directly:
102
+
103
+ ```ruby
104
+ Dalli::Instrumentation.tracer = my_custom_tracer
105
+ ```
106
+
93
107
  ### Zero Overhead
94
108
 
95
109
  When OpenTelemetry is not present, there is zero overhead - the tracing code checks once at startup and bypasses all instrumentation logic entirely when the SDK is not loaded.
data/lib/dalli/client.rb CHANGED
@@ -311,7 +311,11 @@ module Dalli
311
311
  return if hash.empty?
312
312
 
313
313
  Instrumentation.trace('set_multi', multi_trace_attrs('set_multi', hash.size, hash.keys)) do
314
- pipelined_setter.process(hash, ttl_or_default(ttl), req_options)
314
+ if ring.servers.size == 1
315
+ single_server_set_multi(hash, ttl_or_default(ttl), req_options)
316
+ else
317
+ pipelined_setter.process(hash, ttl_or_default(ttl), req_options)
318
+ end
315
319
  end
316
320
  end
317
321
 
@@ -368,7 +372,11 @@ module Dalli
368
372
  return if keys.empty?
369
373
 
370
374
  Instrumentation.trace('delete_multi', multi_trace_attrs('delete_multi', keys.size, keys)) do
371
- pipelined_deleter.process(keys)
375
+ if ring.servers.size == 1
376
+ single_server_delete_multi(keys)
377
+ else
378
+ pipelined_deleter.process(keys)
379
+ end
372
380
  end
373
381
  end
374
382
 
@@ -522,13 +530,52 @@ module Dalli
522
530
 
523
531
  def get_multi_hash(keys)
524
532
  Instrumentation.trace_with_result('get_multi', get_multi_attributes(keys)) do |span|
525
- {}.tap do |hash|
526
- pipelined_getter.process(keys) { |k, data| hash[k] = data.first }
527
- record_hit_miss_metrics(span, keys.size, hash.size)
528
- end
533
+ hash = if ring.servers.size == 1
534
+ single_server_get_multi(keys)
535
+ else
536
+ {}.tap do |h|
537
+ pipelined_getter.process(keys) { |k, data| h[k] = data.first }
538
+ end
539
+ end
540
+ record_hit_miss_metrics(span, keys.size, hash.size)
541
+ hash
529
542
  end
530
543
  end
531
544
 
545
+ def single_server
546
+ server = ring.servers.first
547
+ server if server&.alive?
548
+ end
549
+
550
+ def single_server_get_multi(keys)
551
+ keys.map! { |k| @key_manager.validate_key(k.to_s) }
552
+ return {} unless (server = single_server)
553
+
554
+ result = server.request(:read_multi_req, keys)
555
+ result.transform_keys! { |k| @key_manager.key_without_namespace(k) }
556
+ result
557
+ rescue Dalli::NetworkError
558
+ {}
559
+ end
560
+
561
+ def single_server_set_multi(hash, ttl, req_options)
562
+ pairs = hash.transform_keys { |k| @key_manager.validate_key(k.to_s) }
563
+ return unless (server = single_server)
564
+
565
+ server.request(:write_multi_req, pairs, ttl, req_options)
566
+ rescue Dalli::NetworkError
567
+ nil
568
+ end
569
+
570
+ def single_server_delete_multi(keys)
571
+ validated_keys = keys.map { |k| @key_manager.validate_key(k.to_s) }
572
+ return unless (server = single_server)
573
+
574
+ server.request(:delete_multi_req, validated_keys)
575
+ rescue Dalli::NetworkError
576
+ nil
577
+ end
578
+
532
579
  def get_multi_attributes(keys)
533
580
  multi_trace_attrs('get_multi', keys.size, keys)
534
581
  end
@@ -603,11 +650,11 @@ module Dalli
603
650
  # a particular memcached instance becomes unreachable, or the
604
651
  # operation times out.
605
652
  ##
606
- def perform(*all_args)
653
+ # rubocop:disable Naming/MethodParameterName
654
+ def perform(op, key, *args)
655
+ # rubocop:enable Naming/MethodParameterName
607
656
  return yield if block_given?
608
657
 
609
- op, key, *args = all_args
610
-
611
658
  key = key.to_s
612
659
  key = @key_manager.validate_key(key)
613
660
 
@@ -61,13 +61,14 @@ module Dalli
61
61
  # Uses the library name 'dalli' and current Dalli::VERSION.
62
62
  #
63
63
  # @return [OpenTelemetry::Trace::Tracer, nil] the tracer or nil if OTel unavailable
64
- # rubocop:disable ThreadSafety/ClassInstanceVariable
64
+ # rubocop:disable ThreadSafety/ClassInstanceVariable, ThreadSafety/ClassAndModuleAttributes
65
65
  def tracer
66
66
  return @tracer if defined?(@tracer)
67
67
 
68
68
  @tracer = (OpenTelemetry.tracer_provider.tracer('dalli', Dalli::VERSION) if defined?(OpenTelemetry))
69
69
  end
70
- # rubocop:enable ThreadSafety/ClassInstanceVariable
70
+
71
+ attr_writer :tracer
71
72
 
72
73
  # Returns true if instrumentation is enabled (OpenTelemetry SDK is available).
73
74
  #
@@ -76,6 +77,15 @@ module Dalli
76
77
  !tracer.nil?
77
78
  end
78
79
 
80
+ # Disable instrumentation.
81
+ #
82
+ # @return [nil]
83
+ def disable!
84
+ @tracer = nil
85
+ end
86
+
87
+ # rubocop:enable ThreadSafety/ClassInstanceVariable, ThreadSafety/ClassAndModuleAttributes
88
+
79
89
  # Wraps a block with a span if instrumentation is enabled.
80
90
  #
81
91
  # Creates a client span with the given name and attributes merged with
@@ -257,6 +257,82 @@ module Dalli
257
257
  @connection_manager.flush
258
258
  end
259
259
 
260
+ # Single-server fast path for get_multi. Inlines request formatting and
261
+ # response parsing to minimize per-key overhead. Avoids the PipelinedGetter
262
+ # machinery (IO.select, response buffering, server grouping).
263
+ def read_multi_req(keys)
264
+ is_raw = raw_mode?
265
+ # Inline request formatting — avoids RequestFormatter.meta_get overhead per key.
266
+ # In raw mode: "mg <key> v k q s\r\n" (no f flag, key at index 2)
267
+ # Normal mode: "mg <key> v f k q s\r\n" (key at index 3)
268
+ post_get = is_raw ? " v k q s\r\n" : " v f k q s\r\n"
269
+ keys.each do |key|
270
+ encoded_key, base64 = KeyRegularizer.encode(key)
271
+ write(base64 ? "mg #{encoded_key} b#{post_get}" : "mg #{encoded_key}#{post_get}")
272
+ end
273
+ write("mn\r\n")
274
+ @connection_manager.flush
275
+
276
+ read_multi_get_responses(is_raw)
277
+ end
278
+
279
+ def read_multi_get_responses(is_raw)
280
+ hash = {}
281
+ key_index = is_raw ? 2 : 3
282
+ while (line = @connection_manager.read_line)
283
+ break if line.start_with?('MN')
284
+ next unless line.start_with?('VA ')
285
+
286
+ key, value = parse_multi_get_value(line, key_index, is_raw)
287
+ hash[key] = value if key
288
+ end
289
+ hash
290
+ end
291
+
292
+ def parse_multi_get_value(line, key_index, is_raw)
293
+ tokens = line.chomp!(TERMINATOR).split
294
+ value = @connection_manager.read(tokens[1].to_i + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
295
+ raw_key = tokens[key_index]
296
+ return unless raw_key
297
+
298
+ key = KeyRegularizer.decode(raw_key[1..], tokens.include?('b'))
299
+ bitflags = is_raw ? 0 : response_processor.bitflags_from_tokens(tokens)
300
+ [key, @value_marshaller.retrieve(value, bitflags)]
301
+ end
302
+
303
+ # Single-server fast path for set_multi. Inlines request formatting to
304
+ # minimize per-key overhead. Avoids PipelinedSetter server grouping.
305
+ def write_multi_req(pairs, ttl, req_options)
306
+ ttl = TtlSanitizer.sanitize(ttl) if ttl
307
+ pairs.each do |key, raw_value|
308
+ (value, bitflags) = @value_marshaller.store(key, raw_value, req_options)
309
+ encoded_key, base64 = KeyRegularizer.encode(key)
310
+ # 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)
319
+ end
320
+ write_noop
321
+ response_processor.consume_all_responses_until_mn
322
+ end
323
+
324
+ # Single-server fast path for delete_multi. Writes all quiet delete requests
325
+ # terminated by a noop, then consumes all responses.
326
+ def delete_multi_req(keys)
327
+ keys.each do |key|
328
+ encoded_key, base64 = KeyRegularizer.encode(key)
329
+ # Inline format: "md <key> [b] q\r\n"
330
+ write(base64 ? "md #{encoded_key} b q\r\n" : "md #{encoded_key} q\r\n")
331
+ end
332
+ write_noop
333
+ response_processor.consume_all_responses_until_mn
334
+ end
335
+
260
336
  require_relative 'key_regularizer'
261
337
  require_relative 'request_formatter'
262
338
  require_relative 'response_processor'
data/lib/dalli/socket.rb CHANGED
@@ -106,14 +106,19 @@ module Dalli
106
106
  end
107
107
 
108
108
  # Detect and cache whether TCPSocket supports the connect_timeout: keyword argument.
109
- # Returns false if TCPSocket#initialize has been monkey-patched by gems like
110
- # socksify or resolv-replace, which don't support keyword arguments.
109
+ # Returns true for an unmodified TCPSocket on Ruby 3.0+, or for resolv-replace >= 0.2.0
110
+ # which forwards keyword arguments through its patch.
111
+ # Returns false when monkey-patched by gems like socksify or resolv-replace < 0.2.0.
111
112
  # rubocop:disable ThreadSafety/ClassInstanceVariable
112
113
  def self.supports_connect_timeout?
113
114
  return @supports_connect_timeout if defined?(@supports_connect_timeout)
114
115
 
115
- @supports_connect_timeout = RUBY_VERSION >= '3.0' &&
116
- ::TCPSocket.instance_method(:initialize).parameters == TCPSOCKET_NATIVE_PARAMETERS
116
+ @supports_connect_timeout = RUBY_ENGINE == 'ruby' && RUBY_VERSION >= '3.0' &&
117
+ ::TCPSocket.instance_method(:initialize).parameters.then do |params|
118
+ params == TCPSOCKET_NATIVE_PARAMETERS || params.any? do |type, _|
119
+ type == :keyrest
120
+ end
121
+ end
117
122
  end
118
123
  # rubocop:enable ThreadSafety/ClassInstanceVariable
119
124
 
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.1'
4
+ VERSION = '5.0.3'
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.1
4
+ version: 5.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter M. Goldstein
@@ -87,7 +87,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
87
87
  - !ruby/object:Gem::Version
88
88
  version: '0'
89
89
  requirements: []
90
- rubygems_version: 4.0.6
90
+ rubygems_version: 4.0.10
91
91
  specification_version: 4
92
92
  summary: High performance memcached client for Ruby
93
93
  test_files: []