Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API suggestion: add ROM for Lua+exec #2346

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
60 changes: 60 additions & 0 deletions src/StackExchange.Redis/Interfaces/IDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1264,6 +1264,36 @@ public interface IDatabase : IRedis, IDatabaseAsync
/// </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="key">The key 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>
/// <seealso href="https://redis.io/commands/eval"/>,
/// <seealso href="https://redis.io/commands/evalsha"/>
/// </remarks>
[System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Not ambiguous due to type delta")]
RedisResult ScriptEvaluate(string script, RedisKey key, ReadOnlyMemory<RedisValue> values = default, 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>
/// <seealso href="https://redis.io/commands/eval"/>,
/// <seealso href="https://redis.io/commands/evalsha"/>
/// </remarks>
[System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Not ambiguous due to type delta")]
RedisResult ScriptEvaluate(string script, ReadOnlyMemory<RedisKey> keys, ReadOnlyMemory<RedisValue> values = default, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Execute a Lua script against the server using just the SHA1 hash.
/// </summary>
Expand Down Expand Up @@ -1316,6 +1346,36 @@ public interface IDatabase : IRedis, IDatabaseAsync
/// </remarks>
RedisResult ScriptEvaluateReadOnly(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Read-only variant of the EVAL command that cannot execute commands that modify data, Execute a Lua script against the server.
/// </summary>
/// <param name="script">The script to execute.</param>
/// <param name="key">The key 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>
/// <seealso href="https://redis.io/commands/eval_ro"/>,
/// <seealso href="https://redis.io/commands/evalsha_ro"/>
/// </remarks>
[System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Not ambiguous due to type delta")]
RedisResult ScriptEvaluateReadOnly(string script, RedisKey key, ReadOnlyMemory<RedisValue> values = default, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Read-only variant of the EVAL command that cannot execute commands that modify data, 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>
/// <seealso href="https://redis.io/commands/eval_ro"/>,
/// <seealso href="https://redis.io/commands/evalsha_ro"/>
/// </remarks>
[System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Not ambiguous due to type delta")]
RedisResult ScriptEvaluateReadOnly(string script, ReadOnlyMemory<RedisKey> keys, ReadOnlyMemory<RedisValue> values = default, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Read-only variant of the EVALSHA command that cannot execute commands that modify data, Execute a Lua script against the server using just the SHA1 hash.
/// </summary>
Expand Down
60 changes: 60 additions & 0 deletions src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1240,6 +1240,36 @@ public interface IDatabaseAsync : IRedisAsync
/// </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="key">The key 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>
/// <seealso href="https://redis.io/commands/eval"/>,
/// <seealso href="https://redis.io/commands/evalsha"/>
/// </remarks>
[System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Not ambiguous due to type delta")]
Task<RedisResult> ScriptEvaluateAsync(string script, RedisKey key, ReadOnlyMemory<RedisValue> values = default, 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>
/// <seealso href="https://redis.io/commands/eval"/>,
/// <seealso href="https://redis.io/commands/evalsha"/>
/// </remarks>
[System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Not ambiguous due to type delta")]
Task<RedisResult> ScriptEvaluateAsync(string script, ReadOnlyMemory<RedisKey> keys, ReadOnlyMemory<RedisValue> values = default, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Execute a Lua script against the server using just the SHA1 hash.
/// </summary>
Expand Down Expand Up @@ -1303,6 +1333,36 @@ public interface IDatabaseAsync : IRedisAsync
/// <remarks><seealso href="https://redis.io/commands/evalsha_ro"/></remarks>
Task<RedisResult> ScriptEvaluateReadOnlyAsync(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Read-only variant of the EVAL command that cannot execute commands that modify data, Execute a Lua script against the server.
/// </summary>
/// <param name="script">The script to execute.</param>
/// <param name="key">The key 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>
/// <seealso href="https://redis.io/commands/eval_ro"/>,
/// <seealso href="https://redis.io/commands/evalsha_ro"/>
/// </remarks>
[System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Not ambiguous due to type delta")]
Task<RedisResult> ScriptEvaluateReadOnlyAsync(string script, RedisKey key, ReadOnlyMemory<RedisValue> values = default, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Read-only variant of the EVAL command that cannot execute commands that modify data, 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>
/// <seealso href="https://redis.io/commands/eval_ro"/>,
/// <seealso href="https://redis.io/commands/evalsha_ro"/>
/// </remarks>
[System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Not ambiguous due to type delta")]
Task<RedisResult> ScriptEvaluateReadOnlyAsync(string script, ReadOnlyMemory<RedisKey> keys, ReadOnlyMemory<RedisValue> values = default, 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.
Expand Down
96 changes: 86 additions & 10 deletions src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
Expand Down Expand Up @@ -332,9 +333,31 @@ internal KeyPrefixed(TInner inner, byte[] keyPrefix)
// TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those?
Inner.ScriptEvaluateAsync(hash, ToInner(keys), values, flags);

public Task<RedisResult> ScriptEvaluateAsync(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?
Inner.ScriptEvaluateAsync(script, ToInner(keys), values, flags);
public Task<RedisResult> ScriptEvaluateAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None)
{ // note we may end up using the memory overloads to enable better pooling etc usage
if (keys is null || keys.Length == 0) return Inner.ScriptEvaluateAsync(script, keys, values, flags);
if (keys.Length == 1) return Inner.ScriptEvaluateAsync(script, ToInner(keys[0]), values, flags);
if ((flags & CommandFlags.FireAndForget) != 0) return Inner.ScriptEvaluateAsync(script, ToInnerCopy(keys), values, flags);
return Lease_ScriptEvaluateAsync(script, keys, values, flags);
}

public Task<RedisResult> ScriptEvaluateAsync(string script, RedisKey key, ReadOnlyMemory<RedisValue> values = default, CommandFlags flags = CommandFlags.None) =>
Inner.ScriptEvaluateAsync(script, ToInner(key), values, flags);

public Task<RedisResult> ScriptEvaluateAsync(string script, ReadOnlyMemory<RedisKey> keys, ReadOnlyMemory<RedisValue> values = default, CommandFlags flags = CommandFlags.None)
{
if (keys.Length == 0) return Inner.ScriptEvaluateAsync(script, keys, values, flags);
if (keys.Length == 1) return Inner.ScriptEvaluateAsync(script, ToInner(keys.Span[0]), values, flags);
if ((flags & CommandFlags.FireAndForget) != 0) return Inner.ScriptEvaluateAsync(script, ToInnerCopy(keys), values, flags);
return Lease_ScriptEvaluateAsync(script, keys, values, flags);
}

private async Task<RedisResult> Lease_ScriptEvaluateAsync(string script, ReadOnlyMemory<RedisKey> keys, ReadOnlyMemory<RedisValue> values, CommandFlags flags = CommandFlags.None)
{
var result = await Inner.ScriptEvaluateAsync(script, ToInnerLease(keys, out var lease), values, flags);
ArrayPool<RedisKey>.Shared.Return(lease); // happy to only recycle on success
return result;
}

public Task<RedisResult> ScriptEvaluateAsync(LuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None) =>
// TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those?
Expand All @@ -346,11 +369,61 @@ internal KeyPrefixed(TInner inner, byte[] keyPrefix)

public Task<RedisResult> ScriptEvaluateReadOnlyAsync(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?
Inner.ScriptEvaluateAsync(hash, ToInner(keys), values, flags);
Inner.ScriptEvaluateReadOnlyAsync(hash, ToInner(keys), values, flags);

public Task<RedisResult> ScriptEvaluateReadOnlyAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None)
{ // note we may end up using the memory overloads to enable better pooling etc usage
if (keys is null || keys.Length == 0) return Inner.ScriptEvaluateReadOnlyAsync(script, keys, values, flags);
if (keys.Length == 1) return Inner.ScriptEvaluateReadOnlyAsync(script, ToInner(keys[0]), values, flags);
if ((flags & CommandFlags.FireAndForget) != 0) return Inner.ScriptEvaluateReadOnlyAsync(script, ToInnerCopy(keys), values, flags);
return Lease_ScriptEvaluateReadOnlyAsync(script, keys, values, flags);
}

public Task<RedisResult> ScriptEvaluateReadOnlyAsync(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?
Inner.ScriptEvaluateAsync(script, ToInner(keys), values, flags);
public Task<RedisResult> ScriptEvaluateReadOnlyAsync(string script, RedisKey key, ReadOnlyMemory<RedisValue> values = default, CommandFlags flags = CommandFlags.None) =>
Inner.ScriptEvaluateReadOnlyAsync(script, ToInner(key), values, flags);

public Task<RedisResult> ScriptEvaluateReadOnlyAsync(string script, ReadOnlyMemory<RedisKey> keys, ReadOnlyMemory<RedisValue> values = default, CommandFlags flags = CommandFlags.None)
{
if (keys.Length == 0) return Inner.ScriptEvaluateReadOnlyAsync(script, keys, values, flags);
if (keys.Length == 1) return Inner.ScriptEvaluateReadOnlyAsync(script, ToInner(keys.Span[0]), values, flags);
if ((flags & CommandFlags.FireAndForget) != 0) return Inner.ScriptEvaluateReadOnlyAsync(script, ToInnerCopy(keys), values, flags);
return Lease_ScriptEvaluateReadOnlyAsync(script, keys, values, flags);
}

private async Task<RedisResult> Lease_ScriptEvaluateReadOnlyAsync(string script, ReadOnlyMemory<RedisKey> keys, ReadOnlyMemory<RedisValue> values, CommandFlags flags)
{
var result = await Inner.ScriptEvaluateReadOnlyAsync(script, ToInnerLease(keys, out var lease), values, flags);
ArrayPool<RedisKey>.Shared.Return(lease); // happy to only recycle on success
return result;
}

protected ReadOnlyMemory<RedisKey> ToInnerCopy(ReadOnlyMemory<RedisKey> keys)
{
if (keys.Length == 0) return default;
var arr = new RedisKey[keys.Length];
ToInner(keys, arr);
return arr;
}

protected ReadOnlyMemory<RedisKey> ToInnerLease(ReadOnlyMemory<RedisKey> keys, out RedisKey[] lease)
{
if (keys.Length == 0)
{
lease = Array.Empty<RedisKey>();
return default;
}
lease = ArrayPool<RedisKey>.Shared.Rent(keys.Length);
return new ReadOnlyMemory<RedisKey>(lease, 0, ToInner(keys, lease));
}
private int ToInner(ReadOnlyMemory<RedisKey> from, RedisKey[] arr)
{
int i = 0;
foreach (ref readonly var key in from.Span)
{
arr[i++] = ToInner(key);
}
return i;
}

public Task<long> SetAddAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) =>
Inner.SetAddAsync(ToInner(key), values, flags);
Expand Down Expand Up @@ -734,10 +807,10 @@ internal KeyPrefixed(TInner inner, byte[] keyPrefix)
public void WaitAll(params Task[] tasks) =>
Inner.WaitAll(tasks);

protected internal RedisKey ToInner(RedisKey outer) =>
protected internal RedisKey ToInner(in RedisKey outer) =>
RedisKey.WithPrefix(Prefix, outer);

protected RedisKey ToInnerOrDefault(RedisKey outer) =>
protected RedisKey ToInnerOrDefault(in RedisKey outer) =>
(outer == default(RedisKey)) ? outer : ToInner(outer);

[return: NotNullIfNotNull("args")]
Expand Down Expand Up @@ -847,6 +920,9 @@ internal KeyPrefixed(TInner inner, byte[] keyPrefix)
private Func<RedisKey, RedisKey>? mapFunction;
protected Func<RedisKey, RedisKey> GetMapFunction() =>
// create as a delegate when first required, then re-use
mapFunction ??= new Func<RedisKey, RedisKey>(ToInner);
mapFunction ??= CreateMapFunction(); // avoid inlining this because of capture scopes etc

private Func<RedisKey, RedisKey> CreateMapFunction() => ToInnerNoRef;
private RedisKey ToInnerNoRef(RedisKey value) => ToInner(value);
}
}