dalli 3.1.5 → 3.2.6

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.
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dalli
4
+ module Protocol
5
+ class Meta
6
+ ##
7
+ # Class that encapsulates logic for processing meta protocol responses
8
+ # from memcached. Includes logic for pulling data from an IO source
9
+ # and parsing into local values. Handles errors on unexpected values.
10
+ ##
11
+ class ResponseProcessor
12
+ EN = 'EN'
13
+ END_TOKEN = 'END'
14
+ EX = 'EX'
15
+ HD = 'HD'
16
+ MN = 'MN'
17
+ NF = 'NF'
18
+ NS = 'NS'
19
+ OK = 'OK'
20
+ RESET = 'RESET'
21
+ STAT = 'STAT'
22
+ VA = 'VA'
23
+ VERSION = 'VERSION'
24
+
25
+ def initialize(io_source, value_marshaller)
26
+ @io_source = io_source
27
+ @value_marshaller = value_marshaller
28
+ end
29
+
30
+ def meta_get_with_value(cache_nils: false)
31
+ tokens = error_on_unexpected!([VA, EN, HD])
32
+ return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
33
+ return true unless tokens.first == VA
34
+
35
+ @value_marshaller.retrieve(read_line, bitflags_from_tokens(tokens))
36
+ end
37
+
38
+ def meta_get_with_value_and_cas
39
+ tokens = error_on_unexpected!([VA, EN, HD])
40
+ return [nil, 0] if tokens.first == EN
41
+
42
+ cas = cas_from_tokens(tokens)
43
+ return [nil, cas] unless tokens.first == VA
44
+
45
+ [@value_marshaller.retrieve(read_line, bitflags_from_tokens(tokens)), cas]
46
+ end
47
+
48
+ def meta_get_without_value
49
+ tokens = error_on_unexpected!([EN, HD])
50
+ tokens.first == EN ? nil : true
51
+ end
52
+
53
+ def meta_set_with_cas
54
+ tokens = error_on_unexpected!([HD, NS, NF, EX])
55
+ return false unless tokens.first == HD
56
+
57
+ cas_from_tokens(tokens)
58
+ end
59
+
60
+ def meta_set_append_prepend
61
+ tokens = error_on_unexpected!([HD, NS, NF, EX])
62
+ return false unless tokens.first == HD
63
+
64
+ true
65
+ end
66
+
67
+ def meta_delete
68
+ tokens = error_on_unexpected!([HD, NF, EX])
69
+ tokens.first == HD
70
+ end
71
+
72
+ def decr_incr
73
+ tokens = error_on_unexpected!([VA, NF, NS, EX])
74
+ return false if [NS, EX].include?(tokens.first)
75
+ return nil if tokens.first == NF
76
+
77
+ read_line.to_i
78
+ end
79
+
80
+ def stats
81
+ tokens = error_on_unexpected!([END_TOKEN, STAT])
82
+ values = {}
83
+ while tokens.first != END_TOKEN
84
+ values[tokens[1]] = tokens[2]
85
+ tokens = next_line_to_tokens
86
+ end
87
+ values
88
+ end
89
+
90
+ def flush
91
+ error_on_unexpected!([OK])
92
+
93
+ true
94
+ end
95
+
96
+ def reset
97
+ error_on_unexpected!([RESET])
98
+
99
+ true
100
+ end
101
+
102
+ def version
103
+ tokens = error_on_unexpected!([VERSION])
104
+ tokens.last
105
+ end
106
+
107
+ def consume_all_responses_until_mn
108
+ tokens = next_line_to_tokens
109
+
110
+ tokens = next_line_to_tokens while tokens.first != MN
111
+ true
112
+ end
113
+
114
+ def tokens_from_header_buffer(buf)
115
+ header = header_from_buffer(buf)
116
+ tokens = header.split
117
+ header_len = header.bytesize + TERMINATOR.length
118
+ body_len = body_len_from_tokens(tokens)
119
+ [tokens, header_len, body_len]
120
+ end
121
+
122
+ def full_response_from_buffer(tokens, body, resp_size)
123
+ value = @value_marshaller.retrieve(body, bitflags_from_tokens(tokens))
124
+ [resp_size, tokens.first == VA, cas_from_tokens(tokens), key_from_tokens(tokens), value]
125
+ end
126
+
127
+ ##
128
+ # This method returns an array of values used in a pipelined
129
+ # getk process. The first value is the number of bytes by
130
+ # which to advance the pointer in the buffer. If the
131
+ # complete response is found in the buffer, this will
132
+ # be the response size. Otherwise it is zero.
133
+ #
134
+ # The remaining three values in the array are the ResponseHeader,
135
+ # key, and value.
136
+ ##
137
+ def getk_response_from_buffer(buf)
138
+ # There's no header in the buffer, so don't advance
139
+ return [0, nil, nil, nil, nil] unless contains_header?(buf)
140
+
141
+ tokens, header_len, body_len = tokens_from_header_buffer(buf)
142
+
143
+ # We have a complete response that has no body.
144
+ # This is either the response to the terminating
145
+ # noop or, if the status is not MN, an intermediate
146
+ # error response that needs to be discarded.
147
+ return [header_len, true, nil, nil, nil] if body_len.zero?
148
+
149
+ resp_size = header_len + body_len + TERMINATOR.length
150
+ # The header is in the buffer, but the body is not. As we don't have
151
+ # a complete response, don't advance the buffer
152
+ return [0, nil, nil, nil, nil] unless buf.bytesize >= resp_size
153
+
154
+ # The full response is in our buffer, so parse it and return
155
+ # the values
156
+ body = buf.slice(header_len, body_len)
157
+ full_response_from_buffer(tokens, body, resp_size)
158
+ end
159
+
160
+ def contains_header?(buf)
161
+ buf.include?(TERMINATOR)
162
+ end
163
+
164
+ def header_from_buffer(buf)
165
+ buf.split(TERMINATOR, 2).first
166
+ end
167
+
168
+ def error_on_unexpected!(expected_codes)
169
+ tokens = next_line_to_tokens
170
+ raise Dalli::DalliError, "Response error: #{tokens.first}" unless expected_codes.include?(tokens.first)
171
+
172
+ tokens
173
+ end
174
+
175
+ def bitflags_from_tokens(tokens)
176
+ value_from_tokens(tokens, 'f')&.to_i
177
+ end
178
+
179
+ def cas_from_tokens(tokens)
180
+ value_from_tokens(tokens, 'c')&.to_i
181
+ end
182
+
183
+ def key_from_tokens(tokens)
184
+ encoded_key = value_from_tokens(tokens, 'k')
185
+ base64_encoded = tokens.any?('b')
186
+ KeyRegularizer.decode(encoded_key, base64_encoded)
187
+ end
188
+
189
+ def body_len_from_tokens(tokens)
190
+ value_from_tokens(tokens, 's')&.to_i
191
+ end
192
+
193
+ def value_from_tokens(tokens, flag)
194
+ bitflags_token = tokens.find { |t| t.start_with?(flag) }
195
+ return 0 unless bitflags_token
196
+
197
+ bitflags_token[1..]
198
+ end
199
+
200
+ def read_line
201
+ @io_source.read_line&.chomp!(TERMINATOR)
202
+ end
203
+
204
+ def next_line_to_tokens
205
+ line = read_line
206
+ line&.split || []
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'socket'
5
+ require 'timeout'
6
+
7
+ module Dalli
8
+ module Protocol
9
+ ##
10
+ # Access point for a single Memcached server, accessed via Memcached's meta
11
+ # protocol. Contains logic for managing connection state to the server (retries, etc),
12
+ # formatting requests to the server, and unpacking responses.
13
+ ##
14
+ class Meta < Base
15
+ TERMINATOR = "\r\n"
16
+
17
+ def response_processor
18
+ @response_processor ||= ResponseProcessor.new(@connection_manager, @value_marshaller)
19
+ end
20
+
21
+ # NOTE: Additional public methods should be overridden in Dalli::Threadsafe
22
+
23
+ private
24
+
25
+ # Retrieval Commands
26
+ def get(key, options = nil)
27
+ encoded_key, base64 = KeyRegularizer.encode(key)
28
+ req = RequestFormatter.meta_get(key: encoded_key, base64: base64)
29
+ write(req)
30
+ response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
31
+ end
32
+
33
+ def quiet_get_request(key)
34
+ encoded_key, base64 = KeyRegularizer.encode(key)
35
+ RequestFormatter.meta_get(key: encoded_key, return_cas: true, base64: base64, quiet: true)
36
+ end
37
+
38
+ def gat(key, ttl, options = nil)
39
+ ttl = TtlSanitizer.sanitize(ttl)
40
+ encoded_key, base64 = KeyRegularizer.encode(key)
41
+ req = RequestFormatter.meta_get(key: encoded_key, ttl: ttl, base64: base64)
42
+ write(req)
43
+ response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
44
+ end
45
+
46
+ def touch(key, ttl)
47
+ ttl = TtlSanitizer.sanitize(ttl)
48
+ encoded_key, base64 = KeyRegularizer.encode(key)
49
+ req = RequestFormatter.meta_get(key: encoded_key, ttl: ttl, value: false, base64: base64)
50
+ write(req)
51
+ response_processor.meta_get_without_value
52
+ end
53
+
54
+ # TODO: This is confusing, as there's a cas command in memcached
55
+ # and this isn't it. Maybe rename? Maybe eliminate?
56
+ def cas(key)
57
+ encoded_key, base64 = KeyRegularizer.encode(key)
58
+ req = RequestFormatter.meta_get(key: encoded_key, value: true, return_cas: true, base64: base64)
59
+ write(req)
60
+ response_processor.meta_get_with_value_and_cas
61
+ end
62
+
63
+ # Storage Commands
64
+ def set(key, value, ttl, cas, options)
65
+ write_storage_req(:set, key, value, ttl, cas, options)
66
+ response_processor.meta_set_with_cas unless quiet?
67
+ end
68
+
69
+ def add(key, value, ttl, options)
70
+ write_storage_req(:add, key, value, ttl, nil, options)
71
+ response_processor.meta_set_with_cas unless quiet?
72
+ end
73
+
74
+ def replace(key, value, ttl, cas, options)
75
+ write_storage_req(:replace, key, value, ttl, cas, options)
76
+ response_processor.meta_set_with_cas unless quiet?
77
+ end
78
+
79
+ # rubocop:disable Metrics/ParameterLists
80
+ def write_storage_req(mode, key, raw_value, ttl = nil, cas = nil, options = {})
81
+ (value, bitflags) = @value_marshaller.store(key, raw_value, options)
82
+ ttl = TtlSanitizer.sanitize(ttl) if ttl
83
+ encoded_key, base64 = KeyRegularizer.encode(key)
84
+ req = RequestFormatter.meta_set(key: encoded_key, value: value,
85
+ bitflags: bitflags, cas: cas,
86
+ ttl: ttl, mode: mode, quiet: quiet?, base64: base64)
87
+ write(req)
88
+ end
89
+ # rubocop:enable Metrics/ParameterLists
90
+
91
+ def append(key, value)
92
+ write_append_prepend_req(:append, key, value)
93
+ response_processor.meta_set_append_prepend unless quiet?
94
+ end
95
+
96
+ def prepend(key, value)
97
+ write_append_prepend_req(:prepend, key, value)
98
+ response_processor.meta_set_append_prepend unless quiet?
99
+ end
100
+
101
+ # rubocop:disable Metrics/ParameterLists
102
+ def write_append_prepend_req(mode, key, value, ttl = nil, cas = nil, _options = {})
103
+ ttl = TtlSanitizer.sanitize(ttl) if ttl
104
+ encoded_key, base64 = KeyRegularizer.encode(key)
105
+ req = RequestFormatter.meta_set(key: encoded_key, value: value, base64: base64,
106
+ cas: cas, ttl: ttl, mode: mode, quiet: quiet?)
107
+ write(req)
108
+ end
109
+ # rubocop:enable Metrics/ParameterLists
110
+
111
+ # Delete Commands
112
+ def delete(key, cas)
113
+ encoded_key, base64 = KeyRegularizer.encode(key)
114
+ req = RequestFormatter.meta_delete(key: encoded_key, cas: cas,
115
+ base64: base64, quiet: quiet?)
116
+ write(req)
117
+ response_processor.meta_delete unless quiet?
118
+ end
119
+
120
+ # Arithmetic Commands
121
+ def decr(key, count, ttl, initial)
122
+ decr_incr false, key, count, ttl, initial
123
+ end
124
+
125
+ def incr(key, count, ttl, initial)
126
+ decr_incr true, key, count, ttl, initial
127
+ end
128
+
129
+ def decr_incr(incr, key, delta, ttl, initial)
130
+ ttl = initial ? TtlSanitizer.sanitize(ttl) : nil # Only set a TTL if we want to set a value on miss
131
+ encoded_key, base64 = KeyRegularizer.encode(key)
132
+ write(RequestFormatter.meta_arithmetic(key: encoded_key, delta: delta, initial: initial, incr: incr, ttl: ttl,
133
+ quiet: quiet?, base64: base64))
134
+ response_processor.decr_incr unless quiet?
135
+ end
136
+
137
+ # Other Commands
138
+ def flush(delay = 0)
139
+ write(RequestFormatter.flush(delay: delay))
140
+ response_processor.flush unless quiet?
141
+ end
142
+
143
+ # Noop is a keepalive operation but also used to demarcate the end of a set of pipelined commands.
144
+ # We need to read all the responses at once.
145
+ def noop
146
+ write_noop
147
+ response_processor.consume_all_responses_until_mn
148
+ end
149
+
150
+ def stats(info = nil)
151
+ write(RequestFormatter.stats(info))
152
+ response_processor.stats
153
+ end
154
+
155
+ def reset_stats
156
+ write(RequestFormatter.stats('reset'))
157
+ response_processor.reset
158
+ end
159
+
160
+ def version
161
+ write(RequestFormatter.version)
162
+ response_processor.version
163
+ end
164
+
165
+ def write_noop
166
+ write(RequestFormatter.meta_noop)
167
+ end
168
+
169
+ def authenticate_connection
170
+ raise Dalli::DalliError, 'Authentication not supported for the meta protocol.'
171
+ end
172
+
173
+ require_relative 'meta/key_regularizer'
174
+ require_relative 'meta/request_formatter'
175
+ require_relative 'meta/response_processor'
176
+ end
177
+ end
178
+ end
@@ -12,6 +12,7 @@ module Dalli
12
12
  def initialize(io_source, response_processor)
