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.
@@ -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