Skip to content

Commit 1013cd8

Browse files
committed
Merge remote-tracking branch 'origin/master' into 3.3
2 parents 9d6e606 + 5af8c80 commit 1013cd8

File tree

5 files changed

+183
-46
lines changed

5 files changed

+183
-46
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
security updates in June of 2013; continuing to support it would prevent
1313
the use of newer features of Ruby.
1414

15+
# (unreleased)
16+
17+
* Added support for `ZADD` options `NX`, `XX`, `CH`, `INCR`. See #547.
18+
1519
# 3.2.1
1620

1721
* Added support for `PUBSUB` command.

lib/redis.rb

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1449,20 +1449,50 @@ def zcard(key)
14491449
# @param [[Float, String], Array<[Float, String]>] args
14501450
# - a single `[score, member]` pair
14511451
# - an array of `[score, member]` pairs
1452-
#
1453-
# @return [Boolean, Fixnum]
1452+
# @param [Hash] options
1453+
# - `:xx => true`: Only update elements that already exist (never
1454+
# add elements)
1455+
# - `:nx => true`: Don't update already existing elements (always
1456+
# add new elements)
1457+
# - `:ch => true`: Modify the return value from the number of new
1458+
# elements added, to the total number of elements changed (CH is an
1459+
# abbreviation of changed); changed elements are new elements added
1460+
# and elements already existing for which the score was updated
1461+
# - `:incr => true`: When this option is specified ZADD acts like
1462+
# ZINCRBY; only one score-element pair can be specified in this mode
1463+
#
1464+
# @return [Boolean, Fixnum, Float]
14541465
# - `Boolean` when a single pair is specified, holding whether or not it was
1455-
# **added** to the sorted set
1466+
# **added** to the sorted set.
14561467
# - `Fixnum` when an array of pairs is specified, holding the number of
1457-
# pairs that were **added** to the sorted set
1458-
def zadd(key, *args)
1468+
# pairs that were **added** to the sorted set.
1469+
# - `Float` when option :incr is specified, holding the score of the member
1470+
# after incrementing it.
1471+
def zadd(key, *args) #, options
1472+
zadd_options = []
1473+
if args.last.is_a?(Hash)
1474+
options = args.pop
1475+
1476+
nx = options[:nx]
1477+
zadd_options << "NX" if nx
1478+
1479+
xx = options[:xx]
1480+
zadd_options << "XX" if xx
1481+
1482+
ch = options[:ch]
1483+
zadd_options << "CH" if ch
1484+
1485+
incr = options[:incr]
1486+
zadd_options << "INCR" if incr
1487+
end
1488+
14591489
synchronize do |client|
14601490
if args.size == 1 && args[0].is_a?(Array)
1461-
# Variadic: return integer
1462-
client.call([:zadd, key] + args[0])
1491+
# Variadic: return float if INCR, integer if !INCR
1492+
client.call([:zadd, key] + zadd_options + args[0], &(incr ? _floatify : _identity))
14631493
elsif args.size == 2
1464-
# Single pair: return boolean
1465-
client.call([:zadd, key, args[0], args[1]], &_boolify)
1494+
# Single pair: return float if INCR, boolean if !INCR
1495+
client.call([:zadd, key] + zadd_options + args, &(incr ? _floatify : _boolify))
14661496
else
14671497
raise ArgumentError, "wrong number of arguments"
14681498
end
@@ -2653,6 +2683,12 @@ def _pairify(array)
26532683
array.each_slice(2).to_a
26542684
end
26552685

2686+
def _identity
2687+
lambda { |value|
2688+
value
2689+
}
2690+
end
2691+
26562692
def _subscription(method, channels, block)
26572693
return @client.call([method] + channels) if subscribed?
26582694