13
13
  @io_source = io_source
14
14
  @response_processor = response_processor
15
+ @buffer = nil
15
16
  end
16
17
 
17
18
  def read
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'uri'
4
+
3
5
  module Dalli
4
6
  module Protocol
5
7
  ##
@@ -1,8 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'timeout'
4
+
3
5
  module Dalli
4
6
  module Protocol
5
7
  # Preserved for backwards compatibility. Should be removed in 4.0
6
8
  NOT_FOUND = ::Dalli::NOT_FOUND
9
+
10
+ # Ruby 3.2 raises IO::TimeoutError on blocking reads/writes, but
11
+ # it is not defined in earlier Ruby versions.
12
+ TIMEOUT_ERRORS =
13
+ if defined?(IO::TimeoutError)
14
+ [Timeout::Error, IO::TimeoutError]
15
+ else
16
+ [Timeout::Error]
17
+ end
7
18
  end
8
19
  end
data/lib/dalli/ring.rb CHANGED
@@ -23,8 +23,10 @@ module Dalli
23
23
 
24
24
  attr_accessor :servers, :continuum
25
25
 
26
- def initialize(servers, options)
27
- @servers = servers
26
+ def initialize(servers_arg, protocol_implementation, options)
27
+ @servers = servers_arg.map do |s|
28
+ protocol_implementation.new(s, options)
29
+ end
28
30
  @continuum = nil
