Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
370 changes: 245 additions & 125 deletions docs/ActiveActive.md

Large diffs are not rendered by default.

483 changes: 483 additions & 0 deletions eng/StackExchange.Redis.Build/AutoDatabaseGenerator.cs

Large diffs are not rendered by default.

25 changes: 23 additions & 2 deletions eng/StackExchange.Redis.Build/BasicArray.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public bool Equals(BasicArray<T> other)
int i = 0;
foreach (ref readonly T el in this.Span)
{
if (!_comparer.Equals(el, y[i])) return false;
if (!_comparer.Equals(el, y[i++])) return false;
}

return true;
Expand All @@ -58,7 +58,7 @@ public override int GetHashCode()
var hash = Length;
foreach (ref readonly T el in this.Span)
{
_ = (hash * -37) + _comparer.GetHashCode(el);
hash = (hash * -37) + _comparer.GetHashCode(el);
}

return hash;
Expand All @@ -82,4 +82,25 @@ public void Add(in T value)

public BasicArray<T> Build() => new(elements, Count);
}

public static BasicArray<T> From(ICollection<T> collection)
{
if (collection.Count is 0) return default;
var arr = new T[collection.Count];
collection.CopyTo(arr, 0);
return new(arr, arr.Length);
}

public static BasicArray<T> From<TSource>(ICollection<TSource> collection, Func<TSource, T> selector)
{
if (collection.Count is 0) return default;
var arr = new T[collection.Count];
int i = 0;
foreach (var item in collection)
{
arr[i++] = selector(item);
}

return new(arr, i);
}
}
18 changes: 17 additions & 1 deletion eng/StackExchange.Redis.Build/CodeWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ internal sealed class CodeWriter(StringBuilder buffer)
/// <summary>Starts a new line, applying the current indent.</summary>
public CodeWriter NewLine()
{
buffer.AppendLine().Append(' ', _indent * 4);
buffer.AppendLine();
_lineHasContent = false;
return this;
}

Expand All @@ -32,26 +33,41 @@ public CodeWriter Outdent()
return this;
}

private bool _lineHasContent;

private void IndentIfNeeded()
{
if (!_lineHasContent)
{
buffer.Append(' ', _indent * 4);
_lineHasContent = true;
}
}

public CodeWriter Append(string? value)
{
IndentIfNeeded();
buffer.Append(value);
return this;
}

public CodeWriter Append(char value)
{
IndentIfNeeded();
buffer.Append(value);
return this;
}

public CodeWriter Append(int value)
{
IndentIfNeeded();
buffer.Append(value);
return this;
}

public CodeWriter Append(long value)
{
IndentIfNeeded();
buffer.Append(value);
return this;
}
Expand Down
23 changes: 23 additions & 0 deletions src/RESPite/Messages/RespReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,29 @@ public readonly unsafe bool TryParseScalar<T>(
return TryGetSpan(out var span) ? parser(span, out value) : TryParseSlow(parser, out value);
}

// same thing as ^^^, but as a utility method for text values, paying UTF8 encode
internal static unsafe bool TryParseScalar<T>(
ReadOnlySpan<char> source,
delegate* managed<ReadOnlySpan<byte>, out T, bool> parser,
out T value)
{
const int MAX_STACK = 128;
byte[]? lease = null;
int maxLen = RespConstants.UTF8.GetMaxByteCount(source.Length);
Span<byte> buffer = maxLen <= MAX_STACK
? stackalloc byte[MAX_STACK] // prefer fixed size for perf
: (lease = ArrayPool<byte>.Shared.Rent(maxLen));
try
{
var len = RespConstants.UTF8.GetBytes(source, buffer);
return parser(buffer.Slice(0, len), out value);
}
finally
{
if (lease is not null) ArrayPool<byte>.Shared.Return(lease);
}
}

[MethodImpl(MethodImplOptions.NoInlining)]
private readonly unsafe bool TryParseSlow<T>(
delegate* managed<ReadOnlySpan<byte>, out T, bool> parser,
Expand Down
168 changes: 168 additions & 0 deletions src/StackExchange.Redis/AutoDatabase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;

namespace StackExchange.Redis;

[Conditional("DEBUG")]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = false)]
internal sealed class AutoDatabaseAttribute : Attribute
{
}

internal interface IRedisArgs
{
void Map(IRedisArgsMutator mutator);
object? UnMapper { get; }
}

internal interface IRedisArgsMutator
{
RedisKey Map(RedisKey key);
RedisChannel Map(RedisChannel channel);

RedisKey UnMap(RedisKey key);
RedisChannel UnMap(RedisChannel channel);
}

internal interface IRedisArgsResult<T>
{
T UnMap(IRedisArgsMutator mutator, T value);
}

