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.
- data/LICENSE +20 -0
- data/README.rdoc +161 -0
- data/Rakefile +55 -0
- data/VERSION +1 -0
- data/lib/memcache.rb +1118 -0
- data/lib/memcache/local_server.rb +91 -0
- data/lib/memcache/migration.rb +23 -0
- data/lib/memcache/null_server.rb +30 -0
- data/lib/memcache/pg_server.rb +159 -0
- data/lib/memcache/segmented_server.rb +89 -0
- data/lib/memcache/server.rb +249 -0
- data/lib/memcache_extended.rb +120 -0
- data/lib/memcache_mock.rb +137 -0
- data/lib/memcache_util.rb +90 -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 +17 -0
- data/test/memcache_server_test_helper.rb +159 -0
- data/test/memcache_test.rb +223 -0
- data/test/test_helper.rb +24 -0
- data/test/test_mem_cache.rb +739 -0
- data/test/test_memcache_extended.rb +44 -0
- data/test/test_memcache_mock.rb +94 -0
- metadata +89 -0
@@ -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
|