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.
@@ -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