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;
......
This diff is collapsed.
......@@ -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