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.
- data/.gitignore +1 -0
- data/LICENSE +20 -0
- data/README.rdoc +179 -0
- data/Rakefile +56 -0
- data/VERSION.yml +4 -0
- data/ext/extconf.rb +3 -0
- data/lib/memcache/local_server.rb +107 -0
- data/lib/memcache/migration.rb +23 -0
- data/lib/memcache/null_server.rb +30 -0
- data/lib/memcache/pg_server.rb +163 -0
- data/lib/memcache/segmented_server.rb +97 -0
- data/lib/memcache/server.rb +265 -0
- data/lib/memcache.rb +382 -0
- data/memcache.gemspec +69 -0
- data/test/memcache_local_server_test.rb +11 -0
- data/test/memcache_null_server_test.rb +65 -0
- data/test/memcache_pg_server_test.rb +28 -0
- data/test/memcache_segmented_server_test.rb +21 -0
- data/test/memcache_server_test.rb +35 -0
- data/test/memcache_server_test_helper.rb +159 -0
- data/test/memcache_test.rb +233 -0
- data/test/test_helper.rb +26 -0
- metadata +83 -0
@@ -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
|
+
|