astro-remcached 0.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/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
|