29
31
  @continuum = build_continuum(servers) if servers.size > 1
30
32
 
@@ -40,7 +40,7 @@ module Dalli
40
40
  def self.apply_defaults(arg)
41
41
  return arg unless arg.nil?
42
42
 
43
- ENV[ENV_VAR_NAME] || DEFAULT_SERVERS
43
+ ENV.fetch(ENV_VAR_NAME, nil) || DEFAULT_SERVERS
44
44
  end
45
45
 
46
46
  def self.validate_type(arg)
data/lib/dalli/socket.rb CHANGED
@@ -13,7 +13,7 @@ module Dalli
13
13
  ##
14
14
  module InstanceMethods
15
15
  def readfull(count)
16
- value = +''
16
+ value = String.new(capacity: count + 1)
17
17
  loop do
18
18
  result = read_nonblock(count - value.bytesize, exception: false)
19
19
  value << result if append_to_buffer?(result)
data/lib/dalli/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dalli
4
- VERSION = '3.1.5'
4
+ VERSION = '3.2.6'
5
5
 
6
6
  MIN_SUPPORTED_MEMCACHED_VERSION = '1.4'
7
7
  end
data/lib/dalli.rb CHANGED
@@ -65,6 +65,7 @@ require_relative 'dalli/protocol'
65
65
  require_relative 'dalli/protocol/base'
