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 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
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /spec/examples.txt
10
+ /tmp/
11
+ /benchmarks/Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --no-private --protected
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
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
@@ -0,0 +1,2 @@
1
+ require 'bundler/gem_tasks'
2
+ task default: :spec
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'oxblood', path: '..'
4
+ gem 'benchmark-ips'
5
+ gem 'redis'
@@ -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,7 @@
1
+ require 'oxblood/version'
2
+ require 'oxblood/protocol'
3
+ require 'oxblood/connection'
4
+ require 'oxblood/pool'
5
+
6
+ module Oxblood
7
+ end
@@ -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
@@ -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,3 @@
1
+ module Oxblood
2
+ VERSION = '0.1.0.dev1'
3
+ 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: