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;
using System.Diagnostics; using System.Diagnostics;
using NUnit.Framework; using NUnit.Framework;
using System.Linq;
namespace StackExchange.Redis.Tests namespace StackExchange.Redis.Tests
{ {
...@@ -140,5 +141,33 @@ public void CompareScriptToDirect() ...@@ -140,5 +141,33 @@ public void CompareScriptToDirect()
directTime.TotalMilliseconds); 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 ...@@ -433,10 +433,17 @@ public interface IDatabase : IRedis, IDatabaseAsync
/// <summary> /// <summary>
/// Execute a Lua script against the server /// Execute a Lua script against the server
/// </summary> /// </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> /// <returns>A dynamic representation of the script's result</returns>
RedisResult ScriptEvaluate(string script, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None); 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> /// <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. /// 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> /// </summary>
......
...@@ -414,10 +414,17 @@ public interface IDatabaseAsync : IRedisAsync ...@@ -414,10 +414,17 @@ public interface IDatabaseAsync : IRedisAsync
/// <summary> /// <summary>
/// Execute a Lua script against the server /// Execute a Lua script against the server
/// </summary> /// </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> /// <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); 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> /// <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. /// 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> /// </summary>
......
...@@ -466,6 +466,29 @@ static void WriteUnified(Stream stream, byte[] value) ...@@ -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) static void WriteUnified(Stream stream, byte[] prefix, byte[] value)
{ {
stream.WriteByte((byte)'$'); stream.WriteByte((byte)'$');
...@@ -805,6 +828,7 @@ private RawResult ReadBulkString(byte[] buffer, ref int offset, ref int count) ...@@ -805,6 +828,7 @@ private RawResult ReadBulkString(byte[] buffer, ref int offset, ref int count)
return RawResult.Nil; return RawResult.Nil;
} }
private RawResult ReadLineTerminatedString(ResultType type, byte[] buffer, ref int offset, ref int count) private RawResult ReadLineTerminatedString(ResultType type, byte[] buffer, ref int offset, ref int count)
{ {
int max = offset + count - 2; int max = offset + count - 2;
......
...@@ -758,7 +758,7 @@ public Task<long> PublishAsync(RedisChannel channel, RedisValue message, Command ...@@ -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) 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 try
{ {
return ExecuteSync(msg, ResultProcessor.ScriptResult); return ExecuteSync(msg, ResultProcessor.ScriptResult);
...@@ -769,10 +769,20 @@ public RedisResult ScriptEvaluate(string script, RedisKey[] keys = null, RedisVa ...@@ -769,10 +769,20 @@ public RedisResult ScriptEvaluate(string script, RedisKey[] keys = null, RedisVa
throw; 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) 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); return ExecuteAsync(msg, ResultProcessor.ScriptResult);
} }
...@@ -2114,11 +2124,25 @@ private sealed class ScriptEvalMessage : Message, IMultiMessage ...@@ -2114,11 +2124,25 @@ private sealed class ScriptEvalMessage : Message, IMultiMessage
private readonly RedisKey[] keys; private readonly RedisKey[] keys;
private readonly string script; private readonly string script;
private readonly RedisValue[] values; private readonly RedisValue[] values;
private RedisValue hash; private byte[] asciiHash, hexHash;
public ScriptEvalMessage(int db, CommandFlags flags, RedisCommand command, string script, RedisKey[] keys, RedisValue[] values) : base(db, flags, command) 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"); 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.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++) for (int i = 0; i < keys.Length; i++)
keys[i].AssertNotNull(); keys[i].AssertNotNull();
this.keys = keys; this.keys = keys;
...@@ -2126,6 +2150,7 @@ public ScriptEvalMessage(int db, CommandFlags flags, RedisCommand command, strin ...@@ -2126,6 +2150,7 @@ public ScriptEvalMessage(int db, CommandFlags flags, RedisCommand command, strin
values[i].AssertNotNull(); values[i].AssertNotNull();
this.values = values; this.values = values;
} }
public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy)
{ {
int slot = ServerSelectionStrategy.NoSlot; int slot = ServerSelectionStrategy.NoSlot;
...@@ -2136,28 +2161,37 @@ public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) ...@@ -2136,28 +2161,37 @@ public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy)
public IEnumerable<Message> GetMessages(PhysicalConnection connection) public IEnumerable<Message> GetMessages(PhysicalConnection connection)
{ {
this.hash = connection.Bridge.ServerEndPoint.GetScriptHash(script); if (script != null) // a script was provided (rather than a hash); check it is known
if (hash.IsNull)
{ {
var msg = new ScriptLoadMessage(Flags, script); asciiHash = connection.Bridge.ServerEndPoint.GetScriptHash(script, command);
msg.SetInternalCall();
msg.SetSource(ResultProcessor.ScriptLoad, null); if (asciiHash == null)
yield return msg; {
var msg = new ScriptLoadMessage(Flags, script);
msg.SetInternalCall();
msg.SetSource(ResultProcessor.ScriptLoad, null);
yield return msg;
}
} }
yield return this; yield return this;
} }
internal override void WriteImpl(PhysicalConnection physical) internal override void WriteImpl(PhysicalConnection physical)
{ {
if (hash.IsNull) if(hexHash != null)
{ {
physical.WriteHeader(RedisCommand.EVAL, 2 + keys.Length + values.Length); physical.WriteHeader(RedisCommand.EVALSHA, 2 + keys.Length + values.Length);
physical.Write((RedisValue)script); physical.WriteAsHex(hexHash);
} }
else else if (asciiHash != null)
{ {
physical.WriteHeader(RedisCommand.EVALSHA, 2 + keys.Length + values.Length); 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); physical.Write(keys.Length);
for (int i = 0; i < keys.Length; i++) for (int i = 0; i < keys.Length; i++)
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
namespace StackExchange.Redis namespace StackExchange.Redis
{ {
...@@ -323,6 +324,51 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes ...@@ -323,6 +324,51 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes
internal sealed class ScriptLoadProcessor : ResultProcessor<byte[]> 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 // note that top-level error messages still get handled by SetResult, but nested errors
// (is that a thing?) will be wrapped in the RedisResult // (is that a thing?) will be wrapped in the RedisResult
protected override bool SetResultCore(PhysicalConnection connection, Message message, RawResult result) protected override bool SetResultCore(PhysicalConnection connection, Message message, RawResult result)
...@@ -330,11 +376,18 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes ...@@ -330,11 +376,18 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes
switch (result.Type) switch (result.Type)
{ {
case ResultType.BulkString: 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; var sl = message as RedisDatabase.ScriptLoadMessage;
if (sl != null) if (sl != null)
{ {
connection.Bridge.ServerEndPoint.AddScript(sl.Script, hash); connection.Bridge.ServerEndPoint.AddScript(sl.Script, asciiHash);
} }
SetResult(message, hash); SetResult(message, hash);
return true; return true;
......
...@@ -352,9 +352,19 @@ internal string GetProfile() ...@@ -352,9 +352,19 @@ internal string GetProfile()
return sb.ToString(); 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) 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