test/lint/sorted_sets.rb

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,43 @@ def test_zadd
99
assert_equal true, r.zadd("foo", 1, "s1")
1010
assert_equal false, r.zadd("foo", 1, "s1")
1111
assert_equal 1, r.zcard("foo")
12+
r.del "foo"
13+
14+
target_version "3.0.2" do
15+
# XX option
16+
assert_equal 0, r.zcard("foo")
17+
assert_equal false, r.zadd("foo", 1, "s1", :xx => true)
18+
r.zadd("foo", 1, "s1")
19+
assert_equal false, r.zadd("foo", 2, "s1", :xx => true)
20+
assert_equal 2, r.zscore("foo", "s1")
21+
r.del "foo"
22+
23+
# NX option
24+
assert_equal 0, r.zcard("foo")
25+
assert_equal true, r.zadd("foo", 1, "s1", :nx => true)
26+
assert_equal false, r.zadd("foo", 2, "s1", :nx => true)
27+
assert_equal 1, r.zscore("foo", "s1")
28+
assert_equal 1, r.zcard("foo")
29+
r.del "foo"
30+
31+
# CH option
32+
assert_equal 0, r.zcard("foo")
33+
assert_equal true, r.zadd("foo", 1, "s1", :ch => true)
34+
assert_equal false, r.zadd("foo", 1, "s1", :ch => true)
35+
assert_equal true, r.zadd("foo", 2, "s1", :ch => true)
36+
assert_equal 1, r.zcard("foo")
37+
r.del "foo"
38+
39+
# INCR option
40+
assert_equal 1.0, r.zadd("foo", 1, "s1", :incr => true)
41+
assert_equal 11.0, r.zadd("foo", 10, "s1", :incr => true)
42+
assert_equal -Infinity, r.zadd("bar", "-inf", "s1", :incr => true)
43+
assert_equal +Infinity, r.zadd("bar", "+inf", "s2", :incr => true)
44+
r.del "foo", "bar"
45+
46+
# Incompatible options combination
47+
assert_raise(Redis::CommandError) { r.zadd("foo", 1, "s1", :xx => true, :nx => true) }
48+
end
1249
end
1350

1451
def test_variadic_zadd
@@ -31,6 +68,47 @@ def test_variadic_zadd
3168
assert_raise(Redis::CommandError) { r.zadd("foo", ["bar"]) }
3269
assert_raise(Redis::CommandError) { r.zadd("foo", ["bar", "qux", "zap"]) }
3370
end
71+
72+
target_version "3.0.2" do
73+
# XX option
74+
assert_equal 0, r.zcard("foo")
75+
assert_equal 0, r.zadd("foo", [1, "s1", 2, "s2"], :xx => true)
76+
r.zadd("foo", [1, "s1", 2, "s2"])
77+
assert_equal 0, r.zadd("foo", [2, "s1", 3, "s2", 4, "s3"], :xx => true)
78+
assert_equal 2, r.zscore("foo", "s1")
79+
assert_equal 3, r.zscore("foo", "s2")
80+
assert_equal nil, r.zscore("foo", "s3")
81+
assert_equal 2, r.zcard("foo")
82+
r.del "foo"
83+
84+
# NX option
85+
assert_equal 0, r.zcard("foo")
86+
assert_equal 2, r.zadd("foo", [1, "s1", 2, "s2"], :nx => true)
87+
assert_equal 1, r.zadd("foo", [2, "s1", 3, "s2", 4, "s3"], :nx => true)
88+
assert_equal 1, r.zscore("foo", "s1")
89+
assert_equal 2, r.zscore("foo", "s2")
90+
assert_equal 4, r.zscore("foo", "s3")
91+
assert_equal 3, r.zcard("foo")
92+
r.del "foo"
93+
94+
# CH option
95+
assert_equal 0, r.zcard("foo")
96+
assert_equal 2, r.zadd("foo", [1, "s1", 2, "s2"], :ch => true)
97+
assert_equal 2, r.zadd("foo", [1, "s1", 3, "s2", 4, "s3"], :ch => true)
98+
assert_equal 3, r.zcard("foo")
99+
r.del "foo"
100+
101+
# INCR option
102+
assert_equal 1.0, r.zadd("foo", [1, "s1"], :incr => true)
103+
assert_equal 11.0, r.zadd("foo", [10, "s1"], :incr => true)
104+
assert_equal -Infinity, r.zadd("bar", ["-inf", "s1"], :incr => true)
105+
assert_equal +Infinity, r.zadd("bar", ["+inf", "s2"], :incr => true)
106+
assert_raise(Redis::CommandError) { r.zadd("foo", [1, "s1", 2, "s2"], :incr => true) }
107+
r.del "foo", "bar"
108+
109+
# Incompatible options combination
110+
assert_raise(Redis::CommandError) { r.zadd("foo", [1, "s1"], :xx => true, :nx => true) }
111+
end
34112
end
35113