66
66
  require_relative 'dalli/protocol/binary'
67
67
  require_relative 'dalli/protocol/connection_manager'
68
+ require_relative 'dalli/protocol/meta'
68
69
  require_relative 'dalli/protocol/response_buffer'
69
70
  require_relative 'dalli/protocol/server_config_parser'
70
71
  require_relative 'dalli/protocol/ttl_sanitizer'
@@ -82,6 +82,9 @@ module Rack
82
82
  def write_session(_req, sid, session, options)
83
83
  return false unless sid
84
84
 
85
+ key = memcached_key_from_sid(sid)
86
+ return false unless key
87
+
85
88
  with_dalli_client(false) do |dc|
86
89
  dc.set(memcached_key_from_sid(sid), session, ttl(options[:expire_after]))
87
90
  sid
@@ -90,7 +93,8 @@ module Rack
90
93
 
91
94
  def delete_session(_req, sid, options)
92
95
  with_dalli_client do |dc|
93
- dc.delete(memcached_key_from_sid(sid))
96
+ key = memcached_key_from_sid(sid)
97
+ dc.delete(key) if key
94
98
  generate_sid_with(dc) unless options[:drop]
95
99
  end
96
100
  end
@@ -98,20 +102,24 @@ module Rack
98
102
  private
99
103
 
