dalli 2.6.4 → 2.7.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of dalli might be problematic. Click here for more details.

@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4e1d2bd1cdadde91a31a3e1311e4189e54b57623
4
+ data.tar.gz: 6e2fbab2dd27e1cf5f62eef39d7292d1ac6f93b8
5
+ SHA512:
6
+ metadata.gz: ad6caea0b89835f15f7ddc4fd4ecab66eaa87ed969020c41fae9324da520b3e9f3b15afe53d1b14e515eafcb29cbc76c62267ce12a8c708f525ab2863e62f682
7
+ data.tar.gz: 8646c41bab51faa972bd16c5e06157c1c201a862f911f3f961b252ce9fd96c1b583345068090b2606f1a302f5d106df072eb7a91acb2ecb1c73464b3ca2f6f58
data/Gemfile CHANGED
@@ -5,6 +5,7 @@ gemspec
5
5
  gem 'rake'
6
6
  gem 'kgio', :platform => :mri
7
7
  gem 'appraisal'
8
+ gem 'connection_pool'
8
9
 
9
10
  group :test do
10
11
  gem 'simplecov'
data/History.md CHANGED
@@ -1,11 +1,35 @@
1
1
  Dalli Changelog
2
2
  =====================
3
3
 
4
- HEAD
4
+ 2.7.0
5
+ ==========
6
+
7
+ - Multithreading support with dalli\_store:
8
+ Use :pool\_size to create a pool of shared, threadsafe Dalli clients in Rails:
9
+ ```ruby
10
+ config.cache_store = :dalli_store, "cache-1.example.com", "cache-2.example.com", :compress => true, :pool_size => 5, :expires_in => 300
11
+ ```
12
+ This will ensure the Rails.cache singleton does not become a source of contention.
13
+ **PLEASE NOTE** Rails's :mem\_cache\_store does not support pooling as of
14
+ Rails 4.0. You must use :dalli\_store.
15
+
16
+ - Implement `version` for retrieving version of connected servers [dterei, #384]
17
+ - Implement `fetch_multi` for batched read/write [sorentwo, #380]
18
+ - Add more support for safe updates with multiple writers: [philipmw, #395]
19
+ `require 'dalli/cas/client'` augments Dalli::Client with the following methods:
20
+ * Get value with CAS: `[value, cas] = get_cas(key)`
21
+ `get_cas(key) {|value, cas| ...}`
22
+ * Get multiple values with CAS: `get_multi_cas(k1, k2, ...) {|value, metadata| cas = metadata[:cas]}`
23
+ * Set value with CAS: `new_cas = set_cas(key, value, cas, ttl, options)`
24
+ * Replace value with CAS: `replace_cas(key, new_value, cas, ttl, options)`
25
+ * Delete value with CAS: `delete_cas(key, cas)`
26
+ - Fix bug with get key with "Not found" value [uzzz, #375]
27
+
28
+ 2.6.4
5
29
  =======
6
30
 
7
31
  - Fix ADD command, aka `write(unless_exist: true)` (pitr, #365)
8
- - Upgrade test suite from mini_shoulda to minitest.
32
+ - Upgrade test suite from mini\_shoulda to minitest.
9
33
  - Even more performance improvements for get\_multi (xaop, #331)
10
34
 
11
35
  2.6.3
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2012 Mike Perham
1
+ Copyright (c) Mike Perham
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- Dalli [![Build Status](https://secure.travis-ci.org/mperham/dalli.png)](http://travis-ci.org/mperham/dalli) [![Dependency Status](https://gemnasium.com/mperham/dalli.png)](https://gemnasium.com/mperham/dalli)
1
+ Dalli [![Build Status](https://secure.travis-ci.org/mperham/dalli.png)](http://travis-ci.org/mperham/dalli) [![Dependency Status](https://gemnasium.com/mperham/dalli.png)](https://gemnasium.com/mperham/dalli) [![Code Climate](https://codeclimate.com/github/mperham/dalli.png)](https://codeclimate.com/github/mperham/dalli)
2
2
  =====
3
3
 
4
4
  Dalli is a high performance pure Ruby client for accessing memcached servers. It works with memcached 1.4+ only as it uses the newer binary protocol. It should be considered a replacement for the memcache-client gem.
@@ -35,8 +35,7 @@ Supported Ruby versions and implementations
35
35
  Dalli should work identically on:
36
36
 
37
37
  * JRuby 1.6+
38
- * Ruby 1.9.2+
39
- * Ruby 1.8.7+
38
+ * Ruby 1.9.3+
40
39
  * Rubinius 2.0
41
40
 
42
41
  If you have problems, please enter an issue.
@@ -73,7 +72,7 @@ Dalli has no runtime dependencies and never will. You can optionally install th
73
72
  give Dalli a 20-30% performance boost.
74
73
 
75
74
 
76
- Usage with Rails 3.x
75
+ Usage with Rails 3.x and 4.x
77
76
  ---------------------------
78
77
 
79
78
  In your Gemfile:
@@ -113,8 +112,22 @@ Rails.application.config.session_store :dalli_store, :memcache_server => ['host1
113
112
  Dalli does not support Rails 2.x.
114
113
 
115
114
 
115
+ Multithreading and Rails
116
+ --------------------------
117
+
118
+ If you use Puma or another threaded app server, as of Dalli 2.7, you can use a pool
119
+ of Dalli clients with Rails to ensure the `Rails.cache` singleton does not become a
120
+ source of thread contention. You must add `gem 'connection_pool'` to your Gemfile and
121
+ add :pool\_size to your `dalli_store` config:
122
+
123
+ ```ruby
124
+ config.cache_store = :dalli_store, 'cache-1.example.com', { :pool_size => 5 }
125
+ ```
126
+
127
+
116
128
  Configuration
117
129
  ------------------------
130
+
118
131
  Dalli::Client accepts the following options. All times are in seconds.
119
132
 
120
133
  **expires_in**: Global default for key TTL. Default is 0, which means no expiry.
@@ -161,13 +174,14 @@ socket.
161
174
 
162
175
  Note that Dalli does not require ActiveSupport or Rails. You can safely use it in your own Ruby projects.
163
176
 
164
- [View the API](http://www.ruby-doc.org/gems/docs/d/dalli-2.6.2/Dalli/Client.html)
177
+ [View the Client API](http://www.rubydoc.info/github/mperham/dalli/Dalli/Client)
165
178
 
166
179
  Helping Out
167
180
  -------------
168
181
 
169
182
  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 History.md with a one sentence description of your fix so you get credit as a contributor.
170
183
 
184
+ We're not accepting new compressors. They are trivial to add in an initializer. See #385 (LZ4), #406 (Snappy)
171
185
 
172
186
  Thanks
173
187
  ------------
@@ -182,12 +196,10 @@ Brian Mitchell - for his remix-stash project which was helpful when implementing
182
196
  Author
183
197
  ----------
184
198
 
185
- Mike Perham, mperham@gmail.com, [mikeperham.com](http://mikeperham.com), [@mperham](http://twitter.com/mperham) If you like and use this project, please give me a recommendation at [WWR](http://workingwithrails.com/person/10797-mike-perham) or send a few bucks my way via my Pledgie page below. Happy caching!
186
-
187
- <a href='http://www.pledgie.com/campaigns/16623'><img alt='Click here to lend your support to Open Source and make a donation at www.pledgie.com !' src='http://www.pledgie.com/campaigns/16623.png?skin_name=chrome' border='0' /></a>
199
+ Mike Perham, [mikeperham.com](http://mikeperham.com), [@mperham](http://twitter.com/mperham)
188
200
 
189
201
 
190
202
  Copyright
191
203
  -----------
192
204
 
193
- Copyright (c) 2012 Mike Perham. See LICENSE for details.
205
+ Copyright (c) Mike Perham. See LICENSE for details.
@@ -22,7 +22,7 @@ Gem::Specification.new do |s|
22
22
  s.require_paths = ["lib"]
23
23
  s.summary = %q{High performance memcached client for Ruby}
24
24
  s.test_files = Dir.glob("test/**/*")
25
- s.add_development_dependency(%q<minitest>, [">= 5.0.0"])
25
+ s.add_development_dependency(%q<minitest>, [">= 4.2.0"])
26
26
  s.add_development_dependency(%q<mocha>, [">= 0"])
27
27
  s.add_development_dependency(%q<rails>, ["~> 3"])
28
28
  end
@@ -33,29 +33,52 @@ module ActiveSupport
33
33
  # If no addresses are specified, then DalliStore will connect to
34
34
  # localhost port 11211 (the default memcached port).
35
35
  #
36
+ # Connection Pool support
37
+ #
38
+ # If you are using multithreaded Rails, the Rails.cache singleton can become a source
39
+ # of contention. You can use a connection pool of Dalli clients with Rails.cache by
40
+ # passing :pool_size and/or :pool_timeout:
41
+ #
42
+ # config.cache_store = :dalli_store, 'localhost:11211', :pool_size => 10
43
+ #
44
+ # Both pool options default to 5. You must include the `connection_pool` gem if you
45
+ # wish to use pool support.
46
+ #
36
47
  def initialize(*addresses)
37
48
  addresses = addresses.flatten
38
49
  options = addresses.extract_options!
39
50
  @options = options.dup
51
+
52
+ pool_options = {}
53
+ pool_options[:size] = options[:pool_size] if options[:pool_size]
54
+ pool_options[:timeout] = options[:pool_timeout] if options[:pool_timeout]
55
+
40
56
  @options[:compress] ||= @options[:compression]
41
- @raise_errors = !!@options[:raise_errors]
42
57
  servers = if addresses.empty?
43
58
  nil # use the default from Dalli::Client
44
59
  else
45
60
  addresses
46
61
  end
47
- @data = Dalli::Client.new(servers, @options)
62
+ if pool_options.empty?
63
+ @data = Dalli::Client.new(servers, @options)
64
+ else
65
+ @data = ::ConnectionPool.new(pool_options) { Dalli::Client.new(servers, @options.merge(:threadsafe => false)) }
66
+ end
48
67
 
49
68
  extend Strategy::LocalCache
50
69
  end
51
70
 
52
71
  ##
53
- # Access the underlying Dalli::Client instance for
72
+ # Access the underlying Dalli::Client or ConnectionPool instance for
54
73
  # access to get_multi, etc.
55
74
  def dalli
56
75
  @data
57
76
  end
58
77
 
78
+ def with(&block)
79
+ @data.with(&block)
80
+ end
81
+
59
82
  def fetch(name, options=nil)
60
83
  options ||= {}
61
84
  name = expanded_key name
@@ -103,7 +126,10 @@ module ActiveSupport
103
126
  name = expanded_key name
104
127
 
105
128
  instrument(:write, name, options) do |payload|
106
- write_entry(name, value, options)
129
+ with do |connection|
130
+ options = options.merge(:connection => connection)
131
+ write_entry(name, value, options)
132
+ end
107
133
  end
108
134
  end
109
135
 
@@ -139,7 +165,8 @@ module ActiveSupport
139
165
  end
140
166
  end
141
167
 
142
- results.merge!(@data.get_multi(mapping.keys - results.keys))
168
+ data = with { |c| c.get_multi(mapping.keys - results.keys) }
169
+ results.merge!(data)
143
170
  results.inject({}) do |memo, (inner, _)|
144
171
  entry = results[inner]
145
172
  # NB Backwards data compatibility, to be removed at some point
@@ -151,6 +178,35 @@ module ActiveSupport
151
178
  end
152
179
  end
153
180
 
181
+ # Fetches data from the cache, using the given keys. If there is data in
182
+ # the cache with the given keys, then that data is returned. Otherwise,
183
+ # the supplied block is called for each key for which there was no data,
184
+ # and the result will be written to the cache and returned.
185
+ def fetch_multi(*names)
186
+ options = names.extract_options!
187
+ mapping = names.inject({}) { |memo, name| memo[expanded_key(name)] = name; memo }
188
+
189
+ instrument(:fetch_multi, names) do
190
+ with do |connection|
191
+ results = connection.get_multi(mapping.keys)
192
+
193
+ connection.multi do
194
+ mapping.inject({}) do |memo, (expanded, name)|
195
+ memo[name] = results[expanded]
196
+ if memo[name].nil?
197
+ value = yield(name)
198
+ memo[name] = value
199
+ options = options.merge(:connection => connection)
200
+ write_entry(expanded, value, options)
201
+ end
202
+
203
+ memo
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
209
+
154
210
  # Increment a cached value. This method uses the memcached incr atomic
155
211
  # operator and can only be used on values written with the :raw option.
156
212
  # Calling it on a value not stored with :raw will fail.
@@ -162,11 +218,11 @@ module ActiveSupport
162
218
  initial = options.has_key?(:initial) ? options[:initial] : amount
163
219
  expires_in = options[:expires_in]
164
220
  instrument(:increment, name, :amount => amount) do
165
- @data.incr(name, amount, expires_in, initial)
221
+ with { |c| c.incr(name, amount, expires_in, initial) }
166
222
  end
167
223
  rescue Dalli::DalliError => e
168
224
  logger.error("DalliError: #{e.message}") if logger
169
- raise if @raise_errors
225
+ raise if raise_errors?
170
226
  nil
171
227
  end
172
228
 
@@ -181,11 +237,11 @@ module ActiveSupport
181
237
  initial = options.has_key?(:initial) ? options[:initial] : 0
182
238
  expires_in = options[:expires_in]
183
239
  instrument(:decrement, name, :amount => amount) do
184
- @data.decr(name, amount, expires_in, initial)
240
+ with { |c| c.decr(name, amount, expires_in, initial) }
185
241
  end
186
242
  rescue Dalli::DalliError => e
187
243
  logger.error("DalliError: #{e.message}") if logger
188
- raise if @raise_errors
244
+ raise if raise_errors?
189
245
  nil
190
246
  end
191
247
 
@@ -193,11 +249,11 @@ module ActiveSupport
193
249
  # be used with care when using a shared cache.
194
250
  def clear(options=nil)
195
251
  instrument(:clear, 'flushing all keys') do
196
- @data.flush_all
252
+ with { |c| c.flush_all }
197
253
  end
198
254
  rescue Dalli::DalliError => e
199
255
  logger.error("DalliError: #{e.message}") if logger
200
- raise if @raise_errors
256
+ raise if raise_errors?
201
257
  nil
202
258
  end
203
259
 
@@ -207,11 +263,11 @@ module ActiveSupport
207
263
 
208
264
  # Get the statistics from the memcached servers.
209
265
  def stats
210
- @data.stats
266
+ with { |c| c.stats }
211
267
  end
212
268
 
213
269
  def reset
214
- @data.reset
270
+ with { |c| c.reset }
215
271
  end
216
272
 
217
273
  def logger
@@ -226,12 +282,12 @@ module ActiveSupport
226
282
 
227
283
  # Read an entry from the cache.
228
284
  def read_entry(key, options) # :nodoc:
229
- entry = @data.get(key, options)
285
+ entry = with { |c| c.get(key, options) }
230
286
  # NB Backwards data compatibility, to be removed at some point
231
287
  entry.is_a?(ActiveSupport::Cache::Entry) ? entry.value : entry
232
288
  rescue Dalli::DalliError => e
233
289
  logger.error("DalliError: #{e.message}") if logger
234
- raise if @raise_errors
290
+ raise if raise_errors?
235
291
  nil
236
292
  end
237
293
 
@@ -241,19 +297,20 @@ module ActiveSupport
241
297
  cleanup if options[:unless_exist]
242
298
  method = options[:unless_exist] ? :add : :set
243
299
  expires_in = options[:expires_in]
244
- @data.send(method, key, value, expires_in, options)
300
+ connection = options.delete(:connection)
301
+ connection.send(method, key, value, expires_in, options)
245
302
  rescue Dalli::DalliError => e
246
303
  logger.error("DalliError: #{e.message}") if logger
247
- raise if @raise_errors
304
+ raise if raise_errors?
248
305
  false
249
306
  end
250
307
 
251
308
  # Delete an entry from the cache.
252
309
  def delete_entry(key, options) # :nodoc:
253
- @data.delete(key)
310
+ with { |c| c.delete(key) }
254
311
  rescue Dalli::DalliError => e
255
312
  logger.error("DalliError: #{e.message}") if logger
256
- raise if @raise_errors
313
+ raise if raise_errors?
257
314
  false
258
315
  end
259
316
 
@@ -296,6 +353,9 @@ module ActiveSupport
296
353
  logger.debug("Cache #{operation}: #{key}#{options.blank? ? "" : " (#{options.inspect})"}")
297
354
  end
298
355
 
356
+ def raise_errors?
357
+ !!@options[:raise_errors]
358
+ end
299
359
  end
300
360
  end
301
361
  end
@@ -0,0 +1,58 @@
1
+ require 'dalli/client'
2
+
3
+ module Dalli
4
+ class Client
5
+ ##
6
+ # Get the value and CAS ID associated with the key. If a block is provided,
7
+ # value and CAS will be passed to the block.
8
+ def get_cas(key)
9
+ (value, cas) = perform(:cas, key)
10
+ value = (!value || value == 'Not found') ? nil : value
11
+ if block_given?
12
+ yield value, cas
13
+ else
14
+ [value, cas]
15
+ end
16
+ end
17
+
18
+ ##
19
+ # Fetch multiple keys efficiently, including available metadata such as CAS.
20
+ # If a block is given, yields key/data pairs one a time. Data is an array:
21
+ # [value, cas_id]
22
+ # If no block is given, returns a hash of
23
+ # { 'key' => [value, cas_id] }
24
+ def get_multi_cas(*keys)
25
+ if block_given?
26
+ get_multi_yielder(keys) {|*args| yield(*args)}
27
+ else
28
+ Hash.new.tap do |hash|
29
+ get_multi_yielder(keys) {|k, data| hash[k] = data}
30
+ end
31
+ end
32
+ end
33
+
34
+ ##
35
+ # Set the key-value pair, verifying existing CAS.
36
+ # Returns the resulting CAS value if succeeded, and false otherwise.
37
+ def set_cas(key, value, cas, ttl=nil, options=nil)
38
+ ttl ||= @options[:expires_in].to_i
39
+ perform(:set, key, value, ttl, cas, options)
40
+ end
41
+
42
+ ##
43
+ # Conditionally add a key/value pair, verifying existing CAS, only if the
44
+ # key already exists on the server. Returns the new CAS value if the
45
+ # operation succeeded, or false otherwise.
46
+ def replace_cas(key, value, cas, ttl=nil, options=nil)
47
+ ttl ||= @options[:expires_in].to_i
48
+ perform(:replace, key, value, ttl, cas, options)
49
+ end
50
+
51
+ # Delete a key/value pair, verifying existing CAS.
52
+ # Returns true if succeeded, and false otherwise.
53
+ def delete_cas(key, cas=0)
54
+ perform(:delete, key, cas)
55
+ end
56
+
57
+ end
58
+ end
@@ -31,17 +31,6 @@ module Dalli
31
31
  @ring = nil
32
32
  end
33
33
 
34
- ##
35
- # Normalizes the argument into an array of servers. If the argument is a string, it's expected to be of
36
- # the format "memcache1.example.com:11211[,memcache2.example.com:11211[,memcache3.example.com:11211[...]]]
37
- def normalize_servers(servers)
38
- if servers.is_a? String
39
- return servers.split(",")
40
- else
41
- return servers
42
- end
43
- end
44
-
45
34
  #
46
35
  # The standard memcached instruction set
47
36
  #
@@ -58,107 +47,22 @@ module Dalli
58
47
  Thread.current[:dalli_multi] = old
59
48
  end
60
49
 
50
+ ##
51
+ # Get the value associated with the key.
61
52
  def get(key, options=nil)
62
- resp = perform(:get, key)
63
- resp.nil? || 'Not found' == resp ? nil : resp
53
+ perform(:get, key)
64
54
  end
65
55
 
66
56
  ##
67
57
  # Fetch multiple keys efficiently.
68
- # Returns a hash of { 'key' => 'value', 'key2' => 'value1' }
58
+ # If a block is given, yields key/value pairs one at a time.
59
+ # Otherwise returns a hash of { 'key' => 'value', 'key2' => 'value1' }
69
60
  def get_multi(*keys)
70
- perform do
71
- return {} if keys.empty?
72
- options = nil
73
- options = keys.pop if keys.last.is_a?(Hash) || keys.last.nil?
74
- ring.lock do
75
- begin
76
- mapped_keys = keys.flatten.map {|a| validate_key(a.to_s)}
77
- groups = mapped_keys.flatten.group_by do |key|
78
- begin
79
- ring.server_for_key(key)
80
- rescue Dalli::RingError
81
- Dalli.logger.debug { "unable to get key #{key}" }
82
- nil
83
- end
84
- end
85
- if unfound_keys = groups.delete(nil)
86
- Dalli.logger.debug { "unable to get keys for #{unfound_keys.length} keys because no matching server was found" }
87
- end
88
-
89
- groups.each do |server, keys_for_server|
90
- begin
91
- # TODO: do this with the perform chokepoint?
92
- # But given the fact that fetching the response doesn't take place
93
- # in that slot it's misleading anyway. Need to move all of this method
94
- # into perform to be meaningful
95
- server.request(:send_multiget, keys_for_server)
96
- rescue DalliError, NetworkError => e
97
- Dalli.logger.debug { e.inspect }
98
- Dalli.logger.debug { "unable to get keys for server #{server.hostname}:#{server.port}" }
99
- end
100
- end
101
-
102
- servers = groups.keys
103
- values = {}
104
- return values if servers.empty?
105
-
106
- servers.each do |server|
107
- next unless server.alive?
108
- begin
109
- server.multi_response_start
110
- rescue DalliError, NetworkError => e
111
- Dalli.logger.debug { e.inspect }
112
- Dalli.logger.debug { "results from this server will be missing" }
113
- servers.delete(server)
114
- end
115
- end
116
-
117
- start = Time.now
118
- loop do
119
- # remove any dead servers
120
- servers.delete_if { |s| s.sock.nil? }
121
- break if servers.empty?
122
-
123
- # calculate remaining timeout
124
- elapsed = Time.now - start
125
- timeout = servers.first.options[:socket_timeout]
126
- if elapsed > timeout
127
- readable = nil
128
- else
129
- sockets = servers.map(&:sock)
130
- readable, _ = IO.select(sockets, nil, nil, timeout - elapsed)
131
- end
132
-
133
- if readable.nil?
134
- # no response within timeout; abort pending connections
135
- servers.each do |server|
136
- Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" }
137
- server.multi_response_abort
138
- end
139
- break
140
-
141
- else
142
- readable.each do |sock|
143
- server = sock.server
144
-
145
- begin
146
- server.multi_response_nonblock.each do |key, value|
147
- values[key_without_namespace(key)] = value
148
- end
149
-
150
- if server.multi_response_completed?
151
- servers.delete(server)
152
- end
153
- rescue NetworkError
154
- servers.delete(server)
155
- end
156
- end
157
- end
158
- end
159
-
160
- values
161
- end
61
+ if block_given?
62
+ get_multi_yielder(keys) {|k, data| yield k, data.first}
63
+ else
64
+ Hash.new.tap do |hash|
65
+ get_multi_yielder(keys) {|k, data| hash[k] = data.first}
162
66
  end
163
67
  end
164
68
  end
@@ -212,11 +116,11 @@ module Dalli
212
116
  # on the server. Returns true if the operation succeeded.
213
117
  def replace(key, value, ttl=nil, options=nil)
214
118
  ttl ||= @options[:expires_in].to_i
215
- perform(:replace, key, value, ttl, options)
119
+ perform(:replace, key, value, ttl, 0, options)
216
120
  end
217
121
 
218
122
  def delete(key)
219
- perform(:delete, key)
123
+ perform(:delete, key, 0)
220
124
  end
221
125
 
222
126
  ##
@@ -308,6 +212,16 @@ module Dalli
308
212
  end
309
213
  end
310
214
 
215
+ ##
216
+ ## Version of the memcache servers.
217
+ def version
218
+ values = {}
219
+ ring.servers.each do |server|
220
+ values["#{server.hostname}:#{server.port}"] = server.alive? ? server.request(:version) : nil
221
+ end
222
+ values
223
+ end
224
+
311
225
  ##
312
226
  # Close our connection to each server.
313
227
  # If you perform another operation after this, the connections will be re-established.
@@ -319,8 +233,69 @@ module Dalli
319
233
  end
320
234
  alias_method :reset, :close
321
235
 
236
+ # Stub method so a bare Dalli client can pretend to be a connection pool.
237
+ def with
238
+ yield self
239
+ end
240
+
322
241
  private
323
242
 
243
+ def groups_for_keys(*keys)
244
+ groups = mapped_keys(keys).flatten.group_by do |key|
245
+ begin
246
+ ring.server_for_key(key)
247
+ rescue Dalli::RingError
248
+ Dalli.logger.debug { "unable to get key #{key}" }
249
+ nil
250
+ end
251
+ end
252
+ return groups
253
+ end
254
+
255
+ def mapped_keys(keys)
256
+ keys.flatten.map {|a| validate_key(a.to_s)}
257
+ end
258
+
259
+ def make_multi_get_requests(groups)
260
+ groups.each do |server, keys_for_server|
261
+ begin
262
+ # TODO: do this with the perform chokepoint?
263
+ # But given the fact that fetching the response doesn't take place
264
+ # in that slot it's misleading anyway. Need to move all of this method
265
+ # into perform to be meaningful
266
+ server.request(:send_multiget, keys_for_server)
267
+ rescue DalliError, NetworkError => e
268
+ Dalli.logger.debug { e.inspect }
269
+ Dalli.logger.debug { "unable to get keys for server #{server.hostname}:#{server.port}" }
270
+ end
271
+ end
272
+ end
273
+
274
+ def perform_multi_response_start(servers)
275
+ servers.each do |server|
276
+ next unless server.alive?
277
+ begin
278
+ server.multi_response_start
279
+ rescue DalliError, NetworkError => e
280
+ Dalli.logger.debug { e.inspect }
281
+ Dalli.logger.debug { "results from this server will be missing" }
282
+ servers.delete(server)
283
+ end
284
+ end
285
+ servers
286
+ end
287
+
288
+ ##
289
+ # Normalizes the argument into an array of servers. If the argument is a string, it's expected to be of
290
+ # the format "memcache1.example.com:11211[,memcache2.example.com:11211[,memcache3.example.com:11211[...]]]
291
+ def normalize_servers(servers)
292
+ if servers.is_a? String
293
+ return servers.split(",")
294
+ else
295
+ return servers
296
+ end
297
+ end
298
+
324
299
  def ring
325
300
  @ring ||= Dalli::Ring.new(
326
301
  @servers.map do |s|
@@ -388,5 +363,70 @@ module Dalli
388
363
  end
389
364
  opts
390
365
  end
366
+
367
+ ##
368
+ # Yields, one at a time, keys and their values+attributes.
369
+ def get_multi_yielder(keys)
370
+ perform do
371
+ return {} if keys.empty?
372
+ ring.lock do
373
+ begin
374
+ groups = groups_for_keys(keys)
375
+ if unfound_keys = groups.delete(nil)
376
+ Dalli.logger.debug { "unable to get keys for #{unfound_keys.length} keys because no matching server was found" }
377
+ end
378
+ make_multi_get_requests(groups)
379
+
380
+ servers = groups.keys
381
+ return if servers.empty?
382
+ servers = perform_multi_response_start(servers)
383
+
384
+ start = Time.now
385
+ loop do
386
+ # remove any dead servers
387
+ servers.delete_if { |s| s.sock.nil? }
388
+ break if servers.empty?
389
+
390
+ # calculate remaining timeout
391
+ elapsed = Time.now - start
392
+ timeout = servers.first.options[:socket_timeout]
393
+ if elapsed > timeout
394
+ readable = nil
395
+ else
396
+ sockets = servers.map(&:sock)
397
+ readable, _ = IO.select(sockets, nil, nil, timeout - elapsed)
398
+ end
399
+
400
+ if readable.nil?
401
+ # no response within timeout; abort pending connections
402
+ servers.each do |server|
403
+ Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" }
404
+ server.multi_response_abort
405
+ end
406
+ break
407
+
408
+ else
409
+ readable.each do |sock|
410
+ server = sock.server
411
+
412
+ begin
413
+ server.multi_response_nonblock.each_pair do |key, value_list|
414
+ yield key_without_namespace(key), value_list
415
+ end
416
+
417
+ if server.multi_response_completed?
418
+ servers.delete(server)
419
+ end
420
+ rescue NetworkError
421
+ servers.delete(server)
422
+ end
423
+ end
424
+ end
425
+ end
426
+ end
427
+ end
428
+ end
429
+ end
430
+
391
431
  end
392
432
  end