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