dalli 4.1.0 → 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e342d39d58d552607783486a9c9f0ffa23d9c479079c62cb760ec84a97d064da
4
- data.tar.gz: 1ae923ede204d0a3e82426404cfb4882f4414a12cb351cf3d04b9ef8653051ce
3
+ metadata.gz: 533bed149f02ca2960fa436d776b920cce0c95b841296c678a247141f8f7f239
4
+ data.tar.gz: cd3ca6a066d005fd177726b3c29ab460cba9d8ddc9937c1809053bf22b20e387
5
5
  SHA512:
6
- metadata.gz: aeb1bec84092f4e07db884b415d6b25375dc995fff04cc58a1764d9ad0937986ba7ff5db5a665a831bd8ab488b3fa4868fce79834e93a26848bc37ac1c9f9855
7
- data.tar.gz: 8c5a4e21f4b0a86279bf28edc729c0a1981964438342117f3bc180d15d5cdfc93e395913267db7af66b3907416051be7629e687708c163f4d4015d4e2a768508
6
+ metadata.gz: cc927b7e0f8906d326ede287b9d0fb9b3c1fb216b271350bc1e02cd4531b86d365e286da73cb8be9082646d747a494ccc0cdd81d0f9f053cf2439dd58bc40157
7
+ data.tar.gz: ce17e49264f9cf1550f709eb6d67a3f0f2f7fefaecfab04bf2c3f4356b7636ff95e2d6f76a44abcd8f0efae79ab8bbf21c5ba7d59a802735626af73e7bf0d777
data/CHANGELOG.md CHANGED
@@ -1,6 +1,41 @@
1
1
  Dalli Changelog
2
2
  =====================
3
3
 
4
+ 4.2.1
5
+ ==========
6
+
7
+ OpenTelemetry:
8
+
9
+ - Migrate to stable OTel semantic conventions
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
18
+
19
+ 4.2.0
20
+ ==========
21
+
22
+ Performance:
23
+
24
+ - Buffered I/O: Use `socket.sync = false` with explicit flush to reduce syscalls for pipelined operations
25
+ - get_multi optimizations: Use Set for O(1) server tracking lookups
26
+ - Raw mode optimization: Skip bitflags request in meta protocol when in raw mode (saves 2 bytes per request)
27
+
28
+ New Features:
29
+
30
+ - OpenTelemetry tracing support: Automatically instruments operations when OpenTelemetry SDK is present
31
+ - Zero overhead when OpenTelemetry is not loaded
32
+ - Traces `get`, `set`, `delete`, `get_multi`, `set_multi`, `delete_multi`, `get_with_metadata`, and `fetch_with_lock`
33
+ - Spans include `db.system: memcached` and `db.operation` attributes
34
+ - Single-key operations include `server.address` attribute
35
+ - Multi-key operations include `db.memcached.key_count` attribute
36
+ - `get_multi` spans include `db.memcached.hit_count` and `db.memcached.miss_count` for cache efficiency metrics
37
+ - Exceptions are automatically recorded on spans with error status
38
+
4
39
  4.1.0
5
40
  ==========
6
41
 
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
 
@@ -40,6 +41,46 @@ Dalli::Client.new('localhost:11211', serializer: JSON)
40
41
 
41
42
  See the [4.0-Upgrade.md](4.0-Upgrade.md) guide for more information.
42
43
 
