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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b71d14548833154a78afeaff9ac69af91456087d
|
4
|
+
data.tar.gz: fc4363db29d51f7d7f69870bebf1ba47e10ff397
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9a79f73e98b8bf455d7e8522012b4bb4b0100d466da80d9fe242a87de20cf1161758c17a4ed504f0ca0742743ff96f694c77a9362d34916f397cdaf6461e5d0f
|
7
|
+
data.tar.gz: 0625170d31443d3ce56152c741350ff9dce243d4519f90949bf5c20e3c65ae7f23a6b9f2b1e62721d52ba7f71e951e3eb0e966cc32fd993947f8b4a748f912c4
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
GEM
|
2
|
+
remote: https://rubygems.org/
|
3
|
+
specs:
|
4
|
+
diff-lcs (1.3)
|
5
|
+
redis (3.3.3)
|
6
|
+
rspec (3.6.0)
|
7
|
+
rspec-core (~> 3.6.0)
|
8
|
+
rspec-expectations (~> 3.6.0)
|
9
|
+
rspec-mocks (~> 3.6.0)
|
10
|
+
rspec-core (3.6.0)
|
11
|
+
rspec-support (~> 3.6.0)
|
12
|
+
rspec-expectations (3.6.0)
|
13
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
14
|
+
rspec-support (~> 3.6.0)
|
15
|
+
rspec-mocks (3.6.0)
|
16
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
17
|
+
rspec-support (~> 3.6.0)
|
18
|
+
rspec-support (3.6.0)
|
19
|
+
|
20
|
+
PLATFORMS
|
21
|
+
ruby
|
22
|
+
|
23
|
+
DEPENDENCIES
|
24
|
+
redis
|
25
|
+
rspec
|
26
|
+
|
27
|
+
BUNDLED WITH
|
28
|
+
1.15.3
|
data/README.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
A tutorial for ruby. Implementing redis in ruby.
|
data/bin/rspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
#
|
4
|
+
# This file was generated by Bundler.
|
5
|
+
#
|
6
|
+
# The application 'rspec' is installed as part of a gem, and
|
7
|
+
# this file is here to facilitate running it.
|
8
|
+
#
|
9
|
+
|
10
|
+
require "pathname"
|
11
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
12
|
+
Pathname.new(__FILE__).realpath)
|
13
|
+
|
14
|
+
require "rubygems"
|
15
|
+
require "bundler/setup"
|
16
|
+
|
17
|
+
load Gem.bin_path("rspec-core", "rspec")
|
data/bin/server
ADDED
data/lib/dr_rubbis.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'rubbis/transaction'
|
2
|
+
module Rubbis
|
3
|
+
class Handler
|
4
|
+
|
5
|
+
attr_reader :client, :buffer, :tx, :server
|
6
|
+
|
7
|
+
def initialize(socket, server)
|
8
|
+
@client = socket
|
9
|
+
@server = server
|
10
|
+
@buffer = ""
|
11
|
+
reset_tx!
|
12
|
+
end
|
13
|
+
|
14
|
+
def reset_tx!
|
15
|
+
@tx = Transaction.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def process!(state)
|
19
|
+
buffer << client.read_nonblock(1024)
|
20
|
+
cmds, processed = Rubbis::Protocol.unmarshal(buffer)
|
21
|
+
|
22
|
+
@buffer = buffer[processed..-1]
|
23
|
+
|
24
|
+
cmds.each do |cmd|
|
25
|
+
response = if tx.active?
|
26
|
+
case cmd[0].downcase
|
27
|
+
when 'exec'
|
28
|
+
result = tx.buffer.map do |cmd|
|
29
|
+
dispatch state, cmd
|
30
|
+
end unless tx.dirty?
|
31
|
+
reset_tx!
|
32
|
+
result
|
33
|
+
else
|
34
|
+
tx.queue cmd
|
35
|
+
:queued
|
36
|
+
end
|
37
|
+
else
|
38
|
+
dispatch state, cmd
|
39
|
+
end
|
40
|
+
|
41
|
+
unless response == :block
|
42
|
+
respond! response
|
43
|
+
end
|
44
|
+
|
45
|
+
state.process_list_watches!
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def respond!(response)
|
50
|
+
if active?
|
51
|
+
server.commit!
|
52
|
+
client.write Rubbis::Protocol.marshal(response)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def active?
|
57
|
+
client
|
58
|
+
end
|
59
|
+
|
60
|
+
def disconnect!(state)
|
61
|
+
state.unsubscribe_all(self)
|
62
|
+
@client = nil
|
63
|
+
end
|
64
|
+
|
65
|
+
def dispatch(state, cmd)
|
66
|
+
case cmd[0].downcase
|
67
|
+
when 'bgsave' then server.bgsave; :ok
|
68
|
+
when 'multi' then tx.start!; :ok
|
69
|
+
when 'watch' then
|
70
|
+
curr_tx = tx
|
71
|
+
state.watch(cmd[1]) {
|
72
|
+
tx.dirty! if curr_tx == tx
|
73
|
+
}
|
74
|
+
else state.apply_command(self, cmd)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Rubbis
|
2
|
+
class Protocol
|
3
|
+
class ProtocolError < RuntimeError; end
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def marshal(ruby)
|
7
|
+
case ruby
|
8
|
+
when Symbol then "+#{ruby.to_s.upcase}\r\n"
|
9
|
+
when nil then "$-1\r\n"
|
10
|
+
when Integer then ":#{ruby}\r\n"
|
11
|
+
when String then "$#{ruby.length}\r\n#{ruby}\r\n"
|
12
|
+
when Error then "-ERR #{ruby.message}\r\n"
|
13
|
+
when Array then "*#{ruby.length}\r\n#{ruby.map {|x| marshal(x)}.join}"
|
14
|
+
else raise "Dont know how to marshal #{ruby}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def unmarshal(data)
|
19
|
+
io = StringIO.new(data)
|
20
|
+
result = []
|
21
|
+
processed = 0
|
22
|
+
begin
|
23
|
+
loop do
|
24
|
+
header = safe_readline(io)
|
25
|
+
|
26
|
+
raise ProtocolError unless header[0] == '*'
|
27
|
+
|
28
|
+
n = header[1..-1].to_i
|
29
|
+
|
30
|
+
result << n.times.map do
|
31
|
+
raise ProtocolError unless io.readpartial(1) == '$'
|
32
|
+
|
33
|
+
length = safe_readline(io).to_i
|
34
|
+
safe_readpartial(io, length).tap do
|
35
|
+
safe_readline(io)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
processed = io.pos
|
40
|
+
end
|
41
|
+
rescue ProtocolError
|
42
|
+
processed = io.pos
|
43
|
+
rescue EOFError
|
44
|
+
end
|
45
|
+
|
46
|
+
[result, processed]
|
47
|
+
end
|
48
|
+
|
49
|
+
def safe_readline(io)
|
50
|
+
io.readline("\r\n").tap do |line|
|
51
|
+
raise EOFError unless line.end_with?("\r\n")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def safe_readpartial(io, length)
|
56
|
+
io.readpartial(length).tap do |data|
|
57
|
+
raise EOFError unless data.length == length
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
require "socket"
|
2
|
+
require 'stringio'
|
3
|
+
require 'tempfile'
|
4
|
+
|
5
|
+
require 'rubbis/protocol'
|
6
|
+
require 'rubbis/state'
|
7
|
+
require 'rubbis/handler'
|
8
|
+
|
9
|
+
module Rubbis
|
10
|
+
class Server
|
11
|
+
|
12
|
+
class Clock
|
13
|
+
def now
|
14
|
+
Time.now.to_f
|
15
|
+
end
|
16
|
+
|
17
|
+
def sleep(x)
|
18
|
+
::Kernel.sleep x
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(opts = {})
|
23
|
+
@port = opts[:port]
|
24
|
+
@shutdown_pipe = IO.pipe
|
25
|
+
@clock = Clock.new
|
26
|
+
@state = State.new(@clock)
|
27
|
+
@server_file = opts[:server_file]
|
28
|
+
@aof_file = opts[:aof_file]
|
29
|
+
end
|
30
|
+
|
31
|
+
def bgsave
|
32
|
+
return unless server_file
|
33
|
+
begin
|
34
|
+
tmpf = Tempfile.new(File.basename(server_file))
|
35
|
+
tmpf.write state.serialize
|
36
|
+
tmpf.close
|
37
|
+
FileUtils.mv(tmpf, server_file)
|
38
|
+
:ok
|
39
|
+
ensure
|
40
|
+
if tmpf
|
41
|
+
tmpf.close
|
42
|
+
tmpf.unlink
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def bgrewriteaof
|
48
|
+
return unless aof_file
|
49
|
+
begin
|
50
|
+
tmpf = Tempfile.new(File.basename(aof_file))
|
51
|
+
state.minimal_log.each do |cmd|
|
52
|
+
tmpf.write Rubbis::Protocol.marshal(cmd)
|
53
|
+
end
|
54
|
+
|
55
|
+
tmpf.close
|
56
|
+
|
57
|
+
command_log.close
|
58
|
+
FileUtils.mv(tmpf, aof_file)
|
59
|
+
@command_log = File.open(aof_file, 'a')
|
60
|
+
:ok
|
61
|
+
ensure
|
62
|
+
if tmpf
|
63
|
+
tmpf.close
|
64
|
+
tmpf.unlink
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def commit!
|
70
|
+
return unless command_log
|
71
|
+
|
72
|
+
state.log.each do |cmd|
|
73
|
+
command_log.write Rubbis::Protocol.marshal(cmd)
|
74
|
+
end
|
75
|
+
command_log.fsync
|
76
|
+
state.log.clear
|
77
|
+
end
|
78
|
+
|
79
|
+
def check_background_processes!
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
def shutdown
|
84
|
+
shutdown_pipe[1].close
|
85
|
+
end
|
86
|
+
|
87
|
+
class AofClient < StringIO
|
88
|
+
def write(*_)
|
89
|
+
# noop
|
90
|
+
end
|
91
|
+
end
|
92
|
+
def apply_log(contents)
|
93
|
+
Handler.new(AofClient.new(contents), self).process!(state)
|
94
|
+
end
|
95
|
+
|
96
|
+
def listen
|
97
|
+
readable = []
|
98
|
+
clients = {}
|
99
|
+
running = true
|
100
|
+
|
101
|
+
server = TCPServer.new(port)
|
102
|
+
|
103
|
+
expire_pipe = IO.pipe
|
104
|
+
readable << server
|
105
|
+
readable << shutdown_pipe[0]
|
106
|
+
readable << expire_pipe[0]
|
107
|
+
|
108
|
+
@command_log = File.open(aof_file, 'a') if aof_file
|
109
|
+
|
110
|
+
if aof_file && File.exists?(aof_file)
|
111
|
+
apply_log File.read(aof_file)
|
112
|
+
elsif server_file && File.exists?(server_file)
|
113
|
+
@state.deserialize File.read(server_file)
|
114
|
+
end
|
115
|
+
|
116
|
+
timer_thread = Thread.new do
|
117
|
+
begin
|
118
|
+
while running
|
119
|
+
sleep 0.1
|
120
|
+
expire_pipe[1].write '.'
|
121
|
+
end
|
122
|
+
rescue Errno::EPIPE, IOError
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
while running
|
127
|
+
ready_to_read, _ = IO.select(readable + clients.keys)
|
128
|
+
ready_to_read.each do |socket|
|
129
|
+
case socket
|
130
|
+
when server
|
131
|
+
child_socket = socket.accept_nonblock
|
132
|
+
clients[child_socket] = Handler.new(child_socket, self)
|
133
|
+
when shutdown_pipe[0]
|
134
|
+
running = false
|
135
|
+
when expire_pipe[0]
|
136
|
+
state.expire_keys!
|
137
|
+
check_background_processes!
|
138
|
+
else
|
139
|
+
begin
|
140
|
+
clients[socket].process!(state)
|
141
|
+
rescue EOFError
|
142
|
+
handler = clients.delete(socket)
|
143
|
+
handler.disconnect!(state)
|
144
|
+
socket.close
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
ensure
|
150
|
+
running = false
|
151
|
+
(readable + clients.keys).each do |file|
|
152
|
+
file.close
|
153
|
+
end
|
154
|
+
expire_pipe[1].close if expire_pipe
|
155
|
+
timer_thread.join if timer_thread
|
156
|
+
end
|
157
|
+
|
158
|
+
private
|
159
|
+
|
160
|
+
attr_reader :shutdown_pipe, :state, :clock, :server_file, :aof_file, :command_log
|
161
|
+
|
162
|
+
def port
|
163
|
+
@port
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|