astro-remcached 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ begin
2
+ require 'jeweler'
3
+ Jeweler::Tasks.new do |gemspec|
4
+ gemspec.name = "remcached"
5
+ gemspec.summary = "Ruby EventMachine memcached client"
6
+ gemspec.description = gemspec.summary
7
+ gemspec.email = "astro@spaceboyz.net"
8
+ gemspec.homepage = "http://github.com/astro/remcached/"
9
+ gemspec.authors = ["Stephan Maka"]
10
+ end
11
+ rescue LoadError
12
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
13
+ end
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 2
4
+ :patch: 0
@@ -0,0 +1,160 @@
1
+ require 'eventmachine'
2
+
3
+ module Memcached
4
+ class Connection < EventMachine::Connection
5
+ def self.connect(host, port=11211, &connect_callback)
6
+ df = EventMachine::DefaultDeferrable.new
7
+ df.callback &connect_callback
8
+
9
+ EventMachine.connect(host, port, self) do |me|
10
+ me.instance_eval {
11
+ @host, @port = host, port
12
+ @connect_deferrable = df
13
+ }
14
+ end
15
+ end
16
+
17
+ def connected?
18
+ @connected
19
+ end
20
+
21
+ def reconnect
22
+ @connect_deferrable = EventMachine::DefaultDeferrable.new
23
+ super @host, @port
24
+ @connect_deferrable
25
+ end
26
+
27
+ def close
28
+ close_connection
29
+ @connected = false
30
+ @pending.each do |opaque, callback|
31
+ callback.call :status => Errors::DISCONNECTED
32
+ end
33
+ end
34
+
35
+ def post_init
36
+ @recv_buf = ""
37
+ @recv_state = :header
38
+ @connected = false
39
+ end
40
+
41
+ def connection_completed
42
+ @connected = true
43
+ @connect_deferrable.succeed(self)
44
+ end
45
+
46
+ RECONNECT_DELAY = 10
47
+ RECONNECT_JITTER = 5
48
+ def unbind
49
+ close
50
+ EventMachine::Timer.new(RECONNECT_DELAY + rand(RECONNECT_JITTER),
51
+ method(:reconnect))
52
+ end
53
+
54
+ def send_packet(pkt)
55
+ send_data pkt.to_s
56
+ end
57
+
58
+ def receive_data(data)
59
+ @recv_buf += data
60
+
61
+ done = false
62
+ while not done
63
+
64
+ if @recv_state == :header && @recv_buf.length >= 24
65
+ @received = Response.parse_header(@recv_buf[0..23])
66
+ @recv_buf = @recv_buf[24..-1]
67
+ @recv_state = :body
68
+
69
+ elsif @recv_state == :body && @recv_buf.length >= @received[:total_body_length]
70
+ @recv_buf = @received.parse_body(@recv_buf)
71
+ receive_packet(@received)
72
+
73
+ @recv_state = :header
74
+
75
+ else
76
+ done = true
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ class Client < Connection
83
+ def post_init
84
+ super
85
+ @opaque_counter = 0
86
+ @pending = []
87
+ end
88
+
89
+ def send_request(pkt, &callback)
90
+ @opaque_counter += 1
91
+ @opaque_counter %= 1 << 32
92
+ pkt[:opaque] = @opaque_counter
93
+ send_packet pkt
94
+
95
+ if callback
96
+ @pending << [@opaque_counter, callback]
97
+ end
98
+ end
99
+
100
+ ##
101
+ # memcached responses possess the same order as their
102
+ # corresponding requests. Therefore quiet requests that have not
103
+ # yielded responses will be dropped silently to free memory from
104
+ # +@pending+
105
+ #
106
+ # When a callback has been fired and returned +:proceed+ without a
107
+ # succeeding packet, we still keep it referenced around for
108
+ # commands such as STAT which has multiple response packets.
109
+ def receive_packet(response)
110
+ pending_pos = nil
111
+ pending_callback = nil
112
+ @pending.each_with_index do |(pending_opaque,pending_cb),i|
113
+ if response[:opaque] == pending_opaque
114
+ pending_pos = i
115
+ pending_callback = pending_cb
116
+ break
117
+ end
118
+ end
119
+
120
+ if pending_pos
121
+ @pending = @pending[pending_pos..-1]
122
+ begin
123
+ if pending_callback.call(response) != :proceed
124
+ @pending.shift
125
+ end
126
+ rescue Exception => e
127
+ $stderr.puts "#{e.class}: #{e}\n" + e.backtrace.join("\n")
128
+ end
129
+ end
130
+ end
131
+
132
+
133
+ def get(contents, &callback)
134
+ send_request Request::Get.new(contents), &callback
135
+ end
136
+
137
+ def add(contents, &callback)
138
+ send_request Request::Add.new(contents), &callback
139
+ end
140
+
141
+ def set(contents, &callback)
142
+ send_request Request::Set.new(contents), &callback
143
+ end
144
+
145
+ def delete(contents, &callback)
146
+ send_request Request::Delete.new(contents), &callback
147
+ end
148
+
149
+ # Callback will be called multiple times
150
+ def stats(contents={}, &callback)
151
+ send_request Request::Stats.new(contents) do |result|
152
+ callback.call result
153
+
154
+ if result[:status] == Errors::NO_ERROR && result[:key] != ''
155
+ :proceed
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,60 @@
1
+ module Memcached
2
+ module Datatypes
3
+ RAW_BYTES = 0x00
4
+ end
5
+
6
+ module Errors
7
+ NO_ERROR = 0x0000
8
+ KEY_NOT_FOUND = 0x0001
9
+ KEY_EXISTS = 0x0002
10
+ VALUE_TOO_LARGE = 0x0003
11
+ INVALID_ARGS = 0x0004
12
+ ITEM_NOT_STORED = 0x0005
13
+ NON_NUMERIC_VALUE = 0x0006
14
+
15
+ DISCONNECTED = 0xffff
16
+ end
17
+
18
+ module Commands
19
+ GET = 0x00
20
+ SET = 0x01
21
+ ADD = 0x02
22
+ REPLACE = 0x03
23
+ DELETE = 0x04
24
+ INCREMENT = 0x05
25
+ DECREMENT = 0x06
26
+ QUIT = 0x07
27
+ STAT = 0x10
28
+
29
+ =begin
30
+ Possible values of the one-byte field:
31
+ 0x00 Get
32
+ 0x01 Set
33
+ 0x02 Add
34
+ 0x03 Replace
35
+ 0x04 Delete
36
+ 0x05 Increment
37
+ 0x06 Decrement
38
+ 0x07 Quit
39
+ 0x08 Flush
40
+ 0x09 GetQ
41
+ 0x0A No-op
42
+ 0x0B Version
43
+ 0x0C GetK
44
+ 0x0D GetKQ
45
+ 0x0E Append
46
+ 0x0F Prepend
47
+ 0x10 Stat
48
+ 0x11 SetQ
49
+ 0x12 AddQ
50
+ 0x13 ReplaceQ
51
+ 0x14 DeleteQ
52
+ 0x15 IncrementQ
53
+ 0x16 DecrementQ
54
+ 0x17 QuitQ
55
+ 0x18 FlushQ
56
+ 0x19 AppendQ
57
+ 0x1A PrependQ
58
+ =end
59
+ end
60
+ end
@@ -0,0 +1,46 @@
1
+ ##
2
+ # Works exactly like Array#pack and String#unpack, except that it
3
+ # inverts 'q' & 'Q' prior packing/after unpacking. This is done to
4
+ # achieve network byte order for these values on a little-endian machine.
5
+ #
6
+ # FIXME: implement check for big-endian machines.
7
+ module Memcached::PackArray
8
+ def self.pack(ary, fmt1)
9
+ fmt2 = ''
10
+ values = []
11
+ fmt1.each_char do |c|
12
+ if c == 'Q' || c == 'q'
13
+ fmt2 += 'a8'
14
+ values << [ary.shift].pack(c).reverse
15
+ else
16
+ fmt2 += c
17
+ values << ary.shift
18
+ end
19
+ end
20
+
21
+ values.pack(fmt2)
22
+ end
23
+
24
+ def self.unpack(buf, fmt1)
25
+ fmt2 = ''
26
+ reverse = []
27
+ i = 0
28
+ fmt1.each_char do |c|
29
+ if c == 'Q' || c == 'q'
30
+ fmt2 += 'a8'
31
+ reverse << [i, c]
32
+ else
33
+ fmt2 += c
34
+ end
35
+ i += 1
36
+ end
37
+
38
+ ary = buf.unpack(fmt2)
39
+
40
+ reverse.each do |i, c|
41
+ ary[i], = ary[i].reverse.unpack(c)
42
+ end
43
+
44
+ ary
45
+ end
46
+ end
@@ -0,0 +1,227 @@
1
+ require 'remcached/pack_array'
2
+
3
+ module Memcached
4
+ class Packet
5
+ def initialize(contents={})
6
+ @contents = contents
7
+ (self.class.fields +
8
+ self.class.extras).each do |name,fmt,default|
9
+ self[name] ||= default if default
10
+ end
11
+ end
12
+
13
+ def [](field)
14
+ @contents[field]
15
+ end
16
+
17
+ def []=(field, value)
18
+ @contents[field] = value
19
+ end
20
+
21
+ # Define fields for subclasses
22
+ def self.field(name, packed, default=nil)
23
+ instance_eval do
24
+ @fields ||= []
25
+ @fields << [name, packed, default]
26
+ end
27
+ end
28
+
29
+ def self.fields
30
+ parent_class = ancestors[1]
31
+ parent_fields = parent_class.respond_to?(:fields) ? parent_class.fields : []
32
+ class_fields = instance_eval { @fields || [] }
33
+ parent_fields + class_fields
34
+ end
35
+
36
+ def self.extra(name, packed, default=nil)
37
+ instance_eval do
38
+ @extras ||= []
39
+ @extras << [name, packed, default]
40
+ end
41
+ end
42
+
43
+ def self.extras
44
+ instance_eval { @extras || [] }
45
+ end
46
+
47
+ def self.parse_header(buf)
48
+ pack_fmt = fields.collect { |name,fmt,default| fmt }.join
49
+ values = PackArray.unpack(buf, pack_fmt)
50
+
51
+ contents = {}
52
+ fields.each do |name,fmt,default|
53
+ contents[name] = values.shift
54
+ end
55
+
56
+ new contents
57
+ end
58
+
59
+ # Return remaining bytes
60
+ def parse_body(buf)
61
+ buf, rest = buf[0..(self[:total_body_length] - 1)], buf[self[:total_body_length]..-1]
62
+
63
+ if self[:extras_length] > 0
64
+ self[:extras] = parse_extras(buf[0..(self[:extras_length]-1)])
65
+ else
66
+ self[:extras] = parse_extras("")
67
+ end
68
+ if self[:key_length] > 0
69
+ self[:key] = buf[self[:extras_length]..(self[:extras_length]+self[:key_length]-1)]
70
+ else
71
+ self[:key] = ""
72
+ end
73
+ self[:value] = buf[(self[:extras_length]+self[:key_length])..-1]
74
+
75
+ rest
76
+ end
77
+
78
+ def to_s
79
+ extras_s = extras_to_s
80
+ key_s = self[:key].to_s
81
+ value_s = self[:value].to_s
82
+ self[:extras_length] = extras_s.length
83
+ self[:key_length] = key_s.length
84
+ self[:total_body_length] = extras_s.length + key_s.length + value_s.length
85
+ header_to_s + extras_s + key_s + value_s
86
+ end
87
+
88
+ protected
89
+
90
+ def parse_extras(buf)
91
+ pack_fmt = self.class.extras.collect { |name,fmt,default| fmt }.join
92
+ values = PackArray.unpack(buf, pack_fmt)
93
+ self.class.extras.each do |name,fmt,default|
94
+ @self[name] = values.shift || default
95
+ end
96
+ end
97
+
98
+ def header_to_s
99
+ pack_fmt = ''
100
+ values = []
101
+ self.class.fields.each do |name,fmt,default|
102
+ values << self[name]
103
+ pack_fmt += fmt
104
+ end
105
+ PackArray.pack(values, pack_fmt)
106
+ end
107
+
108
+ def extras_to_s
109
+ values = []
110
+ pack_fmt = ''
111
+ self.class.extras.each do |name,fmt,default|
112
+ values << self[name] || default
113
+ pack_fmt += fmt
114
+ end
115
+
116
+ PackArray.pack(values, pack_fmt)
117
+ end
118
+ end
119
+
120
+ ##
121
+ # Request header:
122
+ #
123
+ # Byte/ 0 | 1 | 2 | 3 |
124
+ # / | | | |
125
+ # |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|
126
+ # +---------------+---------------+---------------+---------------+
127
+ # 0| Magic | Opcode | Key length |
128
+ # +---------------+---------------+---------------+---------------+
129
+ # 4| Extras length | Data type | Reserved |
130
+ # +---------------+---------------+---------------+---------------+
131
+ # 8| Total body length |
132
+ # +---------------+---------------+---------------+---------------+
133
+ # 12| Opaque |
134
+ # +---------------+---------------+---------------+---------------+
135
+ # 16| CAS |
136
+ # | |
137
+ # +---------------+---------------+---------------+---------------+
138
+ # Total 24 bytes
139
+ class Request < Packet
140
+ field :magic, 'C', 0x80
141
+ field :opcode, 'C', 0
142
+ field :key_length, 'n'
143
+ field :extras_length, 'C'
144
+ field :data_type, 'C', 0
145
+ field :reserved, 'n', 0
146
+ field :total_body_length, 'N'
147
+ field :opaque, 'N', 0
148
+ field :cas, 'Q', 0
149
+
150
+ def self.parse_header(buf)
151
+ me = super
152
+ me[:magic] == 0x80 ? me : nil
153
+ end
154
+
155
+ class Get < Request
156
+ def initialize(contents)
157
+ super(contents.merge :opcode=>Commands::GET)
158
+ end
159
+ end
160
+
161
+ class Add < Request
162
+ extra :flags, 'N', 0
163
+ extra :expiration, 'N', 0
164
+
165
+ def initialize(contents)
166
+ super(contents.merge :opcode=>Commands::ADD)
167
+ end
168
+ end
169
+
170
+ class Set < Request
171
+ extra :flags, 'N', 0
172
+ extra :expiration, 'N', 0
173
+
174
+ def initialize(contents)
175
+ super(contents.merge :opcode=>Commands::SET)
176
+ end
177
+ end
178
+
179
+ class Delete < Request
180
+ def initialize(contents)
181
+ super(contents.merge :opcode=>Commands::DELETE)
182
+ end
183
+ end
184
+
185
+ class Stats < Request
186
+ def initialize(contents)
187
+ super(contents.merge :opcode=>Commands::STAT)
188
+ end
189
+ end
190
+ end
191
+
192
+ ##
193
+ # Response header:
194
+ #
195
+ # Byte/ 0 | 1 | 2 | 3 |
196
+ # / | | | |
197
+ # |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|
198
+ # +---------------+---------------+---------------+---------------+
199
+ # 0| Magic | Opcode | Key Length |
200
+ # +---------------+---------------+---------------+---------------+
201
+ # 4| Extras length | Data type | Status |
202
+ # +---------------+---------------+---------------+---------------+
203
+ # 8| Total body length |
204
+ # +---------------+---------------+---------------+---------------+
205
+ # 12| Opaque |
206
+ # +---------------+---------------+---------------+---------------+
207
+ # 16| CAS |
208
+ # | |
209
+ # +---------------+---------------+---------------+---------------+
210
+ # Total 24 bytes
211
+ class Response < Packet
212
+ field :magic, 'C', 0x81
213
+ field :opcode, 'C', 0
214
+ field :key_length, 'n'
215
+ field :extras_length, 'C'
216
+ field :data_type, 'C', 0
217
+ field :status, 'n', Errors::NO_ERROR
218
+ field :total_body_length, 'N'
219
+ field :opaque, 'N', 0
220
+ field :cas, 'Q', 0
221
+
222
+ def self.parse_header(buf)
223
+ me = super
224
+ me[:magic] == 0x81 ? me : nil
225
+ end
226
+ end
227
+ end
data/lib/remcached.rb ADDED
@@ -0,0 +1,81 @@
1
+ require 'remcached/const'
2
+ require 'remcached/packet'
3
+ require 'remcached/client'
4
+
5
+ module Memcached
6
+ class << self
7
+ ##
8
+ # +servers+: either Array of host:port strings or Hash of
9
+ # host:port => weight integers
10
+ def servers=(servers)
11
+ if defined?(@clients) && @clients
12
+ while client = @clients.shift
13
+ client.close
14
+ end
15
+ end
16
+
17
+ @clients = servers.collect { |server|
18
+ host, port = server.split(':')
19
+ Client.connect host, (port ? port.to_i : 11211)
20
+ }
21
+ end
22
+
23
+ def usable?
24
+ usable_clients.length > 0
25
+ end
26
+
27
+ def usable_clients
28
+ unless defined?(@clients) && @clients
29
+ []
30
+ else
31
+ @clients.select { |client| client.connected? }
32
+ end
33
+ end
34
+
35
+ def client_for_key(key)
36
+ usable_clients_ = usable_clients
37
+ if usable_clients_.empty?
38
+ nil
39
+ else
40
+ h = hash_key(key) % usable_clients_.length
41
+ usable_clients_[h]
42
+ end
43
+ end
44
+
45
+ def hash_key(key)
46
+ hashed = 0
47
+ key.bytes.each_with_index do |b, i|
48
+ j = key.length - i - 1 % 4
49
+ hashed ^= b << (j * 8)
50
+ end
51
+ hashed
52
+ end
53
+
54
+ def operation(op, contents, &callback)
55
+ client = client_for_key(contents[:key])
56
+ if client
57
+ client.send(op, contents, &callback)
58
+ elsif callback
59
+ callback.call :status => Errors::DISCONNECTED
60
+ end
61
+ end
62
+
63
+
64
+ ##
65
+ # Memcached operations
66
+ ##
67
+
68
+ def add(contents, &callback)
69
+ operation :add, contents, &callback
70
+ end
71
+ def get(contents, &callback)
72
+ operation :get, contents, &callback
73
+ end
74
+ def set(contents, &callback)
75
+ operation :set, contents, &callback
76
+ end
77
+ def delete(contents, &callback)
78
+ operation :delete, contents, &callback
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,151 @@
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
+ EM.stop
13
+ end
14
+
15
+
16
+ it "should add a value" do
17
+ run do
18
+ @cl.add(:key => 'Hello',
19
+ :value => 'World') do |result|
20
+ result.should be_kind_of(Memcached::Response)
21
+ result[:status].should == Memcached::Errors::NO_ERROR
22
+ result[:cas].should_not == 0
23
+ stop
24
+ end
25
+ end
26
+ end
27
+
28
+ it "should get a value" do
29
+ run do
30
+ @cl.get(:key => 'Hello') do |result|
31
+ result.should be_kind_of(Memcached::Response)
32
+ result[:status].should == Memcached::Errors::NO_ERROR
33
+ result[:value].should == 'World'
34
+ result[:cas].should_not == 0
35
+ @old_cas = result[:cas]
36
+ stop
37
+ end
38
+ end
39
+ end
40
+
41
+ it "should set a value" do
42
+ run do
43
+ @cl.set(:key => 'Hello',
44
+ :value => 'Planet') do |result|
45
+ result.should be_kind_of(Memcached::Response)
46
+ result[:status].should == Memcached::Errors::NO_ERROR
47
+ result[:cas].should_not == 0
48
+ result[:cas].should_not == @old_cas
49
+ stop
50
+ end
51
+ end
52
+ end
53
+
54
+ it "should get a value" do
55
+ run do
56
+ @cl.get(:key => 'Hello') do |result|
57
+ result.should be_kind_of(Memcached::Response)
58
+ result[:status].should == Memcached::Errors::NO_ERROR
59
+ result[:value].should == 'Planet'
60
+ result[:cas].should_not == @old_cas
61
+ stop
62
+ end
63
+ end
64
+ end
65
+
66
+ it "should delete a value" do
67
+ run do
68
+ @cl.delete(:key => 'Hello') do |result|
69
+ result.should be_kind_of(Memcached::Response)
70
+ result[:status].should == Memcached::Errors::NO_ERROR
71
+ stop
72
+ end
73
+ end
74
+ end
75
+
76
+ it "should not get a value" do
77
+ run do
78
+ @cl.get(:key => 'Hello') do |result|
79
+ result.should be_kind_of(Memcached::Response)
80
+ result[:status].should == Memcached::Errors::KEY_NOT_FOUND
81
+ stop
82
+ end
83
+ end
84
+ end
85
+
86
+ $n = 100
87
+ context "when incrementing a counter #{$n} times" do
88
+ it "should initialize the counter" do
89
+ run do
90
+ @cl.set(:key => 'counter',
91
+ :value => '0') do |result|
92
+ stop
93
+ end
94
+ end
95
+ end
96
+
97
+ it "should count #{$n} times" do
98
+ $counted = 0
99
+ def count
100
+ @cl.get(:key => 'counter') do |result|
101
+ result[:status].should == Memcached::Errors::NO_ERROR
102
+ value = result[:value].to_i
103
+ @cl.set(:key => 'counter',
104
+ :value => (value + 1).to_s,
105
+ :cas => result[:cas]) do |result|
106
+ if result[:status] == Memcached::Errors::KEY_EXISTS
107
+ count # again
108
+ else
109
+ result[:status].should == Memcached::Errors::NO_ERROR
110
+ $counted += 1
111
+ stop if $counted >= $n
112
+ end
113
+ end
114
+ end
115
+ end
116
+ run do
117
+ $n.times { count }
118
+ end
119
+ end
120
+
121
+ it "should have counted up to #{$n}" do
122
+ run do
123
+ @cl.get(:key => 'counter') do |result|
124
+ result[:status].should == Memcached::Errors::NO_ERROR
125
+ result[:value].to_i.should == $n
126
+ stop
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ context "when getting stats" do
133
+ before :all do
134
+ @stats = {}
135
+ run do
136
+ @cl.stats do |result|
137
+ result[:status].should == Memcached::Errors::NO_ERROR
138
+ if result[:key] != ''
139
+ @stats[result[:key]] = result[:value]
140
+ else
141
+ stop
142
+ end
143
+ end
144
+ end
145
+ end
146
+
147
+ it "should have received some keys" do
148
+ @stats.should include(*%w(pid uptime time version curr_connections total_connections))
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,58 @@
1
+ $: << File.dirname(__FILE__) + '/../lib'
2
+ require 'remcached'
3
+
4
+ describe Memcached::Client do
5
+ def run(&block)
6
+ EM.run do
7
+ Memcached.servers = %w(127.0.0.2 localhost:11212 localhost localhost)
8
+
9
+ started = false
10
+ EM::PeriodicTimer.new(0.1) do
11
+ if !started && Memcached.usable?
12
+ started = true
13
+ block.call
14
+ end
15
+ end
16
+
17
+ end
18
+ end
19
+ def stop
20
+ EM.stop
21
+ end
22
+
23
+ context "when using multiple servers" do
24
+ it "should not return the same hash for the succeeding key" do
25
+ run do
26
+ Memcached.hash_key('0').should_not == Memcached.hash_key('1')
27
+ stop
28
+ end
29
+ end
30
+
31
+ it "should not return the same client for the succeeding key" do
32
+ run do
33
+ # wait for 2nd client to be connected
34
+ EM::Timer.new(0.1) do
35
+ Memcached.client_for_key('0').should_not == Memcached.client_for_key('1')
36
+ stop
37
+ end
38
+ end
39
+ end
40
+
41
+ it "should spread load (observe from outside :-)" do
42
+ run do
43
+
44
+ n = 10000
45
+ replies = 0
46
+ n.times do |i|
47
+ Memcached.set(:key => "#{i % 100}",
48
+ :value => rand(1 << 31).to_s) {
49
+ replies += 1
50
+ stop if replies >= n
51
+ }
52
+ end
53
+ end
54
+
55
+ end
56
+ end
57
+
58
+ end
@@ -0,0 +1,125 @@
1
+ $: << File.dirname(__FILE__) + '/../lib'
2
+ require 'remcached'
3
+
4
+ describe Memcached::Packet do
5
+
6
+ context "when generating a request" do
7
+ it "should set default values" do
8
+ pkt = Memcached::Request.new
9
+ pkt[:magic].should == 0x80
10
+ end
11
+
12
+ context "example 4.2.1" do
13
+ before :all do
14
+ pkt = Memcached::Request.new(:key => 'Hello')
15
+ @s = pkt.to_s
16
+ end
17
+
18
+ it "should serialize correctly" do
19
+ @s.should == "\x80\x00\x00\x05" +
20
+ "\x00\x00\x00\x00" +
21
+ "\x00\x00\x00\x05" +
22
+ "\x00\x00\x00\x00" +
23
+ "\x00\x00\x00\x00" +
24
+ "\x00\x00\x00\x00" +
25
+ "Hello"
26
+ end
27
+ end
28
+
29
+ context "example 4.3.1 (add)" do
30
+ before :all do
31
+ pkt = Memcached::Request::Add.new(:flags => 0xdeadbeef,
32
+ :expiration => 0xe10,
33
+ :key => "Hello",
34
+ :value => "World")
35
+ @s = pkt.to_s
36
+ end
37
+
38
+ it "should serialize correctly" do
39
+ @s.should == "\x80\x02\x00\x05" +
40
+ "\x08\x00\x00\x00" +
41
+ "\x00\x00\x00\x12" +
42
+ "\x00\x00\x00\x00" +
43
+ "\x00\x00\x00\x00" +
44
+ "\x00\x00\x00\x00" +
45
+ "\xde\xad\xbe\xef" +
46
+ "\x00\x00\x0e\x10" +
47
+ "Hello" +
48
+ "World"
49
+ end
50
+ end
51
+ end
52
+
53
+ context "when parsing a response" do
54
+ context "example 4.1.1" do
55
+ before :all do
56
+ s = "\x81\x00\x00\x00\x00\x00\x00\x01" +
57
+ "\x00\x00\x00\x09\x00\x00\x00\x00" +
58
+ "\x00\x00\x00\x00\x00\x00\x00\x00" +
59
+ "Not found"
60
+ @pkt = Memcached::Response.parse_header(s[0..23])
61
+ @pkt.parse_body(s[24..-1])
62
+ end
63
+
64
+ it "should return the right class according to magic & opcode" do
65
+ @pkt[:magic].should == 0x81
66
+ @pkt[:opcode].should == 0
67
+ @pkt.class.should == Memcached::Response
68
+ end
69
+ it "should return the right data type" do
70
+ @pkt[:data_type].should == 0
71
+ end
72
+ it "should return the right status" do
73
+ @pkt[:status].should == Memcached::Errors::KEY_NOT_FOUND
74
+ end
75
+ it "should return the right opaque" do
76
+ @pkt[:opaque].should == 0
77
+ end
78
+ it "should return the right CAS" do
79
+ @pkt[:cas].should == 0
80
+ end
81
+ it "should parse the body correctly" do
82
+ @pkt[:extras].should be_empty
83
+ @pkt[:key].should == ""
84
+ @pkt[:value].should == "Not found"
85
+ end
86
+ end
87
+
88
+ context "example 4.2.1" do
89
+ before :all do
90
+ s = "\x81\x00\x00\x00" +
91
+ "\x04\x00\x00\x00" +
92
+ "\x00\x00\x00\x09" +
93
+ "\x00\x00\x00\x00" +
94
+ "\x00\x00\x00\x00" +
95
+ "\x00\x00\x00\x01" +
96
+ "\xde\xad\xbe\xef" +
97
+ "World"
98
+ @pkt = Memcached::Response.parse_header(s[0..23])
99
+ @pkt.parse_body(s[24..-1])
100
+ end
101
+
102
+ it "should return the right class according to magic & opcode" do
103
+ @pkt[:magic].should == 0x81
104
+ @pkt[:opcode].should == 0
105
+ @pkt.class.should == Memcached::Response
106
+ end
107
+ it "should return the right data type" do
108
+ @pkt[:data_type].should == 0
109
+ end
110
+ it "should return the right status" do
111
+ @pkt[:status].should == Memcached::Errors::NO_ERROR
112
+ end
113
+ it "should return the right opaque" do
114
+ @pkt[:opaque].should == 0
115
+ end
116
+ it "should return the right CAS" do
117
+ @pkt[:cas].should == 1
118
+ end
119
+ it "should parse the body correctly" do
120
+ @pkt[:key].should == ""
121
+ @pkt[:value].should == "World"
122
+ end
123
+ end
124
+ end
125
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: astro-remcached
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Stephan Maka
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-09-08 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Ruby EventMachine memcached client
17
+ email: astro@spaceboyz.net
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - Rakefile
26
+ - VERSION.yml
27
+ - lib/remcached.rb
28
+ - lib/remcached/client.rb
29
+ - lib/remcached/const.rb
30
+ - lib/remcached/pack_array.rb
31
+ - lib/remcached/packet.rb
32
+ - spec/client_spec.rb
33
+ - spec/memcached_spec.rb
34
+ - spec/packet_spec.rb
35
+ has_rdoc: false
36
+ homepage: http://github.com/astro/remcached/
37
+ post_install_message:
38
+ rdoc_options:
39
+ - --charset=UTF-8
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: "0"
47
+ version:
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: "0"
53
+ version:
54
+ requirements: []
55
+
56
+ rubyforge_project:
57
+ rubygems_version: 1.2.0
58
+ signing_key:
59
+ specification_version: 3
60
+ summary: Ruby EventMachine memcached client
61
+ test_files:
62
+ - spec/packet_spec.rb
63
+ - spec/client_spec.rb
64
+ - spec/memcached_spec.rb