astro-remcached 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +13 -0
- data/VERSION.yml +4 -0
- data/lib/remcached/client.rb +160 -0
- data/lib/remcached/const.rb +60 -0
- data/lib/remcached/pack_array.rb +46 -0
- data/lib/remcached/packet.rb +227 -0
- data/lib/remcached.rb +81 -0
- data/spec/client_spec.rb +151 -0
- data/spec/memcached_spec.rb +58 -0
- data/spec/packet_spec.rb +125 -0
- metadata +64 -0
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
begin
|
2
|
+
require 'jeweler'
|
3
|
+
Jeweler::Tasks.new do |gemspec|
|
4
|
+
gemspec.name = "remcached"
|
5
|
+
gemspec.summary = "Ruby EventMachine memcached client"
|
6
|
+
gemspec.description = gemspec.summary
|
7
|
+
gemspec.email = "astro@spaceboyz.net"
|
8
|
+
gemspec.homepage = "http://github.com/astro/remcached/"
|
9
|
+
gemspec.authors = ["Stephan Maka"]
|
10
|
+
end
|
11
|
+
rescue LoadError
|
12
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
13
|
+
end
|
data/VERSION.yml
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
module Memcached
|
4
|
+
class Connection < EventMachine::Connection
|
5
|
+
def self.connect(host, port=11211, &connect_callback)
|
6
|
+
df = EventMachine::DefaultDeferrable.new
|
7
|
+
df.callback &connect_callback
|
8
|
+
|
9
|
+
EventMachine.connect(host, port, self) do |me|
|
10
|
+
me.instance_eval {
|
11
|
+
@host, @port = host, port
|
12
|
+
@connect_deferrable = df
|
13
|
+
}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def connected?
|
18
|
+
@connected
|
19
|
+
end
|
20
|
+
|
21
|
+
def reconnect
|
22
|
+
@connect_deferrable = EventMachine::DefaultDeferrable.new
|
23
|
+
super @host, @port
|
24
|
+
@connect_deferrable
|
25
|
+
end
|
26
|
+
|
27
|
+
def close
|
28
|
+
close_connection
|
29
|
+
@connected = false
|
30
|
+
@pending.each do |opaque, callback|
|
31
|
+
callback.call :status => Errors::DISCONNECTED
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def post_init
|
36
|
+
@recv_buf = ""
|
37
|
+
@recv_state = :header
|
38
|
+
@connected = false
|
39
|
+
end
|
40
|
+
|
41
|
+
def connection_completed
|
42
|
+
@connected = true
|
43
|
+
@connect_deferrable.succeed(self)
|
44
|
+
end
|
45
|
+
|
46
|
+
RECONNECT_DELAY = 10
|
47
|
+
RECONNECT_JITTER = 5
|
48
|
+
def unbind
|
49
|
+
close
|
50
|
+
EventMachine::Timer.new(RECONNECT_DELAY + rand(RECONNECT_JITTER),
|
51
|
+
method(:reconnect))
|
52
|
+
end
|
53
|
+
|
54
|
+
def send_packet(pkt)
|
55
|
+
send_data pkt.to_s
|
56
|
+
end
|
57
|
+
|
58
|
+
def receive_data(data)
|
59
|
+
@recv_buf += data
|
60
|
+
|
61
|
+
done = false
|
62
|
+
while not done
|
63
|
+
|
64
|
+
if @recv_state == :header && @recv_buf.length >= 24
|
65
|
+
@received = Response.parse_header(@recv_buf[0..23])
|
66
|
+
@recv_buf = @recv_buf[24..-1]
|
67
|
+
@recv_state = :body
|
68
|
+
|
69
|
+
elsif @recv_state == :body && @recv_buf.length >= @received[:total_body_length]
|
70
|
+
@recv_buf = @received.parse_body(@recv_buf)
|
71
|
+
receive_packet(@received)
|
72
|
+
|
73
|
+
@recv_state = :header
|
74
|
+
|
75
|
+
else
|
76
|
+
done = true
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
class Client < Connection
|
83
|
+
def post_init
|
84
|
+
super
|
85
|
+
@opaque_counter = 0
|
86
|
+
@pending = []
|
87
|
+
end
|
88
|
+
|
89
|
+
def send_request(pkt, &callback)
|
90
|
+
@opaque_counter += 1
|
91
|
+
@opaque_counter %= 1 << 32
|
92
|
+
pkt[:opaque] = @opaque_counter
|
93
|
+
send_packet pkt
|
94
|
+
|
95
|
+
if callback
|
96
|
+
@pending << [@opaque_counter, callback]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
##
|
101
|
+
# memcached responses possess the same order as their
|
102
|
+
# corresponding requests. Therefore quiet requests that have not
|
103
|
+
# yielded responses will be dropped silently to free memory from
|
104
|
+
# +@pending+
|
105
|
+
#
|
106
|
+
# When a callback has been fired and returned +:proceed+ without a
|
107
|
+
# succeeding packet, we still keep it referenced around for
|
108
|
+
# commands such as STAT which has multiple response packets.
|
109
|
+
def receive_packet(response)
|
110
|
+
pending_pos = nil
|
111
|
+
pending_callback = nil
|
112
|
+
@pending.each_with_index do |(pending_opaque,pending_cb),i|
|
113
|
+
if response[:opaque] == pending_opaque
|
114
|
+
pending_pos = i
|
115
|
+
pending_callback = pending_cb
|
116
|
+
break
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
if pending_pos
|
121
|
+
@pending = @pending[pending_pos..-1]
|
122
|
+
begin
|
123
|
+
if pending_callback.call(response) != :proceed
|
124
|
+
@pending.shift
|
125
|
+
end
|
126
|
+
rescue Exception => e
|
127
|
+
$stderr.puts "#{e.class}: #{e}\n" + e.backtrace.join("\n")
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
def get(contents, &callback)
|
134
|
+
send_request Request::Get.new(contents), &callback
|
135
|
+
end
|
136
|
+
|
137
|
+
def add(contents, &callback)
|
138
|
+
send_request Request::Add.new(contents), &callback
|
139
|
+
end
|
140
|
+
|
141
|
+
def set(contents, &callback)
|
142
|
+
send_request Request::Set.new(contents), &callback
|
143
|
+
end
|
144
|
+
|
145
|
+
def delete(contents, &callback)
|
146
|
+
send_request Request::Delete.new(contents), &callback
|
147
|
+
end
|
148
|
+
|
149
|
+
# Callback will be called multiple times
|
150
|
+
def stats(contents={}, &callback)
|
151
|
+
send_request Request::Stats.new(contents) do |result|
|
152
|
+
callback.call result
|
153
|
+
|
154
|
+
if result[:status] == Errors::NO_ERROR && result[:key] != ''
|
155
|
+
:proceed
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Memcached
|
2
|
+
module Datatypes
|
3
|
+
RAW_BYTES = 0x00
|
4
|
+
end
|
5
|
+
|
6
|
+
module Errors
|
7
|
+
NO_ERROR = 0x0000
|
8
|
+
KEY_NOT_FOUND = 0x0001
|
9
|
+
KEY_EXISTS = 0x0002
|
10
|
+
VALUE_TOO_LARGE = 0x0003
|
11
|
+
INVALID_ARGS = 0x0004
|
12
|
+
ITEM_NOT_STORED = 0x0005
|
13
|
+
NON_NUMERIC_VALUE = 0x0006
|
14
|
+
|
15
|
+
DISCONNECTED = 0xffff
|
16
|
+
end
|
17
|
+
|
18
|
+
module Commands
|
19
|
+
GET = 0x00
|
20
|
+
SET = 0x01
|
21
|
+
ADD = 0x02
|
22
|
+
REPLACE = 0x03
|
23
|
+
DELETE = 0x04
|
24
|
+
INCREMENT = 0x05
|
25
|
+
DECREMENT = 0x06
|
26
|
+
QUIT = 0x07
|
27
|
+
STAT = 0x10
|
28
|
+
|
29
|
+
=begin
|
30
|
+
Possible values of the one-byte field:
|
31
|
+
0x00 Get
|
32
|
+
0x01 Set
|
33
|
+
0x02 Add
|
34
|
+
0x03 Replace
|
35
|
+
0x04 Delete
|
36
|
+
0x05 Increment
|
37
|
+
0x06 Decrement
|
38
|
+
0x07 Quit
|
39
|
+
0x08 Flush
|
40
|
+
0x09 GetQ
|
41
|
+
0x0A No-op
|
42
|
+
0x0B Version
|
43
|
+
0x0C GetK
|
44
|
+
0x0D GetKQ
|
45
|
+
0x0E Append
|
46
|
+
0x0F Prepend
|
47
|
+
0x10 Stat
|
48
|
+
0x11 SetQ
|
49
|
+
0x12 AddQ
|
50
|
+
0x13 ReplaceQ
|
51
|
+
0x14 DeleteQ
|
52
|
+
0x15 IncrementQ
|
53
|
+
0x16 DecrementQ
|
54
|
+
0x17 QuitQ
|
55
|
+
0x18 FlushQ
|
56
|
+
0x19 AppendQ
|
57
|
+
0x1A PrependQ
|
58
|
+
=end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
##
|
2
|
+
# Works exactly like Array#pack and String#unpack, except that it
|
3
|
+
# inverts 'q' & 'Q' prior packing/after unpacking. This is done to
|
4
|
+
# achieve network byte order for these values on a little-endian machine.
|
5
|
+
#
|
6
|
+
# FIXME: implement check for big-endian machines.
|
7
|
+
module Memcached::PackArray
|
8
|
+
def self.pack(ary, fmt1)
|
9
|
+
fmt2 = ''
|
10
|
+
values = []
|
11
|
+
fmt1.each_char do |c|
|
12
|
+
if c == 'Q' || c == 'q'
|
13
|
+
fmt2 += 'a8'
|
14
|
+
values << [ary.shift].pack(c).reverse
|
15
|
+
else
|
16
|
+
fmt2 += c
|
17
|
+
values << ary.shift
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
values.pack(fmt2)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.unpack(buf, fmt1)
|
25
|
+
fmt2 = ''
|
26
|
+
reverse = []
|
27
|
+
i = 0
|
28
|
+
fmt1.each_char do |c|
|
29
|
+
if c == 'Q' || c == 'q'
|
30
|
+
fmt2 += 'a8'
|
31
|
+
reverse << [i, c]
|
32
|
+
else
|
33
|
+
fmt2 += c
|
34
|
+
end
|
35
|
+
i += 1
|
36
|
+
end
|
37
|
+
|
38
|
+
ary = buf.unpack(fmt2)
|
39
|
+
|
40
|
+
reverse.each do |i, c|
|
41
|
+
ary[i], = ary[i].reverse.unpack(c)
|
42
|
+
end
|
43
|
+
|
44
|
+
ary
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,227 @@
|
|
1
|
+
require 'remcached/pack_array'
|
2
|
+
|
3
|
+
module Memcached
|
4
|
+
class Packet
|
5
|
+
def initialize(contents={})
|
6
|
+
@contents = contents
|
7
|
+
(self.class.fields +
|
8
|
+
self.class.extras).each do |name,fmt,default|
|
9
|
+
self[name] ||= default if default
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def [](field)
|
14
|
+
@contents[field]
|
15
|
+
end
|
16
|
+
|
17
|
+
def []=(field, value)
|
18
|
+
@contents[field] = value
|
19
|
+
end
|
20
|
+
|
21
|
+
# Define fields for subclasses
|
22
|
+
def self.field(name, packed, default=nil)
|
23
|
+
instance_eval do
|
24
|
+
@fields ||= []
|
25
|
+
@fields << [name, packed, default]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.fields
|
30
|
+
parent_class = ancestors[1]
|
31
|
+
parent_fields = parent_class.respond_to?(:fields) ? parent_class.fields : []
|
32
|
+
class_fields = instance_eval { @fields || [] }
|
33
|
+
parent_fields + class_fields
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.extra(name, packed, default=nil)
|
37
|
+
instance_eval do
|
38
|
+
@extras ||= []
|
39
|
+
@extras << [name, packed, default]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.extras
|
44
|
+
instance_eval { @extras || [] }
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.parse_header(buf)
|
48
|
+
pack_fmt = fields.collect { |name,fmt,default| fmt }.join
|
49
|
+
values = PackArray.unpack(buf, pack_fmt)
|
50
|
+
|
51
|
+
contents = {}
|
52
|
+
fields.each do |name,fmt,default|
|
53
|
+
contents[name] = values.shift
|
54
|
+
end
|
55
|
+
|
56
|
+
new contents
|
57
|
+
end
|
58
|
+
|
59
|
+
# Return remaining bytes
|
60
|
+
def parse_body(buf)
|
61
|
+
buf, rest = buf[0..(self[:total_body_length] - 1)], buf[self[:total_body_length]..-1]
|
62
|
+
|
63
|
+
if self[:extras_length] > 0
|
64
|
+
self[:extras] = parse_extras(buf[0..(self[:extras_length]-1)])
|
65
|
+
else
|
66
|
+
self[:extras] = parse_extras("")
|
67
|
+
end
|
68
|
+
if self[:key_length] > 0
|
69
|
+
self[:key] = buf[self[:extras_length]..(self[:extras_length]+self[:key_length]-1)]
|
70
|
+
else
|
71
|
+
self[:key] = ""
|
72
|
+
end
|
73
|
+
self[:value] = buf[(self[:extras_length]+self[:key_length])..-1]
|
74
|
+
|
75
|
+
rest
|
76
|
+
end
|
77
|
+
|
78
|
+
def to_s
|
79
|
+
extras_s = extras_to_s
|
80
|
+
key_s = self[:key].to_s
|
81
|
+
value_s = self[:value].to_s
|
82
|
+
self[:extras_length] = extras_s.length
|
83
|
+
self[:key_length] = key_s.length
|
84
|
+
self[:total_body_length] = extras_s.length + key_s.length + value_s.length
|
85
|
+
header_to_s + extras_s + key_s + value_s
|
86
|
+
end
|
87
|
+
|
88
|
+
protected
|
89
|
+
|
90
|
+
def parse_extras(buf)
|
91
|
+
pack_fmt = self.class.extras.collect { |name,fmt,default| fmt }.join
|
92
|
+
values = PackArray.unpack(buf, pack_fmt)
|
93
|
+
self.class.extras.each do |name,fmt,default|
|
94
|
+
@self[name] = values.shift || default
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def header_to_s
|
99
|
+
pack_fmt = ''
|
100
|
+
values = []
|
101
|
+
self.class.fields.each do |name,fmt,default|
|
102
|
+
values << self[name]
|
103
|
+
pack_fmt += fmt
|
104
|
+
end
|
105
|
+
PackArray.pack(values, pack_fmt)
|
106
|
+
end
|
107
|
+
|
108
|
+
def extras_to_s
|
109
|
+
values = []
|
110
|
+
pack_fmt = ''
|
111
|
+
self.class.extras.each do |name,fmt,default|
|
112
|
+
values << self[name] || default
|
113
|
+
pack_fmt += fmt
|
114
|
+
end
|
115
|
+
|
116
|
+
PackArray.pack(values, pack_fmt)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
##
|
121
|
+
# Request header:
|
122
|
+
#
|
123
|
+
# Byte/ 0 | 1 | 2 | 3 |
|
124
|
+
# / | | | |
|
125
|
+
# |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
|
126
|
+
# +---------------+---------------+---------------+---------------+
|
127
|
+
# 0| Magic | Opcode | Key length |
|
128
|
+
# +---------------+---------------+---------------+---------------+
|
129
|
+
# 4| Extras length | Data type | Reserved |
|
130
|
+
# +---------------+---------------+---------------+---------------+
|
131
|
+
# 8| Total body length |
|
132
|
+
# +---------------+---------------+---------------+---------------+
|
133
|
+
# 12| Opaque |
|
134
|
+
# +---------------+---------------+---------------+---------------+
|
135
|
+
# 16| CAS |
|
136
|
+
# | |
|
137
|
+
# +---------------+---------------+---------------+---------------+
|
138
|
+
# Total 24 bytes
|
139
|
+
class Request < Packet
|
140
|
+
field :magic, 'C', 0x80
|
141
|
+
field :opcode, 'C', 0
|
142
|
+
field :key_length, 'n'
|
143
|
+
field :extras_length, 'C'
|
144
|
+
field :data_type, 'C', 0
|
145
|
+
field :reserved, 'n', 0
|
146
|
+
field :total_body_length, 'N'
|
147
|
+
field :opaque, 'N', 0
|
148
|
+
field :cas, 'Q', 0
|
149
|
+
|
150
|
+
def self.parse_header(buf)
|
151
|
+
me = super
|
152
|
+
me[:magic] == 0x80 ? me : nil
|
153
|
+
end
|
154
|
+
|
155
|
+
class Get < Request
|
156
|
+
def initialize(contents)
|
157
|
+
super(contents.merge :opcode=>Commands::GET)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
class Add < Request
|
162
|
+
extra :flags, 'N', 0
|
163
|
+
extra :expiration, 'N', 0
|
164
|
+
|
165
|
+
def initialize(contents)
|
166
|
+
super(contents.merge :opcode=>Commands::ADD)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
class Set < Request
|
171
|
+
extra :flags, 'N', 0
|
172
|
+
extra :expiration, 'N', 0
|
173
|
+
|
174
|
+
def initialize(contents)
|
175
|
+
super(contents.merge :opcode=>Commands::SET)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
class Delete < Request
|
180
|
+
def initialize(contents)
|
181
|
+
super(contents.merge :opcode=>Commands::DELETE)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
class Stats < Request
|
186
|
+
def initialize(contents)
|
187
|
+
super(contents.merge :opcode=>Commands::STAT)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
##
|
193
|
+
# Response header:
|
194
|
+
#
|
195
|
+
# Byte/ 0 | 1 | 2 | 3 |
|
196
|
+
# / | | | |
|
197
|
+
# |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
|
198
|
+
# +---------------+---------------+---------------+---------------+
|
199
|
+
# 0| Magic | Opcode | Key Length |
|
200
|
+
# +---------------+---------------+---------------+---------------+
|
201
|
+
# 4| Extras length | Data type | Status |
|
202
|
+
# +---------------+---------------+---------------+---------------+
|
203
|
+
# 8| Total body length |
|
204
|
+
# +---------------+---------------+---------------+---------------+
|
205
|
+
# 12| Opaque |
|
206
|
+
# +---------------+---------------+---------------+---------------+
|
207
|
+
# 16| CAS |
|
208
|
+
# | |
|
209
|
+
# +---------------+---------------+---------------+---------------+
|
210
|
+
# Total 24 bytes
|
211
|
+
class Response < Packet
|
212
|
+
field :magic, 'C', 0x81
|
213
|
+
field :opcode, 'C', 0
|
214
|
+
field :key_length, 'n'
|
215
|
+
field :extras_length, 'C'
|
216
|
+
field :data_type, 'C', 0
|
217
|
+
field :status, 'n', Errors::NO_ERROR
|
218
|
+
field :total_body_length, 'N'
|
219
|
+
field :opaque, 'N', 0
|
220
|
+
field :cas, 'Q', 0
|
221
|
+
|
222
|
+
def self.parse_header(buf)
|
223
|
+
me = super
|
224
|
+
me[:magic] == 0x81 ? me : nil
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
data/lib/remcached.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'remcached/const'
|
2
|
+
require 'remcached/packet'
|
3
|
+
require 'remcached/client'
|
4
|
+
|
5
|
+
module Memcached
|
6
|
+
class << self
|
7
|
+
##
|
8
|
+
# +servers+: either Array of host:port strings or Hash of
|
9
|
+
# host:port => weight integers
|
10
|
+
def servers=(servers)
|
11
|
+
if defined?(@clients) && @clients
|
12
|
+
while client = @clients.shift
|
13
|
+
client.close
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
@clients = servers.collect { |server|
|
18
|
+
host, port = server.split(':')
|
19
|
+
Client.connect host, (port ? port.to_i : 11211)
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
def usable?
|
24
|
+
usable_clients.length > 0
|
25
|
+
end
|
26
|
+
|
27
|
+
def usable_clients
|
28
|
+
unless defined?(@clients) && @clients
|
29
|
+
[]
|
30
|
+
else
|
31
|
+
@clients.select { |client| client.connected? }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def client_for_key(key)
|
36
|
+
usable_clients_ = usable_clients
|
37
|
+
if usable_clients_.empty?
|
38
|
+
nil
|
39
|
+
else
|
40
|
+
h = hash_key(key) % usable_clients_.length
|
41
|
+
usable_clients_[h]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def hash_key(key)
|
46
|
+
hashed = 0
|
47
|
+
key.bytes.each_with_index do |b, i|
|
48
|
+
j = key.length - i - 1 % 4
|
49
|
+
hashed ^= b << (j * 8)
|
50
|
+
end
|
51
|
+
hashed
|
52
|
+
end
|
53
|
+
|
54
|
+
def operation(op, contents, &callback)
|
55
|
+
client = client_for_key(contents[:key])
|
56
|
+
if client
|
57
|
+
client.send(op, contents, &callback)
|
58
|
+
elsif callback
|
59
|
+
callback.call :status => Errors::DISCONNECTED
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
##
|
65
|
+
# Memcached operations
|
66
|
+
##
|
67
|
+
|
68
|
+
def add(contents, &callback)
|
69
|
+
operation :add, contents, &callback
|
70
|
+
end
|
71
|
+
def get(contents, &callback)
|
72
|
+
operation :get, contents, &callback
|
73
|
+
end
|
74
|
+
def set(contents, &callback)
|
75
|
+
operation :set, contents, &callback
|
76
|
+
end
|
77
|
+
def delete(contents, &callback)
|
78
|
+
operation :delete, contents, &callback
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/spec/client_spec.rb
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
$: << File.dirname(__FILE__) + '/../lib'
|
2
|
+
require 'remcached'
|
3
|
+
|
4
|
+
describe Memcached::Client do
|
5
|
+
|
6
|
+
def run(&block)
|
7
|
+
EM.run do
|
8
|
+
@cl = Memcached::Client.connect('localhost', &block)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
def stop
|
12
|
+
EM.stop
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
it "should add a value" do
|
17
|
+
run do
|
18
|
+
@cl.add(:key => 'Hello',
|
19
|
+
:value => 'World') do |result|
|
20
|
+
result.should be_kind_of(Memcached::Response)
|
21
|
+
result[:status].should == Memcached::Errors::NO_ERROR
|
22
|
+
result[:cas].should_not == 0
|
23
|
+
stop
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should get a value" do
|
29
|
+
run do
|
30
|
+
@cl.get(:key => 'Hello') do |result|
|
31
|
+
result.should be_kind_of(Memcached::Response)
|
32
|
+
result[:status].should == Memcached::Errors::NO_ERROR
|
33
|
+
result[:value].should == 'World'
|
34
|
+
result[:cas].should_not == 0
|
35
|
+
@old_cas = result[:cas]
|
36
|
+
stop
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should set a value" do
|
42
|
+
run do
|
43
|
+
@cl.set(:key => 'Hello',
|
44
|
+
:value => 'Planet') do |result|
|
45
|
+
result.should be_kind_of(Memcached::Response)
|
46
|
+
result[:status].should == Memcached::Errors::NO_ERROR
|
47
|
+
result[:cas].should_not == 0
|
48
|
+
result[:cas].should_not == @old_cas
|
49
|
+
stop
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should get a value" do
|
55
|
+
run do
|
56
|
+
@cl.get(:key => 'Hello') do |result|
|
57
|
+
result.should be_kind_of(Memcached::Response)
|
58
|
+
result[:status].should == Memcached::Errors::NO_ERROR
|
59
|
+
result[:value].should == 'Planet'
|
60
|
+
result[:cas].should_not == @old_cas
|
61
|
+
stop
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should delete a value" do
|
67
|
+
run do
|
68
|
+
@cl.delete(:key => 'Hello') do |result|
|
69
|
+
result.should be_kind_of(Memcached::Response)
|
70
|
+
result[:status].should == Memcached::Errors::NO_ERROR
|
71
|
+
stop
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should not get a value" do
|
77
|
+
run do
|
78
|
+
@cl.get(:key => 'Hello') do |result|
|
79
|
+
result.should be_kind_of(Memcached::Response)
|
80
|
+
result[:status].should == Memcached::Errors::KEY_NOT_FOUND
|
81
|
+
stop
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
$n = 100
|
87
|
+
context "when incrementing a counter #{$n} times" do
|
88
|
+
it "should initialize the counter" do
|
89
|
+
run do
|
90
|
+
@cl.set(:key => 'counter',
|
91
|
+
:value => '0') do |result|
|
92
|
+
stop
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should count #{$n} times" do
|
98
|
+
$counted = 0
|
99
|
+
def count
|
100
|
+
@cl.get(:key => 'counter') do |result|
|
101
|
+
result[:status].should == Memcached::Errors::NO_ERROR
|
102
|
+
value = result[:value].to_i
|
103
|
+
@cl.set(:key => 'counter',
|
104
|
+
:value => (value + 1).to_s,
|
105
|
+
:cas => result[:cas]) do |result|
|
106
|
+
if result[:status] == Memcached::Errors::KEY_EXISTS
|
107
|
+
count # again
|
108
|
+
else
|
109
|
+
result[:status].should == Memcached::Errors::NO_ERROR
|
110
|
+
$counted += 1
|
111
|
+
stop if $counted >= $n
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
run do
|
117
|
+
$n.times { count }
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
it "should have counted up to #{$n}" do
|
122
|
+
run do
|
123
|
+
@cl.get(:key => 'counter') do |result|
|
124
|
+
result[:status].should == Memcached::Errors::NO_ERROR
|
125
|
+
result[:value].to_i.should == $n
|
126
|
+
stop
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
context "when getting stats" do
|
133
|
+
before :all do
|
134
|
+
@stats = {}
|
135
|
+
run do
|
136
|
+
@cl.stats do |result|
|
137
|
+
result[:status].should == Memcached::Errors::NO_ERROR
|
138
|
+
if result[:key] != ''
|
139
|
+
@stats[result[:key]] = result[:value]
|
140
|
+
else
|
141
|
+
stop
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
it "should have received some keys" do
|
148
|
+
@stats.should include(*%w(pid uptime time version curr_connections total_connections))
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
$: << File.dirname(__FILE__) + '/../lib'
|
2
|
+
require 'remcached'
|
3
|
+
|
4
|
+
describe Memcached::Client do
|
5
|
+
def run(&block)
|
6
|
+
EM.run do
|
7
|
+
Memcached.servers = %w(127.0.0.2 localhost:11212 localhost localhost)
|
8
|
+
|
9
|
+
started = false
|
10
|
+
EM::PeriodicTimer.new(0.1) do
|
11
|
+
if !started && Memcached.usable?
|
12
|
+
started = true
|
13
|
+
block.call
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
def stop
|
20
|
+
EM.stop
|
21
|
+
end
|
22
|
+
|
23
|
+
context "when using multiple servers" do
|
24
|
+
it "should not return the same hash for the succeeding key" do
|
25
|
+
run do
|
26
|
+
Memcached.hash_key('0').should_not == Memcached.hash_key('1')
|
27
|
+
stop
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should not return the same client for the succeeding key" do
|
32
|
+
run do
|
33
|
+
# wait for 2nd client to be connected
|
34
|
+
EM::Timer.new(0.1) do
|
35
|
+
Memcached.client_for_key('0').should_not == Memcached.client_for_key('1')
|
36
|
+
stop
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should spread load (observe from outside :-)" do
|
42
|
+
run do
|
43
|
+
|
44
|
+
n = 10000
|
45
|
+
replies = 0
|
46
|
+
n.times do |i|
|
47
|
+
Memcached.set(:key => "#{i % 100}",
|
48
|
+
:value => rand(1 << 31).to_s) {
|
49
|
+
replies += 1
|
50
|
+
stop if replies >= n
|
51
|
+
}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
data/spec/packet_spec.rb
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
$: << File.dirname(__FILE__) + '/../lib'
|
2
|
+
require 'remcached'
|
3
|
+
|
4
|
+
describe Memcached::Packet do
|
5
|
+
|
6
|
+
context "when generating a request" do
|
7
|
+
it "should set default values" do
|
8
|
+
pkt = Memcached::Request.new
|
9
|
+
pkt[:magic].should == 0x80
|
10
|
+
end
|
11
|
+
|
12
|
+
context "example 4.2.1" do
|
13
|
+
before :all do
|
14
|
+
pkt = Memcached::Request.new(:key => 'Hello')
|
15
|
+
@s = pkt.to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should serialize correctly" do
|
19
|
+
@s.should == "\x80\x00\x00\x05" +
|
20
|
+
"\x00\x00\x00\x00" +
|
21
|
+
"\x00\x00\x00\x05" +
|
22
|
+
"\x00\x00\x00\x00" +
|
23
|
+
"\x00\x00\x00\x00" +
|
24
|
+
"\x00\x00\x00\x00" +
|
25
|
+
"Hello"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context "example 4.3.1 (add)" do
|
30
|
+
before :all do
|
31
|
+
pkt = Memcached::Request::Add.new(:flags => 0xdeadbeef,
|
32
|
+
:expiration => 0xe10,
|
33
|
+
:key => "Hello",
|
34
|
+
:value => "World")
|
35
|
+
@s = pkt.to_s
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should serialize correctly" do
|
39
|
+
@s.should == "\x80\x02\x00\x05" +
|
40
|
+
"\x08\x00\x00\x00" +
|
41
|
+
"\x00\x00\x00\x12" +
|
42
|
+
"\x00\x00\x00\x00" +
|
43
|
+
"\x00\x00\x00\x00" +
|
44
|
+
"\x00\x00\x00\x00" +
|
45
|
+
"\xde\xad\xbe\xef" +
|
46
|
+
"\x00\x00\x0e\x10" +
|
47
|
+
"Hello" +
|
48
|
+
"World"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context "when parsing a response" do
|
54
|
+
context "example 4.1.1" do
|
55
|
+
before :all do
|
56
|
+
s = "\x81\x00\x00\x00\x00\x00\x00\x01" +
|
57
|
+
"\x00\x00\x00\x09\x00\x00\x00\x00" +
|
58
|
+
"\x00\x00\x00\x00\x00\x00\x00\x00" +
|
59
|
+
"Not found"
|
60
|
+
@pkt = Memcached::Response.parse_header(s[0..23])
|
61
|
+
@pkt.parse_body(s[24..-1])
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should return the right class according to magic & opcode" do
|
65
|
+
@pkt[:magic].should == 0x81
|
66
|
+
@pkt[:opcode].should == 0
|
67
|
+
@pkt.class.should == Memcached::Response
|
68
|
+
end
|
69
|
+
it "should return the right data type" do
|
70
|
+
@pkt[:data_type].should == 0
|
71
|
+
end
|
72
|
+
it "should return the right status" do
|
73
|
+
@pkt[:status].should == Memcached::Errors::KEY_NOT_FOUND
|
74
|
+
end
|
75
|
+
it "should return the right opaque" do
|
76
|
+
@pkt[:opaque].should == 0
|
77
|
+
end
|
78
|
+
it "should return the right CAS" do
|
79
|
+
@pkt[:cas].should == 0
|
80
|
+
end
|
81
|
+
it "should parse the body correctly" do
|
82
|
+
@pkt[:extras].should be_empty
|
83
|
+
@pkt[:key].should == ""
|
84
|
+
@pkt[:value].should == "Not found"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context "example 4.2.1" do
|
89
|
+
before :all do
|
90
|
+
s = "\x81\x00\x00\x00" +
|
91
|
+
"\x04\x00\x00\x00" +
|
92
|
+
"\x00\x00\x00\x09" +
|
93
|
+
"\x00\x00\x00\x00" +
|
94
|
+
"\x00\x00\x00\x00" +
|
95
|
+
"\x00\x00\x00\x01" +
|
96
|
+
"\xde\xad\xbe\xef" +
|
97
|
+
"World"
|
98
|
+
@pkt = Memcached::Response.parse_header(s[0..23])
|
99
|
+
@pkt.parse_body(s[24..-1])
|
100
|
+
end
|
101
|
+
|
102
|
+
it "should return the right class according to magic & opcode" do
|
103
|
+
@pkt[:magic].should == 0x81
|
104
|
+
@pkt[:opcode].should == 0
|
105
|
+
@pkt.class.should == Memcached::Response
|
106
|
+
end
|
107
|
+
it "should return the right data type" do
|
108
|
+
@pkt[:data_type].should == 0
|
109
|
+
end
|
110
|
+
it "should return the right status" do
|
111
|
+
@pkt[:status].should == Memcached::Errors::NO_ERROR
|
112
|
+
end
|
113
|
+
it "should return the right opaque" do
|
114
|
+
@pkt[:opaque].should == 0
|
115
|
+
end
|
116
|
+
it "should return the right CAS" do
|
117
|
+
@pkt[:cas].should == 1
|
118
|
+
end
|
119
|
+
it "should parse the body correctly" do
|
120
|
+
@pkt[:key].should == ""
|
121
|
+
@pkt[:value].should == "World"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
metadata
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: astro-remcached
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Stephan Maka
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-09-08 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Ruby EventMachine memcached client
|
17
|
+
email: astro@spaceboyz.net
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- Rakefile
|
26
|
+
- VERSION.yml
|
27
|
+
- lib/remcached.rb
|
28
|
+
- lib/remcached/client.rb
|
29
|
+
- lib/remcached/const.rb
|
30
|
+
- lib/remcached/pack_array.rb
|
31
|
+
- lib/remcached/packet.rb
|
32
|
+
- spec/client_spec.rb
|
33
|
+
- spec/memcached_spec.rb
|
34
|
+
- spec/packet_spec.rb
|
35
|
+
has_rdoc: false
|
36
|
+
homepage: http://github.com/astro/remcached/
|
37
|
+
post_install_message:
|
38
|
+
rdoc_options:
|
39
|
+
- --charset=UTF-8
|
40
|
+
require_paths:
|
41
|
+
- lib
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: "0"
|
47
|
+
version:
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: "0"
|
53
|
+
version:
|
54
|
+
requirements: []
|
55
|
+
|
56
|
+
rubyforge_project:
|
57
|
+
rubygems_version: 1.2.0
|
58
|
+
signing_key:
|
59
|
+
specification_version: 3
|
60
|
+
summary: Ruby EventMachine memcached client
|
61
|
+
test_files:
|
62
|
+
- spec/packet_spec.rb
|
63
|
+
- spec/client_spec.rb
|
64
|
+
- spec/memcached_spec.rb
|