dalli 4.3.3 → 5.0.2

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: 57b78e30ee409a2d742fc47ee57ccdd482fe9c75f369366ae315b7ef8b649934
4
- data.tar.gz: b8cad66f3cba53bbcb84b18f406eed60f22209c10dd4a312063a1e13189185ca
3
+ metadata.gz: ae3cbae2603955279bb78b75682ac1f56105d5ec2b2e0d464ab15255ed23369b
4
+ data.tar.gz: 2e5a3960e638293cfdf5663ef959d1ab9690620e09f0151a844cdeb9424cbf80
5
5
  SHA512:
6
- metadata.gz: 18b26e3c4aa30e5f8b4195d95307afca6c9240dd1154a36966d40d52047e638758850ec06e2a40fd1bd8e69fe150dee7409b1f4a565f0a80e397aaf115315463
7
- data.tar.gz: b6db69ecbc1e6d34587d8ec29b861f6a71c863b36229d3c642e3d0e646ba6234e3bac04225d64ff174d12f35b728663e82e1d2b5f7e93dc9ac3e1ff33fd58db3
6
+ metadata.gz: 243382482bc345bf982f2cb96fae28b974f37b2eee653a71c7f3a85518ef5acdbfda6f2fddacfa31da6252774000e11a1c96d3707d29808c636951d85ace17be
7
+ data.tar.gz: 5938608c26cd307e397cad4fdf6e2fda4519fdbd8df2b7d75352b691996fec4dca59632cc0053b8bf410612c8e6cada73ccad7baf5c000a183bd04bb196d056e
data/CHANGELOG.md CHANGED
@@ -1,6 +1,86 @@
1
1
  Dalli Changelog
2
2
  =====================
3
3
 
4
+ 5.0.2
5
+ ==========
6
+
7
+ Performance:
8
+
9
+ - Add single-server fast path for `get_multi`, `set_multi`, and `delete_multi` (#1077)
10
+ - When only one memcached server is configured, bypass the `Pipelined*` machinery (IO.select, response buffering, server grouping) and issue all quiet meta requests inline followed by a noop terminator
11
+ - `get_multi` shows ~1.5x improvement at 10 keys and ~1.75x at 100–500 keys compared to the `PipelinedGetter` path
12
+ - Thanks to Dan Mayer (Shopify) for this contribution
13
+
14
+ Development:
15
+
16
+ - Add `bin/benchmark_branch` script for benchmarking against the current branch
17
+
18
+ 5.0.1
19
+ ==========
20
+
21
+ Performance:
22
+
23
+ - Reduce object allocations in pipelined get response processing (#1072, #1078)
24
+ - Offset-based `ResponseBuffer`: track a read offset instead of slicing a new string after every parsed response; compact only when the consumed portion exceeds 4KB and more than half the buffer
25
+ - Inline response processor parsing: avoid intermediate array allocations from `split`-based header parsing
26
+ - Block-based `pipeline_next_responses`: yield `(key, value, cas)` directly when a block is given, avoiding per-call Hash allocation
27
+ - `PipelinedGetter`: replace Hash-based socket-to-server mapping with linear scan (faster for typical 1-5 server counts); use `Process.clock_gettime(CLOCK_MONOTONIC)` instead of `Time.now`
28
+ - Add cross-version benchmark script (`bin/compare_versions`) for reproducible performance comparisons across Dalli versions
29
+
30
+ Bug Fixes:
31
+
32
+ - Rescue `IOError` in connection manager `write`/`flush` methods (#1075)
33
+ - Prevents unhandled exceptions when a connection is closed mid-operation
34
+ - Thanks to Graham Cooper (Shopify) for this fix
35
+
36
+ Development:
37
+
38
+ - Add `rubocop-thread_safety` for detecting thread-safety issues (#1076)
39
+ - Add CONTRIBUTING.md with AI contribution policy (#1074)
40
+
41
+ 5.0.0
42
+ ==========
43
+
44
+ **Breaking Changes:**
45
+
46
+ - **Removed binary protocol** - The meta protocol is now the only supported protocol
47
+ - The `:protocol` option is no longer used
48
+ - Requires memcached 1.6+ (for meta protocol support)
49
+ - Users on older memcached versions must upgrade or stay on Dalli 4.x
50
+
51
+ - **Removed SASL authentication** - The meta protocol does not support authentication
52
+ - Use network-level security (firewall rules, VPN) or memcached's TLS support instead
53
+ - Users requiring SASL authentication must stay on Dalli 4.x with binary protocol
54
+
55
+ - **Ruby 3.3+ required** - Dropped support for Ruby 3.1 and 3.2
56
+ - Ruby 3.2 reached end-of-life in March 2026
57
+ - JRuby remains supported
58
+
59
+ Performance:
60
+
61
+ - **~7% read performance improvement** (CRuby only)
62
+ - Use native `IO#read` instead of custom `readfull` implementation
63
+ - Enabled by Ruby 3.3's `IO#timeout=` support
64
+ - JRuby continues to use `readfull` for compatibility
65
+
66
+ OpenTelemetry:
67
+
68
+ - Migrate to stable OTel semantic conventions (#1070)
69
+ - `db.system` renamed to `db.system.name`
70
+ - `db.operation` renamed to `db.operation.name`
71
+ - `server.address` now contains hostname only; `server.port` is a separate integer attribute
72
+ - `get_with_metadata` and `fetch_with_lock` now include `server.address`/`server.port`
73
+ - Add `db.query.text` span attribute with configurable modes
74
+ - `:otel_db_statement` option: `:include`, `:obfuscate`, or `nil` (default: omitted)
75
+ - Add `peer.service` span attribute
76
+ - `:otel_peer_service` option for logical service naming
77
+
78
+ Internal:
79
+
80
+ - Simplified protocol directory structure: moved `lib/dalli/protocol/meta/*` to `lib/dalli/protocol/`
81
+ - Removed deprecated binary protocol files and SASL authentication code
82
+ - Removed `require 'set'` (autoloaded in Ruby 3.3+)
83
+
4
84
  4.3.3
5
85
  ==========
6
86
 
data/Gemfile CHANGED
@@ -22,6 +22,7 @@ group :development, :test do
22
22
  gem 'rubocop-minitest'
23
23
  gem 'rubocop-performance'
24
24
  gem 'rubocop-rake'
25
+ gem 'rubocop-thread_safety'
25
26
  gem 'simplecov'
26
27
  end
27
28
 
data/README.md CHANGED
@@ -10,26 +10,14 @@ Dalli supports:
10
10
  * Fine-grained control of data serialization and compression
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
- * SASL authentication
14
13
  * OpenTelemetry distributed tracing (automatic when SDK is present)
15
14
 
16
15
  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).
17
16
 
18
17
  ## Requirements
19
18
 
20
- * Ruby 3.1 or later
21
- * memcached 1.4 or later (1.6+ recommended for meta protocol support)
22
-
23
- ## Protocol Options
24
-
25
- Dalli supports two protocols for communicating with memcached:
26
-
27
- * `:binary` (default) - Works with all memcached versions, supports SASL authentication
28
- * `:meta` - Requires memcached 1.6+, better performance for some operations, no authentication support
29
-
30
- ```ruby
31
- Dalli::Client.new('localhost:11211', protocol: :meta)
32
- ```
19
+ * Ruby 3.3 or later (JRuby also supported)
20
+ * memcached 1.6 or later
33
21
 
34
22
  ## Configuration Options
35
23
 
@@ -64,7 +52,7 @@ By default, Dalli uses Ruby's Marshal for serialization. Deserializing untrusted
64
52
  Dalli::Client.new('localhost:11211', serializer: JSON)
65
53
  ```
66
54
 
67
- See the [4.0-Upgrade.md](4.0-Upgrade.md) guide for more information.
55
+ See the [5.0-Upgrade.md](5.0-Upgrade.md) guide for upgrade information.
68
56
 
69
57
  ## OpenTelemetry Tracing
70
58
 
@@ -125,7 +113,7 @@ To install this gem onto your local machine, run `bundle exec rake install`.
125
113
 
126
114
  ## Contributing
127
115
 
128
- If you have a fix you wish to provide, please fork the code, fix in your local project and then send a pull request on github. Please ensure that you include a test which verifies your fix and update the [changelog](CHANGELOG.md) with a one sentence description of your fix so you get credit as a contributor.
116
+ Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on how to contribute, including our policy on AI-authored contributions.
129
117
 
130
118
  ## Appreciation
131
119
 
data/lib/dalli/client.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'digest/md5'
4
- require 'set'
5
4
 
6
5
  # encoding: ascii
7
6
  module Dalli
@@ -48,8 +47,6 @@ module Dalli
48
47
  # serialization pipeline.
49
48
  # - :digest_class - defaults to Digest::MD5, allows you to pass in an object that responds to the hexdigest method,
50
49
  # useful for injecting a FIPS compliant hash object.
51
- # - :protocol - one of either :binary or :meta, defaulting to :binary. This sets the protocol that Dalli uses
52
- # to communicate with memcached.
53
50
  # - :otel_db_statement - controls the +db.query.text+ span attribute when OpenTelemetry is loaded.
54
51
  # +:include+ logs the full operation and key(s), +:obfuscate+ replaces keys with "?",
55
52
  # +nil+ (default) omits the attribute entirely.
@@ -58,9 +55,9 @@ module Dalli
58
55
  def initialize(servers = nil, options = {})
59
56
  @normalized_servers = ::Dalli::ServersArgNormalizer.normalize_servers(servers)
60
57
  @options = normalize_options(options)
58
+ warn_removed_options(@options)
61
59
  @key_manager = ::Dalli::KeyManager.new(@options)
62
60
  @ring = nil
63
- emit_deprecation_warnings
64
61
  end
65
62
 
66
63
  #
@@ -102,10 +99,7 @@ module Dalli
102
99
  end
103
100
 
104
101
  ##
105
- # Get value with extended metadata using the meta protocol.
106
- #
107
- # IMPORTANT: This method requires memcached 1.6+ and the meta protocol (protocol: :meta).
108
- # It will raise an error if used with the binary protocol.
102
+ # Get value with extended metadata.
109
103
  #
110
104
  # @param key [String] the cache key
111
105
  # @param options [Hash] options controlling what metadata to return
@@ -133,8 +127,6 @@ module Dalli
133
127
  # # => { value: "data", cas: 123, hit_before: true, last_access: 42 }
134
128
  #
135
129
  def get_with_metadata(key, options = {})
136
- raise_unless_meta_protocol!
137
-
138
130
  key = key.to_s
139
131
  key = @key_manager.validate_key(key)
140
132
 
@@ -208,9 +200,6 @@ module Dalli
208
200
  # cache entry (the "thundering herd" problem). Only one client wins the right to
209
201
  # regenerate; other clients receive the stale value (if available) or wait.
210
202
  #
211
- # IMPORTANT: This method requires memcached 1.6+ and the meta protocol (protocol: :meta).
212
- # It will raise an error if used with the binary protocol.
213
- #
214
203
  # @param key [String] the cache key
215
204
  # @param ttl [Integer] time-to-live for the cached value in seconds
216
205
  # @param lock_ttl [Integer] how long the lock/stub lives (default: 30 seconds)
@@ -236,8 +225,6 @@ module Dalli
236
225
  def fetch_with_lock(key, ttl: nil, lock_ttl: 30, recache_threshold: nil, req_options: nil, &block)
237
226
  raise ArgumentError, 'Block is required for fetch_with_lock' unless block_given?
238
227
 
239
- raise_unless_meta_protocol!
240
-
241
228
  key = key.to_s
242
229
  key = @key_manager.validate_key(key)
243
230
 
@@ -324,7 +311,11 @@ module Dalli
324
311
  return if hash.empty?
325
312
 
326
313
  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)
314
+ if ring.servers.size == 1
315
+ single_server_set_multi(hash, ttl_or_default(ttl), req_options)
316
+ else
317
+ pipelined_setter.process(hash, ttl_or_default(ttl), req_options)
318
+ end
328
319
  end
329
320
  end
330
321
 
@@ -381,7 +372,11 @@ module Dalli
381
372
  return if keys.empty?
382
373
 
383
374
  Instrumentation.trace('delete_multi', multi_trace_attrs('delete_multi', keys.size, keys)) do
384
- pipelined_deleter.process(keys)
375
+ if ring.servers.size == 1
376
+ single_server_delete_multi(keys)
377
+ else
378
+ pipelined_deleter.process(keys)
379
+ end
385
380
  end
386
381
  end
387
382
 
@@ -514,10 +509,6 @@ module Dalli
514
509
 
515
510
  private
516
511
 
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
512
  def record_hit_miss_metrics(span, key_count, hit_count)
522
513
  return unless span
523
514
 
@@ -539,13 +530,52 @@ module Dalli
539
530
 
540
531
  def get_multi_hash(keys)
541
532
  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
533
+ hash = if ring.servers.size == 1
534
+ single_server_get_multi(keys)
535
+ else
536
+ {}.tap do |h|
537
+ pipelined_getter.process(keys) { |k, data| h[k] = data.first }
538
+ end
539
+ end
540
+ record_hit_miss_metrics(span, keys.size, hash.size)
541
+ hash
546
542
  end
547
543
  end
548
544
 
545
+ def single_server
546
+ server = ring.servers.first
547
+ server if server&.alive?
548
+ end
549
+
550
+ def single_server_get_multi(keys)
551
+ keys.map! { |k| @key_manager.validate_key(k.to_s) }
552
+ return {} unless (server = single_server)
553
+
554
+ result = server.request(:read_multi_req, keys)
555
+ result.transform_keys! { |k| @key_manager.key_without_namespace(k) }
556
+ result
557
+ rescue Dalli::NetworkError
558
+ {}
559
+ end
560
+
561
+ def single_server_set_multi(hash, ttl, req_options)
562
+ pairs = hash.transform_keys { |k| @key_manager.validate_key(k.to_s) }
563
+ return unless (server = single_server)
564
+
565
+ server.request(:write_multi_req, pairs, ttl, req_options)
566
+ rescue Dalli::NetworkError
567
+ nil
568
+ end
569
+
570
+ def single_server_delete_multi(keys)
571
+ validated_keys = keys.map { |k| @key_manager.validate_key(k.to_s) }
572
+ return unless (server = single_server)
573
+
574
+ server.request(:delete_multi_req, validated_keys)
575
+ rescue Dalli::NetworkError
576
+ nil
577
+ end
578
+
549
579
  def get_multi_attributes(keys)
550
580
  multi_trace_attrs('get_multi', keys.size, keys)
551
581
  end
@@ -607,16 +637,7 @@ module Dalli
607
637
  end
608
638
 
609
639
  def ring
610
- @ring ||= Dalli::Ring.new(@normalized_servers, protocol_implementation, @options)
611
- end
612
-
613
- def protocol_implementation
614
- @protocol_implementation ||= case @options[:protocol]&.to_s
615
- when 'meta'
616
- Dalli::Protocol::Meta
617
- else
618
- Dalli::Protocol::Binary
619
- end
640
+ @ring ||= Dalli::Ring.new(@normalized_servers, @options)
620
641
  end
621
642
 
622
643
  ##
@@ -627,7 +648,7 @@ module Dalli
627
648
  #
628
649
  # This method also forces retries on network errors - when
629
650
  # a particular memcached instance becomes unreachable, or the
630
- # operational times out.
651
+ # operation times out.
631
652
  ##
632
653
  def perform(*all_args)
633
654
  return yield if block_given?
@@ -654,6 +675,21 @@ module Dalli
654
675
  raise ArgumentError, "cannot convert :expires_in => #{opts[:expires_in].inspect} to an integer"
655
676
  end
656
677
 
678
+ REMOVED_OPTIONS = {
679
+ protocol: 'Dalli 5.0 only supports the meta protocol. The :protocol option has been removed.',
680
+ username: 'Dalli 5.0 removed SASL authentication support. The :username option is ignored.',
681
+ password: 'Dalli 5.0 removed SASL authentication support. The :password option is ignored.'
682
+ }.freeze
683
+ private_constant :REMOVED_OPTIONS
684
+
685
+ def warn_removed_options(opts)
686
+ REMOVED_OPTIONS.each do |key, message|
687
+ next unless opts.key?(key)
688
+
689
+ Dalli.logger.warn(message)
690
+ end
691
+ end
692
+
657
693
  def pipelined_getter
658
694
  PipelinedGetter.new(ring, @key_manager)
659
695
  end
@@ -665,15 +701,5 @@ module Dalli
665
701
  def pipelined_deleter
666
702
  PipelinedDeleter.new(ring, @key_manager)
667
703
  end
668
-
669
- def raise_unless_meta_protocol!
670
- return if protocol_implementation == Dalli::Protocol::Meta
671
-
672
- raise Dalli::DalliError,
673
- 'This operation requires the meta protocol (memcached 1.6+). ' \
674
- 'Use protocol: :meta when creating the client.'
675
- end
676
-
677
- include ProtocolDeprecations
678
704
  end
679
705
  end
@@ -8,7 +8,7 @@ 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.3.2 uses the stable OTel semantic conventions for database spans.
11
+ # Dalli 5.0 uses the stable OTel semantic conventions for database spans.
12
12
  #
13
13
  # == Span Attributes
14
14
  #
@@ -61,11 +61,13 @@ module Dalli
61
61
  # Uses the library name 'dalli' and current Dalli::VERSION.
62
62
  #
63
63
  # @return [OpenTelemetry::Trace::Tracer, nil] the tracer or nil if OTel unavailable
64
+ # rubocop:disable ThreadSafety/ClassInstanceVariable
64
65
  def tracer
65
66
  return @tracer if defined?(@tracer)
66
67
 
67
68
  @tracer = (OpenTelemetry.tracer_provider.tracer('dalli', Dalli::VERSION) if defined?(OpenTelemetry))
68
69
  end
70
+ # rubocop:enable ThreadSafety/ClassInstanceVariable
69
71
 
70
72
  # Returns true if instrumentation is enabled (OpenTelemetry SDK is available).
71
73
  #
data/lib/dalli/options.rb CHANGED
@@ -6,7 +6,7 @@ module Dalli
6
6
  # Make Dalli threadsafe by using a lock around all
7
7
  # public server methods.
8
8
  #
9
- # Dalli::Protocol::Binary.extend(Dalli::Threadsafe)
9
+ # Dalli::Protocol::Meta.extend(Dalli::Threadsafe)
10
10
  #
11
11
  module Threadsafe
12
12
  def self.extended(obj)
@@ -13,7 +13,7 @@ module Dalli
13
13
  attr_reader :pid
14
14
 
15
15
  def update!
16
- @pid = Process.pid
16
+ @pid = Process.pid # rubocop:disable ThreadSafety/ClassInstanceVariable
17
17
  end
18
18
  end
19
19
  update!
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'set'
4
-
5
3
  module Dalli
6
4
  ##
7
5
  # Contains logic for the pipelined gets implemented by the client.
@@ -22,6 +22,7 @@ module Dalli
22
22
 
23
23
  def initialize(attribs, client_options = {})
24
24
  hostname, port, socket_type, @weight, user_creds = ServerConfigParser.parse(attribs)
25
+ warn_uri_credentials(user_creds)
25
26
  @options = client_options.merge(user_creds)
26
27
  @raw_mode = client_options[:raw]
27
28
  @value_marshaller = @raw_mode ? StringMarshaller.new(@options) : ValueMarshaller.new(@options)
@@ -152,18 +153,6 @@ module Dalli
152
153
  !response_buffer.in_progress?
153
154
  end
154
155
 
155
- def username
156
- @options[:username] || ENV.fetch('MEMCACHE_USERNAME', nil)
157
- end
158
-
159
- def password
160
- @options[:password] || ENV.fetch('MEMCACHE_PASSWORD', nil)
161
- end
162
-
163
- def require_auth?
164
- !username.nil?
165
- end
166
-
167
156
  def quiet?
168
157
  Thread.current[::Dalli::QUIET]
169
158
  end
@@ -173,6 +162,16 @@ module Dalli
173
162
 
174
163
  private
175
164
 
165
+ URI_CREDENTIAL_WARNING = 'Dalli 5.0 removed SASL authentication. ' \
166
+ 'Credentials in memcached:// URIs are ignored.'
167
+ private_constant :URI_CREDENTIAL_WARNING
168
+
169
+ def warn_uri_credentials(user_creds)
170
+ return if user_creds[:username].nil? && user_creds[:password].nil?
171
+
172
+ Dalli.logger.warn(URI_CREDENTIAL_WARNING)
173
+ end
174
+
176
175
  ALLOWED_QUIET_OPS = %i[add replace set delete incr decr append prepend flush noop].freeze
177
176
  private_constant :ALLOWED_QUIET_OPS
178
177
 
@@ -227,8 +226,7 @@ module Dalli
227
226
 
228
227
  def connect
229
228
  @connection_manager.establish_connection
230
- authenticate_connection if require_auth?
231
- @version = version # Connect socket if not authed
229
+ @version = version
232
230
  up!
233
231
  end
234
232
 
@@ -156,20 +156,26 @@ module Dalli
156
156
  end
157
157
 
158
158
  def read(count)
159
- @sock.readfull(count)
159
+ # JRuby doesn't support IO#timeout=, so use custom readfull implementation
160
+ # CRuby 3.3+ has IO#timeout= which makes IO#read work with timeouts
161
+ if RUBY_ENGINE == 'jruby'
162
+ @sock.readfull(count)
163
+ else
164
+ @sock.read(count)
165
+ end
160
166
  rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
161
167
  error_on_request!(e)
162
168
  end
163
169
 
164
170
  def write(bytes)
165
171
  @sock.write(bytes)
166
- rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS => e
172
+ rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, IOError => e
167
173
  error_on_request!(e)
168
174
  end
169
175
 
170
176
  def flush
171
177
  @sock.flush
172
- rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS => e
178
+ rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, IOError => e
173
179
  error_on_request!(e)
174
180
  end
175
181
 
@@ -257,13 +257,85 @@ module Dalli
257
257
  @connection_manager.flush
258
258
  end
259
259
 
260
- def authenticate_connection
261
- raise Dalli::DalliError, 'Authentication not supported for the meta protocol.'
260
+ # Single-server fast path for get_multi. Inlines request formatting and
261
+ # response parsing to minimize per-key overhead. Avoids the PipelinedGetter
262
+ # machinery (IO.select, response buffering, server grouping).
263
+ def read_multi_req(keys)
264
+ is_raw = raw_mode?
265
+ # Inline request formatting — avoids RequestFormatter.meta_get overhead per key.
266
+ # In raw mode: "mg <key> v k q s\r\n" (no f flag, key at index 2)
267
+ # Normal mode: "mg <key> v f k q s\r\n" (key at index 3)
268
+ post_get = is_raw ? " v k q s\r\n" : " v f k q s\r\n"
269
+ keys.each do |key|
270
+ encoded_key, base64 = KeyRegularizer.encode(key)
271
+ write(base64 ? "mg #{encoded_key} b#{post_get}" : "mg #{encoded_key}#{post_get}")
272
+ end
273
+ write("mn\r\n")
274
+ @connection_manager.flush
275
+
276
+ read_multi_get_responses(is_raw)
277
+ end
278
+
279
+ def read_multi_get_responses(is_raw)
280
+ hash = {}
281
+ key_index = is_raw ? 2 : 3
282
+ while (line = @connection_manager.read_line)
283
+ break if line.start_with?('MN')
284
+ next unless line.start_with?('VA ')
285
+
286
+ key, value = parse_multi_get_value(line, key_index, is_raw)
287
+ hash[key] = value if key
288
+ end
289
+ hash
290
+ end
291
+
292
+ def parse_multi_get_value(line, key_index, is_raw)
293
+ tokens = line.chomp!(TERMINATOR).split
294
+ value = @connection_manager.read(tokens[1].to_i + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
295
+ raw_key = tokens[key_index]
296
+ return unless raw_key
297
+
298
+ key = KeyRegularizer.decode(raw_key[1..], tokens.include?('b'))
299
+ bitflags = is_raw ? 0 : response_processor.bitflags_from_tokens(tokens)
300
+ [key, @value_marshaller.retrieve(value, bitflags)]
301
+ end
302
+
303
+ # Single-server fast path for set_multi. Inlines request formatting to
304
+ # minimize per-key overhead. Avoids PipelinedSetter server grouping.
305
+ def write_multi_req(pairs, ttl, req_options)
306
+ ttl = TtlSanitizer.sanitize(ttl) if ttl
307
+ pairs.each do |key, raw_value|
308
+ (value, bitflags) = @value_marshaller.store(key, raw_value, req_options)
309
+ encoded_key, base64 = KeyRegularizer.encode(key)
310
+ # Inline format: "ms <key> <size> c [b] F<flags> T<ttl> MS q\r\n"
311
+ cmd = "ms #{encoded_key} #{value.bytesize} c"
312
+ cmd << ' b' if base64
313
+ cmd << " F#{bitflags}" if bitflags
314
+ cmd << " T#{ttl}" if ttl
315
+ cmd << " MS q\r\n"
316
+ write(cmd)
317
+ write(value)
318
+ write(TERMINATOR)
319
+ end
320
+ write_noop
321
+ response_processor.consume_all_responses_until_mn
322
+ end
323
+
324
+ # Single-server fast path for delete_multi. Writes all quiet delete requests
325
+ # terminated by a noop, then consumes all responses.
326
+ def delete_multi_req(keys)
327
+ keys.each do |key|
328
+ encoded_key, base64 = KeyRegularizer.encode(key)
329
+ # Inline format: "md <key> [b] q\r\n"
330
+ write(base64 ? "md #{encoded_key} b q\r\n" : "md #{encoded_key} q\r\n")
331
+ end
332
+ write_noop
333
+ response_processor.consume_all_responses_until_mn
262
334
  end
263
335
 
264
- require_relative 'meta/key_regularizer'
265
- require_relative 'meta/request_formatter'
266
- require_relative 'meta/response_processor'
336
+ require_relative 'key_regularizer'
337
+ require_relative 'request_formatter'
338
+ require_relative 'response_processor'
267
339
  end
268
340
  end
269
341
  end
@@ -6,7 +6,7 @@ module Dalli
6
6
  module Protocol
7
7
  ##
8
8
  # Dalli::Protocol::ServerConfigParser parses a server string passed to
9
- # a Dalli::Protocol::Binary instance into the hostname, port, weight, and
9
+ # a Dalli::Protocol::Meta instance into the hostname, port, weight, and
10
10
  # socket_type.
11
11
  ##
12
12
  class ServerConfigParser
data/lib/dalli/ring.rb CHANGED
@@ -23,9 +23,9 @@ module Dalli
23
23
 
24
24
  attr_accessor :servers, :continuum
25
25
 
26
- def initialize(servers_arg, protocol_implementation, options)
26
+ def initialize(servers_arg, options)
27
27
  @servers = servers_arg.map do |s|
28
- protocol_implementation.new(s, options)
28
+ Dalli::Protocol::Meta.new(s, options)
29
29
  end
30
30
  @continuum = nil
31
31
  @continuum = build_continuum(servers) if servers.size > 1
@@ -16,7 +16,7 @@ module Dalli
16
16
  # weight are optional (e.g. 'localhost', 'abc.com:12345', 'example.org:22222:3')
17
17
  # * A colon separated string of (UNIX socket, weight) where the weight is optional
18
18
  # (e.g. '/var/run/memcached/socket', '/tmp/xyz:3') (not supported on Windows)
19
- # * A URI with a 'memcached' protocol, which will typically include a username/password
19
+ # * A URI with a 'memcached' protocol (e.g. 'memcached://localhost:11211')
20
20
  #
21
21
  # The methods in this module do not validate the format of individual server strings, but
22
22
  # rather normalize the argument into a compact array, wherein each array entry corresponds
data/lib/dalli/socket.rb CHANGED
@@ -108,12 +108,14 @@ module Dalli
108
108
  # Detect and cache whether TCPSocket supports the connect_timeout: keyword argument.
109
109
  # Returns false if TCPSocket#initialize has been monkey-patched by gems like
110
110
  # socksify or resolv-replace, which don't support keyword arguments.
111
+ # rubocop:disable ThreadSafety/ClassInstanceVariable
111
112
  def self.supports_connect_timeout?
112
113
  return @supports_connect_timeout if defined?(@supports_connect_timeout)
113
114
 
114
115
  @supports_connect_timeout = RUBY_VERSION >= '3.0' &&
115
116
  ::TCPSocket.instance_method(:initialize).parameters == TCPSOCKET_NATIVE_PARAMETERS
116
117
  end
118
+ # rubocop:enable ThreadSafety/ClassInstanceVariable
117
119
 
118
120
  def self.create_socket_with_timeout(host, port, options)
119
121
  if supports_connect_timeout?
@@ -170,12 +172,14 @@ module Dalli
170
172
 
171
173
  # Detect and cache the correct pack format for struct timeval on this platform.
172
174
  # Different architectures have different sizes for time_t and suseconds_t.
175
+ # rubocop:disable ThreadSafety/ClassInstanceVariable
173
176
  def self.timeval_pack_format(sock)
174
177
  @timeval_pack_format ||= begin
175
178
  expected_size = sock.getsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVTIMEO).data.bytesize
176
179
  TIMEVAL_PACK_FORMATS.find { |fmt| TIMEVAL_TEST_VALUES.pack(fmt).bytesize == expected_size } || 'll'
177
180
  end
178
181
  end
182
+ # rubocop:enable ThreadSafety/ClassInstanceVariable
179
183
 
180
184
  def self.pack_timeval(sock, seconds, microseconds)
181
185
  [seconds, microseconds].pack(timeval_pack_format(sock))