Commit cad91c7b authored by Volodymyr Kostochka's avatar Volodymyr Kostochka Committed by Marc Gravell

Added: additional transaction conditions (#994)

* Added: additional transaction conditions

* Fixed: failed tests
Added: tests for new transaction conditions

* Conflicts resolving
parent 0ff83549
......@@ -22,7 +22,7 @@ public static Condition HashEqual(RedisKey key, RedisValue hashField, RedisValue
{
if (hashField.IsNull) throw new ArgumentNullException(nameof(hashField));
if (value.IsNull) return HashNotExists(key, hashField);
return new EqualsCondition(key, hashField, true, value);
return new EqualsCondition(key, RedisType.Hash, hashField, true, value);
}
/// <summary>
......@@ -46,7 +46,7 @@ public static Condition HashNotEqual(RedisKey key, RedisValue hashField, RedisVa
{
if (hashField.IsNull) throw new ArgumentNullException(nameof(hashField));
if (value.IsNull) return HashExists(key, hashField);
return new EqualsCondition(key, hashField, false, value);
return new EqualsCondition(key, RedisType.Hash, hashField, false, value);
}
/// <summary>
......@@ -110,7 +110,7 @@ public static Condition HashNotExists(RedisKey key, RedisValue hashField)
public static Condition StringEqual(RedisKey key, RedisValue value)
{
if (value.IsNull) return KeyNotExists(key);
return new EqualsCondition(key, RedisValue.Null, true, value);
return new EqualsCondition(key, RedisType.Hash, RedisValue.Null, true, value);
}
/// <summary>
......@@ -121,7 +121,7 @@ public static Condition StringEqual(RedisKey key, RedisValue value)
public static Condition StringNotEqual(RedisKey key, RedisValue value)
{
if (value.IsNull) return KeyExists(key);
return new EqualsCondition(key, RedisValue.Null, false, value);
return new EqualsCondition(key, RedisType.Hash, RedisValue.Null, false, value);
}
/// <summary>
......@@ -257,6 +257,52 @@ public static Condition StringNotEqual(RedisKey key, RedisValue value)
/// <param name="member">The member the sorted set must not contain.</param>
public static Condition SortedSetNotContains(RedisKey key, RedisValue member) => new ExistsCondition(key, RedisType.SortedSet, member, false);
/// <summary>
/// Enforces that the given sorted set member must have the specified score.
/// </summary>
/// <param name="key">The key of the sorted set to check.</param>
/// <param name="member">The member the sorted set to check.</param>
/// <param name="score">The score that member must have.</param>
public static Condition SortedSetEqual(RedisKey key, RedisValue member, RedisValue score) => new EqualsCondition(key, RedisType.SortedSet, member, true, score);
/// <summary>
/// Enforces that the given sorted set member must not have the specified score.
/// </summary>
/// <param name="key">The key of the sorted set to check.</param>
/// <param name="member">The member the sorted set to check.</param>
/// <param name="score">The score that member must not have.</param>
public static Condition SortedSetNotEqual(RedisKey key, RedisValue member, RedisValue score) => new EqualsCondition(key, RedisType.SortedSet, member, false, score);
/// <summary>
/// Enforces that the given sorted set must have the given score.
/// </summary>
/// <param name="key">The key of the sorted set to check.</param>
/// <param name="score">The score that the sorted set must have.</param>
public static Condition SortedSetScoreExists(RedisKey key, RedisValue score) => new SortedSetScoreCondition(key, score, false, 0);
/// <summary>
/// Enforces that the given sorted set must not have the given score.
/// </summary>
/// <param name="key">The key of the sorted set to check.</param>
/// <param name="score">The score that the sorted set must not have.</param>
public static Condition SortedSetScoreNotExists(RedisKey key, RedisValue score) => new SortedSetScoreCondition(key, score, true, 0);
/// <summary>
/// Enforces that the given sorted set must have the specified count of the given score.
/// </summary>
/// <param name="key">The key of the sorted set to check.</param>
/// <param name="score">The score that the sorted set must have.</param>
/// <param name="count">The number of members which sorted set must have.</param>
public static Condition SortedSetScoreExists(RedisKey key, RedisValue score, RedisValue count) => new SortedSetScoreCondition(key, score, true, count);
/// <summary>
/// Enforces that the given sorted set must not have the specified count of the given score.
/// </summary>
/// <param name="key">The key of the sorted set to check.</param>
/// <param name="score">The score that the sorted set must not have.</param>
/// <param name="count">The number of members which sorted set must not have.</param>
public static Condition SortedSetScoreNotExists(RedisKey key, RedisValue score, RedisValue count) => new SortedSetScoreCondition(key, score, false, count);
internal abstract void CheckCommands(CommandMap commandMap);
internal abstract IEnumerable<Message> CreateMessages(int db, ResultBox resultBox);
......@@ -273,6 +319,11 @@ public static Message CreateMessage(Condition condition, int db, CommandFlags fl
return new ConditionMessage(condition, db, flags, command, key, value);
}
public static Message CreateMessage(Condition condition, int db, CommandFlags flags, RedisCommand command, RedisKey key, RedisValue value, RedisValue value1)
{
return new ConditionMessage(condition, db, flags, command, key, value, value1);
}
protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result)
{
connection?.BridgeCouldBeNull?.Multiplexer?.OnTransactionLog($"condition '{message.CommandAndKey}' got '{result.ToString()}'");
......@@ -289,7 +340,8 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes
private class ConditionMessage : Message.CommandKeyBase
{
public readonly Condition Condition;
private RedisValue value;
private readonly RedisValue value;
private readonly RedisValue value1;
public ConditionMessage(Condition condition, int db, CommandFlags flags, RedisCommand command, RedisKey key, RedisValue value)
: base(db, flags, command, key)
......@@ -298,6 +350,12 @@ public ConditionMessage(Condition condition, int db, CommandFlags flags, RedisCo
this.value = value; // note no assert here
}
public ConditionMessage(Condition condition, int db, CommandFlags flags, RedisCommand command, RedisKey key, RedisValue value, RedisValue value1)
: this(condition, db, flags, command, key, value)
{
this.value1 = value1; // note no assert here
}
protected override void WriteImpl(PhysicalConnection physical)
{
if (value.IsNull)
......@@ -307,12 +365,16 @@ protected override void WriteImpl(PhysicalConnection physical)
}
else
{
physical.WriteHeader(command, 2);
physical.WriteHeader(command, value1.IsNull ? 2 : 3);
physical.Write(Key);
physical.WriteBulkString(value);
if (!value1.IsNull)
{
physical.WriteBulkString(value1);
}
}
}
public override int ArgCount => value.IsNull ? 1 : 2;
public override int ArgCount => value.IsNull ? 1 : value1.IsNull ? 2 : 3;
}
}
......@@ -410,39 +472,52 @@ internal class EqualsCondition : Condition
{
internal override Condition MapKeys(Func<RedisKey, RedisKey> map)
{
return new EqualsCondition(map(key), hashField, expectedEqual, expectedValue);
return new EqualsCondition(map(key), type, memberName, expectedEqual, expectedValue);
}
private readonly bool expectedEqual;
private readonly RedisValue hashField, expectedValue;
private readonly RedisValue memberName, expectedValue;
private readonly RedisKey key;
public EqualsCondition(RedisKey key, RedisValue hashField, bool expectedEqual, RedisValue expectedValue)
private readonly RedisType type;
private readonly RedisCommand cmd;
public EqualsCondition(RedisKey key, RedisType type, RedisValue memberName, bool expectedEqual, RedisValue expectedValue)
{
if (key.IsNull) throw new ArgumentException("key");
this.key = key;
this.hashField = hashField;
this.memberName = memberName;
this.expectedEqual = expectedEqual;
this.expectedValue = expectedValue;
this.type = type;
switch (type)
{
case RedisType.Hash:
cmd = memberName.IsNull ? RedisCommand.GET : RedisCommand.HGET;
break;
case RedisType.SortedSet:
cmd = RedisCommand.ZSCORE;
break;
default:
throw new ArgumentException(nameof(type));
}
}
public override string ToString()
{
return (hashField.IsNull ? key.ToString() : ((string)key) + " > " + hashField)
return (memberName.IsNull ? key.ToString() : ((string)key) + " " + type + " > " + memberName)
+ (expectedEqual ? " == " : " != ")
+ expectedValue;
}
internal override void CheckCommands(CommandMap commandMap)
{
commandMap.AssertAvailable(hashField.IsNull ? RedisCommand.GET : RedisCommand.HGET);
}
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 cmd = hashField.IsNull ? RedisCommand.GET : RedisCommand.HGET;
var message = ConditionProcessor.CreateMessage(this, db, CommandFlags.None, cmd, key, hashField);
var message = ConditionProcessor.CreateMessage(this, db, CommandFlags.None, cmd, key, memberName);
message.SetSource(ConditionProcessor.Default, resultBox);
yield return message;
}
......@@ -454,6 +529,24 @@ internal override int GetHashSlot(ServerSelectionStrategy serverSelectionStrateg
internal override bool TryValidate(in RawResult result, out bool value)
{
switch (type)
{
case RedisType.SortedSet:
var parsedValue = RedisValue.Null;
if (!result.IsNull)
{
if (result.TryGetDouble(out var val))
{
parsedValue = val;
}
}
value = (parsedValue == expectedValue) == expectedEqual;
ConnectionMultiplexer.TraceWithoutContext("actual: " + (string)parsedValue + "; expected: " + (string)expectedValue +
"; wanted: " + (expectedEqual ? "==" : "!=") + "; voting: " + value);
return true;
default:
switch (result.Type)
{
case ResultType.BulkString:
......@@ -469,6 +562,7 @@ internal override bool TryValidate(in RawResult result, out bool value)
return false;
}
}
}
internal class ListCondition : Condition
{
......@@ -631,6 +725,65 @@ internal override bool TryValidate(in RawResult result, out bool value)
return false;
}
}
internal class SortedSetScoreCondition : Condition
{
internal override Condition MapKeys(Func<RedisKey, RedisKey> map)
{
return new SortedSetScoreCondition(map(key), sortedSetScore, expectedEqual, expectedValue);
}
private readonly bool expectedEqual;
private readonly RedisValue sortedSetScore, expectedValue;
private readonly RedisKey key;
public SortedSetScoreCondition(RedisKey key, RedisValue sortedSetScore, bool expectedEqual, RedisValue expectedValue)
{
if (key.IsNull)
{
throw new ArgumentException("key");
}
this.key = key;
this.sortedSetScore = sortedSetScore;
this.expectedEqual = expectedEqual;
this.expectedValue = expectedValue;
}
public override string ToString()
{
return key.ToString() + (expectedEqual ? " contains " : " not contains ") + expectedValue + " members with score: " + sortedSetScore;
}
internal override void CheckCommands(CommandMap commandMap) => commandMap.AssertAvailable(RedisCommand.ZCOUNT);
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, RedisCommand.ZCOUNT, key, sortedSetScore, sortedSetScore);
message.SetSource(ConditionProcessor.Default, resultBox);
yield return message;
}
internal override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => serverSelectionStrategy.HashSlot(key);
internal override bool TryValidate(in RawResult result, out bool value)
{
switch (result.Type)
{
case ResultType.Integer:
var parsedValue = result.AsRedisValue();
value = (parsedValue == expectedValue) == expectedEqual;
ConnectionMultiplexer.TraceWithoutContext("actual: " + (string)parsedValue + "; expected: " + (string)expectedValue + "; wanted: " + (expectedEqual ? "==" : "!=") + "; voting: " + value);
return true;
}
value = false;
return false;
}
}
}
/// <summary>
......
......@@ -20,14 +20,14 @@ public TransactionWrapperTests()
public void AddCondition_HashEqual()
{
wrapper.AddCondition(Condition.HashEqual("key", "field", "value"));
mock.Verify(_ => _.AddCondition(It.Is<Condition>(value => "prefix:key > field == value" == value.ToString())));
mock.Verify(_ => _.AddCondition(It.Is<Condition>(value => "prefix:key Hash > field == value" == value.ToString())));
}
[Fact]
public void AddCondition_HashNotEqual()
{
wrapper.AddCondition(Condition.HashNotEqual("key", "field", "value"));
mock.Verify(_ => _.AddCondition(It.Is<Condition>(value => "prefix:key > field != value" == value.ToString())));
mock.Verify(_ => _.AddCondition(It.Is<Condition>(value => "prefix:key Hash > field != value" == value.ToString())));
}
[Fact]
......@@ -72,6 +72,48 @@ public void AddCondition_StringNotEqual()
mock.Verify(_ => _.AddCondition(It.Is<Condition>(value => "prefix:key != value" == value.ToString())));
}
[Fact]
public void AddCondition_SortedSetEqual()
{
wrapper.AddCondition(Condition.SortedSetEqual("key", "member", "score"));
mock.Verify(_ => _.AddCondition(It.Is<Condition>(value => "prefix:key SortedSet > member == score" == value.ToString())));
}
[Fact]
public void AddCondition_SortedSetNotEqual()
{
wrapper.AddCondition(Condition.SortedSetNotEqual("key", "member", "score"));
mock.Verify(_ => _.AddCondition(It.Is<Condition>(value => "prefix:key SortedSet > member != score" == value.ToString())));
}
[Fact]
public void AddCondition_SortedSetScoreExists()
{
wrapper.AddCondition(Condition.SortedSetScoreExists("key", "score"));
mock.Verify(_ => _.AddCondition(It.Is<Condition>(value => "prefix:key not contains 0 members with score: score" == value.ToString())));
}
[Fact]
public void AddCondition_SortedSetScoreNotExists()
{
wrapper.AddCondition(Condition.SortedSetScoreNotExists("key", "score"));
mock.Verify(_ => _.AddCondition(It.Is<Condition>(value => "prefix:key contains 0 members with score: score" == value.ToString())));
}
[Fact]
public void AddCondition_SortedSetScoreCountExists()
{
wrapper.AddCondition(Condition.SortedSetScoreExists("key", "score", "count"));
mock.Verify(_ => _.AddCondition(It.Is<Condition>(value => "prefix:key contains count members with score: score" == value.ToString())));
}
[Fact]
public void AddCondition_SortedSetScoreCountNotExists()
{
wrapper.AddCondition(Condition.SortedSetScoreNotExists("key", "score", "count"));
mock.Verify(_ => _.AddCondition(It.Is<Condition>(value => "prefix:key not contains count members with score: score" == value.ToString())));
}
[Fact]
public void ExecuteAsync()
{
......
......@@ -748,6 +748,184 @@ public async Task BasicTranWithSortedSetContainsCondition(bool demandKeyExists,
}
}
[Theory]
[InlineData(4D, 4D, true, true)]
[InlineData(4D, 5D, true, false)]
[InlineData(4D, null, true, false)]
[InlineData(null, 5D, true, false)]
[InlineData(null, null, true, true)]
[InlineData(4D, 4D, false, false)]
[InlineData(4D, 5D, false, true)]
[InlineData(4D, null, false, true)]
[InlineData(null, 5D, false, true)]
[InlineData(null, null, false, false)]
public async Task BasicTranWithSortedSetEqualCondition(double? expected, double? value, bool expectEqual, bool expectedTranResult)
{
using (var muxer = Create())
{
RedisKey key = Me(), key2 = Me() + "2";
var db = muxer.GetDatabase();
db.KeyDelete(key, CommandFlags.FireAndForget);
db.KeyDelete(key2, CommandFlags.FireAndForget);
RedisValue member = "member";
if (value != null) db.SortedSetAdd(key2, member, value.Value, flags: CommandFlags.FireAndForget);
Assert.False(db.KeyExists(key));
Assert.Equal(value, db.SortedSetScore(key2, member));
var tran = db.CreateTransaction();
var cond = tran.AddCondition(expectEqual ? Condition.SortedSetEqual(key2, member, expected) : Condition.SortedSetNotEqual(key2, member, expected));
var incr = tran.StringIncrementAsync(key);
var exec = tran.ExecuteAsync();
var get = db.StringGet(key);
Assert.Equal(expectedTranResult, await exec);
if (expectEqual == (value == expected))
{
Assert.True(await exec, "eq: exec");
Assert.True(cond.WasSatisfied, "eq: was satisfied");
Assert.Equal(1, await incr); // eq: incr
Assert.Equal(1, (long)get); // eq: get
}
else
{
Assert.False(await exec, "neq: exec");
Assert.False(cond.WasSatisfied, "neq: was satisfied");
Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr
Assert.Equal(0, (long)get); // neq: get
}
}
}
[Theory]
[InlineData(true, true, true, true)]
[InlineData(true, false, true, true)]
[InlineData(false, true, true, true)]
[InlineData(true, true, false, false)]
[InlineData(true, false, false, false)]
[InlineData(false, true, false, false)]
[InlineData(false, false, true, false)]
[InlineData(false, false, false, true)]
public async Task BasicTranWithSortedSetScoreExistsCondition(bool member1HasScore, bool member2HasScore, bool demandScoreExists, bool expectedTranResult)
{
using (var muxer = Create())
{
RedisKey key = Me(), key2 = Me() + "2";
var db = muxer.GetDatabase();
db.KeyDelete(key, CommandFlags.FireAndForget);
db.KeyDelete(key2, CommandFlags.FireAndForget);
const double Score = 4D;
RedisValue member1 = "member1";
RedisValue member2 = "member2";
if (member1HasScore)
{
db.SortedSetAdd(key2, member1, Score, flags: CommandFlags.FireAndForget);
}
if (member2HasScore)
{
db.SortedSetAdd(key2, member2, Score, flags: CommandFlags.FireAndForget);
}
Assert.False(db.KeyExists(key));
Assert.Equal(member1HasScore ? (double?)Score : null, db.SortedSetScore(key2, member1));
Assert.Equal(member2HasScore ? (double?)Score : null, db.SortedSetScore(key2, member2));
var tran = db.CreateTransaction();
var cond = tran.AddCondition(demandScoreExists ? Condition.SortedSetScoreExists(key2, Score) : Condition.SortedSetScoreNotExists(key2, Score));
var incr = tran.StringIncrementAsync(key);
var exec = tran.ExecuteAsync();
var get = db.StringGet(key);
Assert.Equal(expectedTranResult, await exec);
if ((member1HasScore || member2HasScore) == demandScoreExists)
{
Assert.True(await exec, "eq: exec");
Assert.True(cond.WasSatisfied, "eq: was satisfied");
Assert.Equal(1, await incr); // eq: incr
Assert.Equal(1, (long)get); // eq: get
}
else
{
Assert.False(await exec, "neq: exec");
Assert.False(cond.WasSatisfied, "neq: was satisfied");
Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr
Assert.Equal(0, (long)get); // neq: get
}
}
}
[Theory]
[InlineData(true, true, 2L, true, true)]
[InlineData(true, true, 2L, false, false)]
[InlineData(true, true, 1L, true, false)]
[InlineData(true, true, 1L, false, true)]
[InlineData(true, false, 2L, true, false)]
[InlineData(true, false, 2L, false, true)]
[InlineData(true, false, 1L, true, true)]
[InlineData(true, false, 1L, false, false)]
[InlineData(false, true, 2L, true, false)]
[InlineData(false, true, 2L, false, true)]
[InlineData(false, true, 1L, true, true)]
[InlineData(false, true, 1L, false, false)]
[InlineData(false, false, 2L, true, false)]
[InlineData(false, false, 2L, false, true)]
[InlineData(false, false, 1L, true, false)]
[InlineData(false, false, 1L, false, true)]
public async Task BasicTranWithSortedSetScoreCountExistsCondition(bool member1HasScore, bool member2HasScore, long expectedLength, bool expectEqual, bool expectedTranResult)
{
using (var muxer = Create())
{
RedisKey key = Me(), key2 = Me() + "2";
var db = muxer.GetDatabase();
db.KeyDelete(key, CommandFlags.FireAndForget);
db.KeyDelete(key2, CommandFlags.FireAndForget);
const double Score = 4D;
var length = 0L;
RedisValue member1 = "member1";
RedisValue member2 = "member2";
if (member1HasScore)
{
db.SortedSetAdd(key2, member1, Score, flags: CommandFlags.FireAndForget);
length++;
}
if (member2HasScore)
{
db.SortedSetAdd(key2, member2, Score, flags: CommandFlags.FireAndForget);
length++;
}
Assert.False(db.KeyExists(key));
Assert.Equal(length, db.SortedSetLength(key2, Score, Score));
var tran = db.CreateTransaction();
var cond = tran.AddCondition(expectEqual ? Condition.SortedSetScoreExists(key2, Score, expectedLength) : Condition.SortedSetScoreNotExists(key2, Score, expectedLength));
var incr = tran.StringIncrementAsync(key);
var exec = tran.ExecuteAsync();
var get = db.StringGet(key);
Assert.Equal(expectedTranResult, await exec);
if (expectEqual == (length == expectedLength))
{
Assert.True(await exec, "eq: exec");
Assert.True(cond.WasSatisfied, "eq: was satisfied");
Assert.Equal(1, await incr); // eq: incr
Assert.Equal(1, (long)get); // eq: get
}
else
{
Assert.False(await exec, "neq: exec");
Assert.False(cond.WasSatisfied, "neq: was satisfied");
Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr
Assert.Equal(0, (long)get); // neq: get
}
}
}
[Theory]
[InlineData("five", ComparisonType.Equal, 5L, false)]
[InlineData("four", ComparisonType.Equal, 4L, true)]
......
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