oxblood 0.1.0.dev1
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/.gitignore +11 -0
- data/.rspec +2 -0
- data/.yardopts +1 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +38 -0
- data/Rakefile +2 -0
- data/benchmarks/Gemfile +5 -0
- data/benchmarks/serializer.rb +17 -0
- data/lib/oxblood.rb +7 -0
- data/lib/oxblood/buffered_io.rb +46 -0
- data/lib/oxblood/command.rb +90 -0
- data/lib/oxblood/connection.rb +137 -0
- data/lib/oxblood/pipeline.rb +19 -0
- data/lib/oxblood/pool.rb +39 -0
- data/lib/oxblood/protocol.rb +128 -0
- data/lib/oxblood/session.rb +203 -0
- data/lib/oxblood/version.rb +3 -0
- data/lib/redis/connection/oxblood.rb +57 -0
- data/oxblood.gemspec +26 -0
- metadata +149 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e3f412e0d96b8bf2a8520c3bea9465376bf3f5ea
|
4
|
+
data.tar.gz: fcbe20525510727bd767ff3e134ae35f8d7572cc
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 16ff977840a0feea5e931f0427e1f5bd8afa67336964b180a7dc166f5df6a042efde3251c9a1abfd65518aa147fbd8c1a8515dd77f89579c6997d6857288bc69
|
7
|
+
data.tar.gz: 791cd9fea4d673513a28b2e81f16b554ecb216c628cfbb97c9647715be065a8a34dc782aea4d6c06f4bc048cade2d9df9a2aced7ba43da8ab02cbf10a6321a3d
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--no-private --protected
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Konstantin Shabanov
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# Oxblood
|
2
|
+
|
3
|
+
An experimental Redis Ruby client.
|
4
|
+
|
5
|
+
## Usage
|
6
|
+
|
7
|
+
### Standalone
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
require 'oxblood'
|
11
|
+
pool = Oxblood::Pool.new(size: 8)
|
12
|
+
pool.with { |c| c.ping }
|
13
|
+
```
|
14
|
+
|
15
|
+
### As [redis-rb](https://github.com/redis/redis-rb) driver
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
[1] pry(main)> require 'redis/connection/oxblood'
|
19
|
+
=> true
|
20
|
+
[2] pry(main)> require 'redis'
|
21
|
+
=> true
|
22
|
+
# For implicit usage connection should be required before redis gem
|
23
|
+
[3] pry(main)> Redis.new.client.options[:driver]
|
24
|
+
=> Redis::Connection::Oxblood
|
25
|
+
# Explicitly
|
26
|
+
[4] pry(main)> Redis.new(driver: :oxblood).client.options[:driver]
|
27
|
+
=> Redis::Connection::Oxblood
|
28
|
+
```
|
29
|
+
|
30
|
+
|
31
|
+
## Contributing
|
32
|
+
|
33
|
+
Bug reports and pull requests are welcome on [GitHub](https://github.com/etehtsea/oxblood).
|
34
|
+
|
35
|
+
|
36
|
+
## License
|
37
|
+
|
38
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/benchmarks/Gemfile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'benchmark/ips'
|
2
|
+
require 'oxblood/protocol'
|
3
|
+
require 'redis/connection/command_helper'
|
4
|
+
|
5
|
+
CommandHelper = Object.new.tap { |o| o.extend Redis::Connection::CommandHelper }
|
6
|
+
|
7
|
+
command = [:set, 'foo', ['bar', Float::INFINITY, -Float::INFINITY, 3]]
|
8
|
+
|
9
|
+
p CommandHelper.build_command(command)
|
10
|
+
p Oxblood::Protocol.build_command(command)
|
11
|
+
raise unless CommandHelper.build_command(command) == Oxblood::Protocol.build_command(command)
|
12
|
+
|
13
|
+
Benchmark.ips do |x|
|
14
|
+
x.config(warmup: 20, benchmark: 10)
|
15
|
+
x.report('redis-ruby') { CommandHelper.build_command(command) }
|
16
|
+
x.report('Oxblood') { Oxblood::Protocol.build_command(command) }
|
17
|
+
end
|
data/lib/oxblood.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
module Oxblood
|
2
|
+
# @private
|
3
|
+
class BufferedIO
|
4
|
+
def initialize(socket)
|
5
|
+
@socket = socket
|
6
|
+
@buffer = String.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def gets(separator, timeout)
|
10
|
+
crlf = nil
|
11
|
+
|
12
|
+
while (crlf = @buffer.index(separator)) == nil
|
13
|
+
@buffer << _read_from_socket(1024, timeout)
|
14
|
+
end
|
15
|
+
|
16
|
+
@buffer.slice!(0, crlf + separator.bytesize)
|
17
|
+
end
|
18
|
+
|
19
|
+
def read(nbytes, timeout)
|
20
|
+
result = @buffer.slice!(0, nbytes)
|
21
|
+
|
22
|
+
while result.bytesize < nbytes
|
23
|
+
result << _read_from_socket(nbytes - result.bytesize, timeout)
|
24
|
+
end
|
25
|
+
|
26
|
+
result
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def _read_from_socket(nbytes, timeout)
|
32
|
+
begin
|
33
|
+
@socket.read_nonblock(nbytes)
|
34
|
+
rescue Errno::EWOULDBLOCK, Errno::EAGAIN
|
35
|
+
if IO.select([@socket], nil, nil, timeout)
|
36
|
+
retry
|
37
|
+
else
|
38
|
+
raise Connection::TimeoutError
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
rescue EOFError
|
43
|
+
raise Errno::ECONNRESET
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Oxblood
|
2
|
+
module Command
|
3
|
+
class << self
|
4
|
+
def hdel(key, fields)
|
5
|
+
serialize([:HDEL, key, fields])
|
6
|
+
end
|
7
|
+
|
8
|
+
def hexists(key, field)
|
9
|
+
serialize([:HEXISTS, key, field])
|
10
|
+
end
|
11
|
+
|
12
|
+
def hmset(key, *args)
|
13
|
+
serialize(args.unshift(:HMSET, key))
|
14
|
+
end
|
15
|
+
|
16
|
+
def hget(key, field)
|
17
|
+
serialize([:HGET, key, field])
|
18
|
+
end
|
19
|
+
|
20
|
+
def hmget(key, *fields)
|
21
|
+
serialize(fields.unshift(:HMGET, key))
|
22
|
+
end
|
23
|
+
|
24
|
+
def hgetall(key)
|
25
|
+
serialize([:HGETALL, key])
|
26
|
+
end
|
27
|
+
|
28
|
+
# ------------------ Strings ---------------------
|
29
|
+
|
30
|
+
# ------------------ Connection ---------------------
|
31
|
+
|
32
|
+
def ping(message = nil)
|
33
|
+
command = [:PING]
|
34
|
+
command << message if message
|
35
|
+
|
36
|
+
serialize(command)
|
37
|
+
end
|
38
|
+
|
39
|
+
# ------------------ Server ---------------------
|
40
|
+
|
41
|
+
def info(section = nil)
|
42
|
+
command = [:INFO]
|
43
|
+
command << section if section
|
44
|
+
|
45
|
+
serialize(command)
|
46
|
+
end
|
47
|
+
|
48
|
+
# ------------------ Keys ------------------------
|
49
|
+
|
50
|
+
def del(*keys)
|
51
|
+
serialize(keys.unshift(:DEL))
|
52
|
+
end
|
53
|
+
|
54
|
+
def keys(pattern)
|
55
|
+
serialize([:KEYS, pattern])
|
56
|
+
end
|
57
|
+
|
58
|
+
def expire(key, seconds)
|
59
|
+
serialize([:EXPIRE, key, seconds])
|
60
|
+
end
|
61
|
+
|
62
|
+
# ------------------ Sets ------------------------
|
63
|
+
|
64
|
+
def sadd(key, *members)
|
65
|
+
serialize(members.unshift(:SADD, key))
|
66
|
+
end
|
67
|
+
|
68
|
+
def sunion(*keys)
|
69
|
+
serialize(keys.unshift(:SUNION))
|
70
|
+
end
|
71
|
+
|
72
|
+
# ------------------ Sorted Sets -----------------
|
73
|
+
|
74
|
+
def zadd(key, *args)
|
75
|
+
serialize(args.unshift(:ZADD, key))
|
76
|
+
end
|
77
|
+
|
78
|
+
# @todo Support optional args (WITHSCORES/LIMIT)
|
79
|
+
def zrangebyscore(key, min, max)
|
80
|
+
serialize([:ZRANGEBYSCORE, key, min, max])
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def serialize(command)
|
86
|
+
Protocol.build_command(command)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'oxblood/protocol'
|
2
|
+
require 'oxblood/buffered_io'
|
3
|
+
|
4
|
+
module Oxblood
|
5
|
+
# Class responsible for connection maintenance
|
6
|
+
class Connection
|
7
|
+
TimeoutError = Class.new(RuntimeError)
|
8
|
+
|
9
|
+
class << self
|
10
|
+
# Open Redis connection
|
11
|
+
#
|
12
|
+
# @param [Hash] options Connection options
|
13
|
+
#
|
14
|
+
# @option (see .connect_tcp)
|
15
|
+
# @option (see .connect_unix)
|
16
|
+
def open(options = {})
|
17
|
+
options.key?(:path) ? connect_unix(options) : connect_tcp(options)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Connect to Redis server through TCP
|
21
|
+
#
|
22
|
+
# @param [Hash] options Connection options
|
23
|
+
#
|
24
|
+
# @option options [String] :host ('localhost') Hostname or IP address to connect to
|
25
|
+
# @option options [Integer] :port (5672) Port Redis server listens on
|
26
|
+
# @option options [Float] :timeout (1.0) socket read timeout
|
27
|
+
# @option options [Float] :connect_timeout (1.0) socket connect timeout
|
28
|
+
#
|
29
|
+
# @return [Oxblood::Connection] connection instance
|
30
|
+
def connect_tcp(options = {})
|
31
|
+
host = options.fetch(:host, 'localhost')
|
32
|
+
port = options.fetch(:port, 6379)
|
33
|
+
timeout = options.fetch(:timeout, 1.0)
|
34
|
+
connect_timeout = options.fetch(:connect_timeout, 1.0)
|
35
|
+
|
36
|
+
socket = Socket.tcp(host, port, connect_timeout: connect_timeout)
|
37
|
+
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
38
|
+
|
39
|
+
new(socket, timeout)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Connect to Redis server through UNIX socket
|
43
|
+
#
|
44
|
+
# @param [Hash] options Connection options
|
45
|
+
#
|
46
|
+
# @option options [String] :path UNIX socket path
|
47
|
+
# @option options [Float] :timeout (1.0) socket read timeout
|
48
|
+
#
|
49
|
+
# @raise [KeyError] if :path was not passed
|
50
|
+
#
|
51
|
+
# @return [Oxblood::Connection] connection instance
|
52
|
+
def connect_unix(options = {})
|
53
|
+
path = options.fetch(:path)
|
54
|
+
timeout = options.fetch(:timeout, 1.0)
|
55
|
+
|
56
|
+
socket = ::Socket.unix(path)
|
57
|
+
new(socket, timeout)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def initialize(socket, timeout)
|
62
|
+
@socket = socket
|
63
|
+
@timeout = timeout
|
64
|
+
@buffer = BufferedIO.new(socket)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Send comand to Redis server
|
68
|
+
# @example send_command(['CONFIG', 'GET', '*']) => 32
|
69
|
+
# @param [Array] command Array of command name with it's args
|
70
|
+
# @return [Integer] Number of bytes written to socket
|
71
|
+
def send_command(command)
|
72
|
+
write(Protocol.build_command(command))
|
73
|
+
end
|
74
|
+
|
75
|
+
# FIXME: docs
|
76
|
+
def write(command)
|
77
|
+
@socket.write(command)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Send command to Redis server and read response from it
|
81
|
+
# @example run_command(['PING']) => PONG
|
82
|
+
# @param [Array] command Array of command name with it's args
|
83
|
+
# @return #FIXME
|
84
|
+
def run_command(command)
|
85
|
+
send_command(command)
|
86
|
+
read_response
|
87
|
+
end
|
88
|
+
|
89
|
+
# True if connection is established
|
90
|
+
# @return [Boolean] connection status
|
91
|
+
def connected?
|
92
|
+
!!@socket
|
93
|
+
end
|
94
|
+
|
95
|
+
# Close connection to server
|
96
|
+
def close
|
97
|
+
@socket.close
|
98
|
+
ensure
|
99
|
+
@socket = nil
|
100
|
+
end
|
101
|
+
|
102
|
+
# Read number of bytes
|
103
|
+
# @param [Integer] nbytes number of bytes to read
|
104
|
+
# @return [String] read result
|
105
|
+
def read(nbytes)
|
106
|
+
@buffer.read(nbytes, @timeout)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Read until separator
|
110
|
+
# @param [String] sep separator
|
111
|
+
# @return [String] read result
|
112
|
+
def gets(sep)
|
113
|
+
@buffer.gets(sep, @timeout)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Set new read timeout
|
117
|
+
# @param [Float] timeout new timeout
|
118
|
+
def timeout=(timeout)
|
119
|
+
@timeout = timeout
|
120
|
+
end
|
121
|
+
|
122
|
+
# Read response from server
|
123
|
+
# @raise [TimeoutError] if timeout happen
|
124
|
+
# @note Will raise TimeoutError even if there is simply no response to read
|
125
|
+
# from server. For example, if you are trying to read response before
|
126
|
+
# sending command.
|
127
|
+
# @todo Raise specific error if server has nothing to answer.
|
128
|
+
def read_response
|
129
|
+
Protocol.parse(self)
|
130
|
+
end
|
131
|
+
|
132
|
+
# FIXME: docs
|
133
|
+
def read_responses(n)
|
134
|
+
Array.new(n) { read_response }
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'oxblood/command'
|
2
|
+
|
3
|
+
module Oxblood
|
4
|
+
class Pipeline < Session
|
5
|
+
def initialize(connection)
|
6
|
+
super
|
7
|
+
@commands = Array.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def run(command)
|
11
|
+
@commands << command
|
12
|
+
end
|
13
|
+
|
14
|
+
def sync
|
15
|
+
@connection.write(@commands.join)
|
16
|
+
@connection.read_responses(@commands.size)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/oxblood/pool.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'connection_pool'
|
2
|
+
require 'oxblood/session'
|
3
|
+
require 'oxblood/pipeline'
|
4
|
+
|
5
|
+
module Oxblood
|
6
|
+
class Pool
|
7
|
+
# Initialize connection pool
|
8
|
+
#
|
9
|
+
# @param [Hash] options Connection options
|
10
|
+
#
|
11
|
+
# @option options [Float] :timeout (1.0) Connection acquisition timeout.
|
12
|
+
# @option options [Integer] :size Pool size.
|
13
|
+
# @option options [Hash] :connection see {Connection.open}
|
14
|
+
def initialize(options = {})
|
15
|
+
timeout = options.fetch(:timeout, 1.0)
|
16
|
+
size = options.fetch(:size)
|
17
|
+
|
18
|
+
@pool = ConnectionPool.new(size: size, timeout: timeout) do
|
19
|
+
Connection.open(options.fetch(:connection, {}))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def with
|
24
|
+
conn = @pool.checkout
|
25
|
+
yield Session.new(conn)
|
26
|
+
ensure
|
27
|
+
@pool.checkin if conn
|
28
|
+
end
|
29
|
+
|
30
|
+
def pipelined
|
31
|
+
conn = @pool.checkout
|
32
|
+
pipeline = Pipeline.new(conn)
|
33
|
+
yield pipeline
|
34
|
+
pipeline.sync
|
35
|
+
ensure
|
36
|
+
@pool.checkin if conn
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
module Oxblood
|
2
|
+
module Protocol
|
3
|
+
SerializerError = Class.new(RuntimeError)
|
4
|
+
ParserError = Class.new(RuntimeError)
|
5
|
+
RError = Class.new(RuntimeError)
|
6
|
+
|
7
|
+
SIMPLE_STRING = '+'.freeze
|
8
|
+
private_constant :SIMPLE_STRING
|
9
|
+
|
10
|
+
ERROR = '-'.freeze
|
11
|
+
private_constant :ERROR
|
12
|
+
|
13
|
+
INTEGER = ':'.freeze
|
14
|
+
private_constant :INTEGER
|
15
|
+
|
16
|
+
BULK_STRING = '$'.freeze
|
17
|
+
private_constant :BULK_STRING
|
18
|
+
|
19
|
+
ARRAY = '*'.freeze
|
20
|
+
private_constant :ARRAY
|
21
|
+
|
22
|
+
TERMINATOR = "\r\n".freeze
|
23
|
+
private_constant :TERMINATOR
|
24
|
+
|
25
|
+
EMPTY_ARRAY_RESPONSE = "#{ARRAY}0#{TERMINATOR}".freeze
|
26
|
+
private_constant :EMPTY_ARRAY_RESPONSE
|
27
|
+
|
28
|
+
NULL_ARRAY_RESPONSE = "#{ARRAY}-1#{TERMINATOR}".freeze
|
29
|
+
private_constant :NULL_ARRAY_RESPONSE
|
30
|
+
|
31
|
+
EMPTY_BULK_STRING_RESPONSE = "#{BULK_STRING}0#{TERMINATOR}#{TERMINATOR}".freeze
|
32
|
+
private_constant :EMPTY_BULK_STRING_RESPONSE
|
33
|
+
|
34
|
+
NULL_BULK_STRING_RESPONSE = "#{BULK_STRING}-1#{TERMINATOR}".freeze
|
35
|
+
private_constant :NULL_BULK_STRING_RESPONSE
|
36
|
+
|
37
|
+
EMPTY_STRING = ''.freeze
|
38
|
+
private_constant :EMPTY_STRING
|
39
|
+
|
40
|
+
EMPTY_ARRAY = [].freeze
|
41
|
+
private_constant :EMPTY_ARRAY
|
42
|
+
|
43
|
+
COMMAND_HEADER = [ARRAY, TERMINATOR].join.freeze
|
44
|
+
private_constant :COMMAND_HEADER
|
45
|
+
|
46
|
+
class << self
|
47
|
+
# Parse redis response
|
48
|
+
# @see http://redis.io/topics/protocol
|
49
|
+
# @raise [ParserError] if unable to parse response
|
50
|
+
# @param [#read, #gets] io IO or IO-like object to read from
|
51
|
+
# @return [String, RError, Integer, Array]
|
52
|
+
def parse(io)
|
53
|
+
line = io.gets(TERMINATOR)
|
54
|
+
|
55
|
+
case line[0]
|
56
|
+
when SIMPLE_STRING
|
57
|
+
line[1..-3]
|
58
|
+
when ERROR
|
59
|
+
RError.new(line[1..-3])
|
60
|
+
when INTEGER
|
61
|
+
line[1..-3].to_i
|
62
|
+
when BULK_STRING
|
63
|
+
return if line == NULL_BULK_STRING_RESPONSE
|
64
|
+
|
65
|
+
body_length = line[1..-1].to_i
|
66
|
+
|
67
|
+
case body_length
|
68
|
+
when -1 then nil
|
69
|
+
when 0 then
|
70
|
+
# discard CRLF
|
71
|
+
io.read(2)
|
72
|
+
EMPTY_STRING
|
73
|
+
else
|
74
|
+
# string length plus CRLF
|
75
|
+
body = io.read(body_length + 2)
|
76
|
+
body[0..-3]
|
77
|
+
end
|
78
|
+
when ARRAY
|
79
|
+
return if line == NULL_ARRAY_RESPONSE
|
80
|
+
return EMPTY_ARRAY if line == EMPTY_ARRAY_RESPONSE
|
81
|
+
|
82
|
+
size = line[1..-1].to_i
|
83
|
+
|
84
|
+
Array.new(size) { parse(io) }
|
85
|
+
else
|
86
|
+
raise ParserError.new('Unsupported response type')
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Serialize command to string according to Redis Protocol
|
91
|
+
# @note Redis don't support nested arrays
|
92
|
+
# @note Written in non-idiomatic ruby without error handling due to
|
93
|
+
# performance reasons
|
94
|
+
# @see http://www.redis.io/topics/protocol#sending-commands-to-a-redis-server
|
95
|
+
# @raise [SerializerError] if unable to serialize given command
|
96
|
+
# @param [Array] command array consisting of redis command and arguments
|
97
|
+
# @return [String] serialized command
|
98
|
+
def build_command(command)
|
99
|
+
result = COMMAND_HEADER.dup
|
100
|
+
size = 0
|
101
|
+
command.each do |c|
|
102
|
+
if Array === c
|
103
|
+
c.each do |e|
|
104
|
+
append!(e, result)
|
105
|
+
size += 1
|
106
|
+
end
|
107
|
+
else
|
108
|
+
append!(c, result)
|
109
|
+
size += 1
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
result.insert(1, size.to_s)
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def append!(elem, command)
|
119
|
+
elem = elem.to_s
|
120
|
+
command << BULK_STRING
|
121
|
+
command << elem.bytesize.to_s
|
122
|
+
command << TERMINATOR
|
123
|
+
command << elem
|
124
|
+
command << TERMINATOR
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,203 @@
|
|
1
|
+
require 'oxblood/connection'
|
2
|
+
require 'oxblood/command'
|
3
|
+
|
4
|
+
module Oxblood
|
5
|
+
class Session
|
6
|
+
def initialize(connection)
|
7
|
+
@connection = connection
|
8
|
+
end
|
9
|
+
|
10
|
+
# ------------------ Hashes ---------------------
|
11
|
+
|
12
|
+
# Removes the specified fields from the hash stored at key
|
13
|
+
# @see http://redis.io/commands/hdel
|
14
|
+
#
|
15
|
+
# @param [String] key under which hash is stored
|
16
|
+
# @param [Array<#to_s>] fields to delete
|
17
|
+
#
|
18
|
+
# @return [Integer] the number of fields that were removed from the hash
|
19
|
+
def hdel(key, fields)
|
20
|
+
run(cmd.hdel(key, fields))
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns if field is an existing field in the hash stored at key
|
24
|
+
# @see http://redis.io/commands/hexists
|
25
|
+
#
|
26
|
+
# @param [String] key under which hash is stored
|
27
|
+
# @param [String] field to check for existence
|
28
|
+
#
|
29
|
+
# @return [Boolean] do hash contains field or not
|
30
|
+
def hexists(key, field)
|
31
|
+
1 == run(cmd.hexists(key, field))
|
32
|
+
end
|
33
|
+
|
34
|
+
# Set multiple hash fields to multiple values
|
35
|
+
# @see http://redis.io/commands/hmset
|
36
|
+
#
|
37
|
+
# @param [String] key under which store hash
|
38
|
+
# @param [[String, String], Array<[String, String]>] args fields and values
|
39
|
+
#
|
40
|
+
# @return [String] 'OK'
|
41
|
+
def hmset(key, *args)
|
42
|
+
run(cmd.hmset(key, *args))
|
43
|
+
end
|
44
|
+
|
45
|
+
# Get the value of a hash field
|
46
|
+
# @see http://redis.io/commands/hget
|
47
|
+
#
|
48
|
+
# @param [String] key under which hash is stored
|
49
|
+
# @param [String] field name
|
50
|
+
#
|
51
|
+
# @return [String, nil] the value associated with field
|
52
|
+
# or nil when field is not present in the hash or key does not exist.
|
53
|
+
def hget(key, field)
|
54
|
+
run(cmd.hget(key, field))
|
55
|
+
end
|
56
|
+
|
57
|
+
# Get the field values of all given hash fields
|
58
|
+
# @see http://redis.io/commands/hmget
|
59
|
+
#
|
60
|
+
# @param [String] key under which hash is stored
|
61
|
+
# @param [String, Array<String>] fields to get
|
62
|
+
#
|
63
|
+
# @return [Array] list of values associated with the given fields,
|
64
|
+
# in the same order as they are requested.
|
65
|
+
def hmget(key, *fields)
|
66
|
+
run(cmd.hmget(key, *fields))
|
67
|
+
end
|
68
|
+
|
69
|
+
# Get all the fields and values in a hash
|
70
|
+
# @see http://redis.io/commands/hgetall
|
71
|
+
#
|
72
|
+
# @param [String] key under which hash is stored
|
73
|
+
#
|
74
|
+
# @return [Hash] of fields and their values
|
75
|
+
def hgetall(key)
|
76
|
+
Hash[*run(cmd.hgetall(key))]
|
77
|
+
end
|
78
|
+
|
79
|
+
# ------------------ Strings ---------------------
|
80
|
+
|
81
|
+
# ------------------ Connection ---------------------
|
82
|
+
|
83
|
+
# Returns PONG if no argument is provided, otherwise return a copy of
|
84
|
+
# the argument as a bulk
|
85
|
+
# @see http://redis.io/commands/ping
|
86
|
+
#
|
87
|
+
# @param [String] message to return
|
88
|
+
#
|
89
|
+
# @return [String] message passed as argument
|
90
|
+
def ping(message = nil)
|
91
|
+
run(cmd.ping(message))
|
92
|
+
end
|
93
|
+
|
94
|
+
# ------------------ Server ---------------------
|
95
|
+
|
96
|
+
# Returns information and statistics about the server in a format that is
|
97
|
+
# simple to parse by computers and easy to read by humans
|
98
|
+
# @see http://redis.io/commands/info
|
99
|
+
#
|
100
|
+
# @param [String] section used to select a specific section of information
|
101
|
+
def info(section = nil)
|
102
|
+
command = [:INFO]
|
103
|
+
command << section if section
|
104
|
+
|
105
|
+
response = run(cmd.info(section))
|
106
|
+
# FIXME: Parse response
|
107
|
+
end
|
108
|
+
|
109
|
+
# ------------------ Keys ------------------------
|
110
|
+
|
111
|
+
# Delete a key
|
112
|
+
# @see http://redis.io/commands/del
|
113
|
+
#
|
114
|
+
# @param [String, Array<String>] keys to delete
|
115
|
+
#
|
116
|
+
# @return [Integer] the number of keys that were removed
|
117
|
+
def del(*keys)
|
118
|
+
run(cmd.del(*keys))
|
119
|
+
end
|
120
|
+
|
121
|
+
# Find all keys matching the given pattern
|
122
|
+
# @see http://redis.io/commands/keys
|
123
|
+
#
|
124
|
+
# @param [String] pattern used to match keys
|
125
|
+
def keys(pattern)
|
126
|
+
run(cmd.keys(pattern))
|
127
|
+
end
|
128
|
+
|
129
|
+
# Set a key's time to live in seconds
|
130
|
+
# @see http://redis.io/commands/expire
|
131
|
+
#
|
132
|
+
# @param [String] key to expire
|
133
|
+
# @param [Integer] seconds number of seconds
|
134
|
+
#
|
135
|
+
# @return [Integer] 1 if the timeout was set. 0 if key does not exist or
|
136
|
+
# the timeout could not be set.
|
137
|
+
def expire(key, seconds)
|
138
|
+
run(cmd.expire(key, seconds))
|
139
|
+
end
|
140
|
+
|
141
|
+
# ------------------ Sets ------------------------
|
142
|
+
|
143
|
+
# Add one or more members to a set
|
144
|
+
# @see http://redis.io/commands/sadd
|
145
|
+
#
|
146
|
+
# @param [String] key under which store set
|
147
|
+
# @param [String, Array<String>] members to store
|
148
|
+
#
|
149
|
+
# @return [Integer] the number of elements that were added to the set,
|
150
|
+
# not including all the elements already present into the set.
|
151
|
+
def sadd(key, *members)
|
152
|
+
run(cmd.sadd(key, *members))
|
153
|
+
end
|
154
|
+
|
155
|
+
# Add multiple sets
|
156
|
+
# @see http://redis.io/commands/sunion
|
157
|
+
#
|
158
|
+
# @param [String, Array<String>] keys
|
159
|
+
#
|
160
|
+
# @return [Array] list with members of the resulting set
|
161
|
+
def sunion(*keys)
|
162
|
+
run(cmd.sunion(*keys))
|
163
|
+
end
|
164
|
+
|
165
|
+
# ------------------ Sorted Sets -----------------
|
166
|
+
|
167
|
+
# Add one or more members to a sorted set, or update its score if it already
|
168
|
+
# exists.
|
169
|
+
# @see http://redis.io/commands/zadd
|
170
|
+
#
|
171
|
+
# @todo Add support for zadd options
|
172
|
+
# http://redis.io/commands/zadd#zadd-options-redis-302-or-greater
|
173
|
+
#
|
174
|
+
# @param [String] key under which store set
|
175
|
+
# @param [[Float, String], Array<[Float, String]>] args scores and members
|
176
|
+
def zadd(key, *args)
|
177
|
+
run(cmd.zadd(key, *args))
|
178
|
+
end
|
179
|
+
|
180
|
+
# Return a range of members in a sorted set, by score
|
181
|
+
# @see http://redis.io/commands/zrangebyscore
|
182
|
+
#
|
183
|
+
# @todo Support optional args (WITHSCORES/LIMIT)
|
184
|
+
#
|
185
|
+
# @param [String] key under which set is stored
|
186
|
+
# @param [String] min value
|
187
|
+
# @param [String] max value
|
188
|
+
def zrangebyscore(key, min, max)
|
189
|
+
run(cmd.zrangebyscore(key, min, max))
|
190
|
+
end
|
191
|
+
|
192
|
+
protected
|
193
|
+
|
194
|
+
def cmd
|
195
|
+
Command
|
196
|
+
end
|
197
|
+
|
198
|
+
def run(command)
|
199
|
+
@connection.write(command)
|
200
|
+
@connection.read_response
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'redis/connection/registry'
|
2
|
+
require 'redis/errors'
|
3
|
+
require 'oxblood'
|
4
|
+
|
5
|
+
class Redis
|
6
|
+
module Connection
|
7
|
+
class Oxblood
|
8
|
+
def self.connect(config)
|
9
|
+
conn_type = config[:scheme] == 'unix' ? :unix : :tcp
|
10
|
+
connection = ::Oxblood::Connection.public_send(:"connect_#{conn_type}", config)
|
11
|
+
|
12
|
+
new(connection)
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(connection)
|
16
|
+
@connection = connection
|
17
|
+
end
|
18
|
+
|
19
|
+
def connected?
|
20
|
+
@connection && @connection.connected?
|
21
|
+
end
|
22
|
+
|
23
|
+
def timeout=(timeout)
|
24
|
+
@connection.timeout = timeout > 0 ? timeout : nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def disconnect
|
28
|
+
@connection.close
|
29
|
+
end
|
30
|
+
|
31
|
+
def write(command)
|
32
|
+
@connection.send_command(command)
|
33
|
+
end
|
34
|
+
|
35
|
+
def read
|
36
|
+
reply = @connection.read_response
|
37
|
+
reply = encode(reply) if reply.is_a?(String)
|
38
|
+
reply = CommandError.new(reply.message) if reply.is_a?(::Oxblood::Protocol::RError)
|
39
|
+
reply
|
40
|
+
rescue ::Oxblood::Protocol::ParserError => e
|
41
|
+
raise Redis::ProtocolError.new(e.message)
|
42
|
+
end
|
43
|
+
|
44
|
+
if defined?(Encoding::default_external)
|
45
|
+
def encode(string)
|
46
|
+
string.force_encoding(Encoding::default_external)
|
47
|
+
end
|
48
|
+
else
|
49
|
+
def encode(string)
|
50
|
+
string
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
Redis::Connection.drivers << Redis::Connection::Oxblood
|
data/oxblood.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'oxblood/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'oxblood'
|
8
|
+
spec.version = Oxblood::VERSION
|
9
|
+
spec.authors = ['Konstantin Shabanov']
|
10
|
+
spec.email = ['etehtsea@gmail.com']
|
11
|
+
|
12
|
+
spec.summary = 'A Ruby Redis client'
|
13
|
+
spec.description = 'An experimental Ruby Redis client'
|
14
|
+
spec.homepage = 'https://github.com/etehtsea/oxblood'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
18
|
+
spec.require_paths = ['lib']
|
19
|
+
|
20
|
+
spec.add_dependency 'connection_pool'
|
21
|
+
spec.add_development_dependency 'bundler', '~> 1.11'
|
22
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
23
|
+
spec.add_development_dependency 'rspec', "~> 3.4"
|
24
|
+
spec.add_development_dependency 'pry'
|
25
|
+
spec.add_development_dependency 'yard'
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: oxblood
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0.dev1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Konstantin Shabanov
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-06-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: connection_pool
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.11'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.11'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.4'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.4'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pry
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: yard
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: An experimental Ruby Redis client
|
98
|
+
email:
|
99
|
+
- etehtsea@gmail.com
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- ".gitignore"
|
105
|
+
- ".rspec"
|
106
|
+
- ".yardopts"
|
107
|
+
- Gemfile
|
108
|
+
- LICENSE.txt
|
109
|
+
- README.md
|
110
|
+
- Rakefile
|
111
|
+
- benchmarks/Gemfile
|
112
|
+
- benchmarks/serializer.rb
|
113
|
+
- lib/oxblood.rb
|
114
|
+
- lib/oxblood/buffered_io.rb
|
115
|
+
- lib/oxblood/command.rb
|
116
|
+
- lib/oxblood/connection.rb
|
117
|
+
- lib/oxblood/pipeline.rb
|
118
|
+
- lib/oxblood/pool.rb
|
119
|
+
- lib/oxblood/protocol.rb
|
120
|
+
- lib/oxblood/session.rb
|
121
|
+
- lib/oxblood/version.rb
|
122
|
+
- lib/redis/connection/oxblood.rb
|
123
|
+
- oxblood.gemspec
|
124
|
+
homepage: https://github.com/etehtsea/oxblood
|
125
|
+
licenses:
|
126
|
+
- MIT
|
127
|
+
metadata: {}
|
128
|
+
post_install_message:
|
129
|
+
rdoc_options: []
|
130
|
+
require_paths:
|
131
|
+
- lib
|
132
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
133
|
+
requirements:
|
134
|
+
- - ">="
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: '0'
|
137
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
138
|
+
requirements:
|
139
|
+
- - ">"
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: 1.3.1
|
142
|
+
requirements: []
|
143
|
+
rubyforge_project:
|
144
|
+
rubygems_version: 2.6.4
|
145
|
+
signing_key:
|
146
|
+
specification_version: 4
|
147
|
+
summary: A Ruby Redis client
|
148
|
+
test_files: []
|
149
|
+
has_rdoc:
|