dalli 3.1.5 → 3.2.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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."