Commit bc31336f authored by Marc Gravell's avatar Marc Gravell

Scripting now correctly returns the correct hash byte[] (half-width), and...

Scripting now correctly returns the correct hash byte[] (half-width), and accepts hashes automatically as either byte[] or naked sha1 string
parent a816651d
using System;
using System.Diagnostics;
using NUnit.Framework;
using System.Linq;
namespace StackExchange.Redis.Tests
{
......@@ -140,5 +141,33 @@ public void CompareScriptToDirect()
directTime.TotalMilliseconds);
}
}
[Test]
public void TestCallByHash()
{
const string Script = "return redis.call('incr', KEYS[1])";
using (var conn = Create(allowAdmin: true))
{
var server = conn.GetServer(PrimaryServer, PrimaryPort);
server.FlushAllDatabases();
server.ScriptFlush();
byte[] hash = server.ScriptLoad(Script);
var db = conn.GetDatabase();
RedisKey[] keys = { Me() };
string hexHash = string.Concat(Array.ConvertAll(hash, x => x.ToString("X2")));
Assert.AreEqual("2BAB3B661081DB58BD2341920E0BA7CF5DC77B25", hexHash);
db.ScriptEvaluate(hexHash, keys);
db.ScriptEvaluate(hash, keys);
var count = (int)db.StringGet(keys)[0];
Assert.AreEqual(2, count);
}
}
}
}
......@@ -433,10 +433,17 @@ public interface IDatabase : IRedis, IDatabaseAsync
/// <summary>
/// Execute a Lua script against the server
/// </summary>
/// <remarks>http://redis.io/commands/eval</remarks>
/// <remarks>http://redis.io/commands/eval, http://redis.io/commands/evalsha</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>
/// Execute a Lua script against the server using just the SHA1 hash
/// </summary>
/// <remarks>http://redis.io/commands/evalsha</remarks>
/// <returns>A dynamic representation of the script's result</returns>
RedisResult ScriptEvaluate(byte[] hash, 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>
......
......@@ -414,10 +414,17 @@ public interface IDatabaseAsync : IRedisAsync
/// <summary>
/// Execute a Lua script against the server
/// </summary>
/// <remarks>http://redis.io/commands/eval</remarks>
/// <remarks>http://redis.io/commands/eval, http://redis.io/commands/evalsha</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>
/// Execute a Lua script against the server using just the SHA1 hash
/// </summary>
/// <remarks>http://redis.io/commands/evalsha</remarks>
/// <returns>A dynamic representation of the script's result</returns>
Task<RedisResult> ScriptEvaluateAsync(byte[] hash, 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>
......
......@@ -466,6 +466,29 @@ static void WriteUnified(Stream stream, byte[] value)
}
}
internal void WriteAsHex(byte[] value)
{
var stream = outStream;
stream.WriteByte((byte)'$');
if (value == null)
{
WriteRaw(stream, -1);
} else
{
WriteRaw(stream, value.Length * 2);
for(int i = 0; i < value.Length; i++)
{
stream.WriteByte(ToHexNibble(value[i] >> 4));
stream.WriteByte(ToHexNibble(value[i] & 15));
}
stream.Write(Crlf, 0, 2);
}
}
internal static byte ToHexNibble(int value)
{
return value < 10 ? (byte)('0' + value) : (byte)('a' - 10 + value);
}
static void WriteUnified(Stream stream, byte[] prefix, byte[] value)
{
stream.WriteByte((byte)'$');
......@@ -805,6 +828,7 @@ private RawResult ReadBulkString(byte[] buffer, ref int offset, ref int count)
return RawResult.Nil;
}
private RawResult ReadLineTerminatedString(ResultType type, byte[] buffer, ref int offset, ref int count)
{
int max = offset + count - 2;
......
......@@ -758,7 +758,7 @@ public Task<long> PublishAsync(RedisChannel channel, RedisValue message, Command
}
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);
var msg = new ScriptEvalMessage(Db, flags, script, keys, values);
try
{
return ExecuteSync(msg, ResultProcessor.ScriptResult);
......@@ -769,10 +769,20 @@ public RedisResult ScriptEvaluate(string script, RedisKey[] keys = null, RedisVa
throw;
}
}
public RedisResult ScriptEvaluate(byte[] hash, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None)
{
var msg = new ScriptEvalMessage(Db, flags, hash, keys, values);
return ExecuteSync(msg, ResultProcessor.ScriptResult);
}
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);
var msg = new ScriptEvalMessage(Db, flags, script, keys, values);
return ExecuteAsync(msg, ResultProcessor.ScriptResult);
}
public Task<RedisResult> ScriptEvaluateAsync(byte[] hash, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None)
{
var msg = new ScriptEvalMessage(Db, flags, hash, keys, values);
return ExecuteAsync(msg, ResultProcessor.ScriptResult);
}
......@@ -2114,11 +2124,25 @@ private sealed class ScriptEvalMessage : Message, IMultiMessage
private readonly RedisKey[] keys;
private readonly string script;
private readonly RedisValue[] values;
private RedisValue hash;
public ScriptEvalMessage(int db, CommandFlags flags, RedisCommand command, string script, RedisKey[] keys, RedisValue[] values) : base(db, flags, command)
private byte[] asciiHash, hexHash;
public ScriptEvalMessage(int db, CommandFlags flags, string script, RedisKey[] keys, RedisValue[] values)
: this(db, flags, ResultProcessor.ScriptLoadProcessor.IsSHA1(script) ? RedisCommand.EVALSHA : RedisCommand.EVAL, script, null, keys, values)
{
if (script == null) throw new ArgumentNullException("script");
}
public ScriptEvalMessage(int db, CommandFlags flags, byte[] hash, RedisKey[] keys, RedisValue[] values)
: this(db, flags, RedisCommand.EVAL, null, hash, keys, values)
{
if (hash == null) throw new ArgumentNullException("hash");
}
private ScriptEvalMessage(int db, CommandFlags flags, RedisCommand command, string script, byte[] hexHash, RedisKey[] keys, RedisValue[] values) : base(db, flags, command)
{
this.script = script;
this.hexHash = hexHash;
if (keys == null) keys = RedisKey.EmptyArray;
if (values == null) values = RedisValue.EmptyArray;
for (int i = 0; i < keys.Length; i++)
keys[i].AssertNotNull();
this.keys = keys;
......@@ -2126,6 +2150,7 @@ public ScriptEvalMessage(int db, CommandFlags flags, RedisCommand command, strin
values[i].AssertNotNull();
this.values = values;
}
public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy)
{
int slot = ServerSelectionStrategy.NoSlot;
......@@ -2136,28 +2161,37 @@ public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy)
public IEnumerable<Message> GetMessages(PhysicalConnection connection)
{
this.hash = connection.Bridge.ServerEndPoint.GetScriptHash(script);
if (hash.IsNull)
if (script != null) // a script was provided (rather than a hash); check it is known
{
asciiHash = connection.Bridge.ServerEndPoint.GetScriptHash(script, command);
if (asciiHash == null)
{
var msg = new ScriptLoadMessage(Flags, script);
msg.SetInternalCall();
msg.SetSource(ResultProcessor.ScriptLoad, null);
yield return msg;
}
}
yield return this;
}
internal override void WriteImpl(PhysicalConnection physical)
{
if (hash.IsNull)
if(hexHash != null)
{
physical.WriteHeader(RedisCommand.EVAL, 2 + keys.Length + values.Length);
physical.Write((RedisValue)script);
physical.WriteHeader(RedisCommand.EVALSHA, 2 + keys.Length + values.Length);
physical.WriteAsHex(hexHash);
}
else
else if (asciiHash != null)
{
physical.WriteHeader(RedisCommand.EVALSHA, 2 + keys.Length + values.Length);
physical.Write(hash);
physical.Write((RedisValue)asciiHash);
}
else
{
physical.WriteHeader(RedisCommand.EVAL, 2 + keys.Length + values.Length);
physical.Write((RedisValue)script);
}
physical.Write(keys.Length);
for (int i = 0; i < keys.Length; i++)
......
......@@ -5,6 +5,7 @@
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
namespace StackExchange.Redis
{
......@@ -323,6 +324,51 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes
internal sealed class ScriptLoadProcessor : ResultProcessor<byte[]>
{
static readonly Regex sha1 = new Regex("^[0-9a-f]{40}$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
internal static bool IsSHA1(string script)
{
return script != null && sha1.IsMatch(script);
}
internal static byte[] ParseSHA1(byte[] value)
{
if (value != null && value.Length == 40)
{
var tmp = new byte[20];
int charIndex = 0;
for (int i = 0; i < tmp.Length; i++)
{
int x = FromHex((char)value[charIndex++]), y = FromHex((char)value[charIndex++]);
if (x < 0 || y < 0) return null;
tmp[i] = (byte)((x << 4) | y);
}
return tmp;
}
return null;
}
internal static byte[] ParseSHA1(string value)
{
if (value != null && value.Length == 40 && sha1.IsMatch(value))
{
var tmp = new byte[20];
int charIndex = 0;
for (int i = 0; i < tmp.Length; i++)
{
int x = FromHex(value[charIndex++]), y = FromHex(value[charIndex++]);
if (x < 0 || y < 0) return null;
tmp[i] = (byte)((x << 4) | y);
}
return tmp;
}
return null;
}
private static int FromHex(char c)
{
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
// 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)
......@@ -330,11 +376,18 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes
switch (result.Type)
{
case ResultType.BulkString:
var hash = result.GetBlob();
var asciiHash = result.GetBlob();
if (asciiHash == null || asciiHash.Length != 40) return false;
byte[] hash = null;
if (!message.IsInternalCall)
{
hash = ParseSHA1(asciiHash); // external caller wants the hex bytes, not the ascii bytes
}
var sl = message as RedisDatabase.ScriptLoadMessage;
if (sl != null)
{
connection.Bridge.ServerEndPoint.AddScript(sl.Script, hash);
connection.Bridge.ServerEndPoint.AddScript(sl.Script, asciiHash);
}
SetResult(message, hash);
return true;
......
......@@ -352,9 +352,19 @@ internal string GetProfile()
return sb.ToString();
}
internal byte[] GetScriptHash(string script)
internal byte[] GetScriptHash(string script, RedisCommand command)
{
return (byte[])knownScripts[script];
var found = (byte[])knownScripts[script];
if(found == null && command == RedisCommand.EVALSHA)
{
// the script provided is a hex sha; store and re-use the ascii for that
found = Encoding.ASCII.GetBytes(script);
lock(knownScripts)
{
knownScripts[script] = found;
}
}
return found;
}
internal string GetStormLog(RedisCommand command)
......
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