44
+ ## OpenTelemetry Tracing
45
+
46
+ 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:
47
+
48
+ ```ruby
49
+ # Gemfile
50
+ gem 'opentelemetry-sdk'
51
+ gem 'opentelemetry-exporter-otlp' # or your preferred exporter
52
+ ```
53
+
54
+ When OpenTelemetry is loaded, Dalli creates spans for:
55
+ - Single key operations: `get`, `set`, `delete`, `add`, `replace`, `incr`, `decr`, etc.
56
+ - Multi-key operations: `get_multi`, `set_multi`, `delete_multi`
57
+ - Advanced operations: `get_with_metadata`, `fetch_with_lock`
58
+
59
+ ### Span Attributes
60
+
61
+ All spans include:
62
+ - `db.system`: `memcached`
63
+ - `db.operation`: The operation name (e.g., `get`, `set_multi`)
64
+
65
+ Single-key operations also include:
66
+ - `server.address`: The memcached server that handled the request (e.g., `localhost:11211`)
67
+
68
+ Multi-key operations include cache efficiency metrics:
69
+ - `db.memcached.key_count`: Number of keys in the request
70
+ - `db.memcached.hit_count`: Number of keys found (for `get_multi`)
71
+ - `db.memcached.miss_count`: Number of keys not found (for `get_multi`)
72
+
73
+ ### Error Handling
74
+
75
+ Exceptions are automatically recorded on spans with error status. When an operation fails:
76
+ 1. The exception is recorded on the span via `span.record_exception(e)`
77
+ 2. The span status is set to error with the exception message
78
+ 3. The exception is re-raised to the caller
79
+
80
+ ### Zero Overhead
81
+
82
+ 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.
83
+
43
84
  ![Persistence of Memory](https://upload.wikimedia.org/wikipedia/en/d/dd/The_Persistence_of_Memory.jpg)
44
85
 
45
86
 
data/lib/dalli/client.rb CHANGED
@@ -50,6 +50,10 @@ 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.
53
57
  #
54
58
  def initialize(servers = nil, options = {})
55
59
  @normalized_servers = ::Dalli::ServersArgNormalizer.normalize_servers(servers)
@@ -135,7 +139,9 @@ module Dalli
135
139
  key = @key_manager.validate_key(key)
136
140
 
137
141
  server = ring.server_for_key(key)
138
- server.request(:meta_get, key, options)
142
+ Instrumentation.trace('get_with_metadata', trace_attrs('get_with_metadata', key, server)) do
143
+ server.request(:meta_get, key, options)
144
+ end
139
145
  rescue NetworkError => e
140
146
  Dalli.logger.debug { e.inspect }
141
147
  Dalli.logger.debug { 'retrying get_with_metadata with new server' }
@@ -146,20 +152,19 @@ module Dalli
146
152
  # Fetch multiple keys efficiently.
147
153
  # If a block is given, yields key/value pairs one at a time.
148
154
  # Otherwise returns a hash of { 'key' => 'value', 'key2' => 'value1' }
155
+ # rubocop:disable Style/ExplicitBlockArgument
149
156
  def get_multi(*keys)
150
157
  keys.flatten!
151
158
  keys.compact!
152
-
153
159
  return {} if keys.empty?
154
160
 
155
161
  if block_given?
156
- pipelined_getter.process(keys) { |k, data| yield k, data.first }
162
+ get_multi_yielding(keys) { |k, v| yield k, v }
157
163
  else
158
- {}.tap do |hash|
159
- pipelined_getter.process(keys) { |k, data| hash[k] = data.first }
160
- end
164
+ get_multi_hash(keys)
161
165
  end
162
166
  end
167
+ # rubocop:enable Style/ExplicitBlockArgument
163
168
 
164
169
  ##
165
170
  # Fetch multiple keys efficiently, including available metadata such as CAS.
@@ -228,7 +233,7 @@ module Dalli
228
233
  # expensive_operation
229
234
  # end
230
235
  #
231
- def fetch_with_lock(key, ttl: nil, lock_ttl: 30, recache_threshold: nil, req_options: nil)
236
+ def fetch_with_lock(key, ttl: nil, lock_ttl: 30, recache_threshold: nil, req_options: nil, &block)
232
237
  raise ArgumentError, 'Block is required for fetch_with_lock' unless block_given?
233
238
 
234
239
  raise_unless_meta_protocol!
@@ -237,20 +242,8 @@ module Dalli
237
242
  key = @key_manager.validate_key(key)
238
243
 
239
244
  server = ring.server_for_key(key)
240
- result = server.request(:meta_get, key, {
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]
245
+ Instrumentation.trace('fetch_with_lock', trace_attrs('fetch_with_lock', key, server)) do
246
+ fetch_with_lock_request(key, ttl, lock_ttl, recache_threshold, req_options, &block)
254
247
  end
255
248
  rescue NetworkError => e
256
249
  Dalli.logger.debug { e.inspect }
@@ -330,7 +323,9 @@ module Dalli
330
323
  def set_multi(hash, ttl = nil, req_options = nil)
331
324
  return if hash.empty?
332
325
 
333
- pipelined_setter.process(hash, ttl_or_default(ttl), req_options)
326
+ Instrumentation.trace('set_multi', multi_trace_attrs('set_multi', hash.size, hash.keys)) do
327
+ pipelined_setter.process(hash, ttl_or_default(ttl), req_options)
328
+ end
334
329
  end
335
330
 
336
331
  ##
@@ -385,7 +380,9 @@ module Dalli
385
380
  def delete_multi(keys)
386
381
  return if keys.empty?
387
382
 
388
- pipelined_deleter.process(keys)
383
+ Instrumentation.trace('delete_multi', multi_trace_attrs('delete_multi', keys.size, keys)) do
384
+ pipelined_deleter.process(keys)
385
+ end
389
386
  end
390
387
 
391
388
  ##
@@ -517,6 +514,65 @@ module Dalli
517
514
 
518
515
  private
519
516
 
517
+ # Records hit/miss metrics on a span for cache observability.
518
+ # @param span [OpenTelemetry::Trace::Span, nil] the span to record on
519
+ # @param key_count [Integer] total keys requested
520
+ # @param hit_count [Integer] keys found in cache
521
+ def record_hit_miss_metrics(span, key_count, hit_count)
522
+ return unless span
523
+
524
+ span.set_attribute('db.memcached.hit_count', hit_count)
525
+ span.set_attribute('db.memcached.miss_count', key_count - hit_count)
526
+ end
527
+
528
+ def get_multi_yielding(keys)
529
+ Instrumentation.trace_with_result('get_multi', get_multi_attributes(keys)) do |span|
530
+ hit_count = 0
531
+ pipelined_getter.process(keys) do |k, data|
532
+ hit_count += 1
533
+ yield k, data.first
534
+ end
535
+ record_hit_miss_metrics(span, keys.size, hit_count)
536
+ nil
537
+ end
538
+ end
539
+
540
+ def get_multi_hash(keys)
541
+ Instrumentation.trace_with_result('get_multi', get_multi_attributes(keys)) do |span|
542
+ {}.tap do |hash|
543
+ pipelined_getter.process(keys) { |k, data| hash[k] = data.first }
544
+ record_hit_miss_metrics(span, keys.size, hash.size)
545
+ end
546
+ end
547
+ end
548
+
549
+ def get_multi_attributes(keys)
550
+ multi_trace_attrs('get_multi', keys.size, keys)
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
574
+ end
575
+
520
576
  def check_positive!(amt)
521
577
  raise ArgumentError, "Positive values only: #{amt}" if amt.negative?
522
578
  end
@@ -529,6 +585,17 @@ module Dalli
529
585
  perform(:set, key, newvalue, ttl_or_default(ttl), cas, req_options)
530
586
  end
531
587
 
588
+ def fetch_with_lock_request(key, ttl, lock_ttl, recache_threshold, req_options)
589
+ server = ring.server_for_key(key)
590
+ result = server.request(:meta_get, key, { vivify_ttl: lock_ttl, recache_ttl: recache_threshold })
591
+
592
+ return result[:value] unless result[:won_recache]
593
+
594
+ new_val = yield
595
+ set(key, new_val, ttl_or_default(ttl), req_options)
596
+ new_val
597
+ end
598
+
532
599
  ##
533
600
  # Uses the argument TTL or the client-wide default. Ensures
534
601
  # that the value is an integer
@@ -571,7 +638,9 @@ module Dalli
571
638
  key = @key_manager.validate_key(key)
572
639
 
573
640
  server = ring.server_for_key(key)
574
- server.request(op, key, *args)
641
+ Instrumentation.trace(op.to_s, trace_attrs(op.to_s, key, server)) do
642
+ server.request(op, key, *args)
643
+ end
575
644
  rescue NetworkError => e
576
645
  Dalli.logger.debug { e.inspect }
577
646
  Dalli.logger.debug { 'retrying request with new server' }
@@ -0,0 +1,151 @@
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
+ # Dalli 4.2.1 uses the stable OTel semantic conventions for database spans.
12
+ #
13
+ # == Span Attributes
14
+ #
15
+ # All spans include the following default attributes:
16
+ # - +db.system.name+ - Always "memcached"
17
+ #
18
+ # Single-key operations (+get+, +set+, +delete+, +incr+, +decr+, etc.) add:
19
+ # - +db.operation.name+ - The operation name (e.g., "get", "set")
20
+ # - +server.address+ - The server hostname (e.g., "localhost")
21
+ # - +server.port+ - The server port as an integer (e.g., 11211); omitted for Unix sockets
22
+ #
23
+ # Multi-key operations (+get_multi+) add:
24
+ # - +db.operation.name+ - "get_multi"
25
+ # - +db.memcached.key_count+ - Number of keys requested
26
+ # - +db.memcached.hit_count+ - Number of keys found in cache
27
+ # - +db.memcached.miss_count+ - Number of keys not found
28
+ #
29
+ # Bulk write operations (+set_multi+, +delete_multi+) add:
30
+ # - +db.operation.name+ - The operation name
31
+ # - +db.memcached.key_count+ - Number of keys in the operation
32
+ #
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
+ # == Error Handling
42
+ #
43
+ # When an exception occurs during a traced operation:
44
+ # - The exception is recorded on the span via +record_exception+
45
+ # - The span status is set to error with the exception message
46
+ # - The exception is re-raised to the caller
47
+ #
48
+ # @example Checking if tracing is enabled
49
+ # Dalli::Instrumentation.enabled? # => true if OpenTelemetry is loaded
50
+ #
51
+ ##
52
+ module Instrumentation
53
+ # Default attributes included on all memcached spans.
54
+ # @return [Hash] frozen hash with 'db.system.name' => 'memcached'
55
+ DEFAULT_ATTRIBUTES = { 'db.system.name' => 'memcached' }.freeze
56
+
57
+ class << self
58
+ # Returns the OpenTelemetry tracer if available, nil otherwise.
59
+ #
60
+ # The tracer is cached after first lookup for performance.
61
+ # Uses the library name 'dalli' and current Dalli::VERSION.
62
+ #
63
+ # @return [OpenTelemetry::Trace::Tracer, nil] the tracer or nil if OTel unavailable
64
+ def tracer
65
+ return @tracer if defined?(@tracer)
66
+
67
+ @tracer = (OpenTelemetry.tracer_provider.tracer('dalli', Dalli::VERSION) if defined?(OpenTelemetry))
68
+ end
69
+
70
+ # Returns true if instrumentation is enabled (OpenTelemetry SDK is available).
71
+ #
72
+ # @return [Boolean] true if tracing is active, false otherwise
73
+ def enabled?
74
+ !tracer.nil?
75
+ end
76
+
77
+ # Wraps a block with a span if instrumentation is enabled.
78
+ #
79
+ # Creates a client span with the given name and attributes merged with
80
+ # DEFAULT_ATTRIBUTES. The block is executed within the span context.
81
+ # If an exception occurs, it is recorded on the span before re-raising.
82
+ #
83
+ # When tracing is disabled (OpenTelemetry not loaded), this method
84
+ # simply yields directly with zero overhead.
85
+ #
86
+ # @param name [String] the span name (e.g., 'get', 'set', 'delete')
87
+ # @param attributes [Hash] span attributes to merge with defaults.
88
+ # Common attributes include:
89
+ # - 'db.operation.name' - the operation name
90
+ # - 'server.address' - the server hostname
91
+ # - 'server.port' - the server port (integer)
92
+ # - 'db.memcached.key_count' - number of keys (for multi operations)
93
+ # @yield the cache operation to trace
94
+ # @return [Object] the result of the block
95
+ # @raise [StandardError] re-raises any exception from the block
96
+ #
97
+ # @example Tracing a set operation
98
+ # trace('set', { 'db.operation.name' => 'set', 'server.address' => 'localhost', 'server.port' => 11211 }) do
99
+ # server.set(key, value, ttl)
100
+ # end
101
+ #
102
+ def trace(name, attributes = {})
103
+ return yield unless enabled?
104
+
105
+ tracer.in_span(name, attributes: DEFAULT_ATTRIBUTES.merge(attributes), kind: :client) do |span|
106
+ yield
107
+ rescue StandardError => e
108
+ span.record_exception(e)
109
+ span.status = OpenTelemetry::Trace::Status.error(e.message)
110
+ raise
111
+ end
112
+ end
113
+
114
+ # Like trace, but yields the span to allow adding attributes after execution.
115
+ #
116
+ # This is useful for operations where metrics are only known after the
117
+ # operation completes, such as get_multi where hit/miss counts depend
118
+ # on the cache response.
119
+ #
120
+ # When tracing is disabled, yields nil as the span argument.
121
+ #
122
+ # @param name [String] the span name (e.g., 'get_multi')
123
+ # @param attributes [Hash] initial span attributes to merge with defaults
124
+ # @yield [OpenTelemetry::Trace::Span, nil] the span object, or nil if disabled
125
+ # @return [Object] the result of the block
126
+ # @raise [StandardError] re-raises any exception from the block
127
+ #
128
+ # @example Recording hit/miss metrics after get_multi
129
+ # trace_with_result('get_multi', { 'db.operation.name' => 'get_multi' }) do |span|
130
+ # results = fetch_from_cache(keys)
131
+ # if span
132
+ # span.set_attribute('db.memcached.hit_count', results.size)
133
+ # span.set_attribute('db.memcached.miss_count', keys.size - results.size)
134
+ # end
135
+ # results
136
+ # end
137
+ #
138
+ def trace_with_result(name, attributes = {})
139
+ return yield(nil) unless enabled?
140
+
141
+ tracer.in_span(name, attributes: DEFAULT_ATTRIBUTES.merge(attributes), kind: :client) do |span|
142
+ yield(span)
143
+ rescue StandardError => e
144
+ span.record_exception(e)
145
+ span.status = OpenTelemetry::Trace::Status.error(e.message)
146
+ raise
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -1,5 +1,7 @@
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.
@@ -57,7 +59,7 @@ module Dalli
57
59
  # our set, sending the noop to terminate the set of queries.
58
60
  ##
59
61
  def finish_queries(servers)
60
- deleted = []
62
+ deleted = Set.new
61
63
 
62
64
  servers.each do |server|
63
65
  next unless server.connected?
@@ -67,7 +69,7 @@ module Dalli
67
69
  rescue Dalli::NetworkError
68
70
  raise
69
71
  rescue Dalli::DalliError
70
- deleted.append(server)
72
+ deleted << server
71
73
  end
72
74
  end
73
75
 
@@ -94,7 +96,7 @@ module Dalli
94
96
 
95
97
  def fetch_responses(servers, start_time, timeout, &block)
96
98
  # Remove any servers which are not connected
97
- servers.delete_if { |s| !s.connected? }
99
+ servers.select!(&:connected?)
98
100
  return [] if servers.empty?
99
101
 
100
102
  time_left = remaining_time(start_time, timeout)
@@ -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
- @value_marshaller = client_options[:raw] ? StringMarshaller.new(@options) : ValueMarshaller.new(@options)
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)
@@ -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
- cmd << ' v f' if value
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
@@ -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
- req = RequestFormatter.meta_get(key: encoded_key, base64: base64)
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
- RequestFormatter.meta_get(key: encoded_key, return_cas: true, base64: base64, quiet: true)
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
- req = RequestFormatter.meta_get(key: encoded_key, ttl: ttl, base64: base64)
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
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.private_instance_methods.include?(:original_resolv_initialize)
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
data/lib/dalli/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dalli
4
- VERSION = '4.1.0'
4
+ VERSION = '4.2.1'
5
5
 
6
6
  MIN_SUPPORTED_MEMCACHED_VERSION = '1.4'
7
7
  end
data/lib/dalli.rb CHANGED
@@ -56,6 +56,7 @@ module Dalli
56
56
  end
57
57
 
58
58
  require_relative 'dalli/version'
59
+ require_relative 'dalli/instrumentation'
59
60
 
60
61
  require_relative 'dalli/compressor'
61
62
  require_relative 'dalli/protocol_deprecations'
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.1.0
4
+ version: 4.2.1
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
@@ -92,7 +93,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
92
93
  - !ruby/object:Gem::Version
93
94
  version: '0'
94
95
  requirements: []
95
- rubygems_version: 4.0.4
96
+ rubygems_version: 4.0.6
96
97
  specification_version: 4
97
98
  summary: High performance memcached client for Ruby
98
99
  test_files: []