dalli 4.2.1 → 4.3.0
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 +26 -11
- data/README.md +25 -0
- data/lib/dalli/client.rb +16 -35
- data/lib/dalli/instrumentation.rb +11 -23
- data/lib/dalli/key_manager.rb +22 -7
- data/lib/dalli/pipelined_getter.rb +30 -1
- data/lib/dalli/protocol/base.rb +55 -3
- data/lib/dalli/protocol/response_buffer.rb +9 -0
- data/lib/dalli/protocol/value_serializer.rb +26 -16
- data/lib/dalli/socket.rb +25 -1
- 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: e97e2a407956737b411c33627d1f771be758017b881c68fab66edee95dc3249e
|
|
4
|
+
data.tar.gz: b57638f133a592e9d57b71530bc925ff6589b27549e8785bc6c7f334906636ff
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7dd43e9e5b09b65174f2e46b84de40e4e5bcfae9645c6bb67017ca81a64d344e7345cc4bf685f11d4779aeaafd7c7163df5a67bf8410153034f4adf523385b95
|
|
7
|
+
data.tar.gz: b57506702dbdcb387490b3c0c56d2407aff4f0f789aec01e2b1a92c9f8d925a0225b8563e3dbc1ccf3c3ca72a25ca58f4199f1738d93bdf65a07893ed2930b6d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,20 +1,35 @@
|
|
|
1
1
|
Dalli Changelog
|
|
2
2
|
=====================
|
|
3
3
|
|
|
4
|
-
4.
|
|
4
|
+
4.3.0
|
|
5
5
|
==========
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
New Features:
|
|
8
|
+
|
|
9
|
+
- Add `namespace_separator` option to customize the separator between namespace and key (#1019)
|
|
10
|
+
- Default is `:` for backward compatibility
|
|
11
|
+
- Must be a single non-alphanumeric character (e.g., `:`, `/`, `|`, `.`)
|
|
12
|
+
- Example: `Dalli::Client.new(servers, namespace: 'myapp', namespace_separator: '/')`
|
|
13
|
+
|
|
14
|
+
Bug Fixes:
|
|
15
|
+
|
|
16
|
+
- Fix architecture-dependent struct timeval packing for socket timeouts (#1034)
|
|
17
|
+
- Detects correct pack format for time_t and suseconds_t on each platform
|
|
18
|
+
- Fixes timeout issues on architectures with 64-bit time_t
|
|
19
|
+
|
|
20
|
+
- Fix get_multi hanging with large key counts (#776, #941)
|
|
21
|
+
- Add interleaved read/write for pipelined gets to prevent socket buffer deadlock
|
|
22
|
+
- For batches over 10,000 keys per server, requests are now sent in chunks
|
|
23
|
+
|
|
24
|
+
- **Breaking:** Enforce string-only values in raw mode (#1022)
|
|
25
|
+
- `set(key, nil, raw: true)` now raises `MarshalError` instead of storing `""`
|
|
26
|
+
- `set(key, 123, raw: true)` now raises `MarshalError` instead of storing `"123"`
|
|
27
|
+
- This matches the behavior of client-level `raw: true` mode
|
|
28
|
+
- To store counters, use string values: `set('counter', '0', raw: true)`
|
|
29
|
+
|
|
30
|
+
CI:
|
|
8
31
|
|
|
9
|
-
-
|
|
10
|
-
- `db.system` renamed to `db.system.name`
|
|
11
|
-
- `db.operation` renamed to `db.operation.name`
|
|
12
|
-
- `server.address` now contains hostname only; `server.port` is a separate integer attribute
|
|
13
|
-
- `get_with_metadata` and `fetch_with_lock` now include `server.address`/`server.port`
|
|
14
|
-
- Add `db.query.text` span attribute with configurable modes
|
|
15
|
-
- `:otel_db_statement` option: `:include`, `:obfuscate`, or `nil` (default: omitted)
|
|
16
|
-
- Add `peer.service` span attribute
|
|
17
|
-
- `:otel_peer_service` option for logical service naming
|
|
32
|
+
- Add TruffleRuby to CI test matrix (#988)
|
|
18
33
|
|
|
19
34
|
4.2.0
|
|
20
35
|
==========
|
data/README.md
CHANGED
|
@@ -31,6 +31,31 @@ Dalli supports two protocols for communicating with memcached:
|
|
|
31
31
|
Dalli::Client.new('localhost:11211', protocol: :meta)
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
## Configuration Options
|
|
35
|
+
|
|
36
|
+
### Namespace
|
|
37
|
+
|
|
38
|
+
Use namespaces to partition your cache and avoid key collisions between different applications or environments:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
# All keys will be prefixed with "myapp:"
|
|
42
|
+
Dalli::Client.new('localhost:11211', namespace: 'myapp')
|
|
43
|
+
|
|
44
|
+
# Dynamic namespace using a Proc (evaluated on each operation)
|
|
45
|
+
Dalli::Client.new('localhost:11211', namespace: -> { "tenant:#{Thread.current[:tenant_id]}" })
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Namespace Separator
|
|
49
|
+
|
|
50
|
+
By default, the namespace and key are joined with a colon (`:`). You can customize this with the `namespace_separator` option:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
# Keys will be prefixed with "myapp/" instead of "myapp:"
|
|
54
|
+
Dalli::Client.new('localhost:11211', namespace: 'myapp', namespace_separator: '/')
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The separator must be a single non-alphanumeric character. Valid examples: `:`, `/`, `|`, `.`, `-`, `_`, `#`
|
|
58
|
+
|
|
34
59
|
## Security Note
|
|
35
60
|
|
|
36
61
|
By default, Dalli uses Ruby's Marshal for serialization. Deserializing untrusted data with Marshal can lead to remote code execution. If you cache user-controlled data, consider using a safer serializer:
|
data/lib/dalli/client.rb
CHANGED
|
@@ -50,10 +50,6 @@ module Dalli
|
|
|
50
50
|
# useful for injecting a FIPS compliant hash object.
|
|
51
51
|
# - :protocol - one of either :binary or :meta, defaulting to :binary. This sets the protocol that Dalli uses
|
|
52
52
|
# to communicate with memcached.
|
|
53
|
-
# - :otel_db_statement - controls the +db.query.text+ span attribute when OpenTelemetry is loaded.
|
|
54
|
-
# +:include+ logs the full operation and key(s), +:obfuscate+ replaces keys with "?",
|
|
55
|
-
# +nil+ (default) omits the attribute entirely.
|
|
56
|
-
# - :otel_peer_service - when set, adds a +peer.service+ span attribute with this value for logical service naming.
|
|
57
53
|
#
|
|
58
54
|
def initialize(servers = nil, options = {})
|
|
59
55
|
@normalized_servers = ::Dalli::ServersArgNormalizer.normalize_servers(servers)
|
|
@@ -138,8 +134,8 @@ module Dalli
|
|
|
138
134
|
key = key.to_s
|
|
139
135
|
key = @key_manager.validate_key(key)
|
|
140
136
|
|
|
141
|
-
|
|
142
|
-
|
|
137
|
+
Instrumentation.trace('get_with_metadata', { 'db.operation' => 'get_with_metadata' }) do
|
|
138
|
+
server = ring.server_for_key(key)
|
|
143
139
|
server.request(:meta_get, key, options)
|
|
144
140
|
end
|
|
145
141
|
rescue NetworkError => e
|
|
@@ -241,8 +237,7 @@ module Dalli
|
|
|
241
237
|
key = key.to_s
|
|
242
238
|
key = @key_manager.validate_key(key)
|
|
243
239
|
|
|
244
|
-
|
|
245
|
-
Instrumentation.trace('fetch_with_lock', trace_attrs('fetch_with_lock', key, server)) do
|
|
240
|
+
Instrumentation.trace('fetch_with_lock', { 'db.operation' => 'fetch_with_lock' }) do
|
|
246
241
|
fetch_with_lock_request(key, ttl, lock_ttl, recache_threshold, req_options, &block)
|
|
247
242
|
end
|
|
248
243
|
rescue NetworkError => e
|
|
@@ -323,7 +318,10 @@ module Dalli
|
|
|
323
318
|
def set_multi(hash, ttl = nil, req_options = nil)
|
|
324
319
|
return if hash.empty?
|
|
325
320
|
|
|
326
|
-
Instrumentation.trace('set_multi',
|
|
321
|
+
Instrumentation.trace('set_multi', {
|
|
322
|
+
'db.operation' => 'set_multi',
|
|
323
|
+
'db.memcached.key_count' => hash.size
|
|
324
|
+
}) do
|
|
327
325
|
pipelined_setter.process(hash, ttl_or_default(ttl), req_options)
|
|
328
326
|
end
|
|
329
327
|
end
|
|
@@ -380,7 +378,10 @@ module Dalli
|
|
|
380
378
|
def delete_multi(keys)
|
|
381
379
|
return if keys.empty?
|
|
382
380
|
|
|
383
|
-
Instrumentation.trace('delete_multi',
|
|
381
|
+
Instrumentation.trace('delete_multi', {
|
|
382
|
+
'db.operation' => 'delete_multi',
|
|
383
|
+
'db.memcached.key_count' => keys.size
|
|
384
|
+
}) do
|
|
384
385
|
pipelined_deleter.process(keys)
|
|
385
386
|
end
|
|
386
387
|
end
|
|
@@ -547,30 +548,7 @@ module Dalli
|
|
|
547
548
|
end
|
|
548
549
|
|
|
549
550
|
def get_multi_attributes(keys)
|
|
550
|
-
|
|
551
|
-
end
|
|
552
|
-
|
|
553
|
-
def trace_attrs(operation, key, server)
|
|
554
|
-
attrs = { 'db.operation.name' => operation, 'server.address' => server.hostname }
|
|
555
|
-
attrs['server.port'] = server.port if server.socket_type == :tcp
|
|
556
|
-
attrs['peer.service'] = @options[:otel_peer_service] if @options[:otel_peer_service]
|
|
557
|
-
add_query_text(attrs, operation, key)
|
|
558
|
-
end
|
|
559
|
-
|
|
560
|
-
def multi_trace_attrs(operation, key_count, keys)
|
|
561
|
-
attrs = { 'db.operation.name' => operation, 'db.memcached.key_count' => key_count }
|
|
562
|
-
attrs['peer.service'] = @options[:otel_peer_service] if @options[:otel_peer_service]
|
|
563
|
-
add_query_text(attrs, operation, keys)
|
|
564
|
-
end
|
|
565
|
-
|
|
566
|
-
def add_query_text(attrs, operation, key_or_keys)
|
|
567
|
-
case @options[:otel_db_statement]
|
|
568
|
-
when :include
|
|
569
|
-
attrs['db.query.text'] = "#{operation} #{Array(key_or_keys).join(' ')}"
|
|
570
|
-
when :obfuscate
|
|
571
|
-
attrs['db.query.text'] = "#{operation} ?"
|
|
572
|
-
end
|
|
573
|
-
attrs
|
|
551
|
+
{ 'db.operation' => 'get_multi', 'db.memcached.key_count' => keys.size }
|
|
574
552
|
end
|
|
575
553
|
|
|
576
554
|
def check_positive!(amt)
|
|
@@ -638,7 +616,10 @@ module Dalli
|
|
|
638
616
|
key = @key_manager.validate_key(key)
|
|
639
617
|
|
|
640
618
|
server = ring.server_for_key(key)
|
|
641
|
-
Instrumentation.trace(op.to_s,
|
|
619
|
+
Instrumentation.trace(op.to_s, {
|
|
620
|
+
'db.operation' => op.to_s,
|
|
621
|
+
'server.address' => server.name
|
|
622
|
+
}) do
|
|
642
623
|
server.request(op, key, *args)
|
|
643
624
|
end
|
|
644
625
|
rescue NetworkError => e
|
|
@@ -8,36 +8,25 @@ module Dalli
|
|
|
8
8
|
# When OpenTelemetry is loaded, Dalli automatically creates spans for cache operations.
|
|
9
9
|
# When OpenTelemetry is not available, all tracing methods are no-ops with zero overhead.
|
|
10
10
|
#
|
|
11
|
-
# Dalli 4.2.1 uses the stable OTel semantic conventions for database spans.
|
|
12
|
-
#
|
|
13
11
|
# == Span Attributes
|
|
14
12
|
#
|
|
15
13
|
# All spans include the following default attributes:
|
|
16
|
-
# - +db.system
|
|
14
|
+
# - +db.system+ - Always "memcached"
|
|
17
15
|
#
|
|
18
16
|
# Single-key operations (+get+, +set+, +delete+, +incr+, +decr+, etc.) add:
|
|
19
|
-
# - +db.operation
|
|
20
|
-
# - +server.address+ - The server
|
|
21
|
-
# - +server.port+ - The server port as an integer (e.g., 11211); omitted for Unix sockets
|
|
17
|
+
# - +db.operation+ - The operation name (e.g., "get", "set")
|
|
18
|
+
# - +server.address+ - The memcached server handling the request (e.g., "localhost:11211")
|
|
22
19
|
#
|
|
23
20
|
# Multi-key operations (+get_multi+) add:
|
|
24
|
-
# - +db.operation
|
|
21
|
+
# - +db.operation+ - "get_multi"
|
|
25
22
|
# - +db.memcached.key_count+ - Number of keys requested
|
|
26
23
|
# - +db.memcached.hit_count+ - Number of keys found in cache
|
|
27
24
|
# - +db.memcached.miss_count+ - Number of keys not found
|
|
28
25
|
#
|
|
29
26
|
# Bulk write operations (+set_multi+, +delete_multi+) add:
|
|
30
|
-
# - +db.operation
|
|
27
|
+
# - +db.operation+ - The operation name
|
|
31
28
|
# - +db.memcached.key_count+ - Number of keys in the operation
|
|
32
29
|
#
|
|
33
|
-
# == Optional Attributes
|
|
34
|
-
#
|
|
35
|
-
# - +db.query.text+ - The operation and key(s), controlled by the +:otel_db_statement+ client option:
|
|
36
|
-
# - +:include+ - Full text (e.g., "get mykey")
|
|
37
|
-
# - +:obfuscate+ - Obfuscated (e.g., "get ?")
|
|
38
|
-
# - +nil+ (default) - Attribute omitted
|
|
39
|
-
# - +peer.service+ - Logical service name, set via the +:otel_peer_service+ client option
|
|
40
|
-
#
|
|
41
30
|
# == Error Handling
|
|
42
31
|
#
|
|
43
32
|
# When an exception occurs during a traced operation:
|
|
@@ -51,8 +40,8 @@ module Dalli
|
|
|
51
40
|
##
|
|
52
41
|
module Instrumentation
|
|
53
42
|
# Default attributes included on all memcached spans.
|
|
54
|
-
# @return [Hash] frozen hash with 'db.system
|
|
55
|
-
DEFAULT_ATTRIBUTES = { 'db.system
|
|
43
|
+
# @return [Hash] frozen hash with 'db.system' => 'memcached'
|
|
44
|
+
DEFAULT_ATTRIBUTES = { 'db.system' => 'memcached' }.freeze
|
|
56
45
|
|
|
57
46
|
class << self
|
|
58
47
|
# Returns the OpenTelemetry tracer if available, nil otherwise.
|
|
@@ -86,16 +75,15 @@ module Dalli
|
|
|
86
75
|
# @param name [String] the span name (e.g., 'get', 'set', 'delete')
|
|
87
76
|
# @param attributes [Hash] span attributes to merge with defaults.
|
|
88
77
|
# Common attributes include:
|
|
89
|
-
# - 'db.operation
|
|
90
|
-
# - 'server.address' - the server
|
|
91
|
-
# - 'server.port' - the server port (integer)
|
|
78
|
+
# - 'db.operation' - the operation name
|
|
79
|
+
# - 'server.address' - the target server
|
|
92
80
|
# - 'db.memcached.key_count' - number of keys (for multi operations)
|
|
93
81
|
# @yield the cache operation to trace
|
|
94
82
|
# @return [Object] the result of the block
|
|
95
83
|
# @raise [StandardError] re-raises any exception from the block
|
|
96
84
|
#
|
|
97
85
|
# @example Tracing a set operation
|
|
98
|
-
# trace('set', { 'db.operation
|
|
86
|
+
# trace('set', { 'db.operation' => 'set', 'server.address' => 'localhost:11211' }) do
|
|
99
87
|
# server.set(key, value, ttl)
|
|
100
88
|
# end
|
|
101
89
|
#
|
|
@@ -126,7 +114,7 @@ module Dalli
|
|
|
126
114
|
# @raise [StandardError] re-raises any exception from the block
|
|
127
115
|
#
|
|
128
116
|
# @example Recording hit/miss metrics after get_multi
|
|
129
|
-
# trace_with_result('get_multi', { 'db.operation
|
|
117
|
+
# trace_with_result('get_multi', { 'db.operation' => 'get_multi' }) do |span|
|
|
130
118
|
# results = fetch_from_cache(keys)
|
|
131
119
|
# if span
|
|
132
120
|
# span.set_attribute('db.memcached.hit_count', results.size)
|
data/lib/dalli/key_manager.rb
CHANGED
|
@@ -12,7 +12,7 @@ module Dalli
|
|
|
12
12
|
class KeyManager
|
|
13
13
|
MAX_KEY_LENGTH = 250
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
DEFAULT_NAMESPACE_SEPARATOR = ':'
|
|
16
16
|
|
|
17
17
|
# This is a hard coded md5 for historical reasons
|
|
18
18
|
TRUNCATED_KEY_SEPARATOR = ':md5:'
|
|
@@ -21,19 +21,26 @@ module Dalli
|
|
|
21
21
|
TRUNCATED_KEY_TARGET_SIZE = 249
|
|
22
22
|
|
|
23
23
|
DEFAULTS = {
|
|
24
|
-
digest_class: ::Digest::MD5
|
|
24
|
+
digest_class: ::Digest::MD5,
|
|
25
|
+
namespace_separator: DEFAULT_NAMESPACE_SEPARATOR
|
|
25
26
|
}.freeze
|
|
26
27
|
|
|
27
|
-
OPTIONS = %i[digest_class namespace].freeze
|
|
28
|
+
OPTIONS = %i[digest_class namespace namespace_separator].freeze
|
|
28
29
|
|
|
29
|
-
attr_reader :namespace
|
|
30
|
+
attr_reader :namespace, :namespace_separator
|
|
31
|
+
|
|
32
|
+
# Valid separators: non-alphanumeric, single printable ASCII characters
|
|
33
|
+
# Excludes: alphanumerics, whitespace, control characters
|
|
34
|
+
VALID_NAMESPACE_SEPARATORS = /\A[^a-zA-Z0-9\s\x00-\x1F\x7F]\z/
|
|
30
35
|
|
|
31
36
|
def initialize(client_options)
|
|
32
37
|
@key_options =
|
|
33
38
|
DEFAULTS.merge(client_options.slice(*OPTIONS))
|
|
34
39
|
validate_digest_class_option(@key_options)
|
|
40
|
+
validate_namespace_separator_option(@key_options)
|
|
35
41
|
|
|
36
42
|
@namespace = namespace_from_options
|
|
43
|
+
@namespace_separator = @key_options[:namespace_separator]
|
|
37
44
|
end
|
|
38
45
|
|
|
39
46
|
##
|
|
@@ -61,7 +68,7 @@ module Dalli
|
|
|
61
68
|
def key_with_namespace(key)
|
|
62
69
|
return key if namespace.nil?
|
|
63
70
|
|
|
64
|
-
"#{evaluate_namespace}#{
|
|
71
|
+
"#{evaluate_namespace}#{namespace_separator}#{key}"
|
|
65
72
|
end
|
|
66
73
|
|
|
67
74
|
def key_without_namespace(key)
|
|
@@ -75,9 +82,9 @@ module Dalli
|
|
|
75
82
|
end
|
|
76
83
|
|
|
77
84
|
def namespace_regexp
|
|
78
|
-
return /\A#{Regexp.escape(evaluate_namespace)}
|
|
85
|
+
return /\A#{Regexp.escape(evaluate_namespace)}#{Regexp.escape(namespace_separator)}/ if namespace.is_a?(Proc)
|
|
79
86
|
|
|
80
|
-
@namespace_regexp ||= /\A#{Regexp.escape(namespace)}
|
|
87
|
+
@namespace_regexp ||= /\A#{Regexp.escape(namespace)}#{Regexp.escape(namespace_separator)}/ unless namespace.nil?
|
|
81
88
|
end
|
|
82
89
|
|
|
83
90
|
def validate_digest_class_option(opts)
|
|
@@ -86,6 +93,14 @@ module Dalli
|
|
|
86
93
|
raise ArgumentError, 'The digest_class object must respond to the hexdigest method'
|
|
87
94
|
end
|
|
88
95
|
|
|
96
|
+
def validate_namespace_separator_option(opts)
|
|
97
|
+
sep = opts[:namespace_separator]
|
|
98
|
+
return if VALID_NAMESPACE_SEPARATORS.match?(sep)
|
|
99
|
+
|
|
100
|
+
raise ArgumentError,
|
|
101
|
+
'namespace_separator must be a single non-alphanumeric character (e.g., ":", "/", "|")'
|
|
102
|
+
end
|
|
103
|
+
|
|
89
104
|
def namespace_from_options
|
|
90
105
|
raw_namespace = @key_options[:namespace]
|
|
91
106
|
return nil unless raw_namespace
|
|
@@ -7,6 +7,13 @@ module Dalli
|
|
|
7
7
|
# Contains logic for the pipelined gets implemented by the client.
|
|
8
8
|
##
|
|
9
9
|
class PipelinedGetter
|
|
10
|
+
# For large batches, interleave sends with response draining to prevent
|
|
11
|
+
# socket buffer deadlock. Only kicks in above this threshold.
|
|
12
|
+
INTERLEAVE_THRESHOLD = 10_000
|
|
13
|
+
|
|
14
|
+
# Number of keys to send before draining responses during interleaved mode
|
|
15
|
+
CHUNK_SIZE = 10_000
|
|
16
|
+
|
|
10
17
|
def initialize(ring, key_manager)
|
|
11
18
|
@ring = ring
|
|
12
19
|
@key_manager = key_manager
|
|
@@ -19,8 +26,14 @@ module Dalli
|
|
|
19
26
|
return {} if keys.empty?
|
|
20
27
|
|
|
21
28
|
@ring.lock do
|
|
29
|
+
# Stores partial results collected during interleaved send phase
|
|
30
|
+
@partial_results = {}
|
|
22
31
|
servers = setup_requests(keys)
|
|
23
32
|
start_time = Time.now
|
|
33
|
+
|
|
34
|
+
# First yield any partial results collected during interleaved send
|
|
35
|
+
yield_partial_results(&block)
|
|
36
|
+
|
|
24
37
|
servers = fetch_responses(servers, start_time, @ring.socket_timeout, &block) until servers.empty?
|
|
25
38
|
end
|
|
26
39
|
rescue NetworkError => e
|
|
@@ -29,6 +42,15 @@ module Dalli
|
|
|
29
42
|
retry
|
|
30
43
|
end
|
|
31
44
|
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def yield_partial_results
|
|
48
|
+
@partial_results.each_pair do |key, value_list|
|
|
49
|
+
yield @key_manager.key_without_namespace(key), value_list
|
|
50
|
+
end
|
|
51
|
+
@partial_results.clear
|
|
52
|
+
end
|
|
53
|
+
|
|
32
54
|
def setup_requests(keys)
|
|
33
55
|
groups = groups_for_keys(keys)
|
|
34
56
|
make_getkq_requests(groups)
|
|
@@ -47,7 +69,14 @@ module Dalli
|
|
|
47
69
|
##
|
|
48
70
|
def make_getkq_requests(groups)
|
|
49
71
|
groups.each do |server, keys_for_server|
|
|
50
|
-
|
|
72
|
+
if keys_for_server.size <= INTERLEAVE_THRESHOLD
|
|
73
|
+
# Small batch - send all at once (existing behavior)
|
|
74
|
+
server.request(:pipelined_get, keys_for_server)
|
|
75
|
+
else
|
|
76
|
+
# Large batch - interleave sends with response draining
|
|
77
|
+
# Pass @partial_results directly to avoid hash allocation/merge overhead
|
|
78
|
+
server.request(:pipelined_get_interleaved, keys_for_server, CHUNK_SIZE, @partial_results)
|
|
79
|
+
end
|
|
51
80
|
rescue DalliError, NetworkError => e
|
|
52
81
|
Dalli.logger.debug { e.inspect }
|
|
53
82
|
Dalli.logger.debug { "unable to get keys for server #{server.name}" }
|
data/lib/dalli/protocol/base.rb
CHANGED
|
@@ -42,8 +42,8 @@ module Dalli
|
|
|
42
42
|
@connection_manager.start_request!
|
|
43
43
|
response = send(opkey, *args)
|
|
44
44
|
|
|
45
|
-
# pipelined_get emit query but
|
|
46
|
-
@connection_manager.finish_request! unless opkey
|
|
45
|
+
# pipelined_get/pipelined_get_interleaved emit query but don't read the response(s)
|
|
46
|
+
@connection_manager.finish_request! unless %i[pipelined_get pipelined_get_interleaved].include?(opkey)
|
|
47
47
|
|
|
48
48
|
response
|
|
49
49
|
rescue Dalli::MarshalError => e
|
|
@@ -81,7 +81,9 @@ module Dalli
|
|
|
81
81
|
def pipeline_response_setup
|
|
82
82
|
verify_pipelined_state(:getkq)
|
|
83
83
|
write_noop
|
|
84
|
-
|
|
84
|
+
# Use ensure_ready instead of reset to preserve any data already buffered
|
|
85
|
+
# during interleaved pipelined get draining
|
|
86
|
+
response_buffer.ensure_ready
|
|
85
87
|
end
|
|
86
88
|
|
|
87
89
|
# Attempt to receive and parse as many key/value pairs as possible
|
|
@@ -220,6 +222,11 @@ module Dalli
|
|
|
220
222
|
end
|
|
221
223
|
|
|
222
224
|
def pipelined_get(keys)
|
|
225
|
+
# Clear buffer to remove any stale data from interrupted operations.
|
|
226
|
+
# Use clear (not reset) to keep pipeline_complete? = true, which is
|
|
227
|
+
# the expected state before pipeline_response_setup is called.
|
|
228
|
+
response_buffer.clear
|
|
229
|
+
|
|
223
230
|
req = +''
|
|
224
231
|
keys.each do |key|
|
|
225
232
|
req << quiet_get_request(key)
|
|
@@ -228,6 +235,51 @@ module Dalli
|
|
|
228
235
|
write(req)
|
|
229
236
|
end
|
|
230
237
|
|
|
238
|
+
# For large batches, interleave writing requests with draining responses.
|
|
239
|
+
# This prevents socket buffer deadlock when sending many keys.
|
|
240
|
+
# Populates the provided results hash with any responses drained during send.
|
|
241
|
+
def pipelined_get_interleaved(keys, chunk_size, results)
|
|
242
|
+
# Initialize the response buffer for draining during send phase
|
|
243
|
+
response_buffer.ensure_ready
|
|
244
|
+
|
|
245
|
+
keys.each_slice(chunk_size) do |chunk|
|
|
246
|
+
# Build and write this chunk of requests
|
|
247
|
+
req = +''
|
|
248
|
+
chunk.each do |key|
|
|
249
|
+
req << quiet_get_request(key)
|
|
250
|
+
end
|
|
251
|
+
write(req)
|
|
252
|
+
@connection_manager.flush
|
|
253
|
+
|
|
254
|
+
# Drain any available responses directly into results hash
|
|
255
|
+
drain_pipeline_responses(results)
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Non-blocking read and processing of any available pipeline responses.
|
|
260
|
+
# Used during interleaved pipelined gets to prevent buffer deadlock.
|
|
261
|
+
# Populates the provided results hash directly to avoid allocation overhead.
|
|
262
|
+
def drain_pipeline_responses(results)
|
|
263
|
+
return unless connected?
|
|
264
|
+
|
|
265
|
+
# Non-blocking check if socket has data available
|
|
266
|
+
return unless sock.wait_readable(0)
|
|
267
|
+
|
|
268
|
+
# Read available data without blocking
|
|
269
|
+
response_buffer.read
|
|
270
|
+
|
|
271
|
+
# Process any complete responses in the buffer
|
|
272
|
+
loop do
|
|
273
|
+
status, cas, key, value = response_buffer.process_single_getk_response
|
|
274
|
+
break if status.nil? # No complete response available
|
|
275
|
+
|
|
276
|
+
results[key] = [value, cas] unless key.nil?
|
|
277
|
+
end
|
|
278
|
+
rescue SystemCallError, Dalli::NetworkError
|
|
279
|
+
# Ignore errors during drain - they'll be handled in fetch_responses
|
|
280
|
+
nil
|
|
281
|
+
end
|
|
282
|
+
|
|
231
283
|
def response_buffer
|
|
232
284
|
@response_buffer ||= ResponseBuffer.new(@connection_manager, response_processor)
|
|
233
285
|
end
|
|
@@ -41,6 +41,15 @@ module Dalli
|
|
|
41
41
|
@buffer = ''.b
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
# Ensures the buffer is initialized for reading without discarding
|
|
45
|
+
# existing data. Used by interleaved pipelined get which may have
|
|
46
|
+
# already buffered partial responses during the send phase.
|
|
47
|
+
def ensure_ready
|
|
48
|
+
return if in_progress?
|
|
49
|
+
|
|
50
|
+
@buffer = ''.b
|
|
51
|
+
end
|
|
52
|
+
|
|
44
53
|
# Clear the internal response buffer
|
|
45
54
|
def clear
|
|
46
55
|
@buffer = nil
|
|
@@ -32,22 +32,8 @@ module Dalli
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def store(value, req_options, bitflags)
|
|
35
|
-
if req_options
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
# If the value is a simple string, going through serialization is costly
|
|
39
|
-
# for no benefit other than preserving encoding.
|
|
40
|
-
# Assuming most strings are either UTF-8 or BINARY we can just store
|
|
41
|
-
# that information in the bitflags.
|
|
42
|
-
if req_options[:string_fastpath] && value.instance_of?(String)
|
|
43
|
-
case value.encoding
|
|
44
|
-
when Encoding::BINARY
|
|
45
|
-
return [value, bitflags]
|
|
46
|
-
when Encoding::UTF_8
|
|
47
|
-
return [value, bitflags | FLAG_UTF8]
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
35
|
+
return store_raw(value, bitflags) if req_options&.dig(:raw)
|
|
36
|
+
return store_string_fastpath(value, bitflags) if use_string_fastpath?(value, req_options)
|
|
51
37
|
|
|
52
38
|
[serialize_value(value), bitflags | FLAG_SERIALIZED]
|
|
53
39
|
end
|
|
@@ -85,6 +71,30 @@ module Dalli
|
|
|
85
71
|
|
|
86
72
|
private
|
|
87
73
|
|
|
74
|
+
def store_raw(value, bitflags)
|
|
75
|
+
unless value.is_a?(String)
|
|
76
|
+
raise Dalli::MarshalError, "Dalli raw mode requires string values, got: #{value.class}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
[value, bitflags]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# If the value is a simple string, going through serialization is costly
|
|
83
|
+
# for no benefit other than preserving encoding.
|
|
84
|
+
# Assuming most strings are either UTF-8 or BINARY we can just store
|
|
85
|
+
# that information in the bitflags.
|
|
86
|
+
def store_string_fastpath(value, bitflags)
|
|
87
|
+
case value.encoding
|
|
88
|
+
when Encoding::BINARY then [value, bitflags]
|
|
89
|
+
when Encoding::UTF_8 then [value, bitflags | FLAG_UTF8]
|
|
90
|
+
else [serialize_value(value), bitflags | FLAG_SERIALIZED]
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def use_string_fastpath?(value, req_options)
|
|
95
|
+
req_options&.dig(:string_fastpath) && value.instance_of?(String)
|
|
96
|
+
end
|
|
97
|
+
|
|
88
98
|
def warn_if_marshal_default(protocol_options)
|
|
89
99
|
return if protocol_options.key?(:serializer)
|
|
90
100
|
return if @@marshal_warning_logged
|
data/lib/dalli/socket.rb
CHANGED
|
@@ -138,16 +138,40 @@ module Dalli
|
|
|
138
138
|
return unless options[:socket_timeout]
|
|
139
139
|
|
|
140
140
|
if sock.respond_to?(:timeout=)
|
|
141
|
+
# Ruby 3.2+ has IO#timeout for reliable cross-platform timeout handling
|
|
141
142
|
sock.timeout = options[:socket_timeout]
|
|
142
143
|
else
|
|
144
|
+
# Ruby 3.1 fallback using socket options
|
|
145
|
+
# struct timeval has architecture-dependent sizes (time_t, suseconds_t)
|
|
143
146
|
seconds, fractional = options[:socket_timeout].divmod(1)
|
|
144
|
-
|
|
147
|
+
microseconds = (fractional * 1_000_000).to_i
|
|
148
|
+
timeval = pack_timeval(sock, seconds, microseconds)
|
|
145
149
|
|
|
146
150
|
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVTIMEO, timeval)
|
|
147
151
|
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDTIMEO, timeval)
|
|
148
152
|
end
|
|
149
153
|
end
|
|
150
154
|
|
|
155
|
+
# Pack formats for struct timeval across architectures.
|
|
156
|
+
# Uses fixed-size formats for JRuby compatibility (JRuby doesn't support _ modifier on q).
|
|
157
|
+
# - ll: 8 bytes (32-bit time_t, 32-bit suseconds_t)
|
|
158
|
+
# - qq: 16 bytes (64-bit time_t, 64-bit suseconds_t or padded 32-bit)
|
|
159
|
+
TIMEVAL_PACK_FORMATS = %w[ll qq].freeze
|
|
160
|
+
TIMEVAL_TEST_VALUES = [0, 0].freeze
|
|
161
|
+
|
|
162
|
+
# Detect and cache the correct pack format for struct timeval on this platform.
|
|
163
|
+
# Different architectures have different sizes for time_t and suseconds_t.
|
|
164
|
+
def self.timeval_pack_format(sock)
|
|
165
|
+
@timeval_pack_format ||= begin
|
|
166
|
+
expected_size = sock.getsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVTIMEO).data.bytesize
|
|
167
|
+
TIMEVAL_PACK_FORMATS.find { |fmt| TIMEVAL_TEST_VALUES.pack(fmt).bytesize == expected_size } || 'll'
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def self.pack_timeval(sock, seconds, microseconds)
|
|
172
|
+
[seconds, microseconds].pack(timeval_pack_format(sock))
|
|
173
|
+
end
|
|
174
|
+
|
|
151
175
|
def self.wrapping_ssl_socket(tcp_socket, host, ssl_context)
|
|
152
176
|
ssl_socket = Dalli::Socket::SSLSocket.new(tcp_socket, ssl_context)
|
|
153
177
|
ssl_socket.hostname = host
|
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.
|
|
4
|
+
version: 4.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter M. Goldstein
|
|
@@ -93,7 +93,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
93
93
|
- !ruby/object:Gem::Version
|
|
94
94
|
version: '0'
|
|
95
95
|
requirements: []
|
|
96
|
-
rubygems_version: 4.0.
|
|
96
|
+
rubygems_version: 4.0.4
|
|
97
97
|
specification_version: 4
|
|
98
98
|
summary: High performance memcached client for Ruby
|
|
99
99
|
test_files: []
|