astro-remcached 0.2.0

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/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