dr-rubbis 0.0.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.
- checksums.yaml +7 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +28 -0
- data/README.md +1 -0
- data/bin/rspec +17 -0
- data/bin/server +9 -0
- data/lib/dr_rubbis.rb +3 -0
- data/lib/rubbis/handler.rb +78 -0
- data/lib/rubbis/protocol.rb +63 -0
- data/lib/rubbis/server.rb +166 -0
- data/lib/rubbis/state.rb +407 -0
- data/lib/rubbis/transaction.rb +34 -0
- data/lib/rubbis/zset.rb +27 -0
- data/spec/acceptance/expiry_spec.rb +27 -0
- data/spec/acceptance/hash_spec.rb +11 -0
- data/spec/acceptance/list_spec.rb +51 -0
- data/spec/acceptance/persistence_spec.rb +25 -0
- data/spec/acceptance/pubsub_spec.rb +48 -0
- data/spec/acceptance/skeleton_spec.rb +34 -0
- data/spec/acceptance/transactions_spec.rb +49 -0
- data/spec/spec_helper.rb +88 -0
- data/spec/unit/protocol_spec.rb +20 -0
- data/spec/unit/state_spec.rb +238 -0
- metadata +65 -0
data/lib/rubbis/state.rb
ADDED
@@ -0,0 +1,407 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'rubbis/zset'
|
3
|
+
|
4
|
+
module Rubbis
|
5
|
+
Error = Struct.new(:message) do
|
6
|
+
def self.incorrect_args(cmd)
|
7
|
+
new "wrong number of arguments for '#{cmd}' command"
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.unknown_cmd(cmd)
|
11
|
+
new "unknown command '#{cmd}'"
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.type_error
|
15
|
+
new "wrong type for command"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class State
|
20
|
+
def initialize(clock)
|
21
|
+
@clock = clock
|
22
|
+
@data = {}
|
23
|
+
@log = []
|
24
|
+
@expires = {}
|
25
|
+
@watches = {}
|
26
|
+
@list_watches = {}
|
27
|
+
@ready_keys = []
|
28
|
+
@channels = Hash.new { |hash, key| hash[key] = Set.new }
|
29
|
+
@subscribers = Hash.new { |hash, key| hash[key] = Set.new }
|
30
|
+
@pchannels = Hash.new { |hash, key| hash[key] = Set.new }
|
31
|
+
@psubscribers = Hash.new { |hash, key| hash[key] = Set.new }
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.valid_command?(cmd)
|
35
|
+
@valid_commands ||= Set.new(
|
36
|
+
StateCommands.public_instance_methods.map(&:to_s)
|
37
|
+
)
|
38
|
+
@valid_commands.include?(cmd) || blocking_command?(cmd)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.blocking_command?(cmd)
|
42
|
+
@blocking_commands ||= Set.new(
|
43
|
+
BlockingCommands.public_instance_methods.map(&:to_s)
|
44
|
+
)
|
45
|
+
@blocking_commands.include?(cmd)
|
46
|
+
end
|
47
|
+
|
48
|
+
def serialize
|
49
|
+
Marshal.dump [@data, @expires]
|
50
|
+
end
|
51
|
+
|
52
|
+
def deserialize(bytes)
|
53
|
+
@data, @expires = *Marshal.load(bytes)
|
54
|
+
end
|
55
|
+
|
56
|
+
def channel_count(client)
|
57
|
+
channels[client].length + pchannels[client].length
|
58
|
+
end
|
59
|
+
|
60
|
+
def apply_command(client, cmd)
|
61
|
+
unless State.valid_command?(cmd[0])
|
62
|
+
return Error.unknown_cmd(cmd[0])
|
63
|
+
end
|
64
|
+
|
65
|
+
if State.blocking_command?(cmd[0])
|
66
|
+
cmd << client
|
67
|
+
end
|
68
|
+
|
69
|
+
public_send *cmd
|
70
|
+
end
|
71
|
+
|
72
|
+
def subscribers_for(channel)
|
73
|
+
(
|
74
|
+
subscribers[channel].to_a +
|
75
|
+
psubscribers.select {|pattern, _|
|
76
|
+
File.fnmatch(pattern, channel)
|
77
|
+
}.values.map(&:to_a).flatten
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
module StateCommands
|
82
|
+
def publish(channel, message)
|
83
|
+
subscribers_for(channel).each do |client|
|
84
|
+
client.respond! ["message", channel, message]
|
85
|
+
end.length
|
86
|
+
end
|
87
|
+
|
88
|
+
def ping
|
89
|
+
:pong
|
90
|
+
end
|
91
|
+
|
92
|
+
def echo(text)
|
93
|
+
text
|
94
|
+
end
|
95
|
+
|
96
|
+
def watch(key, &block)
|
97
|
+
watches[key] ||= []
|
98
|
+
watches[key] << block if block
|
99
|
+
:ok
|
100
|
+
end
|
101
|
+
|
102
|
+
def unsubscribe_all(client)
|
103
|
+
(channels.delete(client) || Set.new).each do |channel|
|
104
|
+
subscribers.delete channel
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def expire(key, secs)
|
109
|
+
pexpire(key, secs.to_i * 1000)
|
110
|
+
end
|
111
|
+
|
112
|
+
def pexpire(key, ms)
|
113
|
+
pexpireat(key, clock.now * 1000.0 + ms.to_i)
|
114
|
+
end
|
115
|
+
|
116
|
+
def minimal_log
|
117
|
+
data.map do |key, value|
|
118
|
+
case value
|
119
|
+
when String
|
120
|
+
[['set', key, value]]
|
121
|
+
when Array
|
122
|
+
value.reverse.map do |v|
|
123
|
+
['lpush', key, v]
|
124
|
+
end
|
125
|
+
else raise 'unimplemented'
|
126
|
+
end
|
127
|
+
end.reduce(:+) + expires.map do |key, value|
|
128
|
+
['pexpireat', key, (value * 1000).to_i.to_s]
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def pexpireat(key, value)
|
133
|
+
if get(key)
|
134
|
+
expires[key] = value.to_i / 1000.0
|
135
|
+
log << ['pexpireat', key, value.to_i.round.to_s]
|
136
|
+
1
|
137
|
+
else
|
138
|
+
0
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def expire_keys!(n: 100, threshold: 0.25, rng: Random.new)
|
143
|
+
begin
|
144
|
+
expired = expires.keys.sample(n, random: rng).count do |key|
|
145
|
+
get(key)
|
146
|
+
end
|
147
|
+
end while expired > n * threshold
|
148
|
+
end
|
149
|
+
|
150
|
+
def process_list_watches!
|
151
|
+
ready_keys.each do |key|
|
152
|
+
list = get(key)
|
153
|
+
watches = list_watches.fetch(key, [])
|
154
|
+
|
155
|
+
while list.any? && watches.any?
|
156
|
+
op, client = *watches.shift
|
157
|
+
client.respond!(op.call) if client.active?
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
ready_keys.clear
|
162
|
+
end
|
163
|
+
|
164
|
+
def set(*args)
|
165
|
+
key, value ,modifier = *args
|
166
|
+
|
167
|
+
return Error.incorrect_args("set") unless key && value
|
168
|
+
|
169
|
+
exists = data.has_key?(key)
|
170
|
+
nx = modifier == "NX"
|
171
|
+
xx = modifier == "XX"
|
172
|
+
|
173
|
+
if (!nx && !xx) || (nx && !exists) || (xx && exists)
|
174
|
+
touch! key
|
175
|
+
log << ['set', key, value]
|
176
|
+
data[key] = value
|
177
|
+
:ok
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def get(key)
|
182
|
+
expiry = expires[key]
|
183
|
+
del(key) if expiry && expiry <= clock.now
|
184
|
+
|
185
|
+
data[key]
|
186
|
+
end
|
187
|
+
|
188
|
+
def del(key)
|
189
|
+
touch! key
|
190
|
+
if data.delete(key)
|
191
|
+
log << ['del', key]
|
192
|
+
end
|
193
|
+
expires.delete(key)
|
194
|
+
end
|
195
|
+
|
196
|
+
def hset(*args)
|
197
|
+
hash, key, value = *args
|
198
|
+
|
199
|
+
return Error.incorrect_args("hset") unless hash && key && value
|
200
|
+
touch! key
|
201
|
+
data[hash] ||= {}
|
202
|
+
data[hash][key] = value
|
203
|
+
:ok
|
204
|
+
end
|
205
|
+
|
206
|
+
def hget(*args)
|
207
|
+
hash, key = *args
|
208
|
+
|
209
|
+
return Error.incorrect_args("hget") unless hash && key
|
210
|
+
|
211
|
+
value = get(hash)
|
212
|
+
|
213
|
+
value[key] if value
|
214
|
+
end
|
215
|
+
|
216
|
+
def hmget(hash, *keys)
|
217
|
+
existing = get(hash) || {}
|
218
|
+
if existing.is_a?(Hash)
|
219
|
+
existing.values_at(*keys)
|
220
|
+
else
|
221
|
+
Error.type_error
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def hincrby(hash, key, amount)
|
226
|
+
value = get(hash)
|
227
|
+
if value
|
228
|
+
existing = value[key]
|
229
|
+
value[key] = existing.to_i + amount.to_i
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def exists(key)
|
234
|
+
if data[key]
|
235
|
+
1
|
236
|
+
else
|
237
|
+
0
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def keys(pattern)
|
242
|
+
if pattern == '*'
|
243
|
+
result = data.keys
|
244
|
+
|
245
|
+
result
|
246
|
+
else
|
247
|
+
raise 'unimplemented'
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def zadd(key, score, member)
|
252
|
+
score = score.to_f
|
253
|
+
|
254
|
+
value = get(key) || data[key] = ZSet.new
|
255
|
+
|
256
|
+
value.add(score, member)
|
257
|
+
end
|
258
|
+
|
259
|
+
def zrange(key, start, stop)
|
260
|
+
value = get(key)
|
261
|
+
if value
|
262
|
+
value.range(start.to_i, stop.to_i)
|
263
|
+
else
|
264
|
+
[]
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def zrank(key, member)
|
269
|
+
value = get(key)
|
270
|
+
if value
|
271
|
+
value.rank(member)
|
272
|
+
else
|
273
|
+
-1
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def zscore(key, member)
|
278
|
+
value = get(key)
|
279
|
+
if value
|
280
|
+
value.score(member)
|
281
|
+
else
|
282
|
+
-1
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
|
287
|
+
def lpush(key, value)
|
288
|
+
list = get(key)
|
289
|
+
list ||= data[key] = []
|
290
|
+
|
291
|
+
if list_watches.fetch(key, []).any?
|
292
|
+
ready_keys << key
|
293
|
+
end
|
294
|
+
touch! key
|
295
|
+
list.unshift value
|
296
|
+
list.length
|
297
|
+
end
|
298
|
+
|
299
|
+
def llen(key)
|
300
|
+
list = get(key)
|
301
|
+
list ||= data[key] = []
|
302
|
+
|
303
|
+
list.length
|
304
|
+
end
|
305
|
+
|
306
|
+
def lrange(key, start, stop)
|
307
|
+
list = get(key)
|
308
|
+
|
309
|
+
if list
|
310
|
+
list[start.to_i..stop.to_i]
|
311
|
+
else
|
312
|
+
[]
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
def rpop(key)
|
317
|
+
list = get(key)
|
318
|
+
list ||= data[key] = []
|
319
|
+
|
320
|
+
touch! key
|
321
|
+
list.pop
|
322
|
+
end
|
323
|
+
|
324
|
+
|
325
|
+
def rpoplpush(pop_key, push_key)
|
326
|
+
item = rpop(pop_key)
|
327
|
+
return unless item
|
328
|
+
lpush push_key, item
|
329
|
+
item
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
|
334
|
+
module BlockingCommands
|
335
|
+
def subscribe(channel, client)
|
336
|
+
subscribers[channel] << client
|
337
|
+
channels[client] << channel
|
338
|
+
|
339
|
+
['subscribe', channel, channel_count(client)]
|
340
|
+
end
|
341
|
+
|
342
|
+
def unsubscribe(channel, client)
|
343
|
+
subscribers[channel].delete client
|
344
|
+
channels[client].delete channel
|
345
|
+
|
346
|
+
['unsubscribe', channel, channel_count(client)]
|
347
|
+
end
|
348
|
+
|
349
|
+
def psubscribe(channel, client)
|
350
|
+
psubscribers[channel] << client
|
351
|
+
pchannels[client] << channel
|
352
|
+
|
353
|
+
['psubscribe', channel, channel_count(client)]
|
354
|
+
end
|
355
|
+
|
356
|
+
def punsubscribe(channel, client)
|
357
|
+
psubscribers[channel].delete client
|
358
|
+
pchannels[client].delete channel
|
359
|
+
|
360
|
+
['punsubscribe', channel, channel_count(client)]
|
361
|
+
end
|
362
|
+
|
363
|
+
def brpoplpush(pop_key, push_key, timeout, client)
|
364
|
+
|
365
|
+
action = ->{ rpoplpush(pop_key, push_key) }
|
366
|
+
|
367
|
+
if llen(pop_key) == 0
|
368
|
+
list_watches[pop_key] ||= []
|
369
|
+
list_watches[pop_key] << [action, client]
|
370
|
+
:block
|
371
|
+
else
|
372
|
+
action.call
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
def brpop(key, timeout, client)
|
377
|
+
list = get(key)
|
378
|
+
list ||= data[key] = []
|
379
|
+
|
380
|
+
action = ->{ rpop(key) }
|
381
|
+
|
382
|
+
if llen(key) == 0
|
383
|
+
list_watches[key] ||= []
|
384
|
+
list_watches[key] << [action, client]
|
385
|
+
:block
|
386
|
+
else
|
387
|
+
action.call
|
388
|
+
end
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
include StateCommands
|
393
|
+
include BlockingCommands
|
394
|
+
|
395
|
+
attr_reader :log
|
396
|
+
|
397
|
+
private
|
398
|
+
|
399
|
+
attr_reader :data, :expires, :clock, :watches, :list_watches, :ready_keys,
|
400
|
+
:channels, :subscribers, :pchannels, :psubscribers
|
401
|
+
|
402
|
+
def touch!(key)
|
403
|
+
ws = watches.delete(key) || []
|
404
|
+
ws.each(&:call)
|
405
|
+
end
|
406
|
+
end
|
407
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Rubbis
|
2
|
+
class Transaction
|
3
|
+
def initialize
|
4
|
+
@active = false
|
5
|
+
@buffer = []
|
6
|
+
@dirty = false
|
7
|
+
end
|
8
|
+
|
9
|
+
def start!
|
10
|
+
@active = true
|
11
|
+
end
|
12
|
+
|
13
|
+
def active?
|
14
|
+
@active
|
15
|
+
end
|
16
|
+
|
17
|
+
def dirty!
|
18
|
+
@dirty = true
|
19
|
+
end
|
20
|
+
|
21
|
+
def dirty?
|
22
|
+
@dirty
|
23
|
+
end
|
24
|
+
|
25
|
+
def queue(cmd)
|
26
|
+
raise unless @active
|
27
|
+
@buffer << cmd
|
28
|
+
end
|
29
|
+
|
30
|
+
def buffer
|
31
|
+
@buffer
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/rubbis/zset.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
class ZSet
|
2
|
+
# i've skipped refactoring this code to work in O(logn)
|
3
|
+
def initialize
|
4
|
+
@entries = []
|
5
|
+
end
|
6
|
+
|
7
|
+
def add(score, member)
|
8
|
+
entries << [score, member]
|
9
|
+
entries.sort!
|
10
|
+
end
|
11
|
+
|
12
|
+
def range(start, stop)
|
13
|
+
entries[start..stop].map {|x| x[1]}
|
14
|
+
end
|
15
|
+
|
16
|
+
def rank(member)
|
17
|
+
entries.index {|x| x[1] == member}
|
18
|
+
end
|
19
|
+
|
20
|
+
def score(member)
|
21
|
+
entries.detect {|x| x[1] == member}[0]
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
attr_reader :entries
|
27
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Rubbis', :acceptance do
|
4
|
+
it 'actively expires keys' do
|
5
|
+
with_server do
|
6
|
+
n = 10
|
7
|
+
n.times do |x|
|
8
|
+
kkey = "keep#{x}"
|
9
|
+
ekey = "expire#{x}"
|
10
|
+
client.set(kkey, "123")
|
11
|
+
client.set(ekey, "123")
|
12
|
+
client.pexpire(ekey, rand(600))
|
13
|
+
end
|
14
|
+
condition = ->{
|
15
|
+
client.keys("*").count {|x| x.start_with?("expire") } == 0
|
16
|
+
}
|
17
|
+
|
18
|
+
start_time = Time.now
|
19
|
+
while !condition.call && (Time.now < start_time + 2)
|
20
|
+
sleep 0.01
|
21
|
+
end
|
22
|
+
|
23
|
+
expect(condition.call).to eq(true)
|
24
|
+
expect(client.keys('*').size).to eq(n)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Rubbis', :acceptance do
|
4
|
+
it 'supports hashes' do
|
5
|
+
with_server do
|
6
|
+
client.hset('myhash', 'abc', '123')
|
7
|
+
client.hset('myhash', 'def', '456')
|
8
|
+
expect(client.hmget("myhash", "abc", "def")).to eq(["123", "456"])
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Rubbis", :acceptance do
|
4
|
+
it 'supports blocking pop' do
|
5
|
+
with_server do
|
6
|
+
items = %w(a b)
|
7
|
+
t1 = Thread.new do
|
8
|
+
item = client.brpop("queue")
|
9
|
+
expect(item).to eq(items.shift)
|
10
|
+
end
|
11
|
+
t2 = Thread.new do
|
12
|
+
item = client.brpop("queue")
|
13
|
+
expect(item).to eq(items.shift)
|
14
|
+
end
|
15
|
+
|
16
|
+
items.dup.each do |item|
|
17
|
+
expect(client.lpush("queue", item)).to eq(1)
|
18
|
+
end
|
19
|
+
t1.value
|
20
|
+
t2.value
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'supports brpoplpush' do
|
25
|
+
with_server do
|
26
|
+
c = client
|
27
|
+
|
28
|
+
t1 = Thread.new do
|
29
|
+
item = c.brpoplpush("q","p")
|
30
|
+
expect(item).to eq('a')
|
31
|
+
expect(c.lrange('p', "0", "-1")).to eq(%w(a))
|
32
|
+
end
|
33
|
+
|
34
|
+
c.lpush("q", "a")
|
35
|
+
|
36
|
+
t1.value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'handles disconnecting clients' do
|
41
|
+
with_server do
|
42
|
+
|
43
|
+
c = TCPSocket.new('localhost', TEST_PORT)
|
44
|
+
c.write ("*3\r\n$5\r\nbrpop\r\n$1\r\nq\r\n$1\r\n0\r\n")
|
45
|
+
c.close
|
46
|
+
|
47
|
+
expect(client.lpush("q", "a")).to eq(1)
|
48
|
+
expect(client.lpush("q", "b")).to eq(2)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Rubbis', :acceptance do
|
4
|
+
it 'can persist data' do
|
5
|
+
with_server do
|
6
|
+
client.set("a", "1")
|
7
|
+
client.bgsave
|
8
|
+
end
|
9
|
+
|
10
|
+
with_server do
|
11
|
+
expect(client.get("a")).to eq("1")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'can persist data with aof' do
|
16
|
+
with_server(server_file: false, aof_file: true) do
|
17
|
+
client.set("a", "1")
|
18
|
+
client.bgsave
|
19
|
+
end
|
20
|
+
|
21
|
+
with_server(server_file: false, aof_file: true) do
|
22
|
+
expect(client.get("a")).to eq("1")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Rubbis', :acceptance do
|
4
|
+
it 'support pubsub' do
|
5
|
+
with_server do
|
6
|
+
received = nil
|
7
|
+
c = client
|
8
|
+
|
9
|
+
t1 = Thread.new do
|
10
|
+
c.subscribe("mychan") do |on|
|
11
|
+
on.message do |channel, msg|
|
12
|
+
received = msg
|
13
|
+
c.unsubscribe("mychan")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
sleep 0.1
|
19
|
+
|
20
|
+
client.publish('mychan', 'hello')
|
21
|
+
t1.value
|
22
|
+
expect(received).to eq('hello')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'supports pubsub with patterns' do
|
27
|
+
with_server do
|
28
|
+
received = nil
|
29
|
+
c = client
|
30
|
+
|
31
|
+
t1 = Thread.new do
|
32
|
+
c.psubscribe("my.*") do |on|
|
33
|
+
on.message do |channel, msg|
|
34
|
+
received = msg
|
35
|
+
c.punsubscribe("my.*")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
sleep 0.1
|
41
|
+
|
42
|
+
client.publish('your.chan', 'bogus')
|
43
|
+
client.publish('my.chan', 'hello')
|
44
|
+
t1.value
|
45
|
+
expect(received).to eq('hello')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe 'Rubbis', :acceptance do
|
4
|
+
it 'responds to ping' do
|
5
|
+
with_server do
|
6
|
+
c = client
|
7
|
+
c.without_reconnect do
|
8
|
+
expect(c.ping).to eq("PONG")
|
9
|
+
expect(c.ping).to eq("PONG")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'echoes msgs' do
|
15
|
+
with_server do
|
16
|
+
expect(client.echo("hello\nthere")).to eq("hello\nthere")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'supports multiple clients simultaneously' do
|
21
|
+
with_server do
|
22
|
+
expect(client.echo("hello\nthere")).to eq("hello\nthere")
|
23
|
+
expect(client.echo("hello\nthere")).to eq("hello\nthere")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'gets and sets values' do
|
28
|
+
with_server do
|
29
|
+
expect(client.get("abc")).to eq(nil)
|
30
|
+
expect(client.set("abc", "123")).to eq("OK")
|
31
|
+
expect(client.get("abc")).to eq("123")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Rubbis', :acceptance do
|
4
|
+
it 'handles transactions' do
|
5
|
+
with_server do
|
6
|
+
c = client
|
7
|
+
|
8
|
+
result = c.multi do
|
9
|
+
c.set("abc", "123")
|
10
|
+
c.get("abc")
|
11
|
+
end
|
12
|
+
|
13
|
+
expect(result).to eq(%w(OK 123))
|
14
|
+
|
15
|
+
begin
|
16
|
+
c.multi do
|
17
|
+
c.set("abc", "456")
|
18
|
+
raise
|
19
|
+
end
|
20
|
+
rescue
|
21
|
+
end
|
22
|
+
|
23
|
+
expect(c.get("abc")).to eq("123")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'supports WATCH' do
|
28
|
+
with_server do
|
29
|
+
c1 = client
|
30
|
+
c2 = client
|
31
|
+
|
32
|
+
c1.set("abc", 1)
|
33
|
+
|
34
|
+
c1.watch("abc")
|
35
|
+
c1.watch("def")
|
36
|
+
|
37
|
+
prev = c1.get("abc")
|
38
|
+
|
39
|
+
c2.set("abc", "10")
|
40
|
+
|
41
|
+
c1.multi do
|
42
|
+
c1.set("abc", prev.to_i + 1)
|
43
|
+
end
|
44
|
+
|
45
|
+
expect(c1.get("abc")).to eq("10")
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|