36114
def test_zrem

test/sentinel_test.rb

Lines changed: 51 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ def test_sentinel_connection
2424
}
2525
end
2626

27-
RedisMock.start(handler.call(:s1), {}, 26381) do
28-
RedisMock.start(handler.call(:s2), {}, 26382) do
27+
RedisMock.start(handler.call(:s1), {}, 0) do |s1_port|
28+
RedisMock.start(handler.call(:s2), {}, 0) do |s2_port|
29+
sentinels[0][:port] = s1_port
30+
sentinels[1][:port] = s2_port
2931
redis = Redis.new(:url => "redis://master1", :sentinels => sentinels, :role => :master)
3032

3133
assert redis.ping
@@ -59,8 +61,10 @@ def test_sentinel_failover
5961
end
6062
}
6163

62-
RedisMock.start(s1, {}, 26381) do
63-
RedisMock.start(s2, {}, 26382) do
64+
RedisMock.start(s1, {}, 0) do |s1_port|
65+
RedisMock.start(s2, {}, 0) do |s2_port|
66+
sentinels[0][:port] = s1_port
67+
sentinels[1][:port] = s2_port
6468
redis = Redis.new(:url => "redis://master1", :sentinels => sentinels, :role => :master)
6569

6670
assert redis.ping
@@ -94,8 +98,10 @@ def test_sentinel_failover_prioritize_healthy_sentinel
9498
end
9599
}
96100

97-
RedisMock.start(s1, {}, 26381) do
98-
RedisMock.start(s2, {}, 26382) do
101+
RedisMock.start(s1, {}, 0) do |s1_port|
102+
RedisMock.start(s2, {}, 0) do |s2_port|
103+
sentinels[0][:port] = s1_port
104+
sentinels[1][:port] = s2_port
99105
redis = Redis.new(:url => "redis://master1", :sentinels => sentinels, :role => :master)
100106

101107
assert redis.ping
@@ -118,20 +124,22 @@ def test_sentinel_with_non_sentinel_options
118124
:m1 => []
119125
}
120126

121-
sentinel = {
122-
:auth => lambda do |pass|
123-
commands[:s1] << ["auth", pass]
124-
"-ERR unknown command 'auth'"
125-
end,
126-
:select => lambda do |db|
127-
commands[:s1] << ["select", db]
128-
"-ERR unknown command 'select'"
129-
end,
130-
:sentinel => lambda do |command, *args|
131-
commands[:s1] << [command, *args]
132-
["127.0.0.1", "6382"]
133-
end
134-
}
127+
sentinel = lambda do |port|
128+
{
129+
:auth => lambda do |pass|
130+
commands[:s1] << ["auth", pass]
131+
"-ERR unknown command 'auth'"
132+
end,
133+
:select => lambda do |db|
134+
commands[:s1] << ["select", db]
135+
"-ERR unknown command 'select'"
136+
end,
137+
:sentinel => lambda do |command, *args|
138+
commands[:s1] << [command, *args]
139+
["127.0.0.1", port.to_s]
140+
end
141+
}
142+
end
135143

136144
master = {
137145
:auth => lambda do |pass|
@@ -144,8 +152,9 @@ def test_sentinel_with_non_sentinel_options
144152
end
145153
}
146154

147-
RedisMock.start(master, {}, 6382) do
148-
RedisMock.start(sentinel, {}, 26381) do
155+
RedisMock.start(master, {}, 0) do |master_port|
156+
RedisMock.start(sentinel.call(master_port), {}, 0) do |sen_port|
157+
sentinels[0][:port] = sen_port
149158
redis = Redis.new(:url => "redis://:foo@master1/15", :sentinels => sentinels, :role => :master)
150159

151160
assert redis.ping
@@ -159,11 +168,13 @@ def test_sentinel_with_non_sentinel_options
159168
def test_sentinel_role_mismatch
160169
sentinels = [{:host => "127.0.0.1", :port => 26381}]
161170

162-
sentinel = {
163-
:sentinel => lambda do |command, *args|
164-
["127.0.0.1", "6382"]
165-
end
166-
}
171+
sentinel = lambda do |port|
172+
{
173+
:sentinel => lambda do |command, *args|
174+
["127.0.0.1", port.to_s]
175+
end
176+
}
177+
end
167178

168179
master = {
169180
:role => lambda do
@@ -172,8 +183,9 @@ def test_sentinel_role_mismatch
172183
}
173184

174185
ex = assert_raise(Redis::ConnectionError) do
175-
RedisMock.start(master, {}, 6382) do
176-
RedisMock.start(sentinel, {}, 26381) do
186+
RedisMock.start(master, {}, 0) do |master_port|
187+
RedisMock.start(sentinel.call(master_port), {}, 0) do |sen_port|
188+
sentinels[0][:port] = sen_port
177189
redis = Redis.new(:url => "redis://master1", :sentinels => sentinels, :role => :master)
178190

179191
assert redis.ping
@@ -190,15 +202,15 @@ def test_sentinel_retries
190202

191203
connections = []
192204

193-
handler = lambda do |id|
205+
handler = lambda do |id, port|
194206
{
195207
:sentinel => lambda do |command, *args|
196208
connections << id
197209

198210
if connections.count(id) < 2
199211
:close
200212
else
201-
["127.0.0.1", "6382"]
213+
["127.0.0.1", port.to_s]
202214
end
203215
end
204216
}
@@ -210,9 +222,11 @@ def test_sentinel_retries
210222
end
211223
}
212224

213-
RedisMock.start(master, {}, 6382) do
214-
RedisMock.start(handler.call(:s1), {}, 26381) do
215-
RedisMock.start(handler.call(:s2), {}, 26382) do
225+
RedisMock.start(master, {}, 0) do |master_port|
226+
RedisMock.start(handler.call(:s1, master_port), {}, 0) do |s1_port|
227+
RedisMock.start(handler.call(:s2, master_port), {}, 0) do |s2_port|
228+
sentinels[0][:port] = s1_port
229+
sentinels[1][:port] = s2_port
216230
redis = Redis.new(:url => "redis://master1", :sentinels => sentinels, :role => :master, :reconnect_attempts => 1)
217231

218232
assert redis.ping
@@ -225,9 +239,9 @@ def test_sentinel_retries
225239
connections.clear
226240

227241
ex = assert_raise(Redis::CannotConnectError) do
228-
RedisMock.start(master, {}, 6382) do
229-
RedisMock.start(handler.call(:s1), {}, 26381) do
230-
RedisMock.start(handler.call(:s2), {}, 26382) do
242+
RedisMock.start(master, {}, 0) do |master_port|
243+
RedisMock.start(handler.call(:s1, master_port), {}, 0) do |s1_port|
244+
RedisMock.start(handler.call(:s2, master_port), {}, 0) do |s2_port|
231245
redis = Redis.new(:url => "redis://master1", :sentinels => sentinels, :role => :master, :reconnect_attempts => 0)
232246

233247
assert redis.ping

test/support/redis_mock.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ def initialize(port, options = {}, &block)
99
@server.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true)
1010
end
1111

12+
def port
13+
@server.addr[1]
14+
end
15+
1216
def start(&block)
1317
@thread = Thread.new { run(&block) }
1418
end
@@ -61,6 +65,7 @@ def run
6165
#
6266
def self.start_with_handler(blk, options = {}, port = MOCK_PORT)
6367
server = Server.new(port, options)
68+
port = server.port
6469

6570
begin
6671
server.start(&blk)

0 commit comments

Comments
 (0)