segmented-memcache 1.2.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 +215 -0
- data/Rakefile +56 -0
- data/VERSION.yml +5 -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 +116 -0
- data/lib/memcache/server.rb +265 -0
- data/lib/memcache.rb +409 -0
- data/segmented-memcache.gemspec +68 -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,163 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'memcache/migration'
|
3
|
+
|
4
|
+
class Memcache
|
5
|
+
class PGServer
|
6
|
+
attr_reader :db, :table
|
7
|
+
|
8
|
+
def initialize(opts)
|
9
|
+
@table = opts[:table]
|
10
|
+
@db = opts[:db] || ActiveRecord::Base.connection.raw_connection
|
11
|
+
end
|
12
|
+
|
13
|
+
def name
|
14
|
+
@name ||= begin
|
15
|
+
db_config = db.instance_variable_get(:@config)
|
16
|
+
"#{db_config[:host]}:#{db_config[:database]}:#{table}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def flush_all(delay = nil)
|
21
|
+
db.exec("TRUNCATE #{table}")
|
22
|
+
end
|
23
|
+
|
24
|
+
def get(keys)
|
25
|
+
return get([keys])[keys.to_s] unless keys.kind_of?(Array)
|
26
|
+
|
27
|
+
keys = keys.collect {|key| quote(key.to_s)}.join(',')
|
28
|
+
sql = %{
|
29
|
+
SELECT key, value FROM #{table}
|
30
|
+
WHERE key IN (#{keys}) AND #{expiry_clause}
|
31
|
+
}
|
32
|
+
results = {}
|
33
|
+
db.query(sql).each do |key, value|
|
34
|
+
results[key] = value
|
35
|
+
end
|
36
|
+
results
|
37
|
+
end
|
38
|
+
|
39
|
+
def incr(key, amount = 1)
|
40
|
+
transaction do
|
41
|
+
value = get(key)
|
42
|
+
return unless value
|
43
|
+
return unless value =~ /^\d+$/
|
44
|
+
|
45
|
+
value = value.to_i + amount
|
46
|
+
value = 0 if value < 0
|
47
|
+
db.exec %{
|
48
|
+
UPDATE #{table} SET value = #{quote(value)}, updated_at = NOW()
|
49
|
+
WHERE key = #{quote(key)}
|
50
|
+
}
|
51
|
+
value
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def decr(key, amount = 1)
|
56
|
+
incr(key, -amount)
|
57
|
+
end
|
58
|
+
|
59
|
+
def delete(key)
|
60
|
+
result = db.exec %{
|
61
|
+
DELETE FROM #{table}
|
62
|
+
WHERE key = #{quote(key)}
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
def set(key, value, expiry = 0)
|
67
|
+
transaction do
|
68
|
+
delete(key)
|
69
|
+
insert(key, value, expiry)
|
70
|
+
end
|
71
|
+
value
|
72
|
+
end
|
73
|
+
|
74
|
+
def add(key, value, expiry = 0)
|
75
|
+
delete_expired(key)
|
76
|
+
insert(key, value, expiry)
|
77
|
+
value
|
78
|
+
rescue PGError => e
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
|
82
|
+
def replace(key, value, expiry = 0)
|
83
|
+
delete_expired(key)
|
84
|
+
result = update(key, value, expiry)
|
85
|
+
result.cmdtuples == 1 ? value : nil
|
86
|
+
end
|
87
|
+
|
88
|
+
def append(key, value)
|
89
|
+
delete_expired(key)
|
90
|
+
result = db.exec %{
|
91
|
+
UPDATE #{table}
|
92
|
+
SET value = value || #{quote(value)}, updated_at = NOW()
|
93
|
+
WHERE key = #{quote(key)}
|
94
|
+
}
|
95
|
+
result.cmdtuples == 1
|
96
|
+
end
|
97
|
+
|
98
|
+
def prepend(key, value)
|
99
|
+
delete_expired(key)
|
100
|
+
result = db.exec %{
|
101
|
+
UPDATE #{table}
|
102
|
+
SET value = #{quote(value)} || value, updated_at = NOW()
|
103
|
+
WHERE key = #{quote(key)}
|
104
|
+
}
|
105
|
+
result.cmdtuples == 1
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def insert(key, value, expiry = 0)
|
111
|
+
db.exec %{
|
112
|
+
INSERT INTO #{table} (key, value, updated_at, expires_at)
|
113
|
+
VALUES (#{quote(key)}, #{quote(value)}, NOW(), #{expiry_sql(expiry)})
|
114
|
+
}
|
115
|
+
end
|
116
|
+
|
117
|
+
def update(key, value, expiry = 0)
|
118
|
+
db.exec %{
|
119
|
+
UPDATE #{table}
|
120
|
+
SET value = #{quote(value)}, updated_at = NOW(), expires_at = #{expiry_sql(expiry)}
|
121
|
+
WHERE key = #{quote(key)}
|
122
|
+
}
|
123
|
+
end
|
124
|
+
|
125
|
+
def transaction
|
126
|
+
return yield if @in_transaction
|
127
|
+
|
128
|
+
begin
|
129
|
+
@in_transaction = true
|
130
|
+
db.exec('BEGIN')
|
131
|
+
value = yield
|
132
|
+
db.exec('COMMIT')
|
133
|
+
value
|
134
|
+
rescue Exception => e
|
135
|
+
db.exec('ROLLBACK')
|
136
|
+
raise e
|
137
|
+
ensure
|
138
|
+
@in_transaction = false
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def quote(string)
|
143
|
+
string.to_s.gsub(/'/,"\'")
|
144
|
+
"'#{string}'"
|
145
|
+
end
|
146
|
+
|
147
|
+
def delete_expired(key)
|
148
|
+
db.exec "DELETE FROM #{table} WHERE key = #{quote(key)} AND NOT (#{expiry_clause})"
|
149
|
+
end
|
150
|
+
|
151
|
+
def expiry_clause
|
152
|
+
"expires_at IS NULL OR expires_at > NOW()"
|
153
|
+
end
|
154
|
+
|
155
|
+
def expiry_sql(expiry)
|
156
|
+
if expiry.kind_of?(Time)
|
157
|
+
quote(expiry.to_s(:db))
|
158
|
+
else
|
159
|
+
expiry == 0 ? 'NULL' : "NOW() + interval '#{expiry} seconds'"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
|
3
|
+
class Memcache
|
4
|
+
class SegmentedServer < Server
|
5
|
+
MAX_SIZE = 1000000 # bytes
|
6
|
+
PARTIAL_VALUE = 0x40000000
|
7
|
+
|
8
|
+
alias :super_get :get
|
9
|
+
|
10
|
+
def get(keys, cas = nil)
|
11
|
+
return get([keys], cas)[keys.to_s] unless keys.kind_of?(Array)
|
12
|
+
return {} if keys.empty?
|
13
|
+
|
14
|
+
results = super
|
15
|
+
keys = {}
|
16
|
+
keys_to_fetch = []
|
17
|
+
|
18
|
+
extract_keys(results, keys_to_fetch, keys)
|
19
|
+
|
20
|
+
parts = super(keys_to_fetch)
|
21
|
+
keys.each do |key, hashes|
|
22
|
+
value = ''
|
23
|
+
hashes.each do |hash_key|
|
24
|
+
if part = parts[hash_key]
|
25
|
+
value << part
|
26
|
+
else
|
27
|
+
value = nil
|
28
|
+
break
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
if value
|
33
|
+
value.memcache_cas = results[key].memcache_cas
|
34
|
+
value.memcache_flags = results[key].memcache_flags ^ PARTIAL_VALUE
|
35
|
+
end
|
36
|
+
results[key] = value
|
37
|
+
end
|
38
|
+
results
|
39
|
+
end
|
40
|
+
|
41
|
+
def delete(keys, cas = nil)
|
42
|
+
return delete([keys], cas) unless keys.kind_of?(Array)
|
43
|
+
return {} if keys.empty?
|
44
|
+
|
45
|
+
results = super_get(keys, cas)
|
46
|
+
keys_to_fetch = keys
|
47
|
+
extract_keys(results, keys_to_fetch)
|
48
|
+
keys_to_fetch.each{|k| super k}
|
49
|
+
end
|
50
|
+
|
51
|
+
def set(key, value, expiry = 0, flags = 0)
|
52
|
+
delete key if !(super_get(key)).nil?
|
53
|
+
value, flags = store_segments(key, value, expiry, flags)
|
54
|
+
super(key, value, expiry, flags) && value
|
55
|
+
end
|
56
|
+
|
57
|
+
def cas(key, value, cas, expiry = 0, flags = 0)
|
58
|
+
value, flags = store_segments(key, value, expiry, flags)
|
59
|
+
super(key, value, cas, expiry, flags)
|
60
|
+
end
|
61
|
+
|
62
|
+
def add(key, value, expiry = 0, flags = 0)
|
63
|
+
value, flags = store_segments(key, value, expiry, flags)
|
64
|
+
super(key, value, expiry, flags)
|
65
|
+
end
|
66
|
+
|
67
|
+
def replace(key, value, expiry = 0, flags = 0)
|
68
|
+
value, flags = store_segments(key, value, expiry, flags)
|
69
|
+
super(key, value, expiry, flags)
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def segmented?(value)
|
75
|
+
value.memcache_flags & PARTIAL_VALUE == PARTIAL_VALUE
|
76
|
+
end
|
77
|
+
|
78
|
+
def segment(key, value)
|
79
|
+
hash = Digest::SHA1.hexdigest("#{key}:#{Time.now}:#{rand}")
|
80
|
+
parts = {}
|
81
|
+
i = 0; offset = 0
|
82
|
+
while offset < value.size
|
83
|
+
parts["#{hash}:#{i}"] = value[offset, MAX_SIZE]
|
84
|
+
offset += MAX_SIZE; i += 1
|
85
|
+
end
|
86
|
+
master_key = "#{hash}:#{parts.size}"
|
87
|
+
[master_key, parts]
|
88
|
+
end
|
89
|
+
|
90
|
+
def store_segments(key, value, expiry = 0, flags = 0)
|
91
|
+
if value and value.size > MAX_SIZE
|
92
|
+
master_key, parts = segment(key, value)
|
93
|
+
parts.each do |hash, data|
|
94
|
+
set(hash, data, expiry==0 ? 0 : expiry + 1) # We want the segments to expire slightly after the master key.
|
95
|
+
end
|
96
|
+
[master_key, flags | PARTIAL_VALUE]
|
97
|
+
else
|
98
|
+
[value, flags]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def extract_keys(results, keys_to_fetch, keys=nil)
|
103
|
+
results.each do |key, value|
|
104
|
+
next unless segmented?(value)
|
105
|
+
hash, num = value.split(':')
|
106
|
+
keys[key] = [] unless keys.nil?
|
107
|
+
num.to_i.times do |i|
|
108
|
+
hash_key = "#{hash}:#{i}"
|
109
|
+
keys_to_fetch << hash_key
|
110
|
+
keys[key] << hash_key unless keys.nil?
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
end
|
@@ -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
|
+
"<#{self.class.name}: %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 = nil)
|
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
|