ninjudd-memcache 0.9.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,91 @@
1
+ class Memcache
2
+ class LocalServer
3
+ def initialize
4
+ @data = {}
5
+ @expiry = {}
6
+ end
7
+
8
+ def name
9
+ "local:#{hash}"
10
+ end
11
+
12
+ def flush_all(delay = nil)
13
+ raise 'flush_all not supported with delay' if delay
14
+ @data.clear
15
+ @expiry.clear
16
+ end
17
+
18
+ def get(keys)
19
+ if keys.kind_of?(Array)
20
+ hash = {}
21
+ keys.each do |key|
22
+ key = key.to_s
23
+ val = get(key)
24
+ hash[key] = val if val
25
+ end
26
+ hash
27
+ else
28
+ key = keys.to_s
29
+ if @expiry[key] and Time.now > @expiry[key]
30
+ @data[key] = nil
31
+ @expiry[key] = nil
32
+ end
33
+ @data[key]
34
+ end
35
+ end
36
+
37
+ def incr(key, amount = 1)
38
+ key = key.to_s
39
+ value = get(key)
40
+ return unless value
41
+ return unless value =~ /^\d+$/
42
+
43
+ value = value.to_i + amount
44
+ value = 0 if value < 0
45
+ @data[key] = value.to_s
46
+ value
47
+ end
48
+
49
+ def decr(key, amount = 1)
50
+ incr(key, -amount)
51
+ end
52
+
53
+ def delete(key, expiry = nil)
54
+ if expiry
55
+ old_expiry = @expiry[key.to_s] || Time.now + expiry
56
+ @expiry[key.to_s] = [old_expiry, expiry].min
57
+ else
58
+ @data.delete(key.to_s)
59
+ end
60
+ end
61
+
62
+ def set(key, value, expiry = nil)
63
+ key = key.to_s
64
+ @data[key] = value
65
+ @expiry[key] = Time.now + expiry if expiry and expiry != 0
66
+ value
67
+ end
68
+
69
+ def add(key, value, expiry = nil)
70
+ return nil if get(key)
71
+ set(key, value, expiry)
72
+ end
73
+
74
+ def replace(key, value, expiry = nil)
75
+ return nil if get(key).nil?
76
+ set(key, value, expiry)
77
+ end
78
+
79
+ def append(key, value)
80
+ existing = get(key)
81
+ return nil if existing.nil?
82
+ set(key, existing + value)
83
+ end
84
+
85
+ def prepend(key, value)
86
+ existing = get(key)
87
+ return nil if existing.nil?
88
+ set(key, value + existing)
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,23 @@
1
+ class Memcache
2
+ class Migration < ActiveRecord::Migration
3
+ class << self
4
+ attr_accessor :table
5
+ end
6
+
7
+ def self.up
8
+ create_table table, :id => false do |t|
9
+ t.string :key
10
+ t.text :value
11
+ t.timestamp :expires_at
12
+ t.timestamp :updated_at
13
+ end
14
+
15
+ add_index table, [:key], :unique => true
16
+ add_index table, [:expires_at]
17
+ end
18
+
19
+ def self.down
20
+ drop_table table
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,30 @@
1
+ class Memcache
2
+ class NullServer
3
+ def name
4
+ "null"
5
+ end
6
+
7
+ def flush_all(delay = nil)
8
+ end
9
+
10
+ def get(keys)
11
+ keys.kind_of?(Array) ? {} : nil
12
+ end
13
+
14
+ def incr(key, amount = nil)
15
+ nil
16
+ end
17
+
18
+ def delete(key, expiry = nil)
19
+ nil
20
+ end
21
+
22
+ def set(key, value, expiry = nil)
23
+ nil
24
+ end
25
+
26
+ def add(key, value, expiry = nil)
27
+ nil
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,159 @@
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
+ expiry == 0 ? 'NULL' : "NOW() + interval '#{expiry} seconds'"
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,89 @@
1
+ require 'digest/sha1'
2
+
3
+ class Memcache
4
+ class SegmentedServer < Server
5
+ MAX_SIZE = 1000000 # bytes
6
+ PARTIAL_VALUE = 0x40000000
7
+
8
+ def get(keys, opts = {})
9
+ return get([keys], opts)[keys.to_s] unless keys.kind_of?(Array)
10
+ return {} if keys.empty?
11
+
12
+ results = super
13
+ keys = {}
14
+ keys_to_fetch = []
15
+ results.each do |key, value|
16
+ next unless segmented?(value)
17
+ hash, num = value.split(':')
18
+ keys[key] = []
19
+ num.to_i.times do |i|
20
+ hash_key = "#{hash}:#{i}"
21
+ keys_to_fetch << hash_key
22
+ keys[key] << hash_key
23
+ end
24
+ end
25
+
26
+ parts = super(keys_to_fetch)
27
+ keys.each do |key, hashes|
28
+ value = ''
29
+ hashes.each do |hash_key|
30
+ value << parts[hash_key]
31
+ end
32
+ value.memcache_cas = results[key].memcache_cas
33
+ value.memcache_flags = results[key].memcache_flags ^ PARTIAL_VALUE
34
+ results[key] = value
35
+ end
36
+ results
37
+ end
38
+
39
+ def set(key, value, expiry = 0, flags = 0)
40
+ value, flags = store_segments(key, value, expiry, flags)
41
+ super(key, value, expiry, flags) && value
42
+ end
43
+
44
+ def cas(key, value, cas, expiry = 0, flags = 0)
45
+ value, flags = store_segments(key, value, expiry, flags)
46
+ super(key, value, cas, expiry, flags)
47
+ end
48
+
49
+ def add(key, value, expiry = 0, flags = 0)
50
+ value, flags = store_segments(key, value, expiry, flags)
51
+ super(key, value, expiry, flags)
52
+ end
53
+
54
+ def replace(key, value, expiry = 0, flags = 0)
55
+ value, flags = store_segments(key, value, expiry, flags)
56
+ super(key, value, expiry, flags)
57
+ end
58
+
59
+ private
60
+
61
+ def segmented?(value)
62
+ value.memcache_flags & PARTIAL_VALUE == PARTIAL_VALUE
63
+ end
64
+
65
+ def segment(key, value)
66
+ hash = Digest::SHA1.hexdigest("#{key}:#{Time.now}:#{rand}")
67
+ parts = {}
68
+ i = 0; offset = 0
69
+ while offset < value.size
70
+ parts["#{hash}:#{i}"] = value[offset, MAX_SIZE]
71
+ offset += MAX_SIZE; i += 1
72
+ end
73
+ master_key = "#{hash}:#{parts.size}"
74
+ [master_key, parts]
75
+ end
76
+
77
+ def store_segments(key, value, expiry = 0, flags = 0)
78
+ if value and value.size > MAX_SIZE
79
+ master_key, parts = segment(key, value)
80
+ parts.each do |hash, data|
81
+ set(hash, data, expiry)
82
+ end
83
+ [master_key, flags | PARTIAL_VALUE]
84
+ else
85
+ [value, flags]
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,249 @@
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
+ attr_writer :strict_reads
13
+
14
+ class MemcacheError < StandardError; end
15
+ class ServerError < MemcacheError; end
16
+ class ClientError < MemcacheError; end
17
+
18
+ def initialize(opts)
19
+ @host = opts[:host]
20
+ @port = opts[:port] || DEFAULT_PORT
21
+ @readonly = opts[:readonly]
22
+ @strict_reads = opts[:strict_reads]
23
+ @status = 'NOT CONNECTED'
24
+ end
25
+
26
+ def inspect
27
+ "<Memcache::Server: %s:%d (%s)>" % [@host, @port, @status]
28
+ end
29
+
30
+ def name
31
+ "#{host}:#{port}"
32
+ end
33
+
34
+ def alive?
35
+ @retry_at.nil? or @retry_at < Time.now
36
+ end
37
+
38
+ def readonly?
39
+ @readonly
40
+ end
41
+
42
+ def strict_reads?
43
+ @strict_reads
44
+ end
45
+
46
+ def close(error = nil)
47
+ # Close the socket. If there is an error, mark the server dead.
48
+ @socket.close if @socket and not @socket.closed?
49
+ @socket = nil
50
+
51
+ if error
52
+ @retry_at = Time.now + READ_RETRY_DELAY
53
+ @status = "DEAD: %s: %s, will retry at %s" % [error.class, error.message, @retry_at]
54
+ else
55
+ @retry_at = nil
56
+ @status = "NOT CONNECTED"
57
+ end
58
+ end
59
+
60
+ def stats
61
+ stats = {}
62
+ read_command('stats') do |response|
63
+ key, value = match_response!(response, /^STAT ([\w]+) ([\w\.\:]+)/)
64
+
65
+ if ['rusage_user', 'rusage_system'].include?(key)
66
+ seconds, microseconds = value.split(/:/, 2)
67
+ microseconds ||= 0
68
+ stats[key] = Float(seconds) + (Float(microseconds) / 1_000_000)
69
+ else
70
+ stats[key] = (value =~ /^\d+$/ ? value.to_i : value)
71
+ end
72
+ end
73
+ stats
74
+ end
75
+
76
+ def flush_all(delay = nil)
77
+ check_writable!
78
+ write_command("flush_all #{delay}")
79
+ end
80
+
81
+ alias clear flush_all
82
+
83
+ def gets(keys)
84
+ get(keys, true)
85
+ end
86
+
87
+ def get(keys, cas = false)
88
+ return get([keys], cas)[keys.to_s] unless keys.kind_of?(Array)
89
+ return {} if keys.empty?
90
+
91
+ method = cas ? 'gets' : 'get'
92
+
93
+ results = {}
94
+ read_command("#{method} #{keys.join(' ')}") do |response|
95
+ if cas
96
+ key, flags, length, cas = match_response!(response, /^VALUE ([^\s]+) ([^\s]+) ([^\s]+) ([^\s]+)/)
97
+ else
98
+ key, flags, length = match_response!(response, /^VALUE ([^\s]+) ([^\s]+) ([^\s]+)/)
99
+ end
100
+
101
+ value = socket.read(length.to_i)
102
+ value.memcache_flags = flags.to_i
103
+ value.memcache_cas = cas
104
+ results[key] = value
105
+ end
106
+ results
107
+ end
108
+
109
+ def incr(key, amount = 1)
110
+ check_writable!
111
+ raise MemcacheError, "incr requires unsigned value" if amount < 0
112
+ response = write_command("incr #{key} #{amount}")
113
+ response == "NOT_FOUND\r\n" ? nil : response.to_i
114
+ end
115
+
116
+ def decr(key, amount = 1)
117
+ check_writable!
118
+ raise MemcacheError, "decr requires unsigned value" if amount < 0
119
+ response = write_command("decr #{key} #{amount}")
120
+ response == "NOT_FOUND\r\n" ? nil : response.to_i
121
+ end
122
+
123
+ def delete(key)
124
+ check_writable!
125
+ write_command("delete #{key}") == "DELETED\r\n"
126
+ end
127
+
128
+ def set(key, value, expiry = 0, flags = 0)
129
+ return delete(key) if value.nil?
130
+ check_writable!
131
+ write_command("set #{key} #{flags.to_i} #{expiry.to_i} #{value.to_s.size}", value)
132
+ value
133
+ end
134
+
135
+ def cas(key, value, cas, expiry = 0, flags = 0)
136
+ check_writable!
137
+ response = write_command("cas #{key} #{flags.to_i} #{expiry.to_i} #{value.to_s.size} #{cas.to_i}", value)
138
+ response == "STORED\r\n" ? value : nil
139
+ end
140
+
141
+ def add(key, value, expiry = 0, flags = 0)
142
+ check_writable!
143
+ response = write_command("add #{key} #{flags.to_i} #{expiry.to_i} #{value.to_s.size}", value)
144
+ response == "STORED\r\n" ? value : nil
145
+ end
146
+
147
+ def replace(key, value, expiry = 0, flags = 0)
148
+ check_writable!
149
+ response = write_command("replace #{key} #{flags.to_i} #{expiry.to_i} #{value.to_s.size}", value)
150
+ response == "STORED\r\n" ? value : nil
151
+ end
152
+
153
+ def append(key, value)
154
+ check_writable!
155
+ response = write_command("append #{key} 0 0 #{value.to_s.size}", value)
156
+ response == "STORED\r\n"
157
+ end
158
+
159
+ def prepend(key, value)
160
+ check_writable!
161
+ response = write_command("prepend #{key} 0 0 #{value.to_s.size}", value)
162
+ response == "STORED\r\n"
163
+ end
164
+
165
+ private
166
+
167
+ def check_writable!
168
+ raise MemcacheError, "Update of readonly cache" if readonly?
169
+ end
170
+
171
+ def match_response!(response, regexp)
172
+ # Make sure that the response matches the protocol.
173
+ unexpected_eof! if response.nil?
174
+ match = response.match(regexp)
175
+ raise ServerError, "unexpected response: #{response.inspect}" unless match
176
+
177
+ match.to_a[1, match.size]
178
+ end
179
+
180
+ def send_command(*command)
181
+ command = command.join("\r\n") if command.kind_of?(Array)
182
+ #puts command
183
+ #puts '==========================='
184
+ socket.write("#{command}\r\n")
185
+ response = socket.gets
186
+
187
+ unexpected_eof! if response.nil?
188
+ if response =~ /^(ERROR|CLIENT_ERROR|SERVER_ERROR) (.*)\r\n/
189
+ raise ($1 == 'SERVER_ERROR' ? ServerError : ClientError), $2
190
+ end
191
+
192
+ block_given? ? yield(response) : response
193
+ end
194
+
195
+ def write_command(*command, &block)
196
+ retried = false
197
+ begin
198
+ send_command(*command, &block)
199
+ rescue Exception => e
200
+ puts "Memcache write error: #{e.class}: #{e.to_s}"
201
+ unless retried
202
+ # Close the socket and retry once.
203
+ retried = true
204
+ close
205
+ retry
206
+ end
207
+ close(e) # Mark dead.
208
+ raise(e)
209
+ end
210
+ end
211
+
212
+ def read_command(command, &block)
213
+ raise MemcacheError, "Server dead, will retry at #{retry_at}" unless alive?
214
+ send_command(command) do |response|
215
+ while response do
216
+ return if response == "END\r\n"
217
+ yield(response)
218
+ match_response!(socket.read(2), "\r\n")
219
+ response = socket.gets
220
+ end
221
+ unexpected_eof!
222
+ end
223
+ rescue Exception => e
224
+ puts "Memcache read error: #{e.class}: #{e.to_s}"
225
+ close(e) # Mark dead.
226
+ raise(e) if strict_reads?
227
+ end
228
+
229
+ def socket
230
+ if @socket.nil? or @socket.closed?
231
+ # Attempt to connect.
232
+ @socket = timeout(CONNECT_TIMEOUT) do
233
+ TCPSocket.new(host, port)
234
+ end
235
+ if Socket.constants.include? 'TCP_NODELAY'
236
+ @socket.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
237
+ end
238
+
239
+ @retry_at = nil
240
+ @status = 'CONNECTED'
241
+ end
242
+ @socket
243
+ end
244
+
245
+ def unexpected_eof!
246
+ raise MemcacheError, 'unexpected end of file'
247
+ end
248
+ end
249
+ end