memcache 1.0.0

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,265 @@
1
+ require 'socket'
2
+ require 'thread'
3
+ require 'timeout'
4
+
5
+ class Memcache
6
+ class Server
7
+ CONNECT_TIMEOUT = 1.0
8
+ READ_RETRY_DELAY = 5.0
9
+ DEFAULT_PORT = 11211
10
+
11
+ attr_reader :host, :port, :status, :retry_at
12
+
13
+ def initialize(opts)
14
+ @host = opts[:host]
15
+ @port = opts[:port] || DEFAULT_PORT
16
+ @readonly = opts[:readonly]
17
+ @strict_reads = opts[:strict_reads]
18
+ @status = 'NOT CONNECTED'
19
+ end
20
+
21
+ def clone
22
+ self.class.new(:host => host, :port => port, :readonly => readonly?, :strict_reads => strict_reads?)
23
+ end
24
+
25
+ def inspect
26
+ "<Memcache::Server: %s:%d (%s)>" % [@host, @port, @status]
27
+ end
28
+
29
+ def name
30
+ "#{host}:#{port}"
31
+ end
32
+
33
+ def alive?
34
+ @retry_at.nil? or @retry_at < Time.now
35
+ end
36
+
37
+ def readonly?
38
+ @readonly
39
+ end
40
+
41
+ def strict_reads?
42
+ @strict_reads
43
+ end
44
+
45
+ def close(error = nil)
46
+ # Close the socket. If there is an error, mark the server dead.
47
+ @socket.close if @socket and not @socket.closed?
48
+ @socket = nil
49
+
50
+ if error
51
+ @retry_at = Time.now + READ_RETRY_DELAY
52
+ @status = "DEAD: %s: %s, will retry at %s" % [error.class, error.message, @retry_at]
53
+ else
54
+ @retry_at = nil
55
+ @status = "NOT CONNECTED"
56
+ end
57
+ end
58
+
59
+ def stats
60
+ stats = {}
61
+ read_command('stats') do |response|
62
+ key, value = match_response!(response, /^STAT ([\w]+) (-?[\w\.\:]+)/)
63
+
64
+ if ['rusage_user', 'rusage_system'].include?(key)
65
+ seconds, microseconds = value.split(/:/, 2)
66
+ microseconds ||= 0
67
+ stats[key] = Float(seconds) + (Float(microseconds) / 1_000_000)
68
+ else
69
+ stats[key] = (value =~ /^-?\d+$/ ? value.to_i : value)
70
+ end
71
+ end
72
+ stats
73
+ end
74
+
75
+ def count
76
+ stats['curr_items']
77
+ end
78
+
79
+ def flush_all(delay = nil)
80
+ check_writable!
81
+ write_command("flush_all #{delay}")
82
+ end
83
+
84
+ alias clear flush_all
85
+
86
+ def gets(keys)
87
+ get(keys, true)
88
+ end
89
+
90
+ def get(keys, cas = false)
91
+ return get([keys], cas)[keys.to_s] unless keys.kind_of?(Array)
92
+ return {} if keys.empty?
93
+
94
+ method = cas ? 'gets' : 'get'
95
+
96
+ results = {}
97
+ read_command("#{method} #{keys.join(' ')}") do |response|
98
+ if cas
99
+ key, flags, length, cas = match_response!(response, /^VALUE ([^\s]+) ([^\s]+) ([^\s]+) ([^\s]+)/)
100
+ else
101
+ key, flags, length = match_response!(response, /^VALUE ([^\s]+) ([^\s]+) ([^\s]+)/)
102
+ end
103
+
104
+ value = socket.read(length.to_i)
105
+ match_response!(socket.read(2), "\r\n")
106
+
107
+ value.memcache_flags = flags.to_i
108
+ value.memcache_cas = cas
109
+ results[key] = value
110
+ end
111
+ results
112
+ end
113
+
114
+ def incr(key, amount = 1)
115
+ check_writable!
116
+ raise Error, "incr requires unsigned value" if amount < 0
117
+ response = write_command("incr #{key} #{amount}")
118
+ response == "NOT_FOUND\r\n" ? nil : response.to_i
119
+ end
120
+
121
+ def decr(key, amount = 1)
122
+ check_writable!
123
+ raise Error, "decr requires unsigned value" if amount < 0
124
+ response = write_command("decr #{key} #{amount}")
125
+ response == "NOT_FOUND\r\n" ? nil : response.to_i
126
+ end
127
+
128
+ def delete(key)
129
+ check_writable!
130
+ write_command("delete #{key}") == "DELETED\r\n" ? true : nil
131
+ end
132
+
133
+ def set(key, value, expiry = 0, flags = 0)
134
+ return delete(key) if value.nil?
135
+ check_writable!
136
+ write_command("set #{key} #{flags.to_i} #{expiry.to_i} #{value.to_s.size}", value)
137
+ value
138
+ end
139
+
140
+ def cas(key, value, cas, expiry = 0, flags = 0)
141
+ check_writable!
142
+ response = write_command("cas #{key} #{flags.to_i} #{expiry.to_i} #{value.to_s.size} #{cas.to_i}", value)
143
+ response == "STORED\r\n" ? value : nil
144
+ end
145
+
146
+ def add(key, value, expiry = 0, flags = 0)
147
+ check_writable!
148
+ response = write_command("add #{key} #{flags.to_i} #{expiry.to_i} #{value.to_s.size}", value)
149
+ response == "STORED\r\n" ? value : nil
150
+ end
151
+
152
+ def replace(key, value, expiry = 0, flags = 0)
153
+ check_writable!
154
+ response = write_command("replace #{key} #{flags.to_i} #{expiry.to_i} #{value.to_s.size}", value)
155
+ response == "STORED\r\n" ? value : nil
156
+ end
157
+
158
+ def append(key, value)
159
+ check_writable!
160
+ response = write_command("append #{key} 0 0 #{value.to_s.size}", value)
161
+ response == "STORED\r\n"
162
+ end
163
+
164
+ def prepend(key, value)
165
+ check_writable!
166
+ response = write_command("prepend #{key} 0 0 #{value.to_s.size}", value)
167
+ response == "STORED\r\n"
168
+ end
169
+
170
+ class Error < StandardError; end
171
+ class ConnectionError < Error
172
+ def initialize(e)
173
+ if e.kind_of?(String)
174
+ super
175
+ else
176
+ super("(#{e.class}) #{e.message}")
177
+ set_backtrace(e.backtrace)
178
+ end
179
+ end
180
+ end
181
+ class ServerError < Error; end
182
+ class ClientError < Error; end
183
+
184
+ private
185
+
186
+ def check_writable!
187
+ raise Error, "Update of readonly cache" if readonly?
188
+ end
189
+
190
+ def match_response!(response, regexp)
191
+ # Make sure that the response matches the protocol.
192
+ unexpected_eof! if response.nil?
193
+ match = response.match(regexp)
194
+ raise ServerError, "unexpected response: #{response.inspect}" unless match
195
+
196
+ match.to_a[1, match.size]
197
+ end
198
+
199
+ def send_command(*command)
200
+ command = command.join("\r\n")
201
+ socket.write("#{command}\r\n")
202
+ response = socket.gets
203
+
204
+ unexpected_eof! if response.nil?
205
+ if response =~ /^(ERROR|CLIENT_ERROR|SERVER_ERROR) (.*)\r\n/
206
+ raise ($1 == 'SERVER_ERROR' ? ServerError : ClientError), $2
207
+ end
208
+
209
+ block_given? ? yield(response) : response
210
+ rescue Exception => e
211
+ close(e) # Mark dead.
212
+ raise e if e.kind_of?(Error)
213
+ raise ConnectionError.new(e)
214
+ end
215
+
216
+ def write_command(*command, &block)
217
+ retried = false
218
+ begin
219
+ send_command(*command, &block)
220
+ rescue Exception => e
221
+ puts "Memcache write error: #{e.class} #{e.to_s}"
222
+ unless retried
223
+ retried = true
224
+ retry
225
+ end
226
+ raise(e)
227
+ end
228
+ end
229
+
230
+ def read_command(command, &block)
231
+ raise ConnectionError, "Server dead, will retry at #{retry_at}" unless alive?
232
+ send_command(command) do |response|
233
+ while response do
234
+ return if response == "END\r\n"
235
+ yield(response)
236
+ response = socket.gets
237
+ end
238
+ unexpected_eof!
239
+ end
240
+ rescue Exception => e
241
+ puts "Memcache read error: #{e.class} #{e.to_s}"
242
+ raise(e) if strict_reads?
243
+ end
244
+
245
+ def socket
246
+ if @socket.nil? or @socket.closed?
247
+ # Attempt to connect.
248
+ @socket = timeout(CONNECT_TIMEOUT) do
249
+ TCPSocket.new(host, port)
250
+ end
251
+ if Socket.constants.include? 'TCP_NODELAY'
252
+ @socket.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
253
+ end
254
+
255
+ @retry_at = nil
256
+ @status = 'CONNECTED'
257
+ end
258
+ @socket
259
+ end
260
+
261
+ def unexpected_eof!
262
+ raise Error, 'unexpected end of file'
263
+ end
264
+ end
265
+ end
data/lib/memcache.rb ADDED
@@ -0,0 +1,382 @@
1
+ require 'zlib'
2
+
3
+ $:.unshift(File.dirname(__FILE__))
4
+ require 'memcache/server'
5
+ require 'memcache/local_server'
6
+ require 'memcache/segmented_server'
7
+
8
+ class Memcache
9
+ DEFAULT_EXPIRY = 0
10
+ LOCK_TIMEOUT = 5
11
+ WRITE_LOCK_WAIT = 1
12
+
13
+ attr_reader :default_expiry, :default_namespace, :servers
14
+
15
+ def initialize(opts)
16
+ @default_expiry = opts[:default_expiry] || DEFAULT_EXPIRY
17
+ @default_namespace = opts[:namespace]
18
+ default_server = opts[:segment_large_values] ? SegmentedServer : Server
19
+
20
+ @servers = (opts[:servers] || [ opts[:server] ]).collect do |server|
21
+ case server
22
+ when Hash
23
+ server = default_server.new(opts.merge(server))
24
+ when String
25
+ host, port = server.split(':')
26
+ server = default_server.new(opts.merge(:host => host, :port => port))
27
+ when Class
28
+ server = server.new
29
+ when :local
30
+ server = Memcache::LocalServer.new
31
+ end
32
+ server
33
+ end
34
+ end
35
+
36
+ def clone
37
+ self.class.new(
38
+ :default_expiry => default_expiry,
39
+ :default_namespace => default_namespace,
40
+ :servers => servers.collect {|s| s.clone}
41
+ )
42
+ end
43
+
44
+ def inspect
45
+ "<Memcache: %d servers, ns: %p>" % [@servers.length, namespace]
46
+ end
47
+
48
+ def namespace
49
+ @namespace || default_namespace
50
+ end
51
+
52
+ def namespace=(namespace)
53
+ if default_namespace == namespace
54
+ @namespace = nil
55
+ else
56
+ @namespace = namespace
57
+ end
58
+ end
59
+
60
+ def in_namespace(namespace)
61
+ # Temporarily change the namespace for convenience.
62
+ begin
63
+ old_namespace = self.namespace
64
+ self.namespace = "#{old_namespace}#{namespace}"
65
+ yield
66
+ ensure
67
+ self.namespace = old_namespace
68
+ end
69
+ end
70
+
71
+ def get(keys, opts = {})
72
+ raise 'opts must be hash' unless opts.kind_of?(Hash)
73
+
74
+ if keys.kind_of?(Array)
75
+ multi_get(keys, opts)
76
+ else
77
+ key = cache_key(keys)
78
+
79
+ if opts[:expiry]
80
+ value = server(key).gets(key)
81
+ server(key).cas(key, value, value.memcache_cas, opts[:expiry]) if value
82
+ else
83
+ value = server(key).get(key, opts[:cas])
84
+ end
85
+ unmarshal(value, opts)
86
+ end
87
+ end
88
+
89
+ def read(keys, opts = {})
90
+ get(keys, opts.merge(:raw => true))
91
+ end
92
+
93
+ def set(key, value, opts = {})
94
+ raise 'opts must be hash' unless opts.kind_of?(Hash)
95
+
96
+ expiry = opts[:expiry] || default_expiry
97
+ flags = opts[:flags] || 0
98
+ key = cache_key(key)
99
+ data = marshal(value, opts)
100
+ server(key).set(key, data, expiry, flags)
101
+ value
102
+ end
103
+
104
+ def write(key, value, opts = {})
105
+ set(key, value, opts.merge(:raw => true))
106
+ end
107
+
108
+ def add(key, value, opts = {})
109
+ raise 'opts must be hash' unless opts.kind_of?(Hash)
110
+
111
+ expiry = opts[:expiry] || default_expiry
112
+ flags = opts[:flags] || 0
113
+ key = cache_key(key)
114
+ data = marshal(value, opts)
115
+ server(key).add(key, data, expiry, flags) && value
116
+ end
117
+
118
+ def replace(key, value, opts = {})
119
+ raise 'opts must be hash' unless opts.kind_of?(Hash)
120
+
121
+ expiry = opts[:expiry] || default_expiry
122
+ flags = opts[:flags] || 0
123
+ key = cache_key(key)
124
+ data = marshal(value, opts)
125
+ server(key).replace(key, data, expiry, flags) && value
126
+ end
127
+
128
+ def cas(key, value, opts = {})
129
+ raise 'opts must be hash' unless opts.kind_of?(Hash)
130
+
131
+ expiry = opts[:expiry] || default_expiry
132
+ flags = opts[:flags] || 0
133
+ key = cache_key(key)
134
+ data = marshal(value, opts)
135
+ server(key).cas(key, data, opts[:cas], expiry, flags) && value
136
+ end
137
+
138
+ def append(key, value)
139
+ key = cache_key(key)
140
+ server(key).append(key, value)
141
+ end
142
+
143
+ def prepend(key, value)
144
+ key = cache_key(key)
145
+ server(key).prepend(key, value)
146
+ end
147
+
148
+ def count(key)
149
+ key = cache_key(key)
150
+ server(key).get(key).to_i
151
+ end
152
+
153
+ def incr(key, amount = 1)
154
+ key = cache_key(key)
155
+ server(key).incr(key, amount)
156
+ end
157
+
158
+ def decr(key, amount = 1)
159
+ key = cache_key(key)
160
+ server(key).decr(key, amount)
161
+ end
162
+
163
+ def update(key, opts = {})
164
+ value = get(key, :cas => true)
165
+ if value
166
+ cas(key, yield(value), opts.merge!(:cas => value.memcache_cas))
167
+ else
168
+ add(key, yield(value), opts)
169
+ end
170
+ end
171
+
172
+ def get_or_add(key, *args)
173
+ # Pseudo-atomic get and update.
174
+ if block_given?
175
+ opts = args[0] || {}
176
+ get(key) || add(key, yield, opts) || get(key)
177
+ else
178
+ opts = args[1] || {}
179
+ get(key) || add(key, args[0], opts) || get(key)
180
+ end
181
+ end
182
+
183
+ def get_or_set(key, *args)
184
+ if block_given?
185
+ opts = args[0] || {}
186
+ get(key) || set(key, yield, opts)
187
+ else
188
+ opts = args[1] || {}
189
+ get(key) || set(key, args[0], opts)
190
+ end
191
+ end
192
+
193
+ def get_some(keys, opts = {})
194
+ keys = keys.collect {|key| key.to_s}
195
+
196
+ records = opts[:disable] ? {} : self.get(keys, opts)
197
+ if opts[:validation]
198
+ records.delete_if do |key, value|
199
+ not opts[:validation].call(key, value)
200
+ end
201
+ end
202
+
203
+ keys_to_fetch = keys - records.keys
204
+ method = opts[:overwrite] ? :set : :add
205
+ if keys_to_fetch.any?
206
+ yield(keys_to_fetch).each do |key, value|
207
+ self.send(method, key, value, opts) unless opts[:disable] or opts[:disable_write]
208
+ records[key] = value
209
+ end
210
+ end
211
+ records
212
+ end
213
+
214
+ def lock(key, opts = {})
215
+ # Returns false if the lock already exists.
216
+ expiry = opts[:expiry] || LOCK_TIMEOUT
217
+ add(lock_key(key), Socket.gethostname, :expiry => expiry, :raw => true)
218
+ end
219
+
220
+ def unlock(key)
221
+ delete(lock_key(key))
222
+ end
223
+
224
+ def with_lock(key, opts = {})
225
+ until lock(key) do
226
+ return if opts[:ignore]
227
+ sleep(WRITE_LOCK_WAIT) # just wait
228
+ end
229
+ yield
230
+ unlock(key) unless opts[:keep]
231
+ end
232
+
233
+ def lock_key(key)
234
+ "lock:#{key}"
235
+ end
236
+
237
+ def locked?(key)
238
+ get(lock_key(key), :raw => true)
239
+ end
240
+
241
+ def delete(key)
242
+ key = cache_key(key)
243
+ server(key).delete(key)
244
+ end
245
+
246
+ def flush_all(opts = {})
247
+ delay = opts[:delay].to_i
248
+ interval = opts[:interval].to_i
249
+
250
+ servers.each do |server|
251
+ server.flush_all(delay)
252
+ delay += interval
253
+ end
254
+ end
255
+
256
+ def reset
257
+ servers.each {|server| server.close if server.respond_to?(:close)}
258
+ end
259
+
260
+ def stats(field = nil)
261
+ if field
262
+ servers.collect do |server|
263
+ server.stats[field]
264
+ end
265
+ else
266
+ stats = {}
267
+ servers.each do |server|
268
+ stats[server.name] = server.stats
269
+ end
270
+ stats
271
+ end
272
+ end
273
+
274
+ alias clear flush_all
275
+
276
+ def [](key)
277
+ get(key)
278
+ end
279
+
280
+ def []=(key, value)
281
+ set(key, value)
282
+ end
283
+
284
+ protected
285
+
286
+ def multi_get(keys, opts = {})
287
+ return {} if keys.empty?
288
+
289
+ key_to_input_key = {}
290
+ keys_by_server = Hash.new { |h,k| h[k] = [] }
291
+
292
+ # Store keys by servers. Also store a mapping from cache key to input key.
293
+ keys.each do |input_key|
294
+ key = cache_key(input_key)
295
+ server = server(key)
296
+ key_to_input_key[key] = input_key.to_s
297
+ keys_by_server[server] << key
298
+ end
299
+
300
+ # Fetch and combine the results. Also, map the cache keys back to the input keys.
301
+ results = {}
302
+ keys_by_server.each do |server, keys|
303
+ server.get(keys, opts[:cas]).each do |key, value|
304
+ input_key = key_to_input_key[key]
305
+ results[input_key] = unmarshal(value, opts)
306
+ end
307
+ end
308
+ results
309
+ end
310
+
311
+ def cache_key(key)
312
+ safe_key = key ? key.to_s.gsub(/%/, '%%').gsub(/ /, '%s') : key
313
+ if namespace.nil? then
314
+ safe_key
315
+ else
316
+ "#{namespace}:#{safe_key}"
317
+ end
318
+ end
319
+
320
+ def marshal(value, opts = {})
321
+ opts[:raw] ? value : Marshal.dump(value)
322
+ end
323
+
324
+ def unmarshal(value, opts = {})
325
+ return value if value.nil? or opts[:raw]
326
+
327
+ object = Marshal.load(value)
328
+ object.memcache_flags = value.memcache_flags
329
+ object.memcache_cas = value.memcache_cas
330
+ object
331
+ rescue Exception => e
332
+ puts "Memcache read error: #{e.class} #{e.to_s} while unmarshalling value: #{value}"
333
+ nil
334
+ end
335
+
336
+ def server(key)
337
+ raise ArgumentError, "key too long #{key.inspect}" if key.length > 250
338
+ return servers.first if servers.length == 1
339
+
340
+ hash = (Zlib.crc32(key) >> 16) & 0x7fff
341
+ servers[hash % servers.length]
342
+ end
343
+
344
+ class Pool
345
+ attr_reader :fallback
346
+
347
+ def initialize
348
+ @cache_by_scope = {}
349
+ @cache_by_scope[:default] = Memcache.new(:server => Memcache::LocalServer)
350
+ @fallback = :default
351
+ end
352
+
353
+ def include?(scope)
354
+ @cache_by_scope.include?(scope.to_sym)
355
+ end
356
+
357
+ def fallback=(scope)
358
+ @fallback = scope.to_sym
359
+ end
360
+
361
+ def [](scope)
362
+ @cache_by_scope[scope.to_sym] || @cache_by_scope[fallback]
363
+ end
364
+
365
+ def []=(scope, cache)
366
+ @cache_by_scope[scope.to_sym] = cache
367
+ end
368
+
369
+ def reset
370
+ @cache_by_scope.values.each {|c| c.reset}
371
+ end
372
+ end
373
+
374
+ def self.pool
375
+ @@cache_pool ||= Pool.new
376
+ end
377
+ end
378
+
379
+ # Add flags and cas
380
+ class Object
381
+ attr_accessor :memcache_flags, :memcache_cas
382
+ end
data/memcache.gemspec ADDED
@@ -0,0 +1,69 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{memcache}
8
+ s.version = "1.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Justin Balthrop"]
12
+ s.date = %q{2009-11-20}
13
+ s.description = %q{Ruby client for memcached supporting advanced protocol features and pluggable architecture.}
14
+ s.email = %q{code@justinbalthrop.com}
15
+ s.extensions = ["ext/extconf.rb"]
16
+ s.extra_rdoc_files = [
17
+ "LICENSE",
18
+ "README.rdoc"
19
+ ]
20
+ s.files = [
21
+ ".gitignore",
22
+ "LICENSE",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION.yml",
26
+ "lib/memcache.rb",
27
+ "lib/memcache/local_server.rb",
28
+ "lib/memcache/migration.rb",
29
+ "lib/memcache/null_server.rb",
30
+ "lib/memcache/pg_server.rb",
31
+ "lib/memcache/segmented_server.rb",
32
+ "lib/memcache/server.rb",
33
+ "memcache.gemspec",
34
+ "test/memcache_local_server_test.rb",
35
+ "test/memcache_null_server_test.rb",
36
+ "test/memcache_pg_server_test.rb",
37
+ "test/memcache_segmented_server_test.rb",
38
+ "test/memcache_server_test.rb",
39
+ "test/memcache_server_test_helper.rb",
40
+ "test/memcache_test.rb",
41
+ "test/test_helper.rb"
42
+ ]
43
+ s.homepage = %q{http://github.com/ninjudd/memcache}
44
+ s.rdoc_options = ["--charset=UTF-8"]
45
+ s.require_paths = ["lib"]
46
+ s.rubygems_version = %q{1.3.5}
47
+ s.summary = %q{Advanced ruby memcache client}
48
+ s.test_files = [
49
+ "test/memcache_local_server_test.rb",
50
+ "test/memcache_null_server_test.rb",
51
+ "test/memcache_pg_server_test.rb",
52
+ "test/memcache_segmented_server_test.rb",
53
+ "test/memcache_server_test.rb",
54
+ "test/memcache_server_test_helper.rb",
55
+ "test/memcache_test.rb",
56
+ "test/test_helper.rb"
57
+ ]
58
+
59
+ if s.respond_to? :specification_version then
60
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
61
+ s.specification_version = 3
62
+
63
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
64
+ else
65
+ end
66
+ else
67
+ end
68
+ end
69
+