1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
|
module EventMachine
module Protocols
# Implements the Memcache protocol (http://code.sixapart.com/svn/memcached/trunk/server/doc/protocol.txt).
# Requires memcached >= 1.2.4 w/ noreply support
#
# == Usage example
#
# EM.run{
# cache = EM::P::Memcache.connect 'localhost', 11211
#
# cache.set :a, 'hello'
# cache.set :b, 'hi'
# cache.set :c, 'how are you?'
# cache.set :d, ''
#
# cache.get(:a){ |v| p v }
# cache.get_hash(:a, :b, :c, :d){ |v| p v }
# cache.get(:a,:b,:c,:d){ |a,b,c,d| p [a,b,c,d] }
#
# cache.get(:a,:z,:b,:y,:d){ |a,z,b,y,d| p [a,z,b,y,d] }
#
# cache.get(:missing){ |m| p [:missing=, m] }
# cache.set(:missing, 'abc'){ p :stored }
# cache.get(:missing){ |m| p [:missing=, m] }
# cache.del(:missing){ p :deleted }
# cache.get(:missing){ |m| p [:missing=, m] }
# }
#
module Memcache
include EM::Deferrable
##
# constants
unless defined? Cempty
# @private
Cstored = 'STORED'.freeze
# @private
Cend = 'END'.freeze
# @private
Cdeleted = 'DELETED'.freeze
# @private
Cunknown = 'NOT_FOUND'.freeze
# @private
Cerror = 'ERROR'.freeze
# @private
Cempty = ''.freeze
# @private
Cdelimiter = "\r\n".freeze
end
##
# commands
# Get the value associated with one or multiple keys
#
# cache.get(:a){ |v| p v }
# cache.get(:a,:b,:c,:d){ |a,b,c,d| p [a,b,c,d] }
#
def get *keys
raise ArgumentError unless block_given?
callback{
keys = keys.map{|k| k.to_s.gsub(/\s/,'_') }
send_data "get #{keys.join(' ')}\r\n"
@get_cbs << [keys, proc{ |values|
yield *keys.map{ |k| values[k] }
}]
}
end
# Set the value for a given key
#
# cache.set :a, 'hello'
# cache.set(:missing, 'abc'){ puts "stored the value!" }
#
def set key, val, exptime = 0, &cb
callback{
val = val.to_s
send_cmd :set, key, 0, exptime, val.respond_to?(:bytesize) ? val.bytesize : val.size, !block_given?
send_data val
send_data Cdelimiter
@set_cbs << cb if cb
}
end
# Gets multiple values as a hash
#
# cache.get_hash(:a, :b, :c, :d){ |h| puts h[:a] }
#
def get_hash *keys
raise ArgumentError unless block_given?
get *keys do |*values|
yield keys.inject({}){ |hash, k| hash.update k => values[keys.index(k)] }
end
end
# Delete the value associated with a key
#
# cache.del :a
# cache.del(:b){ puts "deleted the value!" }
#
def delete key, expires = 0, &cb
callback{
send_data "delete #{key} #{expires}#{cb ? '' : ' noreply'}\r\n"
@del_cbs << cb if cb
}
end
alias del delete
# Connect to a memcached server (must support NOREPLY, memcached >= 1.2.4)
def self.connect host = 'localhost', port = 11211
EM.connect host, port, self, host, port
end
def send_cmd cmd, key, flags = 0, exptime = 0, bytes = 0, noreply = false
send_data "#{cmd} #{key} #{flags} #{exptime} #{bytes}#{noreply ? ' noreply' : ''}\r\n"
end
private :send_cmd
##
# errors
# @private
class ParserError < StandardError
end
##
# em hooks
# @private
def initialize host, port = 11211
@host, @port = host, port
end
# @private
def connection_completed
@get_cbs = []
@set_cbs = []
@del_cbs = []
@values = {}
@reconnecting = false
@connected = true
succeed
# set_delimiter "\r\n"
# set_line_mode
end
#--
# 19Feb09 Switched to a custom parser, LineText2 is recursive and can cause
# stack overflows when there is too much data.
# include EM::P::LineText2
# @private
def receive_data data
(@buffer||='') << data
while index = @buffer.index(Cdelimiter)
begin
line = @buffer.slice!(0,index+2)
process_cmd line
rescue ParserError
@buffer[0...0] = line
break
end
end
end
#--
# def receive_line line
# @private
def process_cmd line
case line.strip
when /^VALUE\s+(.+?)\s+(\d+)\s+(\d+)/ # VALUE <key> <flags> <bytes>
bytes = Integer($3)
# set_binary_mode bytes+2
# @cur_key = $1
if @buffer.size >= bytes + 2
@values[$1] = @buffer.slice!(0,bytes)
@buffer.slice!(0,2) # \r\n
else
raise ParserError
end
when Cend # END
if entry = @get_cbs.shift
keys, cb = entry
cb.call(@values)
end
@values = {}
when Cstored # STORED
if cb = @set_cbs.shift
cb.call(true)
end
when Cdeleted # DELETED
if cb = @del_cbs.shift
cb.call(true)
end
when Cunknown # NOT_FOUND
if cb = @del_cbs.shift
cb.call(false)
end
else
p [:MEMCACHE_UNKNOWN, line]
end
end
#--
# def receive_binary_data data
# @values[@cur_key] = data[0..-3]
# end
# @private
def unbind
if @connected or @reconnecting
EM.add_timer(1){ reconnect @host, @port }
@connected = false
@reconnecting = true
@deferred_status = nil
else
raise 'Unable to connect to memcached server'
end
end
end
end
end
if __FILE__ == $0
# ruby -I ext:lib -r eventmachine -rubygems lib/protocols/memcache.rb
require 'em/spec'
# @private
class TestConnection
include EM::P::Memcache
def send_data data
sent_data << data
end
def sent_data
@sent_data ||= ''
end
def initialize
connection_completed
end
end
EM.describe EM::Protocols::Memcache do
before{
@c = TestConnection.new
}
should 'send get requests' do
@c.get('a'){}
@c.sent_data.should == "get a\r\n"
done
end
should 'send set requests' do
@c.set('a', 1){}
@c.sent_data.should == "set a 0 0 1\r\n1\r\n"
done
end
should 'use noreply on set without block' do
@c.set('a', 1)
@c.sent_data.should == "set a 0 0 1 noreply\r\n1\r\n"
done
end
should 'send delete requests' do
@c.del('a')
@c.sent_data.should == "delete a 0 noreply\r\n"
done
end
should 'work when get returns no values' do
@c.get('a'){ |a|
a.should.be.nil
done
}
@c.receive_data "END\r\n"
end
should 'invoke block on set' do
@c.set('a', 1){
done
}
@c.receive_data "STORED\r\n"
end
should 'invoke block on delete' do
@c.delete('a'){ |found|
found.should.be.false
}
@c.delete('b'){ |found|
found.should.be.true
done
}
@c.receive_data "NOT_FOUND\r\n"
@c.receive_data "DELETED\r\n"
end
should 'parse split responses' do
@c.get('a'){ |a|
a.should == 'abc'
done
}
@c.receive_data "VAL"
@c.receive_data "UE a 0 "
@c.receive_data "3\r\n"
@c.receive_data "ab"
@c.receive_data "c"
@c.receive_data "\r\n"
@c.receive_data "EN"
@c.receive_data "D\r\n"
end
end
end
|