rubcask 0.1.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/.standard.yml +3 -0
- data/Gemfile +20 -0
- data/Gemfile.lock +74 -0
- data/LICENSE.txt +21 -0
- data/README.md +111 -0
- data/Rakefile +14 -0
- data/benchmark/benchmark_io.rb +49 -0
- data/benchmark/benchmark_server.rb +10 -0
- data/benchmark/benchmark_server_pipeline.rb +24 -0
- data/benchmark/benchmark_worker.rb +46 -0
- data/benchmark/op_times.rb +32 -0
- data/benchmark/profile.rb +15 -0
- data/benchmark/server_benchmark_helper.rb +138 -0
- data/example/server_runner.rb +15 -0
- data/lib/rubcask/bytes.rb +11 -0
- data/lib/rubcask/concurrency/fake_atomic_fixnum.rb +34 -0
- data/lib/rubcask/concurrency/fake_lock.rb +41 -0
- data/lib/rubcask/concurrency/fake_monitor_mixin.rb +21 -0
- data/lib/rubcask/config.rb +55 -0
- data/lib/rubcask/data_entry.rb +9 -0
- data/lib/rubcask/data_file.rb +91 -0
- data/lib/rubcask/directory.rb +437 -0
- data/lib/rubcask/expirable_entry.rb +9 -0
- data/lib/rubcask/hint_entry.rb +9 -0
- data/lib/rubcask/hint_file.rb +56 -0
- data/lib/rubcask/hinted_file.rb +148 -0
- data/lib/rubcask/keydir_entry.rb +9 -0
- data/lib/rubcask/merge_directory.rb +75 -0
- data/lib/rubcask/protocol.rb +74 -0
- data/lib/rubcask/server/abstract_server.rb +113 -0
- data/lib/rubcask/server/async.rb +78 -0
- data/lib/rubcask/server/client.rb +131 -0
- data/lib/rubcask/server/config.rb +31 -0
- data/lib/rubcask/server/pipeline.rb +49 -0
- data/lib/rubcask/server/runner/config.rb +43 -0
- data/lib/rubcask/server/runner.rb +107 -0
- data/lib/rubcask/server/threaded.rb +171 -0
- data/lib/rubcask/task/clean_directory.rb +19 -0
- data/lib/rubcask/tombstone.rb +40 -0
- data/lib/rubcask/version.rb +5 -0
- data/lib/rubcask/worker/direct_worker.rb +23 -0
- data/lib/rubcask/worker/factory.rb +42 -0
- data/lib/rubcask/worker/ractor_worker.rb +40 -0
- data/lib/rubcask/worker/thread_worker.rb +40 -0
- data/lib/rubcask.rb +19 -0
- metadata +102 -0
@@ -0,0 +1,148 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
require "tempfile"
|
5
|
+
require "fileutils"
|
6
|
+
|
7
|
+
module Rubcask
|
8
|
+
# HintedFile represents DataFile with the associated hint file
|
9
|
+
# it delegated all read/write responsibility to the @data_file
|
10
|
+
class HintedFile
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
ID_REGEX = /(\d+)\.data$/
|
14
|
+
HINT_EXTENSION_REGEX = /\.data$/
|
15
|
+
|
16
|
+
def_delegators :@data_file, :seek, :[], :close, :flush, :each, :pos, :write_pos
|
17
|
+
|
18
|
+
# @return [String] path of the file
|
19
|
+
attr_reader :path
|
20
|
+
|
21
|
+
# @return [Integer] id of the file
|
22
|
+
attr_reader :id
|
23
|
+
|
24
|
+
# @return [String] Path of the hint file associated with the data file
|
25
|
+
attr_reader :hint_path
|
26
|
+
|
27
|
+
# @param [String] file_path Path of the data_file
|
28
|
+
# @param [Boolean] os_sync Should O_SYNC flag be used on the data file?
|
29
|
+
# @param [Boolean] read_only Should the data file be opened read-only?
|
30
|
+
# @param [Boolean] ruby_sync Should ruby I/O buffers by bupassed?
|
31
|
+
def initialize(file_path, os_sync: false, read_only: false, ruby_sync: false)
|
32
|
+
@id = file_path.scan(ID_REGEX)[0][0].to_i
|
33
|
+
@hint_path = file_path.sub(HINT_EXTENSION_REGEX, ".hint")
|
34
|
+
@path = file_path
|
35
|
+
@read_only = read_only
|
36
|
+
|
37
|
+
io = nil
|
38
|
+
size = nil
|
39
|
+
flags = (os_sync && ruby_sync) ? File::SYNC : 0
|
40
|
+
if File.exist?(file_path)
|
41
|
+
size = File.size(file_path)
|
42
|
+
@dirty = false
|
43
|
+
io = File.open(file_path, "#{read_only ? "r" : "a+"}b", flags: flags)
|
44
|
+
else # If file does not exist we ignore read_only as it does not make sense
|
45
|
+
size = 0
|
46
|
+
@dirty = true
|
47
|
+
io = File.open(file_path, "a+b", flags: flags)
|
48
|
+
end
|
49
|
+
@data_file = DataFile.new(io, size)
|
50
|
+
|
51
|
+
if ruby_sync
|
52
|
+
@data_file.sync = true
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# yields every KeydirEntry in the file
|
57
|
+
# @yield [keydir_entry]
|
58
|
+
# @yieldparam [KeydirEntry] keydirEntry
|
59
|
+
# @return [Enumerator] if no block given
|
60
|
+
def each_keydir_entry(&block)
|
61
|
+
return to_enum(__method__) unless block
|
62
|
+
if has_hint_file?
|
63
|
+
return each_hint_file_keydir_entry(&block)
|
64
|
+
end
|
65
|
+
each_data_file_keydir_entry(&block)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Appends the entry to the end of the file
|
69
|
+
# @param [DataEntry] entry entry to append
|
70
|
+
# @return [KeydirEntry]
|
71
|
+
def append(entry)
|
72
|
+
if !dirty?
|
73
|
+
FileUtils.rm_f(hint_path)
|
74
|
+
@dirty = true
|
75
|
+
end
|
76
|
+
write_entry = @data_file.append(entry)
|
77
|
+
KeydirEntry.new(id, write_entry.value_size, write_entry.value_pos, entry.expire_timestamp)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Creates a new hint file
|
81
|
+
def save_hint_file
|
82
|
+
tempfile = Tempfile.new("hint")
|
83
|
+
current_pos = 0
|
84
|
+
map = {}
|
85
|
+
data_file.each do |entry|
|
86
|
+
new_pos = data_file.pos
|
87
|
+
new_entry = HintEntry.new(entry.expire_timestamp, entry.key, current_pos, new_pos - current_pos)
|
88
|
+
current_pos = new_pos
|
89
|
+
map[entry.key] = new_entry
|
90
|
+
end
|
91
|
+
|
92
|
+
begin
|
93
|
+
hint_file = HintFile.new(tempfile)
|
94
|
+
map.each_value do |entry|
|
95
|
+
hint_file.append(entry)
|
96
|
+
end
|
97
|
+
hint_file.close
|
98
|
+
FileUtils.mv(tempfile.path, hint_path)
|
99
|
+
@dirty = false
|
100
|
+
ensure
|
101
|
+
tempfile.close(true)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# @return true if hint path exists
|
106
|
+
def has_hint_file?
|
107
|
+
File.exist?(hint_path)
|
108
|
+
end
|
109
|
+
|
110
|
+
# @return true if there were any appends to the data file
|
111
|
+
def dirty?
|
112
|
+
@dirty
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
attr_reader :data_file
|
118
|
+
|
119
|
+
def each_data_file_keydir_entry
|
120
|
+
current_pos = 0
|
121
|
+
@data_file.each do |entry|
|
122
|
+
new_pos = @data_file.pos
|
123
|
+
value_size = new_pos - current_pos
|
124
|
+
value_pos = current_pos
|
125
|
+
current_pos = new_pos
|
126
|
+
yield [
|
127
|
+
entry.key,
|
128
|
+
KeydirEntry.new(
|
129
|
+
id, value_size, value_pos, entry.expire_timestamp
|
130
|
+
)
|
131
|
+
]
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def each_hint_file_keydir_entry
|
136
|
+
File.open(hint_path, "rb") do |file|
|
137
|
+
HintFile.new(file).each do |entry|
|
138
|
+
yield [
|
139
|
+
entry.key,
|
140
|
+
KeydirEntry.new(
|
141
|
+
id, entry.value_size, entry.value_pos, entry.expire_timestamp
|
142
|
+
)
|
143
|
+
]
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "hinted_file"
|
4
|
+
|
5
|
+
module Rubcask
|
6
|
+
# A temporary directory that is used during the merge operation.
|
7
|
+
# You probably should not use this class outside of this context.
|
8
|
+
# @see Rubcask::Directory
|
9
|
+
class MergeDirectory
|
10
|
+
def initialize(dir, max_id_ref:, config: Config.new)
|
11
|
+
@dir = dir
|
12
|
+
@config = config
|
13
|
+
@max_id = max_id_ref
|
14
|
+
|
15
|
+
@data_files = []
|
16
|
+
|
17
|
+
create_new_file!
|
18
|
+
end
|
19
|
+
|
20
|
+
def append(entry)
|
21
|
+
value_pos = active.write_pos
|
22
|
+
active.append(entry)
|
23
|
+
value_size = active.write_pos
|
24
|
+
@active_hints[entry.key] = HintEntry.new(entry.expire_timestamp, entry.key, value_pos, value_size)
|
25
|
+
|
26
|
+
if active.write_pos >= config.max_file_size
|
27
|
+
prepare_old_file!
|
28
|
+
create_new_file!
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def close
|
33
|
+
if active.write_pos == 0
|
34
|
+
File.delete(active.path)
|
35
|
+
else
|
36
|
+
prepare_old_file!
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
attr_reader :config
|
43
|
+
|
44
|
+
def prepare_old_file!
|
45
|
+
active.close
|
46
|
+
save_active_hint_file!
|
47
|
+
end
|
48
|
+
|
49
|
+
def save_active_hint_file!
|
50
|
+
File.open(active.hint_path, "ab") do |io|
|
51
|
+
hint_file = HintFile.new(io)
|
52
|
+
@active_hints.each_value do |entry|
|
53
|
+
hint_file.append(entry)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def active
|
59
|
+
@data_files.last
|
60
|
+
end
|
61
|
+
|
62
|
+
def create_new_file!
|
63
|
+
@active_hints = {}
|
64
|
+
|
65
|
+
id = @max_id.increment
|
66
|
+
file = HintedFile.new(
|
67
|
+
File.join(@dir, "#{id}.data"),
|
68
|
+
os_sync: false,
|
69
|
+
read_only: false,
|
70
|
+
ruby_sync: config.io_strategy != :ruby
|
71
|
+
)
|
72
|
+
@data_files << file
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rubcask
|
4
|
+
# Rubcask protocol is inspired by Redis, but is even simpler implementation-wise
|
5
|
+
# The format of response is
|
6
|
+
# "#{message.byte_size}"\r\n#{message}
|
7
|
+
# eg: "2\r\nOK"
|
8
|
+
# The format of request it is
|
9
|
+
# "#{message.byte_size}\r\n#{method}\r\n#{first_argument.byte_size}\r\n#{first_argument}
|
10
|
+
# eg: "13\r\nget\r\n5\r\nlorem"
|
11
|
+
module Protocol
|
12
|
+
# Success
|
13
|
+
OK = "ok"
|
14
|
+
|
15
|
+
# Repesents no data
|
16
|
+
NIL = "nil"
|
17
|
+
|
18
|
+
# Error message
|
19
|
+
ERROR = "error"
|
20
|
+
|
21
|
+
PING = "ping"
|
22
|
+
PONG = "pong"
|
23
|
+
|
24
|
+
SEPARATOR = "\r\n"
|
25
|
+
|
26
|
+
module_function
|
27
|
+
|
28
|
+
# Returns the provided message with the header of the start
|
29
|
+
# @param [String] message Message to encode
|
30
|
+
# @return [String]
|
31
|
+
def encode_message(message)
|
32
|
+
buffer = (+"").b
|
33
|
+
buffer << message.bytesize.to_s
|
34
|
+
buffer << SEPARATOR
|
35
|
+
buffer << message
|
36
|
+
buffer
|
37
|
+
end
|
38
|
+
|
39
|
+
# @param [String] method Name of the method
|
40
|
+
# @param [Array<String>] args method arguments
|
41
|
+
# @return [String]
|
42
|
+
def create_call_message(method, *args)
|
43
|
+
buffer = (+"").b
|
44
|
+
buffer << method
|
45
|
+
buffer << SEPARATOR
|
46
|
+
args.each do |arg|
|
47
|
+
buffer << encode_message(arg)
|
48
|
+
end
|
49
|
+
|
50
|
+
encode_message(buffer)
|
51
|
+
end
|
52
|
+
|
53
|
+
class << self
|
54
|
+
private
|
55
|
+
|
56
|
+
# @!macro [attach] generate_cached_message
|
57
|
+
# @method $1_message
|
58
|
+
# @note This method is autogenerated
|
59
|
+
# @return [String] Encoded "$1" messege.
|
60
|
+
def generate_cached_message(method)
|
61
|
+
value = encode_message(const_get(method.upcase)).freeze
|
62
|
+
define_method "#{method}_message" do
|
63
|
+
value
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
generate_cached_message "ok"
|
69
|
+
generate_cached_message "nil"
|
70
|
+
generate_cached_message "error"
|
71
|
+
generate_cached_message "ping"
|
72
|
+
generate_cached_message "pong"
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../bytes"
|
4
|
+
require_relative "../protocol"
|
5
|
+
require_relative "config"
|
6
|
+
|
7
|
+
module Rubcask
|
8
|
+
module Server
|
9
|
+
class AbstractServer
|
10
|
+
BLOCK_SIZE = Rubcask::Bytes::KILOBYTE * 64
|
11
|
+
MAX_READ_SIZE = BLOCK_SIZE * 128
|
12
|
+
|
13
|
+
include Protocol
|
14
|
+
|
15
|
+
attr_reader :dir
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def client_loop(conn)
|
20
|
+
loop do
|
21
|
+
length = conn.gets(Protocol::SEPARATOR)
|
22
|
+
|
23
|
+
break unless length
|
24
|
+
length = length.to_i
|
25
|
+
|
26
|
+
command_body = read_command_body(conn, length)
|
27
|
+
|
28
|
+
break unless command_body
|
29
|
+
break if command_body.bytesize != length
|
30
|
+
|
31
|
+
reader = StringIO.new(command_body)
|
32
|
+
|
33
|
+
command = reader.gets(SEPARATOR)
|
34
|
+
command&.chomp!(SEPARATOR)
|
35
|
+
|
36
|
+
args = parse_args(reader)
|
37
|
+
|
38
|
+
conn.write(execute_command!(command, args))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def execute_command!(command, args)
|
43
|
+
begin
|
44
|
+
if command == "ping"
|
45
|
+
return pong_message
|
46
|
+
end
|
47
|
+
|
48
|
+
if command == "get"
|
49
|
+
return error_message if args.size != 1
|
50
|
+
val = @dir[args[0]]
|
51
|
+
return val ? encode_message(val) : nil_message
|
52
|
+
end
|
53
|
+
|
54
|
+
if command == "set"
|
55
|
+
return error_message if args.size != 2
|
56
|
+
|
57
|
+
@dir[args[0]] = args[1]
|
58
|
+
|
59
|
+
return ok_message
|
60
|
+
end
|
61
|
+
|
62
|
+
if command == "setex"
|
63
|
+
return error_message if args.size != 3
|
64
|
+
ttl = args[2].to_i
|
65
|
+
return error_message if ttl.negative?
|
66
|
+
@dir.set_with_ttl(args[0], args[1], ttl)
|
67
|
+
return ok_message
|
68
|
+
end
|
69
|
+
|
70
|
+
if command == "del"
|
71
|
+
return error_message if args.size != 1
|
72
|
+
|
73
|
+
return @dir.delete(args[0]) ? ok_message : nil_message
|
74
|
+
end
|
75
|
+
rescue => e
|
76
|
+
logger.warn("Error " + e.to_s)
|
77
|
+
end
|
78
|
+
|
79
|
+
error_message
|
80
|
+
end
|
81
|
+
|
82
|
+
def parse_word(reader)
|
83
|
+
length = reader.gets(SEPARATOR).to_i
|
84
|
+
return nil if length.zero?
|
85
|
+
reader.read(length)
|
86
|
+
end
|
87
|
+
|
88
|
+
def read_command_body(conn, length)
|
89
|
+
command_body = (+"").b
|
90
|
+
size = 0
|
91
|
+
|
92
|
+
while size < length
|
93
|
+
val = conn.read([MAX_READ_SIZE, length - size].min)
|
94
|
+
return nil if val.nil?
|
95
|
+
size += val.bytesize
|
96
|
+
command_body << val
|
97
|
+
end
|
98
|
+
|
99
|
+
command_body
|
100
|
+
end
|
101
|
+
|
102
|
+
def parse_args(reader)
|
103
|
+
args = []
|
104
|
+
|
105
|
+
while (word = parse_word(reader))
|
106
|
+
args << word
|
107
|
+
end
|
108
|
+
|
109
|
+
args
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "async/io"
|
4
|
+
require "async/io/trap"
|
5
|
+
require "async/io/stream"
|
6
|
+
|
7
|
+
require_relative "abstract_server"
|
8
|
+
|
9
|
+
module Rubcask
|
10
|
+
module Server
|
11
|
+
# Async-based server supporting Rubcask protocol
|
12
|
+
# It requires "async-io" gem.
|
13
|
+
class Async < AbstractServer
|
14
|
+
def initialize(dir, config: Server::Config.new)
|
15
|
+
@dir = dir
|
16
|
+
@config = config
|
17
|
+
@hostname = config.hostname
|
18
|
+
@port = config.port
|
19
|
+
@logger = Logger.new($stdout)
|
20
|
+
@endpoint = ::Async::IO::Endpoint.tcp(@hostname, @port)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Shuts down the server
|
24
|
+
# @note You might want to use it inside signal trap
|
25
|
+
def shutdown
|
26
|
+
return unless @task
|
27
|
+
Sync do
|
28
|
+
@shutdown_condition.signal
|
29
|
+
@task.wait
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Starts the server
|
34
|
+
# @param [::Async::Condition, nil] on_start_condition The condition will be signalled after a successful bind
|
35
|
+
def start(on_start_condition = nil)
|
36
|
+
Async do
|
37
|
+
@shutdown_condition = ::Async::Condition.new
|
38
|
+
|
39
|
+
_, @task = @endpoint.bind do |server, task|
|
40
|
+
if @config.keepalive
|
41
|
+
server.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
|
42
|
+
end
|
43
|
+
|
44
|
+
define_close_routine(server, task)
|
45
|
+
|
46
|
+
Console.logger.info(server) { "Accepting connections on #{server.local_address.inspect}" }
|
47
|
+
|
48
|
+
server.listen(Socket::SOMAXCONN)
|
49
|
+
on_start_condition&.signal
|
50
|
+
|
51
|
+
server.accept_each do |conn|
|
52
|
+
conn.binmode
|
53
|
+
client_loop(::Async::IO::Stream.new(conn))
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def define_close_routine(server, task)
|
62
|
+
task.async do |subtask|
|
63
|
+
@shutdown_condition.wait
|
64
|
+
|
65
|
+
Console.logger.info(server) { "Shutting down connections on #{server.local_address.inspect}" }
|
66
|
+
|
67
|
+
server.close
|
68
|
+
|
69
|
+
task.stop
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def read_command_body(conn, length)
|
74
|
+
conn.read(length) # Async does the looping for us
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "socket"
|
4
|
+
|
5
|
+
require_relative "../protocol"
|
6
|
+
require_relative "pipeline"
|
7
|
+
|
8
|
+
module Rubcask
|
9
|
+
module Server
|
10
|
+
class Client
|
11
|
+
# @!macro [new] raises_invalid_response
|
12
|
+
# @raise [InvalidResponseError] If the response is invalid
|
13
|
+
|
14
|
+
class InvalidResponseError < Error; end
|
15
|
+
|
16
|
+
include Protocol
|
17
|
+
|
18
|
+
# yields a new client to the block
|
19
|
+
# closes the client after the block is terminated
|
20
|
+
# @param host [String] hostname of the server
|
21
|
+
# @param port [String] port of the server
|
22
|
+
# @yieldparam [Client] the running client
|
23
|
+
def self.with_client(host, port)
|
24
|
+
client = new(host, port)
|
25
|
+
begin
|
26
|
+
yield client
|
27
|
+
ensure
|
28
|
+
client.close
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# @param host [String] hostname of the server
|
33
|
+
# @param port [String] port of the server
|
34
|
+
def initialize(host, port)
|
35
|
+
@socket = TCPSocket.new(host, port)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Get value associated with the key
|
39
|
+
# @param [String] key
|
40
|
+
# @return [String] Binary string representing the value
|
41
|
+
# @return [Protocol::NIL] If no data associated with the key
|
42
|
+
# @macro raises_invalid_response
|
43
|
+
def get(key)
|
44
|
+
call_method("get", key)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Set value associated with the key
|
48
|
+
# @param [String] key
|
49
|
+
# @param [String] value
|
50
|
+
# @return [Protocol::OK] If set succeeded
|
51
|
+
# @return [Protocol::ERROR] If failed to set the value
|
52
|
+
# @macro raises_invalid_response
|
53
|
+
def set(key, value)
|
54
|
+
call_method("set", key, value)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Remove value associated with the key
|
58
|
+
# @param [String] key
|
59
|
+
# @return [Protocol::OK] If delete succeeded
|
60
|
+
# @return [Protocol::NIL] Otherwise
|
61
|
+
# @macro raises_invalid_response
|
62
|
+
def del(key)
|
63
|
+
call_method("del", key)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Ping the server
|
67
|
+
# Use this method to check if server is running and responding
|
68
|
+
# @return [Protocol::PONG]
|
69
|
+
# @macro raises_invalid_response
|
70
|
+
def ping
|
71
|
+
call_method("ping")
|
72
|
+
end
|
73
|
+
|
74
|
+
# Ping the server
|
75
|
+
# Use this method to check if server is running and responding
|
76
|
+
# @param [String] key
|
77
|
+
# @param [String] value
|
78
|
+
# @param [Integer, String] ttl
|
79
|
+
# @return [String] Binary string representing the value
|
80
|
+
# @return [Protocol::NIL] If no data associated with the key
|
81
|
+
# @macro raises_invalid_response
|
82
|
+
def setex(key, value, ttl)
|
83
|
+
call_method("setex", key, value, ttl.to_s)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Run the block in the pipeline
|
87
|
+
# @note pipeline execution IS NOT atomic
|
88
|
+
# @note instance_eval is used so you can call methods directly instead of using block argument
|
89
|
+
# @yield_param [Pipeline] pipeline
|
90
|
+
# @return [Array<String>] List of responses to the executed methods
|
91
|
+
# @macro raises_invalid_response
|
92
|
+
def pipelined(&block)
|
93
|
+
pipeline = Pipeline.new
|
94
|
+
pipeline.instance_eval(&block)
|
95
|
+
call(pipeline.out)
|
96
|
+
pipeline.count.times.map { get_response }
|
97
|
+
end
|
98
|
+
|
99
|
+
# Close the client
|
100
|
+
def close
|
101
|
+
@socket.close
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def call_method(method, *args)
|
107
|
+
call(create_call_message(method, *args))
|
108
|
+
get_response
|
109
|
+
end
|
110
|
+
|
111
|
+
def call(message)
|
112
|
+
@socket.write(message)
|
113
|
+
end
|
114
|
+
|
115
|
+
def get_response
|
116
|
+
length = @socket.gets(Protocol::SEPARATOR)
|
117
|
+
|
118
|
+
if length.nil?
|
119
|
+
raise InvalidResponseError, "no response"
|
120
|
+
end
|
121
|
+
length = length.to_i
|
122
|
+
|
123
|
+
response = @socket.read(length)
|
124
|
+
if response.bytesize < length
|
125
|
+
raise InvalidResponseError, "response too short"
|
126
|
+
end
|
127
|
+
response
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rubcask
|
4
|
+
module Server
|
5
|
+
# @!attribute hostname
|
6
|
+
# @return [String] Hostname of the server
|
7
|
+
# @!attribute port
|
8
|
+
# @return [Integer] Port of the server
|
9
|
+
# @!attribute timeout
|
10
|
+
# Timeut of the server
|
11
|
+
#
|
12
|
+
# If the client does not send any messages for provided number of seconds the connection with it s closed
|
13
|
+
# @return [Integer]
|
14
|
+
# @!attribute keepalive
|
15
|
+
# @return [boolean] Flag whether to set TCP's keepalive
|
16
|
+
Config = Struct.new(:hostname, :port, :timeout, :keepalive) do
|
17
|
+
def initialize
|
18
|
+
self.hostname = "localhost"
|
19
|
+
self.timeout = nil
|
20
|
+
self.keepalive = true
|
21
|
+
self.port = 8080
|
22
|
+
|
23
|
+
yield(self) if block_given?
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.configure(&block)
|
27
|
+
new(&block).freeze
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rubcask
|
4
|
+
module Server
|
5
|
+
# @!macro [new] see_client
|
6
|
+
# @see Client#$0
|
7
|
+
|
8
|
+
# Pipeline represents a sequence of commands.
|
9
|
+
# @note Pipeline execution IS NOT atomic.
|
10
|
+
# @see Client
|
11
|
+
class Pipeline
|
12
|
+
include Protocol
|
13
|
+
|
14
|
+
attr_reader :out, :count
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@out = (+"").b
|
18
|
+
@count = 0
|
19
|
+
end
|
20
|
+
|
21
|
+
# @macro see_client
|
22
|
+
def get(key)
|
23
|
+
@out << create_call_message("get", key)
|
24
|
+
end
|
25
|
+
|
26
|
+
# @macro see_client
|
27
|
+
def set(key, value)
|
28
|
+
@out << create_call_message("set", key, value)
|
29
|
+
end
|
30
|
+
|
31
|
+
# @macro see_client
|
32
|
+
def del(key)
|
33
|
+
@out << create_call_message("del", key)
|
34
|
+
end
|
35
|
+
|
36
|
+
# @macro see_client
|
37
|
+
def ping
|
38
|
+
@out << create_call_message("ping")
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def create_call_message(method, *args)
|
44
|
+
@count += 1
|
45
|
+
super
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|