Commit 9d3de2ae authored by Marc Gravell's avatar Marc Gravell

Basic Lua scripting (no script cache / evalsha yet)

parent b1d6d788
......@@ -98,6 +98,9 @@ At the Redis layer (and assuming `HSETNX` did not exist) this could be implement
EVAL "if redis.call('hexists', KEYS[1], 'UniqueId') then return redis.call('hset', KEYS[1], 'UniqueId', ARGV[1]) else return 0 end" 1 {custKey} {newId}
This scrip
This can be used in StackExchange.Redis via:
Lua scripting is not currently implemented in StackExchange.Redis, but will be very soon.
\ No newline at end of file
var wasSet = (bool) db.ScriptEvaluate(@"if redis.call('hexists', KEYS[1], 'UniqueId') then return redis.call('hset', KEYS[1], 'UniqueId', ARGV[1]) else return 0 end",
new RedisKey[] { custKey }, new RedisValue[] { newId });
(note that the response from `ScriptEvaluate` and `ScriptEvaluateAsync` is variable depending on your exact script; the response can be interpreted by casting - in this case as a `bool`)
\ No newline at end of file
......@@ -185,6 +185,7 @@ void CheckMethod(MethodInfo method, bool isAsync)
|| shortName.StartsWith("String") || shortName.StartsWith("List")
|| shortName.StartsWith("SortedSet") || shortName.StartsWith("Set")
|| shortName.StartsWith("Debug") || shortName.StartsWith("Lock")
|| shortName.StartsWith("Script")
, fullName + ":Prefix");
}
......
using System;
using NUnit.Framework;
namespace StackExchange.Redis.Tests
{
[TestFixture]
public class Scripting : TestBase
{
[Test]
public void TestBasicScripting()
{
using (var conn = Create())
{
RedisValue newId = Guid.NewGuid().ToString();
RedisKey custKey = Me();
var db = conn.GetDatabase();
db.KeyDelete(custKey);
db.HashSet(custKey, "id", 123);
var wasSet = (bool) db.ScriptEvaluate(@"if redis.call('hexists', KEYS[1], 'UniqueId') then return redis.call('hset', KEYS[1], 'UniqueId', ARGV[1]) else return 0 end",
new RedisKey[] { custKey }, new RedisValue[] { newId });
Assert.IsTrue(wasSet);
wasSet = (bool)db.ScriptEvaluate(@"if redis.call('hexists', KEYS[1], 'UniqueId') then return redis.call('hset', KEYS[1], 'UniqueId', ARGV[1]) else return 0 end",
new RedisKey[] { custKey }, new RedisValue[] { newId });
Assert.IsFalse(wasSet);
}
}
}
}
......@@ -77,6 +77,7 @@
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="PubSub.cs" />
<Compile Include="RealWorld.cs" />
<Compile Include="Scripting.cs" />
<Compile Include="Secure.cs" />
<Compile Include="Sets.cs" />
<Compile Include="SSL.cs" />
......
......@@ -115,6 +115,7 @@
<Compile Include="StackExchange\Redis\MessageQueue.cs" />
<Compile Include="StackExchange\Redis\PhysicalConnection.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="StackExchange\Redis\RedisResult.cs" />
<Compile Include="StackExchange\Redis\RedisTransaction.cs" />
<Compile Include="StackExchange\Redis\RedisDatabase.cs" />
<Compile Include="StackExchange\Redis\RedisFeatures.cs" />
......
......@@ -385,6 +385,13 @@ public interface IDatabase : IRedis, IDatabaseAsync
[IgnoreNamePrefix]
RedisKey RandomKey(CommandFlags flags = CommandFlags.None);
/// <summary>
/// Execute a Lua script against the server
/// </summary>
/// <remarks>http://redis.io/commands/eval</remarks>
/// <returns>A dynamic representation of the script's result</returns>
RedisResult ScriptEvaluate(string script, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None);
/// <summary>
/// Add the specified member to the set stored at key. Specified members that are already a member of this set are ignored. If key does not exist, a new set is created before adding the specified members.
/// </summary>
......
......@@ -374,6 +374,13 @@ public interface IDatabaseAsync : IRedisAsync
/// <summary>
/// Execute a Lua script against the server
/// </summary>
/// <remarks>http://redis.io/commands/eval</remarks>
/// <returns>A dynamic representation of the script's result</returns>
Task<RedisResult> ScriptEvaluateAsync(string script, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None);
/// <summary>
/// Add the specified member to the set stored at key. Specified members that are already a member of this set are ignored. If key does not exist, a new set is created before adding the specified members.
/// </summary>
......
......@@ -673,6 +673,18 @@ public Task<RedisKey> RandomKeyAsync(CommandFlags flags = CommandFlags.None)
return ExecuteAsync(msg, ResultProcessor.RedisKey);
}
public RedisResult ScriptEvaluate(string script, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None)
{
var msg = new ScriptEvalMessage(Db, flags, RedisCommand.EVAL, script, keys ?? RedisKey.EmptyArray, values ?? RedisValue.EmptyArray);
return ExecuteSync(msg, ResultProcessor.RedisResult);
}
public Task<RedisResult> ScriptEvaluateAsync(string script, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None)
{
var msg = new ScriptEvalMessage(Db, flags, RedisCommand.EVAL, script, keys ?? RedisKey.EmptyArray, values ?? RedisValue.EmptyArray);
return ExecuteAsync(msg, ResultProcessor.RedisResult);
}
public bool SetAdd(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None)
{
var msg = Message.Create(Db, flags, RedisCommand.SADD, key, value);
......@@ -1869,6 +1881,41 @@ Message CreateMessage(long cursor, bool running)
}
}
private sealed class ScriptEvalMessage : Message
{
private readonly RedisKey[] keys;
private readonly RedisValue script;
private readonly RedisValue[] values;
public ScriptEvalMessage(int db, CommandFlags flags, RedisCommand command, string script, RedisKey[] keys, RedisValue[] values) : base(db, flags, command)
{
this.script = script;
for (int i = 0; i < keys.Length; i++)
keys[i].Assert();
this.keys = keys;
for (int i = 0; i < values.Length; i++)
values[i].Assert();
this.values = values;
}
public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy)
{
int slot = ServerSelectionStrategy.NoSlot;
for (int i = 0; i < keys.Length; i++)
slot = serverSelectionStrategy.CombineSlot(slot, keys[i]);
return slot;
}
internal override void WriteImpl(PhysicalConnection physical)
{
physical.WriteHeader(command, 2 + keys.Length + values.Length);
physical.Write(script);
physical.Write(keys.Length);
for (int i = 0; i < keys.Length; i++)
physical.Write(keys[i]);
for (int i = 0; i < values.Length; i++)
physical.Write(values[i]);
}
}
sealed class SortedSetCombineAndStoreCommandMessage : Message.CommandKeyBase // ZINTERSTORE and ZUNIONSTORE have a very unusual signature
{
private readonly RedisKey[] keys;
......
using System;
namespace StackExchange.Redis
{
/// <summary>
/// Represents a general-purpose result from redis, that may be cast into various anticipated types
/// </summary>
public abstract class RedisResult
{
// internally, this is very similar to RawResult, except it is designed to be usable
// outside of the IO-processing pipeline: the buffers are standalone, etc
internal static RedisResult TryCreate(RawResult result)
{
try
{
switch (result.Type)
{
case ResultType.Integer:
case ResultType.SimpleString:
case ResultType.BulkString:
return new SingleRedisResult(result.AsRedisValue());
case ResultType.Array:
var items = result.GetItems();
var arr = new RedisResult[items.Length];
for (int i = 0; i < arr.Length; i++)
{
var next = TryCreate(items[i]);
if (next == null) return null; // means we didn't understand
arr[i] = next;
}
return new ArrayRedisResult(arr);
case ResultType.Error:
return new ErrorRedisResult(result.GetString());
default:
return null;
}
} catch
{
return null; // will be logged as a protocol fail by the processor
}
}
/// <summary>
/// Interprets the result as a String
/// </summary>
public static explicit operator string (RedisResult result) { return result.AsString(); }
/// <summary>
/// Interprets the result as a Byte[]
/// </summary>
public static explicit operator byte[] (RedisResult result) { return result.AsByteArray(); }
/// <summary>
/// Interprets the result as a Double
/// </summary>
public static explicit operator double (RedisResult result) { return result.AsDouble(); }
/// <summary>
/// Interprets the result as an Int64
/// </summary>
public static explicit operator long (RedisResult result) { return result.AsInt64(); }
/// <summary>
/// Interprets the result as an Int32
/// </summary>
public static explicit operator int (RedisResult result) { return result.AsInt32(); }
/// <summary>
/// Interprets the result as a Boolean
/// </summary>
public static explicit operator bool (RedisResult result) { return result.AsBoolean(); }
/// <summary>
/// Interprets the result as a RedisValue
/// </summary>
public static explicit operator RedisValue (RedisResult result) { return result.AsRedisValue(); }
/// <summary>
/// Interprets the result as a RedisKey
/// </summary>
public static explicit operator RedisKey (RedisResult result) { return result.AsRedisKey(); }
/// <summary>
/// Interprets the result as a Nullable Double
/// </summary>
public static explicit operator double? (RedisResult result) { return result.AsNullableDouble(); }
/// <summary>
/// Interprets the result as a Nullable Int64
/// </summary>
public static explicit operator long? (RedisResult result) { return result.AsNullableInt64(); }
/// <summary>
/// Interprets the result as a Nullable Int32
/// </summary>
public static explicit operator int? (RedisResult result) { return result.AsNullableInt32(); }
/// <summary>
/// Interprets the result as a Nullable Boolean
/// </summary>
public static explicit operator bool? (RedisResult result) { return result.AsNullableBoolean(); }
/// <summary>
/// Interprets the result as an array of String
/// </summary>
public static explicit operator string[] (RedisResult result) { return result.AsStringArray(); }
/// <summary>
/// Interprets the result as an array of Byte[]
/// </summary>
public static explicit operator byte[][] (RedisResult result) { return result.AsByteArrayArray(); }
/// <summary>
/// Interprets the result as an array of Double
/// </summary>
public static explicit operator double[] (RedisResult result) { return result.AsDoubleArray(); }
/// <summary>
/// Interprets the result as an array of Int64
/// </summary>
public static explicit operator long[] (RedisResult result) { return result.AsInt64Array(); }
/// <summary>
/// Interprets the result as an array of Int32
/// </summary>
public static explicit operator int[] (RedisResult result) { return result.AsInt32Array(); }
/// <summary>
/// Interprets the result as an array of Boolean
/// </summary>
public static explicit operator bool[] (RedisResult result) { return result.AsBooleanArray(); }
/// <summary>
/// Interprets the result as an array of RedisValue
/// </summary>
public static explicit operator RedisValue[] (RedisResult result) { return result.AsRedisValueArray(); }
/// <summary>
/// Interprets the result as an array of RedisKey
/// </summary>
public static explicit operator RedisKey[] (RedisResult result) { return result.AsRedisKeyArray(); }
internal abstract bool AsBoolean();
internal abstract bool[] AsBooleanArray();
internal abstract byte[] AsByteArray();
internal abstract byte[][] AsByteArrayArray();
internal abstract double AsDouble();
internal abstract double[] AsDoubleArray();
internal abstract int AsInt32();
internal abstract int[] AsInt32Array();
internal abstract long AsInt64();
internal abstract long[] AsInt64Array();
internal abstract bool? AsNullableBoolean();
internal abstract double? AsNullableDouble();
internal abstract int? AsNullableInt32();
internal abstract long? AsNullableInt64();
internal abstract RedisKey AsRedisKey();
internal abstract RedisKey[] AsRedisKeyArray();
internal abstract RedisValue AsRedisValue();
internal abstract RedisValue[] AsRedisValueArray();
internal abstract string AsString();
internal abstract string[] AsStringArray();
private sealed class ArrayRedisResult : RedisResult
{
private readonly RedisResult[] value;
public ArrayRedisResult(RedisResult[] value)
{
if (value == null) throw new ArgumentNullException("value");
this.value = value;
}
public override string ToString()
{
return value.Length + " element(s)";
}
internal override bool AsBoolean()
{
if (value.Length == 1) return value[0].AsBoolean();
throw new InvalidCastException();
}
internal override bool[] AsBooleanArray() { return Array.ConvertAll(value, x => x.AsBoolean()); }
internal override byte[] AsByteArray()
{
if (value.Length == 1) return value[0].AsByteArray();
throw new InvalidCastException();
}
internal override byte[][] AsByteArrayArray() { return Array.ConvertAll(value, x => x.AsByteArray()); }
internal override double AsDouble()
{
if (value.Length == 1) return value[0].AsDouble();
throw new InvalidCastException();
}
internal override double[] AsDoubleArray() { return Array.ConvertAll(value, x => x.AsDouble()); }
internal override int AsInt32()
{
if (value.Length == 1) return value[0].AsInt32();
throw new InvalidCastException();
}
internal override int[] AsInt32Array() { return Array.ConvertAll(value, x => x.AsInt32()); }
internal override long AsInt64()
{
if (value.Length == 1) return value[0].AsInt64();
throw new InvalidCastException();
}
internal override long[] AsInt64Array() { return Array.ConvertAll(value, x => x.AsInt64()); }
internal override bool? AsNullableBoolean()
{
if (value.Length == 1) return value[0].AsNullableBoolean();
throw new InvalidCastException();
}
internal override double? AsNullableDouble()
{
if (value.Length == 1) return value[0].AsNullableDouble();
throw new InvalidCastException();
}
internal override int? AsNullableInt32()
{
if (value.Length == 1) return value[0].AsNullableInt32();
throw new InvalidCastException();
}
internal override long? AsNullableInt64()
{
if (value.Length == 1) return value[0].AsNullableInt64();
throw new InvalidCastException();
}
internal override RedisKey AsRedisKey()
{
if (value.Length == 1) return value[0].AsRedisKey();
throw new InvalidCastException();
}
internal override RedisKey[] AsRedisKeyArray() { return Array.ConvertAll(value, x => x.AsRedisKey()); }
internal override RedisValue AsRedisValue()
{
if (value.Length == 1) return value[0].AsRedisValue();
throw new InvalidCastException();
}
internal override RedisValue[] AsRedisValueArray() { return Array.ConvertAll(value, x => x.AsRedisValue()); }
internal override string AsString()
{
if (value.Length == 1) return value[0].AsString();
throw new InvalidCastException();
}
internal override string[] AsStringArray() { return Array.ConvertAll(value, x => x.AsString()); }
}
private sealed class ErrorRedisResult : RedisResult
{
private readonly string value;
public ErrorRedisResult(string value)
{
if (value == null) throw new ArgumentNullException("value");
this.value = value;
}
public override string ToString() { return value; }
internal override bool AsBoolean() { throw new RedisServerException(value); }
internal override bool[] AsBooleanArray() { throw new RedisServerException(value); }
internal override byte[] AsByteArray() { throw new RedisServerException(value); }
internal override byte[][] AsByteArrayArray() { throw new RedisServerException(value); }
internal override double AsDouble() { throw new RedisServerException(value); }
internal override double[] AsDoubleArray() { throw new RedisServerException(value); }
internal override int AsInt32() { throw new RedisServerException(value); }
internal override int[] AsInt32Array() { throw new RedisServerException(value); }
internal override long AsInt64() { throw new RedisServerException(value); }
internal override long[] AsInt64Array() { throw new RedisServerException(value); }
internal override bool? AsNullableBoolean() { throw new RedisServerException(value); }
internal override double? AsNullableDouble() { throw new RedisServerException(value); }
internal override int? AsNullableInt32() { throw new RedisServerException(value); }
internal override long? AsNullableInt64() { throw new RedisServerException(value); }
internal override RedisKey AsRedisKey() { throw new RedisServerException(value); }
internal override RedisKey[] AsRedisKeyArray() { throw new RedisServerException(value); }
internal override RedisValue AsRedisValue() { throw new RedisServerException(value); }
internal override RedisValue[] AsRedisValueArray() { throw new RedisServerException(value); }
internal override string AsString() { throw new RedisServerException(value); }
internal override string[] AsStringArray() { throw new RedisServerException(value); }
}
private sealed class SingleRedisResult : RedisResult
{
private readonly RedisValue value;
public SingleRedisResult(RedisValue value)
{
this.value = value;
}
public override string ToString() { return value.ToString(); }
internal override bool AsBoolean() { return (bool)value; }
internal override bool[] AsBooleanArray() { return new[] { AsBoolean() }; }
internal override byte[] AsByteArray() { return (byte[])value; }
internal override byte[][] AsByteArrayArray() { return new[] { AsByteArray() }; }
internal override double AsDouble() { return (double)value; }
internal override double[] AsDoubleArray() { return new[] { AsDouble() }; }
internal override int AsInt32() { return (int)value; }
internal override int[] AsInt32Array() { return new[] { AsInt32() }; }
internal override long AsInt64() { return (long)value; }
internal override long[] AsInt64Array() { return new[] { AsInt64() }; }
internal override bool? AsNullableBoolean() { return (bool?)value; }
internal override double? AsNullableDouble() { return (double?)value; }
internal override int? AsNullableInt32() { return (int?)value; }
internal override long? AsNullableInt64() { return (long?)value; }
internal override RedisKey AsRedisKey() { return (byte[])value; }
internal override RedisKey[] AsRedisKeyArray() { return new[] { AsRedisKey() }; }
internal override RedisValue AsRedisValue() { return value; }
internal override RedisValue[] AsRedisValueArray() { return new[] { AsRedisValue() }; }
internal override string AsString() { return (string)value; }
internal override string[] AsStringArray() { return new[] { AsString() }; }
}
}
}
......@@ -80,6 +80,9 @@ public static readonly TimeSpanProcessor
public static readonly ResultProcessor<KeyValuePair<RedisValue, double>[]>
SortedSetWithScores = new SortedSetWithScoresProcessor();
public static readonly ResultProcessor<RedisResult>
RedisResult = new RedisResultProcessor();
static readonly byte[] MOVED = Encoding.UTF8.GetBytes("MOVED "), ASK = Encoding.UTF8.GetBytes("ASK ");
......@@ -1008,6 +1011,22 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes
return true; // we'll always acknowledge that we saw a non-error response
}
}
private class RedisResultProcessor : ResultProcessor<RedisResult>
{
// note that top-level error messages still get handled by SetResult, but nested errors
// (is that a thing?) will be wrapped in the RedisResult
protected override bool SetResultCore(PhysicalConnection connection, Message message, RawResult result)
{
var value = Redis.RedisResult.TryCreate(result);
if(value != null)
{
SetResult(message, value);
return true;
}
return false;
}
}
}
internal abstract class ResultProcessor<T> : ResultProcessor
{
......
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