remcached 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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