Skip to content

Commit 5c6d9a1

Browse files
committed
Merge branch 'sentinel-tests' into zmack-fix_unconnectable_sentinel
Conflicts: lib/redis/client.rb
2 parents e7168e6 + fe31b6d commit 5c6d9a1

File tree

9 files changed

+313
-34
lines changed

9 files changed

+313
-34
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020

2121
* Added `:connect_timeout` option.
2222

23+
* Added support for `:limit` option for `ZREVRANGEBYLEX`.
24+
25+
* Fixed an issue where connections become inconsistent when using Ruby's
26+
Timeout module outside of the client (see #501, #502).
27+
2328
# 3.2.0
2429

2530
* Redis Sentinel support.

README.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@ and one or more slaves (`mymaster` in the example).
105105

106106
* It is possible to optionally provide a role. The allowed roles are `master`
107107
and `slave`. When the role is `slave`, the client will try to connect to a
108-
random slave of the specified master.
108+
random slave of the specified master. If a role is not specified, the client
109+
will connect to the master.
109110

110111
* When using the Sentinel support you need to specify a list of sentinels to
111112
connect to. The list does not need to enumerate all your Sentinel instances,
@@ -170,7 +171,7 @@ end
170171
Replies to commands in a pipeline can be accessed via the *futures* they
171172
emit (since redis-rb 3.0). All calls inside a pipeline block return a
172173
`Future` object, which responds to the `#value` method. When the
173-
pipeline has succesfully executed, all futures are assigned their
174+
pipeline has successfully executed, all futures are assigned their
174175
respective replies and can be used.
175176

176177
```ruby
@@ -186,6 +187,26 @@ end
186187
# => 1
187188
```
188189

190+
## Error Handling
191+
192+
In general, if something goes wrong you'll get an exception. For example, if
193+
it can't connect to the server a `Redis::CannotConnectError` error will be raised.
194+
195+
```ruby
196+
begin
197+
redis.ping
198+
rescue Exception => e
199+
e.inspect
200+
# => #<Redis::CannotConnectError: Timed out connecting to Redis on 10.0.1.1:6380>
201+
202+
e.message
203+
# => Timed out connecting to Redis on 10.0.1.1:6380
204+
end
205+
```
206+
207+
See lib/redis/errors.rb for information about what exceptions are possible.
208+
209+
189210
## Expert-Mode Options
190211

191212
- `inherit_socket: true`: disable safety check that prevents a forked child

lib/redis.rb

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -764,7 +764,7 @@ def msetnx(*args)
764764
# Set one or more values, only if none of the keys exist.
765765
#
766766
# @example
767-
# redis.msetnx({ "key1" => "v1", "key2" => "v2" })
767+
# redis.mapped_msetnx({ "key1" => "v1", "key2" => "v2" })
768768
# # => true
769769
#
770770
# @param [Hash] hash keys mapping to values
@@ -1620,6 +1620,28 @@ def zrangebylex(key, min, max, options = {})
16201620
end
16211621
end
16221622

1623+
# Return a range of members with the same score in a sorted set, by reversed lexicographical ordering.
1624+
# Apart from the reversed ordering, #zrevrangebylex is similar to #zrangebylex.
1625+
#
1626+
# @example Retrieve members matching a
1627+
# redis.zrevrangebylex("zset", "[a", "[a\xff")
1628+
# # => ["abbygail", "abby", "abagael", "aaren"]
1629+
# @example Retrieve the last 2 members matching a
1630+
# redis.zrevrangebylex("zset", "[a", "[a\xff", :limit => [0, 2])
1631+
# # => ["abbygail", "abby"]
1632+
#
1633+
# @see #zrangebylex
1634+
def zrevrangebylex(key, max, min, options = {})
1635+
args = []
1636+
1637+
limit = options[:limit]
1638+
args.concat(["LIMIT"] + limit) if limit
1639+
1640+
synchronize do |client|
1641+
client.call([:zrevrangebylex, key, max, min] + args)
1642+
end
1643+
end
1644+
16231645
# Return a range of members in a sorted set, by score.
16241646
#
16251647
# @example Retrieve members with score `>= 5` and `< 100`
@@ -1895,7 +1917,7 @@ def hmget(key, *fields, &blk)
18951917
# Get the values of all the given hash fields.
18961918
#
18971919
# @example
1898-
# redis.hmget("hash", "f1", "f2")
1920+
# redis.mapped_hmget("hash", "f1", "f2")
18991921
# # => { "f1" => "v1", "f2" => "v2" }
19001922
#
19011923
# @param [String] key
@@ -2032,7 +2054,7 @@ def punsubscribe(*channels)
20322054
end
20332055
end
20342056

2035-
# Inspect the state of the Pub/Sub subsystem.
2057+
# Inspect the state of the Pub/Sub subsystem.
20362058
# Possible subcommands: channels, numsub, numpat.
20372059
def pubsub(subcommand, *args)
20382060
synchronize do |client|
@@ -2458,7 +2480,7 @@ def sscan(key, cursor, options={})
24582480
# Scan a set
24592481
#
24602482
# @example Retrieve all of the keys in a set
2461-
# redis.sscan("set").to_a
2483+
# redis.sscan_each("set").to_a
24622484
# # => ["key1", "key2", "key3"]
24632485
#
24642486
# @param [Hash] options

lib/redis/client.rb

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ def initialize(options = {})
7777
@connection = nil
7878
@command_map = {}
7979

80+
@pending_reads = 0
81+
8082
if options.include?(:sentinels)
8183
@connector = Connector::Sentinel.new(@options)
8284
else
@@ -243,12 +245,15 @@ def io
243245

244246
def read
245247
io do
246-
connection.read
248+
value = connection.read
249+
@pending_reads -= 1
250+
value
247251
end
248252
end
249253

250254
def write(command)
251255
io do
256+
@pending_reads += 1
252257
connection.write(command)
253258
end
254259
end
@@ -312,9 +317,10 @@ def establish_connection
312317
server = @connector.resolve.dup
313318

314319
@options[:host] = server[:host]
315-
@options[:port] = server[:port]
320+
@options[:port] = Integer(server[:port]) if server.include?(:port)
316321

317-
@connection = @options[:driver].connect(server)
322+
@connection = @options[:driver].connect(@options)
323+
@pending_reads = 0
318324
rescue TimeoutError,
319325
Errno::ECONNREFUSED,
320326
Errno::EHOSTDOWN,
@@ -326,6 +332,8 @@ def establish_connection
326332
end
327333

328334
def ensure_connected
335+
disconnect if @pending_reads > 0
336+
329337
attempts = 0
330338

331339
begin
@@ -383,7 +391,7 @@ def _parse_options(options)
383391
defaults[:path] = uri.path
384392
elsif uri.scheme == "redis"
385393
# Require the URL to have at least a host
386-
raise ArgumentError, "invalid url" unless uri.host
394+
raise ArgumentError, "invalid url: #{uri}" unless uri.host
387395

388396
defaults[:scheme] = uri.scheme
389397
defaults[:host] = uri.host
@@ -462,7 +470,7 @@ def _parse_driver(driver)
462470

463471
class Connector
464472
def initialize(options)
465-
@options = options
473+
@options = options.dup
466474
end
467475

468476
def resolve
@@ -476,9 +484,9 @@ class Sentinel < Connector
476484
def initialize(options)
477485
super(options)
478486

479-
@sentinels = options.fetch(:sentinels).dup
480-
@role = options[:role].to_s
481-
@master = options[:host]
487+
@sentinels = @options.delete(:sentinels).dup
488+
@role = @options.fetch(:role, "master").to_s
489+
@master = @options[:host]
482490
end
483491

484492
def check(client)
@@ -493,7 +501,7 @@ def check(client)
493501
end
494502

495503
if role != @role
496-
disconnect
504+
client.disconnect
497505
raise ConnectionError, "Instance role mismatch. Expected #{@role}, got #{role}."
498506
end
499507
end
@@ -514,7 +522,10 @@ def resolve
514522
def sentinel_detect
515523
3.times do
516524
@sentinels.each do |sentinel|
517-
client = Client.new(:host => sentinel[:host], :port => sentinel[:port], :timeout => 0.3)
525+
client = Client.new(@options.merge({
526+
:host => sentinel[:host],
527+
:port => sentinel[:port]
528+
}))
518529

519530
begin
520531
if result = yield(client)

lib/redis/connection/hiredis.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ class Hiredis
99

1010
def self.connect(config)
1111
connection = ::Hiredis::Connection.new
12+
connect_timeout = (config.fetch(:connect_timeout, 0) * 1_000_000).to_i
1213

1314
if config[:scheme] == "unix"
14-
connection.connect_unix(config[:path], Integer(config[:connect_timeout] * 1_000_000))
15+
connection.connect_unix(config[:path], connect_timeout)
1516
else
16-
connection.connect(config[:host], config[:port], Integer(config[:connect_timeout] * 1_000_000))
17+
connection.connect(config[:host], config[:port], connect_timeout)
1718
end
1819

1920
instance = new(connection)

test/commands_on_sorted_sets_test.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,20 @@ def test_zrangebylex
2222
end
2323
end
2424

25+
def test_zrevrangebylex
26+
target_version "2.9.9" do
27+
r.zadd "foo", 0, "aaren"
28+
r.zadd "foo", 0, "abagael"
29+
r.zadd "foo", 0, "abby"
30+
r.zadd "foo", 0, "abbygail"
31+
32+
assert_equal ["abbygail", "abby", "abagael", "aaren"], r.zrevrangebylex("foo", "[a\xff", "[a")
33+
assert_equal ["abbygail", "abby"], r.zrevrangebylex("foo", "[a\xff", "[a", :limit => [0, 2])
34+
assert_equal ["abbygail", "abby"], r.zrevrangebylex("foo", "(abb\xff", "(abb")
35+
assert_equal ["abbygail"], r.zrevrangebylex("foo", "(abby\xff", "(abby")
36+
end
37+
end
38+
2539
def test_zcount
2640
r.zadd "foo", 1, "s1"
2741
r.zadd "foo", 2, "s2"

test/connection_handling_test.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,4 +186,25 @@ def test_config_set
186186
r.config :set, "timeout", 300
187187
end
188188
end
189+
190+
driver(:ruby, :hiredis) do
191+
def test_consistency_on_multithreaded_env
192+
t = nil
193+
194+
commands = {
195+
:set => lambda { |key, value| t.kill; "+OK\r\n" },
196+
:incr => lambda { |key| ":1\r\n" },
197+
}
198+
199+
redis_mock(commands) do |redis|
200+
t = Thread.new do
201+
redis.set("foo", "bar")
202+
end
203+
204+
t.join
205+
206+
assert_equal 1, redis.incr("baz")
207+
end
208+
end
209+
end
189210
end

0 commit comments

Comments
 (0)