dalli 4.3.2 → 5.0.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: 7eda4c85eb715e06ef3d8b34ea8e33a3cde89f19379a758f105363457451a034
4
- data.tar.gz: 36ae8b603f8f778edbbf3bed9b7504ff1a440382bf5520423f49c39961f04a55
3
+ metadata.gz: 02d0fa949b7a065f86fb3ac7b511a3feacc50b700e82dcb385e0e79c56583a9d
4
+ data.tar.gz: d72b9e4b014ae1ae0b0d5a1ebe09ea48cdf9fbf0a2cda000efc19f17d5d34368
5
5
  SHA512:
6
- metadata.gz: b9a37ecfc73734a141979a34f2e155baa40cea5b22b016c192514c9d5847335d949ee642620c4508cc2ec8a2ceeb4dcec381715e633893623c42454975999d1a
7
- data.tar.gz: 2a5f38c78c371e0710354a2b915a074512a87225f33fca95c9088ded76cad0dfc22e454fc9f64bddd40d8b0c0a6ab65bb108f467cd5b2b2c39d651285665f845
6
+ metadata.gz: bf26484aa345df2d43a78da570027b68a7fd0dd70d7a62275ebe95a5e29ed36c11a8b1385ff0c71818f55184245e085cd75962510f9f8559aad10b0d284f8d71
7
+ data.tar.gz: 0b3913a33f61873d6e3da0d62ec643dbf49f679740d3469b7cb826083f02f3b32d37e2a20548634d09cecdc4389da7c5bdc43af530bb48956cb4084f91f10d9e
data/CHANGELOG.md CHANGED
@@ -1,6 +1,88 @@
1
1
  Dalli Changelog
2
2
  =====================
3
3
 
4
+ 5.0.1
5
+ ==========
6
+
7
+ Performance:
8
+
9
+ - Reduce object allocations in pipelined get response processing (#1072, #1078)
10
+ - 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
11
+ - Inline response processor parsing: avoid intermediate array allocations from `split`-based header parsing
12
+ - Block-based `pipeline_next_responses`: yield `(key, value, cas)` directly when a block is given, avoiding per-call Hash allocation
13
+ - `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`
14
+ - Add cross-version benchmark script (`bin/compare_versions`) for reproducible performance comparisons across Dalli versions
15
+
16
+ Bug Fixes:
17
+
18
+ - Rescue `IOError` in connection manager `write`/`flush` methods (#1075)
19
+ - Prevents unhandled exceptions when a connection is closed mid-operation
20
+ - Thanks to Graham Cooper (Shopify) for this fix
21
+
22
+ Development:
23
+
24
+ - Add `rubocop-thread_safety` for detecting thread-safety issues (#1076)
25
+ - Add CONTRIBUTING.md with AI contribution policy (#1074)
26
+
27
+ 5.0.0
28
+ ==========
29
+
30
+ **Breaking Changes:**
31
+
32
+ - **Removed binary protocol** - The meta protocol is now the only supported protocol
33
+ - The `:protocol` option is no longer used
34
+ - Requires memcached 1.6+ (for meta protocol support)
35
+ - Users on older memcached versions must upgrade or stay on Dalli 4.x
36
+
37
+ - **Removed SASL authentication** - The meta protocol does not support authentication
38
+ - Use network-level security (firewall rules, VPN) or memcached's TLS support instead
39
+ - Users requiring SASL authentication must stay on Dalli 4.x with binary protocol
40
+
41
+ - **Ruby 3.3+ required** - Dropped support for Ruby 3.1 and 3.2
42
+ - Ruby 3.2 reached end-of-life in March 2026
43
+ - JRuby remains supported
44
+
45
+ Performance:
46
+
47
+ - **~7% read performance improvement** (CRuby only)
48
+ - Use native `IO#read` instead of custom `readfull` implementation
49
+ - Enabled by Ruby 3.3's `IO#timeout=` support
50
+ - JRuby continues to use `readfull` for compatibility
51
+
52
+ OpenTelemetry:
53
+
54
+ - Migrate to stable OTel semantic conventions (#1070)
55
+ - `db.system` renamed to `db.system.name`
56
+ - `db.operation` renamed to `db.operation.name`
57
+ - `server.address` now contains hostname only; `server.port` is a separate integer attribute
58
+ - `get_with_metadata` and `fetch_with_lock` now include `server.address`/`server.port`
59
+ - Add `db.query.text` span attribute with configurable modes
60
+ - `:otel_db_statement` option: `:include`, `:obfuscate`, or `nil` (default: omitted)
61
+ - Add `peer.service` span attribute
62
+ - `:otel_peer_service` option for logical service naming
63
+
64
+ Internal:
65
+
66
+ - Simplified protocol directory structure: moved `lib/dalli/protocol/meta/*` to `lib/dalli/protocol/`
67
+ - Removed deprecated binary protocol files and SASL authentication code
68
+ - Removed `require 'set'` (autoloaded in Ruby 3.3+)
69
+
70
+ 4.3.3
71
+ ==========
72
+
73
+ Performance:
74
+
75
+ - Reduce object allocations in pipelined get response processing (#1072)
76
+ - 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
77
+ - Inline response processor parsing: avoid intermediate array allocations from `split`-based header parsing in both binary and meta protocols
78
+ - Block-based `pipeline_next_responses`: yield `(key, value, cas)` directly when a block is given, avoiding per-call Hash allocation
79
+ - `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`
80
+ - Add cross-version benchmark script (`bin/compare_versions`) for reproducible performance comparisons across Dalli versions
81
+
82
+ Bug Fixes:
83
+
84
+ - Skip OTel integration tests when meta protocol is unavailable (#1072)
85
+
4
86
  4.3.2
5
87
  ==========
6
88
 
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
 
@@ -514,10 +501,6 @@ module Dalli
514
501
 
515
502
  private
516
503
 
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
504
  def record_hit_miss_metrics(span, key_count, hit_count)
522
505
  return unless span
523
506
 
@@ -607,16 +590,7 @@ module Dalli
607
590
  end
608
591
 
609
592
  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
593
+ @ring ||= Dalli::Ring.new(@normalized_servers, @options)
620
594
  end
621
595
 
622
596
  ##
@@ -627,7 +601,7 @@ module Dalli
627
601
  #
628
602
  # This method also forces retries on network errors - when
629
603
  # a particular memcached instance becomes unreachable, or the
630
- # operational times out.
604
+ # operation times out.
631
605
  ##
632
606
  def perform(*all_args)
633
607
  return yield if block_given?
@@ -654,6 +628,21 @@ module Dalli
654
628
  raise ArgumentError, "cannot convert :expires_in => #{opts[:expires_in].inspect} to an integer"
655
629
  end
656
630
 
631
+ REMOVED_OPTIONS = {
632
+ protocol: 'Dalli 5.0 only supports the meta protocol. The :protocol option has been removed.',
633
+ username: 'Dalli 5.0 removed SASL authentication support. The :username option is ignored.',
634
+ password: 'Dalli 5.0 removed SASL authentication support. The :password option is ignored.'
635
+ }.freeze
636
+ private_constant :REMOVED_OPTIONS
637
+
638
+ def warn_removed_options(opts)
639
+ REMOVED_OPTIONS.each do |key, message|
640
+ next unless opts.key?(key)
641
+
642
+ Dalli.logger.warn(message)
643
+ end
644
+ end
645
+
657
646
  def pipelined_getter
658
647
  PipelinedGetter.new(ring, @key_manager)
659
648
  end
@@ -665,15 +654,5 @@ module Dalli
665
654
  def pipelined_deleter
666
655
  PipelinedDeleter.new(ring, @key_manager)
667
656
  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
657
  end
679
658
  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.
@@ -29,7 +27,7 @@ module Dalli
29
27
  # Stores partial results collected during interleaved send phase
30
28
  @partial_results = {}
31
29
  servers = setup_requests(keys)
32
- start_time = Time.now
30
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
33
31
 
34
32
  # First yield any partial results collected during interleaved send
35
33
  yield_partial_results(&block)
@@ -149,7 +147,7 @@ module Dalli
149
147
  end
150
148
 
151
149
  def remaining_time(start, timeout)
152
- elapsed = Time.now - start
150
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
153
151
  return 0 if elapsed > timeout
154
152
 
155
153
  timeout - elapsed
@@ -168,8 +166,8 @@ module Dalli
168
166
  # Processes responses from a server. Returns true if there are no
169
167
  # additional responses from this server.
170
168
  def process_server(server)
171
- server.pipeline_next_responses.each_pair do |key, value_list|
172
- yield @key_manager.key_without_namespace(key), value_list
169
+ server.pipeline_next_responses do |key, value, cas|
170
+ yield @key_manager.key_without_namespace(key), [value, cas]
173
171
  end
174
172
 
175
173
  server.pipeline_complete?
@@ -178,18 +176,13 @@ module Dalli
178
176
  def servers_with_response(servers, timeout)
179
177
  return [] if servers.empty?
180
178
 
181
- # TODO: - This is a bit challenging. Essentially the PipelinedGetter
182
- # is a reactor, but without the benefit of a Fiber or separate thread.
183
- # My suspicion is that we may want to try and push this down into the
184
- # individual servers, but I'm not sure. For now, we keep the
185
- # mapping between the alerted object (the socket) and the
186
- # corrresponding server here.
187
- server_map = servers.each_with_object({}) { |s, h| h[s.sock] = s }
188
-
189
- readable, = IO.select(server_map.keys, nil, nil, timeout)
179
+ sockets = servers.map(&:sock)
180
+ readable, = IO.select(sockets, nil, nil, timeout)
190
181
  return [] if readable.nil?
191
182
 
192
- readable.map { |sock| server_map[sock] }
183
+ # For typical server counts (1-5), linear scan is faster than
184
+ # building and looking up a hash map
185
+ readable.filter_map { |sock| servers.find { |s| s.sock == sock } }
193
186
  end
194
187
 
195
188
  def groups_for_keys(*keys)
@@ -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)
@@ -91,10 +92,13 @@ module Dalli
91
92
  # repeatedly whenever this server's socket is readable until
92
93
  # #pipeline_complete?.
93
94
  #
94
- # Returns a Hash of kv pairs received.
95
- def pipeline_next_responses
95
+ # When a block is given, yields (key, value, cas) for each response,
96
+ # avoiding intermediate Hash allocation. Returns nil.
97
+ # Without a block, returns a Hash of { key => [value, cas] }.
98
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
99
+ def pipeline_next_responses(&block)
96
100
  reconnect_on_pipeline_complete!
97
- values = {}
101
+ values = nil
98
102
 
99
103
  response_buffer.read
100
104
 
@@ -108,16 +112,24 @@ module Dalli
108
112
 
109
113
  # If the status is ok and the key is not nil, then this is a
110
114
  # getkq response with a value that we want to set in the response hash
111
- values[key] = [value, cas] unless key.nil?
115
+ unless key.nil?
116
+ if block
117
+ yield key, value, cas
118
+ else
119
+ values ||= {}
120
+ values[key] = [value, cas]
121
+ end
122
+ end
112
123
 
113
124
  # Get the next response from the buffer
114
125
  status, cas, key, value = response_buffer.process_single_getk_response
115
126
  end
116
127
 
117
- values
128
+ values || {}
118
129
  rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
119
130
  @connection_manager.error_on_request!(e)
120
131
  end
132
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
121
133
 
122
134
  # Abort current pipelined get. Generally used to signal an external
123
135
  # timeout during pipelined get. The underlying socket is
@@ -141,18 +153,6 @@ module Dalli
141
153
  !response_buffer.in_progress?
142
154
  end
143
155
 
144
- def username
145
- @options[:username] || ENV.fetch('MEMCACHE_USERNAME', nil)
146
- end
147
-
148
- def password
149
- @options[:password] || ENV.fetch('MEMCACHE_PASSWORD', nil)
150
- end
151
-
152
- def require_auth?
153
- !username.nil?
154
- end
155
-
156
156
  def quiet?
157
157
  Thread.current[::Dalli::QUIET]
158
158
  end
@@ -162,6 +162,16 @@ module Dalli
162
162
 
163
163
  private
164
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
+
165
175
  ALLOWED_QUIET_OPS = %i[add replace set delete incr decr append prepend flush noop].freeze
166
176
  private_constant :ALLOWED_QUIET_OPS
167
177
 
@@ -216,8 +226,7 @@ module Dalli
216
226
 
217
227
  def connect
218
228
  @connection_manager.establish_connection
219
- authenticate_connection if require_auth?
220
- @version = version # Connect socket if not authed
229
+ @version = version
221
230
  up!
222
231
  end
223
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,9 @@ 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.'
262
- end
263
-
264
- require_relative 'meta/key_regularizer'
265
- require_relative 'meta/request_formatter'
266
- require_relative 'meta/response_processor'
260
+ require_relative 'key_regularizer'
261
+ require_relative 'request_formatter'
262
+ require_relative 'response_processor'
267
263
  end
268
264
  end
269
265
  end
@@ -7,38 +7,39 @@ module Dalli
7
7
  module Protocol
8
8
  ##
9
9
  # Manages the buffer for responses from memcached.
10
+ # Uses an offset-based approach to avoid string allocations
11
+ # when advancing through parsed responses.
10
12
  ##
11
13
  class ResponseBuffer
14
+ # Compact the buffer when the consumed portion exceeds this
15
+ # threshold and represents more than half the buffer
16
+ COMPACT_THRESHOLD = 4096
17
+
12
18
  def initialize(io_source, response_processor)
13
19
  @io_source = io_source
14
20
  @response_processor = response_processor
15
21
  @buffer = nil
22
+ @offset = 0
16
23
  end
17
24
 
18
25
  def read
19
26
  @buffer << @io_source.read_nonblock
20
27
  end
21
28
 
22
- # Attempts to process a single response from the buffer. Starts
23
- # by advancing the buffer to the specified start position
29
+ # Attempts to process a single response from the buffer,
30
+ # advancing the offset past the consumed bytes.
24
31
  def process_single_getk_response
25
- bytes, status, cas, key, value = @response_processor.getk_response_from_buffer(@buffer)
26
- advance(bytes)
32
+ bytes, status, cas, key, value = @response_processor.getk_response_from_buffer(@buffer, @offset)
33
+ @offset += bytes
34
+ compact_if_needed
27
35
  [status, cas, key, value]
28
36
  end
29
37
 
30
- # Advances the internal response buffer by bytes_to_advance
31
- # bytes. The
32
- def advance(bytes_to_advance)
33
- return unless bytes_to_advance.positive?
34
-
35
- @buffer = @buffer.byteslice(bytes_to_advance..-1)
36
- end
37
-
38
38
  # Resets the internal buffer to an empty state,
39
39
  # so that we're ready to read pipelined responses
40
40
  def reset
41
41
  @buffer = ''.b
42
+ @offset = 0
42
43
  end
43
44
 
44
45
  # Ensures the buffer is initialized for reading without discarding
@@ -48,16 +49,30 @@ module Dalli
48
49
  return if in_progress?
49
50
 
50
51
  @buffer = ''.b
52
+ @offset = 0
51
53
  end
52
54
 
53
55
  # Clear the internal response buffer
54
56
  def clear
55
57
  @buffer = nil
58
+ @offset = 0
56
59
  end
57
60
 
58
61
  def in_progress?
59
62
  !@buffer.nil?
60
63
  end
64
+
65
+ private
66
+
67
+ # Only compact when we've consumed a significant portion of the buffer.
68
+ # This avoids per-response string allocation while preventing unbounded
69
+ # memory growth for large pipelines.
70
+ def compact_if_needed
71
+ return unless @offset > COMPACT_THRESHOLD && @offset > @buffer.bytesize / 2
72
+
73
+ @buffer = @buffer.byteslice(@offset..)
74
+ @offset = 0
75
+ end
61
76
  end
62
77
  end
63
78
  end