internal static class RedisArgsMutatorExtensions
{
// these are used by the generated tuple-types via auto-database: each maps the key-bearing parts
// of an argument through the supplied mutator. They hang off IRedisArgsMutator (rather than the
// argument) so a call site reads consistently with the interface's own MapKey/MapChannel.
public static KeyValuePair<RedisKey, RedisValue> Map(
this IRedisArgsMutator mutator,
KeyValuePair<RedisKey, RedisValue> value) =>
new(mutator.Map(value.Key), value.Value);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TResult UnMap<TState, TResult>(this IRedisArgsMutator mutator, in TState state, TResult result)
where TState : struct, IRedisArgs
{
if (typeof(TResult) == typeof(RedisKey))
{
var tmp = mutator.UnMap(Unsafe.As<TResult, RedisKey>(ref result));
return Unsafe.As<RedisKey, TResult>(ref tmp);
}
else if (typeof(TResult) == typeof(RedisChannel))
{
var tmp = mutator.UnMap(Unsafe.As<TResult, RedisChannel>(ref result));
return Unsafe.As<RedisChannel, TResult>(ref tmp);
}
else if (typeof(TResult) == typeof(RedisValue))
{
return result; // never mapped
}
else
{
return state.UnMapper is IRedisArgsResult<TResult> unmap ? unmap.UnMap(mutator, result) : result;
}
}

[return: NotNullIfNotNull("keys")]
public static RedisKey[]? Map(this IRedisArgsMutator mutator, RedisKey[]? keys)
{
if (keys is null || keys.Length is 0) return keys;
var arr = new RedisKey[keys.Length];
for (int i = 0; i < arr.Length; i++)
{
arr[i] = mutator.Map(keys[i]);
}
return arr;
}

[return: NotNullIfNotNull("pairs")]
public static KeyValuePair<RedisKey, RedisValue>[]? Map(
this IRedisArgsMutator mutator,
KeyValuePair<RedisKey, RedisValue>[]? pairs)
{
if (pairs is null || pairs.Length is 0) return pairs;
var arr = new KeyValuePair<RedisKey, RedisValue>[pairs.Length];
for (int i = 0; i < arr.Length; i++)
{
ref readonly KeyValuePair<RedisKey, RedisValue> pair = ref pairs[i];
arr[i] = new(mutator.Map(pair.Key), pair.Value);
}
return arr;
}

public static StreamPosition Map(this IRedisArgsMutator mutator, StreamPosition value) =>
new(mutator.Map(value.Key), value.Position);

[return: NotNullIfNotNull("positions")]
public static StreamPosition[]? Map(this IRedisArgsMutator mutator, StreamPosition[]? positions)
{
if (positions is null || positions.Length is 0) return positions;
var arr = new StreamPosition[positions.Length];
for (int i = 0; i < arr.Length; i++)
{
ref readonly StreamPosition position = ref positions[i];
arr[i] = new(mutator.Map(position.Key), position.Position);
}
return arr;
}

// the Execute/ExecuteAsync escape hatch takes a loosely-typed arg list in which any element may
// be a boxed RedisKey or RedisChannel; these rewrite just those entries (mirroring
// KeyPrefixed.ToInner), copying only when there is something to rewrite so the common call allocates nothing.
[return: NotNullIfNotNull("args")]
public static object[]? Map(this IRedisArgsMutator mutator, object[]? args)
{
if (args is null || args.Length is 0) return args;
object[]? copy = null;
for (int i = 0; i < args.Length; i++)
{
if (args[i] is RedisKey key)
{
(copy ??= (object[])args.Clone())[i] = mutator.Map(key);
}
else if (args[i] is RedisChannel channel)
{
(copy ??= (object[])args.Clone())[i] = mutator.Map(channel);
}
}
return copy ?? args;
}

[return: NotNullIfNotNull("args")]
public static ICollection<object>? Map(this IRedisArgsMutator mutator, ICollection<object>? args)
{
if (args is null || args.Count is 0) return args;
bool any = false;
foreach (var arg in args)
{
if (arg is RedisKey or RedisChannel)
{
any = true;
break;
}
}
if (!any) return args;

var copy = new object[args.Count];
int i = 0;
foreach (var arg in args)
{
copy[i++] = arg switch
{
RedisKey key => mutator.Map(key),
RedisChannel channel => mutator.Map(channel),
_ => arg,
};
}
return copy;
}

public static SortedSetPopResult UnMap(this IRedisArgsMutator mutator, SortedSetPopResult value) =>
value.IsNull ? SortedSetPopResult.Null : new(mutator.Map(value.Key), value.Entries);

public static ListPopResult UnMap(this IRedisArgsMutator mutator, ListPopResult value) =>
value.IsNull ? ListPopResult.Null : new(mutator.Map(value.Key), value.Values);
}
Loading
Loading