remcached 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/README.rst +105 -0
- data/Rakefile +13 -0
- data/VERSION.yml +4 -0
- data/examples/fill.rb +70 -0
- data/lib/remcached/client.rb +143 -0
- data/lib/remcached/const.rb +64 -0
- data/lib/remcached/pack_array.rb +46 -0
- data/lib/remcached/packet.rb +275 -0
- data/lib/remcached.rb +158 -0
- data/remcached.gemspec +55 -0
- data/spec/client_spec.rb +36 -0
- data/spec/memcached_spec.rb +268 -0
- data/spec/packet_spec.rb +125 -0
- metadata +71 -0
@@ -0,0 +1,275 @@
|
|
1
|
+
require 'remcached/pack_array'
|
2
|
+
|
3
|
+
module Memcached
|
4
|
+
class Packet
|
5
|
+
##
|
6
|
+
# Initialize with fields
|
7
|
+
def initialize(contents={})
|
8
|
+
@contents = contents
|
9
|
+
(self.class.fields +
|
10
|
+
self.class.extras).each do |name,fmt,default|
|
11
|
+
self[name] ||= default if default
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
##
|
16
|
+
# Get field
|
17
|
+
def [](field)
|
18
|
+
@contents[field]
|
19
|
+
end
|
20
|
+
|
21
|
+
##
|
22
|
+
# Set field
|
23
|
+
def []=(field, value)
|
24
|
+
@contents[field] = value
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# Define a field for subclasses
|
29
|
+
def self.field(name, packed, default=nil)
|
30
|
+
instance_eval do
|
31
|
+
@fields ||= []
|
32
|
+
@fields << [name, packed, default]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# Fields of parent and this class
|
38
|
+
def self.fields
|
39
|
+
parent_class = ancestors[1]
|
40
|
+
parent_fields = parent_class.respond_to?(:fields) ? parent_class.fields : []
|
41
|
+
class_fields = instance_eval { @fields || [] }
|
42
|
+
parent_fields + class_fields
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Define an extra for subclasses
|
47
|
+
def self.extra(name, packed, default=nil)
|
48
|
+
instance_eval do
|
49
|
+
@extras ||= []
|
50
|
+
@extras << [name, packed, default]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
##
|
55
|
+
# Extras of this class
|
56
|
+
def self.extras
|
57
|
+
parent_class = ancestors[1]
|
58
|
+
parent_extras = parent_class.respond_to?(:extras) ? parent_class.extras : []
|
59
|
+
class_extras = instance_eval { @extras || [] }
|
60
|
+
parent_extras + class_extras
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# Build a packet by parsing header fields
|
65
|
+
def self.parse_header(buf)
|
66
|
+
pack_fmt = fields.collect { |name,fmt,default| fmt }.join
|
67
|
+
values = PackArray.unpack(buf, pack_fmt)
|
68
|
+
|
69
|
+
contents = {}
|
70
|
+
fields.each do |name,fmt,default|
|
71
|
+
contents[name] = values.shift
|
72
|
+
end
|
73
|
+
|
74
|
+
new contents
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# Parse body of packet when the +:total_body_length+ field is
|
79
|
+
# known by header. Pass it at least +total_body_length+ bytes.
|
80
|
+
#
|
81
|
+
# return:: [String] remaining bytes
|
82
|
+
def parse_body(buf)
|
83
|
+
buf, rest = buf[0..(self[:total_body_length] - 1)], buf[self[:total_body_length]..-1]
|
84
|
+
|
85
|
+
if self[:extras_length] > 0
|
86
|
+
self[:extras] = parse_extras(buf[0..(self[:extras_length]-1)])
|
87
|
+
else
|
88
|
+
self[:extras] = parse_extras("")
|
89
|
+
end
|
90
|
+
if self[:key_length] > 0
|
91
|
+
self[:key] = buf[self[:extras_length]..(self[:extras_length]+self[:key_length]-1)]
|
92
|
+
else
|
93
|
+
self[:key] = ""
|
94
|
+
end
|
95
|
+
self[:value] = buf[(self[:extras_length]+self[:key_length])..-1]
|
96
|
+
|
97
|
+
rest
|
98
|
+
end
|
99
|
+
|
100
|
+
##
|
101
|
+
# Serialize for wire
|
102
|
+
def to_s
|
103
|
+
extras_s = extras_to_s
|
104
|
+
key_s = self[:key].to_s
|
105
|
+
value_s = self[:value].to_s
|
106
|
+
self[:extras_length] = extras_s.length
|
107
|
+
self[:key_length] = key_s.length
|
108
|
+
self[:total_body_length] = extras_s.length + key_s.length + value_s.length
|
109
|
+
header_to_s + extras_s + key_s + value_s
|
110
|
+
end
|
111
|
+
|
112
|
+
protected
|
113
|
+
|
114
|
+
def parse_extras(buf)
|
115
|
+
pack_fmt = self.class.extras.collect { |name,fmt,default| fmt }.join
|
116
|
+
values = PackArray.unpack(buf, pack_fmt)
|
117
|
+
self.class.extras.each do |name,fmt,default|
|
118
|
+
@self[name] = values.shift || default
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def header_to_s
|
123
|
+
pack_fmt = ''
|
124
|
+
values = []
|
125
|
+
self.class.fields.each do |name,fmt,default|
|
126
|
+
values << self[name]
|
127
|
+
pack_fmt += fmt
|
128
|
+
end
|
129
|
+
PackArray.pack(values, pack_fmt)
|
130
|
+
end
|
131
|
+
|
132
|
+
def extras_to_s
|
133
|
+
values = []
|
134
|
+
pack_fmt = ''
|
135
|
+
self.class.extras.each do |name,fmt,default|
|
136
|
+
values << self[name] || default
|
137
|
+
pack_fmt += fmt
|
138
|
+
end
|
139
|
+
|
140
|
+
PackArray.pack(values, pack_fmt)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
##
|
145
|
+
# Request header:
|
146
|
+
#
|
147
|
+
# Byte/ 0 | 1 | 2 | 3 |
|
148
|
+
# / | | | |
|
149
|
+
# |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|
|
150
|
+
# +---------------+---------------+---------------+---------------+
|
151
|
+
# 0| Magic | Opcode | Key length |
|
152
|
+
# +---------------+---------------+---------------+---------------+
|
153
|
+
# 4| Extras length | Data type | Reserved |
|
154
|
+
# +---------------+---------------+---------------+---------------+
|
155
|
+
# 8| Total body length |
|
156
|
+
# +---------------+---------------+---------------+---------------+
|
157
|
+
# 12| Opaque |
|
158
|
+
# +---------------+---------------+---------------+---------------+
|
159
|
+
# 16| CAS |
|
160
|
+
# | |
|
161
|
+
# +---------------+---------------+---------------+---------------+
|
162
|
+
# Total 24 bytes
|
163
|
+
class Request < Packet
|
164
|
+
field :magic, 'C', 0x80
|
165
|
+
field :opcode, 'C', 0
|
166
|
+
field :key_length, 'n'
|
167
|
+
field :extras_length, 'C'
|
168
|
+
field :data_type, 'C', 0
|
169
|
+
field :reserved, 'n', 0
|
170
|
+
field :total_body_length, 'N'
|
171
|
+
field :opaque, 'N', 0
|
172
|
+
field :cas, 'Q', 0
|
173
|
+
|
174
|
+
def self.parse_header(buf)
|
175
|
+
me = super
|
176
|
+
me[:magic] == 0x80 ? me : nil
|
177
|
+
end
|
178
|
+
|
179
|
+
class Get < Request
|
180
|
+
def initialize(contents)
|
181
|
+
super({:opcode=>Commands::GET}.merge(contents))
|
182
|
+
end
|
183
|
+
|
184
|
+
class Quiet < Get
|
185
|
+
def initialize(contents)
|
186
|
+
super({:opcode=>Commands::GETQ}.merge(contents))
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
class Add < Request
|
192
|
+
extra :flags, 'N', 0
|
193
|
+
extra :expiration, 'N', 0
|
194
|
+
|
195
|
+
def initialize(contents)
|
196
|
+
super({:opcode=>Commands::ADD}.merge(contents))
|
197
|
+
end
|
198
|
+
|
199
|
+
class Quiet < Add
|
200
|
+
def initialize(contents)
|
201
|
+
super({:opcode=>Commands::ADDQ}.merge(contents))
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
class Set < Request
|
207
|
+
extra :flags, 'N', 0
|
208
|
+
extra :expiration, 'N', 0
|
209
|
+
|
210
|
+
def initialize(contents)
|
211
|
+
super({:opcode=>Commands::SET}.merge(contents))
|
212
|
+
end
|
213
|
+
|
214
|
+
class Quiet < Set
|
215
|
+
def initialize(contents)
|
216
|
+
super({:opcode=>Commands::SETQ}.merge(contents))
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
class Delete < Request
|
222
|
+
def initialize(contents)
|
223
|
+
super({:opcode=>Commands::DELETE}.merge(contents))
|
224
|
+
end
|
225
|
+
|
226
|
+
class Quiet < Delete
|
227
|
+
def initialize(contents)
|
228
|
+
super({:opcode=>Commands::DELETEQ}.merge(contents))
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
class Stats < Request
|
234
|
+
def initialize(contents)
|
235
|
+
super({:opcode=>Commands::STAT}.merge(contents))
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
##
|
241
|
+
# Response header:
|
242
|
+
#
|
243
|
+
# Byte/ 0 | 1 | 2 | 3 |
|
244
|
+
# / | | | |
|
245
|
+
# |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|
|
246
|
+
# +---------------+---------------+---------------+---------------+
|
247
|
+
# 0| Magic | Opcode | Key Length |
|
248
|
+
# +---------------+---------------+---------------+---------------+
|
249
|
+
# 4| Extras length | Data type | Status |
|
250
|
+
# +---------------+---------------+---------------+---------------+
|
251
|
+
# 8| Total body length |
|
252
|
+
# +---------------+---------------+---------------+---------------+
|
253
|
+
# 12| Opaque |
|
254
|
+
# +---------------+---------------+---------------+---------------+
|
255
|
+
# 16| CAS |
|
256
|
+
# | |
|
257
|
+
# +---------------+---------------+---------------+---------------+
|
258
|
+
# Total 24 bytes
|
259
|
+
class Response < Packet
|
260
|
+
field :magic, 'C', 0x81
|
261
|
+
field :opcode, 'C', 0
|
262
|
+
field :key_length, 'n'
|
263
|
+
field :extras_length, 'C'
|
264
|
+
field :data_type, 'C', 0
|
265
|
+
field :status, 'n', Errors::NO_ERROR
|
266
|
+
field :total_body_length, 'N'
|
267
|
+
field :opaque, 'N', 0
|
268
|
+
field :cas, 'Q', 0
|
269
|
+
|
270
|
+
def self.parse_header(buf)
|
271
|
+
me = super
|
272
|
+
me[:magic] == 0x81 ? me : nil
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
data/lib/remcached.rb
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
require 'remcached/const'
|
2
|
+
require 'remcached/packet'
|
3
|
+
require 'remcached/client'
|
4
|
+
|
5
|
+
module Memcached
|
6
|
+
class << self
|
7
|
+
##
|
8
|
+
# +servers+: Array of host:port strings
|
9
|
+
def servers=(servers)
|
10
|
+
if defined?(@clients) && @clients
|
11
|
+
while client = @clients.shift
|
12
|
+
begin
|
13
|
+
client.close
|
14
|
+
rescue Exception
|
15
|
+
# This is allowed to fail silently
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
@clients = servers.collect { |server|
|
21
|
+
host, port = server.split(':')
|
22
|
+
Client.connect host, (port ? port.to_i : 11211)
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def usable?
|
27
|
+
usable_clients.length > 0
|
28
|
+
end
|
29
|
+
|
30
|
+
def usable_clients
|
31
|
+
unless defined?(@clients) && @clients
|
32
|
+
[]
|
33
|
+
else
|
34
|
+
@clients.select { |client| client.connected? }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def client_for_key(key)
|
39
|
+
usable_clients_ = usable_clients
|
40
|
+
if usable_clients_.empty?
|
41
|
+
nil
|
42
|
+
else
|
43
|
+
h = hash_key(key) % usable_clients_.length
|
44
|
+
usable_clients_[h]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def hash_key(key)
|
49
|
+
hashed = 0
|
50
|
+
i = 0
|
51
|
+
key.each_byte do |b|
|
52
|
+
j = key.length - i - 1 % 4
|
53
|
+
hashed ^= b << (j * 8)
|
54
|
+
i += 1
|
55
|
+
end
|
56
|
+
hashed
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
##
|
61
|
+
# Memcached operations
|
62
|
+
##
|
63
|
+
|
64
|
+
def operation(request_klass, contents, &callback)
|
65
|
+
client = client_for_key(contents[:key])
|
66
|
+
if client
|
67
|
+
client.send_request request_klass.new(contents), &callback
|
68
|
+
elsif callback
|
69
|
+
callback.call :status => Errors::DISCONNECTED
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def add(contents, &callback)
|
74
|
+
operation Request::Add, contents, &callback
|
75
|
+
end
|
76
|
+
def get(contents, &callback)
|
77
|
+
operation Request::Get, contents, &callback
|
78
|
+
end
|
79
|
+
def set(contents, &callback)
|
80
|
+
operation Request::Set, contents, &callback
|
81
|
+
end
|
82
|
+
def delete(contents, &callback)
|
83
|
+
operation Request::Delete, contents, &callback
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
##
|
88
|
+
# Multi operations
|
89
|
+
#
|
90
|
+
##
|
91
|
+
|
92
|
+
def multi_operation(request_klass, contents_list, &callback)
|
93
|
+
if contents_list.empty?
|
94
|
+
callback.call []
|
95
|
+
return self
|
96
|
+
end
|
97
|
+
|
98
|
+
results = {}
|
99
|
+
|
100
|
+
# Assemble client connections per keys
|
101
|
+
client_contents = {}
|
102
|
+
contents_list.each do |contents|
|
103
|
+
client = client_for_key(contents[:key])
|
104
|
+
if client
|
105
|
+
client_contents[client] ||= []
|
106
|
+
client_contents[client] << contents
|
107
|
+
else
|
108
|
+
puts "no client for #{contents[:key].inspect}"
|
109
|
+
results[contents[:key]] = {:status => Memcached::Errors::DISCONNECTED}
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# send requests and wait for responses per client
|
114
|
+
clients_pending = client_contents.length
|
115
|
+
client_contents.each do |client,contents_list|
|
116
|
+
last_i = contents_list.length - 1
|
117
|
+
client_results = {}
|
118
|
+
|
119
|
+
contents_list.each_with_index do |contents,i|
|
120
|
+
if i < last_i
|
121
|
+
request = request_klass::Quiet.new(contents)
|
122
|
+
client.send_request(request) { |response|
|
123
|
+
results[contents[:key]] = response
|
124
|
+
}
|
125
|
+
else # last request for this client
|
126
|
+
request = request_klass.new(contents)
|
127
|
+
client.send_request(request) { |response|
|
128
|
+
results[contents[:key]] = response
|
129
|
+
clients_pending -= 1
|
130
|
+
if clients_pending < 1
|
131
|
+
callback.call results
|
132
|
+
end
|
133
|
+
}
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
self
|
139
|
+
end
|
140
|
+
|
141
|
+
def multi_add(contents_list, &callback)
|
142
|
+
multi_operation Request::Add, contents_list, &callback
|
143
|
+
end
|
144
|
+
|
145
|
+
def multi_get(contents_list, &callback)
|
146
|
+
multi_operation Request::Get, contents_list, &callback
|
147
|
+
end
|
148
|
+
|
149
|
+
def multi_set(contents_list, &callback)
|
150
|
+
multi_operation Request::Set, contents_list, &callback
|
151
|
+
end
|
152
|
+
|
153
|
+
def multi_delete(contents_list, &callback)
|
154
|
+
multi_operation Request::Delete, contents_list, &callback
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
end
|
data/remcached.gemspec
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{remcached}
|
8
|
+
s.version = "0.3.1"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Stephan Maka"]
|
12
|
+
s.date = %q{2009-10-12}
|
13
|
+
s.description = %q{Ruby EventMachine memcached client}
|
14
|
+
s.email = %q{astro@spaceboyz.net}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"README.rst"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
".gitignore",
|
20
|
+
"README.rst",
|
21
|
+
"Rakefile",
|
22
|
+
"VERSION.yml",
|
23
|
+
"examples/fill.rb",
|
24
|
+
"lib/remcached.rb",
|
25
|
+
"lib/remcached/client.rb",
|
26
|
+
"lib/remcached/const.rb",
|
27
|
+
"lib/remcached/pack_array.rb",
|
28
|
+
"lib/remcached/packet.rb",
|
29
|
+
"remcached.gemspec",
|
30
|
+
"spec/client_spec.rb",
|
31
|
+
"spec/memcached_spec.rb",
|
32
|
+
"spec/packet_spec.rb"
|
33
|
+
]
|
34
|
+
s.homepage = %q{http://github.com/astro/remcached/}
|
35
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
36
|
+
s.require_paths = ["lib"]
|
37
|
+
s.rubygems_version = %q{1.3.5}
|
38
|
+
s.summary = %q{Ruby EventMachine memcached client}
|
39
|
+
s.test_files = [
|
40
|
+
"spec/client_spec.rb",
|
41
|
+
"spec/memcached_spec.rb",
|
42
|
+
"spec/packet_spec.rb",
|
43
|
+
"examples/fill.rb"
|
44
|
+
]
|
45
|
+
|
46
|
+
if s.respond_to? :specification_version then
|
47
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
48
|
+
s.specification_version = 3
|
49
|
+
|
50
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
51
|
+
else
|
52
|
+
end
|
53
|
+
else
|
54
|
+
end
|
55
|
+
end
|
data/spec/client_spec.rb
ADDED
@@ -0,0 +1,36 @@
|
|
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
|
+
@cl.close_connection
|
13
|
+
EM.stop
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
context "when getting stats" do
|
18
|
+
before :all do
|
19
|
+
@stats = {}
|
20
|
+
run do
|
21
|
+
@cl.stats do |result|
|
22
|
+
result[:status].should == Memcached::Errors::NO_ERROR
|
23
|
+
if result[:key] != ''
|
24
|
+
@stats[result[:key]] = result[:value]
|
25
|
+
else
|
26
|
+
stop
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should have received some keys" do
|
33
|
+
@stats.should include(*%w(pid uptime time version curr_connections total_connections))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|