├── parallel stacks.png
├── Benchmark
├── Program.cs
├── AsyncRedisBenchmarks.cs
├── StackExchangeBenchmarks.cs
├── BenchmarkBase.cs
├── Benchmark.csproj
├── StackExchangeClientBase.cs
└── AsyncRedisClientBase.cs
├── TomLonghurst.AsyncRedisClient
├── GlobalUsings.cs
├── Pack-Nuget.ps1
├── LogLevel.cs
├── Exceptions
│ ├── RedisRecoverableException.cs
│ ├── RedisNonRecoverableException.cs
│ ├── RedisDataException.cs
│ ├── UnexpectedRedisResponseException.cs
│ ├── RedisFailedCommandException.cs
│ ├── RedisReadTimeoutException.cs
│ ├── RedisException.cs
│ ├── RedisConnectionException.cs
│ └── RedisWaitTimeoutException.cs
├── RedisClientSettings.cs
├── Models
│ ├── RedisPipeOptions.cs
│ ├── RedisValue.cs
│ ├── RequestModels
│ │ ├── RedisKeyValue.cs
│ │ └── RedisKeyFieldValue.cs
│ ├── Pong.cs
│ ├── Backlog
│ │ ├── IBacklogItem.cs
│ │ └── BacklogItem.cs
│ ├── ResultProcessors
│ │ ├── EmptyAbstractResultProcessor.cs
│ │ ├── DataAbstractResultProcessor.cs
│ │ ├── FloatAbstractResultProcessor.cs
│ │ ├── SuccessAbstractResultProcessor.cs
│ │ ├── WordAbstractResultProcessor.cs
│ │ ├── ArrayAbstractResultProcessor.cs
│ │ ├── IntegerAbstractResultProcessor.cs
│ │ ├── GenericAbstractResultProcessor.cs
│ │ └── AbstractResultProcessor.cs
│ ├── LuaScript.cs
│ ├── RawResult.cs
│ └── Commands
│ │ ├── RedisInput.cs
│ │ └── RedisCommand.cs
├── Constants
│ ├── StringConstants.cs
│ ├── ByteConstants.cs
│ ├── LastActionConstants.cs
│ └── Commands.cs
├── RedisTelemetryResult.cs
├── Extensions
│ ├── ExceptionExtensions.cs
│ ├── ByteExtensions.cs
│ ├── ConcurrentQueueExtensions.cs
│ ├── TaskExtensions.cs
│ ├── PipeExtensions.cs
│ ├── ClientTaskExtensions.cs
│ ├── StringExtensions.cs
│ └── BufferExtensions.cs
├── Helpers
│ ├── CancellationTokenHelper.cs
│ ├── ApplicationStats.cs
│ └── SpanNumberParser.cs
├── ObjectPool.cs
├── AsyncObjectPool.cs
├── Client
│ ├── RedisClientConfig.cs
│ ├── RedisClient.ResultProcessor.cs
│ ├── RedisClient.Commands.Cluster.cs
│ ├── RedisClientManager.cs
│ ├── RedisClient.Commands.Server.cs
│ ├── RedisClient.Backlog.cs
│ ├── RedisClient.Commands.Scripts.cs
│ ├── RedisClient.ReadWrite.cs
│ ├── RedisClient.Connect.cs
│ └── RedisClient.Commands.cs
├── RedisSocket.cs
├── Logger.cs
├── TomLonghurst.AsyncRedisClient.csproj
├── AsyncCircularQueue.cs
├── BlockingQueue.cs
├── BytesEncoder.cs
└── Pipes
│ └── SocketPipe.cs
├── RedisClient.sln.DotSettings
├── Playground
├── Playground.csproj
├── EnumerableExtension.cs
└── Program.cs
├── RedisClientTest
├── TestBase.cs
├── MyFactory.cs
├── RedisClientTest.csproj
└── IntegrationTests.cs
├── .github
└── workflows
│ └── speed-comparison.yml
├── RedisClient.sln
├── README.md
├── .gitignore
└── LICENSE
/parallel stacks.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thomhurst/A-sync-RedisClient/HEAD/parallel stacks.png
--------------------------------------------------------------------------------
/Benchmark/Program.cs:
--------------------------------------------------------------------------------
1 | using BenchmarkDotNet.Running;
2 |
3 | BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
--------------------------------------------------------------------------------
/TomLonghurst.AsyncRedisClient/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | #if NET9_0_OR_GREATER
2 | global using Lock = System.Threading.Lock;
3 | #else
4 | global using Lock = object;
5 | #endif
--------------------------------------------------------------------------------
/TomLonghurst.AsyncRedisClient/Pack-Nuget.ps1:
--------------------------------------------------------------------------------
1 | $version = Read-Host "Please enter the version number to use in the build"
2 |
3 | dotnet pack -c Release -p:PackageVersion=$version
--------------------------------------------------------------------------------
/TomLonghurst.AsyncRedisClient/LogLevel.cs:
--------------------------------------------------------------------------------
1 | namespace TomLonghurst.AsyncRedisClient;
2 |
3 | public enum LogLevel
4 | {
5 | None = 1,
6 | Error = 2,
7 | Debug = 3,
8 | Info = 4
9 | }
--------------------------------------------------------------------------------
/TomLonghurst.AsyncRedisClient/Exceptions/RedisRecoverableException.cs:
--------------------------------------------------------------------------------
1 | namespace TomLonghurst.AsyncRedisClient.Exceptions;
2 |
3 | public abstract class RedisRecoverableException : RedisException
4 | {
5 |
6 | }
--------------------------------------------------------------------------------
/TomLonghurst.AsyncRedisClient/Exceptions/RedisNonRecoverableException.cs:
--------------------------------------------------------------------------------
1 | namespace TomLonghurst.AsyncRedisClient.Exceptions;
2 |
3 | public abstract class RedisNonRecoverableException : RedisException
4 | {
5 |
6 | }
--------------------------------------------------------------------------------
/TomLonghurst.AsyncRedisClient/RedisClientSettings.cs:
--------------------------------------------------------------------------------
1 | namespace TomLonghurst.AsyncRedisClient;
2 |
3 | public static class RedisClientSettings
4 | {
5 | public static LogLevel LogLevel { get; set; } = LogLevel.Error;
6 | }
--------------------------------------------------------------------------------
/TomLonghurst.AsyncRedisClient/Models/RedisPipeOptions.cs:
--------------------------------------------------------------------------------
1 | using System.IO.Pipelines;
2 |
3 | namespace TomLonghurst.AsyncRedisClient.Models;
4 |
5 | public record RedisPipeOptions
6 | {
7 | public PipeOptions? SendOptions { get; init; }
8 | public PipeOptions? ReceiveOptions { get; init; }
9 | }
--------------------------------------------------------------------------------
/Benchmark/AsyncRedisBenchmarks.cs:
--------------------------------------------------------------------------------
1 | using BenchmarkDotNet.Attributes;
2 |
3 | namespace Benchmark;
4 |
5 | public class AsyncRedisBenchmarks : AsyncRedisClientBase
6 | {
7 | [Benchmark]
8 | public async Task AsyncRedis()
9 | {
10 | await Client.StringSetAsync("MyKey", "MyValue");
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Benchmark/StackExchangeBenchmarks.cs:
--------------------------------------------------------------------------------
1 | using BenchmarkDotNet.Attributes;
2 |
3 | namespace Benchmark;
4 |
5 | public class StackExchangeBenchmarks : StackExchangeClientBase
6 | {
7 | [Benchmark]
8 | public async Task StackExchangeRedis()
9 | {
10 | await Client.StringSetAsync("MyKey", "MyValue");
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/TomLonghurst.AsyncRedisClient/Exceptions/RedisDataException.cs:
--------------------------------------------------------------------------------
1 | namespace TomLonghurst.AsyncRedisClient.Exceptions;
2 |
3 | public class RedisDataException : RedisNonRecoverableException
4 | {
5 | public override string Message { get; }
6 |
7 | public RedisDataException(string message)
8 | {
9 | Message = message;
10 | }
11 | }
--------------------------------------------------------------------------------
/TomLonghurst.AsyncRedisClient/Models/RedisValue.cs:
--------------------------------------------------------------------------------
1 | namespace TomLonghurst.AsyncRedisClient.Models;
2 |
3 | public struct StringRedisValue
4 | {
5 | public string Value { get; }
6 | public bool HasValue => !string.IsNullOrEmpty(Value);
7 |
8 | internal StringRedisValue(string value)
9 | {
10 | Value = value;
11 | }
12 | }
--------------------------------------------------------------------------------
/TomLonghurst.AsyncRedisClient/Models/RequestModels/RedisKeyValue.cs:
--------------------------------------------------------------------------------
1 | namespace TomLonghurst.AsyncRedisClient.Models.RequestModels;
2 |
3 | public struct RedisKeyValue
4 | {
5 | public string Key { get; }
6 | public string Value { get; }
7 |
8 | public RedisKeyValue(string key, string value)
9 | {
10 | Key = key;
11 | Value = value;
12 | }
13 | }
--------------------------------------------------------------------------------
/TomLonghurst.AsyncRedisClient/Exceptions/UnexpectedRedisResponseException.cs:
--------------------------------------------------------------------------------
1 | namespace TomLonghurst.AsyncRedisClient.Exceptions;
2 |
3 | public class UnexpectedRedisResponseException : RedisNonRecoverableException
4 | {
5 | public override string Message { get; }
6 |
7 | public UnexpectedRedisResponseException(string message)
8 | {
9 | Message = message;
10 | }
11 | }
--------------------------------------------------------------------------------
/RedisClient.sln.DotSettings:
--------------------------------------------------------------------------------
1 |
2 | True
--------------------------------------------------------------------------------
/TomLonghurst.AsyncRedisClient/Constants/StringConstants.cs:
--------------------------------------------------------------------------------
1 | namespace TomLonghurst.AsyncRedisClient.Constants;
2 |
3 | public static class StringConstants
4 | {
5 | public static string EncodedLineTerminator { get; } = @"\r\n";
6 | public static string EncodedNewLine { get; } = @"\n";
7 |
8 | public static string LineTerminator { get; } = "\r\n";
9 | public static string NewLine { get; } = "\n";
10 | }
--------------------------------------------------------------------------------
/TomLonghurst.AsyncRedisClient/Models/Pong.cs:
--------------------------------------------------------------------------------
1 | namespace TomLonghurst.AsyncRedisClient.Models;
2 |
3 | public struct Pong
4 | {
5 | public TimeSpan TimeTaken { get; }
6 | public string Message { get; }
7 |
8 | public bool IsSuccessful => Message == "PONG";
9 |
10 | internal Pong(TimeSpan timeTaken, string message)
11 | {
12 | TimeTaken = timeTaken;
13 | Message = message;
14 | }
15 | }
--------------------------------------------------------------------------------
/TomLonghurst.AsyncRedisClient/Exceptions/RedisFailedCommandException.cs:
--------------------------------------------------------------------------------
1 | namespace TomLonghurst.AsyncRedisClient.Exceptions;
2 |
3 | public class RedisFailedCommandException : RedisRecoverableException
4 | {
5 | public RedisFailedCommandException(string message, byte[]? lastCommand)
6 | {
7 | Message = $"{message}\nLast Command: {ToString(lastCommand)}";
8 | }
9 |
10 | public override string Message { get; }
11 | }
--------------------------------------------------------------------------------
/TomLonghurst.AsyncRedisClient/RedisTelemetryResult.cs:
--------------------------------------------------------------------------------
1 | namespace TomLonghurst.AsyncRedisClient;
2 |
3 | public class RedisTelemetryResult
4 | {
5 | private readonly string _command;
6 | private readonly TimeSpan _duration;
7 |
8 | // TODO - More metrics here
9 |
10 | internal RedisTelemetryResult(string command, TimeSpan duration)
11 | {
12 | _command = command;
13 | _duration = duration;
14 | }
15 | }
--------------------------------------------------------------------------------
/TomLonghurst.AsyncRedisClient/Exceptions/RedisReadTimeoutException.cs:
--------------------------------------------------------------------------------
1 | namespace TomLonghurst.AsyncRedisClient.Exceptions;
2 |
3 | public class RedisReadTimeoutException : RedisNonRecoverableException
4 | {
5 | private readonly Exception _exception;
6 |
7 | public RedisReadTimeoutException(Exception exception)
8 | {
9 | _exception = exception;
10 | }
11 |
12 | public override Exception GetBaseException() => _exception;
13 | }
--------------------------------------------------------------------------------
/TomLonghurst.AsyncRedisClient/Extensions/ExceptionExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace TomLonghurst.AsyncRedisClient.Extensions;
2 |
3 | internal static class ExceptionExtensions
4 | {
5 | internal static bool IsSameOrSubclassOf(this Exception exception, Type typeToCheckAgainst)
6 | {
7 | var exceptionType = exception.GetType();
8 | return exceptionType == typeToCheckAgainst || exceptionType.IsSubclassOf(typeToCheckAgainst);
9 | }
10 | }
--------------------------------------------------------------------------------
/TomLonghurst.AsyncRedisClient/Models/RequestModels/RedisKeyFieldValue.cs:
--------------------------------------------------------------------------------
1 | namespace TomLonghurst.AsyncRedisClient.Models.RequestModels;
2 |
3 | public struct RedisKeyFieldValue
4 | {
5 | public string Key { get; }
6 | public string Field { get; }
7 | public string Value { get; }
8 |
9 | public RedisKeyFieldValue(string key, string field, string value)
10 | {
11 | Key = key;
12 | Field = field;
13 | Value = value;
14 | }
15 | }
--------------------------------------------------------------------------------
/Playground/Playground.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net8.0
6 | preview
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/TomLonghurst.AsyncRedisClient/Exceptions/RedisException.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 |
3 | namespace TomLonghurst.AsyncRedisClient.Exceptions;
4 |
5 | public abstract class RedisException : Exception
6 | {
7 | protected static string ToString(byte[]? lastCommand)
8 | {
9 | if (lastCommand is null)
10 | {
11 | return "null";
12 | }
13 |
14 | if (lastCommand.Length == 0)
15 | {
16 | return string.Empty;
17 | }
18 |
19 | return Encoding.UTF8.GetString(lastCommand);
20 | }
21 | }
--------------------------------------------------------------------------------
/TomLonghurst.AsyncRedisClient/Helpers/CancellationTokenHelper.cs:
--------------------------------------------------------------------------------
1 | namespace TomLonghurst.AsyncRedisClient.Helpers;
2 |
3 | public static class CancellationTokenHelper
4 | {
5 |
6 | internal static CancellationTokenSource CancellationTokenWithTimeout(TimeSpan timeout, CancellationToken tokenToCombine)
7 | {
8 | var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(tokenToCombine);
9 | #if !DEBUG
10 | cancellationTokenSource.CancelAfter(timeout);
11 | #endif
12 | return cancellationTokenSource;
13 | }
14 | }
--------------------------------------------------------------------------------
/TomLonghurst.AsyncRedisClient/ObjectPool.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Concurrent;
2 |
3 | namespace TomLonghurst.AsyncRedisClient;
4 |
5 | public class ObjectPool(Func objectGenerator)
6 | {
7 | private readonly ConcurrentBag _objects = [];
8 | private readonly Func _objectGenerator = objectGenerator ?? throw new ArgumentNullException(nameof(objectGenerator));
9 |
10 | public T Get()
11 | {
12 | return _objects.TryTake(out var item) ? item : _objectGenerator();
13 | }
14 |
15 | public void Return(T item) => _objects.Add(item);
16 | }
--------------------------------------------------------------------------------
/RedisClientTest/TestBase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Testcontainers.Redis;
3 |
4 | namespace RedisClientTest;
5 |
6 | public class TestBase
7 | {
8 | [ClassDataSource(Shared = SharedType.Globally)]
9 | public required RedisContainerFactory ContainerFactory { get; init; }
10 |
11 | public Uri ConnectionString => new($"https://{RedisContainer.GetConnectionString()}");
12 |
13 | public string Host => ConnectionString.Host;
14 |
15 | public int Port => ConnectionString.Port;
16 |
17 | public RedisContainer RedisContainer => ContainerFactory.RedisContainer;
18 | }
--------------------------------------------------------------------------------
/TomLonghurst.AsyncRedisClient/Extensions/ByteExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using TomLonghurst.AsyncRedisClient.Models;
3 |
4 | namespace TomLonghurst.AsyncRedisClient.Extensions;
5 |
6 | internal static class ByteExtensions
7 | {
8 |
9 | internal static string FromUtf8(this byte[] bytes)
10 | {
11 | return Encoding.UTF8.GetString(bytes);
12 | }
13 |
14 |
15 | internal static IEnumerable ToRedisValues(this IEnumerable bytesArray)
16 | {
17 | return bytesArray.Select(bytes => new StringRedisValue(bytes.FromUtf8()));
18 | }
19 | }
--------------------------------------------------------------------------------
/TomLonghurst.AsyncRedisClient/Models/Backlog/IBacklogItem.cs:
--------------------------------------------------------------------------------
1 | using TomLonghurst.AsyncRedisClient.Models.ResultProcessors;
2 |
3 | namespace TomLonghurst.AsyncRedisClient.Models.Backlog;
4 |
5 | public interface IBacklog
6 | {
7 | byte[] RedisCommand { get; }
8 | CancellationToken CancellationToken { get; }
9 | void SetCancelled();
10 | void SetException(Exception exception);
11 | Task SetResult();
12 | }
13 |
14 | public interface IBacklogItem : IBacklog
15 | {
16 | TaskCompletionSource TaskCompletionSource { get; }
17 |
18 | AbstractResultProcessor AbstractResultProcessor { get; }
19 | }
--------------------------------------------------------------------------------
/TomLonghurst.AsyncRedisClient/Exceptions/RedisConnectionException.cs:
--------------------------------------------------------------------------------
1 | namespace TomLonghurst.AsyncRedisClient.Exceptions;
2 |
3 | public class RedisConnectionException : RedisNonRecoverableException
4 | {
5 | private readonly Exception _innerException;
6 |
7 | public override Exception GetBaseException()
8 | {
9 | return _innerException;
10 | }
11 |
12 | public override string Message => $"{_innerException.Message} - {_innerException.GetType().Name}\n{_innerException}";
13 |
14 | public RedisConnectionException(Exception innerException)
15 | {
16 | _innerException = innerException;
17 | }
18 | }
--------------------------------------------------------------------------------
/TomLonghurst.AsyncRedisClient/Models/ResultProcessors/EmptyAbstractResultProcessor.cs:
--------------------------------------------------------------------------------
1 | using System.IO.Pipelines;
2 | using TomLonghurst.AsyncRedisClient.Client;
3 |
4 | namespace TomLonghurst.AsyncRedisClient.Models.ResultProcessors;
5 |
6 | public class EmptyResultProcessor : AbstractResultProcessor