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 +4 -4
- data/CHANGELOG.md +34 -0
- data/README.md +14 -0
- data/lib/dalli/client.rb +56 -9
- data/lib/dalli/instrumentation.rb +12 -2
- data/lib/dalli/protocol/meta.rb +76 -0
- data/lib/dalli/socket.rb +9 -4
- 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: e67adaead94f1a41b7ef5554434e3d88742bdf6f80d6c6a7a8552ce325e4e212
|
|
4
|
+
data.tar.gz: 51924b636e902895274ef517305e501a79c3480a440f0be83110eb17a7c88c25
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/dalli/protocol/meta.rb
CHANGED
|
@@ -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
|
|
110
|
-
#
|
|
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
|
|
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
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
|
+
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.
|
|
90
|
+
rubygems_version: 4.0.10
|
|
91
91
|
specification_version: 4
|
|
92
92
|
summary: High performance memcached client for Ruby
|
|
93
93
|
test_files: []
|