remcached 0.3.1
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/.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
|