Skip to content

Commit 9fd3ec4

Browse files
committed
Merge pull request #1 from StackExchange/master
Merge new stuff
2 parents 92765c7 + 1b3e004 commit 9fd3ec4

File tree

10 files changed

+196
-52
lines changed

10 files changed

+196
-52
lines changed

Docs/KeysScan.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ or
99

1010
> There doesn't seem to be a `Flush(...)` method? How can I remove all the keys in the database?
1111
12-
The key word here, oddly enough, is the last one: database. Because StackExchange.Redis aims to target scenarios such as cluster, it is important to know which commands target the *database* (the logical database that could be distributed over multiple nodes), and which commands target the *server*. The folliowing commands all target a single server:
12+
The key word here, oddly enough, is the last one: database. Because StackExchange.Redis aims to target scenarios such as cluster, it is important to know which commands target the *database* (the logical database that could be distributed over multiple nodes), and which commands target the *server*. The following commands all target a single server:
1313

1414
- `KEYS` / `SCAN`
1515
- `FLUSHDB` / `FLUSHALL`

StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
<Compile Include="TestInfoReplicationChecks.cs" />
108108
<Compile Include="Transactions.cs" />
109109
<Compile Include="VPNTest.cs" />
110+
<Compile Include="WithKeyPrefixTests.cs" />
110111
</ItemGroup>
111112
<ItemGroup>
112113
<None Include="packages.config" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
using NUnit.Framework;
2+
using StackExchange.Redis.StackExchange.Redis.KeyspaceIsolation;
3+
using System;
4+
5+
namespace StackExchange.Redis.Tests
6+
{
7+
8+
[TestFixture]
9+
public class WithKeyPrefixTests : TestBase
10+
{
11+
[Test]
12+
public void BlankPrefixYieldsSame_Bytes()
13+
{
14+
using (var conn = Create())
15+
{
16+
var raw = conn.GetDatabase(1);
17+
var prefixed = raw.WithKeyPrefix(new byte[0]);
18+
Assert.AreSame(raw, prefixed);
19+
}
20+
}
21+
[Test]
22+
public void BlankPrefixYieldsSame_String()
23+
{
24+
using (var conn = Create())
25+
{
26+
var raw = conn.GetDatabase(1);
27+
var prefixed = raw.WithKeyPrefix("");
28+
Assert.AreSame(raw, prefixed);
29+
}
30+
}
31+
[Test, ExpectedException(typeof(ArgumentNullException))]
32+
public void NullPrefixIsError_Bytes()
33+
{
34+
using (var conn = Create())
35+
{
36+
var raw = conn.GetDatabase(1);
37+
var prefixed = raw.WithKeyPrefix((byte[])null);
38+
}
39+
}
40+
[Test, ExpectedException(typeof(ArgumentNullException))]
41+
public void NullPrefixIsError_String()
42+
{
43+
using (var conn = Create())
44+
{
45+
var raw = conn.GetDatabase(1);
46+
var prefixed = raw.WithKeyPrefix((string)null);
47+
}
48+
}
49+
50+
[Test, ExpectedException(typeof(ArgumentNullException))]
51+
[TestCase("abc")]
52+
[TestCase("")]
53+
[TestCase(null)]
54+
public void NullDatabaseIsError(string prefix)
55+
{
56+
IDatabase raw = null;
57+
var prefixed = raw.WithKeyPrefix(prefix);
58+
}
59+
[Test]
60+
public void BasicSmokeTest()
61+
{
62+
using(var conn = Create())
63+
{
64+
var raw = conn.GetDatabase(1);
65+
66+
var foo = raw.WithKeyPrefix("foo");
67+
var foobar = foo.WithKeyPrefix("bar");
68+
69+
string key = Me();
70+
71+
string s = Guid.NewGuid().ToString(), t = Guid.NewGuid().ToString();
72+
73+
foo.StringSet(key, s);
74+
var val = (string)foo.StringGet(key);
75+
Assert.AreEqual(s, val); // fooBasicSmokeTest
76+
77+
foobar.StringSet(key, t);
78+
val = (string)foobar.StringGet(key);
79+
Assert.AreEqual(t, val); // foobarBasicSmokeTest
80+
81+
val = (string)foo.StringGet("bar" + key);
82+
Assert.AreEqual(t, val); // foobarBasicSmokeTest
83+
84+
val = (string)raw.StringGet("foo" + key);
85+
Assert.AreEqual(s, val); // fooBasicSmokeTest
86+
87+
val = (string)raw.StringGet("foobar" + key);
88+
Assert.AreEqual(t, val); // foobarBasicSmokeTest
89+
}
90+
}
91+
[Test]
92+
public void ConditionTest()
93+
{
94+
using(var conn = Create())
95+
{
96+
var raw = conn.GetDatabase(2);
97+
98+
var foo = raw.WithKeyPrefix("tran:");
99+
100+
raw.KeyDelete("tran:abc");
101+
raw.KeyDelete("tran:i");
102+
103+
// execute while key exists
104+
raw.StringSet("tran:abc", "def");
105+
var tran = foo.CreateTransaction();
106+
tran.AddCondition(Condition.KeyExists("abc"));
107+
tran.StringIncrementAsync("i");
108+
tran.Execute();
109+
110+
int i = (int)raw.StringGet("tran:i");
111+
Assert.AreEqual(1, i);
112+
113+
// repeat without key
114+
raw.KeyDelete("tran:abc");
115+
tran = foo.CreateTransaction();
116+
tran.AddCondition(Condition.KeyExists("abc"));
117+
tran.StringIncrementAsync("i");
118+
tran.Execute();
119+
120+
i = (int)raw.StringGet("tran:i");
121+
Assert.AreEqual(1, i);
122+
}
123+
}
124+
}
125+
}

StackExchange.Redis/StackExchange/Redis/Condition.cs

+17-9
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ namespace StackExchange.Redis
99
/// </summary>
1010
public abstract class Condition
1111
{
12+
internal abstract Condition MapKeys(Func<RedisKey,RedisKey> map);
1213

1314
private Condition() { }
1415

@@ -144,12 +145,14 @@ internal override void WriteImpl(PhysicalConnection physical)
144145

145146
internal class ExistsCondition : Condition
146147
{
147-
internal readonly bool expectedResult;
148+
private readonly bool expectedResult;
149+
private readonly RedisValue hashField;
150+
private readonly RedisKey key;
148151

149-
internal readonly RedisValue hashField;
150-
151-
internal readonly RedisKey key;
152-
152+
internal override Condition MapKeys(Func<RedisKey,RedisKey> map)
153+
{
154+
return new ExistsCondition(map(key), hashField, expectedResult);
155+
}
153156
public ExistsCondition(RedisKey key, RedisValue hashField, bool expectedResult)
154157
{
155158
if (key.IsNull) throw new ArgumentException("key");
@@ -198,10 +201,15 @@ internal override bool TryValidate(RawResult result, out bool value)
198201
}
199202

200203
internal class EqualsCondition : Condition
201-
{
202-
internal readonly bool expectedEqual;
203-
internal readonly RedisValue hashField, expectedValue;
204-
internal readonly RedisKey key;
204+
{
205+
206+
internal override Condition MapKeys(Func<RedisKey,RedisKey> map)
207+
{
208+
return new EqualsCondition(map(key), hashField, expectedEqual, expectedValue);
209+
}
210+
private readonly bool expectedEqual;
211+
private readonly RedisValue hashField, expectedValue;
212+
private readonly RedisKey key;
205213
public EqualsCondition(RedisKey key, RedisValue hashField, bool expectedEqual, RedisValue expectedValue)
206214
{
207215
if (key.IsNull) throw new ArgumentException("key");

StackExchange.Redis/StackExchange/Redis/IDatabase.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -808,7 +808,7 @@ SortedSetEntry[] SortedSetRangeByScoreWithScores(RedisKey key,
808808

809809
/// <summary>
810810
/// Return the position of the first bit set to 1 or 0 in a string.
811-
/// The position is returned thinking at the string as an array of bits from left to right where the first byte most significant bit is at position 0, the second byte most significant big is at position 8 and so forth.
811+
/// The position is returned thinking at the string as an array of bits from left to right where the first byte most significant bit is at position 0, the second byte most significant bit is at position 8 and so forth.
812812
/// An start and end may be specified; these are in bytes, not bits; start and end can contain negative values in order to index bytes starting from the end of the string, where -1 is the last byte, -2 is the penultimate, and so forth.
813813
/// </summary>
814814
/// <returns>The command returns the position of the first bit set to 1 or 0 according to the request.
@@ -922,4 +922,4 @@ SortedSetEntry[] SortedSetRangeByScoreWithScores(RedisKey key,
922922
/// <remarks>http://redis.io/commands/setrange</remarks>
923923
RedisValue StringSetRange(RedisKey key, long offset, RedisValue value, CommandFlags flags = CommandFlags.None);
924924
}
925-
}
925+
}

StackExchange.Redis/StackExchange/Redis/KeyspaceIsolation/DatabaseExtension.cs

+16-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace StackExchange.Redis.StackExchange.Redis.KeyspaceIsolation
55
/// <summary>
66
/// Provides the <see cref="WithKeyPrefix"/> extension method to <see cref="IDatabase"/>.
77
/// </summary>
8-
public static class DatabaseExtension
8+
public static class DatabaseExtensions
99
{
1010
/// <summary>
1111
/// Creates a new <see cref="IDatabase"/> instance that provides an isolated key space
@@ -44,9 +44,22 @@ public static IDatabase WithKeyPrefix(this IDatabase database, RedisKey keyPrefi
4444
throw new ArgumentNullException("database");
4545
}
4646

47-
if (keyPrefix.IsNull || keyPrefix.Value.Length == 0)
47+
if (keyPrefix.IsNull)
4848
{
49-
throw new ArgumentException("The specified prefix cannot be null or empty", "keyPrefix");
49+
throw new ArgumentNullException("keyPrefix");
50+
}
51+
52+
if (keyPrefix.Value.Length == 0)
53+
{
54+
return database; // fine - you can keep using the original, then
55+
}
56+
57+
if(database is DatabaseWrapper)
58+
{
59+
// combine the key in advance to minimize indirection
60+
var wrapper = (DatabaseWrapper)database;
61+
keyPrefix = wrapper.ToInner(keyPrefix);
62+
database = wrapper.Inner;
5063
}
5164

5265
return new DatabaseWrapper(database, keyPrefix);

StackExchange.Redis/StackExchange/Redis/KeyspaceIsolation/DatabaseWrapper.cs

+3
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,8 @@ public TimeSpan Ping(CommandFlags flags = CommandFlags.None)
623623
return this.Inner.SortedSetScan(this.ToInner(key), pattern, pageSize, flags);
624624
}
625625

626+
627+
#if DEBUG
626628
public string ClientGetName(CommandFlags flags = CommandFlags.None)
627629
{
628630
return this.Inner.ClientGetName(flags);
@@ -632,5 +634,6 @@ public void Quit(CommandFlags flags = CommandFlags.None)
632634
{
633635
this.Inner.Quit(flags);
634636
}
637+
#endif
635638
}
636639
}

StackExchange.Redis/StackExchange/Redis/KeyspaceIsolation/TransactionWrapper.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public TransactionWrapper(ITransaction inner, RedisKey prefix)
1212

1313
public ConditionResult AddCondition(Condition condition)
1414
{
15-
return this.Inner.AddCondition(this.ToInner(condition));
15+
return this.Inner.AddCondition(condition == null ? null : condition.MapKeys(GetMapFunction()));
1616
}
1717

1818
public bool Execute(CommandFlags flags = CommandFlags.None)

StackExchange.Redis/StackExchange/Redis/KeyspaceIsolation/WrapperBase.cs

+6-36
Original file line numberDiff line numberDiff line change
@@ -644,7 +644,7 @@ public Task<string> ClientGetNameAsync(CommandFlags flags = CommandFlags.None)
644644
return this.Inner.ClientGetNameAsync(flags);
645645
}
646646

647-
protected RedisKey ToInner(RedisKey outer)
647+
protected internal RedisKey ToInner(RedisKey outer)
648648
{
649649
return this.Prefix + outer;
650650
}
@@ -754,44 +754,14 @@ protected RedisValue[] SortGetToInner(RedisValue[] outer)
754754

755755
protected RedisChannel ToInner(RedisChannel outer)
756756
{
757-
return this.Prefix + outer;
758-
}
759-
760-
protected Condition ToInner(Condition outer)
761-
{
762-
Condition.ExistsCondition asExists = outer as Condition.ExistsCondition;
763-
764-
if (asExists != null)
765-
{
766-
return this.ToInner(asExists);
767-
}
768-
769-
Condition.EqualsCondition asEquals = outer as Condition.EqualsCondition;
770-
771-
if (asEquals != null)
772-
{
773-
return this.ToInner(asEquals);
774-
}
775-
776-
throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture,
777-
"Unsupported condition: {0}", outer));
778-
}
779-
780-
private Condition.ExistsCondition ToInner(Condition.ExistsCondition outer)
781-
{
782-
return new Condition.ExistsCondition(
783-
this.ToInner(outer.key),
784-
outer.hashField,
785-
outer.expectedResult);
757+
return RedisKey.Concatenate((byte[])Prefix, (byte[])outer);
786758
}
787759

788-
private Condition.EqualsCondition ToInner(Condition.EqualsCondition outer)
760+
private Func<RedisKey, RedisKey> mapFunction;
761+
protected Func<RedisKey, RedisKey> GetMapFunction()
789762
{
790-
return new Condition.EqualsCondition(
791-
this.ToInner(outer.key),
792-
outer.hashField,
793-
outer.expectedEqual,
794-
outer.expectedValue);
763+
// create as a delegate when first required, then re-use
764+
return mapFunction ?? (mapFunction = new Func<RedisKey, RedisKey>(this.ToInner));
795765
}
796766
}
797767
}

StackExchange.Redis/StackExchange/Redis/RedisKey.cs

+24
Original file line numberDiff line numberDiff line change
@@ -195,5 +195,29 @@ public static implicit operator string(RedisKey key)
195195
return BitConverter.ToString(arr);
196196
}
197197
}
198+
199+
/// <summary>
200+
/// Concatenate two keys
201+
/// </summary>
202+
public static RedisKey operator +(RedisKey x, RedisKey y)
203+
{
204+
return Concatenate(x.value, y.value);
205+
}
206+
207+
internal static byte[] Concatenate(byte[] x, byte[] y)
208+
{
209+
// either null? yield the other; note this includes the "both null becomes null" case
210+
if (x == null) return y;
211+
if (y == null) return x;
212+
213+
// either empty? yield the other
214+
if (x.Length == 0) return y;
215+
if (y.Length == 0) return x;
216+
217+
byte[] result = new byte[x.Length + y.Length];
218+
Buffer.BlockCopy(x, 0, result, 0, x.Length);
219+
Buffer.BlockCopy(y, 0, result, x.Length, y.Length);
220+
return result;
221+
}
198222
}
199223
}

0 commit comments

Comments
 (0)