Commit 217efc0f authored by mgravell's avatar mgravell

spike to see what we'd need to do more efficient eval/execute - does not even remotely work

parent 41c6175e
......@@ -65,5 +65,6 @@ public enum CommandFlags
NoScriptCache = 512,
// 1024: used for timed-out; never user-specified, so not visible on the public API
// 2048: used to indicate to auto-recycle arrays; we may add this to the public API later
}
}
......@@ -822,6 +822,18 @@ public interface IDatabase : IRedis, IDatabaseAsync
/// <returns>A dynamic representation of the command's result</returns>
RedisResult Execute(string command, ICollection<object> args, CommandFlags flags = CommandFlags.None);
/// <summary>
/// Execute an arbitrary command against the server; this is primarily intended for
/// executing modules, but may also be used to provide access to new features that lack
/// a direct API.
/// </summary>
/// <param name="command">The command to run.</param>
/// <param name="args">The arguments to pass for the command.</param>
/// <param name="flags">The flags to use for this operation.</param>
/// <remarks>This API should be considered an advanced feature; inappropriate use can be harmful</remarks>
/// <returns>A dynamic representation of the command's result</returns>
RedisResult Execute(string command, ReadOnlyMemory<object> args, CommandFlags flags = CommandFlags.None);
/// <summary>
/// Execute a Lua script against the server.
/// </summary>
......@@ -834,6 +846,18 @@ public interface IDatabase : IRedis, IDatabaseAsync
/// <remarks>https://redis.io/commands/evalsha</remarks>
RedisResult ScriptEvaluate(string script, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None);
/// <summary>
/// Execute a Lua script against the server.
/// </summary>
/// <param name="script">The script to execute.</param>
/// <param name="keys">The keys to execute against.</param>
/// <param name="values">The values to execute against.</param>
/// <param name="flags">The flags to use for this operation.</param>
/// <returns>A dynamic representation of the script's result</returns>
/// <remarks>https://redis.io/commands/eval</remarks>
/// <remarks>https://redis.io/commands/evalsha</remarks>
RedisResult ScriptEvaluate(string script, ReadOnlyMemory<RedisKey> keys, ReadOnlyMemory<RedisValue> values, CommandFlags flags = CommandFlags.None);
/// <summary>
/// Execute a Lua script against the server using just the SHA1 hash
/// </summary>
......@@ -845,6 +869,17 @@ public interface IDatabase : IRedis, IDatabaseAsync
/// <remarks>https://redis.io/commands/evalsha</remarks>
RedisResult ScriptEvaluate(byte[] hash, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None);
/// <summary>
/// Execute a Lua script against the server using just the SHA1 hash
/// </summary>
/// <param name="hash">The hash of the script to execute.</param>
/// <param name="keys">The keys to execute against.</param>
/// <param name="values">The values to execute against.</param>
/// <param name="flags">The flags to use for this operation.</param>
/// <returns>A dynamic representation of the script's result</returns>
/// <remarks>https://redis.io/commands/evalsha</remarks>
RedisResult ScriptEvaluate(byte[] hash, ReadOnlyMemory<RedisKey> keys, ReadOnlyMemory<RedisValue> values, CommandFlags flags = CommandFlags.None);
/// <summary>
/// Execute a lua script against the server, using previously prepared script.
/// Named parameters, if any, are provided by the `parameters` object.
......
......@@ -786,6 +786,18 @@ public interface IDatabaseAsync : IRedisAsync
/// <returns>A dynamic representation of the command's result</returns>
Task<RedisResult> ExecuteAsync(string command, ICollection<object> args, CommandFlags flags = CommandFlags.None);
/// <summary>
/// Execute an arbitrary command against the server; this is primarily intended for
/// executing modules, but may also be used to provide access to new features that lack
/// a direct API.
/// </summary>
/// <param name="command">The command to run.</param>
/// <param name="args">The arguments to pass for the command.</param>
/// <param name="flags">The flags to use for this operation.</param>
/// <remarks>This API should be considered an advanced feature; inappropriate use can be harmful</remarks>
/// <returns>A dynamic representation of the command's result</returns>
Task<RedisResult> ExecuteAsync(string command, ReadOnlyMemory<object> args, CommandFlags flags = CommandFlags.None);
/// <summary>
/// Execute a Lua script against the server.
/// </summary>
......@@ -798,6 +810,18 @@ public interface IDatabaseAsync : IRedisAsync
/// <remarks>https://redis.io/commands/evalsha</remarks>
Task<RedisResult> ScriptEvaluateAsync(string script, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None);
/// <summary>
/// Execute a Lua script against the server.
/// </summary>
/// <param name="script">The script to execute.</param>
/// <param name="keys">The keys to execute against.</param>
/// <param name="values">The values to execute against.</param>
/// <param name="flags">The flags to use for this operation.</param>
/// <returns>A dynamic representation of the script's result</returns>
/// <remarks>https://redis.io/commands/eval</remarks>
/// <remarks>https://redis.io/commands/evalsha</remarks>
Task<RedisResult> ScriptEvaluateAsync(string script, ReadOnlyMemory<RedisKey> keys, ReadOnlyMemory<RedisValue> values, CommandFlags flags = CommandFlags.None);
/// <summary>
/// Execute a Lua script against the server using just the SHA1 hash
/// </summary>
......@@ -809,6 +833,17 @@ public interface IDatabaseAsync : IRedisAsync
/// <remarks>https://redis.io/commands/evalsha</remarks>
Task<RedisResult> ScriptEvaluateAsync(byte[] hash, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None);
/// <summary>
/// Execute a Lua script against the server using just the SHA1 hash
/// </summary>
/// <param name="hash">The hash of the script to execute.</param>
/// <param name="keys">The keys to execute against.</param>
/// <param name="values">The values to execute against.</param>
/// <param name="flags">The flags to use for this operation.</param>
/// <returns>A dynamic representation of the script's result</returns>
/// <remarks>https://redis.io/commands/evalsha</remarks>
Task<RedisResult> ScriptEvaluateAsync(byte[] hash, ReadOnlyMemory<RedisKey> keys, ReadOnlyMemory<RedisValue> values, CommandFlags flags = CommandFlags.None);
/// <summary>
/// Execute a lua script against the server, using previously prepared script.
/// Named parameters, if any, are provided by the `parameters` object.
......
......@@ -258,6 +258,18 @@ public partial interface IServer : IRedis
/// <returns>A dynamic representation of the command's result</returns>
RedisResult Execute(string command, ICollection<object> args, CommandFlags flags = CommandFlags.None);
/// <summary>
/// Execute an arbitrary command against the server; this is primarily intended for
/// executing modules, but may also be used to provide access to new features that lack
/// a direct API.
/// </summary>
/// <param name="command">The command to run.</param>
/// <param name="args">The arguments to pass for the command.</param>
/// <param name="flags">The flags to use for this operation.</param>
/// <remarks>This API should be considered an advanced feature; inappropriate use can be harmful</remarks>
/// <returns>A dynamic representation of the command's result</returns>
RedisResult Execute(string command, ReadOnlyMemory<object> args, CommandFlags flags = CommandFlags.None);
/// <summary>
/// Execute an arbitrary command against the server; this is primarily intended for
/// executing modules, but may also be used to provide access to new features that lack
......@@ -281,6 +293,18 @@ public partial interface IServer : IRedis
/// <returns>A dynamic representation of the command's result</returns>
Task<RedisResult> ExecuteAsync(string command, ICollection<object> args, CommandFlags flags = CommandFlags.None);
/// <summary>
/// Execute an arbitrary command against the server; this is primarily intended for
/// executing modules, but may also be used to provide access to new features that lack
/// a direct API.
/// </summary>
/// <param name="command">The command to run.</param>
/// <param name="args">The arguments to pass for the command.</param>
/// <param name="flags">The flags to use for this operation.</param>
/// <remarks>This API should be considered an advanced feature; inappropriate use can be harmful</remarks>
/// <returns>A dynamic representation of the command's result</returns>
Task<RedisResult> ExecuteAsync(string command, ReadOnlyMemory<object> args, CommandFlags flags = CommandFlags.None);
/// <summary>
/// Delete all the keys of all databases on the server.
/// </summary>
......
......@@ -377,21 +377,48 @@ public long Publish(RedisChannel channel, RedisValue message, CommandFlags flags
}
public RedisResult Execute(string command, params object[] args)
=> Inner.Execute(command, ToInner(args), CommandFlags.None);
=> Inner.Execute(command, LeaseInner(args), CommandFlags.None | Message.AutoRecycleMemories);
public RedisResult Execute(string command, ICollection<object> args, CommandFlags flags = CommandFlags.None)
=> Inner.Execute(command, ToInner(args), flags);
=> Inner.Execute(command, LeaseInnerCol(args), flags | Message.AutoRecycleMemories);
public RedisResult Execute(string command, ReadOnlyMemory<object> args, CommandFlags flags = CommandFlags.None)
=> Inner.Execute(command, LeaseInner(args.Span), flags | Message.AutoRecycleMemories);
public RedisResult ScriptEvaluate(byte[] hash, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None)
{
// TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those?
return Inner.ScriptEvaluate(hash, ToInner(keys), values, flags);
using (var inner = LeaseInner(keys))
{
return Inner.ScriptEvaluate(hash, inner, values, flags);
}
}
public RedisResult ScriptEvaluate(string script, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None)
{
// TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those?
return Inner.ScriptEvaluate(script, ToInner(keys), values, flags);
using (var inner = LeaseInner(keys))
{
return Inner.ScriptEvaluate(script, inner, values, flags);
}
}
public RedisResult ScriptEvaluate(string script, ReadOnlyMemory<RedisKey> keys, ReadOnlyMemory<RedisValue> values, CommandFlags flags = CommandFlags.None)
{
// TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those?
using (var inner = LeaseInner(keys.Span))
{
return Inner.ScriptEvaluate(script, inner, values, flags);
}
}
public RedisResult ScriptEvaluate(byte[] hash, ReadOnlyMemory<RedisKey> keys, ReadOnlyMemory<RedisValue> values, CommandFlags flags = CommandFlags.None)
{
// TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those?
using (var inner = LeaseInner(keys.Span))
{
return Inner.ScriptEvaluate(hash, inner, values, flags);
}
}
public RedisResult ScriptEvaluate(LuaScript script, object parameters = null, CommandFlags flags = CommandFlags.None)
......
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
namespace StackExchange.Redis.KeyspaceIsolation
......@@ -359,21 +362,48 @@ public Task<long> PublishAsync(RedisChannel channel, RedisValue message, Command
}
public Task<RedisResult> ExecuteAsync(string command, params object[] args)
=> Inner.ExecuteAsync(command, ToInner(args), CommandFlags.None);
=> Inner.ExecuteAsync(command, LeaseInner(args), CommandFlags.None | Message.AutoRecycleMemories);
public Task<RedisResult> ExecuteAsync(string command, ICollection<object> args, CommandFlags flags = CommandFlags.None)
=> Inner.ExecuteAsync(command, ToInner(args), flags);
=> Inner.ExecuteAsync(command, LeaseInnerCol(args), flags | Message.AutoRecycleMemories);
public Task<RedisResult> ScriptEvaluateAsync(byte[] hash, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None)
public Task<RedisResult> ExecuteAsync(string command, ReadOnlyMemory<object> args, CommandFlags flags = CommandFlags.None)
=> Inner.ExecuteAsync(command, LeaseInner(args.Span), flags | Message.AutoRecycleMemories);
public async Task<RedisResult> ScriptEvaluateAsync(byte[] hash, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None)
{
// TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those?
return Inner.ScriptEvaluateAsync(hash, ToInner(keys), values, flags);
using (var inner = LeaseInner(keys))
{
return await Inner.ScriptEvaluateAsync(hash, inner, values, flags);
}
}
public async Task<RedisResult> ScriptEvaluateAsync(string script, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None)
{
using (var inner = LeaseInner(keys))
{
// TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those?
return await Inner.ScriptEvaluateAsync(script, inner, values, flags);
}
}
public Task<RedisResult> ScriptEvaluateAsync(string script, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None)
public async Task<RedisResult> ScriptEvaluateAsync(byte[] hash, ReadOnlyMemory<RedisKey> keys, ReadOnlyMemory<RedisValue> values, CommandFlags flags = CommandFlags.None)
{
// TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those?
return Inner.ScriptEvaluateAsync(script, ToInner(keys), values, flags);
using (var inner = LeaseInner(keys.Span))
{
return await Inner.ScriptEvaluateAsync(hash, inner, values, flags);
}
}
public async Task<RedisResult> ScriptEvaluateAsync(string script, ReadOnlyMemory<RedisKey> keys, ReadOnlyMemory<RedisValue> values, CommandFlags flags = CommandFlags.None)
{
using (var inner = LeaseInner(keys.Span))
{
// TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those?
return await Inner.ScriptEvaluateAsync(script, inner, values, flags);
}
}
public Task<RedisResult> ScriptEvaluateAsync(LuaScript script, object parameters = null, CommandFlags flags = CommandFlags.None)
......@@ -862,34 +892,129 @@ protected RedisKey ToInnerOrDefault(RedisKey outer)
}
}
internal readonly struct ValueLease<T> : IDisposable
{
private readonly T[] _array;
private readonly int _length;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal ValueLease(T[] array, int length)
{
_array = array;
_length = length;
Debug.Assert(array != null);
Debug.Assert(length >= array.Length);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static ValueLease<T> Rent(int size)
=> new ValueLease<T>(ArrayPool<T>.Shared.Rent(size), size);
public Memory<T> Memory
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new Memory<T>(_array, 0, _length);
}
public Span<T> Span
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new Span<T>(_array, 0, _length);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator Memory<T> (ValueLease<T> lease) => lease.Memory;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator ReadOnlyMemory<T>(ValueLease<T> lease) => lease.Memory;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator Span<T>(ValueLease<T> lease) => lease.Span;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator ReadOnlySpan<T>(ValueLease<T> lease) => lease.Span;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Dispose()
{
if (_length != 0)
ArrayPool<T>.Shared.Return(_array);
}
}
protected ValueLease<object> LeaseInnerCol(ICollection<object> args)
{
if (args == null || args.Count == 0) return default;
var withPrefix = ArrayPool<object>.Shared.Rent(args.Count);
AddPrefix(args, withPrefix);
return new ValueLease<object>(withPrefix, args.Count);
}
protected ValueLease<object> LeaseInner(ReadOnlySpan<object> args)
{
if (args.IsEmpty) return default;
var withPrefix = ArrayPool<object>.Shared.Rent(args.Length);
AddPrefix(args, withPrefix);
return new ValueLease<object>(withPrefix, args.Length);
}
protected ValueLease<RedisKey> LeaseInner(ReadOnlySpan<RedisKey> args)
{
if (args.IsEmpty) return default;
var withPrefix = ArrayPool<RedisKey>.Shared.Rent(args.Length);
for(int i = 0; i < args.Length; i++)
{
withPrefix[i] = ToInner(args[i]);
}
return new ValueLease<RedisKey>(withPrefix, args.Length);
}
protected ICollection<object> ToInner(ICollection<object> args)
{
if (args?.Any(x => x is RedisKey || x is RedisChannel) == true)
{
var withPrefix = new object[args.Count];
int i = 0;
foreach(var oldArg in args)
{
object newArg;
if (oldArg is RedisKey key)
{
newArg = ToInner(key);
}
else if (oldArg is RedisChannel channel)
{
newArg = ToInner(channel);
}
else
{
newArg = oldArg;
}
withPrefix[i++] = newArg;
}
AddPrefix(args, withPrefix);
args = withPrefix;
}
return args;
}
private void AddPrefix(ReadOnlySpan<object> from, object[] to)
{
int i = 0;
foreach (var oldArg in from)
{
object newArg;
if (oldArg is RedisKey key)
{
newArg = ToInner(key);
}
else if (oldArg is RedisChannel channel)
{
newArg = ToInner(channel);
}
else
{
newArg = oldArg;
}
to[i++] = newArg;
}
}
private void AddPrefix(ICollection<object> from, object[] to)
{
int i = 0;
foreach (var oldArg in from)
{
object newArg;
if (oldArg is RedisKey key)
{
newArg = ToInner(key);
}
else if (oldArg is RedisChannel channel)
{
newArg = ToInner(channel);
}
else
{
newArg = oldArg;
}
to[i++] = newArg;
}
}
protected RedisKey[] ToInner(RedisKey[] outer)
{
if (outer == null || outer.Length == 0)
......
......@@ -95,7 +95,7 @@ public static LuaScript Prepare(string script)
return ret;
}
internal void ExtractParameters(object ps, RedisKey? keyPrefix, out RedisKey[] keys, out RedisValue[] args)
internal void ExtractParameters(object ps, RedisKey? keyPrefix, out ReadOnlyMemory<RedisKey> keys, out ReadOnlyMemory<RedisValue> args)
{
if (HasArguments)
{
......@@ -127,12 +127,12 @@ internal void ExtractParameters(object ps, RedisKey? keyPrefix, out RedisKey[] k
var mapped = mapper(ps, keyPrefix);
keys = mapped.Keys;
args = mapped.Arguments;
args = mapped.Args;
}
else
{
keys = null;
args = null;
keys = default;
args = default;
}
}
......@@ -145,8 +145,8 @@ internal void ExtractParameters(object ps, RedisKey? keyPrefix, out RedisKey[] k
/// <param name="flags">The command flags to use.</param>
public RedisResult Evaluate(IDatabase db, object ps = null, RedisKey? withKeyPrefix = null, CommandFlags flags = CommandFlags.None)
{
ExtractParameters(ps, withKeyPrefix, out RedisKey[] keys, out RedisValue[] args);
return db.ScriptEvaluate(ExecutableScript, keys, args, flags);
ExtractParameters(ps, withKeyPrefix, out var keys, out var args);
return db.ScriptEvaluate(ExecutableScript, keys, args, flags | Message.AutoRecycleMemories);
}
/// <summary>
......@@ -158,8 +158,8 @@ public RedisResult Evaluate(IDatabase db, object ps = null, RedisKey? withKeyPre
/// <param name="flags">The command flags to use.</param>
public Task<RedisResult> EvaluateAsync(IDatabaseAsync db, object ps = null, RedisKey? withKeyPrefix = null, CommandFlags flags = CommandFlags.None)
{
ExtractParameters(ps, withKeyPrefix, out RedisKey[] keys, out RedisValue[] args);
return db.ScriptEvaluateAsync(ExecutableScript, keys, args, flags);
ExtractParameters(ps, withKeyPrefix, out var keys, out var args);
return db.ScriptEvaluateAsync(ExecutableScript, keys, args, flags | Message.AutoRecycleMemories);
}
/// <summary>
......@@ -263,7 +263,7 @@ internal LoadedLuaScript(LuaScript original, byte[] hash)
/// <param name="flags">The command flags to use.</param>
public RedisResult Evaluate(IDatabase db, object ps = null, RedisKey? withKeyPrefix = null, CommandFlags flags = CommandFlags.None)
{
Original.ExtractParameters(ps, withKeyPrefix, out RedisKey[] keys, out RedisValue[] args);
Original.ExtractParameters(ps, withKeyPrefix, out var keys, out var args);
return db.ScriptEvaluate(Hash, keys, args, flags);
}
......@@ -280,7 +280,7 @@ public RedisResult Evaluate(IDatabase db, object ps = null, RedisKey? withKeyPre
/// <param name="flags">The command flags to use.</param>
public Task<RedisResult> EvaluateAsync(IDatabaseAsync db, object ps = null, RedisKey? withKeyPrefix = null, CommandFlags flags = CommandFlags.None)
{
Original.ExtractParameters(ps, withKeyPrefix, out RedisKey[] keys, out RedisValue[] args);
Original.ExtractParameters(ps, withKeyPrefix, out var keys, out var args);
return db.ScriptEvaluateAsync(Hash, keys, args, flags);
}
}
......
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
......@@ -52,6 +54,12 @@ protected override void WriteImpl(PhysicalConnection physical)
internal abstract class Message : ICompletable
{
protected static void Recycle<T>(in ReadOnlyMemory<T> memory)
{
if (!memory.IsEmpty && MemoryMarshal.TryGetArray(memory, out var segment))
ArrayPool<T>.Shared.Return(segment.Array);
}
public virtual void RecycleMemories() { }
public readonly int Db;
#if DEBUG
......@@ -67,7 +75,9 @@ internal void SetBacklogState(int position, PhysicalConnection physical)
#endif
}
internal const CommandFlags InternalCallFlag = (CommandFlags)128;
internal const CommandFlags
InternalCallFlag = (CommandFlags)128,
AutoRecycleMemories = (CommandFlags)2048;
protected RedisCommand command;
......
This diff is collapsed.
......@@ -849,17 +849,26 @@ public Task SentinelFailoverAsync(string serviceName, CommandFlags flags = Comma
#endregion
public RedisResult Execute(string command, params object[] args) => Execute(command, args, CommandFlags.None);
public RedisResult Execute(string command, params object[] args) => Execute(command, (ReadOnlyMemory<object>)args, CommandFlags.None);
public RedisResult Execute(string command, ICollection<object> args, CommandFlags flags = CommandFlags.None)
{
var cpy = RedisDatabase.LeasedCopy(args, ref flags);
return Execute(command, cpy, flags);
}
public RedisResult Execute(string command, ReadOnlyMemory<object> args, CommandFlags flags = CommandFlags.None)
{
var msg = new RedisDatabase.ExecuteMessage(multiplexer?.CommandMap, -1, flags, command, args);
return ExecuteSync(msg, ResultProcessor.ScriptResult);
}
public Task<RedisResult> ExecuteAsync(string command, params object[] args) => ExecuteAsync(command, args, CommandFlags.None);
public Task<RedisResult> ExecuteAsync(string command, params object[] args) => ExecuteAsync(command, (ReadOnlyMemory<object>)args, CommandFlags.None);
public Task<RedisResult> ExecuteAsync(string command, ICollection<object> args, CommandFlags flags = CommandFlags.None)
{
var cpy = RedisDatabase.LeasedCopy(args, ref flags);
return ExecuteAsync(command, cpy, flags);
}
public Task<RedisResult> ExecuteAsync(string command, ReadOnlyMemory<object> args, CommandFlags flags = CommandFlags.None)
{
var msg = new RedisDatabase.ExecuteMessage(multiplexer?.CommandMap, -1, flags, command, args);
return ExecuteAsync(msg, ResultProcessor.ScriptResult);
......
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
......@@ -12,15 +14,21 @@ internal static class ScriptParameterMapper
{
public readonly struct ScriptParameters
{
public readonly RedisKey[] Keys;
public readonly RedisValue[] Arguments;
public readonly RedisKey[] KeyArray;
public readonly RedisValue[] ArgArray;
public readonly int KeyCount;
public readonly int ArgCount;
public static readonly ConstructorInfo Cons = typeof(ScriptParameters).GetConstructor(new[] { typeof(RedisKey[]), typeof(RedisValue[]) });
public ScriptParameters(RedisKey[] keys, RedisValue[] args)
public static readonly ConstructorInfo Constructor = typeof(ScriptParameters).GetConstructor(new[] { typeof(int), typeof(int) });
public ScriptParameters(int keyCount, int argCount)
{
Keys = keys;
Arguments = args;
KeyCount = keyCount;
KeyArray = keyCount == 0 ? Array.Empty<RedisKey>() : ArrayPool<RedisKey>.Shared.Rent(keyCount);
ArgCount = argCount;
ArgArray = argCount == 0 ? Array.Empty<RedisValue>() : ArrayPool<RedisValue>.Shared.Rent(argCount);
}
public ReadOnlyMemory<RedisKey> Keys => new ReadOnlyMemory<RedisKey>(KeyArray, 0, KeyCount);
public ReadOnlyMemory<RedisValue> Args => new ReadOnlyMemory<RedisValue>(ArgArray, 0, ArgCount);
}
private static readonly Regex ParameterExtractor = new Regex(@"@(?<paramName> ([a-z]|_) ([a-z]|_|\d)*)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
......@@ -187,6 +195,9 @@ public static bool IsValidParameterHash(Type t, LuaScript script, out string mis
return true;
}
private static readonly MethodInfo s_prepend = typeof(RedisKey).GetMethod(nameof(RedisKey.Prepend), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
private static readonly MethodInfo s_asRedisValue = typeof(RedisKey).GetMethod(nameof(RedisKey.AsRedisValue), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
/// <summary>
/// <para>Creates a Func that extracts parameters from the given type for use by a LuaScript.</para>
/// <para>
......@@ -241,65 +252,89 @@ Expression GetMember(Expression root, MemberInfo member)
args.Add(member);
}
// parameters
var objUntyped = Expression.Parameter(typeof(object), "obj");
var objTyped = Expression.Convert(objUntyped, t);
var keyPrefix = Expression.Parameter(typeof(RedisKey?), "keyPrefix");
Expression keysResult, valuesResult;
MethodInfo asRedisValue = null;
Expression[] keysResultArr = null;
if (keys.Count == 0)
{
// if there are no keys, don't allocate
keysResult = Expression.Constant(null, typeof(RedisKey[]));
}
else
// locals
var operations = new List<Expression>();
var objTyped = Expression.Variable(t);
var result = Expression.Variable(typeof(ScriptParameters));
var keyArr = Expression.Variable(typeof(RedisKey[]));
var argArr = Expression.Variable(typeof(RedisValue[]));
var needsPrefix = Expression.Variable(typeof(bool));
var prefixValue = Expression.Variable(typeof(RedisKey));
// objTyped = (t)objUntyped
operations.Add(Expression.Assign(objTyped, Expression.Convert(objUntyped, t)));
// result = new ScriptParameters(keys.Count, args.Count)
operations.Add(Expression.Assign(result, Expression.New(ScriptParameters.Constructor, Expression.Constant(keys.Count), Expression.Constant(args.Count))));
if (keys.Count != 0)
{
// keyArr = result.KeyArray;
operations.Add(Expression.Assign(keyArr, Expression.PropertyOrField(result, nameof(ScriptParameters.KeyArray))));
// needsPrefix = keyPrefix.HasValue
// prefixValue = prefixValue.GetValueOrDefault()
operations.Add(Expression.Assign(needsPrefix, Expression.PropertyOrField(keyPrefix, nameof(Nullable<RedisKey>.HasValue))));
operations.Add(Expression.Assign(prefixValue, Expression.Call(keyPrefix, nameof(Nullable<RedisKey>.GetValueOrDefault), null, null)));
var needsKeyPrefix = Expression.Property(keyPrefix, nameof(Nullable<RedisKey>.HasValue));
var keyPrefixValueArr = new[] { Expression.Call(keyPrefix,
nameof(Nullable<RedisKey>.GetValueOrDefault), null, null) };
var prepend = typeof(RedisKey).GetMethod(nameof(RedisKey.Prepend),
BindingFlags.Public | BindingFlags.Instance);
asRedisValue = typeof(RedisKey).GetMethod(nameof(RedisKey.AsRedisValue),
BindingFlags.NonPublic | BindingFlags.Instance);
keysResultArr = new Expression[keys.Count];
for (int i = 0; i < keysResultArr.Length; i++)
var prefixValueAsArgs = new[] { prefixValue };
int i = 0;
foreach(var key in keys)
{
var member = GetMember(objTyped, keys[i]);
keysResultArr[i] = Expression.Condition(needsKeyPrefix,
Expression.Call(member, prepend, keyPrefixValueArr),
member);
// keyArr[i++] = needsKeyPrefix ? objTyped.{member} : objTyped.{Member}.Prepend(prefixValue)
var member = GetMember(objTyped, key);
operations.Add(Expression.Assign(Expression.ArrayAccess(keyArr, Expression.Constant(i++)),
Expression.Condition(needsKeyPrefix, Expression.Call(member, s_prepend, prefixValueAsArgs), member)));
}
keysResult = Expression.NewArrayInit(typeof(RedisKey), keysResultArr);
}
if (args.Count == 0)
{
// if there are no args, don't allocate
valuesResult = Expression.Constant(null, typeof(RedisValue[]));
}
else
if (args.Count != 0)
{
valuesResult = Expression.NewArrayInit(typeof(RedisValue), args.Select(arg =>
// argArr = result.ArgsArray;
operations.Add(Expression.Assign(argArr, Expression.PropertyOrField(result, nameof(ScriptParameters.ArgArray))));
int i = 0;
foreach (var arg in args)
{
Expression rhs;
var member = GetMember(objTyped, arg);
if (member.Type == typeof(RedisValue)) return member; // pass-thru
if (member.Type == typeof(RedisKey))
{ // need to apply prefix (note we can re-use the body from earlier)
var val = keysResultArr[keys.IndexOf(arg)];
return Expression.Call(val, asRedisValue);
if (member.Type == typeof(RedisValue))
{
// ... = objTyped.{member}
rhs = member; // pass-thru
}
// otherwise: use the conversion operator
var conversion = _conversionOperators[member.Type];
return Expression.Call(conversion, member);
}));
else if (member.Type == typeof(RedisKey))
{
int keyIndex = keys.IndexOf(arg);
Debug.Assert(keyIndex >= 0);
// ... = keys[{index}].AsRedisValue()
rhs = Expression.Call(Expression.ArrayAccess(keyArr, Expression.Constant(keyIndex)), s_asRedisValue);
}
else
{
// ... = (SomeConversion)objTyped.{member}
var conversion = _conversionOperators[member.Type];
rhs = Expression.Call(conversion, member);
}
// argArr[i++] = ...
operations.Add(Expression.Assign(Expression.ArrayAccess(argArr, Expression.Constant(i++)), rhs));
}
}
operations.Add(result); // final operation: return result
var body = Expression.Lambda<Func<object, RedisKey?, ScriptParameters>>(
Expression.New(ScriptParameters.Cons, keysResult, valuesResult),
objUntyped, keyPrefix);
Expression.Block(
typeof(ScriptParameters), // return type of the block
new ParameterExpression[] { objTyped, result, keyArr, argArr, needsPrefix, prefixValue }, // locals scoped by the block
operations), // the operations to perform
objUntyped, keyPrefix); // parameters to the lambda
return body.Compile();
}
}
......
......@@ -164,7 +164,7 @@ public void TestIdentity()
public void IntentionalWrongServer()
{
string StringGet(IServer server, RedisKey key, CommandFlags flags = CommandFlags.None)
=> (string)server.Execute("GET", new object[] { key }, flags);
=> (string)server.Execute("GET", (ReadOnlyMemory<object>)new object[] { key }, flags);
using (var conn = Create())
{
......
......@@ -603,10 +603,9 @@ public void LuaScriptWithKeys()
Assert.Equal(123, (int)val);
// no super clean way to extract this; so just abuse InternalsVisibleTo
script.ExtractParameters(p, null, out RedisKey[] keys, out RedisValue[] args);
Assert.NotNull(keys);
Assert.Single(keys);
Assert.Equal(key, keys[0]);
script.ExtractParameters(p, null, out var keys, out var args);
Assert.Equal(1, keys.Length);
Assert.Equal(key, keys.Span[0]);
}
}
......@@ -718,10 +717,9 @@ public void LoadedLuaScriptWithKeys()
Assert.Equal(123, (int)val);
// no super clean way to extract this; so just abuse InternalsVisibleTo
prepared.Original.ExtractParameters(p, null, out RedisKey[] keys, out RedisValue[] args);
Assert.NotNull(keys);
Assert.Single(keys);
Assert.Equal(key, keys[0]);
prepared.Original.ExtractParameters(p, null, out var keys, out var args);
Assert.Equal(1, keys.Length);
Assert.Equal(key, keys.Span[0]);
}
}
......@@ -821,13 +819,12 @@ public void LuaScriptPrefixedKeys()
var p = new { key = (RedisKey)key, value = "hello" };
// no super clean way to extract this; so just abuse InternalsVisibleTo
prepared.ExtractParameters(p, "prefix-", out RedisKey[] keys, out RedisValue[] args);
Assert.NotNull(keys);
Assert.Single(keys);
Assert.Equal("prefix-" + key, keys[0]);
prepared.ExtractParameters(p, "prefix-", out var keys, out var args);
Assert.Equal(1, keys.Length);
Assert.Equal("prefix-" + key, keys.Span[0]);
Assert.Equal(2, args.Length);
Assert.Equal("prefix-" + key, args[0]);
Assert.Equal("hello", args[1]);
Assert.Equal("prefix-" + key, args.Span[0]);
Assert.Equal("hello", args.Span[1]);
}
[Fact]
......
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