100
104
  def memcached_key_from_sid(sid)
101
- sid.private_id
105
+ sid.private_id if sid.respond_to?(:private_id)
102
106
  end
103
107
 
104
108
  def existing_session_for_sid(client, sid)
105
109
  return nil unless sid && !sid.empty?
106
110
 
107
- client.get(memcached_key_from_sid(sid))
111
+ key = memcached_key_from_sid(sid)
112
+ return nil if key.nil?
113
+
114
+ client.get(key)
108
115
  end
109
116
 
110
117
  def create_sid_with_empty_session(client)
111
118
  loop do
112
119
  sid = generate_sid_with(client)
120
+ key = memcached_key_from_sid(sid)
113
121
 
114
- break sid if client.add(memcached_key_from_sid(sid), {}, @default_ttl)
122
+ break sid if key && client.add(key, {}, @default_ttl)
115
123
  end
116
124
  end
117
125
 
@@ -119,7 +127,8 @@ module Rack
119
127
  loop do
120
128
  raw_sid = generate_sid
121
129
  sid = raw_sid.is_a?(String) ? Rack::Session::SessionId.new(raw_sid) : raw_sid
122
- break sid unless client.get(memcached_key_from_sid(sid))
130
+ key = memcached_key_from_sid(sid)
131
+ break sid unless key && client.get(key)
123
132
  end
124
133
  end
125
134
 
@@ -161,7 +170,7 @@ module Rack
161
170
  def ensure_connection_pool_added!
162
171
  require 'connection_pool'
163
172
  rescue LoadError => e
164
- warn "You don't have connection_pool installed in your application. "\
173
+ warn "You don't have connection_pool installed in your application. " \
165
174
  'Please add it to your Gemfile and run bundle install'
166
175
  raise e
167
176
  end
@@ -169,7 +178,7 @@ module Rack
169
178
  def with_dalli_client(result_on_error = nil, &block)
170
179
  @data.with(&block)
171
180
  rescue ::Dalli::DalliError, Errno::ECONNREFUSED
172
- raise if /undefined class/.match?($ERROR_INFO.message)
181
+ raise if $ERROR_INFO.message.include?('undefined class')
173
182
 
174
183
  if $VERBOSE
175
184
  warn "#{self} is unable to find memcached server."