oxblood 0.1.0.dev1

Sign up to get free protection for your applications and to get access to all the features.
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: