Commit b17d62d9 authored by Andrew's avatar Andrew

Add Transaction Conditions for Length comparisons (across most types)

parent e116defc
using System.Threading.Tasks; using System;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
namespace StackExchange.Redis.Tests namespace StackExchange.Redis.Tests
...@@ -297,6 +299,382 @@ public void BasicTranWithListEqualsCondition(string expected, string value, bool ...@@ -297,6 +299,382 @@ public void BasicTranWithListEqualsCondition(string expected, string value, bool
} }
} }
public enum ComparisonType
{
Equal,
LessThan,
GreaterThan
}
[Test]
[TestCase("five", ComparisonType.Equal, 5L, false)]
[TestCase("four", ComparisonType.Equal, 4L, true)]
[TestCase("three", ComparisonType.Equal, 3L, false)]
[TestCase("", ComparisonType.Equal, 2L, false)]
[TestCase("", ComparisonType.Equal, 0L, true)]
[TestCase(null, ComparisonType.Equal, 1L, false)]
[TestCase(null, ComparisonType.Equal, 0L, true)]
[TestCase("five", ComparisonType.LessThan, 5L, true)]
[TestCase("four", ComparisonType.LessThan, 4L, false)]
[TestCase("three", ComparisonType.LessThan, 3L, false)]
[TestCase("", ComparisonType.LessThan, 2L, true)]
[TestCase("", ComparisonType.LessThan, 0L, false)]
[TestCase(null, ComparisonType.LessThan, 1L, true)]
[TestCase(null, ComparisonType.LessThan, 0L, false)]
[TestCase("five", ComparisonType.GreaterThan, 5L, false)]
[TestCase("four", ComparisonType.GreaterThan, 4L, false)]
[TestCase("three", ComparisonType.GreaterThan, 3L, true)]
[TestCase("", ComparisonType.GreaterThan, 2L, false)]
[TestCase("", ComparisonType.GreaterThan, 0L, false)]
[TestCase(null, ComparisonType.GreaterThan, 1L, false)]
[TestCase(null, ComparisonType.GreaterThan, 0L, false)]
public void BasicTranWithStringLengthCondition(string value, ComparisonType type, long length, bool expectTran)
{
using (var muxer = Create())
{
RedisKey key = Me(), key2 = Me() + "2";
var db = muxer.GetDatabase();
db.KeyDelete(key, CommandFlags.FireAndForget);
db.KeyDelete(key2, CommandFlags.FireAndForget);
var expectSuccess = false;
Condition condition = null;
var valueLength = value?.Length ?? 0;
switch (type) {
case ComparisonType.Equal:
expectSuccess = valueLength == length;
condition = Condition.StringLengthEqual(key2, length);
break;
case ComparisonType.GreaterThan:
expectSuccess = valueLength > length;
condition = Condition.StringLengthGreaterThan(key2, length);
break;
case ComparisonType.LessThan:
expectSuccess = valueLength < length;
condition = Condition.StringLengthLessThan(key2, length);
break;
}
if (value != null) db.StringSet(key2, value, flags: CommandFlags.FireAndForget);
Assert.IsFalse(db.KeyExists(key));
Assert.AreEqual(value, (string)db.StringGet(key2));
var tran = db.CreateTransaction();
var cond = tran.AddCondition(condition);
var push = tran.StringSetAsync(key, "any value");
var exec = tran.ExecuteAsync();
var get = db.StringLength(key);
Assert.AreEqual(expectTran, db.Wait(exec), "expected tran result");
if (expectSuccess) {
Assert.IsTrue(db.Wait(exec), "eq: exec");
Assert.IsTrue(cond.WasSatisfied, "eq: was satisfied");
Assert.AreEqual(true, db.Wait(push), "eq: push");
Assert.AreEqual("any value".Length, get, "eq: get");
} else {
Assert.IsFalse(db.Wait(exec), "neq: exec");
Assert.False(cond.WasSatisfied, "neq: was satisfied");
Assert.AreEqual(TaskStatus.Canceled, push.Status, "neq: push");
Assert.AreEqual(0, get, "neq: get");
}
}
}
[Test]
[TestCase("five", ComparisonType.Equal, 5L, false)]
[TestCase("four", ComparisonType.Equal, 4L, true)]
[TestCase("three", ComparisonType.Equal, 3L, false)]
[TestCase("", ComparisonType.Equal, 2L, false)]
[TestCase("", ComparisonType.Equal, 0L, true)]
[TestCase("five", ComparisonType.LessThan, 5L, true)]
[TestCase("four", ComparisonType.LessThan, 4L, false)]
[TestCase("three", ComparisonType.LessThan, 3L, false)]
[TestCase("", ComparisonType.LessThan, 2L, true)]
[TestCase("", ComparisonType.LessThan, 0L, false)]
[TestCase("five", ComparisonType.GreaterThan, 5L, false)]
[TestCase("four", ComparisonType.GreaterThan, 4L, false)]
[TestCase("three", ComparisonType.GreaterThan, 3L, true)]
[TestCase("", ComparisonType.GreaterThan, 2L, false)]
[TestCase("", ComparisonType.GreaterThan, 0L, false)]
public void BasicTranWithHashLengthCondition(string value, ComparisonType type, long length, bool expectTran)
{
using (var muxer = Create())
{
RedisKey key = Me(), key2 = Me() + "2";
var db = muxer.GetDatabase();
db.KeyDelete(key, CommandFlags.FireAndForget);
db.KeyDelete(key2, CommandFlags.FireAndForget);
var expectSuccess = false;
Condition condition = null;
var valueLength = value?.Length ?? 0;
switch (type) {
case ComparisonType.Equal:
expectSuccess = valueLength == length;
condition = Condition.HashLengthEqual(key2, length);
break;
case ComparisonType.GreaterThan:
expectSuccess = valueLength > length;
condition = Condition.HashLengthGreaterThan(key2, length);
break;
case ComparisonType.LessThan:
expectSuccess = valueLength < length;
condition = Condition.HashLengthLessThan(key2, length);
break;
}
for (var i = 0; i < valueLength; i++) {
db.HashSet(key2, i, value[i].ToString(), flags: CommandFlags.FireAndForget);
}
Assert.IsFalse(db.KeyExists(key));
Assert.AreEqual(valueLength, db.HashLength(key2));
var tran = db.CreateTransaction();
var cond = tran.AddCondition(condition);
var push = tran.StringSetAsync(key, "any value");
var exec = tran.ExecuteAsync();
var get = db.StringLength(key);
Assert.AreEqual(expectTran, db.Wait(exec), "expected tran result");
if (expectSuccess) {
Assert.IsTrue(db.Wait(exec), "eq: exec");
Assert.IsTrue(cond.WasSatisfied, "eq: was satisfied");
Assert.AreEqual(true, db.Wait(push), "eq: push");
Assert.AreEqual("any value".Length, get, "eq: get");
} else {
Assert.IsFalse(db.Wait(exec), "neq: exec");
Assert.False(cond.WasSatisfied, "neq: was satisfied");
Assert.AreEqual(TaskStatus.Canceled, push.Status, "neq: push");
Assert.AreEqual(0, get, "neq: get");
}
}
}
[Test]
[TestCase("five", ComparisonType.Equal, 5L, false)]
[TestCase("four", ComparisonType.Equal, 4L, true)]
[TestCase("three", ComparisonType.Equal, 3L, false)]
[TestCase("", ComparisonType.Equal, 2L, false)]
[TestCase("", ComparisonType.Equal, 0L, true)]
[TestCase("five", ComparisonType.LessThan, 5L, true)]
[TestCase("four", ComparisonType.LessThan, 4L, false)]
[TestCase("three", ComparisonType.LessThan, 3L, false)]
[TestCase("", ComparisonType.LessThan, 2L, true)]
[TestCase("", ComparisonType.LessThan, 0L, false)]
[TestCase("five", ComparisonType.GreaterThan, 5L, false)]
[TestCase("four", ComparisonType.GreaterThan, 4L, false)]
[TestCase("three", ComparisonType.GreaterThan, 3L, true)]
[TestCase("", ComparisonType.GreaterThan, 2L, false)]
[TestCase("", ComparisonType.GreaterThan, 0L, false)]
public void BasicTranWithSetCardinalityCondition(string value, ComparisonType type, long length, bool expectTran)
{
using (var muxer = Create())
{
RedisKey key = Me(), key2 = Me() + "2";
var db = muxer.GetDatabase();
db.KeyDelete(key, CommandFlags.FireAndForget);
db.KeyDelete(key2, CommandFlags.FireAndForget);
var expectSuccess = false;
Condition condition = null;
var valueLength = value?.Length ?? 0;
switch (type) {
case ComparisonType.Equal:
expectSuccess = valueLength == length;
condition = Condition.SetLengthEqual(key2, length);
break;
case ComparisonType.GreaterThan:
expectSuccess = valueLength > length;
condition = Condition.SetLengthGreaterThan(key2, length);
break;
case ComparisonType.LessThan:
expectSuccess = valueLength < length;
condition = Condition.SetLengthLessThan(key2, length);
break;
}
for (var i = 0; i < valueLength; i++) {
db.SetAdd(key2, i, flags: CommandFlags.FireAndForget);
}
Assert.IsFalse(db.KeyExists(key));
Assert.AreEqual(valueLength, db.SetLength(key2));
var tran = db.CreateTransaction();
var cond = tran.AddCondition(condition);
var push = tran.StringSetAsync(key, "any value");
var exec = tran.ExecuteAsync();
var get = db.StringLength(key);
Assert.AreEqual(expectTran, db.Wait(exec), "expected tran result");
if (expectSuccess) {
Assert.IsTrue(db.Wait(exec), "eq: exec");
Assert.IsTrue(cond.WasSatisfied, "eq: was satisfied");
Assert.AreEqual(true, db.Wait(push), "eq: push");
Assert.AreEqual("any value".Length, get, "eq: get");
} else {
Assert.IsFalse(db.Wait(exec), "neq: exec");
Assert.False(cond.WasSatisfied, "neq: was satisfied");
Assert.AreEqual(TaskStatus.Canceled, push.Status, "neq: push");
Assert.AreEqual(0, get, "neq: get");
}
}
}
[Test]
[TestCase("five", ComparisonType.Equal, 5L, false)]
[TestCase("four", ComparisonType.Equal, 4L, true)]
[TestCase("three", ComparisonType.Equal, 3L, false)]
[TestCase("", ComparisonType.Equal, 2L, false)]
[TestCase("", ComparisonType.Equal, 0L, true)]
[TestCase("five", ComparisonType.LessThan, 5L, true)]
[TestCase("four", ComparisonType.LessThan, 4L, false)]
[TestCase("three", ComparisonType.LessThan, 3L, false)]
[TestCase("", ComparisonType.LessThan, 2L, true)]
[TestCase("", ComparisonType.LessThan, 0L, false)]
[TestCase("five", ComparisonType.GreaterThan, 5L, false)]
[TestCase("four", ComparisonType.GreaterThan, 4L, false)]
[TestCase("three", ComparisonType.GreaterThan, 3L, true)]
[TestCase("", ComparisonType.GreaterThan, 2L, false)]
[TestCase("", ComparisonType.GreaterThan, 0L, false)]
public void BasicTranWithSortedSetCardinalityCondition(string value, ComparisonType type, long length, bool expectTran)
{
using (var muxer = Create())
{
RedisKey key = Me(), key2 = Me() + "2";
var db = muxer.GetDatabase();
db.KeyDelete(key, CommandFlags.FireAndForget);
db.KeyDelete(key2, CommandFlags.FireAndForget);
var expectSuccess = false;
Condition condition = null;
var valueLength = value?.Length ?? 0;
switch (type) {
case ComparisonType.Equal:
expectSuccess = valueLength == length;
condition = Condition.SortedSetLengthEqual(key2, length);
break;
case ComparisonType.GreaterThan:
expectSuccess = valueLength > length;
condition = Condition.SortedSetLengthGreaterThan(key2, length);
break;
case ComparisonType.LessThan:
expectSuccess = valueLength < length;
condition = Condition.SortedSetLengthLessThan(key2, length);
break;
}
for (var i = 0; i < valueLength; i++) {
db.SortedSetAdd(key2, i, i, flags: CommandFlags.FireAndForget);
}
Assert.IsFalse(db.KeyExists(key));
Assert.AreEqual(valueLength, db.SortedSetLength(key2));
var tran = db.CreateTransaction();
var cond = tran.AddCondition(condition);
var push = tran.StringSetAsync(key, "any value");
var exec = tran.ExecuteAsync();
var get = db.StringLength(key);
Assert.AreEqual(expectTran, db.Wait(exec), "expected tran result");
if (expectSuccess) {
Assert.IsTrue(db.Wait(exec), "eq: exec");
Assert.IsTrue(cond.WasSatisfied, "eq: was satisfied");
Assert.AreEqual(true, db.Wait(push), "eq: push");
Assert.AreEqual("any value".Length, get, "eq: get");
} else {
Assert.IsFalse(db.Wait(exec), "neq: exec");
Assert.False(cond.WasSatisfied, "neq: was satisfied");
Assert.AreEqual(TaskStatus.Canceled, push.Status, "neq: push");
Assert.AreEqual(0, get, "neq: get");
}
}
}
[Test]
[TestCase("five", ComparisonType.Equal, 5L, false)]
[TestCase("four", ComparisonType.Equal, 4L, true)]
[TestCase("three", ComparisonType.Equal, 3L, false)]
[TestCase("", ComparisonType.Equal, 2L, false)]
[TestCase("", ComparisonType.Equal, 0L, true)]
[TestCase("five", ComparisonType.LessThan, 5L, true)]
[TestCase("four", ComparisonType.LessThan, 4L, false)]
[TestCase("three", ComparisonType.LessThan, 3L, false)]
[TestCase("", ComparisonType.LessThan, 2L, true)]
[TestCase("", ComparisonType.LessThan, 0L, false)]
[TestCase("five", ComparisonType.GreaterThan, 5L, false)]
[TestCase("four", ComparisonType.GreaterThan, 4L, false)]
[TestCase("three", ComparisonType.GreaterThan, 3L, true)]
[TestCase("", ComparisonType.GreaterThan, 2L, false)]
[TestCase("", ComparisonType.GreaterThan, 0L, false)]
public void BasicTranWithListLengthCondition(string value, ComparisonType type, long length, bool expectTran)
{
using (var muxer = Create())
{
RedisKey key = Me(), key2 = Me() + "2";
var db = muxer.GetDatabase();
db.KeyDelete(key, CommandFlags.FireAndForget);
db.KeyDelete(key2, CommandFlags.FireAndForget);
var expectSuccess = false;
Condition condition = null;
var valueLength = value?.Length ?? 0;
switch (type) {
case ComparisonType.Equal:
expectSuccess = valueLength == length;
condition = Condition.ListLengthEqual(key2, length);
break;
case ComparisonType.GreaterThan:
expectSuccess = valueLength > length;
condition = Condition.ListLengthGreaterThan(key2, length);
break;
case ComparisonType.LessThan:
expectSuccess = valueLength < length;
condition = Condition.ListLengthLessThan(key2, length);
break;
}
for (var i = 0; i < valueLength; i++) {
db.ListRightPush(key2, i, flags: CommandFlags.FireAndForget);
}
Assert.IsFalse(db.KeyExists(key));
Assert.AreEqual(valueLength, db.ListLength(key2));
var tran = db.CreateTransaction();
var cond = tran.AddCondition(condition);
var push = tran.StringSetAsync(key, "any value");
var exec = tran.ExecuteAsync();
var get = db.StringLength(key);
Assert.AreEqual(expectTran, db.Wait(exec), "expected tran result");
if (expectSuccess) {
Assert.IsTrue(db.Wait(exec), "eq: exec");
Assert.IsTrue(cond.WasSatisfied, "eq: was satisfied");
Assert.AreEqual(true, db.Wait(push), "eq: push");
Assert.AreEqual("any value".Length, get, "eq: get");
} else {
Assert.IsFalse(db.Wait(exec), "neq: exec");
Assert.False(cond.WasSatisfied, "neq: was satisfied");
Assert.AreEqual(TaskStatus.Canceled, push.Status, "neq: push");
Assert.AreEqual(0, get, "neq: get");
}
}
}
[Test] [Test]
public async void BasicTran() public async void BasicTran()
{ {
......
...@@ -117,6 +117,126 @@ public static Condition StringNotEqual(RedisKey key, RedisValue value) ...@@ -117,6 +117,126 @@ public static Condition StringNotEqual(RedisKey key, RedisValue value)
return new EqualsCondition(key, RedisValue.Null, false, value); return new EqualsCondition(key, RedisValue.Null, false, value);
} }
/// <summary>
/// Enforces that the given hash length is a certain value
/// </summary>
public static Condition HashLengthEqual(RedisKey key, long length)
{
return new LengthCondition(key, RedisType.Hash, 0, length);
}
/// <summary>
/// Enforces that the given hash length is less than a certain value
/// </summary>
public static Condition HashLengthLessThan(RedisKey key, long length)
{
return new LengthCondition(key, RedisType.Hash, 1, length);
}
/// <summary>
/// Enforces that the given hash length is greater than a certain value
/// </summary>
public static Condition HashLengthGreaterThan(RedisKey key, long length)
{
return new LengthCondition(key, RedisType.Hash, -1, length);
}
/// <summary>
/// Enforces that the given string length is a certain value
/// </summary>
public static Condition StringLengthEqual(RedisKey key, long length)
{
return new LengthCondition(key, RedisType.String, 0, length);
}
/// <summary>
/// Enforces that the given string length is less than a certain value
/// </summary>
public static Condition StringLengthLessThan(RedisKey key, long length)
{
return new LengthCondition(key, RedisType.String, 1, length);
}
/// <summary>
/// Enforces that the given string length is greater than a certain value
/// </summary>
public static Condition StringLengthGreaterThan(RedisKey key, long length)
{
return new LengthCondition(key, RedisType.String, -1, length);
}
/// <summary>
/// Enforces that the given list length is a certain value
/// </summary>
public static Condition ListLengthEqual(RedisKey key, long length)
{
return new LengthCondition(key, RedisType.List, 0, length);
}
/// <summary>
/// Enforces that the given list length is less than a certain value
/// </summary>
public static Condition ListLengthLessThan(RedisKey key, long length)
{
return new LengthCondition(key, RedisType.List, 1, length);
}
/// <summary>
/// Enforces that the given list length is greater than a certain value
/// </summary>
public static Condition ListLengthGreaterThan(RedisKey key, long length)
{
return new LengthCondition(key, RedisType.List, -1, length);
}
/// <summary>
/// Enforces that the given set cardinality is a certain value
/// </summary>
public static Condition SetLengthEqual(RedisKey key, long length)
{
return new LengthCondition(key, RedisType.Set, 0, length);
}
/// <summary>
/// Enforces that the given set cardinality is less than a certain value
/// </summary>
public static Condition SetLengthLessThan(RedisKey key, long length)
{
return new LengthCondition(key, RedisType.Set, 1, length);
}
/// <summary>
/// Enforces that the given set cardinality is greater than a certain value
/// </summary>
public static Condition SetLengthGreaterThan(RedisKey key, long length)
{
return new LengthCondition(key, RedisType.Set, -1, length);
}
/// <summary>
/// Enforces that the given sorted set cardinality is a certain value
/// </summary>
public static Condition SortedSetLengthEqual(RedisKey key, long length)
{
return new LengthCondition(key, RedisType.SortedSet, 0, length);
}
/// <summary>
/// Enforces that the given sorted set cardinality is less than a certain value
/// </summary>
public static Condition SortedSetLengthLessThan(RedisKey key, long length)
{
return new LengthCondition(key, RedisType.SortedSet, 1, length);
}
/// <summary>
/// Enforces that the given sorted set cardinality is greater than a certain value
/// </summary>
public static Condition SortedSetLengthGreaterThan(RedisKey key, long length)
{
return new LengthCondition(key, RedisType.SortedSet, -1, length);
}
internal abstract void CheckCommands(CommandMap commandMap); internal abstract void CheckCommands(CommandMap commandMap);
internal abstract IEnumerable<Message> CreateMessages(int db, ResultBox resultBox); internal abstract IEnumerable<Message> CreateMessages(int db, ResultBox resultBox);
...@@ -364,6 +484,97 @@ internal override bool TryValidate(RawResult result, out bool value) ...@@ -364,6 +484,97 @@ internal override bool TryValidate(RawResult result, out bool value)
} }
} }
internal class LengthCondition : Condition
{
internal override Condition MapKeys(Func<RedisKey,RedisKey> map)
{
return new LengthCondition(map(key), type, compareToResult, expectedLength);
}
private readonly int compareToResult;
private readonly long expectedLength;
private readonly RedisKey key;
private readonly RedisType type;
private readonly RedisCommand cmd;
public LengthCondition(RedisKey key, RedisType type, int compareToResult, long expectedLength)
{
if (key.IsNull) throw new ArgumentException(nameof(key));
this.key = key;
this.compareToResult = compareToResult;
this.expectedLength = expectedLength;
switch (type) {
case RedisType.Hash:
cmd = RedisCommand.HLEN;
break;
case RedisType.Set:
cmd = RedisCommand.SCARD;
break;
case RedisType.List:
cmd = RedisCommand.LLEN;
break;
case RedisType.SortedSet:
cmd = RedisCommand.ZCARD;
break;
case RedisType.String:
cmd = RedisCommand.STRLEN;
break;
default:
throw new ArgumentException(nameof(type));
}
}
public override string ToString()
{
return ((string)key) + " " + type + " length" + GetComparisonString() + expectedLength;
}
private string GetComparisonString()
{
return compareToResult == 0 ? " == " : (compareToResult < 0 ? " < " : " > ");
}
internal override void CheckCommands(CommandMap commandMap)
{
commandMap.AssertAvailable(cmd);
}
internal sealed override IEnumerable<Message> CreateMessages(int db, ResultBox resultBox)
{
yield return Message.Create(db, CommandFlags.None, RedisCommand.WATCH, key);
var message = ConditionProcessor.CreateMessage(this, db, CommandFlags.None, cmd, key);
message.SetSource(ConditionProcessor.Default, resultBox);
yield return message;
}
internal override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy)
{
return serverSelectionStrategy.HashSlot(key);
}
internal override bool TryValidate(RawResult result, out bool value)
{
switch (result.Type)
{
case ResultType.BulkString:
case ResultType.SimpleString:
case ResultType.Integer:
var parsed = result.AsRedisValue();
value = parsed.IsInteger && (expectedLength.CompareTo((long) parsed) == compareToResult);
ConnectionMultiplexer.TraceWithoutContext("actual: " + (string)parsed + "; expected: " + expectedLength +
"; wanted: " + GetComparisonString() + "; voting: " + value);
return true;
}
value = false;
return false;
}
}
} }
/// <summary> /// <summary>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment