dalli 4.1.0 → 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 +50 -0
- data/README.md +66 -0
- data/lib/dalli/client.rb +76 -26
- data/lib/dalli/instrumentation.rb +139 -0
- data/lib/dalli/key_manager.rb +22 -7
- data/lib/dalli/pipelined_getter.rb +35 -4
- data/lib/dalli/protocol/base.rb +63 -4
- data/lib/dalli/protocol/binary.rb +13 -0
- data/lib/dalli/protocol/connection_manager.rb +7 -0
- data/lib/dalli/protocol/meta/request_formatter.rb +5 -2
- data/lib/dalli/protocol/meta.rb +23 -3
- data/lib/dalli/protocol/response_buffer.rb +9 -0
- data/lib/dalli/protocol/value_serializer.rb +26 -16
- data/lib/dalli/socket.rb +26 -2
- data/lib/dalli/version.rb +1 -1
- data/lib/dalli.rb +1 -0
- metadata +2 -1
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,6 +1,56 @@
|
|
|
1
1
|
Dalli Changelog
|
|
2
2
|
=====================
|
|
3
3
|
|
|
4
|
+
4.3.0
|
|
5
|
+
==========
|
|
6
|
+
|
|
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:
|
|
31
|
+
|
|
32
|
+
- Add TruffleRuby to CI test matrix (#988)
|
|
33
|
+
|
|
34
|
+
4.2.0
|
|
35
|
+
==========
|
|
36
|
+
|
|
37
|
+
Performance:
|
|
38
|
+
|
|
39
|
+
- Buffered I/O: Use `socket.sync = false` with explicit flush to reduce syscalls for pipelined operations
|
|
40
|
+
- get_multi optimizations: Use Set for O(1) server tracking lookups
|
|
41
|
+
- Raw mode optimization: Skip bitflags request in meta protocol when in raw mode (saves 2 bytes per request)
|
|
42
|
+
|
|
43
|
+
New Features:
|
|
44
|
+
|
|
45
|
+
- OpenTelemetry tracing support: Automatically instruments operations when OpenTelemetry SDK is present
|
|
46
|
+
- Zero overhead when OpenTelemetry is not loaded
|
|
47
|
+
- Traces `get`, `set`, `delete`, `get_multi`, `set_multi`, `delete_multi`, `get_with_metadata`, and `fetch_with_lock`
|
|
48
|
+
- Spans include `db.system: memcached` and `db.operation` attributes
|
|
49
|
+
- Single-key operations include `server.address` attribute
|
|
50
|
+
- Multi-key operations include `db.memcached.key_count` attribute
|
|
51
|
+
- `get_multi` spans include `db.memcached.hit_count` and `db.memcached.miss_count` for cache efficiency metrics
|
|
52
|
+
- Exceptions are automatically recorded on spans with error status
|
|
53
|
+
|
|
4
54
|
4.1.0
|
|
5
55
|
==========
|
|
6
56
|
|
data/README.md
CHANGED
|
@@ -11,6 +11,7 @@ Dalli supports:
|
|
|
11
11
|
* Thread-safe operation (either through use of a connection pool, or by using the Dalli client in threadsafe mode)
|
|
12
12
|
* SSL/TLS connections to memcached
|
|
13
13
|
* SASL authentication
|
|
14
|
+
* OpenTelemetry distributed tracing (automatic when SDK is present)
|
|
14
15
|
|
|
15
16
|
The name is a variant of Salvador Dali for his famous painting [The Persistence of Memory](http://en.wikipedia.org/wiki/The_Persistence_of_Memory).
|
|
16
17
|
|
|
@@ -30,6 +31,31 @@ Dalli supports two protocols for communicating with memcached:
|
|
|
30
31
|
Dalli::Client.new('localhost:11211', protocol: :meta)
|
|
31
32
|
```
|
|
32
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
|
+
|
|
33
59
|
## Security Note
|
|
34
60
|
|
|
35
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:
|
|
@@ -40,6 +66,46 @@ Dalli::Client.new('localhost:11211', serializer: JSON)
|
|
|
40
66
|
|
|
41
67
|
See the [4.0-Upgrade.md](4.0-Upgrade.md) guide for more information.
|
|
42
68
|
|
|
69
|
+
## OpenTelemetry Tracing
|
|
70
|
+
|
|
71
|
+
Dalli automatically instruments operations with [OpenTelemetry](https://opentelemetry.io/) when the SDK is present. No configuration is required - just add the OpenTelemetry gems to your application:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
# Gemfile
|
|
75
|
+
gem 'opentelemetry-sdk'
|
|
76
|
+
gem 'opentelemetry-exporter-otlp' # or your preferred exporter
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
When OpenTelemetry is loaded, Dalli creates spans for:
|
|
80
|
+
- Single key operations: `get`, `set`, `delete`, `add`, `replace`, `incr`, `decr`, etc.
|
|
81
|
+
- Multi-key operations: `get_multi`, `set_multi`, `delete_multi`
|
|
82
|
+
- Advanced operations: `get_with_metadata`, `fetch_with_lock`
|
|
83
|
+
|
|
84
|
+
### Span Attributes
|
|
85
|
+
|
|
86
|
+
All spans include:
|
|
87
|
+
- `db.system`: `memcached`
|
|
88
|
+
- `db.operation`: The operation name (e.g., `get`, `set_multi`)
|
|
89
|
+
|
|
90
|
+
Single-key operations also include:
|
|
91
|
+
- `server.address`: The memcached server that handled the request (e.g., `localhost:11211`)
|
|
92
|
+
|
|
93
|
+
Multi-key operations include cache efficiency metrics:
|
|
94
|
+
- `db.memcached.key_count`: Number of keys in the request
|
|
95
|
+
- `db.memcached.hit_count`: Number of keys found (for `get_multi`)
|
|
96
|
+
- `db.memcached.miss_count`: Number of keys not found (for `get_multi`)
|
|
97
|
+
|
|
98
|
+
### Error Handling
|
|
99
|
+
|
|
100
|
+
Exceptions are automatically recorded on spans with error status. When an operation fails:
|
|
101
|
+
1. The exception is recorded on the span via `span.record_exception(e)`
|
|
102
|
+
2. The span status is set to error with the exception message
|
|
103
|
+
3. The exception is re-raised to the caller
|
|
104
|
+
|
|
105
|
+
### Zero Overhead
|
|
106
|
+
|
|
107
|
+
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.
|
|
108
|
+
|
|
43
109
|

|
|
44
110
|
|
|
45
111
|
|
data/lib/dalli/client.rb
CHANGED
|
@@ -134,8 +134,10 @@ module Dalli
|
|
|
134
134
|
key = key.to_s
|
|
135
135
|
key = @key_manager.validate_key(key)
|
|
136
136
|
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
Instrumentation.trace('get_with_metadata', { 'db.operation' => 'get_with_metadata' }) do
|
|
138
|
+
server = ring.server_for_key(key)
|
|
139
|
+
server.request(:meta_get, key, options)
|
|
140
|
+
end
|
|
139
141
|
rescue NetworkError => e
|
|
140
142
|
Dalli.logger.debug { e.inspect }
|
|
141
143
|
Dalli.logger.debug { 'retrying get_with_metadata with new server' }
|
|
@@ -146,20 +148,19 @@ module Dalli
|
|
|
146
148
|
# Fetch multiple keys efficiently.
|
|
147
149
|
# If a block is given, yields key/value pairs one at a time.
|
|
148
150
|
# Otherwise returns a hash of { 'key' => 'value', 'key2' => 'value1' }
|
|
151
|
+
# rubocop:disable Style/ExplicitBlockArgument
|
|
149
152
|
def get_multi(*keys)
|
|
150
153
|
keys.flatten!
|
|
151
154
|
keys.compact!
|
|
152
|
-
|
|
153
155
|
return {} if keys.empty?
|
|
154
156
|
|
|
155
157
|
if block_given?
|
|
156
|
-
|
|
158
|
+
get_multi_yielding(keys) { |k, v| yield k, v }
|
|
157
159
|
else
|
|
158
|
-
|
|
159
|
-
pipelined_getter.process(keys) { |k, data| hash[k] = data.first }
|
|
160
|
-
end
|
|
160
|
+
get_multi_hash(keys)
|
|
161
161
|
end
|
|
162
162
|
end
|
|
163
|
+
# rubocop:enable Style/ExplicitBlockArgument
|
|
163
164
|
|
|
164
165
|
##
|
|
165
166
|
# Fetch multiple keys efficiently, including available metadata such as CAS.
|
|
@@ -228,7 +229,7 @@ module Dalli
|
|
|
228
229
|
# expensive_operation
|
|
229
230
|
# end
|
|
230
231
|
#
|
|
231
|
-
def fetch_with_lock(key, ttl: nil, lock_ttl: 30, recache_threshold: nil, req_options: nil)
|
|
232
|
+
def fetch_with_lock(key, ttl: nil, lock_ttl: 30, recache_threshold: nil, req_options: nil, &block)
|
|
232
233
|
raise ArgumentError, 'Block is required for fetch_with_lock' unless block_given?
|
|
233
234
|
|
|
234
235
|
raise_unless_meta_protocol!
|
|
@@ -236,21 +237,8 @@ module Dalli
|
|
|
236
237
|
key = key.to_s
|
|
237
238
|
key = @key_manager.validate_key(key)
|
|
238
239
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
vivify_ttl: lock_ttl,
|
|
242
|
-
recache_ttl: recache_threshold
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
if result[:won_recache]
|
|
246
|
-
# This client won the race - regenerate the value
|
|
247
|
-
new_val = yield
|
|
248
|
-
set(key, new_val, ttl_or_default(ttl), req_options)
|
|
249
|
-
new_val
|
|
250
|
-
else
|
|
251
|
-
# Another client is regenerating, or value exists and isn't stale
|
|
252
|
-
# Return the existing value
|
|
253
|
-
result[:value]
|
|
240
|
+
Instrumentation.trace('fetch_with_lock', { 'db.operation' => 'fetch_with_lock' }) do
|
|
241
|
+
fetch_with_lock_request(key, ttl, lock_ttl, recache_threshold, req_options, &block)
|
|
254
242
|
end
|
|
255
243
|
rescue NetworkError => e
|
|
256
244
|
Dalli.logger.debug { e.inspect }
|
|
@@ -330,7 +318,12 @@ module Dalli
|
|
|
330
318
|
def set_multi(hash, ttl = nil, req_options = nil)
|
|
331
319
|
return if hash.empty?
|
|
332
320
|
|
|
333
|
-
|
|
321
|
+
Instrumentation.trace('set_multi', {
|
|
322
|
+
'db.operation' => 'set_multi',
|
|
323
|
+
'db.memcached.key_count' => hash.size
|
|
324
|
+
}) do
|
|
325
|
+
pipelined_setter.process(hash, ttl_or_default(ttl), req_options)
|
|
326
|
+
end
|
|
334
327
|
end
|
|
335
328
|
|
|
336
329
|
##
|
|
@@ -385,7 +378,12 @@ module Dalli
|
|
|
385
378
|
def delete_multi(keys)
|
|
386
379
|
return if keys.empty?
|
|
387
380
|
|
|
388
|
-
|
|
381
|
+
Instrumentation.trace('delete_multi', {
|
|
382
|
+
'db.operation' => 'delete_multi',
|
|
383
|
+
'db.memcached.key_count' => keys.size
|
|
384
|
+
}) do
|
|
385
|
+
pipelined_deleter.process(keys)
|
|
386
|
+
end
|
|
389
387
|
end
|
|
390
388
|
|
|
391
389
|
##
|
|
@@ -517,6 +515,42 @@ module Dalli
|
|
|
517
515
|
|
|
518
516
|
private
|
|
519
517
|
|
|
518
|
+
# Records hit/miss metrics on a span for cache observability.
|
|
519
|
+
# @param span [OpenTelemetry::Trace::Span, nil] the span to record on
|
|
520
|
+
# @param key_count [Integer] total keys requested
|
|
521
|
+
# @param hit_count [Integer] keys found in cache
|
|
522
|
+
def record_hit_miss_metrics(span, key_count, hit_count)
|
|
523
|
+
return unless span
|
|
524
|
+
|
|
525
|
+
span.set_attribute('db.memcached.hit_count', hit_count)
|
|
526
|
+
span.set_attribute('db.memcached.miss_count', key_count - hit_count)
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def get_multi_yielding(keys)
|
|
530
|
+
Instrumentation.trace_with_result('get_multi', get_multi_attributes(keys)) do |span|
|
|
531
|
+
hit_count = 0
|
|
532
|
+
pipelined_getter.process(keys) do |k, data|
|
|
533
|
+
hit_count += 1
|
|
534
|
+
yield k, data.first
|
|
535
|
+
end
|
|
536
|
+
record_hit_miss_metrics(span, keys.size, hit_count)
|
|
537
|
+
nil
|
|
538
|
+
end
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def get_multi_hash(keys)
|
|
542
|
+
Instrumentation.trace_with_result('get_multi', get_multi_attributes(keys)) do |span|
|
|
543
|
+
{}.tap do |hash|
|
|
544
|
+
pipelined_getter.process(keys) { |k, data| hash[k] = data.first }
|
|
545
|
+
record_hit_miss_metrics(span, keys.size, hash.size)
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def get_multi_attributes(keys)
|
|
551
|
+
{ 'db.operation' => 'get_multi', 'db.memcached.key_count' => keys.size }
|
|
552
|
+
end
|
|
553
|
+
|
|
520
554
|
def check_positive!(amt)
|
|
521
555
|
raise ArgumentError, "Positive values only: #{amt}" if amt.negative?
|
|
522
556
|
end
|
|
@@ -529,6 +563,17 @@ module Dalli
|
|
|
529
563
|
perform(:set, key, newvalue, ttl_or_default(ttl), cas, req_options)
|
|
530
564
|
end
|
|
531
565
|
|
|
566
|
+
def fetch_with_lock_request(key, ttl, lock_ttl, recache_threshold, req_options)
|
|
567
|
+
server = ring.server_for_key(key)
|
|
568
|
+
result = server.request(:meta_get, key, { vivify_ttl: lock_ttl, recache_ttl: recache_threshold })
|
|
569
|
+
|
|
570
|
+
return result[:value] unless result[:won_recache]
|
|
571
|
+
|
|
572
|
+
new_val = yield
|
|
573
|
+
set(key, new_val, ttl_or_default(ttl), req_options)
|
|
574
|
+
new_val
|
|
575
|
+
end
|
|
576
|
+
|
|
532
577
|
##
|
|
533
578
|
# Uses the argument TTL or the client-wide default. Ensures
|
|
534
579
|
# that the value is an integer
|
|
@@ -571,7 +616,12 @@ module Dalli
|
|
|
571
616
|
key = @key_manager.validate_key(key)
|
|
572
617
|
|
|
573
618
|
server = ring.server_for_key(key)
|
|
574
|
-
|
|
619
|
+
Instrumentation.trace(op.to_s, {
|
|
620
|
+
'db.operation' => op.to_s,
|
|
621
|
+
'server.address' => server.name
|
|
622
|
+
}) do
|
|
623
|
+
server.request(op, key, *args)
|
|
624
|
+
end
|
|
575
625
|
rescue NetworkError => e
|
|
576
626
|
Dalli.logger.debug { e.inspect }
|
|
577
627
|
Dalli.logger.debug { 'retrying request with new server' }
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dalli
|
|
4
|
+
##
|
|
5
|
+
# Instrumentation support for Dalli. Provides hooks for distributed tracing
|
|
6
|
+
# via OpenTelemetry when the SDK is available.
|
|
7
|
+
#
|
|
8
|
+
# When OpenTelemetry is loaded, Dalli automatically creates spans for cache operations.
|
|
9
|
+
# When OpenTelemetry is not available, all tracing methods are no-ops with zero overhead.
|
|
10
|
+
#
|
|
11
|
+
# == Span Attributes
|
|
12
|
+
#
|
|
13
|
+
# All spans include the following default attributes:
|
|
14
|
+
# - +db.system+ - Always "memcached"
|
|
15
|
+
#
|
|
16
|
+
# Single-key operations (+get+, +set+, +delete+, +incr+, +decr+, etc.) add:
|
|
17
|
+
# - +db.operation+ - The operation name (e.g., "get", "set")
|
|
18
|
+
# - +server.address+ - The memcached server handling the request (e.g., "localhost:11211")
|
|
19
|
+
#
|
|
20
|
+
# Multi-key operations (+get_multi+) add:
|
|
21
|
+
# - +db.operation+ - "get_multi"
|
|
22
|
+
# - +db.memcached.key_count+ - Number of keys requested
|
|
23
|
+
# - +db.memcached.hit_count+ - Number of keys found in cache
|
|
24
|
+
# - +db.memcached.miss_count+ - Number of keys not found
|
|
25
|
+
#
|
|
26
|
+
# Bulk write operations (+set_multi+, +delete_multi+) add:
|
|
27
|
+
# - +db.operation+ - The operation name
|
|
28
|
+
# - +db.memcached.key_count+ - Number of keys in the operation
|
|
29
|
+
#
|
|
30
|
+
# == Error Handling
|
|
31
|
+
#
|
|
32
|
+
# When an exception occurs during a traced operation:
|
|
33
|
+
# - The exception is recorded on the span via +record_exception+
|
|
34
|
+
# - The span status is set to error with the exception message
|
|
35
|
+
# - The exception is re-raised to the caller
|
|
36
|
+
#
|
|
37
|
+
# @example Checking if tracing is enabled
|
|
38
|
+
# Dalli::Instrumentation.enabled? # => true if OpenTelemetry is loaded
|
|
39
|
+
#
|
|
40
|
+
##
|
|
41
|
+
module Instrumentation
|
|
42
|
+
# Default attributes included on all memcached spans.
|
|
43
|
+
# @return [Hash] frozen hash with 'db.system' => 'memcached'
|
|
44
|
+
DEFAULT_ATTRIBUTES = { 'db.system' => 'memcached' }.freeze
|
|
45
|
+
|
|
46
|
+
class << self
|
|
47
|
+
# Returns the OpenTelemetry tracer if available, nil otherwise.
|
|
48
|
+
#
|
|
49
|
+
# The tracer is cached after first lookup for performance.
|
|
50
|
+
# Uses the library name 'dalli' and current Dalli::VERSION.
|
|
51
|
+
#
|
|
52
|
+
# @return [OpenTelemetry::Trace::Tracer, nil] the tracer or nil if OTel unavailable
|
|
53
|
+
def tracer
|
|
54
|
+
return @tracer if defined?(@tracer)
|
|
55
|
+
|
|
56
|
+
@tracer = (OpenTelemetry.tracer_provider.tracer('dalli', Dalli::VERSION) if defined?(OpenTelemetry))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Returns true if instrumentation is enabled (OpenTelemetry SDK is available).
|
|
60
|
+
#
|
|
61
|
+
# @return [Boolean] true if tracing is active, false otherwise
|
|
62
|
+
def enabled?
|
|
63
|
+
!tracer.nil?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Wraps a block with a span if instrumentation is enabled.
|
|
67
|
+
#
|
|
68
|
+
# Creates a client span with the given name and attributes merged with
|
|
69
|
+
# DEFAULT_ATTRIBUTES. The block is executed within the span context.
|
|
70
|
+
# If an exception occurs, it is recorded on the span before re-raising.
|
|
71
|
+
#
|
|
72
|
+
# When tracing is disabled (OpenTelemetry not loaded), this method
|
|
73
|
+
# simply yields directly with zero overhead.
|
|
74
|
+
#
|
|
75
|
+
# @param name [String] the span name (e.g., 'get', 'set', 'delete')
|
|
76
|
+
# @param attributes [Hash] span attributes to merge with defaults.
|
|
77
|
+
# Common attributes include:
|
|
78
|
+
# - 'db.operation' - the operation name
|
|
79
|
+
# - 'server.address' - the target server
|
|
80
|
+
# - 'db.memcached.key_count' - number of keys (for multi operations)
|
|
81
|
+
# @yield the cache operation to trace
|
|
82
|
+
# @return [Object] the result of the block
|
|
83
|
+
# @raise [StandardError] re-raises any exception from the block
|
|
84
|
+
#
|
|
85
|
+
# @example Tracing a set operation
|
|
86
|
+
# trace('set', { 'db.operation' => 'set', 'server.address' => 'localhost:11211' }) do
|
|
87
|
+
# server.set(key, value, ttl)
|
|
88
|
+
# end
|
|
89
|
+
#
|
|
90
|
+
def trace(name, attributes = {})
|
|
91
|
+
return yield unless enabled?
|
|
92
|
+
|
|
93
|
+
tracer.in_span(name, attributes: DEFAULT_ATTRIBUTES.merge(attributes), kind: :client) do |span|
|
|
94
|
+
yield
|
|
95
|
+
rescue StandardError => e
|
|
96
|
+
span.record_exception(e)
|
|
97
|
+
span.status = OpenTelemetry::Trace::Status.error(e.message)
|
|
98
|
+
raise
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Like trace, but yields the span to allow adding attributes after execution.
|
|
103
|
+
#
|
|
104
|
+
# This is useful for operations where metrics are only known after the
|
|
105
|
+
# operation completes, such as get_multi where hit/miss counts depend
|
|
106
|
+
# on the cache response.
|
|
107
|
+
#
|
|
108
|
+
# When tracing is disabled, yields nil as the span argument.
|
|
109
|
+
#
|
|
110
|
+
# @param name [String] the span name (e.g., 'get_multi')
|
|
111
|
+
# @param attributes [Hash] initial span attributes to merge with defaults
|
|
112
|
+
# @yield [OpenTelemetry::Trace::Span, nil] the span object, or nil if disabled
|
|
113
|
+
# @return [Object] the result of the block
|
|
114
|
+
# @raise [StandardError] re-raises any exception from the block
|
|
115
|
+
#
|
|
116
|
+
# @example Recording hit/miss metrics after get_multi
|
|
117
|
+
# trace_with_result('get_multi', { 'db.operation' => 'get_multi' }) do |span|
|
|
118
|
+
# results = fetch_from_cache(keys)
|
|
119
|
+
# if span
|
|
120
|
+
# span.set_attribute('db.memcached.hit_count', results.size)
|
|
121
|
+
# span.set_attribute('db.memcached.miss_count', keys.size - results.size)
|
|
122
|
+
# end
|
|
123
|
+
# results
|
|
124
|
+
# end
|
|
125
|
+
#
|
|
126
|
+
def trace_with_result(name, attributes = {})
|
|
127
|
+
return yield(nil) unless enabled?
|
|
128
|
+
|
|
129
|
+
tracer.in_span(name, attributes: DEFAULT_ATTRIBUTES.merge(attributes), kind: :client) do |span|
|
|
130
|
+
yield(span)
|
|
131
|
+
rescue StandardError => e
|
|
132
|
+
span.record_exception(e)
|
|
133
|
+
span.status = OpenTelemetry::Trace::Status.error(e.message)
|
|
134
|
+
raise
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
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
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
3
5
|
module Dalli
|
|
4
6
|
##
|
|
5
7
|
# Contains logic for the pipelined gets implemented by the client.
|
|
6
8
|
##
|
|
7
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
|
+
|
|
8
17
|
def initialize(ring, key_manager)
|
|
9
18
|
@ring = ring
|
|
10
19
|
@key_manager = key_manager
|
|
@@ -17,8 +26,14 @@ module Dalli
|
|
|
17
26
|
return {} if keys.empty?
|
|
18
27
|
|
|
19
28
|
@ring.lock do
|
|
29
|
+
# Stores partial results collected during interleaved send phase
|
|
30
|
+
@partial_results = {}
|
|
20
31
|
servers = setup_requests(keys)
|
|
21
32
|
start_time = Time.now
|
|
33
|
+
|
|
34
|
+
# First yield any partial results collected during interleaved send
|
|
35
|
+
yield_partial_results(&block)
|
|
36
|
+
|
|
22
37
|
servers = fetch_responses(servers, start_time, @ring.socket_timeout, &block) until servers.empty?
|
|
23
38
|
end
|
|
24
39
|
rescue NetworkError => e
|
|
@@ -27,6 +42,15 @@ module Dalli
|
|
|
27
42
|
retry
|
|
28
43
|
end
|
|
29
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
|
+
|
|
30
54
|
def setup_requests(keys)
|
|
31
55
|
groups = groups_for_keys(keys)
|
|
32
56
|
make_getkq_requests(groups)
|
|
@@ -45,7 +69,14 @@ module Dalli
|
|
|
45
69
|
##
|
|
46
70
|
def make_getkq_requests(groups)
|
|
47
71
|
groups.each do |server, keys_for_server|
|
|
48
|
-
|
|
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
|
|
49
80
|
rescue DalliError, NetworkError => e
|
|
50
81
|
Dalli.logger.debug { e.inspect }
|
|
51
82
|
Dalli.logger.debug { "unable to get keys for server #{server.name}" }
|
|
@@ -57,7 +88,7 @@ module Dalli
|
|
|
57
88
|
# our set, sending the noop to terminate the set of queries.
|
|
58
89
|
##
|
|
59
90
|
def finish_queries(servers)
|
|
60
|
-
deleted =
|
|
91
|
+
deleted = Set.new
|
|
61
92
|
|
|
62
93
|
servers.each do |server|
|
|
63
94
|
next unless server.connected?
|
|
@@ -67,7 +98,7 @@ module Dalli
|
|
|
67
98
|
rescue Dalli::NetworkError
|
|
68
99
|
raise
|
|
69
100
|
rescue Dalli::DalliError
|
|
70
|
-
deleted
|
|
101
|
+
deleted << server
|
|
71
102
|
end
|
|
72
103
|
end
|
|
73
104
|
|
|
@@ -94,7 +125,7 @@ module Dalli
|
|
|
94
125
|
|
|
95
126
|
def fetch_responses(servers, start_time, timeout, &block)
|
|
96
127
|
# Remove any servers which are not connected
|
|
97
|
-
servers.
|
|
128
|
+
servers.select!(&:connected?)
|
|
98
129
|
return [] if servers.empty?
|
|
99
130
|
|
|
100
131
|
time_left = remaining_time(start_time, timeout)
|
data/lib/dalli/protocol/base.rb
CHANGED
|
@@ -23,10 +23,17 @@ module Dalli
|
|
|
23
23
|
def initialize(attribs, client_options = {})
|
|
24
24
|
hostname, port, socket_type, @weight, user_creds = ServerConfigParser.parse(attribs)
|
|
25
25
|
@options = client_options.merge(user_creds)
|
|
26
|
-
@
|
|
26
|
+
@raw_mode = client_options[:raw]
|
|
27
|
+
@value_marshaller = @raw_mode ? StringMarshaller.new(@options) : ValueMarshaller.new(@options)
|
|
27
28
|
@connection_manager = ConnectionManager.new(hostname, port, socket_type, @options)
|
|
28
29
|
end
|
|
29
30
|
|
|
31
|
+
# Returns true if client is in raw mode (no serialization/compression).
|
|
32
|
+
# In raw mode, we can skip requesting bitflags from the server.
|
|
33
|
+
def raw_mode?
|
|
34
|
+
@raw_mode
|
|
35
|
+
end
|
|
36
|
+
|
|
30
37
|
# Chokepoint method for error handling and ensuring liveness
|
|
31
38
|
def request(opkey, *args)
|
|
32
39
|
verify_state(opkey)
|
|
@@ -35,8 +42,8 @@ module Dalli
|
|
|
35
42
|
@connection_manager.start_request!
|
|
36
43
|
response = send(opkey, *args)
|
|
37
44
|
|
|
38
|
-
# pipelined_get emit query but
|
|
39
|
-
@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)
|
|
40
47
|
|
|
41
48
|
response
|
|
42
49
|
rescue Dalli::MarshalError => e
|
|
@@ -74,7 +81,9 @@ module Dalli
|
|
|
74
81
|
def pipeline_response_setup
|
|
75
82
|
verify_pipelined_state(:getkq)
|
|
76
83
|
write_noop
|
|
77
|
-
|
|
84
|
+
# Use ensure_ready instead of reset to preserve any data already buffered
|
|
85
|
+
# during interleaved pipelined get draining
|
|
86
|
+
response_buffer.ensure_ready
|
|
78
87
|
end
|
|
79
88
|
|
|
80
89
|
# Attempt to receive and parse as many key/value pairs as possible
|
|
@@ -213,6 +222,11 @@ module Dalli
|
|
|
213
222
|
end
|
|
214
223
|
|
|
215
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
|
+
|
|
216
230
|
req = +''
|
|
217
231
|
keys.each do |key|
|
|
218
232
|
req << quiet_get_request(key)
|
|
@@ -221,6 +235,51 @@ module Dalli
|
|
|
221
235
|
write(req)
|
|
222
236
|
end
|
|
223
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
|
+
|
|
224
283
|
def response_buffer
|
|
225
284
|
@response_buffer ||= ResponseBuffer.new(@connection_manager, response_processor)
|
|
226
285
|
end
|
|
@@ -22,6 +22,7 @@ module Dalli
|
|
|
22
22
|
def get(key, options = nil)
|
|
23
23
|
req = RequestFormatter.standard_request(opkey: :get, key: key)
|
|
24
24
|
write(req)
|
|
25
|
+
@connection_manager.flush
|
|
25
26
|
response_processor.get(cache_nils: cache_nils?(options))
|
|
26
27
|
end
|
|
27
28
|
|
|
@@ -33,12 +34,14 @@ module Dalli
|
|
|
33
34
|
ttl = TtlSanitizer.sanitize(ttl)
|
|
34
35
|
req = RequestFormatter.standard_request(opkey: :gat, key: key, ttl: ttl)
|
|
35
36
|
write(req)
|
|
37
|
+
@connection_manager.flush
|
|
36
38
|
response_processor.get(cache_nils: cache_nils?(options))
|
|
37
39
|
end
|
|
38
40
|
|
|
39
41
|
def touch(key, ttl)
|
|
40
42
|
ttl = TtlSanitizer.sanitize(ttl)
|
|
41
43
|
write(RequestFormatter.standard_request(opkey: :touch, key: key, ttl: ttl))
|
|
44
|
+
@connection_manager.flush
|
|
42
45
|
response_processor.generic_response
|
|
43
46
|
end
|
|
44
47
|
|
|
@@ -47,6 +50,7 @@ module Dalli
|
|
|
47
50
|
def cas(key)
|
|
48
51
|
req = RequestFormatter.standard_request(opkey: :get, key: key)
|
|
49
52
|
write(req)
|
|
53
|
+
@connection_manager.flush
|
|
50
54
|
response_processor.data_cas_response
|
|
51
55
|
end
|
|
52
56
|
|
|
@@ -81,6 +85,7 @@ module Dalli
|
|
|
81
85
|
value: value, bitflags: bitflags,
|
|
82
86
|
ttl: ttl, cas: cas)
|
|
83
87
|
write(req)
|
|
88
|
+
@connection_manager.flush unless quiet?
|
|
84
89
|
response_processor.storage_response unless quiet?
|
|
85
90
|
end
|
|
86
91
|
# rubocop:enable Metrics/ParameterLists
|
|
@@ -97,6 +102,7 @@ module Dalli
|
|
|
97
102
|
|
|
98
103
|
def write_append_prepend(opkey, key, value)
|
|
99
104
|
write(RequestFormatter.standard_request(opkey: opkey, key: key, value: value))
|
|
105
|
+
@connection_manager.flush unless quiet?
|
|
100
106
|
response_processor.no_body_response unless quiet?
|
|
101
107
|
end
|
|
102
108
|
|
|
@@ -105,6 +111,7 @@ module Dalli
|
|
|
105
111
|
opkey = quiet? ? :deleteq : :delete
|
|
106
112
|
req = RequestFormatter.standard_request(opkey: opkey, key: key, cas: cas)
|
|
107
113
|
write(req)
|
|
114
|
+
@connection_manager.flush unless quiet?
|
|
108
115
|
response_processor.delete unless quiet?
|
|
109
116
|
end
|
|
110
117
|
|
|
@@ -139,6 +146,7 @@ module Dalli
|
|
|
139
146
|
initial ||= 0
|
|
140
147
|
write(RequestFormatter.decr_incr_request(opkey: opkey, key: key,
|
|
141
148
|
count: count, initial: initial, expiry: expiry))
|
|
149
|
+
@connection_manager.flush unless quiet?
|
|
142
150
|
response_processor.decr_incr unless quiet?
|
|
143
151
|
end
|
|
144
152
|
|
|
@@ -146,6 +154,7 @@ module Dalli
|
|
|
146
154
|
def flush(ttl = 0)
|
|
147
155
|
opkey = quiet? ? :flushq : :flush
|
|
148
156
|
write(RequestFormatter.standard_request(opkey: opkey, ttl: ttl))
|
|
157
|
+
@connection_manager.flush unless quiet?
|
|
149
158
|
response_processor.no_body_response unless quiet?
|
|
150
159
|
end
|
|
151
160
|
|
|
@@ -159,22 +168,26 @@ module Dalli
|
|
|
159
168
|
def stats(info = '')
|
|
160
169
|
req = RequestFormatter.standard_request(opkey: :stat, key: info)
|
|
161
170
|
write(req)
|
|
171
|
+
@connection_manager.flush
|
|
162
172
|
response_processor.stats
|
|
163
173
|
end
|
|
164
174
|
|
|
165
175
|
def reset_stats
|
|
166
176
|
write(RequestFormatter.standard_request(opkey: :stat, key: 'reset'))
|
|
177
|
+
@connection_manager.flush
|
|
167
178
|
response_processor.reset
|
|
168
179
|
end
|
|
169
180
|
|
|
170
181
|
def version
|
|
171
182
|
write(RequestFormatter.standard_request(opkey: :version))
|
|
183
|
+
@connection_manager.flush
|
|
172
184
|
response_processor.version
|
|
173
185
|
end
|
|
174
186
|
|
|
175
187
|
def write_noop
|
|
176
188
|
req = RequestFormatter.standard_request(opkey: :noop)
|
|
177
189
|
write(req)
|
|
190
|
+
@connection_manager.flush
|
|
178
191
|
end
|
|
179
192
|
|
|
180
193
|
require_relative 'binary/request_formatter'
|
|
@@ -53,6 +53,7 @@ module Dalli
|
|
|
53
53
|
Dalli.logger.debug { "Dalli::Server#connect #{name}" }
|
|
54
54
|
|
|
55
55
|
@sock = memcached_socket
|
|
56
|
+
@sock.sync = false # Enable buffered I/O for better performance
|
|
56
57
|
@pid = PIDCache.pid
|
|
57
58
|
@request_in_progress = false
|
|
58
59
|
rescue SystemCallError, *TIMEOUT_ERRORS, EOFError, SocketError => e
|
|
@@ -166,6 +167,12 @@ module Dalli
|
|
|
166
167
|
error_on_request!(e)
|
|
167
168
|
end
|
|
168
169
|
|
|
170
|
+
def flush
|
|
171
|
+
@sock.flush
|
|
172
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS => e
|
|
173
|
+
error_on_request!(e)
|
|
174
|
+
end
|
|
175
|
+
|
|
169
176
|
# Non-blocking read. Here to support the operation
|
|
170
177
|
# of the get_multi operation
|
|
171
178
|
def read_nonblock
|
|
@@ -37,9 +37,12 @@ module Dalli
|
|
|
37
37
|
# - l<N>: Seconds since last access
|
|
38
38
|
def self.meta_get(key:, value: true, return_cas: false, ttl: nil, base64: false, quiet: false,
|
|
39
39
|
vivify_ttl: nil, recache_ttl: nil,
|
|
40
|
-
return_hit_status: false, return_last_access: false, skip_lru_bump: false
|
|
40
|
+
return_hit_status: false, return_last_access: false, skip_lru_bump: false,
|
|
41
|
+
skip_flags: false)
|
|
41
42
|
cmd = "mg #{key}"
|
|
42
|
-
|
|
43
|
+
# In raw mode (skip_flags: true), we don't request bitflags since they're not used.
|
|
44
|
+
# This saves 2 bytes per request and skips parsing on response.
|
|
45
|
+
cmd << (skip_flags ? ' v' : ' v f') if value
|
|
43
46
|
cmd << ' c' if return_cas
|
|
44
47
|
cmd << ' b' if base64
|
|
45
48
|
cmd << " T#{ttl}" if ttl
|
data/lib/dalli/protocol/meta.rb
CHANGED
|
@@ -25,21 +25,28 @@ module Dalli
|
|
|
25
25
|
# Retrieval Commands
|
|
26
26
|
def get(key, options = nil)
|
|
27
27
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
28
|
-
|
|
28
|
+
# Skip bitflags in raw mode - saves 2 bytes per request and skips parsing
|
|
29
|
+
skip_flags = raw_mode? || (options && options[:raw])
|
|
30
|
+
req = RequestFormatter.meta_get(key: encoded_key, base64: base64, skip_flags: skip_flags)
|
|
29
31
|
write(req)
|
|
32
|
+
@connection_manager.flush
|
|
30
33
|
response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
|
|
31
34
|
end
|
|
32
35
|
|
|
33
36
|
def quiet_get_request(key)
|
|
34
37
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
35
|
-
|
|
38
|
+
# Skip bitflags in raw mode - saves 2 bytes per request and skips parsing
|
|
39
|
+
RequestFormatter.meta_get(key: encoded_key, return_cas: true, base64: base64, quiet: true,
|
|
40
|
+
skip_flags: raw_mode?)
|
|
36
41
|
end
|
|
37
42
|
|
|
38
43
|
def gat(key, ttl, options = nil)
|
|
39
44
|
ttl = TtlSanitizer.sanitize(ttl)
|
|
40
45
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
41
|
-
|
|
46
|
+
skip_flags = raw_mode? || (options && options[:raw])
|
|
47
|
+
req = RequestFormatter.meta_get(key: encoded_key, ttl: ttl, base64: base64, skip_flags: skip_flags)
|
|
42
48
|
write(req)
|
|
49
|
+
@connection_manager.flush
|
|
43
50
|
response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
|
|
44
51
|
end
|
|
45
52
|
|
|
@@ -48,6 +55,7 @@ module Dalli
|
|
|
48
55
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
49
56
|
req = RequestFormatter.meta_get(key: encoded_key, ttl: ttl, value: false, base64: base64)
|
|
50
57
|
write(req)
|
|
58
|
+
@connection_manager.flush
|
|
51
59
|
response_processor.meta_get_without_value
|
|
52
60
|
end
|
|
53
61
|
|
|
@@ -57,6 +65,7 @@ module Dalli
|
|
|
57
65
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
58
66
|
req = RequestFormatter.meta_get(key: encoded_key, value: true, return_cas: true, base64: base64)
|
|
59
67
|
write(req)
|
|
68
|
+
@connection_manager.flush
|
|
60
69
|
response_processor.meta_get_with_value_and_cas
|
|
61
70
|
end
|
|
62
71
|
|
|
@@ -93,6 +102,7 @@ module Dalli
|
|
|
93
102
|
return_last_access: options[:return_last_access], skip_lru_bump: options[:skip_lru_bump]
|
|
94
103
|
)
|
|
95
104
|
write(req)
|
|
105
|
+
@connection_manager.flush
|
|
96
106
|
response_processor.meta_get_with_metadata(
|
|
97
107
|
cache_nils: cache_nils?(options), return_hit_status: options[:return_hit_status],
|
|
98
108
|
return_last_access: options[:return_last_access]
|
|
@@ -110,6 +120,7 @@ module Dalli
|
|
|
110
120
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
111
121
|
req = RequestFormatter.meta_delete(key: encoded_key, cas: cas, base64: base64, stale: true)
|
|
112
122
|
write(req)
|
|
123
|
+
@connection_manager.flush
|
|
113
124
|
response_processor.meta_delete
|
|
114
125
|
end
|
|
115
126
|
|
|
@@ -146,6 +157,7 @@ module Dalli
|
|
|
146
157
|
write(req)
|
|
147
158
|
write(value)
|
|
148
159
|
write(TERMINATOR)
|
|
160
|
+
@connection_manager.flush unless quiet
|
|
149
161
|
end
|
|
150
162
|
# rubocop:enable Metrics/ParameterLists
|
|
151
163
|
|
|
@@ -168,6 +180,7 @@ module Dalli
|
|
|
168
180
|
write(req)
|
|
169
181
|
write(value)
|
|
170
182
|
write(TERMINATOR)
|
|
183
|
+
@connection_manager.flush unless quiet?
|
|
171
184
|
end
|
|
172
185
|
# rubocop:enable Metrics/ParameterLists
|
|
173
186
|
|
|
@@ -177,6 +190,7 @@ module Dalli
|
|
|
177
190
|
req = RequestFormatter.meta_delete(key: encoded_key, cas: cas,
|
|
178
191
|
base64: base64, quiet: quiet?)
|
|
179
192
|
write(req)
|
|
193
|
+
@connection_manager.flush unless quiet?
|
|
180
194
|
response_processor.meta_delete unless quiet?
|
|
181
195
|
end
|
|
182
196
|
|
|
@@ -202,12 +216,14 @@ module Dalli
|
|
|
202
216
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
203
217
|
write(RequestFormatter.meta_arithmetic(key: encoded_key, delta: delta, initial: initial, incr: incr, ttl: ttl,
|
|
204
218
|
quiet: quiet?, base64: base64))
|
|
219
|
+
@connection_manager.flush unless quiet?
|
|
205
220
|
response_processor.decr_incr unless quiet?
|
|
206
221
|
end
|
|
207
222
|
|
|
208
223
|
# Other Commands
|
|
209
224
|
def flush(delay = 0)
|
|
210
225
|
write(RequestFormatter.flush(delay: delay))
|
|
226
|
+
@connection_manager.flush unless quiet?
|
|
211
227
|
response_processor.flush unless quiet?
|
|
212
228
|
end
|
|
213
229
|
|
|
@@ -220,21 +236,25 @@ module Dalli
|
|
|
220
236
|
|
|
221
237
|
def stats(info = nil)
|
|
222
238
|
write(RequestFormatter.stats(info))
|
|
239
|
+
@connection_manager.flush
|
|
223
240
|
response_processor.stats
|
|
224
241
|
end
|
|
225
242
|
|
|
226
243
|
def reset_stats
|
|
227
244
|
write(RequestFormatter.stats('reset'))
|
|
245
|
+
@connection_manager.flush
|
|
228
246
|
response_processor.reset
|
|
229
247
|
end
|
|
230
248
|
|
|
231
249
|
def version
|
|
232
250
|
write(RequestFormatter.version)
|
|
251
|
+
@connection_manager.flush
|
|
233
252
|
response_processor.version
|
|
234
253
|
end
|
|
235
254
|
|
|
236
255
|
def write_noop
|
|
237
256
|
write(RequestFormatter.meta_noop)
|
|
257
|
+
@connection_manager.flush
|
|
238
258
|
end
|
|
239
259
|
|
|
240
260
|
def authenticate_connection
|
|
@@ -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
|
@@ -107,7 +107,7 @@ module Dalli
|
|
|
107
107
|
# aliases TCPSocket#initialize method to #original_resolv_initialize.
|
|
108
108
|
# https://github.com/ruby/resolv-replace/blob/v0.1.1/lib/resolv-replace.rb#L21
|
|
109
109
|
if RUBY_VERSION >= '3.0' &&
|
|
110
|
-
!::TCPSocket.
|
|
110
|
+
!::TCPSocket.private_method_defined?(:original_resolv_initialize)
|
|
111
111
|
sock = new(host, port, connect_timeout: options[:socket_timeout])
|
|
112
112
|
yield(sock)
|
|
113
113
|
else
|
|
@@ -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
data/lib/dalli.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
|
|
@@ -40,6 +40,7 @@ files:
|
|
|
40
40
|
- lib/dalli/cas/client.rb
|
|
41
41
|
- lib/dalli/client.rb
|
|
42
42
|
- lib/dalli/compressor.rb
|
|
43
|
+
- lib/dalli/instrumentation.rb
|
|
43
44
|
- lib/dalli/key_manager.rb
|
|
44
45
|
- lib/dalli/options.rb
|
|
45
46
|
- lib/dalli/pid_cache.rb
|