├── 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 7 | { 8 | internal override ValueTask Process( 9 | RedisClient redisClient, 10 | PipeReader pipeReader, 11 | ReadResult readResult, 12 | CancellationToken cancellationToken 13 | ) 14 | { 15 | // Do Nothing! 16 | return ValueTask.FromResult(null); 17 | } 18 | } -------------------------------------------------------------------------------- /RedisClientTest/MyFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Testcontainers.Redis; 4 | using TUnit.Core.Interfaces; 5 | 6 | namespace RedisClientTest; 7 | 8 | public class RedisContainerFactory : IAsyncInitializer, IAsyncDisposable 9 | { 10 | public RedisContainer RedisContainer { get; } = new RedisBuilder() 11 | .WithImage("redis:7.4.1") 12 | .Build(); 13 | 14 | public async Task InitializeAsync() 15 | { 16 | await RedisContainer.StartAsync(); 17 | } 18 | 19 | public async ValueTask DisposeAsync() 20 | { 21 | await RedisContainer.DisposeAsync(); 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Constants/ByteConstants.cs: -------------------------------------------------------------------------------- 1 | namespace TomLonghurst.AsyncRedisClient.Constants; 2 | 3 | public static class ByteConstants 4 | { 5 | public const byte BackslashR = (byte) '\r'; 6 | public const byte NewLine = (byte) '\n'; 7 | 8 | public const byte Plus = (byte) '+'; 9 | public const byte Dash = (byte) '-'; 10 | public const byte Dollar = (byte) '$'; 11 | public const byte Asterix = (byte) '*'; 12 | public const byte Colon = (byte) ':'; 13 | 14 | public const byte One = (byte) '1'; 15 | 16 | public const byte O = (byte) 'O'; 17 | public const byte K = (byte) 'K'; 18 | } -------------------------------------------------------------------------------- /Playground/EnumerableExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Playground; 6 | 7 | public static class EnumerableExtension 8 | { 9 | public static T PickRandom(this IEnumerable source) 10 | { 11 | return source.PickRandom(1).Single(); 12 | } 13 | 14 | public static IEnumerable PickRandom(this IEnumerable source, int count) 15 | { 16 | return source.Shuffle().Take(count); 17 | } 18 | 19 | public static IEnumerable Shuffle(this IEnumerable source) 20 | { 21 | return source.OrderBy(x => Guid.NewGuid()); 22 | } 23 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/AsyncObjectPool.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | 3 | namespace TomLonghurst.AsyncRedisClient; 4 | 5 | public class AsyncObjectPool(Func> objectGenerator) 6 | { 7 | private readonly ConcurrentBag _objects = []; 8 | private readonly Func> _objectGenerator = objectGenerator ?? throw new ArgumentNullException(nameof(objectGenerator)); 9 | 10 | public async ValueTask Get() 11 | { 12 | if (_objects.TryTake(out var item)) 13 | { 14 | return item; 15 | } 16 | 17 | return await _objectGenerator(); 18 | } 19 | 20 | public void Return(T item) => _objects.Add(item); 21 | } -------------------------------------------------------------------------------- /Benchmark/BenchmarkBase.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using BenchmarkDotNet.Jobs; 3 | using Testcontainers.Redis; 4 | 5 | namespace Benchmark; 6 | 7 | [SimpleJob(RuntimeMoniker.Net90)] 8 | [MemoryDiagnoser] 9 | public class BenchmarkBase 10 | { 11 | protected RedisContainer RedisContainer { get; private set; } = null!; 12 | 13 | public async Task ContainerSetup() 14 | { 15 | RedisContainer = new RedisBuilder() 16 | .WithImage("redis:7.4.1") 17 | .Build(); 18 | 19 | await RedisContainer.StartAsync(); 20 | } 21 | 22 | public async Task ContainerCleanup() 23 | { 24 | await RedisContainer.DisposeAsync(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Models/ResultProcessors/DataAbstractResultProcessor.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Pipelines; 2 | using TomLonghurst.AsyncRedisClient.Client; 3 | using TomLonghurst.AsyncRedisClient.Extensions; 4 | 5 | namespace TomLonghurst.AsyncRedisClient.Models.ResultProcessors; 6 | 7 | public class DataResultProcessor : AbstractResultProcessor 8 | { 9 | internal override async ValueTask Process( 10 | RedisClient redisClient, 11 | PipeReader pipeReader, 12 | ReadResult readResult, 13 | CancellationToken cancellationToken 14 | ) 15 | { 16 | return (await ReadData(redisClient, pipeReader, readResult, cancellationToken)).AsString(); 17 | } 18 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Extensions/ConcurrentQueueExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | 3 | namespace TomLonghurst.AsyncRedisClient.Extensions; 4 | 5 | public static class ConcurrentQueueExtensions 6 | { 7 | public static async Task> DequeueAll(this ConcurrentQueue queue, SemaphoreSlim sendSemaphoreSlim, 8 | CancellationToken cancellationToken) 9 | { 10 | await sendSemaphoreSlim.WaitAsync(cancellationToken); 11 | 12 | var list = new List(); 13 | 14 | while (queue.TryDequeue(out var result) && list.Count < 500) 15 | { 16 | list.Add(result); 17 | } 18 | 19 | sendSemaphoreSlim.Release(); 20 | 21 | return list; 22 | } 23 | } -------------------------------------------------------------------------------- /Benchmark/Benchmark.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0;net9.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Extensions/TaskExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace TomLonghurst.AsyncRedisClient.Extensions; 2 | 3 | public static class TaskExtensions 4 | { 5 | public static bool IsCompletedSuccessfully(this Task task) => 6 | task.IsCompleted && !(task.IsCanceled || task.IsFaulted); 7 | 8 | public static async Task WhenAny(this IEnumerable> tasks, Predicate condition) 9 | { 10 | var tasksList = tasks.ToList(); 11 | 12 | while (tasksList.Count > 0) 13 | { 14 | var task = await Task.WhenAny(tasksList); 15 | var t = await task; 16 | 17 | if (condition(t)) 18 | { 19 | return t; 20 | } 21 | 22 | tasksList.Remove(task); 23 | } 24 | 25 | return default; 26 | } 27 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Client/RedisClientConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Security; 2 | 3 | namespace TomLonghurst.AsyncRedisClient.Client; 4 | 5 | public record RedisClientConfig(string Host, int Port) 6 | { 7 | public bool Ssl { get; init; } 8 | public int Db { get; init; } 9 | public int PoolSize { get; init; } = 50; 10 | public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(30); 11 | public string? Password { get; init; } 12 | public string? ClientName { get; init; } 13 | public RemoteCertificateValidationCallback? CertificateValidationCallback { get; init; } 14 | public LocalCertificateSelectionCallback? CertificateSelectionCallback { get; init; } 15 | 16 | 17 | public Func? OnConnectionEstablished { get; init; } 18 | 19 | public Func? OnConnectionFailed { get; init; } 20 | } -------------------------------------------------------------------------------- /Benchmark/StackExchangeClientBase.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using StackExchange.Redis; 3 | 4 | namespace Benchmark; 5 | 6 | public class StackExchangeClientBase : BenchmarkBase 7 | { 8 | private ConnectionMultiplexer _connectionMultiplexer = null!; 9 | 10 | public IDatabaseAsync Client { get; private set; } = null!; 11 | 12 | [GlobalSetup] 13 | public async Task Setup() 14 | { 15 | await ContainerSetup(); 16 | 17 | _connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(RedisContainer.GetConnectionString()); 18 | 19 | Client = _connectionMultiplexer.GetDatabase(); 20 | } 21 | 22 | [GlobalCleanup] 23 | public async Task Cleanup() 24 | { 25 | await RedisContainer.DisposeAsync(); 26 | await _connectionMultiplexer.DisposeAsync(); 27 | await ContainerCleanup(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Models/LuaScript.cs: -------------------------------------------------------------------------------- 1 | using TomLonghurst.AsyncRedisClient.Client; 2 | 3 | namespace TomLonghurst.AsyncRedisClient.Models; 4 | 5 | public class LuaScript 6 | { 7 | private readonly RedisClient _redisClient; 8 | private readonly string _hash; 9 | 10 | internal LuaScript(RedisClient redisClient, string hash) 11 | { 12 | _redisClient = redisClient; 13 | _hash = hash; 14 | } 15 | 16 | public Task ExecuteAsync(IEnumerable keys, IEnumerable arguments) 17 | { 18 | return ExecuteAsync(keys, arguments, CancellationToken.None); 19 | } 20 | 21 | public Task ExecuteAsync(IEnumerable keys, IEnumerable arguments, CancellationToken cancellationToken) 22 | { 23 | return _redisClient.Scripts.EvalSha(_hash, keys, arguments, cancellationToken); 24 | } 25 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Extensions/PipeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Pipelines; 2 | using System.Runtime.CompilerServices; 3 | using TomLonghurst.AsyncRedisClient.Exceptions; 4 | 5 | namespace TomLonghurst.AsyncRedisClient.Extensions; 6 | 7 | public static class PipeExtensions 8 | { 9 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 10 | public static async ValueTask ReadAsyncOrThrowReadTimeout(this PipeReader pipeReader, CancellationToken cancellationToken) 11 | { 12 | try 13 | { 14 | if (pipeReader.TryRead(out var readResult)) 15 | { 16 | return readResult; 17 | } 18 | 19 | return await pipeReader.ReadAsync(cancellationToken); 20 | } 21 | catch (OperationCanceledException e) 22 | { 23 | throw new RedisReadTimeoutException(e); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Models/RawResult.cs: -------------------------------------------------------------------------------- 1 | namespace TomLonghurst.AsyncRedisClient.Models; 2 | 3 | public struct RawResult 4 | { 5 | private readonly object _rawResult; 6 | 7 | internal RawResult(object rawResult) 8 | { 9 | _rawResult = rawResult; 10 | } 11 | 12 | public StringRedisValue GetAsWord() 13 | { 14 | return (StringRedisValue) _rawResult; 15 | } 16 | 17 | public StringRedisValue GetAsComplexString() 18 | { 19 | return (StringRedisValue) _rawResult; 20 | } 21 | 22 | public IEnumerable GetAsArray() 23 | { 24 | return (IEnumerable) _rawResult; 25 | } 26 | 27 | public int GetAsInteger() 28 | { 29 | return (int) _rawResult; 30 | } 31 | 32 | public float GetAsFloat() 33 | { 34 | return (float) _rawResult; 35 | } 36 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Helpers/ApplicationStats.cs: -------------------------------------------------------------------------------- 1 | namespace TomLonghurst.AsyncRedisClient.Helpers; 2 | 3 | public static class ApplicationStats 4 | { 5 | internal static void GetThreadPoolStats(out string ioThreadStats, out string workerThreadStats) 6 | { 7 | ThreadPool.GetMaxThreads(out var maxWorkerThreads, out var maxIoThreads); 8 | ThreadPool.GetAvailableThreads(out var freeWorkerThreads, out var freeIoThreads); 9 | ThreadPool.GetMinThreads(out var minWorkerThreads, out var minIoThreads); 10 | 11 | var busyIoThreads = maxIoThreads - freeIoThreads; 12 | var busyWorkerThreads = maxWorkerThreads - freeWorkerThreads; 13 | 14 | ioThreadStats = $"IO Threads: Busy={busyIoThreads}|Free={freeIoThreads}|Min={minIoThreads}|Max={maxIoThreads}"; 15 | workerThreadStats = $"Worker Threads: Busy={busyWorkerThreads}|Free={freeWorkerThreads}|Min={minWorkerThreads}|Max={maxWorkerThreads}"; 16 | } 17 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Client/RedisClient.ResultProcessor.cs: -------------------------------------------------------------------------------- 1 | using TomLonghurst.AsyncRedisClient.Models.ResultProcessors; 2 | 3 | namespace TomLonghurst.AsyncRedisClient.Client; 4 | 5 | public partial class RedisClient 6 | { 7 | internal GenericResultProcessor GenericResultProcessor => new(); 8 | internal EmptyResultProcessor EmptyResultProcessor => new(); 9 | internal SuccessResultProcessor SuccessResultProcessor => new(); 10 | internal DataResultProcessor DataResultProcessor => new(); 11 | internal IntegerResultProcessor IntegerResultProcessor => new(); 12 | internal FloatResultProcessor FloatResultProcessor => new(); 13 | internal ArrayResultProcessor ArrayResultProcessor => new(); 14 | internal SimpleStringResultProcessor SimpleStringResultProcessor => new(); 15 | 16 | } 17 | 18 | public enum ResponseType 19 | { 20 | Empty, 21 | Word, 22 | ComplexString, 23 | Integer, 24 | Float, 25 | Array 26 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Exceptions/RedisWaitTimeoutException.cs: -------------------------------------------------------------------------------- 1 | using TomLonghurst.AsyncRedisClient.Client; 2 | using TomLonghurst.AsyncRedisClient.Helpers; 3 | 4 | namespace TomLonghurst.AsyncRedisClient.Exceptions; 5 | 6 | public class RedisWaitTimeoutException : RedisRecoverableException 7 | { 8 | private readonly RedisClient _redisClient; 9 | 10 | internal RedisWaitTimeoutException(RedisClient redisClient) 11 | { 12 | _redisClient = redisClient; 13 | } 14 | 15 | public override string Message 16 | { 17 | get 18 | { 19 | ApplicationStats.GetThreadPoolStats(out var ioThreadStats, out var workerThreadStats); 20 | return $""" 21 | Client {_redisClient.ClientId} 22 | {workerThreadStats} 23 | {ioThreadStats} 24 | Last Command: {ToString(_redisClient.LastCommand)} 25 | """; 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Models/ResultProcessors/FloatAbstractResultProcessor.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Pipelines; 2 | using TomLonghurst.AsyncRedisClient.Client; 3 | using TomLonghurst.AsyncRedisClient.Exceptions; 4 | using TomLonghurst.AsyncRedisClient.Extensions; 5 | 6 | namespace TomLonghurst.AsyncRedisClient.Models.ResultProcessors; 7 | 8 | public class FloatResultProcessor : AbstractResultProcessor 9 | { 10 | internal override async ValueTask Process( 11 | RedisClient redisClient, 12 | PipeReader pipeReader, 13 | ReadResult readResult, 14 | CancellationToken cancellationToken 15 | ) 16 | { 17 | var floatString = (await ReadData(redisClient, pipeReader, readResult, cancellationToken)).AsString(); 18 | 19 | if (!float.TryParse(floatString, out var number)) 20 | { 21 | throw new UnexpectedRedisResponseException(floatString); 22 | } 23 | 24 | return number; 25 | } 26 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Extensions/ClientTaskExtensions.cs: -------------------------------------------------------------------------------- 1 | using TomLonghurst.AsyncRedisClient.Client; 2 | using TomLonghurst.AsyncRedisClient.Models; 3 | 4 | namespace TomLonghurst.AsyncRedisClient.Extensions; 5 | 6 | public static class ClientTaskExtensions 7 | { 8 | 9 | // TODO Finish this 10 | 11 | public static Task StringGetAsync(this Task redisClient, string key) 12 | { 13 | return StringGetAsync(redisClient, key, CancellationToken.None); 14 | } 15 | 16 | public static async Task StringGetAsync(this Task redisClient, string key, CancellationToken cancellationToken) 17 | { 18 | var client = await GetClient(redisClient); 19 | return await client.StringGetAsync(key, cancellationToken); 20 | } 21 | 22 | private static async Task GetClient(Task redisClient) 23 | { 24 | return await redisClient; 25 | } 26 | } -------------------------------------------------------------------------------- /.github/workflows/speed-comparison.yml: -------------------------------------------------------------------------------- 1 | name: Speed Comparison 2 | 3 | on: 4 | push: 5 | branches: ["*"] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | run-time-benchmarks: 10 | runs-on: ubuntu-latest 11 | concurrency: 12 | group: "speed-comparison-run-time-${{github.ref_name}}" 13 | cancel-in-progress: true 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | persist-credentials: false 20 | 21 | - name: Setup .NET 22 | uses: actions/setup-dotnet@v4 23 | with: 24 | dotnet-version: 9.0.x 25 | 26 | - name: Run Benchmark 27 | run: sudo -E dotnet run -c Release --framework net9.0 --filter '*' --join 28 | working-directory: "Benchmark" 29 | 30 | - name: Upload Markdown 31 | uses: actions/upload-artifact@v4 32 | if: always() 33 | with: 34 | name: ${{ matrix.os }}_markdown_run_time 35 | path: | 36 | **/BenchmarkDotNet.Artifacts/** -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/RedisSocket.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Sockets; 2 | 3 | namespace TomLonghurst.AsyncRedisClient; 4 | 5 | internal class RedisSocket : Socket 6 | { 7 | internal bool IsDisposed { get; private set; } 8 | public bool IsClosed { get; private set; } 9 | 10 | ~RedisSocket() 11 | { 12 | Close(); 13 | Dispose(); 14 | } 15 | 16 | internal RedisSocket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType) : base(addressFamily, socketType, protocolType) 17 | { 18 | } 19 | 20 | internal RedisSocket(SocketType socketType, ProtocolType protocolType) : base(socketType, protocolType) 21 | { 22 | } 23 | 24 | public new void Close() 25 | { 26 | IsClosed = true; 27 | base.Close(); 28 | } 29 | 30 | protected override void Dispose(bool disposing) 31 | { 32 | if (disposing) 33 | { 34 | IsDisposed = true; 35 | } 36 | 37 | base.Dispose(disposing); 38 | } 39 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Logger.cs: -------------------------------------------------------------------------------- 1 | namespace TomLonghurst.AsyncRedisClient; 2 | 3 | internal class Logger 4 | { 5 | internal void Info(string message) 6 | { 7 | if (RedisClientSettings.LogLevel >= LogLevel.Info) 8 | { 9 | Log(message); 10 | } 11 | } 12 | 13 | internal void Debug(string message) 14 | { 15 | if (RedisClientSettings.LogLevel >= LogLevel.Debug) 16 | { 17 | Log(message); 18 | } 19 | } 20 | 21 | internal void Error(string message) 22 | { 23 | if (RedisClientSettings.LogLevel >= LogLevel.Error) 24 | { 25 | Log(message); 26 | } 27 | } 28 | 29 | internal void Error(string message, Exception ex) 30 | { 31 | if (RedisClientSettings.LogLevel >= LogLevel.Info) 32 | { 33 | Log(message); 34 | Log(ex.ToString()); 35 | } 36 | } 37 | 38 | private void Log(string message) 39 | { 40 | Console.WriteLine($"RedisClient ----- {message}"); 41 | } 42 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/TomLonghurst.AsyncRedisClient.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net9.0 5 | true 6 | enable 7 | preview 8 | 0.0.91 9 | An Asynchronous Redis Client for .NET 10 | Tom Longhurst 11 | https://github.com/thomhurst/A-sync-RedisClient 12 | Apache-2.0 13 | https://github.com/thomhurst/A-sync-RedisClient 14 | true 15 | TomLonghurst.AsyncRedisClient 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Client/RedisClient.Commands.Cluster.cs: -------------------------------------------------------------------------------- 1 | using TomLonghurst.AsyncRedisClient.Constants; 2 | 3 | namespace TomLonghurst.AsyncRedisClient.Client; 4 | 5 | public partial class RedisClient : IDisposable 6 | { 7 | public ClusterCommands Cluster { get; } 8 | 9 | public class ClusterCommands 10 | { 11 | private readonly RedisClient _redisClient; 12 | 13 | internal ClusterCommands(RedisClient redisClient) 14 | { 15 | _redisClient = redisClient; 16 | } 17 | 18 | public Task ClusterInfoAsync() 19 | { 20 | return ClusterInfoAsync(CancellationToken.None); 21 | } 22 | 23 | public async Task ClusterInfoAsync(CancellationToken cancellationToken) 24 | { 25 | return await _redisClient.RunWithTimeout(async token => 26 | { 27 | return await _redisClient.SendOrQueueAsync(Commands.ClusterInfo, _redisClient.DataResultProcessor, token); 28 | }, cancellationToken); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /RedisClientTest/RedisClientTest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | false 7 | 8 | preview 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | PreserveNewest 21 | 22 | 23 | PreserveNewest 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Benchmark/AsyncRedisClientBase.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using TomLonghurst.AsyncRedisClient.Client; 3 | 4 | namespace Benchmark; 5 | 6 | public class AsyncRedisClientBase : BenchmarkBase 7 | { 8 | private RedisClientManager _redisClientManager = null!; 9 | 10 | public RedisClient Client { get; private set; } = null!; 11 | 12 | [GlobalSetup] 13 | public async Task Setup() 14 | { 15 | await ContainerSetup(); 16 | var connectionString = new Uri($"https://{RedisContainer.GetConnectionString()}"); 17 | 18 | _redisClientManager = await RedisClientManager.ConnectAsync(new RedisClientConfig(connectionString.Host, connectionString.Port) 19 | { 20 | Ssl = false, 21 | PoolSize = 1 22 | }); 23 | 24 | Client = _redisClientManager.GetRedisClient(); 25 | } 26 | 27 | [GlobalCleanup] 28 | public async Task Cleanup() 29 | { 30 | await RedisContainer.DisposeAsync(); 31 | await _redisClientManager.DisposeAsync(); 32 | await ContainerCleanup(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Client/RedisClientManager.cs: -------------------------------------------------------------------------------- 1 | namespace TomLonghurst.AsyncRedisClient.Client; 2 | 3 | public class RedisClientManager : IAsyncDisposable 4 | { 5 | public RedisClientConfig? ClientConfig { get; } 6 | private readonly CircularQueue _redisClients; 7 | 8 | public static async Task ConnectAsync(RedisClientConfig clientConfig) 9 | { 10 | var manager = new RedisClientManager(clientConfig); 11 | 12 | await manager.InitializeAsync(); 13 | 14 | return manager; 15 | } 16 | 17 | private RedisClientManager(RedisClientConfig clientConfig) 18 | { 19 | var redisClientPoolSize = clientConfig.PoolSize; 20 | 21 | if (redisClientPoolSize < 1) 22 | { 23 | throw new ArgumentOutOfRangeException(nameof(redisClientPoolSize), "Pool size must be 1 or more"); 24 | } 25 | 26 | ClientConfig = clientConfig; 27 | 28 | _redisClients = new(async () => await RedisClient.ConnectAsync(clientConfig), redisClientPoolSize); 29 | } 30 | 31 | private Task InitializeAsync() 32 | { 33 | return _redisClients.InitializeAsync(); 34 | } 35 | 36 | public RedisClient GetRedisClient() 37 | { 38 | return _redisClients.Get(); 39 | } 40 | 41 | public ValueTask DisposeAsync() 42 | { 43 | return _redisClients.DisposeAsync(); 44 | } 45 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Models/ResultProcessors/SuccessAbstractResultProcessor.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Pipelines; 2 | using TomLonghurst.AsyncRedisClient.Client; 3 | using TomLonghurst.AsyncRedisClient.Constants; 4 | using TomLonghurst.AsyncRedisClient.Exceptions; 5 | using TomLonghurst.AsyncRedisClient.Extensions; 6 | 7 | namespace TomLonghurst.AsyncRedisClient.Models.ResultProcessors; 8 | 9 | public class SuccessResultProcessor : AbstractResultProcessor 10 | { 11 | internal override async ValueTask Process( 12 | RedisClient redisClient, 13 | PipeReader pipeReader, 14 | ReadResult readResult, 15 | CancellationToken cancellationToken 16 | ) 17 | { 18 | var line = await ReadLine(pipeReader, cancellationToken); 19 | 20 | if (line.Length < 3 || 21 | line.ItemAt(0) != ByteConstants.Plus || 22 | line.ItemAt(1) != ByteConstants.O || 23 | line.ItemAt(2) != ByteConstants.K) 24 | { 25 | var stringLine = line.AsStringWithoutLineTerminators(); 26 | pipeReader.AdvanceTo(line.End); 27 | 28 | if (stringLine[0] == ByteConstants.Dash) 29 | { 30 | throw new RedisFailedCommandException(stringLine, redisClient.LastCommand); 31 | } 32 | 33 | throw new UnexpectedRedisResponseException(stringLine); 34 | } 35 | 36 | pipeReader.AdvanceTo(line.End); 37 | 38 | return null; 39 | } 40 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Models/ResultProcessors/WordAbstractResultProcessor.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Pipelines; 2 | using TomLonghurst.AsyncRedisClient.Client; 3 | using TomLonghurst.AsyncRedisClient.Constants; 4 | using TomLonghurst.AsyncRedisClient.Exceptions; 5 | using TomLonghurst.AsyncRedisClient.Extensions; 6 | 7 | namespace TomLonghurst.AsyncRedisClient.Models.ResultProcessors; 8 | 9 | public class SimpleStringResultProcessor : AbstractResultProcessor 10 | { 11 | internal override async ValueTask Process( 12 | RedisClient redisClient, 13 | PipeReader pipeReader, 14 | ReadResult readResult, 15 | CancellationToken cancellationToken 16 | ) 17 | { 18 | var line = await ReadLine(pipeReader, cancellationToken); 19 | 20 | if (line.ItemAt(0) != ByteConstants.Plus) 21 | { 22 | var stringLine = line.AsStringWithoutLineTerminators(); 23 | 24 | pipeReader.AdvanceTo(line.End); 25 | 26 | if (line.ItemAt(0) == ByteConstants.Dash) 27 | { 28 | throw new RedisFailedCommandException(stringLine, redisClient.LastCommand); 29 | } 30 | 31 | throw new UnexpectedRedisResponseException(stringLine); 32 | } 33 | 34 | var word = line.Slice(line.GetPosition(1, line.Start)).AsStringWithoutLineTerminators(); 35 | 36 | pipeReader.AdvanceTo(line.End); 37 | 38 | return word; 39 | } 40 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Models/ResultProcessors/ArrayAbstractResultProcessor.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Pipelines; 2 | using TomLonghurst.AsyncRedisClient.Client; 3 | using TomLonghurst.AsyncRedisClient.Constants; 4 | using TomLonghurst.AsyncRedisClient.Exceptions; 5 | using TomLonghurst.AsyncRedisClient.Extensions; 6 | using TomLonghurst.AsyncRedisClient.Helpers; 7 | 8 | namespace TomLonghurst.AsyncRedisClient.Models.ResultProcessors; 9 | 10 | public class ArrayResultProcessor : AbstractResultProcessor> 11 | { 12 | internal override async ValueTask> Process( 13 | RedisClient redisClient, 14 | PipeReader pipeReader, 15 | ReadResult readResult, 16 | CancellationToken cancellationToken 17 | ) 18 | { 19 | var line = await ReadLine(pipeReader, cancellationToken); 20 | 21 | if (line.ItemAt(0) != ByteConstants.Asterix) 22 | { 23 | var stringLine = line.AsStringWithoutLineTerminators(); 24 | pipeReader.AdvanceTo(line.End); 25 | throw new UnexpectedRedisResponseException(stringLine); 26 | } 27 | 28 | var count = SpanNumberParser.Parse(line); 29 | 30 | pipeReader.AdvanceTo(line.End); 31 | 32 | var results = new byte [count][]; 33 | 34 | for (var i = 0; i < count; i++) 35 | { 36 | results[i] = (await ReadData(redisClient, pipeReader, readResult, cancellationToken)).ToArray(); 37 | } 38 | 39 | return results.ToRedisValues(); 40 | } 41 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Client/RedisClient.Commands.Server.cs: -------------------------------------------------------------------------------- 1 | using TomLonghurst.AsyncRedisClient.Constants; 2 | 3 | namespace TomLonghurst.AsyncRedisClient.Client; 4 | 5 | public partial class RedisClient : IDisposable 6 | { 7 | public ServerCommands Server { get; } 8 | 9 | public class ServerCommands 10 | { 11 | private readonly RedisClient _redisClient; 12 | 13 | internal ServerCommands(RedisClient redisClient) 14 | { 15 | _redisClient = redisClient; 16 | } 17 | 18 | public ValueTask Info() 19 | { 20 | return Info(CancellationToken.None); 21 | } 22 | 23 | public async ValueTask Info(CancellationToken cancellationToken) 24 | { 25 | return await _redisClient.RunWithTimeout(async token => 26 | { 27 | return await _redisClient.SendOrQueueAsync(Commands.Info, _redisClient.DataResultProcessor, CancellationToken.None); 28 | }, cancellationToken); 29 | } 30 | 31 | public Task DbSize() 32 | { 33 | return DbSize(CancellationToken.None); 34 | } 35 | 36 | public async Task DbSize(CancellationToken cancellationToken) 37 | { 38 | return await _redisClient.RunWithTimeout(async token => 39 | { 40 | return await _redisClient.SendOrQueueAsync(Commands.DbSize, _redisClient.IntegerResultProcessor, CancellationToken.None); 41 | }, cancellationToken); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Models/ResultProcessors/IntegerAbstractResultProcessor.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Pipelines; 2 | using TomLonghurst.AsyncRedisClient.Client; 3 | using TomLonghurst.AsyncRedisClient.Constants; 4 | using TomLonghurst.AsyncRedisClient.Exceptions; 5 | using TomLonghurst.AsyncRedisClient.Extensions; 6 | using TomLonghurst.AsyncRedisClient.Helpers; 7 | 8 | namespace TomLonghurst.AsyncRedisClient.Models.ResultProcessors; 9 | 10 | public class IntegerResultProcessor : AbstractResultProcessor 11 | { 12 | internal override async ValueTask Process( 13 | RedisClient redisClient, 14 | PipeReader pipeReader, 15 | ReadResult readResult, 16 | CancellationToken cancellationToken 17 | ) 18 | { 19 | var line = await ReadLine(pipeReader, cancellationToken); 20 | 21 | if (line.ItemAt(0) != ByteConstants.Colon) 22 | { 23 | var stringLine = line.AsStringWithoutLineTerminators(); 24 | pipeReader.AdvanceTo(line.End); 25 | 26 | if (line.ItemAt(0) == ByteConstants.Dash) 27 | { 28 | throw new RedisFailedCommandException(stringLine, redisClient.LastCommand); 29 | } 30 | 31 | throw new UnexpectedRedisResponseException(stringLine); 32 | 33 | } 34 | 35 | var number = SpanNumberParser.Parse(line); 36 | 37 | pipeReader.AdvanceTo(line.End); 38 | 39 | if (number == -1) 40 | { 41 | return -1; 42 | } 43 | 44 | return (int) number; 45 | } 46 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/AsyncCircularQueue.cs: -------------------------------------------------------------------------------- 1 | namespace TomLonghurst.AsyncRedisClient; 2 | 3 | public class CircularQueue : IAsyncDisposable 4 | { 5 | private readonly Func> _objectGenerator; 6 | private readonly int _maxPoolSize; 7 | private T[]? _clients; 8 | private readonly Lock _lock = new(); 9 | 10 | private int _index = -1; 11 | 12 | public CircularQueue(Func> objectGenerator, int maxPoolSize) 13 | { 14 | _maxPoolSize = maxPoolSize; 15 | _objectGenerator = objectGenerator ?? throw new ArgumentNullException(nameof(objectGenerator)); 16 | } 17 | 18 | public async Task InitializeAsync() 19 | { 20 | var clients = Enumerable.Range(0, _maxPoolSize) 21 | .Select(_ => _objectGenerator()). 22 | ToArray(); 23 | 24 | _clients = await Task.WhenAll(clients); 25 | } 26 | 27 | public T Get() 28 | { 29 | if (_clients!.Length == 1) 30 | { 31 | return _clients[0]; 32 | } 33 | 34 | return _clients![GetIndex()]; 35 | } 36 | 37 | private int GetIndex() 38 | { 39 | lock (_lock) 40 | { 41 | var index = ++_index; 42 | 43 | if (index == _maxPoolSize - 1) 44 | { 45 | return _index = 0; 46 | } 47 | 48 | return index; 49 | } 50 | } 51 | 52 | public async ValueTask DisposeAsync() 53 | { 54 | await Task.WhenAll(_clients!.Select(DisposeAsync)); 55 | } 56 | 57 | private async Task DisposeAsync(T t) 58 | { 59 | if (t is IAsyncDisposable asyncDisposable) 60 | { 61 | await asyncDisposable.DisposeAsync(); 62 | } 63 | 64 | if (t is IDisposable disposable) 65 | { 66 | disposable.Dispose(); 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Constants/LastActionConstants.cs: -------------------------------------------------------------------------------- 1 | namespace TomLonghurst.AsyncRedisClient.Constants; 2 | 3 | public class LastActionConstants 4 | { 5 | internal static readonly string Connecting = "Connecting"; 6 | internal static readonly string Reconnecting = "Reconnecting"; 7 | internal static readonly string Authorizing = "Authorizing"; 8 | internal static readonly string SelectingDatabase = "Selecting Database"; 9 | internal static readonly string SettingClientName = "Setting Client Name"; 10 | 11 | internal static readonly string WaitingForConnectingLock = "Waiting for Connecting lock to be free"; 12 | internal static readonly string AuthenticatingSslStreamAsClient = "Authenticating SSL Stream as Client"; 13 | internal static readonly string CreatingSslStreamPipe = "Creating SSL Stream Pipe"; 14 | internal static readonly string CreatingSocketPipe = "Creating Socket Pipe"; 15 | 16 | internal static readonly string WritingBytes = "Writing Bytes"; 17 | 18 | internal static readonly string ThrowingCancelledException = "Throwing Cancelled Exception due to Cancelled Token"; 19 | internal static readonly string ReadingDataInReadData = "Reading Data in ReadData"; 20 | internal static readonly string ReadingDataInReadDataLoop = "Reading Data in ReadData Loop"; 21 | internal static readonly string AdvancingBufferInReadDataLoop = "Advancing Buffer in ReadData Loop"; 22 | internal static readonly string FindingEndOfLinePosition = "Finding End of Line Position"; 23 | internal static readonly string ReadingUntilEndOfLinePositionFound = "Reading until End of Line found"; 24 | internal static readonly string StartingResultProcessor = "Starting ResultProcessor.Processor"; 25 | 26 | internal static readonly string DisposingClient = "Disposing Client"; 27 | internal static readonly string DisposingNetwork = "Disposing Network"; 28 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Client/RedisClient.Backlog.cs: -------------------------------------------------------------------------------- 1 | using TomLonghurst.AsyncRedisClient.Models.Backlog; 2 | 3 | namespace TomLonghurst.AsyncRedisClient.Client; 4 | 5 | public partial class RedisClient 6 | { 7 | private readonly BlockingQueue _backlog = new(); 8 | 9 | private void StartBacklogProcessor() 10 | { 11 | Task.Run(ProcessBacklog); 12 | } 13 | 14 | private async Task ProcessBacklog() 15 | { 16 | while (true) 17 | { 18 | try 19 | { 20 | if (!IsConnected) 21 | { 22 | await TryConnectAsync(CancellationToken.None); 23 | } 24 | 25 | var backlogItems = _backlog.DequeueAll(); 26 | 27 | // Items cancelled will be taken care of by the CancellationToken.Register in the SendOrQueue method 28 | var validItems = backlogItems.Where(item => !item.CancellationToken.IsCancellationRequested) 29 | .ToList(); 30 | 31 | var pipelinedCommand = validItems 32 | .SelectMany(backlogItem => backlogItem.RedisCommand) 33 | .ToArray(); 34 | 35 | try 36 | { 37 | await Write(pipelinedCommand); 38 | 39 | foreach (var backlogItem in validItems) 40 | { 41 | await backlogItem.SetResult(); 42 | } 43 | } 44 | catch (Exception e) 45 | { 46 | foreach (var validItem in validItems) 47 | { 48 | validItem.SetException(e); 49 | } 50 | 51 | DisposeNetwork(); 52 | } 53 | } 54 | catch 55 | { 56 | DisposeNetwork(); 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Helpers/SpanNumberParser.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using TomLonghurst.AsyncRedisClient.Constants; 3 | using TomLonghurst.AsyncRedisClient.Extensions; 4 | 5 | namespace TomLonghurst.AsyncRedisClient.Helpers; 6 | 7 | internal static class SpanNumberParser 8 | { 9 | internal static int Parse(ReadOnlySequence buffer) 10 | { 11 | if (buffer.IsEmpty) 12 | { 13 | return 0; 14 | } 15 | 16 | if (buffer.Length >= 2 && buffer.ItemAt(0) == ByteConstants.Dash && buffer.ItemAt(1) == ByteConstants.One) 17 | { 18 | return -1; 19 | } 20 | 21 | if (!char.IsDigit((char) buffer.ItemAt(0)) && buffer.ItemAt(0) != ByteConstants.Dash) 22 | { 23 | return Parse(buffer.Slice(buffer.GetPosition(1, buffer.Start))); 24 | } 25 | 26 | if (buffer.GetEndOfLinePosition() != null) 27 | { 28 | return Parse(buffer.Slice(buffer.Start, buffer.Length - 2)); 29 | } 30 | 31 | return ParseSequence(buffer); 32 | } 33 | 34 | internal static long Parse(params byte[] byteValues) 35 | { 36 | return (long) byteValues.Select((t, i) => GetValue(t) * Math.Pow(10, (double) byteValues.Length - i - 1)).Sum(); 37 | } 38 | 39 | internal static int ParseSequence(ReadOnlySequence byteValues) 40 | { 41 | var result = 0; 42 | var outerIndex = 0; 43 | foreach (var readOnlyMemory in byteValues) 44 | { 45 | foreach (var b in readOnlyMemory.Span) 46 | { 47 | result += (char) GetValue(b) * (int) Math.Pow(10, byteValues.Length - 1 - outerIndex); 48 | outerIndex++; 49 | } 50 | } 51 | 52 | return result; 53 | } 54 | 55 | internal static long GetValue(byte byteValue) 56 | { 57 | return (long) char.GetNumericValue((char) byteValue); 58 | } 59 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Models/Backlog/BacklogItem.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Pipelines; 2 | using TomLonghurst.AsyncRedisClient.Client; 3 | using TomLonghurst.AsyncRedisClient.Models.ResultProcessors; 4 | 5 | namespace TomLonghurst.AsyncRedisClient.Models.Backlog; 6 | 7 | public struct BacklogItem : IBacklogItem 8 | { 9 | public RedisClient RedisClient { get; set; } 10 | public PipeReader PipeReader { get; set; } 11 | public byte[] RedisCommand { get; } 12 | public CancellationToken CancellationToken { get; } 13 | 14 | public void SetCancelled() 15 | { 16 | TaskCompletionSource.TrySetCanceled(); 17 | } 18 | 19 | public void SetException(Exception exception) 20 | { 21 | TaskCompletionSource.TrySetException(exception); 22 | } 23 | 24 | public async Task SetResult() 25 | { 26 | try 27 | { 28 | var result = await AbstractResultProcessor.Start(RedisClient, PipeReader, new ReadResult(), CancellationToken); 29 | TaskCompletionSource.TrySetResult(result); 30 | } 31 | catch (OperationCanceledException) 32 | { 33 | TaskCompletionSource.TrySetCanceled(); 34 | throw; 35 | } 36 | catch (Exception e) 37 | { 38 | TaskCompletionSource.TrySetException(e); 39 | throw; 40 | } 41 | } 42 | 43 | public TaskCompletionSource TaskCompletionSource { get; } 44 | public AbstractResultProcessor AbstractResultProcessor { get; } 45 | 46 | public BacklogItem(byte[] redisCommand, CancellationToken cancellationToken, TaskCompletionSource taskCompletionSource, AbstractResultProcessor abstractResultProcessor, RedisClient redisClient, PipeReader pipe) 47 | { 48 | RedisCommand = redisCommand; 49 | CancellationToken = cancellationToken; 50 | TaskCompletionSource = taskCompletionSource; 51 | AbstractResultProcessor = abstractResultProcessor; 52 | RedisClient = redisClient; 53 | PipeReader = pipe; 54 | } 55 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Client/RedisClient.Commands.Scripts.cs: -------------------------------------------------------------------------------- 1 | using TomLonghurst.AsyncRedisClient.Constants; 2 | using TomLonghurst.AsyncRedisClient.Models; 3 | using TomLonghurst.AsyncRedisClient.Models.Commands; 4 | 5 | namespace TomLonghurst.AsyncRedisClient.Client; 6 | 7 | public partial class RedisClient : IDisposable 8 | { 9 | public ScriptCommands Scripts { get; } 10 | 11 | public class ScriptCommands 12 | { 13 | private readonly RedisClient _redisClient; 14 | 15 | internal LuaScript? MultiExpireScript; 16 | internal LuaScript? MultiSetexScript; 17 | 18 | internal ScriptCommands(RedisClient redisClient) 19 | { 20 | _redisClient = redisClient; 21 | } 22 | 23 | public async Task FlushScripts(CancellationToken cancellationToken) 24 | { 25 | await _redisClient.RunWithTimeout(async token => 26 | { 27 | await _redisClient.SendOrQueueAsync(Commands.ScriptFlush, _redisClient.SuccessResultProcessor, token); 28 | }, cancellationToken); 29 | } 30 | 31 | public async Task LoadScript(string script, CancellationToken cancellationToken) 32 | { 33 | var command = RedisCommand.From(Commands.Script, Commands.Load, script); 34 | 35 | var scriptResponse = await _redisClient.SendOrQueueAsync(command, _redisClient.DataResultProcessor, cancellationToken); 36 | 37 | return new LuaScript(_redisClient, scriptResponse); 38 | } 39 | 40 | internal async Task EvalSha(string sha1Hash, IEnumerable keys, IEnumerable arguments, CancellationToken cancellationToken) 41 | { 42 | // TODO: 43 | return default; 44 | // var keysList = keys.ToList(); 45 | // var command = RedisCommand.FromScript(Commands.EvalSha, sha1Hash.ToRedisEncoded(), keysList, arguments); 46 | // 47 | // var scriptResult = await _redisClient.RunWithTimeout(async token => 48 | // { 49 | // return await _redisClient.SendOrQueueAsync(command, _redisClient.GenericResultProcessor, token); 50 | // }, 51 | // cancellationToken); 52 | // 53 | // return scriptResult; 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Models/Commands/RedisInput.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Collections.ObjectModel; 3 | using System.Text; 4 | using TomLonghurst.AsyncRedisClient.Constants; 5 | 6 | namespace TomLonghurst.AsyncRedisClient.Models.Commands; 7 | 8 | public ref struct RedisInput 9 | { 10 | public byte[] Bytes { get; } 11 | 12 | public RedisInput(ReadOnlySpan input) 13 | { 14 | var byteCount = Encoding.UTF8.GetByteCount(input); 15 | Bytes = new byte[byteCount + 2]; 16 | Encoding.UTF8.GetBytes(input, Bytes); 17 | Bytes[^2] = ByteConstants.BackslashR; 18 | Bytes[^1] = ByteConstants.NewLine; 19 | } 20 | 21 | public RedisInput(byte[] input) 22 | { 23 | Bytes = input; 24 | } 25 | 26 | public RedisInput(int input) 27 | { 28 | Bytes = BitConverter.GetBytes(input); 29 | } 30 | 31 | public RedisInput(long input) 32 | { 33 | Bytes = BitConverter.GetBytes(input); 34 | } 35 | 36 | public RedisInput(double input) 37 | { 38 | Bytes = BitConverter.GetBytes(input); 39 | } 40 | 41 | public RedisInput(float input) 42 | { 43 | Bytes = BitConverter.GetBytes(input); 44 | } 45 | 46 | public RedisInput(IEnumerable inputs) 47 | { 48 | Bytes = inputs.SelectMany(x => new RedisInput(x).Bytes).ToArray(); 49 | } 50 | 51 | public static implicit operator RedisInput(Span input) => new(input); 52 | public static implicit operator RedisInput(string input) => new(input); 53 | public static implicit operator RedisInput(byte[] input) => new(input); 54 | public static implicit operator RedisInput(int input) => new(input); 55 | public static implicit operator RedisInput(long input) => new(input); 56 | public static implicit operator RedisInput(float input) => new(input); 57 | public static implicit operator RedisInput(double input) => new(input); 58 | public static implicit operator RedisInput(string[] inputs) => new(inputs); 59 | public static implicit operator RedisInput(List inputs) => new(inputs); 60 | public static implicit operator RedisInput(Collection inputs) => new(inputs); 61 | public static implicit operator RedisInput(ImmutableArray inputs) => new(inputs); 62 | public static implicit operator RedisInput(ImmutableList inputs) => new(inputs); 63 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Models/Commands/RedisCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace TomLonghurst.AsyncRedisClient.Models.Commands; 4 | 5 | public readonly ref struct RedisCommand 6 | { 7 | public byte[] EncodedBytes { get; } 8 | 9 | public string ConvertToString() => Encoding.UTF8.GetString(EncodedBytes); 10 | 11 | private RedisCommand(ReadOnlySpan input) 12 | { 13 | EncodedBytes = BytesEncoder.EncodeRawBytes(input); 14 | } 15 | 16 | private RedisCommand(ReadOnlySpan input1, ReadOnlySpan input2) 17 | { 18 | EncodedBytes = BytesEncoder.EncodeRawBytes(input1, input2); 19 | } 20 | 21 | private RedisCommand(ReadOnlySpan input1, ReadOnlySpan input2, ReadOnlySpan input3) 22 | { 23 | EncodedBytes = BytesEncoder.EncodeRawBytes(input1, input2, input3); 24 | } 25 | 26 | private RedisCommand(ReadOnlySpan input1, ReadOnlySpan input2, ReadOnlySpan input3, ReadOnlySpan input4) 27 | { 28 | EncodedBytes = BytesEncoder.EncodeRawBytes(input1, input2, input3, input4); 29 | } 30 | 31 | private RedisCommand(ReadOnlySpan input1, ReadOnlySpan input2, ReadOnlySpan input3, ReadOnlySpan input4, ReadOnlySpan input5) 32 | { 33 | EncodedBytes = BytesEncoder.EncodeRawBytes(input1, input2, input3, input4, input5); 34 | } 35 | 36 | public static RedisCommand From(RedisInput input) 37 | { 38 | return new RedisCommand(input.Bytes); 39 | } 40 | 41 | public static RedisCommand From(RedisInput input1, RedisInput input2) 42 | { 43 | return new RedisCommand(input1.Bytes, input2.Bytes); 44 | } 45 | 46 | public static RedisCommand From(RedisInput input1, RedisInput input2, RedisInput input3) 47 | { 48 | return new RedisCommand(input1.Bytes, input2.Bytes, input3.Bytes); 49 | } 50 | 51 | public static RedisCommand From(RedisInput input1, RedisInput input2, RedisInput input3, RedisInput input4) 52 | { 53 | return new RedisCommand(input1.Bytes, input2.Bytes, input3.Bytes, input4.Bytes); 54 | } 55 | 56 | public static RedisCommand From(RedisInput input1, RedisInput input2, RedisInput input3, RedisInput input4, RedisInput input5) 57 | { 58 | return new RedisCommand(input1.Bytes, input2.Bytes, input3.Bytes, input4.Bytes, input5.Bytes); 59 | } 60 | 61 | public static implicit operator byte[](RedisCommand command) => command.EncodedBytes; 62 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Models/ResultProcessors/GenericAbstractResultProcessor.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Pipelines; 2 | using TomLonghurst.AsyncRedisClient.Client; 3 | using TomLonghurst.AsyncRedisClient.Constants; 4 | using TomLonghurst.AsyncRedisClient.Exceptions; 5 | using TomLonghurst.AsyncRedisClient.Extensions; 6 | 7 | namespace TomLonghurst.AsyncRedisClient.Models.ResultProcessors; 8 | 9 | public class GenericResultProcessor : AbstractResultProcessor 10 | { 11 | internal override async ValueTask Process( 12 | RedisClient redisClient, 13 | PipeReader pipeReader, 14 | ReadResult readResult, 15 | CancellationToken cancellationToken 16 | ) 17 | { 18 | var firstChar = await ReadByte(pipeReader, cancellationToken); 19 | 20 | pipeReader.AdvanceTo(readResult.Buffer.Start, readResult.Buffer.Slice(1).Start); 21 | 22 | if (firstChar == ByteConstants.Dash) 23 | { 24 | var line = await ReadLine(pipeReader, cancellationToken); 25 | var redisResponse = line.AsString(); 26 | pipeReader.AdvanceTo(line.End); 27 | throw new RedisFailedCommandException(redisResponse, redisClient.LastCommand); 28 | } 29 | 30 | var result = ProcessData(redisClient, pipeReader, readResult, firstChar, cancellationToken); 31 | 32 | return new RawResult(result); 33 | } 34 | 35 | private async ValueTask ProcessData(RedisClient redisClient, 36 | PipeReader pipeReader, 37 | ReadResult readResult, 38 | byte firstChar, 39 | CancellationToken cancellationToken) 40 | { 41 | if (firstChar == ByteConstants.Asterix) 42 | { 43 | return await redisClient.ArrayResultProcessor.Process(redisClient, pipeReader, readResult, cancellationToken); 44 | } 45 | 46 | if (firstChar == ByteConstants.Plus) 47 | { 48 | return await redisClient.SimpleStringResultProcessor.Process(redisClient, pipeReader, readResult, cancellationToken); 49 | } 50 | 51 | if (firstChar == ByteConstants.Colon) 52 | { 53 | return await redisClient.IntegerResultProcessor.Process(redisClient, pipeReader, readResult, cancellationToken); 54 | } 55 | 56 | if (firstChar == ByteConstants.Dollar) 57 | { 58 | return await redisClient.DataResultProcessor.Process(redisClient, pipeReader, readResult, cancellationToken); 59 | } 60 | 61 | return await redisClient.EmptyResultProcessor.Process(redisClient, pipeReader, readResult, cancellationToken); 62 | } 63 | } -------------------------------------------------------------------------------- /RedisClient.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28307.168 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TomLonghurst.AsyncRedisClient", "TomLonghurst.AsyncRedisClient\TomLonghurst.AsyncRedisClient.csproj", "{DBEC818D-7F09-4D48-B13D-01234EC89D8D}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedisClientTest", "RedisClientTest\RedisClientTest.csproj", "{397634B9-CD0F-4E6F-89F1-D861700D6459}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Playground", "Playground\Playground.csproj", "{5B2A1F87-B7D8-437F-8642-BB4C9BFF67CA}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmark", "Benchmark\Benchmark.csproj", "{9EFF2272-9629-40E7-AA0B-DA862494F395}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {DBEC818D-7F09-4D48-B13D-01234EC89D8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {DBEC818D-7F09-4D48-B13D-01234EC89D8D}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {DBEC818D-7F09-4D48-B13D-01234EC89D8D}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {DBEC818D-7F09-4D48-B13D-01234EC89D8D}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {397634B9-CD0F-4E6F-89F1-D861700D6459}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {397634B9-CD0F-4E6F-89F1-D861700D6459}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {397634B9-CD0F-4E6F-89F1-D861700D6459}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {397634B9-CD0F-4E6F-89F1-D861700D6459}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {5B2A1F87-B7D8-437F-8642-BB4C9BFF67CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {5B2A1F87-B7D8-437F-8642-BB4C9BFF67CA}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {5B2A1F87-B7D8-437F-8642-BB4C9BFF67CA}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {5B2A1F87-B7D8-437F-8642-BB4C9BFF67CA}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {9EFF2272-9629-40E7-AA0B-DA862494F395}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {9EFF2272-9629-40E7-AA0B-DA862494F395}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {9EFF2272-9629-40E7-AA0B-DA862494F395}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {9EFF2272-9629-40E7-AA0B-DA862494F395}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {F362F12F-F814-4AD4-953C-4D701AFF116A} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/BlockingQueue.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | 3 | namespace TomLonghurst.AsyncRedisClient; 4 | 5 | public class BlockingQueue : IDisposable 6 | { 7 | private readonly object _locker = new(); 8 | private readonly ConcurrentQueue _innerQueue; 9 | 10 | public int Count => _innerQueue.Count; 11 | 12 | private int _availableToDequeue; 13 | private bool _disposed; 14 | 15 | public BlockingQueue() 16 | { 17 | _innerQueue = new ConcurrentQueue(); 18 | } 19 | 20 | public void Enqueue(T item) 21 | { 22 | _innerQueue.Enqueue(item); 23 | 24 | lock (_locker) 25 | { 26 | if (_availableToDequeue != 0) 27 | { 28 | Monitor.Pulse(_locker); 29 | } 30 | } 31 | } 32 | 33 | public void EnqueueRange(IEnumerable source) 34 | { 35 | foreach (var item in source) 36 | { 37 | lock (_locker) 38 | { 39 | _innerQueue.Enqueue(item); 40 | 41 | if (_availableToDequeue != 0) 42 | { 43 | Monitor.Pulse(_locker); 44 | } 45 | } 46 | } 47 | } 48 | 49 | public T? Dequeue() 50 | { 51 | // Used to avoid returning null 52 | while (true) 53 | { 54 | lock (_locker) 55 | { 56 | while (Count == 0) 57 | { 58 | if (_disposed) 59 | { 60 | return default; 61 | } 62 | 63 | _availableToDequeue++; 64 | Monitor.Wait(_locker); 65 | _availableToDequeue--; 66 | } 67 | 68 | if (_innerQueue.TryDequeue(out var item)) 69 | { 70 | return item; 71 | } 72 | } 73 | } 74 | } 75 | 76 | public List DequeueAll() 77 | { 78 | // Used to avoid returning null 79 | while (true) 80 | { 81 | lock (_locker) 82 | { 83 | while (Count == 0) 84 | { 85 | if (_disposed) 86 | { 87 | return []; 88 | } 89 | 90 | _availableToDequeue++; 91 | Monitor.Wait(_locker); 92 | _availableToDequeue--; 93 | } 94 | 95 | var list = new List(); 96 | 97 | while (true) 98 | { 99 | if (_innerQueue.TryDequeue(out var item)) 100 | { 101 | list.Add(item); 102 | } 103 | else 104 | { 105 | return list; 106 | } 107 | } 108 | } 109 | } 110 | } 111 | 112 | public void Dispose() 113 | { 114 | _disposed = true; 115 | 116 | lock (_locker) 117 | { 118 | Monitor.PulseAll(_locker); 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A[sync]RedisClient 2 | 3 | [![nuget](https://img.shields.io/nuget/v/TomLonghurst.AsyncRedisClient.svg)](https://www.nuget.org/packages/TomLonghurst.AsyncRedisClient/) 4 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/b692f8fbb14142d3ad8f9ccb65d0889c)](https://app.codacy.com/app/thomhurst/A-sync-RedisClient?utm_source=github.com&utm_medium=referral&utm_content=thomhurst/A-sync-RedisClient&utm_campaign=Badge_Grade_Dashboard) 5 | 6 | ## Please note this is still in development 7 | 8 | ## Install 9 | 10 | Install via Nuget > `Install-Package TomLonghurst.AsyncRedisClient` 11 | 12 | ## Usage 13 | 14 | ### Connect 15 | Create a `RedisClientConfig` object: 16 | 17 | ```csharp 18 | var config = new RedisClientConfig(Host, Port, Password) { 19 | Ssl = true, 20 | Timeout = 5000 21 | }; 22 | ``` 23 | 24 | Create a new `RedisClientManager` object: 25 | 26 | ```csharp 27 | int poolSize = 5; 28 | var redisManager = new RedisClientManager(config, poolSize); 29 | ``` 30 | 31 | Call `RedisClientManager.GetRedisClientAsync()` 32 | 33 | ```csharp 34 | var client = await redisManager.GetRedisClientAsync(); 35 | ``` 36 | 37 | #### Pool Size 38 | Each Redis Client can only perform one operation at a time. Because it's usually very fast, one is enough for most applications. 39 | However if your application takes heavy traffic, and you are seeing `RedisTimeoutException`s then consider upping the pool size. 40 | 41 | ### Commands 42 | 43 | #### Ping 44 | ```csharp 45 | var ping = await client.Ping(); 46 | ``` 47 | 48 | #### Set 49 | ```csharp 50 | await _client.StringSetAsync("key", "123", AwaitOptions.FireAndForget); 51 | ``` 52 | 53 | #### Set with TimeToLive 54 | ```csharp 55 | await _client.StringSetAsync("key", "123", 120, AwaitOptions.FireAndForget); 56 | ``` 57 | 58 | #### Multi Set 59 | ```csharp 60 | var keyValues = new List>() 61 | { 62 | new KeyValuePair("key1", "1"), 63 | new KeyValuePair("key2", "2"), 64 | new KeyValuePair("key3", "3") 65 | }; 66 | await _client.StringSetAsync(keyValues, AwaitOptions.AwaitCompletion); 67 | ``` 68 | 69 | #### Get 70 | ```csharp 71 | var value = await _client.StringGetAsync("key"); 72 | ``` 73 | 74 | #### Multi Get 75 | ```csharp 76 | var values = await _client.StringGetAsync(new [] { "key1", "key2" }); 77 | ``` 78 | 79 | #### Delete 80 | ```csharp 81 | await _client.DeleteKeyAsync("key", AwaitOptions.AwaitCompletion); 82 | ``` 83 | 84 | #### Multi Delete 85 | ```csharp 86 | await _client.DeleteKeyAsync(new [] { "key1", "key2" }, AwaitOptions.AwaitCompletion); 87 | ``` 88 | 89 | #### Key Exists 90 | ```csharp 91 | var exists = await _client.KeyExistsAsync("key"); 92 | ``` 93 | 94 | ### AwaitOptions 95 | Any method taking an AwaitOptions parameter has two options: 96 | 97 | #### AwaitCompletion 98 | Wait for the operation to complete on the Redis server before resuming program execution 99 | 100 | #### FireAndForget 101 | Resume with program execution instantly and forget about checking the result 102 | 103 | If you enjoy, please buy me a coffee :) 104 | 105 | Buy Me A Coffee 106 | -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/BytesEncoder.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using TomLonghurst.AsyncRedisClient.Extensions; 3 | 4 | namespace TomLonghurst.AsyncRedisClient; 5 | 6 | internal static class BytesEncoder 7 | { 8 | private static readonly byte[] OneElement = ((ReadOnlySpan)"*1").ToUtf8BytesWithTerminator(); 9 | private static readonly byte[] TwoElements = ((ReadOnlySpan)"*2").ToUtf8BytesWithTerminator(); 10 | private static readonly byte[] ThreeElements = ((ReadOnlySpan)"*3").ToUtf8BytesWithTerminator(); 11 | private static readonly byte[] FourElements = ((ReadOnlySpan)"*4").ToUtf8BytesWithTerminator(); 12 | private static readonly byte[] FiveElements = ((ReadOnlySpan)"*5").ToUtf8BytesWithTerminator(); 13 | public static byte[] EncodeRawBytes(ReadOnlySpan bytes) 14 | { 15 | 16 | return 17 | [ 18 | ..OneElement, 19 | ..GetPrefixLengthForBytes(bytes), 20 | ..bytes 21 | ]; 22 | } 23 | 24 | public static byte[] EncodeRawBytes(ReadOnlySpan bytes1, ReadOnlySpan bytes2) 25 | { 26 | return [ 27 | ..TwoElements, 28 | ..GetPrefixLengthForBytes(bytes1), 29 | ..bytes1, 30 | ..GetPrefixLengthForBytes(bytes2), 31 | ..bytes2 32 | ]; 33 | } 34 | 35 | public static byte[] EncodeRawBytes(ReadOnlySpan bytes1, ReadOnlySpan bytes2, ReadOnlySpan bytes3) 36 | { 37 | return [ 38 | ..ThreeElements, 39 | ..GetPrefixLengthForBytes(bytes1), 40 | ..bytes1, 41 | ..GetPrefixLengthForBytes(bytes2), 42 | ..bytes2, 43 | ..GetPrefixLengthForBytes(bytes3), 44 | ..bytes3 45 | ]; 46 | } 47 | 48 | public static byte[] EncodeRawBytes(ReadOnlySpan bytes1, ReadOnlySpan bytes2, ReadOnlySpan bytes3, ReadOnlySpan bytes4) 49 | { 50 | return [ 51 | ..FourElements, 52 | ..GetPrefixLengthForBytes(bytes1), 53 | ..bytes1, 54 | ..GetPrefixLengthForBytes(bytes2), 55 | ..bytes2, 56 | ..GetPrefixLengthForBytes(bytes3), 57 | ..bytes3, 58 | ..GetPrefixLengthForBytes(bytes4), 59 | ..bytes4 60 | ]; 61 | } 62 | 63 | public static byte[] EncodeRawBytes(ReadOnlySpan bytes1, ReadOnlySpan bytes2, ReadOnlySpan bytes3, ReadOnlySpan bytes4, ReadOnlySpan bytes5) 64 | { 65 | return [ 66 | ..FiveElements, 67 | ..GetPrefixLengthForBytes(bytes1), 68 | ..bytes1, 69 | ..GetPrefixLengthForBytes(bytes2), 70 | ..bytes2, 71 | ..GetPrefixLengthForBytes(bytes3), 72 | ..bytes3, 73 | ..GetPrefixLengthForBytes(bytes4), 74 | ..bytes4, 75 | ..GetPrefixLengthForBytes(bytes5), 76 | ..bytes5 77 | ]; 78 | } 79 | 80 | public static byte[] EncodeRawBytes(byte[][] bytes) 81 | { 82 | return bytes.SelectMany(x => EncodeRawBytes(x)).ToArray(); 83 | } 84 | 85 | [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] 86 | private static byte[] GetPrefixLengthForBytes(ReadOnlySpan bytes) 87 | { 88 | ReadOnlySpan lengthPrefix = $"${bytes.Length - 2}"; 89 | 90 | return lengthPrefix.ToUtf8BytesWithTerminator(); 91 | } 92 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Text; 3 | using TomLonghurst.AsyncRedisClient.Constants; 4 | using TomLonghurst.AsyncRedisClient.Models.Commands; 5 | 6 | namespace TomLonghurst.AsyncRedisClient.Extensions; 7 | 8 | internal static class StringExtensions 9 | { 10 | internal static unsafe byte[] ToUtf8Bytes(this string value) 11 | { 12 | var encodedLength = Encoding.UTF8.GetByteCount(value); 13 | var byteArray = new byte[encodedLength]; 14 | 15 | fixed (char* charPtr = value) 16 | { 17 | fixed (byte* bytePtr = byteArray) 18 | { 19 | Encoding.UTF8.GetBytes(charPtr, value.Length, bytePtr, encodedLength); 20 | } 21 | } 22 | 23 | return byteArray; 24 | } 25 | 26 | [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] 27 | internal static unsafe byte[] ToUtf8BytesWithTerminator(this ReadOnlySpan value) 28 | { 29 | var encodedLength = Encoding.UTF8.GetByteCount(value); 30 | var byteArray = new byte[encodedLength + 2]; 31 | 32 | fixed (char* charPtr = value) 33 | { 34 | fixed (byte* bytePtr = byteArray) 35 | { 36 | Encoding.UTF8.GetBytes(charPtr, value.Length, bytePtr, encodedLength); 37 | } 38 | } 39 | 40 | byteArray[encodedLength] = ByteConstants.BackslashR; 41 | byteArray[encodedLength + 1] = ByteConstants.NewLine; 42 | 43 | return byteArray; 44 | } 45 | 46 | 47 | internal static unsafe int AsUtf8BytesSpan(this string value, out Span bytesSpan) 48 | { 49 | var charsSpan = value.AsSpan(); 50 | 51 | fixed(char* charPtr = charsSpan) 52 | { 53 | bytesSpan = new byte[Encoding.UTF8.GetByteCount(charPtr, charsSpan.Length)].AsSpan(); 54 | fixed (byte* bytePtr = bytesSpan) 55 | { 56 | return Encoding.UTF8.GetBytes(charPtr, charsSpan.Length, bytePtr, bytesSpan.Length); 57 | } 58 | } 59 | } 60 | 61 | 62 | internal static unsafe int AsUtf8BytesSpanWithTerminator(this string value, out Span bytesSpan) 63 | { 64 | var charsSpan = value.AsSpan(); 65 | int encodedLength; 66 | 67 | fixed(char* charPtr = charsSpan) 68 | { 69 | encodedLength = Encoding.UTF8.GetByteCount(charPtr, charsSpan.Length); 70 | bytesSpan = new byte[encodedLength + 2].AsSpan(); 71 | fixed (byte* bytePtr = bytesSpan) 72 | { 73 | Encoding.UTF8.GetBytes(charPtr, charsSpan.Length, bytePtr, bytesSpan.Length); 74 | } 75 | } 76 | 77 | bytesSpan[encodedLength] = ByteConstants.BackslashR; 78 | bytesSpan[encodedLength + 1] = ByteConstants.NewLine; 79 | 80 | return encodedLength + 2; 81 | } 82 | 83 | 84 | internal static IEnumerable Split(this string value, string delimiter) 85 | { 86 | return value.Split([delimiter], StringSplitOptions.RemoveEmptyEntries); 87 | } 88 | 89 | // internal static IRedisCommand ToPipelinedCommand(this IEnumerable commands) 90 | // { 91 | // var enumerable = commands.ToList(); 92 | // 93 | // if (enumerable.Count > 1) 94 | // { 95 | // return MultiRedisCommand.From(enumerable); 96 | // } 97 | // 98 | // return enumerable[0]; 99 | // } 100 | } -------------------------------------------------------------------------------- /Playground/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Threading.Tasks; 5 | using Testcontainers.Redis; 6 | using TomLonghurst.AsyncRedisClient.Client; 7 | 8 | namespace Playground; 9 | 10 | class Program 11 | { 12 | private static RedisClientManager _redisManager; 13 | private static RedisClient TomLonghurstRedisClient => _redisManager.GetRedisClient(); 14 | 15 | private static readonly List> TestData = []; 16 | private static readonly Dictionary _lastActive = new(); 17 | 18 | 19 | static async Task Main(string[] args) 20 | { 21 | var currentProcessId = Process.GetCurrentProcess().Id; 22 | 23 | await using var redisContainer = new RedisBuilder() 24 | .WithImage("redis:7.4.1") 25 | .Build(); 26 | 27 | var connectionString = new Uri($"https://{redisContainer.GetConnectionString()}"); 28 | 29 | for (int i = 0; i < 10000; i++) 30 | { 31 | TestData.Add(new KeyValuePair(CreateString(20), CreateString(50000))); 32 | } 33 | 34 | var runForDuration = TimeSpan.FromMinutes(5); 35 | 36 | var start = DateTime.Now; 37 | 38 | var config = new RedisClientConfig(connectionString.Host, connectionString.Port) 39 | { 40 | Ssl = false 41 | }; 42 | 43 | _redisManager = await RedisClientManager.ConnectAsync(config); 44 | 45 | var tasks = new List(); 46 | 47 | for (var taskCount = 0; taskCount < 150; taskCount++) 48 | { 49 | var taskId = taskCount; 50 | var task = Task.Run(async () => 51 | { 52 | try 53 | { 54 | while (DateTime.Now - start < runForDuration) 55 | { 56 | var tomLonghurstRedisClientStopwatch = Stopwatch.StartNew(); 57 | 58 | await DoSomething(); 59 | 60 | tomLonghurstRedisClientStopwatch.Stop(); 61 | var tomLonghurstRedisClientStopwatchTimeTaken = 62 | tomLonghurstRedisClientStopwatch.ElapsedMilliseconds; 63 | Console.WriteLine( 64 | $"PID {currentProcessId} -- Task {taskId} -- Time Taken: {tomLonghurstRedisClientStopwatchTimeTaken} ms"); 65 | _lastActive[taskId] = DateTime.Now; 66 | } 67 | } 68 | catch (Exception e) 69 | { 70 | Console.WriteLine($"Exception occured on task {taskId}"); 71 | Console.WriteLine(e); 72 | throw; 73 | } 74 | }); 75 | 76 | tasks.Add(task); 77 | } 78 | 79 | await Task.WhenAll(tasks); 80 | 81 | foreach (var key in _lastActive.Keys) 82 | { 83 | var dateTime = _lastActive[key]; 84 | if (DateTime.Now - dateTime > TimeSpan.FromSeconds(10)) 85 | { 86 | Console.WriteLine($"Task {key} was last active at {dateTime.ToLongTimeString()} - {(DateTime.Now - dateTime).TotalMilliseconds} ms ago"); 87 | } 88 | } 89 | 90 | Console.WriteLine($"Finished at {DateTime.Now.ToLongTimeString()}"); 91 | Console.WriteLine("Press any key to exit."); 92 | Console.Read(); 93 | } 94 | 95 | static async Task DoSomething() 96 | { 97 | if (Random.Next(0, 2) != 0) 98 | { 99 | await TomLonghurstRedisClient.StringGetAsync(TestData.PickRandom().Key); 100 | } 101 | else 102 | { 103 | var (key, value) = TestData.PickRandom(); 104 | 105 | await TomLonghurstRedisClient.StringSetAsync(key, value, 120); 106 | } 107 | } 108 | 109 | private static readonly Random Random = new(); 110 | internal static string CreateString(int stringLength) 111 | { 112 | const string allowedChars = "ABCDEFGHJKLMNOPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz0123456789!@$?_-"; 113 | char[] chars = new char[stringLength]; 114 | 115 | for (int i = 0; i < stringLength; i++) 116 | { 117 | chars[i] = allowedChars[Random.Next(0, allowedChars.Length)]; 118 | } 119 | 120 | return new string(chars); 121 | } 122 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Pipes/SocketPipe.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.IO.Pipelines; 3 | using System.Net.Sockets; 4 | 5 | namespace TomLonghurst.AsyncRedisClient.Pipes; 6 | 7 | public class SocketPipe : IDuplexPipe 8 | { 9 | public static SocketPipe GetDuplexPipe(Socket? socket, PipeOptions sendPipeOptions, 10 | PipeOptions receivePipeOptions) => 11 | new(socket, sendPipeOptions, receivePipeOptions, true, true); 12 | 13 | private readonly Socket? _innerSocket; 14 | 15 | private readonly Pipe? _readPipe; 16 | private readonly Pipe? _writePipe; 17 | 18 | public void Reset() 19 | { 20 | _writePipe?.Reset(); 21 | _readPipe?.Reset(); 22 | } 23 | 24 | private SocketPipe(Socket? socket, PipeOptions? sendPipeOptions, PipeOptions? receivePipeOptions, bool read, 25 | bool write) 26 | { 27 | ArgumentNullException.ThrowIfNull(socket); 28 | 29 | sendPipeOptions ??= PipeOptions.Default; 30 | 31 | receivePipeOptions ??= PipeOptions.Default; 32 | 33 | _innerSocket = socket; 34 | 35 | if (!(read || write)) 36 | { 37 | throw new ArgumentException("At least one of read/write must be set"); 38 | } 39 | 40 | if (read) 41 | { 42 | _readPipe = new Pipe(receivePipeOptions); 43 | 44 | receivePipeOptions.ReaderScheduler.Schedule(o => _ = CopyFromSocketToReadPipe(), null); 45 | } 46 | 47 | if (write) 48 | { 49 | _writePipe = new Pipe(sendPipeOptions); 50 | 51 | sendPipeOptions.WriterScheduler.Schedule(o => _ = CopyFromWritePipeToSocket(), null); 52 | } 53 | } 54 | 55 | public PipeWriter Output => 56 | _writePipe?.Writer ?? throw new InvalidOperationException("Cannot write to this pipe"); 57 | 58 | public PipeReader Input => 59 | _readPipe?.Reader ?? throw new InvalidOperationException("Cannot read from this pipe"); 60 | 61 | private async Task CopyFromSocketToReadPipe() 62 | { 63 | Exception? exception = null; 64 | var writer = _readPipe!.Writer; 65 | 66 | try 67 | { 68 | while (true) 69 | { 70 | try 71 | { 72 | var memory = writer.GetMemory(512); 73 | 74 | var bytesRead = await _innerSocket!.ReceiveAsync(memory, SocketFlags.None); 75 | 76 | if (bytesRead == 0) 77 | { 78 | break; 79 | } 80 | 81 | writer.Advance(bytesRead); 82 | 83 | var result = await writer.FlushAsync(); 84 | 85 | if (result.IsCompleted || result.IsCanceled) 86 | { 87 | break; 88 | } 89 | } 90 | catch (IOException) 91 | { 92 | // TODO Why does this occur? 93 | // "Unable to read data from the transport connection: The I/O operation has been aborted because of either a thread exit or an application request." 94 | } 95 | } 96 | } 97 | catch (Exception e) 98 | { 99 | exception = e; 100 | } 101 | 102 | await writer.CompleteAsync(exception); 103 | } 104 | 105 | private async Task CopyFromWritePipeToSocket() 106 | { 107 | Exception? exception = null; 108 | var reader = _writePipe!.Reader; 109 | 110 | try 111 | { 112 | while (true) 113 | { 114 | var pendingReadResult = reader.ReadAsync(); 115 | 116 | var readResult = await pendingReadResult; 117 | 118 | do 119 | { 120 | if (!readResult.Buffer.IsEmpty) 121 | { 122 | if (readResult.Buffer.IsSingleSegment) 123 | { 124 | var writeTask = WriteSingle(readResult.Buffer); 125 | if (!writeTask.IsCompleted) 126 | { 127 | await writeTask; 128 | } 129 | } 130 | else 131 | { 132 | var writeTask = WriteMultiple(readResult.Buffer); 133 | if (!writeTask.IsCompleted) 134 | { 135 | await writeTask; 136 | } 137 | } 138 | } 139 | 140 | reader.AdvanceTo(readResult.Buffer.End); 141 | 142 | } while (!(readResult.Buffer.IsEmpty && readResult.IsCompleted) 143 | && reader.TryRead(out readResult)); 144 | 145 | if ((readResult.IsCompleted || readResult.IsCanceled) && readResult.Buffer.IsEmpty) 146 | { 147 | break; 148 | } 149 | } 150 | } 151 | catch (Exception e) 152 | { 153 | exception = e; 154 | } 155 | finally 156 | { 157 | await reader.CompleteAsync(exception); 158 | } 159 | } 160 | 161 | private Task WriteSingle(in ReadOnlySequence buffer) 162 | { 163 | var valueTask = _innerSocket!.SendAsync(buffer.First, SocketFlags.None); 164 | return valueTask.IsCompletedSuccessfully ? Task.CompletedTask : valueTask.AsTask(); 165 | } 166 | 167 | private async Task WriteMultiple(ReadOnlySequence buffer) 168 | { 169 | foreach (var segment in buffer) 170 | { 171 | await _innerSocket!.SendAsync(segment, SocketFlags.None); 172 | } 173 | } 174 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | 332 | RedisClientTest/TestInformation.cs -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Extensions/BufferExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.IO.Pipelines; 3 | using System.Runtime.InteropServices; 4 | using System.Text; 5 | using TomLonghurst.AsyncRedisClient.Constants; 6 | using TomLonghurst.AsyncRedisClient.Exceptions; 7 | 8 | namespace TomLonghurst.AsyncRedisClient.Extensions; 9 | 10 | public static class BufferExtensions 11 | { 12 | internal static string AsString(this Memory buffer) 13 | { 14 | return buffer.Span.AsString(); 15 | } 16 | 17 | internal static T? ItemAt(this ReadOnlySequence buffer, int index) 18 | { 19 | if (buffer.IsEmpty || index > buffer.Length) 20 | { 21 | return default; 22 | } 23 | 24 | return buffer.Slice(buffer.GetPosition(index, buffer.Start)).First.Span[0]; 25 | } 26 | 27 | internal static string AsStringWithoutLineTerminators(this in ReadOnlySequence buffer) 28 | { 29 | // Reslice but removing the line terminators 30 | return buffer.GetEndOfLinePosition() == null ? buffer.AsString() : buffer.Slice(buffer.Start, buffer.Length - 2).AsString(); 31 | } 32 | 33 | internal static string AsString(this in ReadOnlySequence buffer) 34 | { 35 | if (buffer.IsEmpty) 36 | { 37 | return string.Empty; 38 | } 39 | 40 | if (buffer.IsSingleSegment) 41 | { 42 | return buffer.First.Span.AsString(); 43 | } 44 | 45 | var arr = ArrayPool.Shared.Rent(checked((int) buffer.Length)); 46 | var span = new Span(arr, 0, (int) buffer.Length); 47 | buffer.CopyTo(span); 48 | var s = span.AsString(); 49 | ArrayPool.Shared.Return(arr); 50 | return s; 51 | } 52 | 53 | internal static string AsString(this in Span span) 54 | { 55 | return ((ReadOnlySpan) span).AsString(); 56 | } 57 | 58 | internal static string AsString(this in ReadOnlySpan span) 59 | { 60 | if (span.IsEmpty) 61 | { 62 | return string.Empty; 63 | } 64 | 65 | return Encoding.UTF8.GetString(span); 66 | } 67 | 68 | internal static async ValueTask AdvanceToLineTerminator(this PipeReader pipeReader, 69 | ReadResult readResult, CancellationToken cancellationToken) 70 | { 71 | SequencePosition? endOfLinePosition; 72 | while ((endOfLinePosition = readResult.Buffer.GetEndOfLinePosition()) == null) 73 | { 74 | if (readResult is { IsCompleted: true, Buffer.IsEmpty: true }) 75 | { 76 | throw new RedisDataException( 77 | "ReadResult is completed and buffer is empty. Can't find EOL in AdvanceToLineTerminator"); 78 | } 79 | 80 | if (readResult.IsCanceled) 81 | { 82 | throw new RedisDataException("ReadResult is cancelled. Can't find EOL in AdvanceToLineTerminator"); 83 | } 84 | 85 | pipeReader.AdvanceTo(readResult.Buffer.End); 86 | 87 | readResult = await pipeReader.ReadAsyncOrThrowReadTimeout(cancellationToken); 88 | } 89 | 90 | pipeReader.AdvanceTo(endOfLinePosition.Value); 91 | 92 | return readResult; 93 | } 94 | 95 | internal static async ValueTask ReadUntilEndOfLineFound(this PipeReader pipeReader, ReadResult readResult, CancellationToken cancellationToken) 96 | { 97 | while (readResult.Buffer.GetEndOfLinePosition() == null) 98 | { 99 | if (readResult is { IsCompleted: true, Buffer.IsEmpty: true }) 100 | { 101 | break; 102 | } 103 | 104 | if (readResult.IsCanceled) 105 | { 106 | break; 107 | } 108 | 109 | // We don't want to consume it yet - So don't advance past the start 110 | // But do tell it we've examined up until the end - But it's not enough and we need more 111 | // We need to call advance before calling another read though 112 | pipeReader.AdvanceTo(readResult.Buffer.Start, readResult.Buffer.End); 113 | 114 | readResult = await pipeReader.ReadAsyncOrThrowReadTimeout(cancellationToken); 115 | } 116 | 117 | if (readResult.Buffer.GetEndOfLinePosition() == null) 118 | { 119 | throw new RedisDataException("No EOL found while executing ReadUntilEndOfLineFound"); 120 | } 121 | 122 | return readResult; 123 | } 124 | 125 | internal static SequencePosition? GetEndOfLinePosition(this in ReadOnlySequence buffer) 126 | { 127 | if (buffer.IsEmpty) 128 | { 129 | throw new RedisDataException("The buffer is empty in GetEndOfLinePosition"); 130 | } 131 | 132 | var sequencePosition = buffer.PositionOf(ByteConstants.NewLine); 133 | 134 | if (sequencePosition == null) 135 | { 136 | return null; 137 | } 138 | 139 | return buffer.GetPosition(1, sequencePosition.Value); 140 | } 141 | 142 | internal static SequencePosition? GetEndOfLinePosition2(this in ReadOnlySequence buffer) 143 | { 144 | var position = buffer.Start; 145 | var previous = position; 146 | var index = -1; 147 | 148 | while (buffer.TryGet(ref position, out var segment)) 149 | { 150 | var span = segment.Span; 151 | 152 | // Look for \r in the current segment 153 | index = span.IndexOf(ByteConstants.BackslashR); 154 | 155 | if (index != -1) 156 | { 157 | // Check next segment for \n 158 | if (index + 1 >= span.Length) 159 | { 160 | var next = position; 161 | 162 | if (!buffer.TryGet(ref next, out var nextSegment)) 163 | { 164 | // We're at the end of the sequence 165 | return null; 166 | } 167 | 168 | if (nextSegment.Span[0] == ByteConstants.NewLine) 169 | { 170 | // We found a match 171 | break; 172 | } 173 | } 174 | // Check the current segment of \n 175 | else if (span[index + 1] == ByteConstants.NewLine) 176 | { 177 | // Found it 178 | break; 179 | } 180 | } 181 | 182 | previous = position; 183 | } 184 | 185 | if (index != -1) 186 | { 187 | // +2 to advance two positions past \r 188 | return buffer.GetPosition(index + 2, previous); 189 | } 190 | 191 | return null; 192 | } 193 | 194 | internal static ArraySegment GetArraySegment(this Memory buffer) => GetArraySegment((ReadOnlyMemory)buffer); 195 | 196 | internal static ArraySegment GetArraySegment(this ReadOnlyMemory buffer) 197 | { 198 | if (!MemoryMarshal.TryGetArray(buffer, out var segment)) 199 | { 200 | throw new InvalidOperationException("MemoryMarshal.TryGetArray could not provide an array"); 201 | } 202 | 203 | return segment; 204 | } 205 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Constants/Commands.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable InconsistentNaming 2 | // ReSharper disable IdentifierTypo 3 | 4 | using TomLonghurst.AsyncRedisClient.Extensions; 5 | 6 | namespace TomLonghurst.AsyncRedisClient.Constants; 7 | 8 | internal static class Commands 9 | { 10 | internal static readonly byte[] Quit = Map("QUIT"); 11 | internal static readonly byte[] Auth = Map("AUTH"); 12 | internal static readonly byte[] Exists = Map("EXISTS"); 13 | internal static readonly byte[] Del = Map("DEL"); 14 | internal static readonly byte[] Type = Map("TYPE"); 15 | internal static readonly byte[] Keys = Map("KEYS"); 16 | internal static readonly byte[] RandomKey = Map("RANDOMKEY"); 17 | internal static readonly byte[] Rename = Map("RENAME"); 18 | internal static readonly byte[] RenameNx = Map("RENAMENX"); 19 | internal static readonly byte[] PExpire = Map("PEXPIRE"); 20 | internal static readonly byte[] PExpireAt = Map("PEXPIREAT"); 21 | internal static readonly byte[] DbSize = Map("DBSIZE"); 22 | internal static readonly byte[] Expire = Map("EXPIRE"); 23 | internal static readonly byte[] ExpireAt = Map("EXPIREAT"); 24 | internal static readonly byte[] Ttl = Map("TTL"); 25 | internal static readonly byte[] PTtl = Map("PTTL"); 26 | internal static readonly byte[] Select = Map("SELECT"); 27 | internal static readonly byte[] FlushDb = Map("FLUSHDB"); 28 | internal static readonly byte[] FlushAll = Map("FLUSHALL"); 29 | internal static readonly byte[] Ping = Map("PING"); 30 | internal static readonly byte[] Echo = Map("ECHO"); 31 | 32 | internal static readonly byte[] Save = Map("SAVE"); 33 | internal static readonly byte[] BgSave = Map("BGSAVE"); 34 | internal static readonly byte[] LastSave = Map("LASTSAVE"); 35 | internal static readonly byte[] Shutdown = Map("SHUTDOWN"); 36 | internal static readonly byte[] NoSave = Map("NOSAVE"); 37 | internal static readonly byte[] BgRewriteAof = Map("BGREWRITEAOF"); 38 | 39 | internal static readonly byte[] Info = Map("INFO"); 40 | internal static readonly byte[] SlaveOf = Map("SLAVEOF"); 41 | internal static readonly byte[] No = Map("NO"); 42 | internal static readonly byte[] One = Map("ONE"); 43 | internal static readonly byte[] ResetStat = Map("RESETSTAT"); 44 | internal static readonly byte[] Rewrite = Map("REWRITE"); 45 | internal static readonly byte[] Time = Map("TIME"); 46 | internal static readonly byte[] Segfault = Map("SEGFAULT"); 47 | internal static readonly byte[] Sleep = Map("SLEEP"); 48 | internal static readonly byte[] Dump = Map("DUMP"); 49 | internal static readonly byte[] Restore = Map("RESTORE"); 50 | internal static readonly byte[] Migrate = Map("MIGRATE"); 51 | internal static readonly byte[] Move = Map("MOVE"); 52 | internal static readonly byte[] Object = Map("OBJECT"); 53 | internal static readonly byte[] IdleTime = Map("IDLETIME"); 54 | internal static readonly byte[] Monitor = Map("MONITOR"); //missing 55 | internal static readonly byte[] Debug = Map("DEBUG"); //missing 56 | internal static readonly byte[] Config = Map("CONFIG"); //missing 57 | internal static readonly byte[] Client = Map("CLIENT"); 58 | internal static readonly byte[] List = Map("LIST"); 59 | internal static readonly byte[] Kill = Map("KILL"); 60 | internal static readonly byte[] Addr = Map("ADDR"); 61 | internal static readonly byte[] Id = Map("ID"); 62 | internal static readonly byte[] SkipMe = Map("SKIPME"); 63 | internal static readonly byte[] SetName = Map("SETNAME"); 64 | internal static readonly byte[] GetName = Map("GETNAME"); 65 | internal static readonly byte[] Pause = Map("PAUSE"); 66 | internal static readonly byte[] Role = Map("ROLE"); 67 | 68 | internal static readonly byte[] StrLen = Map("STRLEN"); 69 | internal static readonly byte[] Set = Map("SET"); 70 | internal static readonly byte[] Get = Map("GET"); 71 | internal static readonly byte[] GetSet = Map("GETSET"); 72 | internal static readonly byte[] MGet = Map("MGET"); 73 | internal static readonly byte[] SetNx = Map("SETNX"); 74 | internal static readonly byte[] SetEx = Map("SETEX"); 75 | internal static readonly byte[] Persist = Map("PERSIST"); 76 | internal static readonly byte[] PSetEx = Map("PSETEX"); 77 | internal static readonly byte[] MSet = Map("MSET"); 78 | internal static readonly byte[] MSetNx = Map("MSETNX"); 79 | internal static readonly byte[] Incr = Map("INCR"); 80 | internal static readonly byte[] IncrBy = Map("INCRBY"); 81 | internal static readonly byte[] IncrByFloat = Map("INCRBYFLOAT"); 82 | internal static readonly byte[] Decr = Map("DECR"); 83 | internal static readonly byte[] DecrBy = Map("DECRBY"); 84 | internal static readonly byte[] Append = Map("APPEND"); 85 | internal static readonly byte[] GetRange = Map("GETRANGE"); 86 | internal static readonly byte[] SetRange = Map("SETRANGE"); 87 | internal static readonly byte[] GetBit = Map("GETBIT"); 88 | internal static readonly byte[] SetBit = Map("SETBIT"); 89 | internal static readonly byte[] BitCount = Map("BITCOUNT"); 90 | 91 | internal static readonly byte[] Scan = Map("SCAN"); 92 | internal static readonly byte[] SScan = Map("SSCAN"); 93 | internal static readonly byte[] HScan = Map("HSCAN"); 94 | internal static readonly byte[] ZScan = Map("ZSCAN"); 95 | internal static readonly byte[] Match = Map("MATCH"); 96 | internal static readonly byte[] Count = Map("COUNT"); 97 | 98 | internal static readonly byte[] HSet = Map("HSET"); 99 | internal static readonly byte[] HSetNx = Map("HSETNX"); 100 | internal static readonly byte[] HGet = Map("HGET"); 101 | internal static readonly byte[] HMSet = Map("HMSET"); 102 | internal static readonly byte[] HMGet = Map("HMGET"); 103 | internal static readonly byte[] HIncrBy = Map("HINCRBY"); 104 | internal static readonly byte[] HIncrByFloat = Map("HINCRBYFLOAT"); 105 | internal static readonly byte[] HExists = Map("HEXISTS"); 106 | internal static readonly byte[] HDel = Map("HDEL"); 107 | internal static readonly byte[] HLen = Map("HLEN"); 108 | internal static readonly byte[] HKeys = Map("HKEYS"); 109 | internal static readonly byte[] HVals = Map("HVALS"); 110 | internal static readonly byte[] HGetAll = Map("HGETALL"); 111 | 112 | internal static readonly byte[] Sort = Map("SORT"); 113 | 114 | internal static readonly byte[] Watch = Map("WATCH"); 115 | internal static readonly byte[] UnWatch = Map("UNWATCH"); 116 | internal static readonly byte[] Multi = Map("MULTI"); 117 | internal static readonly byte[] Exec = Map("EXEC"); 118 | internal static readonly byte[] Discard = Map("DISCARD"); 119 | 120 | internal static readonly byte[] Subscribe = Map("SUBSCRIBE"); 121 | internal static readonly byte[] UnSubscribe = Map("UNSUBSCRIBE"); 122 | internal static readonly byte[] PSubscribe = Map("PSUBSCRIBE"); 123 | internal static readonly byte[] PUnSubscribe = Map("PUNSUBSCRIBE"); 124 | internal static readonly byte[] Publish = Map("PUBLISH"); 125 | 126 | internal static readonly byte[] ClusterInfo = Map("CLUSTER INFO"); 127 | 128 | internal static readonly byte[] ScriptFlush = Map("SCRIPT FLUSH"); 129 | 130 | internal static readonly IEnumerable ScriptLoad = 131 | [ 132 | Map("SCRIPT"), 133 | Map("LOAD") 134 | ]; 135 | 136 | internal static readonly byte[] Script = Map("SCRIPT"); 137 | internal static readonly byte[] Load = Map("LOAD"); 138 | 139 | internal static readonly byte[] Eval = Map("EVAL"); 140 | internal static readonly byte[] EvalSha = Map("EVALSHA"); 141 | 142 | private static byte[] Map(ReadOnlySpan input) 143 | { 144 | return input.ToUtf8BytesWithTerminator(); 145 | } 146 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Client/RedisClient.ReadWrite.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.IO.Pipelines; 3 | using System.Net.Sockets; 4 | using System.Runtime.CompilerServices; 5 | using System.Text; 6 | using TomLonghurst.AsyncRedisClient.Exceptions; 7 | using TomLonghurst.AsyncRedisClient.Extensions; 8 | using TomLonghurst.AsyncRedisClient.Helpers; 9 | using TomLonghurst.AsyncRedisClient.Models.Backlog; 10 | using TomLonghurst.AsyncRedisClient.Models.ResultProcessors; 11 | using TomLonghurst.AsyncRedisClient.Pipes; 12 | #if !NETSTANDARD2_0 13 | #endif 14 | 15 | namespace TomLonghurst.AsyncRedisClient.Client; 16 | 17 | public partial class RedisClient 18 | { 19 | private static readonly Logger Log = new(); 20 | 21 | private readonly SemaphoreSlim _sendAndReceiveSemaphoreSlim = new(1, 1); 22 | 23 | private long _outStandingOperations; 24 | 25 | public long OutstandingOperations => Interlocked.Read(ref _outStandingOperations); 26 | 27 | private long _operationsPerformed; 28 | 29 | private SocketPipe? _socketPipe; 30 | private PipeReader? _pipeReader; 31 | private PipeWriter? _pipeWriter; 32 | 33 | public long OperationsPerformed => Interlocked.Read(ref _operationsPerformed); 34 | 35 | public DateTime LastUsed { get; internal set; } 36 | 37 | private Func? _telemetryCallback; 38 | private int _written; 39 | private bool _isBusy; 40 | 41 | // TODO Make public 42 | private void SetTelemetryCallback(Func? telemetryCallback) 43 | { 44 | _telemetryCallback = telemetryCallback; 45 | } 46 | 47 | 48 | internal ValueTask SendOrQueueAsync(byte[] command, 49 | AbstractResultProcessor abstractResultProcessor, 50 | CancellationToken cancellationToken, 51 | bool isReconnectionAttempt = false) 52 | { 53 | LastUsed = DateTime.Now; 54 | 55 | cancellationToken.ThrowIfCancellationRequested(); 56 | 57 | Interlocked.Increment(ref _outStandingOperations); 58 | 59 | if (isReconnectionAttempt || _isBusy) 60 | { 61 | return SendAndReceiveAsync(command, abstractResultProcessor, cancellationToken, isReconnectionAttempt); 62 | } 63 | 64 | return QueueToBacklog(command, abstractResultProcessor, cancellationToken); 65 | } 66 | 67 | private ValueTask QueueToBacklog(byte[] command, AbstractResultProcessor abstractResultProcessor, 68 | CancellationToken cancellationToken) 69 | { 70 | var taskCompletionSource = new TaskCompletionSource(); 71 | 72 | _backlog.Enqueue(new BacklogItem(command, cancellationToken, taskCompletionSource, abstractResultProcessor, this, _pipeReader!)); 73 | 74 | cancellationToken.Register(() => taskCompletionSource.TrySetCanceled(cancellationToken)); 75 | 76 | return new ValueTask(taskCompletionSource.Task); 77 | } 78 | 79 | internal async ValueTask SendAndReceiveAsync(byte[] command, AbstractResultProcessor abstractResultProcessor, 80 | CancellationToken cancellationToken, bool isReconnectionAttempt) 81 | { 82 | _isBusy = true; 83 | 84 | Interlocked.Increment(ref _operationsPerformed); 85 | 86 | if (!isReconnectionAttempt) 87 | { 88 | await _sendAndReceiveSemaphoreSlim.WaitAsync(cancellationToken); 89 | } 90 | 91 | try 92 | { 93 | if (!isReconnectionAttempt && !IsConnected) 94 | { 95 | await TryConnectAsync(cancellationToken); 96 | } 97 | 98 | await Write(command); 99 | 100 | return await abstractResultProcessor.Start(this, _pipeReader!, new ReadResult(), cancellationToken); 101 | } 102 | catch (Exception innerException) 103 | { 104 | if (innerException.IsSameOrSubclassOf(typeof(RedisRecoverableException)) || 105 | innerException.IsSameOrSubclassOf(typeof(OperationCanceledException))) 106 | { 107 | throw; 108 | } 109 | 110 | DisposeNetwork(); 111 | 112 | if (innerException.IsSameOrSubclassOf(typeof(RedisNonRecoverableException))) 113 | { 114 | throw; 115 | } 116 | 117 | throw new RedisConnectionException(innerException); 118 | } 119 | finally 120 | { 121 | Interlocked.Decrement(ref _outStandingOperations); 122 | if (!isReconnectionAttempt) 123 | { 124 | _sendAndReceiveSemaphoreSlim.Release(); 125 | } 126 | 127 | _isBusy = false; 128 | } 129 | } 130 | 131 | internal ValueTask Write(byte[] command) 132 | { 133 | _written++; 134 | 135 | LastCommand = command; 136 | 137 | #if DEBUG 138 | Console.WriteLine($"Executing Command: {Encoding.UTF8.GetString(command)}"); 139 | #endif 140 | 141 | return _pipeWriter!.WriteAsync(command); 142 | } 143 | 144 | internal async ValueTask RunWithTimeout(Func> action, 145 | CancellationToken originalCancellationToken) 146 | { 147 | originalCancellationToken.ThrowIfCancellationRequested(); 148 | 149 | using var cancellationTokenWithTimeout = 150 | CancellationTokenHelper.CancellationTokenWithTimeout(ClientConfig.Timeout, 151 | originalCancellationToken); 152 | 153 | try 154 | { 155 | return await action.Invoke(cancellationTokenWithTimeout.Token); 156 | } 157 | catch (OperationCanceledException operationCanceledException) 158 | { 159 | throw WaitTimeoutOrCancelledException(operationCanceledException, originalCancellationToken); 160 | } 161 | catch (SocketException socketException) 162 | { 163 | if (socketException.InnerException?.IsSameOrSubclassOf(typeof(OperationCanceledException)) == true) 164 | { 165 | throw WaitTimeoutOrCancelledException(socketException.InnerException, originalCancellationToken); 166 | } 167 | 168 | throw; 169 | } 170 | } 171 | 172 | private void InvokeTelemetryCallback(Stopwatch stopwatch) 173 | { 174 | stopwatch.Stop(); 175 | _telemetryCallback?.Invoke(new RedisTelemetryResult("TODO", stopwatch.Elapsed)); 176 | } 177 | 178 | 179 | [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] 180 | private async ValueTask RunWithTimeout(Func action, 181 | CancellationToken originalCancellationToken) 182 | { 183 | originalCancellationToken.ThrowIfCancellationRequested(); 184 | 185 | using var cts = CancellationTokenSource.CreateLinkedTokenSource(originalCancellationToken); 186 | 187 | cts.CancelAfter(ClientConfig.Timeout); 188 | 189 | try 190 | { 191 | await action.Invoke(cts.Token); 192 | } 193 | catch (OperationCanceledException operationCanceledException) 194 | { 195 | throw WaitTimeoutOrCancelledException(operationCanceledException, originalCancellationToken); 196 | } 197 | catch (SocketException socketException) 198 | { 199 | if (socketException.InnerException?.IsSameOrSubclassOf(typeof(OperationCanceledException)) == 200 | true) 201 | { 202 | throw WaitTimeoutOrCancelledException(socketException.InnerException, originalCancellationToken); 203 | } 204 | 205 | throw; 206 | } 207 | } 208 | 209 | private Exception WaitTimeoutOrCancelledException(Exception exception, CancellationToken originalCancellationToken) 210 | { 211 | return originalCancellationToken.IsCancellationRequested ? exception : new RedisWaitTimeoutException(this); 212 | } 213 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Models/ResultProcessors/AbstractResultProcessor.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.IO.Pipelines; 3 | using System.Text; 4 | using TomLonghurst.AsyncRedisClient.Client; 5 | using TomLonghurst.AsyncRedisClient.Constants; 6 | using TomLonghurst.AsyncRedisClient.Exceptions; 7 | using TomLonghurst.AsyncRedisClient.Extensions; 8 | using TomLonghurst.AsyncRedisClient.Helpers; 9 | 10 | namespace TomLonghurst.AsyncRedisClient.Models.ResultProcessors; 11 | 12 | public abstract class AbstractResultProcessor 13 | { 14 | } 15 | 16 | public abstract class AbstractResultProcessor : AbstractResultProcessor 17 | { 18 | internal async ValueTask Start( 19 | RedisClient redisClient, 20 | PipeReader pipeReader, 21 | ReadResult readResult, 22 | CancellationToken cancellationToken 23 | ) 24 | { 25 | return await Process(redisClient, pipeReader, readResult, cancellationToken); 26 | } 27 | 28 | internal abstract ValueTask Process( 29 | RedisClient redisClient, 30 | PipeReader pipeReader, 31 | ReadResult readResult, 32 | CancellationToken cancellationToken 33 | ); 34 | 35 | 36 | protected async ValueTask> ReadData( 37 | RedisClient redisClient, 38 | PipeReader pipeReader, 39 | ReadResult readResult, 40 | CancellationToken cancellationToken 41 | ) 42 | { 43 | var line = await ReadLine(pipeReader, cancellationToken); 44 | 45 | if (line.IsEmpty) 46 | { 47 | throw new RedisDataException("Empty buffer at start of ReadData"); 48 | } 49 | 50 | var firstChar = line.ItemAt(0); 51 | 52 | if (firstChar != ByteConstants.Dollar) 53 | { 54 | var stringLine = line.AsStringWithoutLineTerminators(); 55 | pipeReader.AdvanceTo(line.End); 56 | 57 | if (firstChar == ByteConstants.Dash) 58 | { 59 | throw new RedisFailedCommandException(stringLine, redisClient.LastCommand); 60 | } 61 | 62 | throw new UnexpectedRedisResponseException($"Unexpected reply: {stringLine}"); 63 | } 64 | 65 | var alreadyReadToLineTerminator = false; 66 | 67 | var byteSizeOfData = SpanNumberParser.Parse(line); 68 | 69 | pipeReader.AdvanceTo(line.End); 70 | 71 | if (byteSizeOfData == -1) 72 | { 73 | return null; 74 | } 75 | 76 | if (readResult is { IsCompleted: true, Buffer.IsEmpty: true }) 77 | { 78 | throw new RedisDataException("ReadResult is completed and buffer is empty starting ReadData"); 79 | } 80 | 81 | readResult = await pipeReader.ReadAtLeastAsync(byteSizeOfData, cancellationToken); 82 | 83 | var buffer = readResult.Buffer; 84 | 85 | if (byteSizeOfData == 0) 86 | { 87 | throw new UnexpectedRedisResponseException("Invalid length"); 88 | } 89 | 90 | var dataByteStorage = new byte[byteSizeOfData].AsMemory(); 91 | 92 | buffer = buffer.Slice(buffer.Start, Math.Min(byteSizeOfData, buffer.Length)); 93 | 94 | var bytesReceived = buffer.Length; 95 | 96 | buffer.CopyTo(dataByteStorage[..(int) bytesReceived].Span); 97 | 98 | if (bytesReceived >= byteSizeOfData) 99 | { 100 | alreadyReadToLineTerminator = TryAdvanceToLineTerminator(ref buffer, readResult, pipeReader); 101 | } 102 | else 103 | { 104 | pipeReader.AdvanceTo(buffer.End); 105 | } 106 | 107 | while (bytesReceived < byteSizeOfData) 108 | { 109 | if (readResult is { IsCompleted: true, Buffer.IsEmpty: true }) 110 | { 111 | throw new RedisDataException( 112 | "ReadResult is completed and buffer is empty reading in loop in ReadData"); 113 | } 114 | 115 | if (readResult.IsCanceled) 116 | { 117 | throw new RedisDataException("ReadResult is cancelled reading in loop in ReadData"); 118 | } 119 | 120 | readResult = await pipeReader.ReadAsyncOrThrowReadTimeout(cancellationToken); 121 | 122 | buffer = readResult.Buffer.Slice(readResult.Buffer.Start, 123 | Math.Min(readResult.Buffer.Length, byteSizeOfData - bytesReceived)); 124 | 125 | buffer 126 | .CopyTo(dataByteStorage.Slice((int) bytesReceived, 127 | (int) Math.Min(buffer.Length, byteSizeOfData - bytesReceived)).Span); 128 | 129 | bytesReceived += buffer.Length; 130 | 131 | if (bytesReceived >= byteSizeOfData) 132 | { 133 | alreadyReadToLineTerminator = TryAdvanceToLineTerminator(ref buffer, readResult, pipeReader); 134 | } 135 | else 136 | { 137 | pipeReader.AdvanceTo(buffer.End); 138 | } 139 | } 140 | 141 | if (!alreadyReadToLineTerminator) 142 | { 143 | readResult = await pipeReader.ReadAsyncOrThrowReadTimeout(cancellationToken); 144 | 145 | await pipeReader.AdvanceToLineTerminator(readResult, cancellationToken); 146 | } 147 | 148 | return dataByteStorage; 149 | } 150 | 151 | private bool TryAdvanceToLineTerminator(ref ReadOnlySequence buffer, ReadResult readResult, PipeReader pipeReader) 152 | { 153 | var slicedBytes = readResult.Buffer.Slice(buffer.End); 154 | if (slicedBytes.IsEmpty) 155 | { 156 | pipeReader.AdvanceTo(buffer.End); 157 | return false; 158 | } 159 | 160 | var endOfLinePosition = slicedBytes.GetEndOfLinePosition(); 161 | if (endOfLinePosition == null) 162 | { 163 | pipeReader.AdvanceTo(buffer.End); 164 | return false; 165 | } 166 | 167 | pipeReader.AdvanceTo(endOfLinePosition.Value); 168 | return true; 169 | } 170 | 171 | protected async ValueTask ReadByte(PipeReader pipeReader, CancellationToken cancellationToken) 172 | { 173 | var readResult = await pipeReader.ReadAsyncOrThrowReadTimeout(cancellationToken); 174 | 175 | if (readResult.Buffer.IsEmpty) 176 | { 177 | throw new RedisDataException("Empty buffer in ReadByte"); 178 | } 179 | 180 | return readResult.Buffer.Slice(readResult.Buffer.Start, 1).First.Span[0]; 181 | } 182 | 183 | protected async ValueTask> ReadLine( 184 | PipeReader pipeReader, 185 | CancellationToken cancellationToken 186 | ) 187 | { 188 | var readResult = await pipeReader.ReadAsyncOrThrowReadTimeout(cancellationToken); 189 | 190 | var endOfLinePosition = readResult.Buffer.GetEndOfLinePosition(); 191 | if (endOfLinePosition != null) 192 | { 193 | return readResult.Buffer.Slice(readResult.Buffer.Start, endOfLinePosition.Value); 194 | } 195 | 196 | if (readResult is { IsCompleted: true, Buffer.IsEmpty: true }) 197 | { 198 | throw new RedisDataException("Read is completed and buffer is empty - Can't find a complete line in ReadLine'"); 199 | } 200 | 201 | return await ReadLineAsync(pipeReader, readResult, cancellationToken); 202 | } 203 | 204 | private async ValueTask> ReadLineAsync( 205 | PipeReader pipeReader, 206 | ReadResult readResult, 207 | CancellationToken cancellationToken 208 | ) 209 | { 210 | var endOfLinePosition = readResult.Buffer.GetEndOfLinePosition(); 211 | if (endOfLinePosition == null) 212 | { 213 | readResult = await pipeReader.ReadUntilEndOfLineFound(readResult, cancellationToken); 214 | 215 | endOfLinePosition = readResult.Buffer.GetEndOfLinePosition(); 216 | } 217 | 218 | if (endOfLinePosition == null) 219 | { 220 | throw new RedisDataException("Can't find EOL in ReadLine"); 221 | } 222 | 223 | var buffer = readResult.Buffer; 224 | 225 | return buffer.Slice(buffer.Start, endOfLinePosition.Value); 226 | } 227 | } -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Client/RedisClient.Connect.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Pipelines; 2 | using System.Net; 3 | using System.Net.Security; 4 | using System.Net.Sockets; 5 | using System.Runtime.CompilerServices; 6 | using System.Security; 7 | using TomLonghurst.AsyncRedisClient.Exceptions; 8 | using TomLonghurst.AsyncRedisClient.Models; 9 | using TomLonghurst.AsyncRedisClient.Pipes; 10 | 11 | namespace TomLonghurst.AsyncRedisClient.Client; 12 | 13 | public partial class RedisClient : IDisposable 14 | { 15 | private static long _idCounter; 16 | public long ClientId { get; } = Interlocked.Increment(ref _idCounter); 17 | 18 | private long _reconnectAttempts; 19 | 20 | public long ReconnectAttempts => Interlocked.Read(ref _reconnectAttempts); 21 | 22 | private readonly SemaphoreSlim _connectSemaphoreSlim = new(1, 1); 23 | 24 | public RedisClientConfig ClientConfig { get; } 25 | 26 | private RedisSocket? _socket; 27 | 28 | public Socket? Socket => _socket; 29 | 30 | private SslStream? _sslStream; 31 | 32 | private bool _isConnected; 33 | 34 | internal Func? OnConnectionEstablished { get; set; } 35 | internal Func? OnConnectionFailed { get; set; } 36 | 37 | public bool IsConnected 38 | { 39 | [MethodImpl(MethodImplOptions.Synchronized)] 40 | get 41 | { 42 | if (_socket == null || _socket.IsDisposed || !_socket.Connected || _socket.IsClosed) 43 | { 44 | _isConnected = false; 45 | } 46 | 47 | return _isConnected; 48 | } 49 | 50 | [MethodImpl(MethodImplOptions.Synchronized)] 51 | private set 52 | { 53 | _isConnected = value; 54 | 55 | if (!value) 56 | { 57 | if (OnConnectionFailed != null) 58 | { 59 | Task.Run(() => OnConnectionFailed.Invoke(this)); 60 | } 61 | } 62 | else 63 | { 64 | if (OnConnectionEstablished != null) 65 | { 66 | Task.Run(() => OnConnectionEstablished.Invoke(this)); 67 | } 68 | } 69 | } 70 | } 71 | 72 | protected RedisClient(RedisClientConfig redisClientConfig) 73 | { 74 | ClientConfig = redisClientConfig ?? throw new ArgumentNullException(nameof(redisClientConfig)); 75 | 76 | Cluster = new ClusterCommands(this); 77 | Server = new ServerCommands(this); 78 | Scripts = new ScriptCommands(this); 79 | 80 | StartBacklogProcessor(); 81 | 82 | _connectionChecker = new Timer(CheckConnection, null, 2500, 250); 83 | } 84 | 85 | ~RedisClient() 86 | { 87 | Dispose(); 88 | } 89 | 90 | private void CheckConnection(object? state) 91 | { 92 | if (!IsConnected) 93 | { 94 | Task.Run(() => TryConnectAsync(CancellationToken.None)); 95 | } 96 | } 97 | 98 | internal static Task ConnectAsync(RedisClientConfig redisClientConfig) 99 | { 100 | return ConnectAsync(redisClientConfig, CancellationToken.None); 101 | } 102 | 103 | internal static async Task ConnectAsync(RedisClientConfig redisClientConfig, CancellationToken cancellationToken) 104 | { 105 | var redisClient = new RedisClient(redisClientConfig); 106 | await redisClient.TryConnectAsync(cancellationToken); 107 | return redisClient; 108 | } 109 | 110 | private async Task TryConnectAsync(CancellationToken cancellationToken) 111 | { 112 | if (IsConnected) 113 | { 114 | return; 115 | } 116 | 117 | try 118 | { 119 | await RunWithTimeout(async token => 120 | { 121 | await ConnectAsync(token); 122 | }, cancellationToken); 123 | } 124 | catch (Exception innerException) 125 | { 126 | DisposeNetwork(); 127 | throw new RedisConnectionException(innerException); 128 | } 129 | } 130 | 131 | private async Task ConnectAsync(CancellationToken cancellationToken) 132 | { 133 | if (IsConnected) 134 | { 135 | return; 136 | } 137 | 138 | await _connectSemaphoreSlim.WaitAsync(cancellationToken); 139 | 140 | if (IsConnected) 141 | { 142 | _connectSemaphoreSlim.Release(); 143 | return; 144 | } 145 | 146 | try 147 | { 148 | Interlocked.Increment(ref _reconnectAttempts); 149 | 150 | _socket = new RedisSocket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) 151 | { 152 | SendTimeout = ClientConfig.Timeout.Milliseconds, 153 | ReceiveTimeout = ClientConfig.Timeout.Milliseconds 154 | }; 155 | 156 | OptimiseSocket(); 157 | 158 | if (IPAddress.TryParse(ClientConfig.Host, out var ip)) 159 | { 160 | await _socket.ConnectAsync(ip, ClientConfig.Port, cancellationToken); 161 | } 162 | else 163 | { 164 | var addresses = await Dns.GetHostAddressesAsync(ClientConfig.Host, cancellationToken); 165 | await _socket.ConnectAsync( 166 | addresses.First(a => a.AddressFamily == AddressFamily.InterNetwork), 167 | ClientConfig.Port, cancellationToken); 168 | } 169 | 170 | 171 | if (!_socket.Connected) 172 | { 173 | Log.Debug("Socket Connect failed"); 174 | 175 | DisposeNetwork(); 176 | return; 177 | } 178 | 179 | Log.Debug("Socket Connected"); 180 | 181 | Stream networkStream = new NetworkStream(_socket); 182 | 183 | var redisPipeOptions = GetPipeOptions(); 184 | 185 | if (ClientConfig.Ssl) 186 | { 187 | _sslStream = new SslStream(networkStream, 188 | false, 189 | ClientConfig.CertificateValidationCallback, 190 | ClientConfig.CertificateSelectionCallback, 191 | EncryptionPolicy.RequireEncryption); 192 | 193 | // TODO 194 | // await _sslStream.AuthenticateAsClientAsync(ClientConfig.Host); 195 | 196 | if (!_sslStream.IsEncrypted) 197 | { 198 | DisposeNetwork(); 199 | throw new SecurityException($"Could not establish an encrypted connection to Redis - {ClientConfig.Host}"); 200 | } 201 | 202 | _pipeWriter = PipeWriter.Create(_sslStream, new StreamPipeWriterOptions(leaveOpen: true)); 203 | _pipeReader = PipeReader.Create(_sslStream, new StreamPipeReaderOptions(leaveOpen: true)); 204 | } 205 | else 206 | { 207 | _socketPipe = SocketPipe.GetDuplexPipe(_socket, redisPipeOptions.SendOptions ?? new PipeOptions(), redisPipeOptions.ReceiveOptions ?? new PipeOptions()); 208 | _pipeWriter = _socketPipe.Output; 209 | _pipeReader = _socketPipe.Input; 210 | } 211 | 212 | if (!string.IsNullOrEmpty(ClientConfig.Password)) 213 | { 214 | await Authorize(cancellationToken); 215 | } 216 | 217 | if (ClientConfig.Db != 0) 218 | { 219 | await SelectDb(cancellationToken); 220 | } 221 | 222 | if (ClientConfig.ClientName != null) 223 | { 224 | await SetClientNameAsync(cancellationToken); 225 | } 226 | 227 | IsConnected = true; 228 | } 229 | finally 230 | { 231 | _connectSemaphoreSlim.Release(); 232 | } 233 | } 234 | 235 | private void OptimiseSocket() 236 | { 237 | if (_socket!.AddressFamily == AddressFamily.Unix) 238 | { 239 | return; 240 | } 241 | 242 | try 243 | { 244 | _socket.NoDelay = true; 245 | } 246 | catch 247 | { 248 | // If we can't set this, just continue - There's nothing we can do! 249 | } 250 | } 251 | 252 | private readonly Timer? _connectionChecker; 253 | private bool _disposed; 254 | 255 | private static RedisPipeOptions GetPipeOptions() 256 | { 257 | const int defaultMinimumSegmentSize = 4 * 16; 258 | 259 | const long sendPauseWriterThreshold = 512 * 1024; 260 | const long sendResumeWriterThreshold = sendPauseWriterThreshold / 2; 261 | 262 | const long receivePauseWriterThreshold = 1024 * 1024 * 1024; 263 | const long receiveResumeWriterThreshold = receivePauseWriterThreshold / 2; 264 | 265 | var scheduler = PipeScheduler.ThreadPool; 266 | var defaultPipeOptions = PipeOptions.Default; 267 | 268 | var receivePipeOptions = new PipeOptions( 269 | defaultPipeOptions.Pool, 270 | scheduler, 271 | scheduler, 272 | receivePauseWriterThreshold, 273 | receiveResumeWriterThreshold, 274 | defaultMinimumSegmentSize, 275 | false); 276 | 277 | var sendPipeOptions = new PipeOptions( 278 | defaultPipeOptions.Pool, 279 | scheduler, 280 | scheduler, 281 | sendPauseWriterThreshold, 282 | sendResumeWriterThreshold, 283 | defaultMinimumSegmentSize, 284 | false); 285 | 286 | return new RedisPipeOptions 287 | { 288 | SendOptions = sendPipeOptions, 289 | ReceiveOptions = receivePipeOptions 290 | }; 291 | } 292 | 293 | public void Dispose() 294 | { 295 | _disposed = true; 296 | DisposeNetwork(); 297 | _connectSemaphoreSlim?.Dispose(); 298 | _sendAndReceiveSemaphoreSlim?.Dispose(); 299 | _backlog?.Dispose(); 300 | _connectionChecker?.Dispose(); 301 | } 302 | 303 | private void DisposeNetwork() 304 | { 305 | IsConnected = false; 306 | _pipeReader?.CompleteAsync(); 307 | _pipeWriter?.CompleteAsync(); 308 | _socket?.Close(); 309 | _socket?.Dispose(); 310 | _sslStream?.Dispose(); 311 | } 312 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /TomLonghurst.AsyncRedisClient/Client/RedisClient.Commands.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using TomLonghurst.AsyncRedisClient.Constants; 3 | using TomLonghurst.AsyncRedisClient.Models; 4 | using TomLonghurst.AsyncRedisClient.Models.Commands; 5 | using TomLonghurst.AsyncRedisClient.Models.RequestModels; 6 | 7 | namespace TomLonghurst.AsyncRedisClient.Client; 8 | 9 | public partial class RedisClient : IDisposable 10 | { 11 | internal byte[]? LastCommand; 12 | 13 | private async ValueTask Authorize(CancellationToken cancellationToken) 14 | { 15 | await RunWithTimeout(async token => 16 | { 17 | var command = RedisCommand.From(Commands.Auth, ClientConfig.Password!); 18 | await SendOrQueueAsync(command, SuccessResultProcessor, CancellationToken.None, true); 19 | }, cancellationToken); 20 | } 21 | 22 | private async ValueTask SelectDb(CancellationToken cancellationToken) 23 | { 24 | await RunWithTimeout(async token => 25 | { 26 | var command = RedisCommand.From(Commands.Select, ClientConfig.Db); 27 | await SendOrQueueAsync(command, SuccessResultProcessor, CancellationToken.None, true); 28 | }, cancellationToken); 29 | } 30 | 31 | public ValueTask Ping() 32 | { 33 | return Ping(CancellationToken.None); 34 | } 35 | 36 | public async ValueTask Ping(CancellationToken cancellationToken) 37 | { 38 | return await RunWithTimeout(async token => 39 | { 40 | var pingCommand = Commands.Ping; 41 | 42 | var sw = Stopwatch.StartNew(); 43 | var pingResponse = await SendOrQueueAsync(pingCommand, SimpleStringResultProcessor, CancellationToken.None); 44 | sw.Stop(); 45 | 46 | return new Pong(sw.Elapsed, pingResponse); 47 | }, cancellationToken); 48 | } 49 | 50 | public ValueTask KeyExistsAsync(string key) 51 | { 52 | return KeyExistsAsync(key, CancellationToken.None); 53 | } 54 | 55 | public async ValueTask KeyExistsAsync(string key, 56 | CancellationToken cancellationToken) 57 | { 58 | return await RunWithTimeout(async token => 59 | { 60 | var command = RedisCommand.From(Commands.Exists, key); 61 | return await SendOrQueueAsync(command, IntegerResultProcessor, token); 62 | }, cancellationToken) == 1; 63 | } 64 | 65 | public ValueTask StringGetAsync(string key) 66 | { 67 | return StringGetAsync(key, CancellationToken.None); 68 | } 69 | 70 | public async ValueTask StringGetAsync(string key, 71 | CancellationToken cancellationToken) 72 | { 73 | return new StringRedisValue(await RunWithTimeout(async token => 74 | { 75 | var command = RedisCommand.From(Commands.Get, key); 76 | return await SendOrQueueAsync(command, DataResultProcessor, token); 77 | }, cancellationToken)); 78 | } 79 | 80 | public ValueTask> StringGetAsync(IEnumerable keys) 81 | { 82 | return StringGetAsync(keys, CancellationToken.None); 83 | } 84 | 85 | public async ValueTask> StringGetAsync(IEnumerable keys, 86 | CancellationToken cancellationToken) 87 | { 88 | return await RunWithTimeout(async token => 89 | { 90 | var command = RedisCommand.From(Commands.MGet, keys.ToArray()); 91 | 92 | return await SendOrQueueAsync(command, ArrayResultProcessor, token); 93 | }, cancellationToken); 94 | } 95 | 96 | public ValueTask StringSetAsync(string key, string value, int timeToLiveInSeconds) 97 | { 98 | return StringSetAsync(key, value, timeToLiveInSeconds, CancellationToken.None); 99 | } 100 | 101 | public async ValueTask StringSetAsync(string key, string value, int timeToLiveInSeconds, 102 | CancellationToken cancellationToken) 103 | { 104 | await RunWithTimeout(async token => 105 | { 106 | var command = RedisCommand.From(Commands.SetEx, key, 107 | timeToLiveInSeconds, value); 108 | 109 | await SendOrQueueAsync(command, SuccessResultProcessor, token); 110 | }, cancellationToken); 111 | } 112 | 113 | public ValueTask StringSetAsync(string key, string value) 114 | { 115 | return StringSetAsync(key, value, CancellationToken.None); 116 | } 117 | 118 | public async ValueTask StringSetAsync(string key, string value, CancellationToken cancellationToken) 119 | { 120 | var command = RedisCommand.From(Commands.Set, key, value); 121 | await SendOrQueueAsync(command, SuccessResultProcessor, cancellationToken); 122 | } 123 | 124 | public ValueTask StringSetAsync(IEnumerable keyValuePairs) 125 | { 126 | return StringSetAsync(keyValuePairs, CancellationToken.None); 127 | } 128 | 129 | public async ValueTask StringSetAsync(IEnumerable keyValuePairs, 130 | CancellationToken cancellationToken) 131 | { 132 | await RunWithTimeout(async token => 133 | { 134 | var command = RedisCommand.From(Commands.MSet, keyValuePairs.SelectMany(x => [x.Key, x.Value]).ToArray()); 135 | await SendOrQueueAsync(command, SuccessResultProcessor, token); 136 | 137 | }, cancellationToken); 138 | } 139 | 140 | public ValueTask StringSetAsync(IEnumerable keyValuePairs, 141 | int timeToLiveInSeconds) 142 | { 143 | return StringSetAsync(keyValuePairs, timeToLiveInSeconds, CancellationToken.None); 144 | } 145 | 146 | public async ValueTask StringSetAsync(IEnumerable keyValuePairs, 147 | int timeToLiveInSeconds, 148 | CancellationToken cancellationToken) 149 | { 150 | await RunWithTimeout(async token => 151 | { 152 | var redisKeyValues = keyValuePairs.ToList(); 153 | 154 | var keys = redisKeyValues.Select(value => value.Key).ToList(); 155 | var arguments = new List { timeToLiveInSeconds.ToString() }.Concat(redisKeyValues.Select(value => value.Value)); 156 | 157 | if (Scripts.MultiSetexScript == null) 158 | { 159 | Scripts.MultiSetexScript = await Scripts.LoadScript( 160 | "for i=1, #KEYS do " + 161 | "redis.call(\"SETEX\", KEYS[i], ARGV[1], ARGV[i+1]); " + 162 | "end", 163 | cancellationToken); 164 | } 165 | 166 | await Scripts.MultiSetexScript.ExecuteAsync(keys, arguments, cancellationToken); 167 | }, cancellationToken); 168 | } 169 | 170 | public ValueTask DeleteKeyAsync(string key) 171 | { 172 | return DeleteKeyAsync(key, CancellationToken.None); 173 | } 174 | 175 | public ValueTask DeleteKeyAsync(string key, 176 | CancellationToken cancellationToken) 177 | { 178 | return DeleteKeyAsync([key], cancellationToken); 179 | } 180 | 181 | public ValueTask DeleteKeyAsync(IEnumerable keys) 182 | { 183 | return DeleteKeyAsync(keys, CancellationToken.None); 184 | } 185 | 186 | public async ValueTask DeleteKeyAsync(IEnumerable keys, 187 | CancellationToken cancellationToken) 188 | { 189 | await RunWithTimeout(async token => 190 | { 191 | var command = RedisCommand.From(Commands.Del, keys.ToArray()); 192 | await SendOrQueueAsync(command, IntegerResultProcessor, token); 193 | }, cancellationToken); 194 | } 195 | 196 | private async ValueTask SetClientNameAsync(CancellationToken cancellationToken) 197 | { 198 | await RunWithTimeout(async token => 199 | { 200 | var command = RedisCommand.From(Commands.Client, Commands.SetName, ClientConfig.ClientName!); 201 | await SendOrQueueAsync(command, SuccessResultProcessor, token, true); 202 | }, cancellationToken); 203 | } 204 | 205 | public ValueTask IncrementAsync(string key) 206 | { 207 | return IncrementAsync(key, CancellationToken.None); 208 | } 209 | 210 | public async ValueTask IncrementAsync(string key, CancellationToken cancellationToken) 211 | { 212 | return await RunWithTimeout(async token => 213 | { 214 | var command = RedisCommand.From(Commands.Incr, key); 215 | return await SendOrQueueAsync(command, IntegerResultProcessor, token); 216 | }, cancellationToken); 217 | } 218 | 219 | public ValueTask IncrementByAsync(string key, int amount) 220 | { 221 | return IncrementByAsync(key, amount, CancellationToken.None); 222 | } 223 | 224 | public async ValueTask IncrementByAsync(string key, int amount, CancellationToken cancellationToken) 225 | { 226 | return await RunWithTimeout(async token => 227 | { 228 | var command = RedisCommand.From(Commands.IncrBy, key, amount); 229 | return await SendOrQueueAsync(command, IntegerResultProcessor, token); 230 | }, cancellationToken); 231 | } 232 | 233 | public ValueTask IncrementByAsync(string key, float amount) 234 | { 235 | return IncrementByAsync(key, amount, CancellationToken.None); 236 | } 237 | 238 | public async ValueTask IncrementByAsync(string key, float amount, CancellationToken cancellationToken) 239 | { 240 | return await RunWithTimeout(async token => 241 | { 242 | var command = RedisCommand.From(Commands.IncrByFloat, key, amount); 243 | return await SendOrQueueAsync(command, FloatResultProcessor, token); 244 | }, cancellationToken); 245 | } 246 | 247 | public ValueTask DecrementAsync(string key) 248 | { 249 | return DecrementAsync(key, CancellationToken.None); 250 | } 251 | 252 | public async ValueTask DecrementAsync(string key, CancellationToken cancellationToken) 253 | { 254 | return await RunWithTimeout(async token => 255 | { 256 | var command = RedisCommand.From(Commands.Decr, key); 257 | return await SendOrQueueAsync(command, IntegerResultProcessor, token); 258 | }, cancellationToken); 259 | } 260 | 261 | public ValueTask DecrementByAsync(string key, int amount) 262 | { 263 | return DecrementByAsync(key, amount, CancellationToken.None); 264 | } 265 | 266 | public async ValueTask DecrementByAsync(string key, int amount, CancellationToken cancellationToken) 267 | { 268 | return await RunWithTimeout(async token => 269 | { 270 | var command = RedisCommand.From(Commands.DecrBy, key, amount); 271 | return await SendOrQueueAsync(command, IntegerResultProcessor, token); 272 | }, cancellationToken); 273 | } 274 | 275 | public ValueTask ExpireAsync(string key, int seconds) 276 | { 277 | return ExpireAsync(key, seconds, CancellationToken.None); 278 | } 279 | 280 | public async ValueTask ExpireAsync(string key, int seconds, CancellationToken cancellationToken) 281 | { 282 | await RunWithTimeout(async token => 283 | { 284 | var command = RedisCommand.From(Commands.Expire, key, seconds); 285 | return await SendOrQueueAsync(command, IntegerResultProcessor, token); 286 | }, cancellationToken); 287 | } 288 | 289 | public ValueTask ExpireAsync(IEnumerable keys, 290 | int timeToLiveInSeconds) 291 | { 292 | return ExpireAsync(keys, timeToLiveInSeconds, new CancellationToken()); 293 | } 294 | 295 | public async ValueTask ExpireAsync(IEnumerable keys, 296 | int timeToLiveInSeconds, 297 | CancellationToken cancellationToken) 298 | { 299 | await RunWithTimeout(async token => 300 | { 301 | var arguments = new List { timeToLiveInSeconds.ToString() }; 302 | 303 | if (Scripts.MultiExpireScript == null) 304 | { 305 | Scripts.MultiExpireScript = await Scripts.LoadScript( 306 | "for i, name in ipairs(KEYS) do redis.call(\"EXPIRE\", name, ARGV[1]); end", 307 | cancellationToken); 308 | } 309 | 310 | await Scripts.MultiExpireScript.ExecuteAsync(keys, arguments, cancellationToken); 311 | }, cancellationToken); 312 | } 313 | 314 | public ValueTask ExpireAtAsync(string key, DateTimeOffset dateTime) 315 | { 316 | return ExpireAtAsync(key, dateTime, CancellationToken.None); 317 | } 318 | 319 | public async ValueTask ExpireAtAsync(string key, DateTimeOffset dateTime, CancellationToken cancellationToken) 320 | { 321 | await RunWithTimeout(async token => 322 | { 323 | var command = RedisCommand.From(Commands.ExpireAt, key, dateTime.ToUnixTimeSeconds()); 324 | return await SendOrQueueAsync(command, IntegerResultProcessor, token); 325 | }, cancellationToken); 326 | } 327 | 328 | public ValueTask PersistAsync(string key) 329 | { 330 | return PersistAsync(key, CancellationToken.None); 331 | } 332 | 333 | public async ValueTask PersistAsync(string key, CancellationToken cancellationToken) 334 | { 335 | await RunWithTimeout(async token => 336 | { 337 | var command = RedisCommand.From(Commands.Persist, key); 338 | return await SendOrQueueAsync(command, IntegerResultProcessor, token); 339 | }, cancellationToken); 340 | } 341 | 342 | public ValueTask TimeToLiveAsync(string key) 343 | { 344 | return TimeToLiveAsync(key, CancellationToken.None); 345 | } 346 | 347 | public async ValueTask TimeToLiveAsync(string key, CancellationToken cancellationToken) 348 | { 349 | return await RunWithTimeout(async token => 350 | { 351 | var command = RedisCommand.From(Commands.Ttl, key); 352 | return await SendOrQueueAsync(command, IntegerResultProcessor, token); 353 | }, cancellationToken); 354 | } 355 | } -------------------------------------------------------------------------------- /RedisClientTest/IntegrationTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Runtime.Versioning; 8 | using System.Threading.Tasks; 9 | using StackExchange.Redis; 10 | using TomLonghurst.AsyncRedisClient.Client; 11 | using TomLonghurst.AsyncRedisClient.Models.RequestModels; 12 | using Assembly = System.Reflection.Assembly; 13 | 14 | namespace RedisClientTest; 15 | 16 | public class Tests : TestBase 17 | { 18 | private RedisClientManager _redisManager; 19 | private RedisClientConfig _config; 20 | 21 | public RedisClient Client => _redisManager.GetRedisClient(); 22 | 23 | [Before(Test)] 24 | public async Task Setup() 25 | { 26 | _config = new RedisClientConfig(Host, Port) 27 | { 28 | Ssl = false 29 | }; 30 | 31 | _redisManager = await RedisClientManager.ConnectAsync(_config); 32 | } 33 | 34 | [Arguments("value with a space")] 35 | [Arguments("value with two spaces")] 36 | [Arguments("value")] 37 | [Arguments("value with a\nnew line")] 38 | [Arguments("value with a\r\nnew line")] 39 | [Repeat(2)] 40 | [Test] 41 | public async Task Values(string value) 42 | { 43 | await Client.StringSetAsync("key", value); 44 | var redisValue = await Client.StringGetAsync("key"); 45 | 46 | await Assert.That(redisValue.Value).IsEqualTo(value); 47 | } 48 | 49 | [Test] 50 | [Repeat(2)] 51 | public async Task Multiple_Values_With_Space() 52 | { 53 | var data = new List 54 | { 55 | new("key1", "value with a space1"), 56 | new("key2", "value with a space2"), 57 | new("key3", "value with a space3") 58 | }; 59 | 60 | await Client.StringSetAsync(data); 61 | 62 | var redisValue1 = await Client.StringGetAsync("key1"); 63 | await Assert.That(redisValue1.Value).IsEqualTo("value with a space1"); 64 | 65 | var redisValue2 = await Client.StringGetAsync("key2"); 66 | await Assert.That(redisValue2.Value).IsEqualTo("value with a space2"); 67 | 68 | var redisValue3 = await Client.StringGetAsync("key3"); 69 | await Assert.That(redisValue3.Value).IsEqualTo("value with a space3"); 70 | } 71 | 72 | public async Task Time(string title, Func action) 73 | { 74 | var sw = Stopwatch.StartNew(); 75 | await action.Invoke(); 76 | sw.Stop(); 77 | Console.WriteLine($"{title} - Time Taken: {sw.ElapsedMilliseconds} ms"); 78 | } 79 | 80 | [Test] 81 | [Repeat(2)] 82 | [Arguments("LargeValue", "large_json.json")] 83 | [Arguments("LargeValue2", "large_json2.json")] 84 | public async Task LargeValue(string key, string filename) 85 | { 86 | var largeValueJson = await File.ReadAllTextAsync(filename); 87 | await Client.StringSetAsync(key, largeValueJson); 88 | var result = await Client.StringGetAsync(key); 89 | await Client.ExpireAsync(key, 120); 90 | 91 | await Assert.That(largeValueJson).IsEqualTo(result.Value); 92 | } 93 | 94 | [Skip("")] 95 | [Test] 96 | public async Task MultipleThreads() 97 | { 98 | var tasks = new List(); 99 | 100 | for (var i = 0; i < 1000; i++) 101 | { 102 | tasks.Add(Task.Factory.StartNew(async () => await LargeValue($"MultiThread{i}", "large_json.json")).Unwrap()); 103 | } 104 | 105 | await Task.WhenAll(tasks); 106 | } 107 | 108 | //[Test] 109 | public async Task MemoryTest() 110 | { 111 | string ver = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.FrameworkName; 112 | 113 | var largeJsonContents = await File.ReadAllTextAsync("large_json.json"); 114 | var sw = Stopwatch.StartNew(); 115 | var tasks = new List(); 116 | 117 | for (int i = 0; i < 50; i++) 118 | { 119 | var i1 = i; 120 | var task = Task.Run(async () => 121 | { 122 | while (sw.Elapsed < TimeSpan.FromMinutes(1)) 123 | { 124 | try 125 | { 126 | var client = _redisManager.GetRedisClient(); 127 | 128 | await Time("Set", async delegate 129 | { 130 | await client.StringSetAsync($"MemoryTestKey{i1}", largeJsonContents, 120); 131 | }); 132 | 133 | await Time("Get", async delegate 134 | { 135 | var result = await client.StringGetAsync($"MemoryTestKey{i1}"); 136 | await Assert.That(result.Value).IsEqualTo(largeJsonContents); 137 | }); 138 | 139 | await Time("Delete", async delegate 140 | { 141 | await client.DeleteKeyAsync($"MultiTestKey{i1}"); 142 | }); 143 | } 144 | catch (Exception e) 145 | { 146 | Console.WriteLine(e); 147 | throw; 148 | } 149 | } 150 | }); 151 | 152 | tasks.Add(task); 153 | } 154 | 155 | await Task.WhenAll(tasks); 156 | Console.WriteLine("Finished."); 157 | } 158 | 159 | public enum TestClient 160 | { 161 | StackExchange, 162 | TomLonghurst 163 | } 164 | 165 | [Skip("")] 166 | [Arguments(TestClient.StackExchange)] 167 | [Arguments(TestClient.TomLonghurst)] 168 | [Test] 169 | public async Task PerformanceTest(TestClient testClient) 170 | { 171 | var tasks = new List(); 172 | 173 | for (var taskCount = 0; taskCount < 5; taskCount++) 174 | { 175 | var task = Task.Run(async () => 176 | { 177 | if (testClient == TestClient.StackExchange) 178 | { 179 | var stackExchange = (await ConnectionMultiplexer.ConnectAsync(new ConfigurationOptions 180 | { 181 | EndPoints = {{Host, Port}}, 182 | Ssl = false 183 | })).GetDatabase(0); 184 | 185 | await stackExchange.StringSetAsync("SingleKey", "123", TimeSpan.FromSeconds(120)); 186 | 187 | for (var outer = 0; outer < 5; outer++) 188 | { 189 | var stackExchangeRedisClientStopwatch = Stopwatch.StartNew(); 190 | 191 | for (var i = 0; i < 200; i++) 192 | { 193 | var redisValue = await stackExchange.StringGetAsync("SingleKey"); 194 | } 195 | 196 | stackExchangeRedisClientStopwatch.Stop(); 197 | var stackExchangeRedisClientStopwatchTimeTaken = 198 | stackExchangeRedisClientStopwatch.ElapsedMilliseconds; 199 | Console.WriteLine($"Time Taken: {stackExchangeRedisClientStopwatchTimeTaken} ms"); 200 | } 201 | } 202 | else 203 | { 204 | await Client.StringSetAsync("SingleKey", 205 | "123", 206 | 120); 207 | 208 | for (var outer = 0; outer < 5; outer++) 209 | { 210 | var tomLonghurstRedisClientStopwatch = Stopwatch.StartNew(); 211 | 212 | for (var i = 0; i < 200; i++) 213 | { 214 | var redisValue = await Client.StringGetAsync("SingleKey"); 215 | } 216 | 217 | tomLonghurstRedisClientStopwatch.Stop(); 218 | var tomLonghurstRedisClientStopwatchTimeTaken = 219 | tomLonghurstRedisClientStopwatch.ElapsedMilliseconds; 220 | Console.WriteLine($"Time Taken: {tomLonghurstRedisClientStopwatchTimeTaken} ms"); 221 | } 222 | } 223 | }); 224 | 225 | tasks.Add(task); 226 | } 227 | 228 | await Task.WhenAll(tasks); 229 | } 230 | 231 | [Arguments(TestClient.StackExchange)] 232 | [Arguments(TestClient.TomLonghurst)] 233 | [Test] 234 | public async Task PerformanceTest2(TestClient testClient) 235 | { 236 | var tasks = new List(); 237 | if (testClient == TestClient.StackExchange) 238 | { 239 | var stackExchange = (await ConnectionMultiplexer.ConnectAsync(new ConfigurationOptions 240 | { 241 | EndPoints = {{Host, Port}}, 242 | Ssl = true 243 | })).GetDatabase(0); 244 | 245 | await stackExchange.StringSetAsync("SingleKey", "123", TimeSpan.FromSeconds(120)); 246 | 247 | var stackExchangeRedisClientStopwatch = Stopwatch.StartNew(); 248 | 249 | for (var i = 0; i < 200; i++) 250 | { 251 | tasks.Add(stackExchange.StringGetAsync("SingleKey")); 252 | } 253 | 254 | await Task.WhenAll(tasks); 255 | 256 | stackExchangeRedisClientStopwatch.Stop(); 257 | var stackExchangeRedisClientStopwatchTimeTaken = 258 | stackExchangeRedisClientStopwatch.ElapsedMilliseconds; 259 | Console.WriteLine($"Time Taken: {stackExchangeRedisClientStopwatchTimeTaken} ms"); 260 | } 261 | else 262 | { 263 | var client = Client; 264 | await client.StringSetAsync("SingleKey", 265 | "123", 266 | 120); 267 | 268 | var tomLonghurstRedisClientStopwatch = Stopwatch.StartNew(); 269 | 270 | for (var i = 0; i < 200; i++) 271 | { 272 | tasks.Add(client.StringGetAsync("SingleKey").AsTask()); 273 | } 274 | 275 | await Task.WhenAll(tasks); 276 | 277 | tomLonghurstRedisClientStopwatch.Stop(); 278 | var tomLonghurstRedisClientStopwatchTimeTaken = 279 | tomLonghurstRedisClientStopwatch.ElapsedMilliseconds; 280 | Console.WriteLine($"Time Taken: {tomLonghurstRedisClientStopwatchTimeTaken} ms"); 281 | } 282 | } 283 | 284 | [Test] 285 | [Repeat(2)] 286 | public async Task Test1Async() 287 | { 288 | var sw = Stopwatch.StartNew(); 289 | 290 | var pong = await Client.Ping(); 291 | await Assert.That(pong.IsSuccessful).IsTrue(); 292 | 293 | var getDoesntExist = (await Client.StringGetAsync(["Blah1", "Blah2"])).ToList(); 294 | await Assert.That(getDoesntExist.Count).IsEqualTo(2); 295 | await Assert.That(getDoesntExist.Count(value => value.HasValue)).IsEqualTo(0); 296 | 297 | await Client.StringSetAsync("TestyMcTestFace", "123", 120); 298 | await Client.StringSetAsync("TestyMcTestFace2", "1234", 120); 299 | await Client.StringSetAsync("TestyMcTestFace3", "12345", 120); 300 | 301 | var getValue = await Client.StringGetAsync(["TestyMcTestFace", "TestyMcTestFace2", "TestyMcTestFace3" 302 | ]); 303 | await Assert.That(getValue.Count()).IsEqualTo(3); 304 | 305 | var getValueSingle = await Client.StringGetAsync("TestyMcTestFace"); 306 | await Assert.That(getValueSingle.Value).IsEqualTo("123"); 307 | 308 | 309 | await Client.StringSetAsync("KeyExists", "123"); 310 | var keyExistsFalse = await Client.KeyExistsAsync("KeyDoesntExist"); 311 | await Assert.That(keyExistsFalse).IsEqualTo(false); 312 | var keyExistsTrue = await Client.KeyExistsAsync("KeyExists"); 313 | await Assert.That(keyExistsTrue).IsEqualTo(true); 314 | 315 | var timeTaken = sw.ElapsedMilliseconds; 316 | Console.WriteLine($"Time Taken: {timeTaken} ms"); 317 | } 318 | 319 | [Test] 320 | [Repeat(2)] 321 | public async Task Ping() 322 | { 323 | var pong = await Client.Ping(); 324 | 325 | await Assert.That(pong.IsSuccessful).IsTrue(); 326 | await Assert.That(pong.Message).IsEqualTo("PONG"); 327 | 328 | Console.WriteLine($"Time Taken: {pong.TimeTaken.TotalMilliseconds} ms"); 329 | } 330 | 331 | [Test] 332 | [Repeat(2)] 333 | public async Task GetNonExistingKey() 334 | { 335 | var nonExistingKey = await Client.StringGetAsync("Blah1"); 336 | await Assert.That(nonExistingKey.HasValue).IsFalse(); 337 | } 338 | 339 | [Test] 340 | [Repeat(2)] 341 | public async Task GetNonExistingKeys() 342 | { 343 | var nonExistingKeys = (await Client.StringGetAsync(["Blah1", "Blah2"])).ToList(); 344 | await Assert.That(nonExistingKeys.Count).IsEqualTo(2); 345 | await Assert.That(nonExistingKeys.Count(value => value.HasValue)).IsEqualTo(0); 346 | } 347 | 348 | [Test] 349 | [Repeat(2)] 350 | public async Task GetExistingKeyAmongstNonExistingKeys() 351 | { 352 | await Client.StringSetAsync("Exists", "123", 30); 353 | var values = (await Client.StringGetAsync(["Blah1", "Blah2", "Exists", "Blah4", "Blah5" 354 | ])).ToList(); 355 | await Assert.That(values.Count).IsEqualTo(5); 356 | await Assert.That(values.Count(value => value.HasValue)).IsEqualTo(1); 357 | } 358 | 359 | [Test] 360 | [Repeat(2)] 361 | public async Task SetGetDeleteSingleKey() 362 | { 363 | await Client.StringSetAsync("SingleKey", "123"); 364 | var redisValue = await Client.StringGetAsync("SingleKey"); 365 | await Assert.That(redisValue.Value).IsEqualTo("123"); 366 | 367 | await Client.DeleteKeyAsync("SingleKey"); 368 | redisValue = await Client.StringGetAsync("SingleKey"); 369 | await Assert.That(redisValue.HasValue).IsEqualTo(false); 370 | } 371 | 372 | [Test] 373 | [Repeat(2)] 374 | public async Task SetGetSingleKeyWithTtl() 375 | { 376 | await Client.StringSetAsync("SingleKeyWithTtl", "123", 30); 377 | var redisValue = await Client.StringGetAsync("SingleKeyWithTtl"); 378 | await Assert.That(redisValue.Value).IsEqualTo("123"); 379 | } 380 | 381 | [Test] 382 | [Repeat(2)] 383 | public async Task SetMultipleTtl() 384 | { 385 | var client = Client; 386 | await client.StringSetAsync(new List 387 | { 388 | new("BlahTTL1", "Blah1"), 389 | new("BlahTTL2", "Blah2"), 390 | new("BlahTTL3", "Blah3"), 391 | new("BlahTTL4", "Blah4"), 392 | new("BlahTTL5", "Blah5") 393 | }, 394 | 120); 395 | 396 | var ttl = await client.TimeToLiveAsync("BlahTTL1"); 397 | await Assert.That(ttl).IsPositive().And.IsLessThanOrEqualTo(125); 398 | } 399 | 400 | [Test] 401 | [Repeat(2)] 402 | public async Task MultipleExpire() 403 | { 404 | var client = Client; 405 | await client.StringSetAsync(new List 406 | { 407 | new("BlahExpire1", "Blah1"), 408 | new("BlahExpire2", "Blah2"), 409 | new("BlahExpire3", "Blah3"), 410 | new("BlahExpire4", "Blah4"), 411 | new("BlahExpire5", "Blah5") 412 | }); 413 | 414 | await client.ExpireAsync(new List 415 | { 416 | "BlahExpire1", 417 | "BlahExpire2", 418 | "BlahExpire3", 419 | "BlahExpire4", 420 | "BlahExpire5" 421 | }, 120); 422 | 423 | var ttl = await client.TimeToLiveAsync("BlahExpire1"); 424 | await Assert.That(ttl).IsPositive().And.IsLessThanOrEqualTo(125); 425 | } 426 | 427 | [Test] 428 | [Repeat(2)] 429 | public async Task SetGetMultipleKey() 430 | { 431 | var keyValues = new List 432 | { 433 | new("MultiKey1", "1"), 434 | new("MultiKey2", "2"), 435 | new("MultiKey3", "3") 436 | }; 437 | 438 | await Client.StringSetAsync(keyValues); 439 | var redisValues = (await Client.StringGetAsync(["MultiKey1", "MultiKey2", "MultiKey3"])).ToList(); 440 | await Assert.That(redisValues.Count).IsEqualTo(3); 441 | await Assert.That(redisValues[0].Value).IsEqualTo("1"); 442 | await Assert.That(redisValues[1].Value).IsEqualTo("2"); 443 | await Assert.That(redisValues[2].Value).IsEqualTo("3"); 444 | } 445 | 446 | [Test] 447 | [Repeat(2)] 448 | public async Task Pipelining_Multiple_Sets() 449 | { 450 | var tomLonghurstRedisClient = Client; 451 | 452 | var keys = new[] 453 | { 454 | "Pipeline1", "Pipeline2", "Pipeline3", "Pipeline4", "Pipeline5", "Pipeline6", "Pipeline7", "Pipeline8" 455 | }; 456 | var results = keys.Select(key => tomLonghurstRedisClient.StringSetAsync(key, "123", 30).AsTask()); 457 | 458 | await Task.WhenAll(results); 459 | 460 | foreach (var key in keys) 461 | { 462 | var value = await Client.StringGetAsync(key); 463 | await Assert.That(value.Value).IsEqualTo("123"); 464 | } 465 | } 466 | 467 | [Test] 468 | [Repeat(2)] 469 | public async Task SetGetMultipleKeyWithTtlMultiple() 470 | { 471 | await SetGetMultipleKeyWithTtl(); 472 | await SetGetMultipleKeyWithTtl(); 473 | await SetGetMultipleKeyWithTtl(); 474 | await SetGetMultipleKeyWithTtl(); 475 | await SetGetMultipleKeyWithTtl(); 476 | } 477 | 478 | [Test] 479 | [Repeat(2)] 480 | public async Task SetGetMultipleKeyWithTtl() 481 | { 482 | var keyValues = new List 483 | { 484 | new("MultiKeyWithTtl1", "1"), 485 | new("MultiKeyWithTtl2", "2"), 486 | new("MultiKeyWithTtl3", "3") 487 | }; 488 | 489 | await Client.StringSetAsync(keyValues, 120); 490 | var redisValues = (await Client.StringGetAsync(["MultiKeyWithTtl1", "MultiKeyWithTtl2", "MultiKeyWithTtl3" 491 | ])).ToList(); 492 | await Assert.That(redisValues.Count).IsEqualTo(3); 493 | await Assert.That(redisValues[0].Value).IsEqualTo("1"); 494 | await Assert.That(redisValues[1].Value).IsEqualTo("2"); 495 | await Assert.That(redisValues[2].Value).IsEqualTo("3"); 496 | 497 | var ttl = await Client.TimeToLiveAsync("MultiKeyWithTtl1"); 498 | await Assert.That(ttl).IsLessThanOrEqualTo(120).And.IsPositive(); 499 | 500 | ttl = await Client.TimeToLiveAsync("MultiKeyWithTtl2"); 501 | await Assert.That(ttl).IsLessThanOrEqualTo(120).And.IsPositive(); 502 | 503 | ttl = await Client.TimeToLiveAsync("MultiKeyWithTtl3"); 504 | await Assert.That(ttl).IsLessThanOrEqualTo(120).And.IsPositive(); 505 | } 506 | 507 | [Test] 508 | [Repeat(2)] 509 | public async Task KeyExists() 510 | { 511 | await Client.StringSetAsync("KeyExistsCheck", "123", 30); 512 | var exists = await Client.KeyExistsAsync("KeyExistsCheck"); 513 | var doesntExist = await Client.KeyExistsAsync("KeyDoesntExistsCheck"); 514 | 515 | await Assert.That(exists).IsEqualTo(true); 516 | await Assert.That(doesntExist).IsEqualTo(false); 517 | } 518 | 519 | [Test, Skip("No Cluster Support on Redis Server")] 520 | public async Task ClusterInfo() 521 | { 522 | var response = await Client.Cluster.ClusterInfoAsync(); 523 | var firstLine = response.Split("\n").First(); 524 | await Assert.That(firstLine).IsEqualTo("cluster_state:ok"); 525 | } 526 | 527 | // Needs Access to Socket (which is private) to Close it 528 | [Test] 529 | [Repeat(2)] 530 | public async Task Disconnected() 531 | { 532 | var client = _redisManager.GetRedisClient(); 533 | await client.StringSetAsync("DisconnectTest", "123", 120); 534 | var redisValue = await client.StringGetAsync("DisconnectTest"); 535 | await Assert.That(redisValue.Value).IsEqualTo("123"); 536 | 537 | client.Socket.Close(); 538 | 539 | await Task.Delay(1000); 540 | 541 | var result = await client.StringGetAsync("DisconnectTest"); 542 | 543 | await Assert.That(result.Value).IsEqualTo("123"); 544 | } 545 | 546 | [Test] 547 | public async Task GetKey() 548 | { 549 | var result = 550 | await Client.StringGetAsync( 551 | "SummaryProduct_V3_1022864315_1724593328_COM_GBP_UK_en-GB_"); 552 | 553 | Console.Write(result); 554 | } 555 | 556 | [Test] 557 | public async Task GetKeys() 558 | { 559 | var keys = GenerateMassKeys(); 560 | 561 | var client1 = _redisManager.GetRedisClient(); 562 | var client2 = _redisManager.GetRedisClient(); 563 | var client3 = _redisManager.GetRedisClient(); 564 | 565 | var resultTask = 566 | client1.StringGetAsync(keys).AsTask(); 567 | 568 | var result2Task = 569 | client2.StringGetAsync(keys).AsTask(); 570 | 571 | var result3Task = 572 | client3.StringGetAsync(keys).AsTask(); 573 | 574 | var result = await Task.WhenAll(resultTask, result2Task, result3Task); 575 | 576 | var result1 = result[0].ToList(); 577 | var result2 = result[1].ToList(); 578 | var result3 = result[2].ToList(); 579 | 580 | Console.Write(result1); 581 | } 582 | 583 | private List GenerateMassKeys() 584 | { 585 | var list = new List(); 586 | for (var i = 0; i < 50; i++) 587 | { 588 | list.AddRange( 589 | [ 590 | "SummaryProduct_V3_1099538108_1873678337_COM_GBP_UK_en-GB_ckp5egq-11", 591 | "SummaryProduct_V3_1216417282_216646695_COM_GBP_UK_en-GB_ckp5egq-11", 592 | "SummaryProduct_V3_1471008232__COM_GBP_UK_en-GB_ckp5egq-11", 593 | "SummaryProduct_V3_1431558723__COM_GBP_UK_en-GB_ckp5egq-11", 594 | "SummaryProduct_V3_1526680692_724222009_COM_GBP_UK_en-GB_ckp5egq-11", 595 | "SummaryProduct_V3_1650151356_234415250_COM_GBP_UK_en-GB_ckp5egq-11", 596 | "SummaryProduct_V3_1650151356_778891134_COM_GBP_UK_en-GB_ckp5egq-11", 597 | "SummaryProduct_V3_1798679041_1695172977_COM_GBP_UK_en-GB_ckp5egq-11", 598 | "SummaryProduct_V3_1809834294_1582795796_COM_GBP_UK_en-GB_ckp5egq-11", 599 | "SummaryProduct_V3_183736170_969769947_COM_GBP_UK_en-GB_ckp5egq-11", 600 | "SummaryProduct_V3_1733691802_1464284012_COM_GBP_UK_en-GB_ckp5egq-11" 601 | ] 602 | ); 603 | } 604 | 605 | return list; 606 | } 607 | 608 | [Arguments("IncrKey")] 609 | [Test] 610 | [Repeat(2)] 611 | public async Task Incr(string key) 612 | { 613 | await Client.DeleteKeyAsync(key); 614 | 615 | var one = await Client.IncrementAsync(key); 616 | await Assert.That(one).IsEqualTo(1); 617 | 618 | var three = await Client.IncrementByAsync(key, 2); 619 | await Assert.That(three).IsEqualTo(3); 620 | 621 | var threeAndAHalf = await Client.IncrementByAsync(key, 0.5f); 622 | await Assert.That(threeAndAHalf).IsEqualTo(3.5f); 623 | 624 | var four = await Client.IncrementByAsync(key, 0.5f); 625 | await Assert.That(four).IsEqualTo(4f); 626 | 627 | var five = await Client.IncrementAsync(key); 628 | await Assert.That(five).IsEqualTo(5); 629 | 630 | await Client.ExpireAsync(key, 120); 631 | } 632 | 633 | [Arguments("IncrKey")] 634 | [Test] 635 | [Repeat(2)] 636 | public async Task IncrLots(string key) 637 | { 638 | var tasks = new List(); 639 | for (int i = 0; i < 100; i++) 640 | { 641 | tasks.Add(Incr($"{key}{i}")); 642 | } 643 | 644 | await Task.WhenAll(tasks); 645 | } 646 | 647 | [Test] 648 | [Repeat(2)] 649 | public async Task Info() 650 | { 651 | var info = await Client.Server.Info(); 652 | } 653 | 654 | [Test] 655 | [Repeat(2)] 656 | public async Task DBSize() 657 | { 658 | var tomLonghurstRedisClient = Client; 659 | var dbSize = Client.Server.DbSize(); 660 | } 661 | 662 | [Test] 663 | [Arguments("DecrKey")] 664 | [Repeat(2)] 665 | public async Task Decr(string key) 666 | { 667 | await Incr(key); 668 | 669 | var four = await Client.DecrementAsync(key); 670 | await Assert.That(four).IsEqualTo(4); 671 | 672 | var two = await Client.DecrementByAsync(key, 2); 673 | await Assert.That(two).IsEqualTo(2); 674 | 675 | var one = await Client.DecrementAsync(key); 676 | await Assert.That(one).IsEqualTo(1); 677 | } 678 | 679 | [Test] 680 | [Repeat(2)] 681 | public async Task Expire() 682 | { 683 | await Client.StringSetAsync("ExpireKey", "123"); 684 | var ttl = await Client.TimeToLiveAsync("ExpireKey"); 685 | await Assert.That(ttl).IsEqualTo(-1); 686 | 687 | await Client.ExpireAsync("ExpireKey", 30); 688 | ttl = await Client.TimeToLiveAsync("ExpireKey"); 689 | await Assert.That(ttl).IsLessThanOrEqualTo(30); 690 | 691 | await Client.PersistAsync("ExpireKey"); 692 | ttl = await Client.TimeToLiveAsync("ExpireKey"); 693 | await Assert.That(ttl).IsEqualTo(-1); 694 | 695 | await Client.ExpireAsync("ExpireKey", 30); 696 | ttl = await Client.TimeToLiveAsync("ExpireKey"); 697 | await Assert.That(ttl).IsLessThanOrEqualTo(30); 698 | } 699 | 700 | [Test] 701 | [Repeat(2)] 702 | public async Task ExpireAt() 703 | { 704 | await Client.StringSetAsync("ExpireKeyDateTime", "123"); 705 | await Client.ExpireAtAsync("ExpireKeyDateTime", DateTimeOffset.Now.AddSeconds(30)); 706 | var ttl = await Client.TimeToLiveAsync("ExpireKeyDateTime"); 707 | await Assert.That(ttl).IsLessThanOrEqualTo(33); 708 | } 709 | 710 | [Test] 711 | [Repeat(2)] 712 | public async Task Mix() 713 | { 714 | var tasks = new List(); 715 | 716 | for (int i = 0; i < 5; i++) 717 | { 718 | tasks.Add(Task.Run(async () => 719 | { 720 | await SetGetMultipleKey(); 721 | await Pipelining_Multiple_Sets(); 722 | await GetKeys(); 723 | await GetNonExistingKey(); 724 | await GetKeys(); 725 | await SetGetMultipleKey(); 726 | })); 727 | } 728 | 729 | await Task.WhenAll(tasks); 730 | } 731 | } --------------------------------------------------------------------------------